diff --git a/.env.production b/.env.production index 28d2829..b2c8524 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,2 @@ -VITE_API_URL=https://localhost/SmartMenuiserieTemplate \ No newline at end of file +VITE_API_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 2def8a6..7118888 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,4 @@ *.drawio filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.dwg filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text diff --git a/README.md b/README.md index 895f9b3..a3d003d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Requierements : - NodeJS - npm - pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory) +- Chrome > 98 # Developping diff --git a/docs/Eric BF/01_141017-WG-11328-SYME-VERNUCCI-DET BF ind A.pdf b/docs/Eric BF/01_141017-WG-11328-SYME-VERNUCCI-DET BF ind A.pdf new file mode 100644 index 0000000..9b78c58 --- /dev/null +++ b/docs/Eric BF/01_141017-WG-11328-SYME-VERNUCCI-DET BF ind A.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c4d97d35d32b6201f0795bbc2b61b475222b23f93652e69f45b1f66d8165d257 +size 765789 diff --git a/docs/Eric BF/02_141017-WG-11328-SYME-VERNUCCI-DET BF ind B.pdf b/docs/Eric BF/02_141017-WG-11328-SYME-VERNUCCI-DET BF ind B.pdf new file mode 100644 index 0000000..c208e81 --- /dev/null +++ b/docs/Eric BF/02_141017-WG-11328-SYME-VERNUCCI-DET BF ind B.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5af9380fda7a9c9b416ce6e58a21fbd6737b3ed9be06b59f94991903ac667d3a +size 771067 diff --git a/docs/Eric BF/03_SYME KLINE cde BV.pdf b/docs/Eric BF/03_SYME KLINE cde BV.pdf new file mode 100644 index 0000000..3ffaf14 --- /dev/null +++ b/docs/Eric BF/03_SYME KLINE cde BV.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a4ca2dbff7444a802297e5d2a46930cca8b9a4ed801cb2315b37fe8ef54bf10 +size 2230676 diff --git a/docs/Eric BF/04_ARC KL_K1485985-ARC-K1485985.pdf b/docs/Eric BF/04_ARC KL_K1485985-ARC-K1485985.pdf new file mode 100644 index 0000000..f737334 --- /dev/null +++ b/docs/Eric BF/04_ARC KL_K1485985-ARC-K1485985.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b064e88d02dfcb1c74ea776b3ca9b3c70595caaba05b76c7d3bc7fbdd609b92 +size 970682 diff --git a/docs/Eric BF/05_DT_K1485985.pdf b/docs/Eric BF/05_DT_K1485985.pdf new file mode 100644 index 0000000..b209be1 --- /dev/null +++ b/docs/Eric BF/05_DT_K1485985.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85c0158092cec03ca86c826671e3cb44899e218ac268ac84919fbc69ae65fb55 +size 2577327 diff --git a/docs/Eric BF/06_ Photo IMG_1406.jpg b/docs/Eric BF/06_ Photo IMG_1406.jpg new file mode 100644 index 0000000..203c6f3 --- /dev/null +++ b/docs/Eric BF/06_ Photo IMG_1406.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50ee7ef27161cac6159573fee35ea664048f0bb01756d47cc65b5d378bcd5a56 +size 2283743 diff --git a/docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.dwg b/docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.dwg new file mode 100644 index 0000000..3b9ea9e --- /dev/null +++ b/docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.dwg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85252dac3e88159e970795d4037f9d6c7bc818914a3097ccacb682025b3f44db +size 1721581 diff --git a/docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.pdf b/docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.pdf new file mode 100644 index 0000000..21804a3 --- /dev/null +++ b/docs/Eric BF/131421 KALIA - 15541 - PARVIS DE RODE - D0371837 - BANDE FILANTE PLAN 01 à 07 -IND B.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88612b0e6d958e4869169eeadbfe69a0a55b84f7e8696429fcc6e27c6a33cab7 +size 2225972 diff --git a/docs/Eric BF/image0000001.jpg b/docs/Eric BF/image0000001.jpg new file mode 100644 index 0000000..c078714 --- /dev/null +++ b/docs/Eric BF/image0000001.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7eeafb479c2c5fa1ac6a2acadc9bee27285ca43cedd029ed43b2da5b6a35dd9 +size 207799 diff --git a/docs/assets/yule-log-cake.jpg b/docs/assets/yule-log-cake.jpg index d0dd3b8..79d2165 100644 Binary files a/docs/assets/yule-log-cake.jpg and b/docs/assets/yule-log-cake.jpg differ diff --git a/public/Interfaces.d.ts b/public/Interfaces.d.ts index 5a466a1..7179edd 100644 --- a/public/Interfaces.d.ts +++ b/public/Interfaces.d.ts @@ -38,7 +38,7 @@ declare interface IProperties extends React.CSSProperties { parentId: string | null x: number y: number - isRigidBody: boolean + XPositionReference?: XPositionReference } diff --git a/src/Components/ContainerProperties/ContainerForm.tsx b/src/Components/ContainerProperties/ContainerForm.tsx index 61a1a9f..0feae02 100644 --- a/src/Components/ContainerProperties/ContainerForm.tsx +++ b/src/Components/ContainerProperties/ContainerForm.tsx @@ -1,9 +1,10 @@ import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline'; import * as React from 'react'; +import { PropertyType } from '../../Enums/PropertyType'; import { XPositionReference } from '../../Enums/XPositionReference'; import IContainerProperties from '../../Interfaces/IContainerProperties'; import { ISymbolModel } from '../../Interfaces/ISymbolModel'; -import { restoreX, transformX } from '../../utils/svg'; +import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, restoreX, transformX } from '../../utils/svg'; import { InputGroup } from '../InputGroup/InputGroup'; import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons'; import { Select } from '../Select/Select'; @@ -11,12 +12,12 @@ import { Select } from '../Select/Select'; interface IContainerFormProps { properties: IContainerProperties symbols: Map - onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void + onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void } const getCSSInputs = ( properties: IContainerProperties, - onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void + onChange: (key: string, value: string | number | boolean, type: PropertyType) => void ): JSX.Element[] => { const groupInput: JSX.Element[] = []; for (const key in properties.style) { @@ -28,7 +29,7 @@ const getCSSInputs = ( inputClassName='' type='string' value={(properties.style as any)[key]} - onChange={(event) => onChange(key, event.target.value, true)} + onChange={(event) => onChange(key, event.target.value, PropertyType.STYLE)} />); } return groupInput; @@ -52,7 +53,16 @@ const ContainerForm: React.FunctionComponent = (props) => { labelClassName='' inputClassName='' type='string' - value={props.properties.parentId?.toString()} + value={props.properties.parentId} + isDisabled={true} + /> + = (props) => { inputClassName='' type='number' isDisabled={props.properties.linkedSymbolId !== ''} - value={transformX(props.properties.x, props.properties.width, props.properties.XPositionReference).toString()} - onChange={(event) => props.onChange('x', restoreX(Number(event.target.value), props.properties.width, props.properties.XPositionReference))} + value={transformX(RemoveXMargin(props.properties.x, props.properties.margin.left), props.properties.width, props.properties.XPositionReference).toString()} + onChange={(event) => props.onChange( + 'x', + ApplyXMargin( + restoreX( + Number(event.target.value), + props.properties.width, + props.properties.XPositionReference + ), + props.properties.margin.left + ) + )} /> = (props) => { labelClassName='' inputClassName='' type='number' - value={props.properties.y.toString()} - onChange={(event) => props.onChange('y', Number(event.target.value))} + value={(props.properties.y - (props.properties.margin?.top ?? 0)).toString()} + onChange={(event) => props.onChange('y', Number(event.target.value) + (props.properties.margin?.top ?? 0))} /> = (props) => { value={props.properties.minWidth.toString()} onChange={(event) => props.onChange('minWidth', Number(event.target.value))} /> + props.onChange('maxWidth', Number(event.target.value))} + /> = (props) => { inputClassName='' type='number' min={props.properties.minWidth} - value={props.properties.width.toString()} - onChange={(event) => props.onChange('width', Number(event.target.value))} + value={(RemoveWidthMargin(props.properties.width, props.properties.margin.left, props.properties.margin.right)).toString()} + onChange={(event) => props.onChange('width', ApplyWidthMargin(Number(event.target.value), props.properties.margin.left, props.properties.margin.right))} + isDisabled={props.properties.isFlex} /> = (props) => { inputClassName='' type='number' min={0} - value={props.properties.height.toString()} - onChange={(event) => props.onChange('height', Number(event.target.value))} + value={(props.properties.height + (props.properties.margin?.top ?? 0) + (props.properties.margin?.bottom ?? 0)).toString()} + onChange={(event) => props.onChange('height', Number(event.target.value) - (props.properties.margin?.top ?? 0) - (props.properties.margin?.bottom ?? 0))} /> props.onChange('left', Number(event.target.value), PropertyType.MARGIN)} + /> + props.onChange('bottom', Number(event.target.value), PropertyType.MARGIN)} + /> + props.onChange('top', Number(event.target.value), PropertyType.MARGIN)} + /> + props.onChange('right', Number(event.target.value), PropertyType.MARGIN)} + /> + props.onChange('isRigidBody', event.target.checked)} + checked={props.properties.isFlex} + onChange={(event) => props.onChange('isFlex', event.target.checked)} /> { it('Some properties, change values with dynamic input', () => { const prop: IContainerProperties = { id: 'stuff', + type: 'type', parentId: 'parentId', linkedSymbolId: '', displayedText: 'stuff', @@ -30,8 +31,10 @@ describe.concurrent('Properties', () => { width: 1, height: 1, minWidth: 1, + maxWidth: Infinity, + margin: {}, XPositionReference: XPositionReference.Left, - isRigidBody: false, + isFlex: false, isAnchor: false }; diff --git a/src/Components/ContainerProperties/ContainerProperties.tsx b/src/Components/ContainerProperties/ContainerProperties.tsx index 357c900..83eaac9 100644 --- a/src/Components/ContainerProperties/ContainerProperties.tsx +++ b/src/Components/ContainerProperties/ContainerProperties.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { PropertyType } from '../../Enums/PropertyType'; import IContainerProperties from '../../Interfaces/IContainerProperties'; import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import ContainerForm from './ContainerForm'; @@ -6,7 +7,7 @@ import ContainerForm from './ContainerForm'; interface IPropertiesProps { properties?: IContainerProperties symbols: Map - onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void + onChange: (key: string, value: string | number | boolean, type?: PropertyType) => void } export const Properties: React.FC = (props: IPropertiesProps) => { diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index a98fe53..a77a01f 100644 --- a/src/Components/Editor/Actions/ContainerOperations.ts +++ b/src/Components/Editor/Actions/ContainerOperations.ts @@ -10,6 +10,9 @@ import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTY import { ApplyBehaviors } from '../Behaviors/Behaviors'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import Swal from 'sweetalert2'; +import { ApplyMargin, transformX } from '../../../utils/svg'; +import { Flex } from '../Behaviors/FlexBehaviors'; +import { PropertyType } from '../../../Enums/PropertyType'; /** * Select a container @@ -87,6 +90,8 @@ export function DeleteContainer( throw new Error('[DeleteContainer] Could not find container among parent\'s children'); } + Flex(container); + // Select the previous container // or select the one above const SelectedContainerId = GetSelectedContainerOnDelete( @@ -147,7 +152,7 @@ export function AddContainerToSelectedContainer( setHistoryCurrentStep: Dispatch> ): void { if (selected === null || - selected === undefined) { + selected === undefined) { return; } @@ -213,9 +218,17 @@ export function AddContainer( if (parentClone === null || parentClone === undefined) { throw new Error('[AddContainer] Container model was not found among children of the main container!'); } + const left: number = containerConfig.Margin?.left ?? 0; + const bottom: number = containerConfig.Margin?.bottom ?? 0; + const top: number = containerConfig.Margin?.top ?? 0; + const right: number = containerConfig.Margin?.right ?? 0; let x = containerConfig.DefaultX ?? 0; - const y = containerConfig.DefaultY ?? 0; + let y = containerConfig.DefaultY ?? 0; + let width = containerConfig.Width ?? containerConfig.MaxWidth ?? containerConfig.MinWidth ?? parentClone.properties.width; + let height = containerConfig.Height ?? parentClone.properties.height; + + ({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right)); x = ApplyAddMethod(index, containerConfig, parentClone, x); @@ -225,6 +238,8 @@ export function AddContainer( parentClone, x, y, + width, + height, containerConfig ); @@ -238,17 +253,15 @@ export function AddContainer( } ); - ApplyBehaviors(newContainer, current.Symbols); - - // And push it the the parent children - if (index === parentClone.children.length) { - parentClone.children.push(newContainer); - } else { - parentClone.children.splice(index, 0, newContainer); - } + parentClone.children.push(newContainer); + UpdateParentChildrenList(parentClone); InitializeDefaultChild(configuration, containerConfig, newContainer, newCounters); + ApplyBehaviors(newContainer, current.Symbols); + + ApplyBehaviorsOnSiblings(newContainer, current.Symbols); + // Update the state history.push({ LastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`, @@ -262,6 +275,16 @@ export function AddContainer( setHistoryCurrentStep(history.length - 1); } +function UpdateParentChildrenList(parentClone: IContainerModel | null): void { + if (parentClone === null) { + return; + } + parentClone.children.sort( + (a, b) => transformX(a.properties.x, a.properties.width, a.properties.XPositionReference) - + transformX(b.properties.x, b.properties.width, b.properties.XPositionReference) + ); +} + function InitializeDefaultChild( configuration: IConfiguration, containerConfig: IAvailableContainer, @@ -286,8 +309,17 @@ function InitializeDefaultChild( } seen.add(currentConfig.Type); - const x = currentConfig.DefaultX ?? 0; - const y = currentConfig.DefaultY ?? 0; + + const left: number = currentConfig.Margin?.left ?? 0; + const bottom: number = currentConfig.Margin?.bottom ?? 0; + const top: number = currentConfig.Margin?.top ?? 0; + const right: number = currentConfig.Margin?.right ?? 0; + let x = currentConfig.DefaultX ?? 0; + let y = currentConfig.DefaultY ?? 0; + let width = currentConfig.Width ?? currentConfig.MaxWidth ?? currentConfig.MinWidth ?? parent.properties.width; + let height = currentConfig.Height ?? parent.properties.height; + + ({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right)); UpdateCounters(newCounters, currentConfig.Type); const count = newCounters[currentConfig.Type]; @@ -297,6 +329,8 @@ function InitializeDefaultChild( parent, x, y, + width, + height, currentConfig ); @@ -353,7 +387,7 @@ function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, par export function OnPropertyChange( key: string, value: string | number | boolean, - isStyle: boolean = false, + type: PropertyType = PropertyType.SIMPLE, selected: IContainerModel | undefined, fullHistory: IHistoryState[], historyCurrentStep: number, @@ -364,7 +398,7 @@ export function OnPropertyChange( const current = history[history.length - 1]; if (selected === null || - selected === undefined) { + selected === undefined) { throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } @@ -375,22 +409,7 @@ export function OnPropertyChange( throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } - const oldSymbolId = container.properties.linkedSymbolId; - - if (isStyle) { - (container.properties.style as any)[key] = value; - } else { - (container.properties as any)[key] = value; - } - - LinkSymbol( - container.properties.id, - oldSymbolId, - container.properties.linkedSymbolId, - current.Symbols - ); - - ApplyBehaviors(container, current.Symbols); + SetContainer(container, key, value, type, current.Symbols); history.push({ LastAction: `Change ${key} of ${container.properties.id}`, @@ -404,6 +423,79 @@ export function OnPropertyChange( setHistoryCurrentStep(history.length - 1); } +/** + * Set the container with properties and behaviors (mutate) + * @param container Container to update + * @param key Key of the property to update + * @param value Value of the property to update + * @param type Type of the property to update + * @param symbols Current list of symbols + */ +function SetContainer( + container: ContainerModel, + key: string, value: string | number | boolean, + type: PropertyType, + symbols: Map +): void { + // get the old symbol to detect unlink + const oldSymbolId = container.properties.linkedSymbolId; + + // update the property + AssignProperty(container, key, value, type); + + // link the symbol if it exists + LinkSymbol( + container.properties.id, + oldSymbolId, + container.properties.linkedSymbolId, + symbols + ); + + // Apply special behaviors: rigid, flex, symbol, anchor + ApplyBehaviors(container, symbols); + + // Apply special behaviors on siblings + ApplyBehaviorsOnSiblings(container, symbols); + + // sort the children list by their position + UpdateParentChildrenList(container.parent); +} + +function AssignProperty(container: ContainerModel, key: string, value: string | number | boolean, type: PropertyType): void { + switch (type) { + case PropertyType.STYLE: + (container.properties.style as any)[key] = value; + break; + case PropertyType.MARGIN: + SetMargin(); + break; + default: + (container.properties as any)[key] = value; + } + + function SetMargin(): void { + const oldMarginValue: number = (container.properties.margin as any)[key]; + const diff = Number(value) - oldMarginValue; + switch (key) { + case 'left': + container.properties.x += diff; + container.properties.width -= diff; + break; + case 'right': + container.properties.width -= diff; + break; + case 'bottom': + container.properties.height -= diff; + break; + case 'top': + container.properties.y += diff; + container.properties.height -= diff; + break; + } + (container.properties.margin as any)[key] = value; + } +} + function LinkSymbol( containerId: string, oldSymbolId: string, @@ -422,3 +514,10 @@ function LinkSymbol( newSymbol.linkedContainers.add(containerId); } +function ApplyBehaviorsOnSiblings(newContainer: ContainerModel, Symbols: Map): void { + if (newContainer.parent === null || newContainer.parent === undefined) { + return; + } + + newContainer.parent.children.filter(container => newContainer !== container).forEach(container => ApplyBehaviors(container, Symbols)); +} diff --git a/src/Components/Editor/Behaviors/AnchorBehaviors.ts b/src/Components/Editor/Behaviors/AnchorBehaviors.ts index 58f50d2..a08c43e 100644 --- a/src/Components/Editor/Behaviors/AnchorBehaviors.ts +++ b/src/Components/Editor/Behaviors/AnchorBehaviors.ts @@ -28,7 +28,7 @@ export function ApplyAnchor(container: IContainerModel): IContainerModel { } const rigidBodies = container.parent.children.filter( - child => child.properties.isRigidBody && !child.properties.isAnchor + child => !child.properties.isAnchor ); const overlappingContainers = getOverlappingContainers(container, rigidBodies); diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index eb9e348..eb0c660 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -2,6 +2,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { APPLY_BEHAVIORS_ON_CHILDREN } from '../../../utils/default'; import { ApplyAnchor } from './AnchorBehaviors'; +import { Flex } from './FlexBehaviors'; import { ApplyRigidBody } from './RigidBodyBehaviors'; import { ApplySymbol } from './SymbolBehaviors'; @@ -16,10 +17,12 @@ 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; +} + +/** + * Flex the container and its siblings (mutate) + * @param container Container to flex + * @returns Flexed container + */ +export function Flex(container: IContainerModel): void { + if (container.parent === null || container.parent === undefined) { + return; + } + const flexibleContainers = container.parent.children + .filter(sibling => sibling.properties.isFlex); + + const minWidths = flexibleContainers + .map(sibling => sibling.properties.minWidth); + + const fixedWidth = container.parent.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 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.' + }); + return; + } + + const maxMinWidths = Math.max(...minWidths); + 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) { + if (!sibling.properties.isFlex) { + sibling.properties.x = right; + right += sibling.properties.width; + continue; + } + sibling.properties.x = ApplyXMargin(right, sibling.properties.margin.left); + sibling.properties.width = ApplyWidthMargin(wantedWidth, sibling.properties.margin.left, sibling.properties.margin.right); + right += wantedWidth; + } + + return; + } + + // does not fit + + /// SIMPLEX /// + const solutions: number[] = Simplex(minWidths, requiredMaxWidth); + + // 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) + } + + // move the containers + let right = 0; + for (const sibling of container.parent.children) { + sibling.properties.x = ApplyXMargin(right, sibling.properties.margin.left); + right += sibling.properties.width; + } +} diff --git a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index f6bcf65..5edf66c 100644 --- a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -9,6 +9,7 @@ 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" @@ -23,6 +24,7 @@ export function ApplyRigidBody( container: IContainerModel ): IContainerModel { container = constraintBodyInsideParent(container); + container = PushContainers(container); container = constraintBodyInsideUnallocatedWidth(container); return container; } @@ -183,7 +185,6 @@ export function constraintBodyInsideUnallocatedWidth( showConfirmButton: false, timer: 5000 }); - container.properties.isRigidBody = false; return container; } @@ -238,8 +239,7 @@ function getAvailableWidths( // Ignore the exception // And we will also only uses containers that also are rigid or are anchors - if (child === exception || - (!child.properties.isRigidBody && !child.properties.isAnchor)) { + if (child === exception) { continue; } const childX = child.properties.x; diff --git a/src/Components/Editor/Behaviors/SymbolBehaviors.ts b/src/Components/Editor/Behaviors/SymbolBehaviors.ts index 2997e01..915c473 100644 --- a/src/Components/Editor/Behaviors/SymbolBehaviors.ts +++ b/src/Components/Editor/Behaviors/SymbolBehaviors.ts @@ -1,9 +1,12 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; +import { applyParentTransform } from '../../../utils/itertools'; import { restoreX, transformX } from '../../../utils/svg'; export function ApplySymbol(container: IContainerModel, symbol: ISymbolModel): IContainerModel { container.properties.x = transformX(symbol.x, symbol.width, symbol.config.XPositionReference); container.properties.x = restoreX(container.properties.x, container.properties.width, container.properties.XPositionReference); + const [x] = applyParentTransform(container.parent, container.properties.x, 0); + container.properties.x = x; return container; } diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 86db810..4a718eb 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -4,7 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration'; import { SVG } from '../SVG/SVG'; import { IHistoryState } from '../../Interfaces/IHistoryState'; import { UI } from '../UI/UI'; -import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, AddContainer, OnPropertyChange } from './Actions/ContainerOperations'; +import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange } from './Actions/ContainerOperations'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save'; import { onKeyDown } from './Actions/Shortcuts'; import EditorEvents from '../../Events/EditorEvents'; @@ -60,7 +60,9 @@ function useWindowEvents( history: IHistoryState[], historyCurrentStep: number, configuration: IConfiguration, - editorRef: React.RefObject + editorRef: React.RefObject, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> ): void { useEffect(() => { const events = EditorEvents; @@ -72,7 +74,12 @@ function useWindowEvents( const funcs = new Map void>(); for (const event of events) { - const func = (): void => event.func(editorState); + const func = (eventInitDict?: CustomEventInit): void => event.func( + editorState, + setHistory, + setHistoryCurrentStep, + eventInitDict + ); editorRef.current?.addEventListener(event.name, func); funcs.set(event.name, func); } @@ -94,7 +101,14 @@ const Editor: React.FunctionComponent = (props) => { const editorRef = useRef(null); useShortcuts(history, historyCurrentStep, setHistoryCurrentStep); - useWindowEvents(history, historyCurrentStep, props.configuration, editorRef); + useWindowEvents( + history, + historyCurrentStep, + props.configuration, + editorRef, + setHistory, + setHistoryCurrentStep + ); const configuration = props.configuration; const current = getCurrentHistoryState(history, historyCurrentStep); @@ -122,15 +136,15 @@ const Editor: React.FunctionComponent = (props) => { setHistory, setHistoryCurrentStep )} - OnPropertyChange={(key, value, isStyle) => OnPropertyChange( - key, value, isStyle, + OnPropertyChange={(key, value, type) => OnPropertyChange( + key, value, type, selected, history, historyCurrentStep, setHistory, setHistoryCurrentStep )} - AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer( + AddContainer={(type) => AddContainerToSelectedContainer( type, selected, configuration, @@ -139,16 +153,6 @@ const Editor: React.FunctionComponent = (props) => { setHistory, setHistoryCurrentStep )} - AddContainer={(index, type, parentId) => AddContainer( - index, - type, - parentId, - configuration, - history, - historyCurrentStep, - setHistory, - setHistoryCurrentStep - )} AddSymbol={(type) => AddSymbol( type, configuration, diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index e6bcde6..d02e25b 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -22,9 +22,12 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, + margin: {}, minWidth: 1, + type: 'type', + maxWidth: Infinity, + isFlex: false, XPositionReference: XPositionReference.Left, - isRigidBody: false, isAnchor: false }, userData: {} @@ -35,7 +38,6 @@ describe.concurrent('Elements sidebar', () => { OnPropertyChange={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} - AddContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -56,8 +58,11 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, + margin: {}, minWidth: 1, - isRigidBody: false, + isFlex: false, + maxWidth: Infinity, + type: 'type', isAnchor: false, XPositionReference: XPositionReference.Left }, @@ -73,7 +78,6 @@ describe.concurrent('Elements sidebar', () => { OnPropertyChange={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} - AddContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -119,7 +123,10 @@ describe.concurrent('Elements sidebar', () => { width: 2000, height: 100, XPositionReference: XPositionReference.Left, - isRigidBody: false, + margin: {}, + isFlex: false, + maxWidth: Infinity, + type: 'type', isAnchor: false }, userData: {} @@ -139,7 +146,10 @@ describe.concurrent('Elements sidebar', () => { minWidth: 1, width: 0, height: 0, - isRigidBody: false, + margin: {}, + isFlex: false, + maxWidth: Infinity, + type: 'type', isAnchor: false, XPositionReference: XPositionReference.Left }, @@ -158,11 +168,14 @@ describe.concurrent('Elements sidebar', () => { displayedText: 'child-2', x: 0, y: 0, + margin: {}, minWidth: 1, width: 0, height: 0, XPositionReference: XPositionReference.Left, - isRigidBody: false, + isFlex: false, + maxWidth: Infinity, + type: 'type', isAnchor: false }, userData: {} @@ -178,7 +191,6 @@ describe.concurrent('Elements sidebar', () => { OnPropertyChange={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} - AddContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -204,7 +216,10 @@ describe.concurrent('Elements sidebar', () => { width: 2000, height: 100, XPositionReference: XPositionReference.Left, - isRigidBody: false, + margin: {}, + isFlex: false, + maxWidth: Infinity, + type: 'type', isAnchor: false }, userData: {} @@ -224,7 +239,10 @@ describe.concurrent('Elements sidebar', () => { width: 0, height: 0, XPositionReference: XPositionReference.Left, - isRigidBody: false, + margin: {}, + isFlex: false, + maxWidth: Infinity, + type: 'type', isAnchor: false }, userData: {} @@ -245,7 +263,6 @@ describe.concurrent('Elements sidebar', () => { OnPropertyChange={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} - AddContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -269,7 +286,6 @@ describe.concurrent('Elements sidebar', () => { OnPropertyChange={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} - AddContainer={() => {}} />); expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy(); diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 2c15fde..7245a15 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -5,10 +5,10 @@ import { IContainerModel } from '../../Interfaces/IContainerModel'; import { getDepth, MakeIterator } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; import { MenuItem } from '../Menu/MenuItem'; -import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick, useMouseEvents } from './MouseEventHandlers'; +import { useMouseEvents } from './MouseEventHandlers'; import { IPoint } from '../../Interfaces/IPoint'; import { ISymbolModel } from '../../Interfaces/ISymbolModel'; -import { Dispatch, RefObject, SetStateAction } from 'react'; +import { PropertyType } from '../../Enums/PropertyType'; interface IElementsSidebarProps { MainContainer: IContainerModel @@ -16,16 +16,23 @@ interface IElementsSidebarProps { isOpen: boolean isHistoryOpen: boolean SelectedContainer: IContainerModel | undefined - OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void + OnPropertyChange: ( + key: string, + value: string | number | boolean, + type?: PropertyType + ) => void SelectContainer: (containerId: string) => void DeleteContainer: (containerid: string) => void - AddContainer: (index: number, type: string, parent: string) => void } -export const ElementsSidebar: React.FC = (props: IElementsSidebarProps): JSX.Element => { +export const ElementsSidebar: React.FC = ( + props: IElementsSidebarProps +): JSX.Element => { // States - const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); - const [onClickContainerId, setOnClickContainerId] = React.useState(''); + const [isContextMenuOpen, setIsContextMenuOpen] = + React.useState(false); + const [onClickContainerId, setOnClickContainerId] = + React.useState(''); const [contextMenuPosition, setContextMenuPosition] = React.useState({ x: 0, y: 0 @@ -45,71 +52,78 @@ export const ElementsSidebar: React.FC = (props: IElement // Render let isOpenClasses = '-right-64'; if (props.isOpen) { - isOpenClasses = props.isHistoryOpen - ? 'right-64' - : 'right-0'; + isOpenClasses = props.isHistoryOpen ? 'right-64' : 'right-0'; } const it = MakeIterator(props.MainContainer); const containers = [...it]; - const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { + const Row = ({ + index, + style + }: { + index: number + style: React.CSSProperties + }): JSX.Element => { const container = containers[index]; const depth: number = getDepth(container); const key = container.properties.id.toString(); - const text = container.properties.displayedText === key - ? `${'|\t'.repeat(depth)} ${key}` - : `${'|\t'.repeat(depth)} ${container.properties.displayedText} (${key})`; - const selectedClass: string = props.SelectedContainer !== undefined && - props.SelectedContainer !== null && - props.SelectedContainer.properties.id === container.properties.id - ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' - : 'bg-slate-300/60 hover:bg-slate-300'; + const text = + container.properties.displayedText === key + ? `${'|\t'.repeat(depth)} ${key}` + : `${'|\t'.repeat(depth)} ${ + container.properties.displayedText + } (${key})`; + const selectedClass: string = + props.SelectedContainer !== undefined && + props.SelectedContainer !== null && + props.SelectedContainer.properties.id === container.properties.id + ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' + : 'bg-slate-300/60 hover:bg-slate-300'; return ( ); }; return ( -
-
- Elements -
-
+
+
Elements
+
- { Row } + {Row}
- { - setIsContextMenuOpen(false); - props.DeleteContainer(onClickContainerId); - }} /> + { + setIsContextMenuOpen(false); + props.DeleteContainer(onClickContainerId); + }} + /> void -): void { - event.preventDefault(); - const type = event.dataTransfer.getData('type'); - const target: HTMLButtonElement = event.target as HTMLButtonElement; - removeBorderClasses(target); - - const targetContainer: IContainerModel | undefined = findContainerById( - mainContainer, - target.id - ); - - if (targetContainer === undefined) { - throw new Error('[handleOnDrop] Tried to drop onto a unknown container!'); - } - - if (targetContainer === mainContainer) { - // if the container is the root, only add type as child - addContainer( - targetContainer.children.length, - type, - targetContainer.properties.id); - return; - } - - if (targetContainer.parent === null || - targetContainer.parent === undefined) { - throw new Error('[handleDrop] Tried to drop into a child container without a parent!'); - } - - const rect = target.getBoundingClientRect(); - const y = event.clientY - rect.top; // y position within the element. - - // locate the hitboxes - if (y < 12) { - const index = targetContainer.parent.children.indexOf(targetContainer); - addContainer( - index, - type, - targetContainer.parent.properties.id - ); - } else if (y < 24) { - addContainer( - targetContainer.children.length, - type, - targetContainer.properties.id); - } else { - const index = targetContainer.parent.children.indexOf(targetContainer); - addContainer( - index + 1, - type, - targetContainer.parent.properties.id - ); - } -} diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index a939d9a..b13630d 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -18,10 +18,17 @@ interface IContainerProps { */ export const Container: React.FC = (props: IContainerProps) => { const containersElements = props.model.children.map(child => ); - const xText = props.model.properties.width / 2; - const yText = props.model.properties.height / 2; - const transform = `translate(${props.model.properties.x}, ${props.model.properties.y})`; + const width: number = props.model.properties.width; + const height: number = props.model.properties.height; + + const x = props.model.properties.x; + const y = props.model.properties.y; + + const xText = width / 2; + const yText = height / 2; + + const transform = `translate(${x}, ${y})`; // g style const defaultStyle: React.CSSProperties = { @@ -39,8 +46,8 @@ export const Container: React.FC = (props: IContainerProps) => const svg = (props.model.properties.customSVG != null) ? CreateReactCustomSVG(props.model.properties.customSVG, props.model.properties) : ( ); @@ -49,10 +56,10 @@ export const Container: React.FC = (props: IContainerProps) => const dimensionMargin = DIMENSION_MARGIN * depth; const id = `dim-${props.model.properties.id}`; const xStart: number = 0; - const xEnd = props.model.properties.width; - const y = -dimensionMargin; + const xEnd = width; + const yDim = -dimensionMargin; const strokeWidth = 1; - const text = (props.model.properties.width ?? 0).toString(); + const text = (width ?? 0).toString(); let dimensionChildren: JSX.Element | null = null; if (props.model.children.length > 1 && SHOW_CHILDREN_DIMENSIONS) { @@ -80,35 +87,34 @@ export const Container: React.FC = (props: IContainerProps) => transform={transform} key={`container-${props.model.properties.id}`} > - { SHOW_PARENT_DIMENSION + {SHOW_PARENT_DIMENSION ? : null } - { dimensionChildren } - { svg } - { SHOW_TEXT + {dimensionChildren} + {svg} + {SHOW_TEXT ? {props.model.properties.displayedText} - : null } - { containersElements } + : null} + {containersElements} ); }; -function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number): -{ childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } { +function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: number): { childrenId: string, xChildrenStart: number, xChildrenEnd: number, yChildren: number, textChildren: string } { const childrenId = `dim-children-${props.model.properties.id}`; const lastChild = props.model.children[props.model.children.length - 1]; @@ -146,7 +152,7 @@ function CreateReactCustomSVG(customSVG: string, props: IContainerProperties): R function transform(node: HTMLElement, children: Node[], props: IContainerProperties): React.ReactNode { const supportedTags = ['line', 'path', 'rect']; if (supportedTags.includes(node.tagName.toLowerCase())) { - const attributes: {[att: string]: string | object | null} = {}; + const attributes: { [att: string]: string | object | null } = {}; node.getAttributeNames().forEach(attName => { const attributeValue = node.getAttribute(attName); if (attributeValue === null) { diff --git a/src/Components/SVG/Elements/Selector.tsx b/src/Components/SVG/Elements/Selector.tsx index 3c5c12f..3ced2e7 100644 --- a/src/Components/SVG/Elements/Selector.tsx +++ b/src/Components/SVG/Elements/Selector.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { getAbsolutePosition } from '../../../utils/itertools'; +import { RemoveMargin } from '../../../utils/svg'; interface ISelectorProps { selected?: IContainerModel @@ -14,11 +15,19 @@ export const Selector: React.FC = (props) => { ); } - const [x, y] = getAbsolutePosition(props.selected); - const [width, height] = [ + let [x, y] = getAbsolutePosition(props.selected); + let [width, height] = [ props.selected.properties.width, props.selected.properties.height ]; + + ({ x, y, width, height } = RemoveMargin(x, y, width, height, + props.selected.properties.margin.left, + props.selected.properties.margin.bottom, + props.selected.properties.margin.top, + props.selected.properties.margin.right + )); + const style: React.CSSProperties = { stroke: '#3B82F6', // tw blue-500 strokeWidth: 4, diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 0763c26..c491467 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -11,6 +11,7 @@ import { Bar } from '../Bar/Bar'; import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol'; import { Symbols } from '../Symbols/Symbols'; import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar'; +import { PropertyType } from '../../Enums/PropertyType'; interface IUIProps { SelectedContainer: IContainerModel | undefined @@ -21,9 +22,8 @@ interface IUIProps { AvailableSymbols: IAvailableSymbol[] SelectContainer: (containerId: string) => void DeleteContainer: (containerId: string) => void - OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void - AddContainerToSelectedContainer: (type: string) => void - AddContainer: (index: number, type: string, parentId: string) => void + OnPropertyChange: (key: string, value: string | number | boolean, type?: PropertyType) => void + AddContainer: (type: string) => void AddSymbol: (type: string) => void OnSymbolPropertyChange: (key: string, value: string | number | boolean) => void SelectSymbol: (symbolId: string) => void @@ -75,7 +75,7 @@ export const UI: React.FunctionComponent = (props: IUIProps) => { = (props: IUIProps) => { OnPropertyChange={props.OnPropertyChange} SelectContainer={props.SelectContainer} DeleteContainer={props.DeleteContainer} - AddContainer={props.AddContainer} /> { + // faire comme la callback de fetch + +} const getEditorState = (editorState: IEditorState): void => { const customEvent = new CustomEvent('getEditorState', { detail: editorState }); @@ -13,14 +23,35 @@ const getCurrentHistoryState = (editorState: IEditorState): void => { document.dispatchEvent(customEvent); }; +const appendNewState = ( + editorState: IEditorState, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch>, + eventInitDict?: CustomEventInit +): void => { + const state: IHistoryState = JSON.parse(eventInitDict?.detail.state); + ReviveState(state); + const history = getCurrentHistory(editorState.history, editorState.historyCurrentStep); + + history.push(state); + setHistory(history); + setHistoryCurrentStep(history.length - 1); +}; + export interface IEditorEvent { name: string - func: (editorState: IEditorState) => void + func: ( + editorState: IEditorState, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch>, + eventInitDict?: CustomEventInit + ) => void } const events: IEditorEvent[] = [ { name: 'getEditorState', func: getEditorState }, - { name: 'getCurrentHistoryState', func: getCurrentHistoryState } + { name: 'getCurrentHistoryState', func: getCurrentHistoryState }, + { name: 'appendNewState', func: appendNewState } ]; export default events; diff --git a/src/Interfaces/IAvailableContainer.ts b/src/Interfaces/IAvailableContainer.ts index 67ad2af..ff61db0 100644 --- a/src/Interfaces/IAvailableContainer.ts +++ b/src/Interfaces/IAvailableContainer.ts @@ -1,6 +1,7 @@ import React from 'react'; import { AddMethod } from '../Enums/AddMethod'; import { XPositionReference } from '../Enums/XPositionReference'; +import { IMargin } from './IMargin'; /** Model of available container used in application configuration */ export interface IAvailableContainer { @@ -10,6 +11,9 @@ export interface IAvailableContainer { Width?: number Height?: number MinWidth?: number + MaxWidth?: number + Margin?: IMargin + IsFlex?: boolean AddMethod?: AddMethod XPositionReference?: XPositionReference CustomSVG?: string diff --git a/src/Interfaces/IContainerProperties.ts b/src/Interfaces/IContainerProperties.ts index 1166bad..2eaa5fd 100644 --- a/src/Interfaces/IContainerProperties.ts +++ b/src/Interfaces/IContainerProperties.ts @@ -1,5 +1,6 @@ import * as React from 'react'; import { XPositionReference } from '../Enums/XPositionReference'; +import { IMargin } from './IMargin'; /** * Properties of a container @@ -8,6 +9,9 @@ export default interface IContainerProperties { /** id of the container */ id: string + /** type matching the configuration on construction */ + type: string + /** id of the parent container (null when there is no parent) */ parentId: string @@ -23,6 +27,9 @@ export default interface IContainerProperties { /** vertical offset */ y: number + /** margin */ + margin: IMargin + /** * Minimum width (min=1) * Allows the container to set isRigidBody to false when it gets squeezed @@ -30,18 +37,23 @@ export default interface IContainerProperties { */ minWidth: number + /** + * Maximum width + */ + maxWidth: number + /** width */ width: number /** height */ height: number - /** true if rigid, false otherwise */ - isRigidBody: boolean - /** true if anchor, false otherwise */ isAnchor: boolean + /** true if flex, false otherwise */ + isFlex: boolean + /** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */ XPositionReference: XPositionReference diff --git a/src/Interfaces/IMargin.ts b/src/Interfaces/IMargin.ts new file mode 100644 index 0000000..55c6d7a --- /dev/null +++ b/src/Interfaces/IMargin.ts @@ -0,0 +1,6 @@ +export interface IMargin { + left?: number + bottom?: number + top?: number + right?: number +} diff --git a/src/utils/default.ts b/src/utils/default.ts index a15bd36..975b9a6 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -68,7 +68,7 @@ export const DEFAULT_CONFIG: IConfiguration = { AvailableContainers: [ { Type: 'Container', - Width: 75, + MaxWidth: 200, Height: 100, Style: { fillOpacity: 0, @@ -79,7 +79,7 @@ export const DEFAULT_CONFIG: IConfiguration = { AvailableSymbols: [], MainContainer: { Type: 'Container', - Width: 2000, + Width: 800, Height: 100, Style: { fillOpacity: 0, @@ -93,16 +93,19 @@ export const DEFAULT_CONFIG: IConfiguration = { */ export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = { id: 'main', + type: 'container', parentId: '', linkedSymbolId: '', displayedText: 'main', x: 0, y: 0, + margin: {}, minWidth: 1, + maxWidth: Number.MAX_SAFE_INTEGER, width: Number(DEFAULT_CONFIG.MainContainer.Width), height: Number(DEFAULT_CONFIG.MainContainer.Height), - isRigidBody: false, isAnchor: false, + isFlex: false, XPositionReference: XPositionReference.Left, style: { stroke: 'black', @@ -126,20 +129,25 @@ export const GetDefaultContainerProps = ( parent: IContainerModel, x: number, y: number, + width: number, + height: number, containerConfig: IAvailableContainer ): IContainerProperties => ({ id: `${type}-${typeCount}`, + type, parentId: parent.properties.id, linkedSymbolId: '', displayedText: `${type}-${typeCount}`, x, y, - width: containerConfig.Width ?? containerConfig.MinWidth ?? parent.properties.width, - height: containerConfig.Height ?? parent.properties.height, - isRigidBody: false, // set this to true to replicate Florian's project + margin: containerConfig.Margin ?? {}, + width, + height, isAnchor: false, + isFlex: containerConfig.IsFlex ?? containerConfig.Width === undefined, XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left, minWidth: containerConfig.MinWidth ?? 1, + maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER, customSVG: containerConfig.CustomSVG, style: structuredClone(containerConfig.Style), userData: structuredClone(containerConfig.UserData) diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts index e52e034..faf1da9 100644 --- a/src/utils/itertools.ts +++ b/src/utils/itertools.ts @@ -14,7 +14,7 @@ export function * MakeIterator(root: IContainerModel): Generator= 0; i--) { const child = container.children[i]; if (visited.has(child)) { - return; + continue; } visited.add(child); queue.push(child); @@ -71,9 +71,20 @@ export function getDepth(parent: IContainerModel): number { * @returns The absolute position of the container */ export function getAbsolutePosition(container: IContainerModel): [number, number] { - let x = container.properties.x; - let y = container.properties.y; - let current = container.parent; + const x = container.properties.x; + const y = container.properties.y; + return cancelParentTransform(container.parent, x, y); +} + +/** + * Cancel the hierarchic transformations to the given x, y + * @param parent Parent of the container to remove its transform + * @param x value to be restored + * @param y value to be restored + * @returns x and y such that the transformations of the parent are cancelled + */ +export function cancelParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] { + let current = parent; while (current != null) { x += current.properties.x; y += current.properties.y; @@ -82,6 +93,23 @@ export function getAbsolutePosition(container: IContainerModel): [number, number return [x, y]; } +/** + * Cancel the hierarchic transformations to the given x, y + * @param parent Parent of the container to remove its transform + * @param x value to be restored + * @param y value to be restored + * @returns x and y such that the transformations of the parent are cancelled + */ +export function applyParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] { + let current = parent; + while (current != null) { + x -= current.properties.x; + y -= current.properties.y; + current = current.parent; + } + return [x, y]; +} + export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined { const it = MakeIterator(root); for (const container of it) { @@ -91,3 +119,20 @@ export function findContainerById(root: IContainerModel, id: string): IContainer } return undefined; } + +export interface IPair { + cur: T + next: T +} + +export function * pairwise(arr: T[]): Generator, void, unknown> { + for (let i = 0; i < arr.length - 1; i++) { + yield { cur: arr[i], next: arr[i + 1] }; + } +} + +export function * reversePairwise(arr: T[]): Generator, void, unknown> { + for (let i = arr.length - 1; i > 0; i--) { + yield { cur: arr[i], next: arr[i - 1] }; + } +} diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts index fcfd022..8c84f19 100644 --- a/src/utils/saveload.ts +++ b/src/utils/saveload.ts @@ -1,5 +1,6 @@ import { findContainerById, MakeIterator } from './itertools'; import { IEditorState } from '../Interfaces/IEditorState'; +import { IHistoryState } from '../Interfaces/IHistoryState'; /** * Revive the Editor state @@ -14,31 +15,37 @@ export function Revive(editorState: IEditorState): void { // restore the parents and the selected container for (const state of history) { - if (state.MainContainer === null || state.MainContainer === undefined) { - continue; - } - - state.Symbols = new Map(state.Symbols); - for (const symbol of state.Symbols.values()) { - symbol.linkedContainers = new Set(symbol.linkedContainers); - } - - const it = MakeIterator(state.MainContainer); - for (const container of it) { - const parentId = container.properties.parentId; - if (parentId === null) { - container.parent = null; - continue; - } - const parent = findContainerById(state.MainContainer, parentId); - if (parent === undefined) { - continue; - } - container.parent = parent; - } + ReviveState(state); } } +export const ReviveState = ( + state: IHistoryState +): void => { + if (state.MainContainer === null || state.MainContainer === undefined) { + return; + } + + state.Symbols = new Map(state.Symbols); + for (const symbol of state.Symbols.values()) { + symbol.linkedContainers = new Set(symbol.linkedContainers); + } + + const it = MakeIterator(state.MainContainer); + for (const container of it) { + const parentId = container.properties.parentId; + if (parentId === null) { + container.parent = null; + continue; + } + const parent = findContainerById(state.MainContainer, parentId); + if (parent === undefined) { + continue; + } + container.parent = parent; + } +}; + export const getCircularReplacer = (): (key: any, value: object | Map | null) => object | null | undefined => { return (key: any, value: object | null) => { if (key === 'parent') { diff --git a/src/utils/simplex.ts b/src/utils/simplex.ts new file mode 100644 index 0000000..60e9724 --- /dev/null +++ b/src/utils/simplex.ts @@ -0,0 +1,246 @@ +/** + * @module {Simplex} Apply the simplex algorithm + * https://www.imse.iastate.edu/files/2015/08/Explanation-of-Simplex-Method.docx + */ + +/** + * Apply the simplex algorithms to the minimum widths + * + * Note: Some optimizations were made to improve performance in order to solve + * with max(minWidths). In point of fact most coefficient are equal to 1 or -1. + * + * Let the following format be the linear problem : + * x >= b are the minimum widths constraint + * sum(x) <= b is the maximum width constraint + * s are slack variables + * @param minWidths + * @param requiredMaxWidth + * @returns + */ +export function Simplex(minWidths: number[], requiredMaxWidth: number): number[] { + /// 1) standardized the equations + // add the min widths constraints + const maximumConstraints = minWidths.map(minWidth => minWidth * -1); + + // add the max width constraint + maximumConstraints.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 rowlength = nVariables + nConstraints + 2; + const matrix = GetInitialMatrix(maximumConstraints, rowlength, nVariables); + + /// Apply the algorithm + const finalMatrix = ApplyMainLoop(matrix, rowlength); + + // 5) read the solutions + const solutions: number[] = GetSolutions(nVariables + nConstraints + 1, finalMatrix); + return solutions; +} + +const MAX_TRIES = 10; + +/** + * Specific to min widths algorithm + * Get the initial matrix from the maximum constraints + * and the number of variables + * @param maximumConstraints + * @param rowlength + * @param nVariables + * @returns + */ +function GetInitialMatrix( + maximumConstraints: number[], + rowlength: number, + nVariables: number +): number[][] { + const nConstraints = maximumConstraints.length; + const matrix = maximumConstraints.map((maximumConstraint, index) => { + const row: number[] = Array(rowlength).fill(0); + + // insert the variable coefficient a of a*x + if (index <= nConstraints - 2) { + // insert the the variable coefficient of the minimum widths constraints (negative identity matrix) + row[index] = -1; + } else { + // insert the the variable coefficient of the maximum width constraint + row.fill(1, 0, nVariables); + } + + // insert the slack variable coefficient b of b*s (identity matrix) + row[index + nVariables] = 1; + + // insert the constraint coefficient (b) + row[rowlength - 1] = maximumConstraint; + return row; + }); + + // add objective function in the last row + const row: number[] = Array(rowlength).fill(0); + + // insert z coefficient + row[rowlength - 2] = 1; + + // insert variable coefficients + row.fill(-1, 0, nVariables); + matrix.push(row); + return matrix; +} + +function getAllIndexes(arr: number[], val: number): number[] { + const indexes = []; let i = -1; + while ((i = arr.indexOf(val, i + 1)) !== -1) { + indexes.push(i); + } + return indexes; +} + +/** + * Apply the main loop of the simplex algorithm and return the final matrix: + * - While the last row of the matrix has negative values : + * - 1) find the column with the smallest negative coefficient in the last row + * - 2) in that column, find the pivot by selecting the row with the smallest ratio + * such as ratio = constraint of last column / coefficient of the selected row of the selected column + * - 3) create the new matrix such as: + * - 4) the selected column must have 1 in the pivot and zeroes in the other rows + * - 5) in the selected rows other columns (other than the selected column) + * must be divided by that pivot: coef / pivot + * - 6) for the others cells, apply the pivot: new value = (-coefficient in the old col) * (coefficient in the new row) + old value + * - 7) if in the new matrix there are still negative values in the last row, + * redo the algorithm with the new matrix as the base matrix + * - 8) otherwise returns the basic variable such as + * a basic variable is defined by a single 1 and only zeroes in its column + * other variables are equal to zeroes + * @param oldMatrix + * @param rowlength + * @returns + */ +function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] { + let matrix = oldMatrix; + let tries = MAX_TRIES; + const indexesTried: Record = {}; + while (matrix[matrix.length - 1].some((v: number) => v < 0) && tries > 0) { + // 1) find the index with smallest coefficient (O(n)+) + const lastRow = matrix[matrix.length - 1]; + const min = Math.min(...lastRow); + const indexes = getAllIndexes(lastRow, min); + // to avoid infinite loop try to select the least used selected index + const pivotColIndex = getLeastUsedIndex(indexes, indexesTried); + // record the usage of index by incrementing + indexesTried[pivotColIndex] = indexesTried[pivotColIndex] !== undefined ? indexesTried[pivotColIndex] + 1 : 1; + + // 2) find the smallest non negative non null ratio bi/xij (O(m)) + const ratios = []; + for (let i = 0; i <= matrix.length - 2; i++) { + const coefficient = matrix[i][pivotColIndex]; + const constraint = matrix[i][rowlength - 1]; + if (coefficient === 0) { + ratios.push(Infinity); + continue; + } + const ratio = constraint / coefficient; + if (ratio < 0) { + ratios.push(Infinity); + continue; + } + ratios.push(ratio); + } + const minRatio = Math.min(...ratios); + const pivotRowIndex = ratios.indexOf(minRatio); // i + + /// Init the new matrix + const newMatrix = structuredClone(matrix); + const pivot = matrix[pivotRowIndex][pivotColIndex]; + + // 3) apply on the pivot row the inverse of the pivot + const newPivotRow = newMatrix[pivotRowIndex]; + newPivotRow.forEach((coef, colIndex) => { + newPivotRow[colIndex] = coef / pivot; + }); + + // 4) update all values + newMatrix.forEach((row, rowIndex) => { + if (rowIndex === pivotRowIndex) { + return; + } + + row.forEach((coef, colIndex) => { + if (colIndex === pivotColIndex) { + // set zeroes on pivot col + row[colIndex] = 0; + return; + } + + // update value = old value + ((-old coef of pivot column) * (new coef of pivot row)) + row[colIndex] = coef + (-matrix[rowIndex][pivotColIndex] * newMatrix[pivotRowIndex][colIndex]); + }); + }); + + matrix = newMatrix; + tries--; + } + + if (tries === 0) { + throw new Error('[Flex]Simplexe: Could not find a solution'); + } + + return matrix; +} + +/** + * Get the solutions from the final matrix + * + * @param {number} nCols Number of solutions that you want to obtain + * @param {number[][]} finalMatrix Final matrix after the algorithm is applied + * @return {*} {number[]} A list of solutions of the final matrix + */ +function GetSolutions(nCols: number, finalMatrix: number[][]): number[] { + const solutions: number[] = Array(nCols).fill(0); + for (let i = 0; i < nCols; i++) { + const counts: Record = {}; + const col: number[] = []; + for (let j = 0; j < finalMatrix.length; j++) { + const row = finalMatrix[j]; + counts[row[i]] = counts[row[i]] !== undefined ? counts[row[i]] + 1 : 1; + col.push(row[i]); + } + + // a basic variable has a single 1 and only zeroes in the column + const nRows = finalMatrix.length; + const isBasic = counts[1] === 1 && counts[0] === (nRows - 1); + if (isBasic) { + const oneIndex = col.indexOf(1); + const row = finalMatrix[oneIndex]; + solutions[i] = row[row.length - 1]; + } else { + solutions[i] = 0; + } + } + return solutions; +} + +/** + * Returns the least used index from the indexesTried + * @param indexes Indexes of all occurences + * @param indexesTried Record of indexes. Count the number of times the index was used. + * @returns The least used index + */ +function getLeastUsedIndex(indexes: number[], indexesTried: Record): number { + let minUsed = Infinity; + let minIndex = -1; + for (const index of indexes) { + const occ = indexesTried[index]; + if (occ === undefined) { + minIndex = index; + break; + } + + if (occ < minUsed) { + minIndex = index; + minUsed = occ; + } + } + return minIndex; +} diff --git a/src/utils/svg.ts b/src/utils/svg.ts index 0248df8..32aa0e6 100644 --- a/src/utils/svg.ts +++ b/src/utils/svg.ts @@ -1,5 +1,15 @@ import { XPositionReference } from '../Enums/XPositionReference'; +// TODO: Big refactoring +/** + * TODO: + * All of these methods should have been + * inside ContainerModel class + * But because of serialization, the methods are lost. + * Rather than adding more functions to this class, + * it is better to fix serialization with the reviver. + */ + export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number { let transformedX = x; if (xPositionReference === XPositionReference.Center) { @@ -19,3 +29,69 @@ export function restoreX(x: number, width: number, xPositionReference = XPositio } return transformedX; } + +export function ApplyMargin( + x: number, + y: number, + width: number, + height: number, + left?: number, + bottom?: number, + top?: number, + right?: number +): { x: number, y: number, width: number, height: number } { + left = left ?? 0; + right = right ?? 0; + bottom = bottom ?? 0; + top = top ?? 0; + x = ApplyXMargin(x, left); + y += top; + width = ApplyWidthMargin(width, left, right); + height -= (bottom + top); + return { x, y, width, height }; +} + +export function RemoveMargin( + x: number, + y: number, + width: number, + height: number, + left?: number, + bottom?: number, + top?: number, + right?: number +): { x: number, y: number, width: number, height: number } { + bottom = bottom ?? 0; + top = top ?? 0; + x = RemoveXMargin(x, left); + y -= top; + width = RemoveWidthMargin(width, left, right); + height += (bottom + top); + return { x, y, width, height }; +} + +export function ApplyXMargin(x: number, left?: number): number { + left = left ?? 0; + x += left; + return x; +} + +export function RemoveXMargin(x: number, left?: number): number { + left = left ?? 0; + x -= left; + return x; +} + +export function ApplyWidthMargin(width: number, left?: number, right?: number): number { + left = left ?? 0; + right = right ?? 0; + width -= (left + right); + return width; +} + +export function RemoveWidthMargin(width: number, left?: number, right?: number): number { + left = left ?? 0; + right = right ?? 0; + width += (left + right); + return width; +} diff --git a/test-server/http.js b/test-server/http.js index 167ff2d..acd975d 100644 --- a/test-server/http.js +++ b/test-server/http.js @@ -53,7 +53,7 @@ const GetSVGLayoutConfiguration = () => { AvailableContainers: [ { Type: 'Chassis', - Width: 500, + MaxWidth: 500, MinWidth: 200, DefaultChildType: 'Trou', Style: { @@ -65,10 +65,14 @@ const GetSVGLayoutConfiguration = () => { }, { Type: 'Trou', - DefaultX: 10, - DefaultY: 10, - Width: 480, - Height: 180, + DefaultX: 0, + DefaultY: 0, + Margin: { + left: 10, + bottom: 10, + top: 10, + right: 10, + }, DefaultChildType: 'Remplissage', Style: { fillOpacity: 1, @@ -108,6 +112,28 @@ const GetSVGLayoutConfiguration = () => { stroke: '#713f12', fill: '#713f12', } + }, + { + Type: '200', + MaxWidth: 500, + MinWidth: 200, + Style: { + fillOpacity: 1, + strokeWidth: 2, + stroke: 'blue', + fill: 'blue', + } + }, + { + Type: '400', + MaxWidth: 500, + MinWidth: 400, + Style: { + fillOpacity: 1, + strokeWidth: 2, + stroke: 'red', + fill: 'red', + } } ], AvailableSymbols: [ @@ -138,7 +164,7 @@ const GetSVGLayoutConfiguration = () => { ], MainContainer: { Height: 200, - Width: 1000 + Width: 800 } }; }; diff --git a/test-server/node-http.js b/test-server/node-http.js index e6c0d74..f2d3594 100644 --- a/test-server/node-http.js +++ b/test-server/node-http.js @@ -53,182 +53,62 @@ const GetSVGLayoutConfiguration = () => { return { AvailableContainers: [ { - BodyColor: null, - BorderColor: '#ff0000', - BorderWidth: 48, - ContainerActions: null, - ContainerDimensionning: null, - DefaultChildrenContainers: null, - Height: 0, - IsPositionFixed: false, - IsWidthFixed: false, - MaxHeight: 0, - MaxWidth: 3000, - MinHeight: 0, - MinWidth: 500, Type: 'Chassis', - TypeChildContainerDefault: 'Trou', Width: 500, - XPositionReference: 0, + MinWidth: 200, + DefaultChildType: 'Trou', Style: { - fillOpacity: 0, - borderWidth: 2, - stroke: 'red' + fillOpacity: 1, + strokeWidth: 2, + stroke: 'red', + fill: '#d3c9b7', } }, { - BodyColor: null, - BorderColor: '#FFFFFF', - BorderWidth: 0, - ContainerActions: null, - ContainerDimensionning: null, - DefaultChildrenContainers: null, - Height: 0, - IsPositionFixed: false, - IsWidthFixed: false, - MaxHeight: 0, - MaxWidth: 0, - MinHeight: 0, - MinWidth: 0, Type: 'Trou', - TypeChildContainerDefault: 'Remplissage', - Width: 0, - XPositionReference: 0 + DefaultX: 10, + DefaultY: 10, + Width: 480, + Height: 180, + DefaultChildType: 'Remplissage', + Style: { + fillOpacity: 1, + strokeWidth: 2, + stroke: 'green', + fill: 'white' + } }, { - BodyColor: '#99C8FF', - BorderColor: '#00FF00', - BorderWidth: 0, - ContainerActions: [ - { - Action: 'SplitRemplissage', - AddingBehavior: 0, - CustomLogo: { - Base64Image: null, - Name: null, - Svg: null, - Url: '' - }, - Description: 'Diviser le remplissage en insérant un montant', - Id: null, - Label: 'Diviser le remplissage' - } - ], - ContainerDimensionning: { - DimensionningStyle: 1, - ShowDimensionning: false, - ShowLabel: false - }, - DefaultChildrenContainers: null, - Height: 0, - IsPositionFixed: false, - IsWidthFixed: false, - MaxHeight: 0, - MaxWidth: 0, - MinHeight: 0, - MinWidth: 0, Type: 'Remplissage', - TypeChildContainerDefault: null, - Width: 0, - XPositionReference: 0 - }, - { - BodyColor: '#FFA947', - BorderColor: '#FFA947', - BorderWidth: 0, - ContainerActions: null, - ContainerDimensionning: null, - DefaultChildrenContainers: null, - Height: 0, - IsPositionFixed: false, - IsWidthFixed: false, - MaxHeight: 0, - MaxWidth: 0, - MinHeight: 0, - MinWidth: 0, - Type: 'Montant', - TypeChildContainerDefault: null, - Width: 50, - XPositionReference: 1 - }, - { - BodyColor: '#FFA3D1', - BorderColor: '#FF6DE6', - BorderWidth: 0, - ContainerActions: null, - ContainerDimensionning: { - DimensionningStyle: 0, - ShowDimensionning: false, - ShowLabel: false + CustomSVG: ` + + + + + ` + , + Style: { + fillOpacity: 1, + strokeWidth: 1, + fill: '#bfdbfe' }, - DefaultChildrenContainers: null, - Height: 0, - IsPositionFixed: false, - IsWidthFixed: false, - MaxHeight: 0, - MaxWidth: 0, - MinHeight: 0, - MinWidth: 0, - Type: 'Ouverture', - TypeChildContainerDefault: null, - Width: 0, - XPositionReference: 0 - }, - { - BodyColor: '#000000', - BorderColor: null, - BorderWidth: 0, - ContainerActions: null, - ContainerDimensionning: { - DimensionningStyle: 0, - ShowDimensionning: false, - ShowLabel: false - }, - DefaultChildrenContainers: null, - Height: 0, - IsPositionFixed: false, - IsWidthFixed: false, - MaxHeight: 0, - MaxWidth: 0, - MinHeight: 0, - MinWidth: 0, - Type: 'Dilatation', - TypeChildContainerDefault: null, - Width: 8, - XPositionReference: 0 - }, - { - BodyColor: '#dee2e4', - BorderColor: '#54616c', - BorderWidth: 0, - ContainerActions: [ - { - Action: 'FillHoleWithChassis', - AddingBehavior: 1, - CustomLogo: { - Base64Image: null, - Name: null, - Svg: null, - Url: '' - }, - Description: 'Remplir le trou avec des châssis', - Id: null, - Label: 'Calepiner' + UserData: { + styleLine: { + transform: "scaleY(0.5) translateY(100%)", + transformBox: "fill-box" } - ], - ContainerDimensionning: null, - DefaultChildrenContainers: null, - Height: 0, - IsPositionFixed: false, - IsWidthFixed: false, - MaxHeight: 0, - MaxWidth: 0, - MinHeight: 0, - MinWidth: 0, - Type: '', - TypeChildContainerDefault: null, - Width: 0, - XPositionReference: 0 + } + }, + { + Type: 'Montant', + Width: 10, + XPositionReference: 1, + Style: { + fillOpacity: 0, + strokeWidth: 2, + stroke: '#713f12', + fill: '#713f12', + } } ], AvailableSymbols: [