diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5c52dac..365a3d3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,7 +21,7 @@ steps: path: $(pnpm_config_cache) displayName: Cache pnpm -- script: | +- bash: | curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7 pnpm config set store-dir $(pnpm_config_cache) displayName: "Setup pnpm" @@ -31,7 +31,8 @@ steps: versionSpec: '16.x' displayName: 'Install Node.js 16.x LTS' -- script: | +- bash: | + set -euo pipefail node --version node ./test-server/node-http.js & jobs @@ -46,7 +47,8 @@ steps: versionSpec: '>=18.7.0' displayName: 'Install Node.js Latest' -- script: | +- bash: | + set -euo pipefail node --version node ./test-server/node-http.js & jobs diff --git a/public/workers/worker.js b/public/workers/worker.js index c11fa7d..9260405 100644 --- a/public/workers/worker.js +++ b/public/workers/worker.js @@ -4,22 +4,19 @@ onmessage = (e) => { }; const getCircularReplacer = () => { - const seen = new WeakSet(); return (key, value) => { if (key === 'parent') { return; } - if (key === 'SelectedContainer') { - return; + if (key === 'Symbols') { + return Array.from(value.entries()); } - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return; - } - seen.add(value); + if (key === 'linkedContainers') { + return Array.from(value); } + return value; }; }; diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx index c85d322..1041814 100644 --- a/src/Components/App/App.tsx +++ b/src/Components/App/App.tsx @@ -26,9 +26,10 @@ export const App: React.FunctionComponent = (props) => { history: [{ LastAction: '', MainContainer: defaultMainContainer, - SelectedContainer: defaultMainContainer, SelectedContainerId: defaultMainContainer.properties.id, - TypeCounters: {} + TypeCounters: {}, + Symbols: new Map(), + SelectedSymbolId: '' }], historyCurrentStep: 0 }); diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts index 601e4c7..4c0c8aa 100644 --- a/src/Components/App/MenuActions.ts +++ b/src/Components/App/MenuActions.ts @@ -4,7 +4,6 @@ import { ContainerModel } from '../../Interfaces/IContainerModel'; import { fetchConfiguration } from '../API/api'; import { IEditorState } from '../../Interfaces/IEditorState'; import { LoadState } from './Load'; -import { XPositionReference } from '../../Enums/XPositionReference'; import { DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default'; export function NewEditor( @@ -26,6 +25,7 @@ export function NewEditor( // Save the configuration and the new MainContainer // and default the selected container to it + // TODO: Put this in default.ts const editorState: IEditorState = { configuration, history: @@ -33,9 +33,10 @@ export function NewEditor( { LastAction: '', MainContainer, - SelectedContainer: MainContainer, SelectedContainerId: MainContainer.properties.id, - TypeCounters: {} + TypeCounters: {}, + Symbols: new Map(), + SelectedSymbolId: '' } ], historyCurrentStep: 0 diff --git a/src/Components/Bar/Bar.tsx b/src/Components/Bar/Bar.tsx index 77cb259..22c617a 100644 --- a/src/Components/Bar/Bar.tsx +++ b/src/Components/Bar/Bar.tsx @@ -1,13 +1,14 @@ -import { ClockIcon, CubeIcon, MapIcon } from '@heroicons/react/outline'; +import { ClockIcon, CubeIcon, LinkIcon, MapIcon } from '@heroicons/react/outline'; import * as React from 'react'; import { BarIcon } from './BarIcon'; interface IBarProps { isSidebarOpen: boolean + isSymbolsOpen: boolean isElementsSidebarOpen: boolean isHistoryOpen: boolean ToggleSidebar: () => void - ToggleElementsSidebar: () => void + ToggleSymbols: () => void ToggleTimeline: () => void } @@ -23,10 +24,10 @@ export const Bar: React.FC = (props) => { props.ToggleElementsSidebar()}> - + isActive={props.isSymbolsOpen} + title='Symbols' + onClick={() => props.ToggleSymbols()}> + ): IContainerModel { if (container.properties.isAnchor) { - ImposePosition(container); + ApplyAnchor(container); } if (container.properties.isRigidBody) { - RecalculatePhysics(container); + ApplyRigidBody(container); + } + + const symbol = symbols.get(container.properties.linkedSymbolId); + if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { + ApplySymbol(container, symbol); } if (APPLY_BEHAVIORS_ON_CHILDREN) { // Apply DFS by recursion for (const child of container.children) { - ApplyBehaviors(child); + ApplyBehaviors(child, symbols); } } diff --git a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index 1ba10a0..f6bcf65 100644 --- a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -19,7 +19,7 @@ import { ISizePointer } from '../../../Interfaces/ISizePointer'; * @param container Container to apply its rigid body properties * @returns A rigid body container */ -export function RecalculatePhysics( +export function ApplyRigidBody( container: IContainerModel ): IContainerModel { container = constraintBodyInsideParent(container); diff --git a/src/Components/Editor/Behaviors/SymbolBehaviors.ts b/src/Components/Editor/Behaviors/SymbolBehaviors.ts new file mode 100644 index 0000000..2997e01 --- /dev/null +++ b/src/Components/Editor/Behaviors/SymbolBehaviors.ts @@ -0,0 +1,9 @@ +import { IContainerModel } from '../../../Interfaces/IContainerModel'; +import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; +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); + return container; +} diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index 8c327e8..018c39d 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -2,19 +2,20 @@ import { Dispatch, SetStateAction } from 'react'; import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IConfiguration } from '../../Interfaces/IConfiguration'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; -import { findContainerById } from '../../utils/itertools'; -import { getCurrentHistory } from './Editor'; +import { findContainerById, MakeIterator } from '../../utils/itertools'; +import { getCurrentHistory, UpdateCounters } from './Editor'; import { AddMethod } from '../../Enums/AddMethod'; import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../utils/default'; import { ApplyBehaviors } from './Behaviors/Behaviors'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; /** * Select a container * @param container Selected container */ export function SelectContainer( - container: ContainerModel, + containerId: string, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, @@ -23,19 +24,13 @@ export function SelectContainer( const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; - const mainContainerClone = structuredClone(current.MainContainer); - const selectedContainer = findContainerById(mainContainerClone, container.properties.id); - - if (selectedContainer === undefined) { - throw new Error('[SelectContainer] Cannot find container among children of main container!'); - } - history.push({ - LastAction: `Select ${selectedContainer.properties.id}`, - MainContainer: mainContainerClone, - SelectedContainer: selectedContainer, - SelectedContainerId: selectedContainer.properties.id, - TypeCounters: Object.assign({}, current.TypeCounters) + LastAction: `Select ${containerId}`, + MainContainer: structuredClone(current.MainContainer), + SelectedContainerId: containerId, + TypeCounters: Object.assign({}, current.TypeCounters), + Symbols: structuredClone(current.Symbols), + SelectedSymbolId: current.SelectedSymbolId }); setHistory(history); setHistoryCurrentStep(history.length - 1); @@ -76,6 +71,8 @@ export function DeleteContainer( if (container === null || container === undefined) { throw new Error('[DeleteContainer] Container model was not found among children of the main container!'); } + const newSymbols = structuredClone(current.Symbols) + UnlinkSymbol(newSymbols, container); const index = container.parent.children.indexOf(container); if (index > -1) { @@ -94,14 +91,26 @@ export function DeleteContainer( history.push({ LastAction: `Delete ${containerId}`, MainContainer: mainContainerClone, - SelectedContainer, SelectedContainerId, - TypeCounters: Object.assign({}, current.TypeCounters) + TypeCounters: Object.assign({}, current.TypeCounters), + Symbols: newSymbols, + SelectedSymbolId: current.SelectedSymbolId }); setHistory(history); setHistoryCurrentStep(history.length - 1); } +function UnlinkSymbol(symbols: Map, container: IContainerModel): void { + const it = MakeIterator(container); + for (const child of it) { + const symbol = symbols.get(child.properties.linkedSymbolId); + if (symbol === undefined) { + continue; + } + symbol.linkedContainers.delete(child.properties.id); + } +} + /** * Add a new container to a selected container * @param type The type of container @@ -114,21 +123,19 @@ export function DeleteContainer( */ export function AddContainerToSelectedContainer( type: string, + selected: IContainerModel | undefined, configuration: IConfiguration, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { - const history = getCurrentHistory(fullHistory, historyCurrentStep); - const current = history[history.length - 1]; - - if (current.SelectedContainer === null || - current.SelectedContainer === undefined) { + if (selected === null || + selected === undefined) { return; } - const parent = current.SelectedContainer; + const parent = selected; AddContainer( parent.children.length, type, @@ -166,11 +173,6 @@ export function AddContainer( const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; - if (current.MainContainer === null || - current.MainContainer === undefined) { - return; - } - // Get the preset properties from the API const containerConfig = configuration.AvailableContainers .find(option => option.Type === type); @@ -220,7 +222,7 @@ export function AddContainer( } ); - ApplyBehaviors(newContainer); + ApplyBehaviors(newContainer, current.Symbols); // And push it the the parent children if (index === parentClone.children.length) { @@ -235,23 +237,15 @@ export function AddContainer( history.push({ LastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`, MainContainer: clone, - SelectedContainer: parentClone, SelectedContainerId: parentClone.properties.id, - TypeCounters: newCounters + TypeCounters: newCounters, + Symbols: structuredClone(current.Symbols), + SelectedSymbolId: current.SelectedSymbolId }); setHistory(history); setHistoryCurrentStep(history.length - 1); } -function UpdateCounters(counters: Record, type: string): void { - if (counters[type] === null || - counters[type] === undefined) { - counters[type] = 0; - } else { - counters[type]++; - } -} - function InitializeDefaultChild( configuration: IConfiguration, containerConfig: IAvailableContainer, diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 75bf90c..f7df902 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -11,6 +11,8 @@ import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations'; import EditorEvents from '../../Events/EditorEvents'; import { IEditorState } from '../../Interfaces/IEditorState'; import { MAX_HISTORY } from '../../utils/default'; +import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './SymbolOperations'; +import { findContainerById } from '../../utils/itertools'; interface IEditorProps { configuration: IConfiguration @@ -18,6 +20,15 @@ interface IEditorProps { historyCurrentStep: number } +export function UpdateCounters(counters: Record, type: string): void { + if (counters[type] === null || + counters[type] === undefined) { + counters[type] = 0; + } else { + counters[type]++; + } +} + export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] => history.slice( Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow) @@ -70,13 +81,16 @@ const Editor: React.FunctionComponent = (props) => { const configuration = props.configuration; const current = getCurrentHistoryState(history, historyCurrentStep); + const selected = findContainerById(current.MainContainer, current.SelectedContainerId); return (
SelectContainer( container, history, @@ -93,6 +107,7 @@ const Editor: React.FunctionComponent = (props) => { )} OnPropertyChange={(key, value, isStyle) => OnPropertyChange( key, value, isStyle, + selected, history, historyCurrentStep, setHistory, @@ -100,6 +115,7 @@ const Editor: React.FunctionComponent = (props) => { )} OnPropertiesSubmit={(event) => OnPropertiesSubmit( event, + selected, history, historyCurrentStep, setHistory, @@ -107,6 +123,7 @@ const Editor: React.FunctionComponent = (props) => { )} AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer( type, + selected, configuration, history, historyCurrentStep, @@ -123,6 +140,35 @@ const Editor: React.FunctionComponent = (props) => { setHistory, setHistoryCurrentStep )} + AddSymbol={(type) => AddSymbol( + type, + configuration, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} + OnSymbolPropertyChange={(key, value) => OnSymbolPropertyChange( + key, value, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} + SelectSymbol={(symbolId) => SelectSymbol( + symbolId, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} + DeleteSymbol={(symbolId) => DeleteSymbol( + symbolId, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} SaveEditorAsJSON={() => SaveEditorAsJSON( history, historyCurrentStep, @@ -134,7 +180,8 @@ const Editor: React.FunctionComponent = (props) => { { current.MainContainer } diff --git a/src/Components/Editor/PropertiesOperations.ts b/src/Components/Editor/PropertiesOperations.ts index 70984da..d939213 100644 --- a/src/Components/Editor/PropertiesOperations.ts +++ b/src/Components/Editor/PropertiesOperations.ts @@ -3,8 +3,9 @@ import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerMode import { IHistoryState } from '../../Interfaces/IHistoryState'; import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; -import { restoreX } from '../SVG/Elements/Container'; import { ApplyBehaviors } from './Behaviors/Behaviors'; +import { restoreX } from '../../utils/svg'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; /** * Handled the property change event in the properties form @@ -16,6 +17,7 @@ export function OnPropertyChange( key: string, value: string | number | boolean, isStyle: boolean = false, + selected: IContainerModel | undefined, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, @@ -24,37 +26,66 @@ export function OnPropertyChange( const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; - if (current.SelectedContainer === null || - current.SelectedContainer === undefined) { + if (selected === null || + selected === undefined) { throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); - const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); + const container: ContainerModel | undefined = findContainerById(mainContainerClone, selected.properties.id); if (container === null || container === undefined) { 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; } - ApplyBehaviors(container); + LinkSymbol( + container.properties.id, + oldSymbolId, + container.properties.linkedSymbolId, + current.Symbols + ); + + ApplyBehaviors(container, current.Symbols); history.push({ LastAction: `Change ${key} of ${container.properties.id}`, MainContainer: mainContainerClone, - SelectedContainer: container, SelectedContainerId: container.properties.id, - TypeCounters: Object.assign({}, current.TypeCounters) + TypeCounters: Object.assign({}, current.TypeCounters), + Symbols: structuredClone(current.Symbols), + SelectedSymbolId: current.SelectedSymbolId }); setHistory(history); setHistoryCurrentStep(history.length - 1); } +function LinkSymbol( + containerId: string, + oldSymbolId: string, + newSymbolId: string, + symbols: Map +): void { + const oldSymbol = symbols.get(oldSymbolId); + const newSymbol = symbols.get(newSymbolId); + + if (newSymbol === undefined) { + if (oldSymbol !== undefined) { + oldSymbol.linkedContainers.delete(containerId); + } + return; + } + + newSymbol.linkedContainers.add(containerId); +} + /** * Handled the property change event in the properties form * @param key Property name @@ -63,6 +94,7 @@ export function OnPropertyChange( */ export function OnPropertiesSubmit( event: React.SyntheticEvent, + selected: IContainerModel | undefined, fullHistory: IHistoryState[], historyCurrentStep: number, setHistory: Dispatch>, @@ -72,13 +104,13 @@ export function OnPropertiesSubmit( const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; - if (current.SelectedContainer === null || - current.SelectedContainer === undefined) { + if (selected === null || + selected === undefined) { throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); - const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); + const container: ContainerModel | undefined = findContainerById(mainContainerClone, selected.properties.id); if (container === null || container === undefined) { throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); @@ -110,14 +142,15 @@ export function OnPropertiesSubmit( } // Apply the behaviors - ApplyBehaviors(container); + ApplyBehaviors(container, current.Symbols); history.push({ LastAction: `Change properties of ${container.properties.id}`, MainContainer: mainContainerClone, - SelectedContainer: container, SelectedContainerId: container.properties.id, - TypeCounters: Object.assign({}, current.TypeCounters) + TypeCounters: Object.assign({}, current.TypeCounters), + Symbols: structuredClone(current.Symbols), + SelectedSymbolId: current.SelectedSymbolId }); setHistory(history); setHistoryCurrentStep(history.length - 1); @@ -187,3 +220,4 @@ const submitRadioButtons = ( (container.properties as any)[property] = radiobutton.value; }; + diff --git a/src/Components/Editor/SymbolOperations.ts b/src/Components/Editor/SymbolOperations.ts new file mode 100644 index 0000000..01844ab --- /dev/null +++ b/src/Components/Editor/SymbolOperations.ts @@ -0,0 +1,180 @@ +import { Dispatch, SetStateAction } from 'react'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { DEFAULT_SYMBOL_HEIGHT, DEFAULT_SYMBOL_WIDTH } from '../../utils/default'; +import { findContainerById } from '../../utils/itertools'; +import { restoreX } from '../../utils/svg'; +import { ApplyBehaviors } from './Behaviors/Behaviors'; +import { getCurrentHistory, UpdateCounters } from './Editor'; + +export function AddSymbol( + name: string, + configuration: IConfiguration, + fullHistory: IHistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + const symbolConfig = configuration.AvailableSymbols + .find(option => option.Name === name); + + if (symbolConfig === undefined) { + throw new Error('[AddSymbol] Symbol could not be found in the config'); + } + const type = `symbol-${name}`; + const newCounters = structuredClone(current.TypeCounters); + UpdateCounters(newCounters, type); + + const newSymbols = structuredClone(current.Symbols); + // TODO: Put this in default.ts as GetDefaultConfig + const newSymbol: ISymbolModel = { + id: `${name}-${newCounters[type]}`, + type: name, + config: structuredClone(symbolConfig), + x: 0, + width: symbolConfig.Width ?? DEFAULT_SYMBOL_WIDTH, + height: symbolConfig.Height ?? DEFAULT_SYMBOL_HEIGHT, + linkedContainers: new Set() + }; + newSymbol.x = restoreX(newSymbol.x, newSymbol.width, newSymbol.config.XPositionReference); + + newSymbols.set(newSymbol.id, newSymbol); + + history.push({ + LastAction: `Add ${name}`, + MainContainer: structuredClone(current.MainContainer), + SelectedContainerId: current.SelectedContainerId, + TypeCounters: newCounters, + Symbols: newSymbols, + SelectedSymbolId: newSymbol.id + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); +} + +export function SelectSymbol( + symbolId: string, + fullHistory: IHistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + history.push({ + LastAction: `Select ${symbolId}`, + MainContainer: structuredClone(current.MainContainer), + SelectedContainerId: current.SelectedContainerId, + TypeCounters: structuredClone(current.TypeCounters), + Symbols: structuredClone(current.Symbols), + SelectedSymbolId: symbolId + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); +} + +export function DeleteSymbol( + symbolId: string, + fullHistory: IHistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + const newSymbols = structuredClone(current.Symbols); + const symbol = newSymbols.get(symbolId); + + if (symbol === undefined) { + throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`); + } + + const newMainContainer = structuredClone(current.MainContainer); + + UnlinkContainers(symbol, newMainContainer); + + newSymbols.delete(symbolId); + + history.push({ + LastAction: `Select ${symbolId}`, + MainContainer: newMainContainer, + SelectedContainerId: current.SelectedContainerId, + TypeCounters: structuredClone(current.TypeCounters), + Symbols: newSymbols, + SelectedSymbolId: symbolId + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); +} + +function UnlinkContainers(symbol: ISymbolModel, newMainContainer: IContainerModel) { + symbol.linkedContainers.forEach((containerId) => { + const container = findContainerById(newMainContainer, containerId); + + if (container === undefined) { + return; + } + + container.properties.linkedSymbolId = ''; + }); +} + +/** + * Handled the property change event in the properties form + * @param key Property name + * @param value New value of the property + * @returns void + */ +export function OnPropertyChange( + key: string, + value: string | number | boolean, + fullHistory: IHistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.SelectedSymbolId === '') { + throw new Error('[OnSymbolPropertyChange] Property was changed before selecting a symbol'); + } + + const newSymbols: Map = structuredClone(current.Symbols); + const symbol = newSymbols.get(current.SelectedSymbolId); + + if (symbol === null || symbol === undefined) { + throw new Error('[OnSymbolPropertyChange] Symbol model was not found in state!'); + } + + (symbol as any)[key] = value; + + const newMainContainer = structuredClone(current.MainContainer); + symbol.linkedContainers.forEach((containerId) => { + const container = findContainerById(newMainContainer, containerId); + + if (container === undefined) { + return; + } + + ApplyBehaviors(container, newSymbols); + }); + + history.push({ + LastAction: `Change ${key} of ${symbol.id}`, + MainContainer: newMainContainer, + SelectedContainerId: current.SelectedContainerId, + TypeCounters: Object.assign({}, current.TypeCounters), + Symbols: newSymbols, + SelectedSymbolId: symbol.id + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); +} diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index 4f3b042..8badea7 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -4,16 +4,19 @@ import { fireEvent, render, screen } from '../../utils/test-utils'; import { ElementsSidebar } from './ElementsSidebar'; import { IContainerModel } from '../../Interfaces/IContainerModel'; import { XPositionReference } from '../../Enums/XPositionReference'; +import { findContainerById } from '../../utils/itertools'; describe.concurrent('Elements sidebar', () => { it('With a MainContainer', () => { render( { }} isOpen={true} isHistoryOpen={false} - SelectedContainer={null} + SelectedContainer={undefined} OnPropertyChange={() => {}} OnPropertiesSubmit={() => {}} SelectContainer={() => {}} @@ -42,12 +45,13 @@ describe.concurrent('Elements sidebar', () => { }); it('With a selected MainContainer', () => { - const MainContainer = { + const MainContainer: IContainerModel = { children: [], parent: null, properties: { id: 'main', parentId: '', + linkedSymbolId: '', displayedText: 'main', x: 0, y: 0, @@ -62,6 +66,7 @@ describe.concurrent('Elements sidebar', () => { }; const { container } = render( { it('With multiple containers', () => { const children: IContainerModel[] = []; - const MainContainer = { + const MainContainer: IContainerModel = { children, parent: null, properties: { id: 'main', parentId: '', + linkedSymbolId: '', displayedText: 'main', x: 0, y: 0, @@ -128,6 +134,7 @@ describe.concurrent('Elements sidebar', () => { properties: { id: 'child-1', parentId: 'main', + linkedSymbolId: '', displayedText: 'child-1', x: 0, y: 0, @@ -149,6 +156,7 @@ describe.concurrent('Elements sidebar', () => { properties: { id: 'child-2', parentId: 'main', + linkedSymbolId: '', displayedText: 'child-2', x: 0, y: 0, @@ -164,6 +172,7 @@ describe.concurrent('Elements sidebar', () => { ); render( { properties: { id: 'main', parentId: '', + linkedSymbolId: '', displayedText: 'main', x: 0, y: 0, @@ -209,6 +219,7 @@ describe.concurrent('Elements sidebar', () => { properties: { id: 'child-1', parentId: 'main', + linkedSymbolId: '', displayedText: 'child-1', x: 0, y: 0, @@ -223,12 +234,13 @@ describe.concurrent('Elements sidebar', () => { }; children.push(child1Model); - let SelectedContainer = MainContainer; - const selectContainer = vi.fn((container: IContainerModel) => { - SelectedContainer = container; + let SelectedContainer: IContainerModel | undefined = MainContainer; + const selectContainer = vi.fn((containerId: string) => { + SelectedContainer = findContainerById(MainContainer, containerId); }); const { container, rerender } = render( { fireEvent.click(child1); rerender( isOpen: boolean isHistoryOpen: boolean - SelectedContainer: IContainerModel | null + SelectedContainer: IContainerModel | undefined OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void OnPropertiesSubmit: (event: React.FormEvent) => void - SelectContainer: (container: IContainerModel) => void + SelectContainer: (containerId: string) => void DeleteContainer: (containerid: string) => void AddContainer: (index: number, type: string, parent: string) => void } @@ -104,7 +106,7 @@ export const ElementsSidebar: React.FC = (props: IElement onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)} onDragOver={(event) => handleDragOver(event, props.MainContainer)} onDragLeave={(event) => handleDragLeave(event)} - onClick={() => props.SelectContainer(container)} + onClick={() => props.SelectContainer(container.properties.id)} > { text } @@ -140,6 +142,7 @@ export const ElementsSidebar: React.FC = (props: IElement diff --git a/src/Components/Properties/DynamicForm.tsx b/src/Components/Properties/DynamicForm.tsx index a34b5bf..be59200 100644 --- a/src/Components/Properties/DynamicForm.tsx +++ b/src/Components/Properties/DynamicForm.tsx @@ -1,18 +1,21 @@ import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline'; import * as React from 'react'; import { XPositionReference } from '../../Enums/XPositionReference'; -import IProperties from '../../Interfaces/IProperties'; +import IContainerProperties from '../../Interfaces/IContainerProperties'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { restoreX, transformX } from '../../utils/svg'; import { InputGroup } from '../InputGroup/InputGroup'; import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons'; -import { restoreX, transformX } from '../SVG/Elements/Container'; +import { Select } from '../Select/Select'; interface IDynamicFormProps { - properties: IProperties + properties: IContainerProperties + symbols: Map onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void } const getCSSInputs = ( - properties: IProperties, + properties: IContainerProperties, onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void ): JSX.Element[] => { const groupInput: JSX.Element[] = []; @@ -67,6 +70,7 @@ const DynamicForm: React.FunctionComponent = (props) => { labelClassName='' 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))} /> @@ -160,9 +164,22 @@ const DynamicForm: React.FunctionComponent = (props) => { ]} onChange={(event) => props.onChange('XPositionReference', Number(event.target.value))} /> + + { options } + + + ); +}; diff --git a/src/Components/SymbolProperties/DynamicForm.tsx b/src/Components/SymbolProperties/DynamicForm.tsx new file mode 100644 index 0000000..8e969b5 --- /dev/null +++ b/src/Components/SymbolProperties/DynamicForm.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { restoreX, transformX } from '../../utils/svg'; +import { InputGroup } from '../InputGroup/InputGroup'; + +interface IDynamicFormProps { + symbol: ISymbolModel + symbols: Map + onChange: (key: string, value: string | number | boolean) => void +} +const DynamicForm: React.FunctionComponent = (props) => { + return ( +
+ + props.onChange('x', restoreX(Number(event.target.value), props.symbol.width, props.symbol.config.XPositionReference))} + /> + props.onChange('height', Number(event.target.value))} + /> + props.onChange('width', Number(event.target.value))} + /> +
+ ); +}; + +export default DynamicForm; diff --git a/src/Components/SymbolProperties/Form.tsx b/src/Components/SymbolProperties/Form.tsx new file mode 100644 index 0000000..a70c7dc --- /dev/null +++ b/src/Components/SymbolProperties/Form.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import DynamicForm from './DynamicForm'; + +interface IFormProps { + symbol: ISymbolModel + symbols: Map + onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void +} + +export const Form: React.FunctionComponent = (props) => { + return ; +}; diff --git a/src/Components/SymbolProperties/SymbolProperties.tsx b/src/Components/SymbolProperties/SymbolProperties.tsx new file mode 100644 index 0000000..b58e3cf --- /dev/null +++ b/src/Components/SymbolProperties/SymbolProperties.tsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; +import IContainerProperties from '../../Interfaces/IContainerProperties'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { ToggleButton } from '../ToggleButton/ToggleButton'; +import { Form } from './Form'; + +interface ISymbolPropertiesProps { + symbol?: ISymbolModel + symbols: Map + onChange: (key: string, value: string | number | boolean) => void +} + +export const SymbolProperties: React.FC = (props: ISymbolPropertiesProps) => { + if (props.symbol === undefined) { + return
; + } + + return ( +
+
+
+ ); +}; diff --git a/src/Components/Symbols/Symbols.tsx b/src/Components/Symbols/Symbols.tsx new file mode 100644 index 0000000..a514a9d --- /dev/null +++ b/src/Components/Symbols/Symbols.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol'; +import { truncateString } from '../../utils/stringtools'; + +interface ISymbolsProps { + componentOptions: IAvailableSymbol[] + isOpen: boolean + buttonOnClick: (type: string) => void +} + +function handleDragStart(event: React.DragEvent): void { + event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id); +} + +export const Symbols: React.FC = (props: ISymbolsProps) => { + const listElements = props.componentOptions.map(componentOption => { + if (componentOption.Image.Url !== undefined || componentOption.Image.Base64Image !== undefined) { + const url = componentOption.Image.Base64Image ?? componentOption.Image.Url; + return (); + } + + return (); + }); + + const isOpenClasses = props.isOpen ? 'left-16' : '-left-64'; + return ( +
+
+ Symbols +
+
+ {listElements} +
+
+ ); +}; diff --git a/src/Components/SymbolsSidebar/MouseEventHandlers.ts b/src/Components/SymbolsSidebar/MouseEventHandlers.ts new file mode 100644 index 0000000..8a8b5b6 --- /dev/null +++ b/src/Components/SymbolsSidebar/MouseEventHandlers.ts @@ -0,0 +1,34 @@ +import { IPoint } from '../../Interfaces/IPoint'; + +export function handleRightClick( + event: MouseEvent, + setIsContextMenuOpen: React.Dispatch>, + setOnClickSymbolId: React.Dispatch>, + setContextMenuPosition: React.Dispatch> +): void { + event.preventDefault(); + + if (!(event.target instanceof HTMLButtonElement)) { + setIsContextMenuOpen(false); + setOnClickSymbolId(''); + return; + } + + const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY }; + setIsContextMenuOpen(true); + setOnClickSymbolId(event.target.id); + setContextMenuPosition(contextMenuPosition); +} + +export function handleLeftClick( + isContextMenuOpen: boolean, + setIsContextMenuOpen: React.Dispatch>, + setOnClickContainerId: React.Dispatch> +): void { + if (!isContextMenuOpen) { + return; + } + + setIsContextMenuOpen(false); + setOnClickContainerId(''); +} diff --git a/src/Components/SymbolsSidebar/SymbolsSidebar.tsx b/src/Components/SymbolsSidebar/SymbolsSidebar.tsx new file mode 100644 index 0000000..2285f47 --- /dev/null +++ b/src/Components/SymbolsSidebar/SymbolsSidebar.tsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import { FixedSizeList as List } from 'react-window'; +import { Menu } from '../Menu/Menu'; +import { MenuItem } from '../Menu/MenuItem'; +import { handleLeftClick, handleRightClick } from './MouseEventHandlers'; +import { IPoint } from '../../Interfaces/IPoint'; +import { ISymbolModel } from '../../Interfaces/ISymbolModel'; +import { SymbolProperties } from '../SymbolProperties/SymbolProperties'; + +interface ISymbolsSidebarProps { + SelectedSymbolId: string + symbols: Map + isOpen: boolean + isHistoryOpen: boolean + OnPropertyChange: (key: string, value: string | number | boolean) => void + SelectSymbol: (symbolId: string) => void + DeleteSymbol: (containerid: string) => void +} + +export const SymbolsSidebar: React.FC = (props: ISymbolsSidebarProps): JSX.Element => { + // States + const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); + const [onClickSymbolId, setOnClickSymbolId] = React.useState(''); + const [contextMenuPosition, setContextMenuPosition] = React.useState({ + x: 0, + y: 0 + }); + + const elementRef = React.useRef(null); + + // Event listeners + React.useEffect(() => { + const onContextMenu = (event: MouseEvent): void => handleRightClick( + event, + setIsContextMenuOpen, + setOnClickSymbolId, + setContextMenuPosition + ); + + const onLeftClick = (): void => handleLeftClick( + isContextMenuOpen, + setIsContextMenuOpen, + setOnClickSymbolId + ); + + elementRef.current?.addEventListener( + 'contextmenu', + onContextMenu + ); + + window.addEventListener( + 'click', + onLeftClick + ); + + return () => { + elementRef.current?.removeEventListener( + 'contextmenu', + onContextMenu + ); + + window.removeEventListener( + 'click', + onLeftClick + ); + }; + }); + + // Render + let isOpenClasses = '-right-64'; + if (props.isOpen) { + isOpenClasses = props.isHistoryOpen + ? 'right-64' + : 'right-0'; + } + + const containers = [...props.symbols.values()]; + const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { + const container = containers[index]; + const key = container.id.toString(); + const text = key; + const selectedClass: string = props.SelectedSymbolId !== '' && + props.SelectedSymbolId === container.id + ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' + : 'bg-slate-300/60 hover:bg-slate-300'; + + return ( + + ); + }; + + return ( +
+
+ Elements +
+
+ + { Row } + +
+ + { + setIsContextMenuOpen(false); + props.DeleteSymbol(onClickSymbolId); + }} /> + + +
+ ); +}; diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index c57daee..db27312 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -3,38 +3,55 @@ import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar'; import { Sidebar } from '../Sidebar/Sidebar'; import { History } from '../History/History'; import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; -import { ContainerModel } from '../../Interfaces/IContainerModel'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IHistoryState } from '../../Interfaces/IHistoryState'; import { PhotographIcon, UploadIcon } from '@heroicons/react/outline'; import { FloatingButton } from '../FloatingButton/FloatingButton'; import { Bar } from '../Bar/Bar'; +import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol'; +import { Symbols } from '../Symbols/Symbols'; +import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar'; interface IUIProps { + SelectedContainer: IContainerModel | undefined current: IHistoryState history: IHistoryState[] historyCurrentStep: number AvailableContainers: IAvailableContainer[] - SelectContainer: (container: ContainerModel) => void + AvailableSymbols: IAvailableSymbol[] + SelectContainer: (containerId: string) => void DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void OnPropertiesSubmit: (event: React.FormEvent) => void AddContainerToSelectedContainer: (type: string) => void AddContainer: (index: number, type: string, parentId: string) => void + AddSymbol: (type: string) => void + OnSymbolPropertyChange: (key: string, value: string | number | boolean) => void + SelectSymbol: (symbolId: string) => void + DeleteSymbol: (symbolId: string) => void SaveEditorAsJSON: () => void SaveEditorAsSVG: () => void LoadState: (move: number) => void } +function CloseOtherSidebars( + setIsSidebarOpen: React.Dispatch>, + setIsSymbolsOpen: React.Dispatch> +): void { + setIsSidebarOpen(false); + setIsSymbolsOpen(false); +} + export const UI: React.FunctionComponent = (props: IUIProps) => { const [isSidebarOpen, setIsSidebarOpen] = React.useState(true); - const [isElementsSidebarOpen, setIsElementsSidebarOpen] = React.useState(false); + const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false); const [isHistoryOpen, setIsHistoryOpen] = React.useState(false); let buttonRightOffsetClasses = 'right-12'; - if (isElementsSidebarOpen || isHistoryOpen) { + if (isSidebarOpen || isHistoryOpen) { buttonRightOffsetClasses = 'right-72'; } - if (isHistoryOpen && isElementsSidebarOpen) { + if (isHistoryOpen && isSidebarOpen) { buttonRightOffsetClasses = 'right-[544px]'; } @@ -42,22 +59,35 @@ export const UI: React.FunctionComponent = (props: IUIProps) => { <> setIsElementsSidebarOpen(!isElementsSidebarOpen)} - ToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)} + ToggleSidebar={() => { + CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen); + setIsSidebarOpen(!isSidebarOpen); + }} + ToggleSymbols={() => { + CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen); + setIsSymbolsOpen(!isSymbolsOpen); + }} ToggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)} /> props.AddContainerToSelectedContainer(type)} + buttonOnClick={props.AddContainerToSelectedContainer} + /> + = (props: IUIProps) => { DeleteContainer={props.DeleteContainer} AddContainer={props.AddContainer} /> + } +/** + * Macro for creating the interface + * Do not add methods since they will be lost during serialization + */ export class ContainerModel implements IContainerModel { public children: IContainerModel[]; public parent: IContainerModel | null; - public properties: IProperties; + public properties: IContainerProperties; public userData: Record; constructor( parent: IContainerModel | null, - properties: IProperties, + properties: IContainerProperties, children: IContainerModel[] = [], userData = {}) { this.parent = parent; diff --git a/src/Interfaces/IProperties.ts b/src/Interfaces/IContainerProperties.ts similarity index 91% rename from src/Interfaces/IProperties.ts rename to src/Interfaces/IContainerProperties.ts index 205d93f..d5f7ae7 100644 --- a/src/Interfaces/IProperties.ts +++ b/src/Interfaces/IContainerProperties.ts @@ -4,13 +4,17 @@ import { XPositionReference } from '../Enums/XPositionReference'; /** * Properties of a container */ -export default interface IProperties { +export default interface IContainerProperties { /** id of the container */ id: string + // TODO: replace null by empty string /** id of the parent container (null when there is no parent) */ parentId: string | null + /** id of the linked symbol ('' when there is no parent) */ + linkedSymbolId: string + /** Text displayed in the container */ displayedText: string diff --git a/src/Interfaces/IHistoryState.ts b/src/Interfaces/IHistoryState.ts index fd46fbc..f906af3 100644 --- a/src/Interfaces/IHistoryState.ts +++ b/src/Interfaces/IHistoryState.ts @@ -1,9 +1,22 @@ import { IContainerModel } from './IContainerModel'; +import { ISymbolModel } from './ISymbolModel'; export interface IHistoryState { + /** Last editor action */ LastAction: string + + /** Reference to the main container */ MainContainer: IContainerModel - SelectedContainer: IContainerModel | null + + /** Id of the selected container */ SelectedContainerId: string + + /** Counter of type of container. Used for ids. */ TypeCounters: Record + + /** List of symbols */ + Symbols: Map + + /** Selected symbols id */ + SelectedSymbolId: string } diff --git a/src/Interfaces/IImage.ts b/src/Interfaces/IImage.ts index 7432440..d22c2e1 100644 --- a/src/Interfaces/IImage.ts +++ b/src/Interfaces/IImage.ts @@ -1,7 +1,20 @@ -/** Model of an image with multiple source */ +/** + * Model of an image with multiple source + * It must at least have one source. + * + * If Url/Base64Image and Svg are set, + * Url/Base64Image will be shown in the menu while SVG will be drawn + */ export interface IImage { + /** Name of the image */ Name: string - Url: string - Base64Image: string - Svg: string + + /** (optional) Url of the image */ + Url?: string + + /** (optional) base64 data of the image */ + Base64Image?: string + + /** (optional) SVG string */ + Svg?: string } diff --git a/src/Interfaces/ISymbolModel.ts b/src/Interfaces/ISymbolModel.ts new file mode 100644 index 0000000..a99966f --- /dev/null +++ b/src/Interfaces/ISymbolModel.ts @@ -0,0 +1,24 @@ +import { IAvailableSymbol } from './IAvailableSymbol'; + +export interface ISymbolModel { + /** Identifier */ + id: string + + /** Type */ + type: string + + /** Configuration of the symbol */ + config: IAvailableSymbol + + /** Horizontal offset */ + x: number + + /** Width */ + width: number + + /** Height */ + height: number + + /** List of linked container id */ + linkedContainers: Set +} diff --git a/src/index.scss b/src/index.scss index d9712f5..c7d716d 100644 --- a/src/index.scss +++ b/src/index.scss @@ -11,6 +11,10 @@ @apply transition-all px-2 py-6 text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300 } + .sidebar-component-card { + @apply transition-all overflow-hidden text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300 + } + .elements-sidebar-row { @apply pl-6 pr-6 pt-2 pb-2 w-full } diff --git a/src/utils/default.ts b/src/utils/default.ts index e2778be..f30323f 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -2,7 +2,7 @@ import { XPositionReference } from '../Enums/XPositionReference'; import { IAvailableContainer } from '../Interfaces/IAvailableContainer'; import { IConfiguration } from '../Interfaces/IConfiguration'; import { IContainerModel } from '../Interfaces/IContainerModel'; -import IProperties from '../Interfaces/IProperties'; +import IContainerProperties from '../Interfaces/IContainerProperties'; /// CONTAINER DEFAULTS /// @@ -18,6 +18,11 @@ export const SHOW_DIMENSIONS_PER_DEPTH = true; export const DIMENSION_MARGIN = 50; export const NOTCHES_LENGTH = 4; +/// SYMBOL DEFAULTS /// + +export const DEFAULT_SYMBOL_WIDTH = 32; +export const DEFAULT_SYMBOL_HEIGHT = 32; + /// EDITOR DEFAULTS /// export const ENABLE_SHORTCUTS = true; @@ -48,9 +53,10 @@ export const DEFAULT_CONFIG: IConfiguration = { } }; -export const DEFAULT_MAINCONTAINER_PROPS: IProperties = { +export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = { id: 'main', parentId: 'null', + linkedSymbolId: '', displayedText: 'main', x: 0, y: 0, @@ -73,9 +79,10 @@ export const GetDefaultContainerProps = ( x: number, y: number, containerConfig: IAvailableContainer -): IProperties => ({ +): IContainerProperties => ({ id: `${type}-${typeCount}`, parentId: parent.properties.id, + linkedSymbolId: '', displayedText: `${type}-${typeCount}`, x, y, diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts index 6867ea3..fcfd022 100644 --- a/src/utils/saveload.ts +++ b/src/utils/saveload.ts @@ -18,6 +18,11 @@ export function Revive(editorState: IEditorState): void { 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; @@ -31,24 +36,21 @@ export function Revive(editorState: IEditorState): void { } container.parent = parent; } - - const selected = findContainerById(state.MainContainer, state.SelectedContainerId); - if (selected === undefined) { - state.SelectedContainer = null; - continue; - } - state.SelectedContainer = selected; } } -export const getCircularReplacer = (): (key: any, value: object | null) => object | null | undefined => { +export const getCircularReplacer = (): (key: any, value: object | Map | null) => object | null | undefined => { return (key: any, value: object | null) => { if (key === 'parent') { return; } - if (key === 'SelectedContainer') { - return; + if (key === 'Symbols') { + return Array.from((value as Map).entries()); + } + + if (key === 'linkedContainers') { + return Array.from(value as Set); } return value; diff --git a/src/utils/stringtools.ts b/src/utils/stringtools.ts index 349e34a..2c858c0 100644 --- a/src/utils/stringtools.ts +++ b/src/utils/stringtools.ts @@ -4,3 +4,7 @@ export function truncateString(str: string, num: number): string { } return `${str.slice(0, num)}...`; } + +export function camelize(str: string): any { + return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join(''); +} diff --git a/src/utils/svg.ts b/src/utils/svg.ts new file mode 100644 index 0000000..0248df8 --- /dev/null +++ b/src/utils/svg.ts @@ -0,0 +1,21 @@ +import { XPositionReference } from '../Enums/XPositionReference'; + +export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number { + let transformedX = x; + if (xPositionReference === XPositionReference.Center) { + transformedX += width / 2; + } else if (xPositionReference === XPositionReference.Right) { + transformedX += width; + } + return transformedX; +} + +export function restoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number { + let transformedX = x; + if (xPositionReference === XPositionReference.Center) { + transformedX -= width / 2; + } else if (xPositionReference === XPositionReference.Right) { + transformedX -= width; + } + return transformedX; +} diff --git a/test-server/http.js b/test-server/http.js index 8053660..167ff2d 100644 --- a/test-server/http.js +++ b/test-server/http.js @@ -112,7 +112,8 @@ const GetSVGLayoutConfiguration = () => { ], AvailableSymbols: [ { - Height: 0, + Width: 32, + Height: 32, Image: { Base64Image: null, Name: null, @@ -120,11 +121,11 @@ const GetSVGLayoutConfiguration = () => { Url: 'https://www.manutan.fr/img/S/GRP/ST/AIG3930272.jpg' }, Name: 'Poteau structure', - Width: 0, XPositionReference: 1 }, { - Height: 0, + Width: 32, + Height: 32, Image: { Base64Image: null, Name: null, @@ -132,7 +133,6 @@ const GetSVGLayoutConfiguration = () => { Url: 'https://e7.pngegg.com/pngimages/647/127/png-clipart-svg-working-group-information-world-wide-web-internet-structure.png' }, Name: 'Joint de structure', - Width: 0, XPositionReference: 0 } ],