diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 5def952..da73f62 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,3 +1,6 @@ +// TODO: https://eslint.org/docs/latest/rules/func-names +// TODO: https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/naming-convention.md + module.exports = { env: { browser: true, diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx index 6acaa8b..4bec5af 100644 --- a/src/Components/App/App.tsx +++ b/src/Components/App/App.tsx @@ -35,6 +35,7 @@ export const App: React.FunctionComponent = (props) => { historyCurrentStep: 0 }); + // TODO: move this into a variable useEffect(() => { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index a77a01f..c3b8af5 100644 --- a/src/Components/Editor/Actions/ContainerOperations.ts +++ b/src/Components/Editor/Actions/ContainerOperations.ts @@ -90,7 +90,7 @@ export function DeleteContainer( throw new Error('[DeleteContainer] Could not find container among parent\'s children'); } - Flex(container); + ApplyBehaviorsOnSiblings(container, current.Symbols); // Select the previous container // or select the one above @@ -275,8 +275,8 @@ export function AddContainer( setHistoryCurrentStep(history.length - 1); } -function UpdateParentChildrenList(parentClone: IContainerModel | null): void { - if (parentClone === null) { +function UpdateParentChildrenList(parentClone: IContainerModel | null | undefined): void { + if (parentClone === null || parentClone === undefined) { return; } parentClone.children.sort( diff --git a/src/Components/Editor/Actions/Save.ts b/src/Components/Editor/Actions/Save.ts index 101f95c..aed7038 100644 --- a/src/Components/Editor/Actions/Save.ts +++ b/src/Components/Editor/Actions/Save.ts @@ -3,6 +3,7 @@ import { IConfiguration } from '../../../Interfaces/IConfiguration'; import { getCircularReplacer } from '../../../utils/saveload'; import { ID } from '../../SVG/SVG'; import { IEditorState } from '../../../Interfaces/IEditorState'; +import { SHOW_SELECTOR_TEXT } from '../../../utils/default'; export function SaveEditorAsJSON( history: IHistoryState[], @@ -54,6 +55,9 @@ export function SaveEditorAsSVG(): void { // remove the selector const group = svg.children[svg.children.length - 1]; group.removeChild(group.children[group.children.length - 1]); + if (SHOW_SELECTOR_TEXT) { + group.removeChild(group.children[group.children.length - 1]); + } // get svg source. const serializer = new XMLSerializer(); diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index eb0c660..928f25d 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -17,9 +17,7 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map container.properties.x; - if (!isOverlapping) { - return container; - } - - // find hole - let lastContainer: IContainerModel | null = null; - let space: number = 0; - - while (space.toFixed(2) < container.properties.width.toFixed(2)) { - // FIXME: possible infinite loop due to floating point - // FIXME: A fix was applied using toFixed(2). - // FIXME: A coverture check must be done to ensure that all scenarios are covered - - const it = reversePairwise(container.parent.children.filter(child => child !== container)); - - for (const { cur, next } of it) { - const hasSpaceBetween = next.properties.x + next.properties.width < cur.properties.x; - if (hasSpaceBetween) { - lastContainer = cur; - space = cur.properties.x - (next.properties.x + next.properties.width); - break; - } - } - - if (lastContainer === null) { - // no space between - break; - } - - const indexLastContainer = container.parent.children.indexOf(lastContainer); - for (let i = indexLastContainer; i <= container.parent.children.length - 2; i++) { - const sibling = container.parent.children[i]; - sibling.properties.x -= space; - } - } - - const hasNoSpaceBetween = lastContainer === null; - if (hasNoSpaceBetween) { - // test gap between the left of the parent and the first container - space = container.parent.children[0].properties.x; - if (space > 0) { - for (let i = 0; i <= container.parent.children.length - 2; i++) { - const sibling = container.parent.children[i]; - sibling.properties.x -= space; - } - return container; - } - } - - Flex(container); - return container; +interface IFlexibleGroup { + group: IContainerModel[] + offset: number + size: number } /** @@ -83,26 +18,37 @@ export function Flex(container: IContainerModel): void { if (container.parent === null || container.parent === undefined) { return; } - const flexibleContainers = container.parent.children + + const flexibleGroups = GetFlexibleGroups(container.parent); + + for (const flexibleGroup of flexibleGroups) { + FlexGroup(flexibleGroup); + } +} + +function FlexGroup(flexibleGroup: IFlexibleGroup): void { + const children = flexibleGroup.group; + const flexibleContainers = children .filter(sibling => sibling.properties.isFlex); const minWidths = flexibleContainers .map(sibling => sibling.properties.minWidth); - const fixedWidth = container.parent.children + const fixedWidth = children .filter(sibling => !sibling.properties.isFlex) .map(sibling => sibling.properties.width) .reduce((partialSum, a) => partialSum + a, 0); - const requiredMaxWidth = container.parent.properties.width - fixedWidth; + const requiredMaxWidth = flexibleGroup.size - fixedWidth; const minimumPossibleWidth = minWidths.reduce((partialSum, a) => partialSum + a, 0); if (minimumPossibleWidth > requiredMaxWidth) { - Swal.fire({ - icon: 'error', - title: 'Cannot fit!', - text: 'Cannot fit at all even when squeezing all flex containers to the minimum.' - }); + // Swal.fire({ + // icon: 'error', + // title: 'Cannot fit!', + // text: 'Cannot fit at all even when squeezing all flex containers to the minimum.' + // }); + console.error('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.'); return; } @@ -110,8 +56,8 @@ export function Flex(container: IContainerModel): void { if (maxMinWidths * minWidths.length < requiredMaxWidth) { const wantedWidth = requiredMaxWidth / minWidths.length; // it fits, flex with maxMinWidths and fixed width - let right = 0; - for (const sibling of container.parent.children) { + let right = flexibleGroup.offset; + for (const sibling of children) { if (!sibling.properties.isFlex) { sibling.properties.x = right; right += sibling.properties.width; @@ -132,13 +78,46 @@ export function Flex(container: IContainerModel): void { // apply the solutions for (let i = 0; i < flexibleContainers.length; i++) { - flexibleContainers[i].properties.width = ApplyWidthMargin(solutions[i], flexibleContainers[i].properties.margin.left, flexibleContainers[i].properties.margin.right) + flexibleContainers[i].properties.width = ApplyWidthMargin(solutions[i], flexibleContainers[i].properties.margin.left, flexibleContainers[i].properties.margin.right); } // move the containers - let right = 0; - for (const sibling of container.parent.children) { + let right = flexibleGroup.offset; + for (const sibling of children) { sibling.properties.x = ApplyXMargin(right, sibling.properties.margin.left); right += sibling.properties.width; } } + +export function GetFlexibleGroups(parent: IContainerModel): IFlexibleGroup[] { + const flexibleGroups: IFlexibleGroup[] = []; + let group: IContainerModel[] = []; + let offset = 0; + let size = 0; + for (const child of parent.children) { + if (child.properties.isAnchor) { + size = child.properties.x - offset; + const flexibleGroup: IFlexibleGroup = { + group, + offset, + size + }; + + flexibleGroups.push(flexibleGroup); + offset = child.properties.x + child.properties.width; + group = []; + continue; + } + + group.push(child); + } + size = parent.properties.width - offset; + const flexibleGroup: IFlexibleGroup = { + group, + offset, + size + }; + + flexibleGroups.push(flexibleGroup); + return flexibleGroups; +} diff --git a/src/Components/Editor/Behaviors/PushBehaviors.ts b/src/Components/Editor/Behaviors/PushBehaviors.ts new file mode 100644 index 0000000..895a0a1 --- /dev/null +++ b/src/Components/Editor/Behaviors/PushBehaviors.ts @@ -0,0 +1,73 @@ +import { IContainerModel } from '../../../Interfaces/IContainerModel'; +import { reversePairwise } from '../../../utils/itertools'; +import { Flex } from './FlexBehaviors'; + +/** + * Try to push the siblings + * @param container + * @returns + */ +export function PushContainers(container: IContainerModel): IContainerModel { + if (container.parent === null) { + return container; + } + + if (container.parent.children.length <= 1) { + return container; + } + + const prevIndex = container.parent.children.length - 2; + const prev: IContainerModel = container.parent.children[prevIndex]; + const isOverlapping = prev.properties.x + prev.properties.width > container.properties.x; + if (!isOverlapping) { + return container; + } + + // find hole + let lastContainer: IContainerModel | null = null; + let space: number = 0; + + while (space.toFixed(2) < container.properties.width.toFixed(2)) { + // FIXME: possible infinite loop due to floating point + // FIXME: A fix was applied using toFixed(2). + // FIXME: A coverture check must be done to ensure that all scenarios are covered + + const it = reversePairwise(container.parent.children.filter(child => child !== container)); + + for (const { cur, next } of it) { + const hasSpaceBetween = next.properties.x + next.properties.width < cur.properties.x; + if (hasSpaceBetween) { + lastContainer = cur; + space = cur.properties.x - (next.properties.x + next.properties.width); + break; + } + } + + if (lastContainer === null) { + // no space between + break; + } + + const indexLastContainer = container.parent.children.indexOf(lastContainer); + for (let i = indexLastContainer; i <= container.parent.children.length - 2; i++) { + const sibling = container.parent.children[i]; + sibling.properties.x -= space; + } + } + + const hasNoSpaceBetween = lastContainer === null; + if (hasNoSpaceBetween) { + // test gap between the left of the parent and the first container + space = container.parent.children[0].properties.x; + if (space > 0) { + for (let i = 0; i <= container.parent.children.length - 2; i++) { + const sibling = container.parent.children[i]; + sibling.properties.x -= space; + } + return container; + } + } + + Flex(container); + return container; +} diff --git a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index 5edf66c..7030186 100644 --- a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -9,7 +9,6 @@ import Swal from 'sweetalert2'; import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { ISizePointer } from '../../../Interfaces/ISizePointer'; -import { PushContainers } from './FlexBehaviors'; /** * "Transform the container into a rigid body" @@ -24,7 +23,6 @@ export function ApplyRigidBody( container: IContainerModel ): IContainerModel { container = constraintBodyInsideParent(container); - container = PushContainers(container); container = constraintBodyInsideUnallocatedWidth(container); return container; } @@ -118,7 +116,7 @@ function constraintBodyInsideSpace( export function constraintBodyInsideUnallocatedWidth( container: IContainerModel ): IContainerModel { - if (container.parent === null) { + if (container.parent === null || container.parent === undefined) { return container; } @@ -177,14 +175,14 @@ export function constraintBodyInsideUnallocatedWidth( if (availableWidth === undefined) { console.warn(`Container ${container.properties.id} cannot fit in any space due to its minimum width being to large. Consequently, its rigid body property is disabled.`); - Swal.fire({ - position: 'top-end', - title: `Container ${container.properties.id} cannot fit!`, - text: 'Its rigid body property is now disabled. Change its the minimum width or free the parent container.', - timerProgressBar: true, - showConfirmButton: false, - timer: 5000 - }); + // Swal.fire({ + // position: 'top-end', + // title: `Container ${container.properties.id} cannot fit!`, + // text: 'Its rigid body property is now disabled. Change its the minimum width or free the parent container.', + // timerProgressBar: true, + // showConfirmButton: false, + // timer: 5000 + // }); return container; } diff --git a/src/Components/Editor/Editor.scss b/src/Components/Editor/Editor.scss index cc037c8..d2792e4 100644 --- a/src/Components/Editor/Editor.scss +++ b/src/Components/Editor/Editor.scss @@ -4,21 +4,4 @@ svg { width: 100%; } -text { - font-size: 18px; - font-weight: 800; - fill: none; - fill-opacity: 0; - stroke: #000000; - stroke-width: 1px; - stroke-linecap: butt; - stroke-linejoin: miter; - stroke-opacity: 1; - transform: translateX(-50%); - transform-box: fill-box; -} -@keyframes fadein { - from { opacity: 0; } - to { opacity: 1; } -} \ No newline at end of file diff --git a/src/Components/SVG/Elements/Selector/Selector.scss b/src/Components/SVG/Elements/Selector/Selector.scss new file mode 100644 index 0000000..906cb5f --- /dev/null +++ b/src/Components/SVG/Elements/Selector/Selector.scss @@ -0,0 +1,4 @@ +@keyframes fadein { + from { opacity: 0; } + to { opacity: 1; } +} \ No newline at end of file diff --git a/src/Components/SVG/Elements/Selector.tsx b/src/Components/SVG/Elements/Selector/Selector.tsx similarity index 60% rename from src/Components/SVG/Elements/Selector.tsx rename to src/Components/SVG/Elements/Selector/Selector.tsx index 3ced2e7..d8fbef5 100644 --- a/src/Components/SVG/Elements/Selector.tsx +++ b/src/Components/SVG/Elements/Selector/Selector.tsx @@ -1,7 +1,9 @@ +import './Selector.scss'; import * as React from 'react'; -import { IContainerModel } from '../../../Interfaces/IContainerModel'; -import { getAbsolutePosition } from '../../../utils/itertools'; -import { RemoveMargin } from '../../../utils/svg'; +import { IContainerModel } from '../../../../Interfaces/IContainerModel'; +import { SHOW_SELECTOR_TEXT } from '../../../../utils/default'; +import { getAbsolutePosition } from '../../../../utils/itertools'; +import { RemoveMargin } from '../../../../utils/svg'; interface ISelectorProps { selected?: IContainerModel @@ -28,6 +30,9 @@ export const Selector: React.FC = (props) => { props.selected.properties.margin.right )); + const xText = x + width / 2; + const yText = y + height / 2; + const style: React.CSSProperties = { stroke: '#3B82F6', // tw blue-500 strokeWidth: 4, @@ -39,13 +44,23 @@ export const Selector: React.FC = (props) => { }; return ( - - + <> + + + {SHOW_SELECTOR_TEXT + ? + {props.selected.properties.displayedText} + + : null} + ); }; diff --git a/src/Components/SVG/SVG.scss b/src/Components/SVG/SVG.scss new file mode 100644 index 0000000..f575471 --- /dev/null +++ b/src/Components/SVG/SVG.scss @@ -0,0 +1,14 @@ +text { + font-family: 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 18px; + font-weight: 800; + fill: #fff; + fill-opacity: 1; + stroke: #000000; + stroke-width: 1px; + stroke-linecap: butt; + stroke-linejoin: miter; + stroke-opacity: 1; + transform: translateX(-50%); + transform-box: fill-box; +} diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index d201bee..d068ce9 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -1,8 +1,9 @@ +import './SVG.scss'; import * as React from 'react'; import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; import { ContainerModel } from '../../Interfaces/IContainerModel'; -import { Selector } from './Elements/Selector'; +import { Selector } from './Elements/Selector/Selector'; import { BAR_WIDTH } from '../Bar/Bar'; import { DepthDimensionLayer } from './Elements/DepthDimensionLayer'; import { SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default'; diff --git a/src/utils/default.ts b/src/utils/default.ts index 975b9a6..98db426 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -9,7 +9,8 @@ import { ISymbolModel } from '../Interfaces/ISymbolModel'; /// CONTAINER DEFAULTS /// -export const SHOW_TEXT = true; +export const SHOW_TEXT = false; +export const SHOW_SELECTOR_TEXT = true; export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false; export const DEFAULTCHILDTYPE_MAX_DEPTH = 10; diff --git a/src/utils/simplex.ts b/src/utils/simplex.ts index 60e9724..e696c75 100644 --- a/src/utils/simplex.ts +++ b/src/utils/simplex.ts @@ -20,17 +20,17 @@ export function Simplex(minWidths: number[], requiredMaxWidth: number): number[] { /// 1) standardized the equations // add the min widths constraints - const maximumConstraints = minWidths.map(minWidth => minWidth * -1); + const constraints = minWidths.map(minWidth => minWidth * -1); - // add the max width constraint - maximumConstraints.push(requiredMaxWidth); + // add the max widths constraint + constraints.push(requiredMaxWidth); /// 2) Create the initial matrix // get row length (nVariables + nConstraints + 1 (z) + 1 (b)) const nVariables = minWidths.length; - const nConstraints = maximumConstraints.length; + const nConstraints = constraints.length; const rowlength = nVariables + nConstraints + 2; - const matrix = GetInitialMatrix(maximumConstraints, rowlength, nVariables); + const matrix = GetInitialMatrix(constraints, rowlength, nVariables); /// Apply the algorithm const finalMatrix = ApplyMainLoop(matrix, rowlength);