From c256a76e01b043a2c00f0945807174beeec1adb1 Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Wed, 12 Oct 2022 09:39:54 +0000 Subject: [PATCH] Merged PR 212: Optimize FindChildrenById from O(n) to O(1) Optimize FindChildrenById from O(n) to O(1): - Deprecate FindContainerByIdDFS - Container: Replace Children to string[] - Add HashMap to IHistoryState that contains all containers To access a container by id now cost O(1) without any additional cost + Implement CICD for SVGLibs --- .env.development | 3 - .env.production | 3 - .env.test | 3 - azure-pipelines.yml | 17 +- .../SVGLDLibs/Models/ApplicationStateModel.cs | 53 +++-- .../SVGLDLibs/Models/ContainerModel.cs | 32 +-- docs/DataStructure.md | 217 ++++++++++++++++++ docs/DevDocs/Pages/SmartComponent.md | 16 +- index.html | 1 + public/svgld-settings.d.ts | 4 + public/svgld-settings.js | 3 + public/workers/worker.js | 4 + src/Components/API/api.test.tsx | 10 +- src/Components/API/api.ts | 5 +- src/Components/App/App.tsx | 7 +- src/Components/Canvas/DimensionLayer.ts | 52 ++++- src/Components/Editor/Actions/AddContainer.ts | 44 ++-- .../Editor/Actions/ContainerOperations.ts | 96 +++++--- .../Editor/Actions/ContextMenuActions.ts | 14 +- .../Editor/Actions/SymbolOperations.ts | 25 +- .../Editor/Behaviors/AnchorBehaviors.ts | 22 +- src/Components/Editor/Behaviors/Behaviors.ts | 69 ++++-- .../Editor/Behaviors/FlexBehaviors.ts | 18 +- .../Editor/Behaviors/PushBehaviors.ts | 13 +- .../Editor/Behaviors/RigidBodyBehaviors.ts | 8 +- .../Editor/Behaviors/SwapBehaviors.ts | 8 +- src/Components/Editor/Editor.tsx | 2 +- src/Components/ElementsList/ElementsList.tsx | 14 +- src/Components/ElementsSidebar/Elements.tsx | 183 --------------- src/Components/MessagesSidebar/Messages.tsx | 6 +- src/Components/SVG/Elements/Container.tsx | 25 +- .../SVG/Elements/DepthDimensionLayer.tsx | 15 +- .../SVG/Elements/DimensionLayer.tsx | 58 ++++- src/Components/SVG/SVG.tsx | 13 +- src/Components/UI/UI.tsx | 10 +- src/Components/UI/UseWorker.tsx | 5 +- src/Components/Viewer/Viewer.tsx | 33 ++- src/Events/EditorEvents.ts | 4 +- src/Interfaces/IContainerModel.ts | 6 +- src/Interfaces/IHistoryState.ts | 3 +- src/utils/default.ts | 19 +- src/utils/itertools.ts | 62 ++++- src/utils/saveload.ts | 15 +- src/vite-env.d.ts | 3 - tsconfig.json | 2 +- 45 files changed, 775 insertions(+), 450 deletions(-) delete mode 100644 .env.development delete mode 100644 .env.production delete mode 100644 .env.test create mode 100644 docs/DataStructure.md create mode 100644 public/svgld-settings.d.ts create mode 100644 public/svgld-settings.js delete mode 100644 src/Components/ElementsSidebar/Elements.tsx diff --git a/.env.development b/.env.development deleted file mode 100644 index 58cf9aa..0000000 --- a/.env.development +++ /dev/null @@ -1,3 +0,0 @@ -VITE_API_FETCH_URL=http://localhost:5000 -VITE_API_SET_CONTAINER_LIST_URL=http://localhost:5000/SetContainerList -VITE_API_GET_FEEDBACK_URL=http://localhost:5000/GetFeedback \ No newline at end of file diff --git a/.env.production b/.env.production deleted file mode 100644 index 4eb6fb6..0000000 --- a/.env.production +++ /dev/null @@ -1,3 +0,0 @@ -VITE_API_FETCH_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration -VITE_API_SET_CONTAINER_LIST_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/SetContainerList -VITE_API_GET_FEEDBACK_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetFeedback \ No newline at end of file diff --git a/.env.test b/.env.test deleted file mode 100644 index 58cf9aa..0000000 --- a/.env.test +++ /dev/null @@ -1,3 +0,0 @@ -VITE_API_FETCH_URL=http://localhost:5000 -VITE_API_SET_CONTAINER_LIST_URL=http://localhost:5000/SetContainerList -VITE_API_GET_FEEDBACK_URL=http://localhost:5000/GetFeedback \ No newline at end of file diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3e62bd0..4cad813 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,6 +13,10 @@ pool: variables: pnpm_config_cache: $(Pipeline.Workspace)/.pnpm-store + CSWebProjectLocation: '$(System.DefaultWorkingDirectory)/csharp/SVGLDLibs/SVGLDWebAPI/SVGLDWebAPI.csproj' + CSLibsProjectLocation: '$(System.DefaultWorkingDirectory)/csharp/SVGLDLibs/SVGLDLibs/SVGLDLibs.csproj' + CSLibsProjectModelsLocation: '$(System.DefaultWorkingDirectory)/csharp/SVGLDLibs/SVGLDLibs/Models' + buildConfiguration: 'Release' steps: - task: Cache@2 @@ -47,7 +51,7 @@ steps: set -euo pipefail node --version node ./test-server/http.js & - dotnet run --project=./csharp/SVGLDLibs/SVGLDWebAPI/SVGLDWebAPI.csproj & + dotnet run --project=$(CSWebProjectLocation) & jobs sleep 10 pnpm i @@ -57,4 +61,13 @@ steps: displayName: 'Test on Node.js 18.x Latest' - publish: $(System.DefaultWorkingDirectory)/dist - artifact: svg-layout-designer \ No newline at end of file + artifact: svg-layout-designer + +- script: dotnet build $(CSLibsProjectLocation) --configuration $(buildConfiguration) --output $(build.artifactstagingdirectory) + displayName: 'dotnet build $(buildConfiguration)' + +- publish: $(Build.ArtifactStagingDirectory) + artifact: svg-layout-designer-net + +- publish: $(CSLibsProjectModelsLocation) + artifact: svg-layout-designer-net-source \ No newline at end of file diff --git a/csharp/SVGLDLibs/SVGLDLibs/Models/ApplicationStateModel.cs b/csharp/SVGLDLibs/SVGLDLibs/Models/ApplicationStateModel.cs index 1c3cf0e..29c9523 100644 --- a/csharp/SVGLDLibs/SVGLDLibs/Models/ApplicationStateModel.cs +++ b/csharp/SVGLDLibs/SVGLDLibs/Models/ApplicationStateModel.cs @@ -1,26 +1,29 @@ -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace SVGLDLibs.Models -{ - public class ApplicationStateModel - { - [DataMember(EmitDefaultValue = false)] - public string lastAction; - - [DataMember(EmitDefaultValue = false)] - public ContainerModel mainContainer; - - [DataMember(EmitDefaultValue = false)] - public string selectedContainerId; - - [DataMember(EmitDefaultValue = false)] - public Dictionary typeCounters; - - [DataMember(EmitDefaultValue = false)] - public Dictionary symbols; - - [DataMember(EmitDefaultValue = false)] - public string selectedSymbolId; - } +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SVGLDLibs.Models +{ + public class ApplicationStateModel + { + [DataMember(EmitDefaultValue = false)] + public string lastAction; + + [DataMember(EmitDefaultValue = false)] + public string mainContainer; + + [DataMember(EmitDefaultValue = false)] + public Dictionary containers; + + [DataMember(EmitDefaultValue = false)] + public string selectedContainerId; + + [DataMember(EmitDefaultValue = false)] + public Dictionary typeCounters; + + [DataMember(EmitDefaultValue = false)] + public Dictionary symbols; + + [DataMember(EmitDefaultValue = false)] + public string selectedSymbolId; + } } \ No newline at end of file diff --git a/csharp/SVGLDLibs/SVGLDLibs/Models/ContainerModel.cs b/csharp/SVGLDLibs/SVGLDLibs/Models/ContainerModel.cs index 0cf2f30..3ae1a1f 100644 --- a/csharp/SVGLDLibs/SVGLDLibs/Models/ContainerModel.cs +++ b/csharp/SVGLDLibs/SVGLDLibs/Models/ContainerModel.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace SVGLDLibs.Models -{ - public class ContainerModel - { - [DataMember(EmitDefaultValue = false)] - public List children; - - [DataMember(EmitDefaultValue = false)] - public ContainerProperties properties; - - [DataMember(EmitDefaultValue = false)] - public Dictionary userData; - } +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace SVGLDLibs.Models +{ + public class ContainerModel + { + [DataMember(EmitDefaultValue = false)] + public List children; + + [DataMember(EmitDefaultValue = false)] + public ContainerProperties properties; + + [DataMember(EmitDefaultValue = false)] + public Dictionary userData; + } } \ No newline at end of file diff --git a/docs/DataStructure.md b/docs/DataStructure.md new file mode 100644 index 0000000..b04f245 --- /dev/null +++ b/docs/DataStructure.md @@ -0,0 +1,217 @@ +# Préface + +Ce document explique la structure de données utilisée pour les conteneurs. + + +# Graphe + +Avant de parler de la structure principale il faut comprendre ce qu'est un graphe. + +Pourquoi ? Parce que la plupart des algorithmes utilisés dans ce projets découle de la théorie des graphes. + +> Definition : Un graphe est une structure composée d'objets dans laquelle certaines paries d'objets sont en relation. (...) On distingue les graphes non orientés et les graphes orientés. (src: wikipedia) + +Ces objets sont appelé Container dans notre projet. Mais dans cette documentation, nous appelerons cela un *Noeud* + +En programmation un graphe peut être représenter par des divers structure de données (Vector/List/Array, Dictionaries, Tree, Pointer etc.). + +Dans notre projet, nous utilisons les pointeurs, les listes et les dictionnaires. + +# Structures de données + +## Pointeur + +Un graphe représenté par des pointeurs peut être représenté de cette manière. + +Un exemple de graphe peut être représenté de la manière suivante : +``` +type Node = { + child: Node +} + +A: Node = { + child: B +} + +B: Node = { + child: A +} +``` + +Donc le graphe est simplement `A <-> B` + +Ceci est un graphe cyclique que nous ne verrons pas souvent dans ce projet. +En effet, nous avons plutôt quelque chose comme ça: + +``` +type Node = { + parent: Node + child: Node +} + +A: Node = { + parent: null + child: B +} + +B: Node = { + parent: A + child: null +} +``` + +Cela permet de représenter un graphe avec deux liens au lieu d'un seul et d'avoir une information de **hiérarchie**. +On le représente donc comme cela: `A -> B`. Donc par définition, un graphe orienté. + + +## Liste + +Un noeud dans un graphe peut avoir plusieurs voisins/enfants. On peut simplement représenter cela par une *liste* de pointeurs. + +Reprenons l'exemple précédent : + +``` +type Node = { + parent: Node + children: Node[] +} + +A: Node { + parent: null, + children: [ + B, + C, + ] +} + +B: Node { + parent: A + children: [] +} + +C: Node { + parent: A + children: [] +} +``` + +Ici, A contient donc deux enfants dans une liste: B et C. +On peut représenter cela de la manière suivante: + +``` +A -> B +A -> C +``` + +A partir d'ici vous pouvez voir ce qu'on appelle un *arbre*. Cette structure de données d'arbre est la base fondamentale dont repose les Conteneurs. + +Pour des examples réels, vous pouvez voir qu'un livre peut être représenté comme des arbres : + +> Un livre contiennent des pages, et des pages contiennent des lignes, des lignes contiennent des lettres, etc. + + +## Dictionnaire + +### Contexte + +Normalement l'arbre devrait être suffisant pour développer SVGLayoutDesigner mais on va s'en rendre compte que ce n'est pas la meilleure solution. + +Depuis le début du projet, la structure utilisée était celle d'un arbre avec relation parent-enfant comme précédemment montré. + +Mon on a remarqué tardivement que cela commençait à avoir un coût très important sur toutes les opérations d'arbres. En effet, car toutes les opérations d'arbre repose sur une fonction principale: la recherche. + +Pour trouver un noeud dans un arbre, il faut parcours l'arbre grâce à des algorithme de parcours d'arbre appelé Depth-First Search et Breath-First Search, qui sont d'excellent algorithme de recherche dans un graphe (oui! car ça ne se limite pas aux arbres). + +Cependant, cela possède un coût au pire cas de O(N). Il est possible que le noeud que le cherche se trouve tout à la fin de l'arbre et ces deux algorithmes n'aident pas du tout dans ce cas là. Imaginez 1000 conteneurs, et on veut juste changer la position d'un seul conteneur. Cela veut dire qu'il faudrait parcourir les 1000 conteneurs ! + +Et c'est ce qui c'est passé jusqu'au commit `9f4796d0` (10 oct 2022). Faites une recherche globale de `FindContainerById`, cela montrera toutes les opérations impactées par l'arbre. Au jour de l'écriture de ce document, il y a environ 60 fonctions utilisant cette méthode utilisée dans divers opérations actives ou passives. C'est très COUTEUX ! + +Réduisons cela à O(1), le meilleur cas. Pour cela nous allons utiliser ce qu'on appelle un dictionnaire ou aussi appelé aussi HashMap. + + +### Qu'est-ce qu'un dictionnaire ? + +> Un dictionnaire est une structure de données qui implémente un tableau associatif. C'est un type de données abstrait qui permet l'association de clé-valeur. + +Exemples : + +``` +Dict { + key: value + ... +} + +Dict = [ + [key, value], + ... +] +``` + +### Mise en pratique + +On veut accéder aux conteneurs rapidement, nous allons sauvegarder chaque conteneur dans le dictionnaire. Donc dès que l'ont crée un conteneur, on ajoute son id pour clé et le conteneur en tant que valeur. Dès qu'on le supprime, on supprime sa clé. + +Cependant pour éviter la duplication de données, il faut aussi changer comment on représente l'arbre. Il n'est plus nécessaire de sauvegarder la référence de l'enfant en tant qu'enfant, on peut juste utiliser son id. + +Ainsi on obtient la structure suivante utilisée dans le projet : + +``` +type Conteneur = { + parent: string + children: string[] +} + +const dict = new Map(); + +dict = { + "conteneur1": Conteneur { + parent: null, + children: [ + "conteneur2", + "conteneur3" + ] + }, + "conteneur3": Conteneur { + parent: "conteneur1", + children: [] + }, + "conteneur2": Conteneur { + parent: "conteneur1", + children: [] + } +} +``` + +Ainsi, `FindContainerById` utilisant précédemment depth-first search, peut être refactoré par une seule ligne : + +```ts +function FindContainerById( + containers: Map, + id: string +): Conteneur { + return containers.get(id) +} +``` + +Et maintenant déplacer un seul conteneur ne coûte plus aussi cher pas vrai ? + +# FAQ + +Pourquoi est-ce important d'utiliser un dictionnaire dans notre cas ? + +> Cela permet d'accéder un conteneur directement par une clé par exemple son id + + +Pourquoi ne pas l'utiliser tout le temps ? + +> Car ce n'est pas très intuitif, on aime voir les arbres comme des arbres et pas comme des listes. Le coût est parfois suffisamment mineur pour ignorer cela. + + +Pourquoi ne pas utiliser l'arbre avec le dictionnaire en même temps ? + +> Car dans notre projet, la sérialisation des données ne permet pas d'avoir deux instances à deux endroits différents. C'est pourquoi nous utilisons un *replacer*, pour supprimer les références de parents. Mais il serait difficile de faire cela pour tous les enfants, il est plus simple de supprimer entièrement l'arbre et de juste conserver le dictionnaire. Et puis, pourquoi dupliquer les données alors que l'on l'accéder avec un coût minimal O(1) avec juste le dictionnaire sans aucun coût supplémentaire ? + + +Et si je veux itérer sur tout les chassis ? + +> Depth-first search et Breath-first search sont toujours valables. Il faut juste adapter légèrement l'algorithme pour qu'il lit le dictionnaire à chaque fois que l'on veut accéder aux enfants. Voir `MakeDFSIterator` ou `MakeBFSIterator` dans `src/utils/itertools.ts` pour l'exemple. diff --git a/docs/DevDocs/Pages/SmartComponent.md b/docs/DevDocs/Pages/SmartComponent.md index e632da8..fcb283b 100644 --- a/docs/DevDocs/Pages/SmartComponent.md +++ b/docs/DevDocs/Pages/SmartComponent.md @@ -26,18 +26,18 @@ Il y deux manières de récupérer les builds du projets: Customiser le build du projet permet de modifier les urls de l'API et de personnaliser des fonctionnalités. -## Configurer les options de build +## Configurer l'API Il y a deux fichiers principaux à configurer : -- `.env.production.local`: pour configurer les URLs d'API +- `src/assets/svgld-settings.js`: pour configurer les URLs d'API - `src/utils/default.ts`: pour configurer les fonctionnalités -Copiez `.env.production` vers `.env.production.local` et modifiez-le comme bon vous semble : +Modifiez `public/svgld-settings.js` -``` -VITE_API_FETCH_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration -VITE_API_SET_CONTAINER_LIST_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/SetContainerList -VITE_API_GET_FEEDBACK_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetFeedback +```js +export const API_FETCH_URL = 'http://localhost:5000'; +export const API_SET_CONTAINER_LIST_URL = 'http://localhost:5000/SetContainerList'; +export const API_GET_FEEDBACK_URL = 'http://localhost:5000/GetFeedback'; ``` Vous pouvez modifiez `src/utils/default.ts` mais ne le committez pas. @@ -92,7 +92,7 @@ Cliquez sur le menu hamburger et cliquez sur **Download Artifacts** :
- + ``` diff --git a/index.html b/index.html index 7342bd5..67ae223 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,7 @@
+ diff --git a/public/svgld-settings.d.ts b/public/svgld-settings.d.ts new file mode 100644 index 0000000..957f521 --- /dev/null +++ b/public/svgld-settings.d.ts @@ -0,0 +1,4 @@ + +export declare const API_FETCH_URL: string; +export declare const API_SET_CONTAINER_LIST_URL: string; +export declare const API_GET_FEEDBACK_URL: string; diff --git a/public/svgld-settings.js b/public/svgld-settings.js new file mode 100644 index 0000000..0937282 --- /dev/null +++ b/public/svgld-settings.js @@ -0,0 +1,3 @@ +export const API_FETCH_URL = 'http://localhost:5000'; +export const API_SET_CONTAINER_LIST_URL = 'http://localhost:5000/SetContainerList'; +export const API_GET_FEEDBACK_URL = 'http://localhost:5000/GetFeedback'; \ No newline at end of file diff --git a/public/workers/worker.js b/public/workers/worker.js index 3faf744..11246a0 100644 --- a/public/workers/worker.js +++ b/public/workers/worker.js @@ -9,6 +9,10 @@ const getCircularReplacer = () => { return; } + if (key === 'containers') { + return Array.from(value.entries()); + } + if (key === 'symbols') { return Array.from(value.entries()); } diff --git a/src/Components/API/api.test.tsx b/src/Components/API/api.test.tsx index 29ebd81..1cb6a09 100644 --- a/src/Components/API/api.test.tsx +++ b/src/Components/API/api.test.tsx @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { describe, it, expect } from 'vitest'; +import { API_FETCH_URL } from '../../../public/svgld-settings'; import { AddMethod } from '../../Enums/AddMethod'; import { Orientation } from '../../Enums/Orientation'; import { Position } from '../../Enums/Position'; @@ -10,7 +11,6 @@ import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol'; import { ICategory } from '../../Interfaces/ICategory'; import { IConfiguration } from '../../Interfaces/IConfiguration'; import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel'; -import { IEditorState } from '../../Interfaces/IEditorState'; import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IPattern } from '../../Interfaces/IPattern'; import { DEFAULT_MAINCONTAINER_PROPS, GetDefaultContainerProps } from '../../utils/default'; @@ -21,9 +21,9 @@ const CHARP_WEB_API_RESOURCE_URL = 'SVGLD'; const CSHARP_WEB_API_URL = CSHARP_WEB_API_BASE_URL + CHARP_WEB_API_RESOURCE_URL + '/'; // TODO: Migrate this test to SVGLDWebAPI rather than using test-server/ -describe.concurrent('Test server test', () => { +describe.concurrent('Test server test', async() => { it('Load environment', () => { - const url = import.meta.env.VITE_API_FETCH_URL; + const url = API_FETCH_URL; expect(url).toBe('http://localhost:5000'); }); @@ -103,9 +103,11 @@ describe.concurrent('Models test suite', () => { DEFAULT_MAINCONTAINER_PROPS ); + const containers = new Map(); const historyState: IHistoryState = { lastAction: 'string', - mainContainer, + mainContainer: mainContainer.properties.id, + containers, selectedContainerId: '3', typeCounters: { main: 1 diff --git a/src/Components/API/api.ts b/src/Components/API/api.ts index 0971135..09ec2e1 100644 --- a/src/Components/API/api.ts +++ b/src/Components/API/api.ts @@ -1,3 +1,4 @@ +import { API_FETCH_URL, API_SET_CONTAINER_LIST_URL } from '../../../public/svgld-settings'; import { IConfiguration } from '../../Interfaces/IConfiguration'; import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest'; import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse'; @@ -8,7 +9,7 @@ import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload'; * @returns {Configation} The model of the configuration for the application */ export async function FetchConfiguration(): Promise { - const url = import.meta.env.VITE_API_FETCH_URL; + const url = API_FETCH_URL; // The test library cannot use the Fetch API // @ts-expect-error // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions @@ -34,7 +35,7 @@ export async function FetchConfiguration(): Promise { } export async function SetContainerList(request: ISetContainerListRequest): Promise { - const url = import.meta.env.VITE_API_SET_CONTAINER_LIST_URL; + const url = API_SET_CONTAINER_LIST_URL; const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure()); // The test library cannot use the Fetch API // @ts-expect-error diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx index 3d0ad1f..4c52c8e 100644 --- a/src/Components/App/App.tsx +++ b/src/Components/App/App.tsx @@ -1,7 +1,7 @@ import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import { events as EVENTS } from '../../Events/AppEvents'; import { MainMenu } from '../MainMenu/MainMenu'; -import { ContainerModel } from '../../Interfaces/IContainerModel'; +import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { Editor } from '../Editor/Editor'; import { IEditorState } from '../../Interfaces/IEditorState'; import { LoadState } from './Actions/Load'; @@ -81,12 +81,15 @@ export function App(props: IAppProps): JSX.Element { null, DEFAULT_MAINCONTAINER_PROPS ); + const containers = new Map(); + containers.set(defaultMainContainer.properties.id, defaultMainContainer); const [editorState, setEditorState] = useState({ configuration: DEFAULT_CONFIG, history: [{ lastAction: '', - mainContainer: defaultMainContainer, + mainContainer: defaultMainContainer.properties.id, + containers, selectedContainerId: defaultMainContainer.properties.id, typeCounters: {}, symbols: new Map(), diff --git a/src/Components/Canvas/DimensionLayer.ts b/src/Components/Canvas/DimensionLayer.ts index c00c835..744b18f 100644 --- a/src/Components/Canvas/DimensionLayer.ts +++ b/src/Components/Canvas/DimensionLayer.ts @@ -2,7 +2,7 @@ import { Orientation } from '../../Enums/Orientation'; import { Position } from '../../Enums/Position'; import { IContainerModel } from '../../Interfaces/IContainerModel'; import { SHOW_SELF_DIMENSIONS, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS } from '../../utils/default'; -import { MakeRecursionDFSIterator, Pairwise } from '../../utils/itertools'; +import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../utils/itertools'; import { TransformX, TransformY } from '../../utils/svg'; import { RenderDimension } from './Dimension'; @@ -10,6 +10,7 @@ const MODULE_STROKE_WIDTH = 1; export function AddDimensions( ctx: CanvasRenderingContext2D, + containers: Map, container: IContainerModel, dimMapped: number[], currentTransform: [number, number], @@ -24,7 +25,8 @@ export function AddDimensions( container.properties.showSelfDimensions, AddHorizontalSelfDimension, AddVerticalSelfDimension, - [container, + [ + container, currentTransform, scale] ); @@ -37,7 +39,9 @@ export function AddDimensions( container.properties.showDimensionWithMarks, AddHorizontalBorrowerDimension, AddVerticalBorrowerDimension, - [container, + [ + containers, + container, depth, currentTransform, scale] @@ -51,7 +55,9 @@ export function AddDimensions( container.properties.showChildrenDimensions, AddHorizontalChildrenDimension, AddVerticalChildrenDimension, - [container, + [ + containers, + container, currentTransform, scale] ); @@ -94,19 +100,30 @@ function ActionByPosition( function AddHorizontalChildrenDimension( ctx: CanvasRenderingContext2D, yDim: number, + containers: Map, container: IContainerModel, currentTransform: [number, number], scale: number ): void { const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`; - const lastChild = container.children[container.children.length - 1]; + const lastChildId = container.children[container.children.length - 1]; + const lastChild = FindContainerById(containers, lastChildId); + + if (lastChild === undefined) { + return; + } let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); // Find the min and max for (let i = container.children.length - 2; i >= 0; i--) { - const child = container.children[i]; + const childId = container.children[i]; + const child = FindContainerById(containers, childId); + + if (child === undefined) { + continue; + } const left = TransformX(child.properties.x, child.properties.width, child.properties.positionReference); if (left < xChildrenStart) { xChildrenStart = left; @@ -142,19 +159,32 @@ function AddHorizontalChildrenDimension( function AddVerticalChildrenDimension( ctx: CanvasRenderingContext2D, xDim: number, + containers: Map, container: IContainerModel, currentTransform: [number, number], scale: number ): void { const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; - const lastChild = container.children[container.children.length - 1]; + const lastChildId = container.children[container.children.length - 1]; + const lastChild = FindContainerById(containers, lastChildId); + + if (lastChild === undefined) { + return; + } + let yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference); let yChildrenEnd = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference); // Find the min and max for (let i = container.children.length - 2; i >= 0; i--) { - const child = container.children[i]; + const childId = container.children[i]; + const child = FindContainerById(containers, childId); + + if (child === undefined) { + continue; + } + const top = TransformY(child.properties.y, child.properties.height, child.properties.positionReference); if (top < yChildrenStart) { yChildrenStart = top; @@ -191,12 +221,13 @@ function AddVerticalChildrenDimension( function AddHorizontalBorrowerDimension( ctx: CanvasRenderingContext2D, yDim: number, + containers: Map, container: IContainerModel, depth: number, currentTransform: [number, number], scale: number ): void { - const it = MakeRecursionDFSIterator(container, depth, currentTransform); + const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const marks = []; // list of vertical lines for the dimension for (const { container: childContainer, currentTransform: childCurrentTransform @@ -243,12 +274,13 @@ function AddHorizontalBorrowerDimension( function AddVerticalBorrowerDimension( ctx: CanvasRenderingContext2D, xDim: number, + containers: Map, container: IContainerModel, depth: number, currentTransform: [number, number], scale: number ): void { - const it = MakeRecursionDFSIterator(container, depth, currentTransform); + const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const marks = []; // list of vertical lines for the dimension for (const { container: childContainer, currentTransform: childCurrentTransform diff --git a/src/Components/Editor/Actions/AddContainer.ts b/src/Components/Editor/Actions/AddContainer.ts index f0c82c2..fb75995 100644 --- a/src/Components/Editor/Actions/AddContainer.ts +++ b/src/Components/Editor/Actions/AddContainer.ts @@ -69,12 +69,11 @@ export function AddContainers( const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; - // Deep clone the main container for the history - const clone: IContainerModel = structuredClone(current.mainContainer); + const containers = structuredClone(current.containers); // Find the parent in the clone const parentClone: IContainerModel | undefined = FindContainerById( - clone, parentId + containers, parentId ); if (parentClone === null || parentClone === undefined) { @@ -90,14 +89,15 @@ export function AddContainers( // Iterate over the containers availableContainers.forEach((availableContainer, typeIndex) => { // Get the preset properties from the API - AddNewContainerToParent(availableContainer, configuration, parentClone, index, typeIndex, newCounters, current.symbols, containerIds); + AddNewContainerToParent(availableContainer, configuration, containers, parentClone, index, typeIndex, newCounters, current.symbols, containerIds); }); // Update the state history.push({ lastAction: `Add [${containerIds.join(', ')}] in ${parentClone.properties.id}`, - mainContainer: clone, + mainContainer: current.mainContainer, selectedContainerId: parentClone.properties.id, + containers, typeCounters: newCounters, symbols: structuredClone(current.symbols), selectedSymbolId: current.selectedSymbolId @@ -109,6 +109,7 @@ export function AddContainers( function AddNewContainerToParent( availableContainer: IAvailableContainer, configuration: IConfiguration, + containers: Map, parentClone: IContainerModel, index: number, typeIndex: number, @@ -143,7 +144,7 @@ function AddNewContainerToParent( ({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right)); // Apply an add method (append or insert/replace) - ({ x, y } = ApplyAddMethod(index + typeIndex, containerConfig, parentClone, x, y)); + ({ x, y } = ApplyAddMethod(containers, index + typeIndex, containerConfig, parentClone, x, y)); // Set the counter of the object type in order to assign an unique id UpdateCounters(newCounters, type); @@ -170,22 +171,25 @@ function AddNewContainerToParent( } ); + // Register the container in the hashmap + containers.set(newContainer.properties.id, newContainer); + // Add it to the parent if (index === parentClone.children.length) { - parentClone.children.push(newContainer); + parentClone.children.push(newContainer.properties.id); } else { - parentClone.children.splice(index, 0, newContainer); + parentClone.children.splice(index, 0, newContainer.properties.id); } // Sort the parent children by x - SortChildren(parentClone); + SortChildren(containers, parentClone); /// Handle behaviors here /// // Apply the behaviors (flex, rigid, anchor) - ApplyBehaviors(newContainer, symbols); + ApplyBehaviors(containers, newContainer, symbols); // Then, apply the behaviors on its siblings (mostly for flex) - ApplyBehaviorsOnSiblingsChildren(newContainer, symbols); + ApplyBehaviorsOnSiblingsChildren(containers, newContainer, symbols); // Initialize default children of the container if (initChilds) { @@ -194,6 +198,7 @@ function AddNewContainerToParent( newContainer, configuration, containerConfig, + containers, newCounters, symbols ); @@ -201,6 +206,7 @@ function AddNewContainerToParent( InitializeChildrenWithPattern( newContainer, configuration, + containers, containerConfig, newCounters, symbols @@ -258,6 +264,7 @@ function InitializeDefaultChild( newContainer: ContainerModel, configuration: IConfiguration, containerConfig: IAvailableContainer, + containers: Map, newCounters: Record, symbols: Map ): void { @@ -276,6 +283,7 @@ function InitializeDefaultChild( AddNewContainerToParent( currentConfig, configuration, + containers, parent, 0, 0, newCounters, @@ -286,6 +294,7 @@ function InitializeDefaultChild( function InitializeChildrenWithPattern( newContainer: ContainerModel, configuration: IConfiguration, + containers: Map, containerConfig: IAvailableContainer, newCounters: Record, symbols: Map @@ -307,6 +316,7 @@ function InitializeChildrenWithPattern( AddNewContainerToParent( container, configuration, + containers, newContainer, 0, 0, newCounters, @@ -334,6 +344,7 @@ function InitializeChildrenWithPattern( AddNewContainerToParent( containerConfig, configuration, + containers, node.parent, 0, 0, newCounters, @@ -350,9 +361,10 @@ function InitializeChildrenWithPattern( console.warn(`[InitializeChildrenFromPattern] IAvailableContainer from pattern was not found in the configuration: ${pattern.wrapper}. Process will ignore the container.`); } else { - parent = AddNewContainerToParent( + const newChildContainer = AddNewContainerToParent( container, configuration, + containers, parent, 0, 0, newCounters, @@ -360,6 +372,9 @@ function InitializeChildrenWithPattern( undefined, false ); + + // iterate + parent = newChildContainer; } } @@ -398,6 +413,7 @@ interface Node { * @returns New offset */ function ApplyAddMethod( + containers: Map, index: number, containerConfig: IAvailableContainer, parent: IContainerModel, @@ -410,8 +426,8 @@ function ApplyAddMethod( containerConfig.AddMethod === AddMethod.Append )) { // Append method (default) - const lastChild: IContainerModel | undefined = parent.children - .at(index - 1); + const lastChildId: string = parent.children[index - 1]; + const lastChild = FindContainerById(containers, lastChildId); if (lastChild !== undefined) { const isHorizontal = parent.properties.orientation === Orientation.Horizontal; diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index b5250a9..d2902c6 100644 --- a/src/Components/Editor/Actions/ContainerOperations.ts +++ b/src/Components/Editor/Actions/ContainerOperations.ts @@ -24,7 +24,8 @@ export function SelectContainer( history.push({ lastAction: `Select ${containerId}`, - mainContainer: structuredClone(current.mainContainer), + mainContainer: current.mainContainer, + containers: structuredClone(current.containers), selectedContainerId: containerId, typeCounters: Object.assign({}, current.typeCounters), symbols: structuredClone(current.symbols), @@ -48,8 +49,9 @@ export function DeleteContainer( const history = GetCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; - const mainContainerClone: IContainerModel = structuredClone(current.mainContainer); - const container = FindContainerById(mainContainerClone, containerId); + const containers = structuredClone(current.containers); + const mainContainerClone: IContainerModel | undefined = FindContainerById(containers, current.mainContainer); + const container = FindContainerById(containers, containerId); if (container === undefined) { throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); @@ -71,21 +73,22 @@ export function DeleteContainer( } const newSymbols = structuredClone(current.symbols); - UnlinkContainerFromSymbols(newSymbols, container); + UnlinkContainerFromSymbols(containers, newSymbols, container); - const index = container.parent.children.indexOf(container); - if (index > -1) { + const index = container.parent.children.indexOf(container.properties.id); + const success = containers.delete(container.properties.id); + if (index > -1 && success) { container.parent.children.splice(index, 1); } else { throw new Error('[DeleteContainer] Could not find container among parent\'s children'); } - ApplyBehaviorsOnSiblings(container, current.symbols); + ApplyBehaviorsOnSiblings(containers, container, current.symbols); // Select the previous container // or select the one above const selectedContainerId = GetSelectedContainerOnDelete( - mainContainerClone, + containers, current.selectedContainerId, container.parent, index @@ -93,7 +96,8 @@ export function DeleteContainer( history.push({ lastAction: `Delete ${containerId}`, - mainContainer: mainContainerClone, + mainContainer: current.mainContainer, + containers, selectedContainerId, typeCounters: Object.assign({}, current.typeCounters), symbols: newSymbols, @@ -115,16 +119,15 @@ export function DeleteContainer( * @returns {IContainerModel} Next selected container */ function GetSelectedContainerOnDelete( - mainContainerClone: IContainerModel, + containers: Map, selectedContainerId: string, parent: IContainerModel, index: number ): string { - const newSelectedContainer = FindContainerById(mainContainerClone, selectedContainerId) ?? + const newSelectedContainerId = FindContainerById(containers, selectedContainerId)?.properties.id ?? parent.children.at(index) ?? parent.children.at(index - 1) ?? - parent; - const newSelectedContainerId = newSelectedContainer.properties.id; + parent.properties.id; return newSelectedContainerId; } @@ -134,8 +137,12 @@ function GetSelectedContainerOnDelete( * @param symbols Symbols to update * @param container Container to unlink */ -function UnlinkContainerFromSymbols(symbols: Map, container: IContainerModel): void { - const it = MakeDFSIterator(container); +function UnlinkContainerFromSymbols( + containers: Map, + symbols: Map, + container: IContainerModel +): void { + const it = MakeDFSIterator(container, containers); for (const child of it) { const symbol = symbols.get(child.properties.linkedSymbolId); if (symbol === undefined) { @@ -167,18 +174,19 @@ export function OnPropertyChange( throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } - const mainContainerClone: IContainerModel = structuredClone(current.mainContainer); - const container: ContainerModel | undefined = FindContainerById(mainContainerClone, selected.properties.id); + const containers = structuredClone(current.containers); + const container: ContainerModel | undefined = FindContainerById(containers, selected.properties.id); if (container === null || container === undefined) { throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } - SetContainer(container, key, value, type, current.symbols); + SetContainer(containers, container, key, value, type, current.symbols); history.push({ lastAction: `Change ${key} of ${container.properties.id}`, - mainContainer: mainContainerClone, + mainContainer: current.mainContainer, + containers, selectedContainerId: container.properties.id, typeCounters: Object.assign({}, current.typeCounters), symbols: structuredClone(current.symbols), @@ -189,20 +197,30 @@ export function OnPropertyChange( /** * Sort the parent children by x - * @param parentClone The clone used for the sort + * @param parent The clone used for the sort * @returns void */ -export function SortChildren(parentClone: IContainerModel | null | undefined): void { - if (parentClone === null || parentClone === undefined) { +export function SortChildren( + containers: Map, + parent: IContainerModel | null | undefined +): void { + if (parent === null || parent === undefined) { return; } - const isHorizontal = parentClone.properties.orientation === Orientation.Horizontal; - const children = parentClone.children; + const isHorizontal = parent.properties.orientation === Orientation.Horizontal; + const children = parent.children; if (!isHorizontal) { - parentClone.children.sort( - (a, b) => { + parent.children.sort( + (aId, bId) => { + const a = FindContainerById(containers, aId); + const b = FindContainerById(containers, bId); + + if (a === undefined || b === undefined) { + return 0; + } + const yA = TransformY(a.properties.y, a.properties.height, a.properties.positionReference); const yB = TransformY(b.properties.y, b.properties.height, b.properties.positionReference); if (yA < yB) { @@ -212,16 +230,23 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v return 1; } // xA = xB - const indexA = children.indexOf(a); - const indexB = children.indexOf(b); + const indexA = children.indexOf(aId); + const indexB = children.indexOf(bId); return indexA - indexB; } ); return; } - parentClone.children.sort( - (a, b) => { + parent.children.sort( + (aId, bId) => { + const a = FindContainerById(containers, aId); + const b = FindContainerById(containers, bId); + + if (a === undefined || b === undefined) { + return 0; + } + const xA = TransformX(a.properties.x, a.properties.width, a.properties.positionReference); const xB = TransformX(b.properties.x, b.properties.width, b.properties.positionReference); if (xA < xB) { @@ -231,8 +256,8 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v return 1; } // xA = xB - const indexA = children.indexOf(a); - const indexB = children.indexOf(b); + const indexA = children.indexOf(aId); + const indexB = children.indexOf(bId); return indexA - indexB; } ); @@ -247,6 +272,7 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v * @param symbols Current list of symbols */ function SetContainer( + containers: Map, container: ContainerModel, key: string, value: string | number | boolean | number[], type: PropertyType, @@ -267,13 +293,13 @@ function SetContainer( ); // sort the children list by their position - SortChildren(container.parent); + SortChildren(containers, container.parent); // Apply special behaviors: rigid, flex, symbol, anchor - ApplyBehaviors(container, symbols); + ApplyBehaviors(containers, container, symbols); // Apply special behaviors on siblings - ApplyBehaviorsOnSiblingsChildren(container, symbols); + ApplyBehaviorsOnSiblingsChildren(containers, container, symbols); } /** diff --git a/src/Components/Editor/Actions/ContextMenuActions.ts b/src/Components/Editor/Actions/ContextMenuActions.ts index 1545359..6b0d24d 100644 --- a/src/Components/Editor/Actions/ContextMenuActions.ts +++ b/src/Components/Editor/Actions/ContextMenuActions.ts @@ -21,7 +21,7 @@ export function GetAction( ): (target: HTMLElement) => void { return (target: HTMLElement) => { const id = target.id; - const container = FindContainerById(currentState.mainContainer, id); + const container = FindContainerById(currentState.containers, id); if (container === undefined) { Swal.fire({ @@ -33,7 +33,7 @@ export function GetAction( } /* eslint-disable @typescript-eslint/naming-convention */ - const { prev, next } = GetPreviousAndNextSiblings(container); + const { prev, next } = GetPreviousAndNextSiblings(currentState.containers, container); const request: ISetContainerListRequest = { Container: container, @@ -59,18 +59,18 @@ export function GetAction( }; } -function GetPreviousAndNextSiblings(container: IContainerModel): { prev: IContainerModel | undefined, next: IContainerModel | undefined } { +function GetPreviousAndNextSiblings(containers: Map, container: IContainerModel): { prev: IContainerModel | undefined, next: IContainerModel | undefined } { let prev; let next; if (container.parent !== undefined && container.parent !== null && container.parent.children.length > 1) { - const index = container.parent.children.indexOf(container); + const index = container.parent.children.indexOf(container.properties.id); if (index > 0) { - prev = container.parent.children[index - 1]; + prev = FindContainerById(containers, container.parent.children[index - 1]); } if (index < container.parent.children.length - 1) { - next = container.parent.children[index + 1]; + next = FindContainerById(containers, container.parent.children[index + 1]); } } return { prev, next }; @@ -144,7 +144,7 @@ function HandleReplace( throw new Error('[ReplaceContainer] Cannot replace a container that does not exists'); } - const index = selectedContainer.parent.children.indexOf(selectedContainer); + const index = selectedContainer.parent.children.indexOf(selectedContainer.properties.id); const newHistoryAfterDelete = DeleteContainer( selectedContainer.properties.id, diff --git a/src/Components/Editor/Actions/SymbolOperations.ts b/src/Components/Editor/Actions/SymbolOperations.ts index 5357f81..63caafb 100644 --- a/src/Components/Editor/Actions/SymbolOperations.ts +++ b/src/Components/Editor/Actions/SymbolOperations.ts @@ -36,6 +36,7 @@ export function AddSymbol( history.push({ lastAction: `Add ${name}`, mainContainer: structuredClone(current.mainContainer), + containers: structuredClone(current.containers), selectedContainerId: current.selectedContainerId, typeCounters: newCounters, symbols: newSymbols, @@ -55,6 +56,7 @@ export function SelectSymbol( history.push({ lastAction: `Select ${symbolId}`, mainContainer: structuredClone(current.mainContainer), + containers: structuredClone(current.containers), selectedContainerId: current.selectedContainerId, typeCounters: structuredClone(current.typeCounters), symbols: structuredClone(current.symbols), @@ -78,15 +80,15 @@ export function DeleteSymbol( throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`); } - const newMainContainer = structuredClone(current.mainContainer); - - UnlinkSymbolFromContainers(symbol, newMainContainer); + const containers = structuredClone(current.containers); + UnlinkSymbolFromContainers(containers, symbol); newSymbols.delete(symbolId); history.push({ lastAction: `Select ${symbolId}`, - mainContainer: newMainContainer, + mainContainer: current.mainContainer, + containers, selectedContainerId: current.selectedContainerId, typeCounters: structuredClone(current.typeCounters), symbols: newSymbols, @@ -100,9 +102,9 @@ export function DeleteSymbol( * @param symbol Symbol to remove * @param root Container and its children to remove a symbol from */ -function UnlinkSymbolFromContainers(symbol: ISymbolModel, root: IContainerModel): void { +function UnlinkSymbolFromContainers(containers: Map, symbol: ISymbolModel): void { symbol.linkedContainers.forEach((containerId) => { - const container = FindContainerById(root, containerId); + const container = FindContainerById(containers, containerId); if (container === undefined) { return; @@ -140,22 +142,23 @@ export function OnPropertyChange( (symbol as any)[key] = value; - const newMainContainer = structuredClone(current.mainContainer); + const containers = structuredClone(current.containers); symbol.linkedContainers.forEach((containerId) => { - const container = FindContainerById(newMainContainer, containerId); + const container = FindContainerById(containers, containerId); if (container === undefined) { return; } - ApplyBehaviors(container, newSymbols); + ApplyBehaviors(containers, container, newSymbols); - ApplyBehaviorsOnSiblingsChildren(container, newSymbols); + ApplyBehaviorsOnSiblingsChildren(containers, container, newSymbols); }); history.push({ lastAction: `Change ${key} of ${symbol.id}`, - mainContainer: newMainContainer, + mainContainer: current.mainContainer, + containers, selectedContainerId: current.selectedContainerId, typeCounters: Object.assign({}, current.typeCounters), symbols: newSymbols, diff --git a/src/Components/Editor/Behaviors/AnchorBehaviors.ts b/src/Components/Editor/Behaviors/AnchorBehaviors.ts index 8cd2b88..34ff573 100644 --- a/src/Components/Editor/Behaviors/AnchorBehaviors.ts +++ b/src/Components/Editor/Behaviors/AnchorBehaviors.ts @@ -16,16 +16,28 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { Orientation } from '../../../Enums/Orientation'; import { ConstraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors'; +import { FindContainerById } from '../../../utils/itertools'; /** * Impose the container position to its siblings * Apply the following modification to the overlapping rigid body container : * @param container Container to impose its position */ -export function ApplyAnchor(container: IContainerModel, parent: IContainerModel): IContainerModel { - const rigidBodies = parent.children.filter( - child => !child.properties.isAnchor - ); +export function ApplyAnchor(containers: Map, container: IContainerModel, parent: IContainerModel): IContainerModel { + const rigidBodies: IContainerModel[] = []; + parent.children.forEach( + childId => { + const child = FindContainerById(containers, childId); + + if (child === undefined) { + return; + } + + if (child.properties.isAnchor) { + return; + } + rigidBodies.push(child); + }); const isHorizontal = parent.properties.orientation === Orientation.Horizontal; const overlappingContainers = isHorizontal @@ -33,7 +45,7 @@ export function ApplyAnchor(container: IContainerModel, parent: IContainerModel) : GetVerticallyOverlappingContainers(container, rigidBodies); for (const overlappingContainer of overlappingContainers) { - ConstraintBodyInsideUnallocatedWidth(overlappingContainer); + ConstraintBodyInsideUnallocatedWidth(containers, overlappingContainer); } return container; } diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index ab09082..e4f777b 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -1,6 +1,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { APPLY_BEHAVIORS_ON_CHILDREN, ENABLE_RIGID, ENABLE_SWAP } from '../../../utils/default'; +import { FindContainerById, MakeChildrenIterator } from '../../../utils/itertools'; import { ApplyAnchor, GetOverlappingContainers } from './AnchorBehaviors'; import { Flex } from './FlexBehaviors'; import { ApplyRigidBody } from './RigidBodyBehaviors'; @@ -13,7 +14,7 @@ import { ApplySymbol } from './SymbolBehaviors'; * @param container Container to recalculate its positions * @returns Updated container */ -export function ApplyBehaviors(container: IContainerModel, symbols: Map): IContainerModel { +export function ApplyBehaviors(containers: Map, container: IContainerModel, symbols: Map): IContainerModel { try { const symbol = symbols.get(container.properties.linkedSymbolId); if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { @@ -24,24 +25,24 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map): void { +export function ApplyBehaviorsOnSiblingsChildren( + containers: Map, + newContainer: IContainerModel, + symbols: Map): void { if (newContainer.parent === null || newContainer.parent === undefined) { return; } newContainer.parent.children - .forEach((container: IContainerModel) => { - if (container.parent != null) { - UpdateWarning(container, container.parent); + .forEach((containerId: string) => { + const container = FindContainerById(containers, containerId); + + if (container === undefined) { + return; + } + + if (container.parent !== null) { + UpdateWarning(containers, container, container.parent); } if (container === newContainer) { return; } - for (const child of container.children) { - ApplyBehaviors(child, symbols); + for (const child of MakeChildrenIterator(containers, container.children)) { + ApplyBehaviors(containers, child, symbols); } }); } @@ -91,30 +101,47 @@ export function ApplyBehaviorsOnSiblingsChildren(newContainer: IContainerModel, * @param symbols * @returns */ -export function ApplyBehaviorsOnSiblings(newContainer: IContainerModel, symbols: Map): void { +export function ApplyBehaviorsOnSiblings(containers: Map, newContainer: IContainerModel, symbols: Map): void { if (newContainer.parent === null || newContainer.parent === undefined) { return; } newContainer.parent.children - .forEach((container: IContainerModel) => { - ApplyBehaviors(container, symbols); + .forEach((containerId: string) => { + const container = FindContainerById(containers, containerId); + + if (container === undefined) { + return; + } + + ApplyBehaviors(containers, container, symbols); if (container.parent != null) { - UpdateWarning(container, container.parent); + UpdateWarning(containers, container, container.parent); } if (container === newContainer) { return; } - for (const child of container.children) { - ApplyBehaviors(child, symbols); + for (const child of MakeChildrenIterator(containers, container.children)) { + ApplyBehaviors(containers, child, symbols); } }); } -function UpdateWarning(container: IContainerModel, parent: IContainerModel): void { - const overlappingContainers = GetOverlappingContainers(container, parent.children); +function UpdateWarning(containers: Map, container: IContainerModel, parent: IContainerModel): void { + const targetContainers: IContainerModel[] = []; + + parent.children.forEach((child) => { + const targetContainer = FindContainerById(containers, child); + + if (targetContainer === undefined) { + return; + } + + targetContainers.push(targetContainer); + }); + const overlappingContainers = GetOverlappingContainers(container, targetContainers); if (overlappingContainers.length > 0) { container.properties.warning = `There are overlapping containers: ${overlappingContainers.map(c => c.properties.id).join(' ')}`; } else { diff --git a/src/Components/Editor/Behaviors/FlexBehaviors.ts b/src/Components/Editor/Behaviors/FlexBehaviors.ts index ade6248..f9dcd59 100644 --- a/src/Components/Editor/Behaviors/FlexBehaviors.ts +++ b/src/Components/Editor/Behaviors/FlexBehaviors.ts @@ -2,6 +2,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { Orientation } from '../../../Enums/Orientation'; import { Simplex } from '../../../utils/simplex'; import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg'; +import { MakeChildrenIterator } from '../../../utils/itertools'; interface IFlexibleGroup { group: IContainerModel[] @@ -13,13 +14,13 @@ interface IFlexibleGroup { * Flex the container and its siblings (mutate) * @returns Flexed container */ -export function Flex(container: IContainerModel, parent: IContainerModel): void { +export function Flex(containers: Map, container: IContainerModel, parent: IContainerModel): void { const isVertical = parent.properties.orientation === Orientation.Vertical; if (isVertical) { const wantedWidth = Math.min(container.properties.maxWidth, parent.properties.width); container.properties.width = ApplyWidthMargin(wantedWidth, container.properties.margin.left, container.properties.margin.right); - const flexibleGroups = GetVerticalFlexibleGroups(parent); + const flexibleGroups = GetVerticalFlexibleGroups(containers, parent); for (const flexibleGroup of flexibleGroups) { FlexGroupVertically(flexibleGroup); } @@ -28,7 +29,7 @@ export function Flex(container: IContainerModel, parent: IContainerModel): void const wantedHeight = Math.min(container.properties.maxHeight, parent.properties.height); container.properties.height = ApplyWidthMargin(wantedHeight, container.properties.margin.top, container.properties.margin.bottom); - const flexibleGroups = GetHorizontalFlexibleGroups(parent); + const flexibleGroups = GetHorizontalFlexibleGroups(containers, parent); for (const flexibleGroup of flexibleGroups) { FlexGroupHorizontally(flexibleGroup); } @@ -39,12 +40,12 @@ export function Flex(container: IContainerModel, parent: IContainerModel): void * @param parent Parent in which the flexible children will be set in groups * @returns a list of groups of flexible containers */ -export function GetHorizontalFlexibleGroups(parent: IContainerModel): IFlexibleGroup[] { +export function GetHorizontalFlexibleGroups(containers: Map, parent: IContainerModel): IFlexibleGroup[] { const flexibleGroups: IFlexibleGroup[] = []; let group: IContainerModel[] = []; let offset = 0; let size = 0; - for (const child of parent.children) { + for (const child of MakeChildrenIterator(containers, parent.children)) { if (child.properties.isAnchor) { size = child.properties.x - offset; const flexibleGroup: IFlexibleGroup = { @@ -77,12 +78,15 @@ export function GetHorizontalFlexibleGroups(parent: IContainerModel): IFlexibleG * @param parent Parent in which the flexible children will be set in groups * @returns a list of groups of flexible containers */ -export function GetVerticalFlexibleGroups(parent: IContainerModel): IFlexibleGroup[] { +export function GetVerticalFlexibleGroups( + containers: Map, + parent: IContainerModel +): IFlexibleGroup[] { const flexibleGroups: IFlexibleGroup[] = []; let group: IContainerModel[] = []; let offset = 0; let size = 0; - for (const child of parent.children) { + for (const child of MakeChildrenIterator(containers, parent.children)) { if (child.properties.isAnchor) { size = child.properties.y - offset; const flexibleGroup: IFlexibleGroup = { diff --git a/src/Components/Editor/Behaviors/PushBehaviors.ts b/src/Components/Editor/Behaviors/PushBehaviors.ts index 499a01a..d728824 100644 --- a/src/Components/Editor/Behaviors/PushBehaviors.ts +++ b/src/Components/Editor/Behaviors/PushBehaviors.ts @@ -1,6 +1,6 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { Orientation } from '../../../Enums/Orientation'; -import { ReversePairwise } from '../../../utils/itertools'; +import { MakeChildrenIterator, ReversePairwise } from '../../../utils/itertools'; import { Flex } from './FlexBehaviors'; /** @@ -8,12 +8,17 @@ import { Flex } from './FlexBehaviors'; * @param container * @returns */ -export function ApplyPush(container: IContainerModel, parent: IContainerModel): IContainerModel { +export function ApplyPush( + containers: Map, + container: IContainerModel, + parent: IContainerModel +): IContainerModel { if (parent.children.length <= 1) { return container; } - const children = parent.children; + const children: IContainerModel[] = [...MakeChildrenIterator(containers, parent.children)]; + const isHorizontal = parent.properties.orientation === Orientation.Horizontal; if (isHorizontal) { @@ -22,7 +27,7 @@ export function ApplyPush(container: IContainerModel, parent: IContainerModel): PushContainersVertically(container, children); } - Flex(container, parent); + Flex(containers, container, parent); return container; } diff --git a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index 743c469..daa1876 100644 --- a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -10,6 +10,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { ISizePointer } from '../../../Interfaces/ISizePointer'; import { Orientation } from '../../../Enums/Orientation'; import { ENABLE_HARD_RIGID } from '../../../utils/default'; +import { MakeChildrenIterator } from '../../../utils/itertools'; /** * "Transform the container into a rigid body" @@ -21,13 +22,14 @@ import { ENABLE_HARD_RIGID } from '../../../utils/default'; * @returns A rigid body container */ export function ApplyRigidBody( + containers: Map, container: IContainerModel, parent: IContainerModel ): IContainerModel { container = ConstraintBodyInsideParent(container, parent); if (ENABLE_HARD_RIGID) { - container = ConstraintBodyInsideUnallocatedWidth(container); + container = ConstraintBodyInsideUnallocatedWidth(containers, container); } return container; @@ -117,6 +119,7 @@ function ConstraintBodyInsideSpace( * @returns Updated container */ export function ConstraintBodyInsideUnallocatedWidth( + containers: Map, container: IContainerModel ): IContainerModel { if (container.parent === null || container.parent === undefined) { @@ -126,10 +129,11 @@ export function ConstraintBodyInsideUnallocatedWidth( // Get the available spaces of the parent const isHorizontal = container.parent.properties.orientation === Orientation.Horizontal; + const children: IContainerModel[] = [...MakeChildrenIterator(containers, container.parent.children)]; const availableWidths = GetAvailableWidths( 0, container.parent.properties.width, - container.parent.children, + children, container, isHorizontal ); diff --git a/src/Components/Editor/Behaviors/SwapBehaviors.ts b/src/Components/Editor/Behaviors/SwapBehaviors.ts index 2a81f56..ebcbfc7 100644 --- a/src/Components/Editor/Behaviors/SwapBehaviors.ts +++ b/src/Components/Editor/Behaviors/SwapBehaviors.ts @@ -5,9 +5,13 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { Orientation } from '../../../Enums/Orientation'; import { GetHorizontallyOverlappingContainers, GetVerticallyOverlappingContainers } from './AnchorBehaviors'; +import { MakeChildrenIterator } from '../../../utils/itertools'; -export function ApplySwap(container: IContainerModel, parent: IContainerModel): void { - const children = parent.children; +export function ApplySwap( + containers: Map, + container: IContainerModel, + parent: IContainerModel): void { + const children = [...MakeChildrenIterator(containers, parent.children)]; const isVertical = parent.properties.orientation === Orientation.Vertical; if (isVertical) { diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 1ae9d4c..ad80274 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -274,7 +274,7 @@ export function Editor(props: IEditorProps): JSX.Element { // Render const configuration = props.configuration; const current = GetCurrentHistoryState(history, historyCurrentStep); - const selected = FindContainerById(current.mainContainer, current.selectedContainerId); + const selected = FindContainerById(current.containers, current.selectedContainerId); return (
diff --git a/src/Components/ElementsList/ElementsList.tsx b/src/Components/ElementsList/ElementsList.tsx index fd66880..a38400e 100644 --- a/src/Components/ElementsList/ElementsList.tsx +++ b/src/Components/ElementsList/ElementsList.tsx @@ -9,6 +9,7 @@ import { PropertyType } from '../../Enums/PropertyType'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; interface IElementsListProps { + containers: Map mainContainer: IContainerModel symbols: Map selectedContainer: IContainerModel | undefined @@ -59,6 +60,7 @@ function HandleDragOver( function HandleOnDrop( event: React.DragEvent, + containers: Map, mainContainer: IContainerModel, addContainer: (index: number, type: string, parent: string) => void ): void { @@ -68,7 +70,7 @@ function HandleOnDrop( RemoveBorderClasses(target); const targetContainer: IContainerModel | undefined = FindContainerById( - mainContainer, + containers, target.id ); @@ -95,7 +97,7 @@ function HandleOnDrop( // locate the hitboxes if (y < 12) { - const index = targetContainer.parent.children.indexOf(targetContainer); + const index = targetContainer.parent.children.indexOf(targetContainer.properties.id); addContainer( index, type, @@ -107,7 +109,7 @@ function HandleOnDrop( type, targetContainer.properties.id); } else { - const index = targetContainer.parent.children.indexOf(targetContainer); + const index = targetContainer.parent.children.indexOf(targetContainer.properties.id); addContainer( index + 1, type, @@ -119,10 +121,10 @@ function HandleOnDrop( export function ElementsList(props: IElementsListProps): JSX.Element { // States const divRef = React.useRef(null); - const [width, height] = useSize(divRef); + const [, height] = useSize(divRef); // Render - const it = MakeRecursionDFSIterator(props.mainContainer, 0, [0, 0], true); + const it = MakeRecursionDFSIterator(props.mainContainer, props.containers, 0, [0, 0], true); const containers = [...it]; function Row({ index, style @@ -154,7 +156,7 @@ export function ElementsList(props: IElementsListProps): JSX.Element { style={style} title={container.properties.warning} onClick={() => props.selectContainer(container.properties.id)} - onDrop={(event) => HandleOnDrop(event, props.mainContainer, props.addContainer)} + onDrop={(event) => HandleOnDrop(event, props.containers, props.mainContainer, props.addContainer)} onDragOver={(event) => HandleDragOver(event, props.mainContainer)} onDragLeave={(event) => HandleDragLeave(event)} > diff --git a/src/Components/ElementsSidebar/Elements.tsx b/src/Components/ElementsSidebar/Elements.tsx deleted file mode 100644 index 6737ff0..0000000 --- a/src/Components/ElementsSidebar/Elements.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import * as React from 'react'; -import { FixedSizeList as List } from 'react-window'; -import { Properties } from '../ContainerProperties/ContainerProperties'; -import { IContainerModel } from '../../Interfaces/IContainerModel'; -import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; -import { ISymbolModel } from '../../Interfaces/ISymbolModel'; -import { PropertyType } from '../../Enums/PropertyType'; -import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; - -interface IElementsProps { - mainContainer: IContainerModel - symbols: Map - selectedContainer: IContainerModel | undefined - onPropertyChange: ( - key: string, - value: string | number | boolean | number[], - type?: PropertyType - ) => void - selectContainer: (containerId: string) => void - addContainer: (index: number, type: string, parent: string) => void -} - -function RemoveBorderClasses(target: HTMLButtonElement, exception: string = ''): void { - const bordersClasses = ['border-t-8', 'border-8', 'border-b-8'].filter(className => className !== exception); - target.classList.remove(...bordersClasses); -} - -function HandleDragLeave(event: React.DragEvent): void { - const target: HTMLButtonElement = event.target as HTMLButtonElement; - RemoveBorderClasses(target); -} - -function HandleDragOver( - event: React.DragEvent, - mainContainer: IContainerModel -): void { - event.preventDefault(); - const target: HTMLButtonElement = event.target as HTMLButtonElement; - const rect = target.getBoundingClientRect(); - const y = event.clientY - rect.top; // y position within the element. - - if (target.id === mainContainer.properties.id) { - target.classList.add('border-8'); - return; - } - - if (y < 12) { - RemoveBorderClasses(target, 'border-t-8'); - target.classList.add('border-t-8'); - } else if (y < 24) { - RemoveBorderClasses(target, 'border-8'); - target.classList.add('border-8'); - } else { - RemoveBorderClasses(target, 'border-b-8'); - target.classList.add('border-b-8'); - } -} - -function HandleOnDrop( - event: React.DragEvent, - mainContainer: IContainerModel, - addContainer: (index: number, type: string, parent: string) => 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 - ); - } -} - -export function Elements(props: IElementsProps): JSX.Element { - // Render - const it = MakeRecursionDFSIterator(props.mainContainer, 0, [0, 0], true); - const containers = [...it]; - function Row({ - index, style - }: { - index: number - style: React.CSSProperties - }): JSX.Element { - const { container, depth } = containers[index]; - const key = container.properties.id.toString(); - const tabs = '|\t'.repeat(depth); - const text = container.properties.displayedText === key - ? `${key}` - : `${container.properties.displayedText}`; - - const isSelected = props.selectedContainer !== undefined && - props.selectedContainer !== null && - props.selectedContainer.properties.id === container.properties.id; - - const selectedClass: string = isSelected - ? 'border-l-4 bg-blue-500 shadow-lg shadow-blue-500/60 hover:bg-blue-600 hover:shadow-blue-500 text-slate-50' - : 'bg-slate-300/60 hover:bg-slate-400 hover:shadow-slate-400'; - - return ( - - ); - } - - return ( - <> - - {Row} - - - - ); -} diff --git a/src/Components/MessagesSidebar/Messages.tsx b/src/Components/MessagesSidebar/Messages.tsx index 82b6c97..8422a36 100644 --- a/src/Components/MessagesSidebar/Messages.tsx +++ b/src/Components/MessagesSidebar/Messages.tsx @@ -1,6 +1,6 @@ -import { TrashIcon } from '@heroicons/react/24/outline'; import * as React from 'react'; import { FixedSizeList as List } from 'react-window'; +import { API_GET_FEEDBACK_URL } from '../../../public/svgld-settings'; import { MessageType } from '../../Enums/MessageType'; import { IGetFeedbackRequest } from '../../Interfaces/IGetFeedbackRequest'; import { IGetFeedbackResponse } from '../../Interfaces/IGetFeedbackResponse'; @@ -25,7 +25,7 @@ function UseWorker( // use webworker for the stringify to avoid freezing myWorker.postMessage({ state, - url: import.meta.env.VITE_API_GET_FEEDBACK_URL + url: API_GET_FEEDBACK_URL }); return () => { @@ -49,7 +49,7 @@ function UseAsync( ApplicationState: state }; const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure()); - fetch(import.meta.env.VITE_API_GET_FEEDBACK_URL, { + fetch(API_GET_FEEDBACK_URL, { method: 'POST', headers: new Headers({ // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index 8230c03..383ddb3 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -4,8 +4,10 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerProperties } from '../../../Interfaces/IContainerProperties'; import { Camelize } from '../../../utils/stringtools'; import { SHOW_TEXT } from '../../../utils/default'; +import { FindContainerById } from '../../../utils/itertools'; interface IContainerProps { + containers: Map model: IContainerModel depth: number scale: number @@ -18,13 +20,22 @@ interface IContainerProps { */ export function Container(props: IContainerProps): JSX.Element { const containersElements = props.model.children.map( - child => ); + childId => { + const child = FindContainerById(props.containers, childId); + + if (child === undefined) { + return <>; + } + + return ; + }); const width: number = props.model.properties.width; const height: number = props.model.properties.height; diff --git a/src/Components/SVG/Elements/DepthDimensionLayer.tsx b/src/Components/SVG/Elements/DepthDimensionLayer.tsx index 6f0eddb..0c3a36e 100644 --- a/src/Components/SVG/Elements/DepthDimensionLayer.tsx +++ b/src/Components/SVG/Elements/DepthDimensionLayer.tsx @@ -1,17 +1,22 @@ import * as React from 'react'; -import { ContainerModel } from '../../../Interfaces/IContainerModel'; +import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel'; import { DIMENSION_MARGIN } from '../../../utils/default'; import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools'; import { TransformX } from '../../../utils/svg'; import { Dimension } from './Dimension'; interface IDimensionLayerProps { + containers: Map roots: ContainerModel | ContainerModel[] | null scale?: number } -function GetDimensionsNodes(root: ContainerModel, scale: number): React.ReactNode[] { - const it = MakeBFSIterator(root); +function GetDimensionsNodes( + containers: Map, + root: ContainerModel, + scale: number +): React.ReactNode[] { + const it = MakeBFSIterator(root, containers); const dimensions: React.ReactNode[] = []; let currentDepth = 0; let min = Infinity; @@ -53,10 +58,10 @@ export function DepthDimensionLayer(props: IDimensionLayerProps): JSX.Element { const scale = props.scale ?? 1; if (Array.isArray(props.roots)) { props.roots.forEach(child => { - dimensions.concat(GetDimensionsNodes(child, scale)); + dimensions.concat(GetDimensionsNodes(props.containers, child, scale)); }); } else if (props.roots !== null) { - dimensions = GetDimensionsNodes(props.roots, scale); + dimensions = GetDimensionsNodes(props.containers, props.roots, scale); } return ( diff --git a/src/Components/SVG/Elements/DimensionLayer.tsx b/src/Components/SVG/Elements/DimensionLayer.tsx index 6c89138..7fe451f 100644 --- a/src/Components/SVG/Elements/DimensionLayer.tsx +++ b/src/Components/SVG/Elements/DimensionLayer.tsx @@ -3,11 +3,12 @@ import { Orientation } from '../../../Enums/Orientation'; import { Position } from '../../../Enums/Position'; import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel'; import { DIMENSION_MARGIN, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, SHOW_SELF_DIMENSIONS } from '../../../utils/default'; -import { MakeRecursionDFSIterator, Pairwise } from '../../../utils/itertools'; +import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../../utils/itertools'; import { TransformX, TransformY } from '../../../utils/svg'; import { Dimension } from './Dimension'; interface IDimensionLayerProps { + containers: Map root: ContainerModel scale: number } @@ -51,8 +52,8 @@ function ActionByPosition( * @param param0 Object with the root container and the scale of the svg * @returns A list of dimensions */ -function Dimensions({ root, scale }: IDimensionLayerProps): React.ReactNode[] { - const it = MakeRecursionDFSIterator(root, 0, [0, 0]); +function Dimensions({ containers, root, scale }: IDimensionLayerProps): React.ReactNode[] { + const it = MakeRecursionDFSIterator(root, containers, 0, [0, 0]); const dimensions: React.ReactNode[] = []; const topDim = root.properties.y; const leftDim = root.properties.x; @@ -75,7 +76,8 @@ function Dimensions({ root, scale }: IDimensionLayerProps): React.ReactNode[] { container.properties.showSelfDimensions, AddHorizontalSelfDimension, AddVerticalSelfDimension, - [container, + [ + container, currentTransform, dimensions, scale] @@ -88,7 +90,9 @@ function Dimensions({ root, scale }: IDimensionLayerProps): React.ReactNode[] { container.properties.showDimensionWithMarks, AddHorizontalBorrowerDimension, AddVerticalBorrowerDimension, - [container, + [ + containers, + container, depth, currentTransform, dimensions, @@ -102,7 +106,9 @@ function Dimensions({ root, scale }: IDimensionLayerProps): React.ReactNode[] { container.properties.showChildrenDimensions, AddHorizontalChildrenDimension, AddVerticalChildrenDimension, - [container, + [ + containers, + container, currentTransform, dimensions, scale] @@ -130,6 +136,7 @@ export function DimensionLayer(props: IDimensionLayerProps): JSX.Element { function AddHorizontalChildrenDimension( yDim: number, + containers: Map, container: IContainerModel, currentTransform: [number, number], dimensions: React.ReactNode[], @@ -137,13 +144,25 @@ function AddHorizontalChildrenDimension( ): void { const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`; - const lastChild = container.children[container.children.length - 1]; + const lastChildId = container.children[container.children.length - 1]; + const lastChild = FindContainerById(containers, lastChildId); + + if (lastChild === undefined) { + return; + } + let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); // Find the min and max for (let i = container.children.length - 2; i >= 0; i--) { - const child = container.children[i]; + const childId = container.children[i]; + const child = FindContainerById(containers, childId); + + if (child === undefined) { + continue; + } + const left = TransformX(child.properties.x, child.properties.width, child.properties.positionReference); if (left < xChildrenStart) { xChildrenStart = left; @@ -178,6 +197,7 @@ function AddHorizontalChildrenDimension( function AddVerticalChildrenDimension( xDim: number, + containers: Map, container: IContainerModel, currentTransform: [number, number], dimensions: React.ReactNode[], @@ -185,13 +205,25 @@ function AddVerticalChildrenDimension( ): void { const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; - const lastChild = container.children[container.children.length - 1]; + const lastChildId = container.children[container.children.length - 1]; + const lastChild = FindContainerById(containers, lastChildId); + + if (lastChild === undefined) { + return; + } + let yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference); let yChildrenEnd = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference); // Find the min and max for (let i = container.children.length - 2; i >= 0; i--) { - const child = container.children[i]; + const childId = container.children[i]; + const child = FindContainerById(containers, childId); + + if (child === undefined) { + continue; + } + const top = TransformY(child.properties.y, child.properties.height, child.properties.positionReference); if (top < yChildrenStart) { yChildrenStart = top; @@ -227,13 +259,14 @@ function AddVerticalChildrenDimension( function AddHorizontalBorrowerDimension( yDim: number, + containers: Map, container: IContainerModel, depth: number, currentTransform: [number, number], dimensions: React.ReactNode[], scale: number ): void { - const it = MakeRecursionDFSIterator(container, depth, currentTransform); + const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const marks = []; // list of vertical lines for the dimension for (const { container: childContainer, currentTransform: childCurrentTransform @@ -279,13 +312,14 @@ function AddHorizontalBorrowerDimension( function AddVerticalBorrowerDimension( xDim: number, + containers: Map, container: IContainerModel, depth: number, currentTransform: [number, number], dimensions: React.ReactNode[], scale: number ): void { - const it = MakeRecursionDFSIterator(container, depth, currentTransform); + const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const marks = []; // list of vertical lines for the dimension for (const { container: childContainer, currentTransform: childCurrentTransform diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 20d6ef1..92bb7fa 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { ReactSVGPanZoom, Tool, TOOL_PAN, Value } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; -import { ContainerModel } from '../../Interfaces/IContainerModel'; +import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { Selector } from './Elements/Selector/Selector'; import { DepthDimensionLayer } from './Elements/DepthDimensionLayer'; import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default'; @@ -15,17 +15,13 @@ interface ISVGProps { viewerHeight: number width: number height: number + containers: Map children: ContainerModel selected?: ContainerModel symbols: Map selectContainer: (containerId: string) => void } -interface Viewer { - viewerWidth: number - viewerHeight: number -} - export const ID = 'svg'; export function SVG(props: ISVGProps): JSX.Element { @@ -55,6 +51,7 @@ export function SVG(props: ISVGProps): JSX.Element { let children: React.ReactNode | React.ReactNode[] = []; children = {children} {SHOW_DIMENSIONS_PER_DEPTH - ? + ? : null} - + {/* leave this at the end so it can be removed during the svg export */} diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 437b470..988dca2 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -18,6 +18,7 @@ import { Settings } from '../Settings/Settings'; import { IMessage } from '../../Interfaces/IMessage'; import { DISABLE_API } from '../../utils/default'; import { UseWorker, UseAsync } from './UseWorker'; +import { FindContainerById } from '../../utils/itertools'; export interface IUIProps { selectedContainer: IContainerModel | undefined @@ -89,6 +90,12 @@ export function UI(props: IUIProps): JSX.Element { let leftChildren: JSX.Element = (<>); let rightChildren: JSX.Element = (<>); + const mainContainer = FindContainerById(props.current.containers, props.current.mainContainer) + + if (mainContainer === undefined) { + throw new Error('Tried to initialized UI but there is no main container!'); + } + switch (selectedSidebar) { case SidebarType.Components: leftSidebarTitle = 'Components'; @@ -100,7 +107,8 @@ export function UI(props: IUIProps): JSX.Element { />; rightSidebarTitle = 'Elements'; rightChildren = { @@ -37,7 +38,7 @@ export function UseAsync( ApplicationState: state }; const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure()); - fetch(import.meta.env.VITE_API_GET_FEEDBACK_URL, { + fetch(API_GET_FEEDBACK_URL, { method: 'POST', headers: new Headers({ // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/src/Components/Viewer/Viewer.tsx b/src/Components/Viewer/Viewer.tsx index 7c23a29..c37d3c6 100644 --- a/src/Components/Viewer/Viewer.tsx +++ b/src/Components/Viewer/Viewer.tsx @@ -3,7 +3,7 @@ import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IPoint } from '../../Interfaces/IPoint'; import { DIMENSION_MARGIN, USE_EXPERIMENTAL_CANVAS_API } from '../../utils/default'; -import { MakeRecursionDFSIterator } from '../../utils/itertools'; +import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; import { BAR_WIDTH } from '../Bar/Bar'; import { Canvas } from '../Canvas/Canvas'; import { AddDimensions } from '../Canvas/DimensionLayer'; @@ -96,22 +96,32 @@ export function Viewer({ viewerHeight: window.innerHeight }); + const mainContainer = FindContainerById(current.containers, current.mainContainer); + + if (mainContainer === undefined) { + return <>; + } + UseSVGAutoResizerOnWindowResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer); UseSVGAutoResizerOnSidebar(isLeftSidebarOpen, isRightSidebarOpen, setViewer); if (USE_EXPERIMENTAL_CANVAS_API) { function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void { - const topDim = current.mainContainer.properties.y; - const leftDim = current.mainContainer.properties.x; - const rightDim = current.mainContainer.properties.x + current.mainContainer.properties.width; - const bottomDim = current.mainContainer.properties.y + current.mainContainer.properties.height; + if (mainContainer === undefined) { + return; + } + + const topDim = mainContainer.properties.y; + const leftDim = mainContainer.properties.x; + const rightDim = mainContainer.properties.x + mainContainer.properties.width; + const bottomDim = mainContainer.properties.y + mainContainer.properties.height; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.save(); ctx.setTransform(scale, 0, 0, scale, translatePos.x, translatePos.y); ctx.fillStyle = '#000000'; - const it = MakeRecursionDFSIterator(current.mainContainer, 0, [0, 0]); + const it = MakeRecursionDFSIterator(mainContainer, current.containers, 0, [0, 0]); for (const { container, depth, currentTransform } of it) { const [x, y] = [ container.properties.x + currentTransform[0], @@ -130,6 +140,7 @@ export function Viewer({ rightDim, depth, scale, + current.containers, container, currentTransform ); @@ -160,13 +171,14 @@ export function Viewer({ className={marginClasses} viewerWidth={viewer.viewerWidth} viewerHeight={viewer.viewerHeight} - width={current.mainContainer?.properties.width} - height={current.mainContainer?.properties.height} + width={mainContainer.properties.width} + height={mainContainer.properties.height} + containers={current.containers} selected={selectedContainer} symbols={current.symbols} selectContainer={selectContainer} > - {current.mainContainer} + {mainContainer} ); } @@ -188,6 +200,7 @@ function RenderDimensions( rightDim: number, depth: number, scale: number, + containers: Map, container: IContainerModel, currentTransform: [number, number] ): void { @@ -197,7 +210,7 @@ function RenderDimensions( const containerBottomDim = bottomDim + (DIMENSION_MARGIN * (depth + 1)) / scale; const containerRightDim = rightDim + (DIMENSION_MARGIN * (depth + 1)) / scale; const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim]; - AddDimensions(ctx, container, dimMapped, currentTransform, scale, depth); + AddDimensions(ctx, containers, container, dimMapped, currentTransform, scale, depth); ctx.restore(); } diff --git a/src/Events/EditorEvents.ts b/src/Events/EditorEvents.ts index 91b7d2b..9006e92 100644 --- a/src/Events/EditorEvents.ts +++ b/src/Events/EditorEvents.ts @@ -174,7 +174,7 @@ function AppendContainer(root: Element | Document, const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep); const currentState = history[editorState.historyCurrentStep]; - const parent = FindContainerById(currentState.mainContainer, parentId); + const parent = FindContainerById(currentState.containers, parentId); const newHistory = AddContainerAction( parent?.children.length ?? 0, @@ -203,7 +203,7 @@ function AppendContainerToSelectedContainer(root: Element | Document, const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep); const currentState = history[editorState.historyCurrentStep]; - const selected = FindContainerById(currentState.mainContainer, currentState.selectedContainerId); + const selected = FindContainerById(currentState.containers, currentState.selectedContainerId); const newHistory = AddContainerToSelectedContainerAction( type, diff --git a/src/Interfaces/IContainerModel.ts b/src/Interfaces/IContainerModel.ts index 5922ebe..ff24d67 100644 --- a/src/Interfaces/IContainerModel.ts +++ b/src/Interfaces/IContainerModel.ts @@ -1,7 +1,7 @@ import { IContainerProperties } from './IContainerProperties'; export interface IContainerModel { - children: IContainerModel[] + children: string[] parent: IContainerModel | null properties: IContainerProperties userData: Record @@ -12,7 +12,7 @@ export interface IContainerModel { * Do not add methods since they will be lost during serialization */ export class ContainerModel implements IContainerModel { - public children: IContainerModel[]; + public children: string[]; public parent: IContainerModel | null; public properties: IContainerProperties; public userData: Record; @@ -20,7 +20,7 @@ export class ContainerModel implements IContainerModel { constructor( parent: IContainerModel | null, properties: IContainerProperties, - children: IContainerModel[] = [], + children: string[] = [], userData = {}) { this.parent = parent; this.properties = properties; diff --git a/src/Interfaces/IHistoryState.ts b/src/Interfaces/IHistoryState.ts index 3576c2a..e5f0428 100644 --- a/src/Interfaces/IHistoryState.ts +++ b/src/Interfaces/IHistoryState.ts @@ -6,11 +6,12 @@ export interface IHistoryState { lastAction: string /** Reference to the main container */ - mainContainer: IContainerModel + mainContainer: string // TODO: Add hashmap to optimize FincContainerById from worst O(n) to O(1) // TODO: this hashmap will not be serialized, modify it in the replacer and reviver in saveload.ts + worker.js // TODO: Update addContainers and deleteContainer to update the hashmap + containers: Map /** Id of the selected container */ selectedContainerId: string diff --git a/src/utils/default.ts b/src/utils/default.ts index b5c2f05..1264b95 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -124,6 +124,8 @@ export function GetDefaultEditorState(configuration: IConfiguration): IEditorSta null, mainContainerConfig ); + const containers = new Map(); + containers.set(mainContainer.properties.id, mainContainer); const typeCounters = {}; (typeCounters as any)[mainContainer.properties.type] = 0; @@ -133,7 +135,8 @@ export function GetDefaultEditorState(configuration: IConfiguration): IEditorSta history: [ { lastAction: '', - mainContainer, + mainContainer: mainContainer.properties.id, + containers, selectedContainerId: mainContainer.properties.id, typeCounters, symbols: new Map(), @@ -175,6 +178,13 @@ export const DEFAULT_CONFIG: IConfiguration = { /* eslint-enable */ }; +const DEFAULT_CONTAINER_STYLE = { + stroke: 'black', + fillOpacity: 1, + fill: 'white', + strokeWidth: 2 +} + /** * Default Main container properties */ @@ -203,10 +213,7 @@ export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = { showDimensionWithMarks: [Position.Down, Position.Right], markPosition: [], warning: '', - style: { - stroke: 'black', - fillOpacity: 0 - } + style: DEFAULT_CONTAINER_STYLE }; /** @@ -256,7 +263,7 @@ export function GetDefaultContainerProps(type: string, showDimensionWithMarks: containerConfig.ShowDimensionWithMarks ?? [], warning: '', customSVG: containerConfig.CustomSVG, - style: structuredClone(containerConfig.Style), + style: Object.assign(structuredClone(DEFAULT_CONTAINER_STYLE), structuredClone(containerConfig.Style)), userData: structuredClone(containerConfig.UserData) }); } diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts index d7b8f6c..0db5812 100644 --- a/src/utils/itertools.ts +++ b/src/utils/itertools.ts @@ -1,9 +1,22 @@ import { IContainerModel } from '../Interfaces/IContainerModel'; +export function * MakeChildrenIterator(containers: Map, childrenIds: string[]): Generator { + for (const childId of childrenIds) { + const child = FindContainerById(containers, childId); + + if (child === undefined) { + return; + } + + yield child; + }; +} + + /** * Returns a Generator iterating of over the children depth-first */ -export function * MakeDFSIterator(root: IContainerModel, enableHideChildrenInTreeview = false): Generator { +export function * MakeDFSIterator(root: IContainerModel, containers: Map, enableHideChildrenInTreeview = false): Generator { const queue: IContainerModel[] = [root]; const visited = new Set(queue); while (queue.length > 0) { @@ -16,8 +29,9 @@ export function * MakeDFSIterator(root: IContainerModel, enableHideChildrenInTre } for (let i = container.children.length - 1; i >= 0; i--) { - const child = container.children[i]; - if (visited.has(child)) { + const childId = container.children[i]; + const child = FindContainerById(containers, childId); + if (child === undefined || visited.has(child)) { continue; } visited.add(child); @@ -38,7 +52,7 @@ export interface ContainerAndDepthAndTransform extends ContainerAndDepth { /** * Returns a Generator iterating of over the children depth-first */ -export function * MakeBFSIterator(root: IContainerModel): Generator { +export function * MakeBFSIterator(root: IContainerModel, containers: Map): Generator { const queue: IContainerModel[] = [root]; let depth = 0; while (queue.length > 0) { @@ -51,7 +65,13 @@ export function * MakeBFSIterator(root: IContainerModel): Generator= 0; i--) { - const child = container.children[i]; + const childId = container.children[i]; + const child = FindContainerById(containers, childId); + + if (child === undefined) { + continue; + } + queue.push(child); } } @@ -61,6 +81,7 @@ export function * MakeBFSIterator(root: IContainerModel): Generator, depth: number, currentTransform: [number, number], enableHideChildrenInTreeview: boolean = false @@ -79,9 +100,16 @@ export function * MakeRecursionDFSIterator( return; } - for (const container of root.children) { + for (const containerId of root.children) { + const container = FindContainerById(containers, containerId); + + if (container === undefined) { + continue; + } + yield * MakeRecursionDFSIterator( container, + containers, depth + 1, [ currentTransform[0] + root.properties.x, @@ -185,8 +213,15 @@ export function ApplyParentTransform( return [x, y]; } -export function FindContainerById(root: IContainerModel, id: string): IContainerModel | undefined { - const it = MakeDFSIterator(root); +/** + * Returns the container by id + * @deprecated Please use FindContainerById + * @param root Root of the container tree + * @param id Id of the container to find + * @returns The container found or undefined if not found + */ +export function FindContainerByIdDFS(root: IContainerModel, containers: Map, id: string): IContainerModel | undefined { + const it = MakeDFSIterator(root, containers); for (const container of it) { if (container.properties.id === id) { return container; @@ -195,6 +230,17 @@ export function FindContainerById(root: IContainerModel, id: string): IContainer return undefined; } +/** + * Returns the container by id + * For now, does the same as containers.get(id) + * @param containers Map of containers + * @param id id of container + * @returns Container by id + */ +export function FindContainerById(containers: Map, id: string): IContainerModel | undefined { + return containers.get(id); +} + export interface IPair { cur: T next: T diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts index e546893..5d7681f 100644 --- a/src/utils/saveload.ts +++ b/src/utils/saveload.ts @@ -32,15 +32,22 @@ export function ReviveState(state: IHistoryState): void { for (const symbol of state.symbols.values()) { symbol.linkedContainers = new Set(symbol.linkedContainers); } + state.containers = new Map(state.containers); - const it = MakeDFSIterator(state.mainContainer); + const root = FindContainerById(state.containers, state.mainContainer); + + if (root === undefined) { + return; + } + + const it = MakeDFSIterator(root, state.containers); for (const container of it) { const parentId = container.properties.parentId; if (parentId === null) { container.parent = null; continue; } - const parent = FindContainerById(state.mainContainer, parentId); + const parent = FindContainerById(state.containers, parentId); if (parent === undefined) { continue; } @@ -53,6 +60,10 @@ export function GetCircularReplacer(): (key: any, value: object | Map).entries()); + } if (key === 'symbols') { return Array.from((value as Map).entries()); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 3fc526e..5119518 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,9 +1,6 @@ /// interface ImportMetaEnv { - readonly VITE_API_FETCH_URL: string - readonly VITE_API_SET_CONTAINER_LIST_URL: string - readonly VITE_API_GET_FEEDBACK_URL: string // more env variables... } diff --git a/tsconfig.json b/tsconfig.json index 4b64a19..76cee5e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["src"], + "include": ["src", "public/svgld-settings.d.ts", "public/svgld-settings.ts"], "exclude": ["test-server"], "references": [{ "path": "./tsconfig.node.json" }] }