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
This commit is contained in:
Eric Nguyen 2022-10-12 09:39:54 +00:00
parent 466ef2b08b
commit c256a76e01
45 changed files with 775 additions and 450 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -13,6 +13,10 @@ pool:
variables: variables:
pnpm_config_cache: $(Pipeline.Workspace)/.pnpm-store 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: steps:
- task: Cache@2 - task: Cache@2
@ -47,7 +51,7 @@ steps:
set -euo pipefail set -euo pipefail
node --version node --version
node ./test-server/http.js & node ./test-server/http.js &
dotnet run --project=./csharp/SVGLDLibs/SVGLDWebAPI/SVGLDWebAPI.csproj & dotnet run --project=$(CSWebProjectLocation) &
jobs jobs
sleep 10 sleep 10
pnpm i pnpm i
@ -58,3 +62,12 @@ steps:
- publish: $(System.DefaultWorkingDirectory)/dist - publish: $(System.DefaultWorkingDirectory)/dist
artifact: svg-layout-designer 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

View file

@ -9,7 +9,10 @@ namespace SVGLDLibs.Models
public string lastAction; public string lastAction;
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public ContainerModel mainContainer; public string mainContainer;
[DataMember(EmitDefaultValue = false)]
public Dictionary<string, ContainerModel> containers;
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string selectedContainerId; public string selectedContainerId;

View file

@ -6,7 +6,7 @@ namespace SVGLDLibs.Models
public class ContainerModel public class ContainerModel
{ {
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public List<ContainerModel> children; public List<string> children;
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public ContainerProperties properties; public ContainerProperties properties;

217
docs/DataStructure.md Normal file
View file

@ -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<string, Conteneur>();
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<string, Conteneur>,
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.

View file

@ -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. 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 : 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 - `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`
``` ```js
VITE_API_FETCH_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration export const API_FETCH_URL = 'http://localhost:5000';
VITE_API_SET_CONTAINER_LIST_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/SetContainerList export const API_SET_CONTAINER_LIST_URL = 'http://localhost:5000/SetContainerList';
VITE_API_GET_FEEDBACK_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetFeedback export const API_GET_FEEDBACK_URL = 'http://localhost:5000/GetFeedback';
``` ```
Vous pouvez modifiez `src/utils/default.ts` mais ne le committez pas. Vous pouvez modifiez `src/utils/default.ts` mais ne le committez pas.

View file

@ -9,6 +9,7 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/svgld-settings.js"></script>
<script type="module" src="./src/main.tsx"></script> <script type="module" src="./src/main.tsx"></script>
</body> </body>
</html> </html>

4
public/svgld-settings.d.ts vendored Normal file
View file

@ -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;

3
public/svgld-settings.js Normal file
View file

@ -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';

View file

@ -9,6 +9,10 @@ const getCircularReplacer = () => {
return; return;
} }
if (key === 'containers') {
return Array.from(value.entries());
}
if (key === 'symbols') { if (key === 'symbols') {
return Array.from(value.entries()); return Array.from(value.entries());
} }

View file

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { API_FETCH_URL } from '../../../public/svgld-settings';
import { AddMethod } from '../../Enums/AddMethod'; import { AddMethod } from '../../Enums/AddMethod';
import { Orientation } from '../../Enums/Orientation'; import { Orientation } from '../../Enums/Orientation';
import { Position } from '../../Enums/Position'; import { Position } from '../../Enums/Position';
@ -10,7 +11,6 @@ import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
import { ICategory } from '../../Interfaces/ICategory'; import { ICategory } from '../../Interfaces/ICategory';
import { IConfiguration } from '../../Interfaces/IConfiguration'; import { IConfiguration } from '../../Interfaces/IConfiguration';
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
import { IEditorState } from '../../Interfaces/IEditorState';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IPattern } from '../../Interfaces/IPattern'; import { IPattern } from '../../Interfaces/IPattern';
import { DEFAULT_MAINCONTAINER_PROPS, GetDefaultContainerProps } from '../../utils/default'; 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 + '/'; 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/ // 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', () => { it('Load environment', () => {
const url = import.meta.env.VITE_API_FETCH_URL; const url = API_FETCH_URL;
expect(url).toBe('http://localhost:5000'); expect(url).toBe('http://localhost:5000');
}); });
@ -103,9 +103,11 @@ describe.concurrent('Models test suite', () => {
DEFAULT_MAINCONTAINER_PROPS DEFAULT_MAINCONTAINER_PROPS
); );
const containers = new Map<string, IContainerModel>();
const historyState: IHistoryState = { const historyState: IHistoryState = {
lastAction: 'string', lastAction: 'string',
mainContainer, mainContainer: mainContainer.properties.id,
containers,
selectedContainerId: '3', selectedContainerId: '3',
typeCounters: { typeCounters: {
main: 1 main: 1

View file

@ -1,3 +1,4 @@
import { API_FETCH_URL, API_SET_CONTAINER_LIST_URL } from '../../../public/svgld-settings';
import { IConfiguration } from '../../Interfaces/IConfiguration'; import { IConfiguration } from '../../Interfaces/IConfiguration';
import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest'; import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest';
import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse'; import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse';
@ -8,7 +9,7 @@ import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload';
* @returns {Configation} The model of the configuration for the application * @returns {Configation} The model of the configuration for the application
*/ */
export async function FetchConfiguration(): Promise<IConfiguration> { export async function FetchConfiguration(): Promise<IConfiguration> {
const url = import.meta.env.VITE_API_FETCH_URL; const url = API_FETCH_URL;
// The test library cannot use the Fetch API // The test library cannot use the Fetch API
// @ts-expect-error // @ts-expect-error
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
@ -34,7 +35,7 @@ export async function FetchConfiguration(): Promise<IConfiguration> {
} }
export async function SetContainerList(request: ISetContainerListRequest): Promise<ISetContainerListResponse> { export async function SetContainerList(request: ISetContainerListRequest): Promise<ISetContainerListResponse> {
const url = import.meta.env.VITE_API_SET_CONTAINER_LIST_URL; const url = API_SET_CONTAINER_LIST_URL;
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure()); const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
// The test library cannot use the Fetch API // The test library cannot use the Fetch API
// @ts-expect-error // @ts-expect-error

View file

@ -1,7 +1,7 @@
import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
import { events as EVENTS } from '../../Events/AppEvents'; import { events as EVENTS } from '../../Events/AppEvents';
import { MainMenu } from '../MainMenu/MainMenu'; import { MainMenu } from '../MainMenu/MainMenu';
import { ContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
import { Editor } from '../Editor/Editor'; import { Editor } from '../Editor/Editor';
import { IEditorState } from '../../Interfaces/IEditorState'; import { IEditorState } from '../../Interfaces/IEditorState';
import { LoadState } from './Actions/Load'; import { LoadState } from './Actions/Load';
@ -81,12 +81,15 @@ export function App(props: IAppProps): JSX.Element {
null, null,
DEFAULT_MAINCONTAINER_PROPS DEFAULT_MAINCONTAINER_PROPS
); );
const containers = new Map<string, IContainerModel>();
containers.set(defaultMainContainer.properties.id, defaultMainContainer);
const [editorState, setEditorState] = useState<IEditorState>({ const [editorState, setEditorState] = useState<IEditorState>({
configuration: DEFAULT_CONFIG, configuration: DEFAULT_CONFIG,
history: [{ history: [{
lastAction: '', lastAction: '',
mainContainer: defaultMainContainer, mainContainer: defaultMainContainer.properties.id,
containers,
selectedContainerId: defaultMainContainer.properties.id, selectedContainerId: defaultMainContainer.properties.id,
typeCounters: {}, typeCounters: {},
symbols: new Map(), symbols: new Map(),

View file

@ -2,7 +2,7 @@ import { Orientation } from '../../Enums/Orientation';
import { Position } from '../../Enums/Position'; import { Position } from '../../Enums/Position';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel } from '../../Interfaces/IContainerModel';
import { SHOW_SELF_DIMENSIONS, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS } from '../../utils/default'; 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 { TransformX, TransformY } from '../../utils/svg';
import { RenderDimension } from './Dimension'; import { RenderDimension } from './Dimension';
@ -10,6 +10,7 @@ const MODULE_STROKE_WIDTH = 1;
export function AddDimensions( export function AddDimensions(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
dimMapped: number[], dimMapped: number[],
currentTransform: [number, number], currentTransform: [number, number],
@ -24,7 +25,8 @@ export function AddDimensions(
container.properties.showSelfDimensions, container.properties.showSelfDimensions,
AddHorizontalSelfDimension, AddHorizontalSelfDimension,
AddVerticalSelfDimension, AddVerticalSelfDimension,
[container, [
container,
currentTransform, currentTransform,
scale] scale]
); );
@ -37,7 +39,9 @@ export function AddDimensions(
container.properties.showDimensionWithMarks, container.properties.showDimensionWithMarks,
AddHorizontalBorrowerDimension, AddHorizontalBorrowerDimension,
AddVerticalBorrowerDimension, AddVerticalBorrowerDimension,
[container, [
containers,
container,
depth, depth,
currentTransform, currentTransform,
scale] scale]
@ -51,7 +55,9 @@ export function AddDimensions(
container.properties.showChildrenDimensions, container.properties.showChildrenDimensions,
AddHorizontalChildrenDimension, AddHorizontalChildrenDimension,
AddVerticalChildrenDimension, AddVerticalChildrenDimension,
[container, [
containers,
container,
currentTransform, currentTransform,
scale] scale]
); );
@ -94,19 +100,30 @@ function ActionByPosition(
function AddHorizontalChildrenDimension( function AddHorizontalChildrenDimension(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
yDim: number, yDim: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
scale: number scale: number
): void { ): void {
const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`; 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 xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference);
let xChildrenEnd = 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 // Find the min and max
for (let i = container.children.length - 2; i >= 0; i--) { 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); const left = TransformX(child.properties.x, child.properties.width, child.properties.positionReference);
if (left < xChildrenStart) { if (left < xChildrenStart) {
xChildrenStart = left; xChildrenStart = left;
@ -142,19 +159,32 @@ function AddHorizontalChildrenDimension(
function AddVerticalChildrenDimension( function AddVerticalChildrenDimension(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
xDim: number, xDim: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
scale: number scale: number
): void { ): void {
const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; 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 yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference);
let yChildrenEnd = 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 // Find the min and max
for (let i = container.children.length - 2; i >= 0; i--) { 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); const top = TransformY(child.properties.y, child.properties.height, child.properties.positionReference);
if (top < yChildrenStart) { if (top < yChildrenStart) {
yChildrenStart = top; yChildrenStart = top;
@ -191,12 +221,13 @@ function AddVerticalChildrenDimension(
function AddHorizontalBorrowerDimension( function AddHorizontalBorrowerDimension(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
yDim: number, yDim: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
depth: number, depth: number,
currentTransform: [number, number], currentTransform: [number, number],
scale: number scale: number
): void { ): void {
const it = MakeRecursionDFSIterator(container, depth, currentTransform); const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const marks = []; // list of vertical lines for the dimension const marks = []; // list of vertical lines for the dimension
for (const { for (const {
container: childContainer, currentTransform: childCurrentTransform container: childContainer, currentTransform: childCurrentTransform
@ -243,12 +274,13 @@ function AddHorizontalBorrowerDimension(
function AddVerticalBorrowerDimension( function AddVerticalBorrowerDimension(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
xDim: number, xDim: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
depth: number, depth: number,
currentTransform: [number, number], currentTransform: [number, number],
scale: number scale: number
): void { ): void {
const it = MakeRecursionDFSIterator(container, depth, currentTransform); const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const marks = []; // list of vertical lines for the dimension const marks = []; // list of vertical lines for the dimension
for (const { for (const {
container: childContainer, currentTransform: childCurrentTransform container: childContainer, currentTransform: childCurrentTransform

View file

@ -69,12 +69,11 @@ export function AddContainers(
const history = GetCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
// Deep clone the main container for the history const containers = structuredClone(current.containers);
const clone: IContainerModel = structuredClone(current.mainContainer);
// Find the parent in the clone // Find the parent in the clone
const parentClone: IContainerModel | undefined = FindContainerById( const parentClone: IContainerModel | undefined = FindContainerById(
clone, parentId containers, parentId
); );
if (parentClone === null || parentClone === undefined) { if (parentClone === null || parentClone === undefined) {
@ -90,14 +89,15 @@ export function AddContainers(
// Iterate over the containers // Iterate over the containers
availableContainers.forEach((availableContainer, typeIndex) => { availableContainers.forEach((availableContainer, typeIndex) => {
// Get the preset properties from the API // 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 // Update the state
history.push({ history.push({
lastAction: `Add [${containerIds.join(', ')}] in ${parentClone.properties.id}`, lastAction: `Add [${containerIds.join(', ')}] in ${parentClone.properties.id}`,
mainContainer: clone, mainContainer: current.mainContainer,
selectedContainerId: parentClone.properties.id, selectedContainerId: parentClone.properties.id,
containers,
typeCounters: newCounters, typeCounters: newCounters,
symbols: structuredClone(current.symbols), symbols: structuredClone(current.symbols),
selectedSymbolId: current.selectedSymbolId selectedSymbolId: current.selectedSymbolId
@ -109,6 +109,7 @@ export function AddContainers(
function AddNewContainerToParent( function AddNewContainerToParent(
availableContainer: IAvailableContainer, availableContainer: IAvailableContainer,
configuration: IConfiguration, configuration: IConfiguration,
containers: Map<string, IContainerModel>,
parentClone: IContainerModel, parentClone: IContainerModel,
index: number, index: number,
typeIndex: number, typeIndex: number,
@ -143,7 +144,7 @@ function AddNewContainerToParent(
({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right)); ({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right));
// Apply an add method (append or insert/replace) // 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 // Set the counter of the object type in order to assign an unique id
UpdateCounters(newCounters, type); 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 // Add it to the parent
if (index === parentClone.children.length) { if (index === parentClone.children.length) {
parentClone.children.push(newContainer); parentClone.children.push(newContainer.properties.id);
} else { } else {
parentClone.children.splice(index, 0, newContainer); parentClone.children.splice(index, 0, newContainer.properties.id);
} }
// Sort the parent children by x // Sort the parent children by x
SortChildren(parentClone); SortChildren(containers, parentClone);
/// Handle behaviors here /// /// Handle behaviors here ///
// Apply the behaviors (flex, rigid, anchor) // Apply the behaviors (flex, rigid, anchor)
ApplyBehaviors(newContainer, symbols); ApplyBehaviors(containers, newContainer, symbols);
// Then, apply the behaviors on its siblings (mostly for flex) // Then, apply the behaviors on its siblings (mostly for flex)
ApplyBehaviorsOnSiblingsChildren(newContainer, symbols); ApplyBehaviorsOnSiblingsChildren(containers, newContainer, symbols);
// Initialize default children of the container // Initialize default children of the container
if (initChilds) { if (initChilds) {
@ -194,6 +198,7 @@ function AddNewContainerToParent(
newContainer, newContainer,
configuration, configuration,
containerConfig, containerConfig,
containers,
newCounters, newCounters,
symbols symbols
); );
@ -201,6 +206,7 @@ function AddNewContainerToParent(
InitializeChildrenWithPattern( InitializeChildrenWithPattern(
newContainer, newContainer,
configuration, configuration,
containers,
containerConfig, containerConfig,
newCounters, newCounters,
symbols symbols
@ -258,6 +264,7 @@ function InitializeDefaultChild(
newContainer: ContainerModel, newContainer: ContainerModel,
configuration: IConfiguration, configuration: IConfiguration,
containerConfig: IAvailableContainer, containerConfig: IAvailableContainer,
containers: Map<string, IContainerModel>,
newCounters: Record<string, number>, newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
): void { ): void {
@ -276,6 +283,7 @@ function InitializeDefaultChild(
AddNewContainerToParent( AddNewContainerToParent(
currentConfig, currentConfig,
configuration, configuration,
containers,
parent, parent,
0, 0, 0, 0,
newCounters, newCounters,
@ -286,6 +294,7 @@ function InitializeDefaultChild(
function InitializeChildrenWithPattern( function InitializeChildrenWithPattern(
newContainer: ContainerModel, newContainer: ContainerModel,
configuration: IConfiguration, configuration: IConfiguration,
containers: Map<string, IContainerModel>,
containerConfig: IAvailableContainer, containerConfig: IAvailableContainer,
newCounters: Record<string, number>, newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
@ -307,6 +316,7 @@ function InitializeChildrenWithPattern(
AddNewContainerToParent( AddNewContainerToParent(
container, container,
configuration, configuration,
containers,
newContainer, newContainer,
0, 0, 0, 0,
newCounters, newCounters,
@ -334,6 +344,7 @@ function InitializeChildrenWithPattern(
AddNewContainerToParent( AddNewContainerToParent(
containerConfig, containerConfig,
configuration, configuration,
containers,
node.parent, node.parent,
0, 0, 0, 0,
newCounters, newCounters,
@ -350,9 +361,10 @@ function InitializeChildrenWithPattern(
console.warn(`[InitializeChildrenFromPattern] IAvailableContainer from pattern was not found in the configuration: ${pattern.wrapper}. console.warn(`[InitializeChildrenFromPattern] IAvailableContainer from pattern was not found in the configuration: ${pattern.wrapper}.
Process will ignore the container.`); Process will ignore the container.`);
} else { } else {
parent = AddNewContainerToParent( const newChildContainer = AddNewContainerToParent(
container, container,
configuration, configuration,
containers,
parent, parent,
0, 0, 0, 0,
newCounters, newCounters,
@ -360,6 +372,9 @@ function InitializeChildrenWithPattern(
undefined, undefined,
false false
); );
// iterate
parent = newChildContainer;
} }
} }
@ -398,6 +413,7 @@ interface Node {
* @returns New offset * @returns New offset
*/ */
function ApplyAddMethod( function ApplyAddMethod(
containers: Map<string, IContainerModel>,
index: number, index: number,
containerConfig: IAvailableContainer, containerConfig: IAvailableContainer,
parent: IContainerModel, parent: IContainerModel,
@ -410,8 +426,8 @@ function ApplyAddMethod(
containerConfig.AddMethod === AddMethod.Append containerConfig.AddMethod === AddMethod.Append
)) { )) {
// Append method (default) // Append method (default)
const lastChild: IContainerModel | undefined = parent.children const lastChildId: string = parent.children[index - 1];
.at(index - 1); const lastChild = FindContainerById(containers, lastChildId);
if (lastChild !== undefined) { if (lastChild !== undefined) {
const isHorizontal = parent.properties.orientation === Orientation.Horizontal; const isHorizontal = parent.properties.orientation === Orientation.Horizontal;

View file

@ -24,7 +24,8 @@ export function SelectContainer(
history.push({ history.push({
lastAction: `Select ${containerId}`, lastAction: `Select ${containerId}`,
mainContainer: structuredClone(current.mainContainer), mainContainer: current.mainContainer,
containers: structuredClone(current.containers),
selectedContainerId: containerId, selectedContainerId: containerId,
typeCounters: Object.assign({}, current.typeCounters), typeCounters: Object.assign({}, current.typeCounters),
symbols: structuredClone(current.symbols), symbols: structuredClone(current.symbols),
@ -48,8 +49,9 @@ export function DeleteContainer(
const history = GetCurrentHistory(fullHistory, historyCurrentStep); const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1]; const current = history[history.length - 1];
const mainContainerClone: IContainerModel = structuredClone(current.mainContainer); const containers = structuredClone(current.containers);
const container = FindContainerById(mainContainerClone, containerId); const mainContainerClone: IContainerModel | undefined = FindContainerById(containers, current.mainContainer);
const container = FindContainerById(containers, containerId);
if (container === undefined) { if (container === undefined) {
throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); 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); const newSymbols = structuredClone(current.symbols);
UnlinkContainerFromSymbols(newSymbols, container); UnlinkContainerFromSymbols(containers, newSymbols, container);
const index = container.parent.children.indexOf(container); const index = container.parent.children.indexOf(container.properties.id);
if (index > -1) { const success = containers.delete(container.properties.id);
if (index > -1 && success) {
container.parent.children.splice(index, 1); container.parent.children.splice(index, 1);
} else { } else {
throw new Error('[DeleteContainer] Could not find container among parent\'s children'); 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 // Select the previous container
// or select the one above // or select the one above
const selectedContainerId = GetSelectedContainerOnDelete( const selectedContainerId = GetSelectedContainerOnDelete(
mainContainerClone, containers,
current.selectedContainerId, current.selectedContainerId,
container.parent, container.parent,
index index
@ -93,7 +96,8 @@ export function DeleteContainer(
history.push({ history.push({
lastAction: `Delete ${containerId}`, lastAction: `Delete ${containerId}`,
mainContainer: mainContainerClone, mainContainer: current.mainContainer,
containers,
selectedContainerId, selectedContainerId,
typeCounters: Object.assign({}, current.typeCounters), typeCounters: Object.assign({}, current.typeCounters),
symbols: newSymbols, symbols: newSymbols,
@ -115,16 +119,15 @@ export function DeleteContainer(
* @returns {IContainerModel} Next selected container * @returns {IContainerModel} Next selected container
*/ */
function GetSelectedContainerOnDelete( function GetSelectedContainerOnDelete(
mainContainerClone: IContainerModel, containers: Map<string, IContainerModel>,
selectedContainerId: string, selectedContainerId: string,
parent: IContainerModel, parent: IContainerModel,
index: number index: number
): string { ): string {
const newSelectedContainer = FindContainerById(mainContainerClone, selectedContainerId) ?? const newSelectedContainerId = FindContainerById(containers, selectedContainerId)?.properties.id ??
parent.children.at(index) ?? parent.children.at(index) ??
parent.children.at(index - 1) ?? parent.children.at(index - 1) ??
parent; parent.properties.id;
const newSelectedContainerId = newSelectedContainer.properties.id;
return newSelectedContainerId; return newSelectedContainerId;
} }
@ -134,8 +137,12 @@ function GetSelectedContainerOnDelete(
* @param symbols Symbols to update * @param symbols Symbols to update
* @param container Container to unlink * @param container Container to unlink
*/ */
function UnlinkContainerFromSymbols(symbols: Map<string, ISymbolModel>, container: IContainerModel): void { function UnlinkContainerFromSymbols(
const it = MakeDFSIterator(container); containers: Map<string, IContainerModel>,
symbols: Map<string, ISymbolModel>,
container: IContainerModel
): void {
const it = MakeDFSIterator(container, containers);
for (const child of it) { for (const child of it) {
const symbol = symbols.get(child.properties.linkedSymbolId); const symbol = symbols.get(child.properties.linkedSymbolId);
if (symbol === undefined) { if (symbol === undefined) {
@ -167,18 +174,19 @@ export function OnPropertyChange(
throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
} }
const mainContainerClone: IContainerModel = structuredClone(current.mainContainer); const containers = structuredClone(current.containers);
const container: ContainerModel | undefined = FindContainerById(mainContainerClone, selected.properties.id); const container: ContainerModel | undefined = FindContainerById(containers, selected.properties.id);
if (container === null || container === undefined) { if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); 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({ history.push({
lastAction: `Change ${key} of ${container.properties.id}`, lastAction: `Change ${key} of ${container.properties.id}`,
mainContainer: mainContainerClone, mainContainer: current.mainContainer,
containers,
selectedContainerId: container.properties.id, selectedContainerId: container.properties.id,
typeCounters: Object.assign({}, current.typeCounters), typeCounters: Object.assign({}, current.typeCounters),
symbols: structuredClone(current.symbols), symbols: structuredClone(current.symbols),
@ -189,20 +197,30 @@ export function OnPropertyChange(
/** /**
* Sort the parent children by x * Sort the parent children by x
* @param parentClone The clone used for the sort * @param parent The clone used for the sort
* @returns void * @returns void
*/ */
export function SortChildren(parentClone: IContainerModel | null | undefined): void { export function SortChildren(
if (parentClone === null || parentClone === undefined) { containers: Map<string, IContainerModel>,
parent: IContainerModel | null | undefined
): void {
if (parent === null || parent === undefined) {
return; return;
} }
const isHorizontal = parentClone.properties.orientation === Orientation.Horizontal; const isHorizontal = parent.properties.orientation === Orientation.Horizontal;
const children = parentClone.children; const children = parent.children;
if (!isHorizontal) { if (!isHorizontal) {
parentClone.children.sort( parent.children.sort(
(a, b) => { (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 yA = TransformY(a.properties.y, a.properties.height, a.properties.positionReference);
const yB = TransformY(b.properties.y, b.properties.height, b.properties.positionReference); const yB = TransformY(b.properties.y, b.properties.height, b.properties.positionReference);
if (yA < yB) { if (yA < yB) {
@ -212,16 +230,23 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v
return 1; return 1;
} }
// xA = xB // xA = xB
const indexA = children.indexOf(a); const indexA = children.indexOf(aId);
const indexB = children.indexOf(b); const indexB = children.indexOf(bId);
return indexA - indexB; return indexA - indexB;
} }
); );
return; return;
} }
parentClone.children.sort( parent.children.sort(
(a, b) => { (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 xA = TransformX(a.properties.x, a.properties.width, a.properties.positionReference);
const xB = TransformX(b.properties.x, b.properties.width, b.properties.positionReference); const xB = TransformX(b.properties.x, b.properties.width, b.properties.positionReference);
if (xA < xB) { if (xA < xB) {
@ -231,8 +256,8 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v
return 1; return 1;
} }
// xA = xB // xA = xB
const indexA = children.indexOf(a); const indexA = children.indexOf(aId);
const indexB = children.indexOf(b); const indexB = children.indexOf(bId);
return indexA - indexB; return indexA - indexB;
} }
); );
@ -247,6 +272,7 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v
* @param symbols Current list of symbols * @param symbols Current list of symbols
*/ */
function SetContainer( function SetContainer(
containers: Map<string, IContainerModel>,
container: ContainerModel, container: ContainerModel,
key: string, value: string | number | boolean | number[], key: string, value: string | number | boolean | number[],
type: PropertyType, type: PropertyType,
@ -267,13 +293,13 @@ function SetContainer(
); );
// sort the children list by their position // sort the children list by their position
SortChildren(container.parent); SortChildren(containers, container.parent);
// Apply special behaviors: rigid, flex, symbol, anchor // Apply special behaviors: rigid, flex, symbol, anchor
ApplyBehaviors(container, symbols); ApplyBehaviors(containers, container, symbols);
// Apply special behaviors on siblings // Apply special behaviors on siblings
ApplyBehaviorsOnSiblingsChildren(container, symbols); ApplyBehaviorsOnSiblingsChildren(containers, container, symbols);
} }
/** /**

View file

@ -21,7 +21,7 @@ export function GetAction(
): (target: HTMLElement) => void { ): (target: HTMLElement) => void {
return (target: HTMLElement) => { return (target: HTMLElement) => {
const id = target.id; const id = target.id;
const container = FindContainerById(currentState.mainContainer, id); const container = FindContainerById(currentState.containers, id);
if (container === undefined) { if (container === undefined) {
Swal.fire({ Swal.fire({
@ -33,7 +33,7 @@ export function GetAction(
} }
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
const { prev, next } = GetPreviousAndNextSiblings(container); const { prev, next } = GetPreviousAndNextSiblings(currentState.containers, container);
const request: ISetContainerListRequest = { const request: ISetContainerListRequest = {
Container: container, Container: container,
@ -59,18 +59,18 @@ export function GetAction(
}; };
} }
function GetPreviousAndNextSiblings(container: IContainerModel): { prev: IContainerModel | undefined, next: IContainerModel | undefined } { function GetPreviousAndNextSiblings(containers: Map<string, IContainerModel>, container: IContainerModel): { prev: IContainerModel | undefined, next: IContainerModel | undefined } {
let prev; let prev;
let next; let next;
if (container.parent !== undefined && if (container.parent !== undefined &&
container.parent !== null && container.parent !== null &&
container.parent.children.length > 1) { container.parent.children.length > 1) {
const index = container.parent.children.indexOf(container); const index = container.parent.children.indexOf(container.properties.id);
if (index > 0) { if (index > 0) {
prev = container.parent.children[index - 1]; prev = FindContainerById(containers, container.parent.children[index - 1]);
} }
if (index < container.parent.children.length - 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 }; return { prev, next };
@ -144,7 +144,7 @@ function HandleReplace(
throw new Error('[ReplaceContainer] Cannot replace a container that does not exists'); 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( const newHistoryAfterDelete = DeleteContainer(
selectedContainer.properties.id, selectedContainer.properties.id,

View file

@ -36,6 +36,7 @@ export function AddSymbol(
history.push({ history.push({
lastAction: `Add ${name}`, lastAction: `Add ${name}`,
mainContainer: structuredClone(current.mainContainer), mainContainer: structuredClone(current.mainContainer),
containers: structuredClone(current.containers),
selectedContainerId: current.selectedContainerId, selectedContainerId: current.selectedContainerId,
typeCounters: newCounters, typeCounters: newCounters,
symbols: newSymbols, symbols: newSymbols,
@ -55,6 +56,7 @@ export function SelectSymbol(
history.push({ history.push({
lastAction: `Select ${symbolId}`, lastAction: `Select ${symbolId}`,
mainContainer: structuredClone(current.mainContainer), mainContainer: structuredClone(current.mainContainer),
containers: structuredClone(current.containers),
selectedContainerId: current.selectedContainerId, selectedContainerId: current.selectedContainerId,
typeCounters: structuredClone(current.typeCounters), typeCounters: structuredClone(current.typeCounters),
symbols: structuredClone(current.symbols), symbols: structuredClone(current.symbols),
@ -78,15 +80,15 @@ export function DeleteSymbol(
throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`); throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`);
} }
const newMainContainer = structuredClone(current.mainContainer); const containers = structuredClone(current.containers);
UnlinkSymbolFromContainers(containers, symbol);
UnlinkSymbolFromContainers(symbol, newMainContainer);
newSymbols.delete(symbolId); newSymbols.delete(symbolId);
history.push({ history.push({
lastAction: `Select ${symbolId}`, lastAction: `Select ${symbolId}`,
mainContainer: newMainContainer, mainContainer: current.mainContainer,
containers,
selectedContainerId: current.selectedContainerId, selectedContainerId: current.selectedContainerId,
typeCounters: structuredClone(current.typeCounters), typeCounters: structuredClone(current.typeCounters),
symbols: newSymbols, symbols: newSymbols,
@ -100,9 +102,9 @@ export function DeleteSymbol(
* @param symbol Symbol to remove * @param symbol Symbol to remove
* @param root Container and its children to remove a symbol from * @param root Container and its children to remove a symbol from
*/ */
function UnlinkSymbolFromContainers(symbol: ISymbolModel, root: IContainerModel): void { function UnlinkSymbolFromContainers(containers: Map<string, IContainerModel>, symbol: ISymbolModel): void {
symbol.linkedContainers.forEach((containerId) => { symbol.linkedContainers.forEach((containerId) => {
const container = FindContainerById(root, containerId); const container = FindContainerById(containers, containerId);
if (container === undefined) { if (container === undefined) {
return; return;
@ -140,22 +142,23 @@ export function OnPropertyChange(
(symbol as any)[key] = value; (symbol as any)[key] = value;
const newMainContainer = structuredClone(current.mainContainer); const containers = structuredClone(current.containers);
symbol.linkedContainers.forEach((containerId) => { symbol.linkedContainers.forEach((containerId) => {
const container = FindContainerById(newMainContainer, containerId); const container = FindContainerById(containers, containerId);
if (container === undefined) { if (container === undefined) {
return; return;
} }
ApplyBehaviors(container, newSymbols); ApplyBehaviors(containers, container, newSymbols);
ApplyBehaviorsOnSiblingsChildren(container, newSymbols); ApplyBehaviorsOnSiblingsChildren(containers, container, newSymbols);
}); });
history.push({ history.push({
lastAction: `Change ${key} of ${symbol.id}`, lastAction: `Change ${key} of ${symbol.id}`,
mainContainer: newMainContainer, mainContainer: current.mainContainer,
containers,
selectedContainerId: current.selectedContainerId, selectedContainerId: current.selectedContainerId,
typeCounters: Object.assign({}, current.typeCounters), typeCounters: Object.assign({}, current.typeCounters),
symbols: newSymbols, symbols: newSymbols,

View file

@ -16,16 +16,28 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { ConstraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors'; import { ConstraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors';
import { FindContainerById } from '../../../utils/itertools';
/** /**
* Impose the container position to its siblings * Impose the container position to its siblings
* Apply the following modification to the overlapping rigid body container : * Apply the following modification to the overlapping rigid body container :
* @param container Container to impose its position * @param container Container to impose its position
*/ */
export function ApplyAnchor(container: IContainerModel, parent: IContainerModel): IContainerModel { export function ApplyAnchor(containers: Map<string, IContainerModel>, container: IContainerModel, parent: IContainerModel): IContainerModel {
const rigidBodies = parent.children.filter( const rigidBodies: IContainerModel[] = [];
child => !child.properties.isAnchor 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 isHorizontal = parent.properties.orientation === Orientation.Horizontal;
const overlappingContainers = isHorizontal const overlappingContainers = isHorizontal
@ -33,7 +45,7 @@ export function ApplyAnchor(container: IContainerModel, parent: IContainerModel)
: GetVerticallyOverlappingContainers(container, rigidBodies); : GetVerticallyOverlappingContainers(container, rigidBodies);
for (const overlappingContainer of overlappingContainers) { for (const overlappingContainer of overlappingContainers) {
ConstraintBodyInsideUnallocatedWidth(overlappingContainer); ConstraintBodyInsideUnallocatedWidth(containers, overlappingContainer);
} }
return container; return container;
} }

View file

@ -1,6 +1,7 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { APPLY_BEHAVIORS_ON_CHILDREN, ENABLE_RIGID, ENABLE_SWAP } from '../../../utils/default'; import { APPLY_BEHAVIORS_ON_CHILDREN, ENABLE_RIGID, ENABLE_SWAP } from '../../../utils/default';
import { FindContainerById, MakeChildrenIterator } from '../../../utils/itertools';
import { ApplyAnchor, GetOverlappingContainers } from './AnchorBehaviors'; import { ApplyAnchor, GetOverlappingContainers } from './AnchorBehaviors';
import { Flex } from './FlexBehaviors'; import { Flex } from './FlexBehaviors';
import { ApplyRigidBody } from './RigidBodyBehaviors'; import { ApplyRigidBody } from './RigidBodyBehaviors';
@ -13,7 +14,7 @@ import { ApplySymbol } from './SymbolBehaviors';
* @param container Container to recalculate its positions * @param container Container to recalculate its positions
* @returns Updated container * @returns Updated container
*/ */
export function ApplyBehaviors(container: IContainerModel, symbols: Map<string, ISymbolModel>): IContainerModel { export function ApplyBehaviors(containers: Map<string, IContainerModel>, container: IContainerModel, symbols: Map<string, ISymbolModel>): IContainerModel {
try { try {
const symbol = symbols.get(container.properties.linkedSymbolId); const symbol = symbols.get(container.properties.linkedSymbolId);
if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
@ -24,24 +25,24 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map<string,
const parent = container.parent; const parent = container.parent;
if (container.properties.isAnchor) { if (container.properties.isAnchor) {
ApplyAnchor(container, parent); ApplyAnchor(containers, container, parent);
} }
if (ENABLE_SWAP) { if (ENABLE_SWAP) {
ApplySwap(container, parent); ApplySwap(containers, container, parent);
} }
Flex(container, parent); Flex(containers, container, parent);
if (ENABLE_RIGID) { if (ENABLE_RIGID) {
ApplyRigidBody(container, parent); ApplyRigidBody(containers, container, parent);
} }
} }
if (APPLY_BEHAVIORS_ON_CHILDREN) { if (APPLY_BEHAVIORS_ON_CHILDREN) {
// Apply DFS by recursion // Apply DFS by recursion
for (const child of container.children) { for (const child of MakeChildrenIterator(containers, container.children)) {
ApplyBehaviors(child, symbols); ApplyBehaviors(containers, child, symbols);
} }
} }
} catch (e) { } catch (e) {
@ -64,23 +65,32 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map<string,
* @param symbols * @param symbols
* @returns * @returns
*/ */
export function ApplyBehaviorsOnSiblingsChildren(newContainer: IContainerModel, symbols: Map<string, ISymbolModel>): void { export function ApplyBehaviorsOnSiblingsChildren(
containers: Map<string, IContainerModel>,
newContainer: IContainerModel,
symbols: Map<string, ISymbolModel>): void {
if (newContainer.parent === null || newContainer.parent === undefined) { if (newContainer.parent === null || newContainer.parent === undefined) {
return; return;
} }
newContainer.parent.children newContainer.parent.children
.forEach((container: IContainerModel) => { .forEach((containerId: string) => {
if (container.parent != null) { const container = FindContainerById(containers, containerId);
UpdateWarning(container, container.parent);
if (container === undefined) {
return;
}
if (container.parent !== null) {
UpdateWarning(containers, container, container.parent);
} }
if (container === newContainer) { if (container === newContainer) {
return; return;
} }
for (const child of container.children) { for (const child of MakeChildrenIterator(containers, container.children)) {
ApplyBehaviors(child, symbols); ApplyBehaviors(containers, child, symbols);
} }
}); });
} }
@ -91,30 +101,47 @@ export function ApplyBehaviorsOnSiblingsChildren(newContainer: IContainerModel,
* @param symbols * @param symbols
* @returns * @returns
*/ */
export function ApplyBehaviorsOnSiblings(newContainer: IContainerModel, symbols: Map<string, ISymbolModel>): void { export function ApplyBehaviorsOnSiblings(containers: Map<string, IContainerModel>, newContainer: IContainerModel, symbols: Map<string, ISymbolModel>): void {
if (newContainer.parent === null || newContainer.parent === undefined) { if (newContainer.parent === null || newContainer.parent === undefined) {
return; return;
} }
newContainer.parent.children newContainer.parent.children
.forEach((container: IContainerModel) => { .forEach((containerId: string) => {
ApplyBehaviors(container, symbols); const container = FindContainerById(containers, containerId);
if (container === undefined) {
return;
}
ApplyBehaviors(containers, container, symbols);
if (container.parent != null) { if (container.parent != null) {
UpdateWarning(container, container.parent); UpdateWarning(containers, container, container.parent);
} }
if (container === newContainer) { if (container === newContainer) {
return; return;
} }
for (const child of container.children) { for (const child of MakeChildrenIterator(containers, container.children)) {
ApplyBehaviors(child, symbols); ApplyBehaviors(containers, child, symbols);
} }
}); });
} }
function UpdateWarning(container: IContainerModel, parent: IContainerModel): void { function UpdateWarning(containers: Map<string, IContainerModel>, container: IContainerModel, parent: IContainerModel): void {
const overlappingContainers = GetOverlappingContainers(container, parent.children); 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) { if (overlappingContainers.length > 0) {
container.properties.warning = `There are overlapping containers: ${overlappingContainers.map(c => c.properties.id).join(' ')}`; container.properties.warning = `There are overlapping containers: ${overlappingContainers.map(c => c.properties.id).join(' ')}`;
} else { } else {

View file

@ -2,6 +2,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { Simplex } from '../../../utils/simplex'; import { Simplex } from '../../../utils/simplex';
import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg'; import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg';
import { MakeChildrenIterator } from '../../../utils/itertools';
interface IFlexibleGroup { interface IFlexibleGroup {
group: IContainerModel[] group: IContainerModel[]
@ -13,13 +14,13 @@ interface IFlexibleGroup {
* Flex the container and its siblings (mutate) * Flex the container and its siblings (mutate)
* @returns Flexed container * @returns Flexed container
*/ */
export function Flex(container: IContainerModel, parent: IContainerModel): void { export function Flex(containers: Map<string, IContainerModel>, container: IContainerModel, parent: IContainerModel): void {
const isVertical = parent.properties.orientation === Orientation.Vertical; const isVertical = parent.properties.orientation === Orientation.Vertical;
if (isVertical) { if (isVertical) {
const wantedWidth = Math.min(container.properties.maxWidth, parent.properties.width); const wantedWidth = Math.min(container.properties.maxWidth, parent.properties.width);
container.properties.width = ApplyWidthMargin(wantedWidth, container.properties.margin.left, container.properties.margin.right); 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) { for (const flexibleGroup of flexibleGroups) {
FlexGroupVertically(flexibleGroup); FlexGroupVertically(flexibleGroup);
} }
@ -28,7 +29,7 @@ export function Flex(container: IContainerModel, parent: IContainerModel): void
const wantedHeight = Math.min(container.properties.maxHeight, parent.properties.height); const wantedHeight = Math.min(container.properties.maxHeight, parent.properties.height);
container.properties.height = ApplyWidthMargin(wantedHeight, container.properties.margin.top, container.properties.margin.bottom); 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) { for (const flexibleGroup of flexibleGroups) {
FlexGroupHorizontally(flexibleGroup); 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 * @param parent Parent in which the flexible children will be set in groups
* @returns a list of groups of flexible containers * @returns a list of groups of flexible containers
*/ */
export function GetHorizontalFlexibleGroups(parent: IContainerModel): IFlexibleGroup[] { export function GetHorizontalFlexibleGroups(containers: Map<string, IContainerModel>, parent: IContainerModel): IFlexibleGroup[] {
const flexibleGroups: IFlexibleGroup[] = []; const flexibleGroups: IFlexibleGroup[] = [];
let group: IContainerModel[] = []; let group: IContainerModel[] = [];
let offset = 0; let offset = 0;
let size = 0; let size = 0;
for (const child of parent.children) { for (const child of MakeChildrenIterator(containers, parent.children)) {
if (child.properties.isAnchor) { if (child.properties.isAnchor) {
size = child.properties.x - offset; size = child.properties.x - offset;
const flexibleGroup: IFlexibleGroup = { 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 * @param parent Parent in which the flexible children will be set in groups
* @returns a list of groups of flexible containers * @returns a list of groups of flexible containers
*/ */
export function GetVerticalFlexibleGroups(parent: IContainerModel): IFlexibleGroup[] { export function GetVerticalFlexibleGroups(
containers: Map<string, IContainerModel>,
parent: IContainerModel
): IFlexibleGroup[] {
const flexibleGroups: IFlexibleGroup[] = []; const flexibleGroups: IFlexibleGroup[] = [];
let group: IContainerModel[] = []; let group: IContainerModel[] = [];
let offset = 0; let offset = 0;
let size = 0; let size = 0;
for (const child of parent.children) { for (const child of MakeChildrenIterator(containers, parent.children)) {
if (child.properties.isAnchor) { if (child.properties.isAnchor) {
size = child.properties.y - offset; size = child.properties.y - offset;
const flexibleGroup: IFlexibleGroup = { const flexibleGroup: IFlexibleGroup = {

View file

@ -1,6 +1,6 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { ReversePairwise } from '../../../utils/itertools'; import { MakeChildrenIterator, ReversePairwise } from '../../../utils/itertools';
import { Flex } from './FlexBehaviors'; import { Flex } from './FlexBehaviors';
/** /**
@ -8,12 +8,17 @@ import { Flex } from './FlexBehaviors';
* @param container * @param container
* @returns * @returns
*/ */
export function ApplyPush(container: IContainerModel, parent: IContainerModel): IContainerModel { export function ApplyPush(
containers: Map<string, IContainerModel>,
container: IContainerModel,
parent: IContainerModel
): IContainerModel {
if (parent.children.length <= 1) { if (parent.children.length <= 1) {
return container; return container;
} }
const children = parent.children; const children: IContainerModel[] = [...MakeChildrenIterator(containers, parent.children)];
const isHorizontal = parent.properties.orientation === Orientation.Horizontal; const isHorizontal = parent.properties.orientation === Orientation.Horizontal;
if (isHorizontal) { if (isHorizontal) {
@ -22,7 +27,7 @@ export function ApplyPush(container: IContainerModel, parent: IContainerModel):
PushContainersVertically(container, children); PushContainersVertically(container, children);
} }
Flex(container, parent); Flex(containers, container, parent);
return container; return container;
} }

View file

@ -10,6 +10,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISizePointer } from '../../../Interfaces/ISizePointer'; import { ISizePointer } from '../../../Interfaces/ISizePointer';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { ENABLE_HARD_RIGID } from '../../../utils/default'; import { ENABLE_HARD_RIGID } from '../../../utils/default';
import { MakeChildrenIterator } from '../../../utils/itertools';
/** /**
* "Transform the container into a rigid body" * "Transform the container into a rigid body"
@ -21,13 +22,14 @@ import { ENABLE_HARD_RIGID } from '../../../utils/default';
* @returns A rigid body container * @returns A rigid body container
*/ */
export function ApplyRigidBody( export function ApplyRigidBody(
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
parent: IContainerModel parent: IContainerModel
): IContainerModel { ): IContainerModel {
container = ConstraintBodyInsideParent(container, parent); container = ConstraintBodyInsideParent(container, parent);
if (ENABLE_HARD_RIGID) { if (ENABLE_HARD_RIGID) {
container = ConstraintBodyInsideUnallocatedWidth(container); container = ConstraintBodyInsideUnallocatedWidth(containers, container);
} }
return container; return container;
@ -117,6 +119,7 @@ function ConstraintBodyInsideSpace(
* @returns Updated container * @returns Updated container
*/ */
export function ConstraintBodyInsideUnallocatedWidth( export function ConstraintBodyInsideUnallocatedWidth(
containers: Map<string, IContainerModel>,
container: IContainerModel container: IContainerModel
): IContainerModel { ): IContainerModel {
if (container.parent === null || container.parent === undefined) { if (container.parent === null || container.parent === undefined) {
@ -126,10 +129,11 @@ export function ConstraintBodyInsideUnallocatedWidth(
// Get the available spaces of the parent // Get the available spaces of the parent
const isHorizontal = const isHorizontal =
container.parent.properties.orientation === Orientation.Horizontal; container.parent.properties.orientation === Orientation.Horizontal;
const children: IContainerModel[] = [...MakeChildrenIterator(containers, container.parent.children)];
const availableWidths = GetAvailableWidths( const availableWidths = GetAvailableWidths(
0, 0,
container.parent.properties.width, container.parent.properties.width,
container.parent.children, children,
container, container,
isHorizontal isHorizontal
); );

View file

@ -5,9 +5,13 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { GetHorizontallyOverlappingContainers, GetVerticallyOverlappingContainers } from './AnchorBehaviors'; import { GetHorizontallyOverlappingContainers, GetVerticallyOverlappingContainers } from './AnchorBehaviors';
import { MakeChildrenIterator } from '../../../utils/itertools';
export function ApplySwap(container: IContainerModel, parent: IContainerModel): void { export function ApplySwap(
const children = parent.children; containers: Map<string, IContainerModel>,
container: IContainerModel,
parent: IContainerModel): void {
const children = [...MakeChildrenIterator(containers, parent.children)];
const isVertical = parent.properties.orientation === Orientation.Vertical; const isVertical = parent.properties.orientation === Orientation.Vertical;
if (isVertical) { if (isVertical) {

View file

@ -274,7 +274,7 @@ export function Editor(props: IEditorProps): JSX.Element {
// Render // Render
const configuration = props.configuration; const configuration = props.configuration;
const current = GetCurrentHistoryState(history, historyCurrentStep); const current = GetCurrentHistoryState(history, historyCurrentStep);
const selected = FindContainerById(current.mainContainer, current.selectedContainerId); const selected = FindContainerById(current.containers, current.selectedContainerId);
return ( return (
<div ref={editorRef} className="Editor font-sans h-full"> <div ref={editorRef} className="Editor font-sans h-full">

View file

@ -9,6 +9,7 @@ import { PropertyType } from '../../Enums/PropertyType';
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
interface IElementsListProps { interface IElementsListProps {
containers: Map<string, IContainerModel>
mainContainer: IContainerModel mainContainer: IContainerModel
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
@ -59,6 +60,7 @@ function HandleDragOver(
function HandleOnDrop( function HandleOnDrop(
event: React.DragEvent, event: React.DragEvent,
containers: Map<string, IContainerModel>,
mainContainer: IContainerModel, mainContainer: IContainerModel,
addContainer: (index: number, type: string, parent: string) => void addContainer: (index: number, type: string, parent: string) => void
): void { ): void {
@ -68,7 +70,7 @@ function HandleOnDrop(
RemoveBorderClasses(target); RemoveBorderClasses(target);
const targetContainer: IContainerModel | undefined = FindContainerById( const targetContainer: IContainerModel | undefined = FindContainerById(
mainContainer, containers,
target.id target.id
); );
@ -95,7 +97,7 @@ function HandleOnDrop(
// locate the hitboxes // locate the hitboxes
if (y < 12) { if (y < 12) {
const index = targetContainer.parent.children.indexOf(targetContainer); const index = targetContainer.parent.children.indexOf(targetContainer.properties.id);
addContainer( addContainer(
index, index,
type, type,
@ -107,7 +109,7 @@ function HandleOnDrop(
type, type,
targetContainer.properties.id); targetContainer.properties.id);
} else { } else {
const index = targetContainer.parent.children.indexOf(targetContainer); const index = targetContainer.parent.children.indexOf(targetContainer.properties.id);
addContainer( addContainer(
index + 1, index + 1,
type, type,
@ -119,10 +121,10 @@ function HandleOnDrop(
export function ElementsList(props: IElementsListProps): JSX.Element { export function ElementsList(props: IElementsListProps): JSX.Element {
// States // States
const divRef = React.useRef<HTMLDivElement>(null); const divRef = React.useRef<HTMLDivElement>(null);
const [width, height] = useSize(divRef); const [, height] = useSize(divRef);
// Render // Render
const it = MakeRecursionDFSIterator(props.mainContainer, 0, [0, 0], true); const it = MakeRecursionDFSIterator(props.mainContainer, props.containers, 0, [0, 0], true);
const containers = [...it]; const containers = [...it];
function Row({ function Row({
index, style index, style
@ -154,7 +156,7 @@ export function ElementsList(props: IElementsListProps): JSX.Element {
style={style} style={style}
title={container.properties.warning} title={container.properties.warning}
onClick={() => props.selectContainer(container.properties.id)} 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)} onDragOver={(event) => HandleDragOver(event, props.mainContainer)}
onDragLeave={(event) => HandleDragLeave(event)} onDragLeave={(event) => HandleDragLeave(event)}
> >

View file

@ -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<string, ISymbolModel>
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 (
<button type="button"
className={`transition-all w-full border-blue-500 hover:shadow-lg elements-sidebar-row whitespace-pre
text-left text-sm font-medium inline-flex ${container.properties.type} ${selectedClass}`}
id={key}
key={key}
style={style}
title={container.properties.warning}
onClick={() => props.selectContainer(container.properties.id)}
onDrop={(event) => HandleOnDrop(event, props.mainContainer, props.addContainer)}
onDragOver={(event) => HandleDragOver(event, props.mainContainer)}
onDragLeave={(event) => HandleDragLeave(event)}
>
{tabs}
{text}
{container.properties.warning.length > 0 &&
<ExclamationTriangleIcon className='w-8'/>
}
</button>
);
}
return (
<>
<List
className="List divide-y divide-black overflow-y-auto"
itemCount={containers.length}
itemSize={35}
height={192}
width={'100%'}
>
{Row}
</List>
<Properties
properties={props.selectedContainer?.properties}
symbols={props.symbols}
onChange={props.onPropertyChange}
/>
</>
);
}

View file

@ -1,6 +1,6 @@
import { TrashIcon } from '@heroicons/react/24/outline';
import * as React from 'react'; import * as React from 'react';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import { API_GET_FEEDBACK_URL } from '../../../public/svgld-settings';
import { MessageType } from '../../Enums/MessageType'; import { MessageType } from '../../Enums/MessageType';
import { IGetFeedbackRequest } from '../../Interfaces/IGetFeedbackRequest'; import { IGetFeedbackRequest } from '../../Interfaces/IGetFeedbackRequest';
import { IGetFeedbackResponse } from '../../Interfaces/IGetFeedbackResponse'; import { IGetFeedbackResponse } from '../../Interfaces/IGetFeedbackResponse';
@ -25,7 +25,7 @@ function UseWorker(
// use webworker for the stringify to avoid freezing // use webworker for the stringify to avoid freezing
myWorker.postMessage({ myWorker.postMessage({
state, state,
url: import.meta.env.VITE_API_GET_FEEDBACK_URL url: API_GET_FEEDBACK_URL
}); });
return () => { return () => {
@ -49,7 +49,7 @@ function UseAsync(
ApplicationState: state ApplicationState: state
}; };
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure()); const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
fetch(import.meta.env.VITE_API_GET_FEEDBACK_URL, { fetch(API_GET_FEEDBACK_URL, {
method: 'POST', method: 'POST',
headers: new Headers({ headers: new Headers({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention

View file

@ -4,8 +4,10 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { IContainerProperties } from '../../../Interfaces/IContainerProperties'; import { IContainerProperties } from '../../../Interfaces/IContainerProperties';
import { Camelize } from '../../../utils/stringtools'; import { Camelize } from '../../../utils/stringtools';
import { SHOW_TEXT } from '../../../utils/default'; import { SHOW_TEXT } from '../../../utils/default';
import { FindContainerById } from '../../../utils/itertools';
interface IContainerProps { interface IContainerProps {
containers: Map<string, IContainerModel>
model: IContainerModel model: IContainerModel
depth: number depth: number
scale: number scale: number
@ -18,13 +20,22 @@ interface IContainerProps {
*/ */
export function Container(props: IContainerProps): JSX.Element { export function Container(props: IContainerProps): JSX.Element {
const containersElements = props.model.children.map( const containersElements = props.model.children.map(
child => <Container childId => {
key={`container-${child.properties.id}`} const child = FindContainerById(props.containers, childId);
model={child}
depth={props.depth + 1} if (child === undefined) {
scale={props.scale} return <></>;
selectContainer={props.selectContainer} }
/>);
return <Container
key={`container-${child.properties.id}`}
containers={props.containers}
model={child}
depth={props.depth + 1}
scale={props.scale}
selectContainer={props.selectContainer}
/>;
});
const width: number = props.model.properties.width; const width: number = props.model.properties.width;
const height: number = props.model.properties.height; const height: number = props.model.properties.height;

View file

@ -1,17 +1,22 @@
import * as React from 'react'; import * as React from 'react';
import { ContainerModel } from '../../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN } from '../../../utils/default'; import { DIMENSION_MARGIN } from '../../../utils/default';
import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools'; import { GetAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
import { TransformX } from '../../../utils/svg'; import { TransformX } from '../../../utils/svg';
import { Dimension } from './Dimension'; import { Dimension } from './Dimension';
interface IDimensionLayerProps { interface IDimensionLayerProps {
containers: Map<string, IContainerModel>
roots: ContainerModel | ContainerModel[] | null roots: ContainerModel | ContainerModel[] | null
scale?: number scale?: number
} }
function GetDimensionsNodes(root: ContainerModel, scale: number): React.ReactNode[] { function GetDimensionsNodes(
const it = MakeBFSIterator(root); containers: Map<string, IContainerModel>,
root: ContainerModel,
scale: number
): React.ReactNode[] {
const it = MakeBFSIterator(root, containers);
const dimensions: React.ReactNode[] = []; const dimensions: React.ReactNode[] = [];
let currentDepth = 0; let currentDepth = 0;
let min = Infinity; let min = Infinity;
@ -53,10 +58,10 @@ export function DepthDimensionLayer(props: IDimensionLayerProps): JSX.Element {
const scale = props.scale ?? 1; const scale = props.scale ?? 1;
if (Array.isArray(props.roots)) { if (Array.isArray(props.roots)) {
props.roots.forEach(child => { props.roots.forEach(child => {
dimensions.concat(GetDimensionsNodes(child, scale)); dimensions.concat(GetDimensionsNodes(props.containers, child, scale));
}); });
} else if (props.roots !== null) { } else if (props.roots !== null) {
dimensions = GetDimensionsNodes(props.roots, scale); dimensions = GetDimensionsNodes(props.containers, props.roots, scale);
} }
return ( return (
<g> <g>

View file

@ -3,11 +3,12 @@ import { Orientation } from '../../../Enums/Orientation';
import { Position } from '../../../Enums/Position'; import { Position } from '../../../Enums/Position';
import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, SHOW_SELF_DIMENSIONS } from '../../../utils/default'; 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 { TransformX, TransformY } from '../../../utils/svg';
import { Dimension } from './Dimension'; import { Dimension } from './Dimension';
interface IDimensionLayerProps { interface IDimensionLayerProps {
containers: Map<string, IContainerModel>
root: ContainerModel root: ContainerModel
scale: number scale: number
} }
@ -51,8 +52,8 @@ function ActionByPosition(
* @param param0 Object with the root container and the scale of the svg * @param param0 Object with the root container and the scale of the svg
* @returns A list of dimensions * @returns A list of dimensions
*/ */
function Dimensions({ root, scale }: IDimensionLayerProps): React.ReactNode[] { function Dimensions({ containers, root, scale }: IDimensionLayerProps): React.ReactNode[] {
const it = MakeRecursionDFSIterator(root, 0, [0, 0]); const it = MakeRecursionDFSIterator(root, containers, 0, [0, 0]);
const dimensions: React.ReactNode[] = []; const dimensions: React.ReactNode[] = [];
const topDim = root.properties.y; const topDim = root.properties.y;
const leftDim = root.properties.x; const leftDim = root.properties.x;
@ -75,7 +76,8 @@ function Dimensions({ root, scale }: IDimensionLayerProps): React.ReactNode[] {
container.properties.showSelfDimensions, container.properties.showSelfDimensions,
AddHorizontalSelfDimension, AddHorizontalSelfDimension,
AddVerticalSelfDimension, AddVerticalSelfDimension,
[container, [
container,
currentTransform, currentTransform,
dimensions, dimensions,
scale] scale]
@ -88,7 +90,9 @@ function Dimensions({ root, scale }: IDimensionLayerProps): React.ReactNode[] {
container.properties.showDimensionWithMarks, container.properties.showDimensionWithMarks,
AddHorizontalBorrowerDimension, AddHorizontalBorrowerDimension,
AddVerticalBorrowerDimension, AddVerticalBorrowerDimension,
[container, [
containers,
container,
depth, depth,
currentTransform, currentTransform,
dimensions, dimensions,
@ -102,7 +106,9 @@ function Dimensions({ root, scale }: IDimensionLayerProps): React.ReactNode[] {
container.properties.showChildrenDimensions, container.properties.showChildrenDimensions,
AddHorizontalChildrenDimension, AddHorizontalChildrenDimension,
AddVerticalChildrenDimension, AddVerticalChildrenDimension,
[container, [
containers,
container,
currentTransform, currentTransform,
dimensions, dimensions,
scale] scale]
@ -130,6 +136,7 @@ export function DimensionLayer(props: IDimensionLayerProps): JSX.Element {
function AddHorizontalChildrenDimension( function AddHorizontalChildrenDimension(
yDim: number, yDim: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
@ -137,13 +144,25 @@ function AddHorizontalChildrenDimension(
): void { ): void {
const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`; 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 xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference);
let xChildrenEnd = 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 // Find the min and max
for (let i = container.children.length - 2; i >= 0; i--) { 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); const left = TransformX(child.properties.x, child.properties.width, child.properties.positionReference);
if (left < xChildrenStart) { if (left < xChildrenStart) {
xChildrenStart = left; xChildrenStart = left;
@ -178,6 +197,7 @@ function AddHorizontalChildrenDimension(
function AddVerticalChildrenDimension( function AddVerticalChildrenDimension(
xDim: number, xDim: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
@ -185,13 +205,25 @@ function AddVerticalChildrenDimension(
): void { ): void {
const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; 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 yChildrenStart = TransformY(lastChild.properties.y, lastChild.properties.height, lastChild.properties.positionReference);
let yChildrenEnd = 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 // Find the min and max
for (let i = container.children.length - 2; i >= 0; i--) { 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); const top = TransformY(child.properties.y, child.properties.height, child.properties.positionReference);
if (top < yChildrenStart) { if (top < yChildrenStart) {
yChildrenStart = top; yChildrenStart = top;
@ -227,13 +259,14 @@ function AddVerticalChildrenDimension(
function AddHorizontalBorrowerDimension( function AddHorizontalBorrowerDimension(
yDim: number, yDim: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
depth: number, depth: number,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number scale: number
): void { ): void {
const it = MakeRecursionDFSIterator(container, depth, currentTransform); const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const marks = []; // list of vertical lines for the dimension const marks = []; // list of vertical lines for the dimension
for (const { for (const {
container: childContainer, currentTransform: childCurrentTransform container: childContainer, currentTransform: childCurrentTransform
@ -279,13 +312,14 @@ function AddHorizontalBorrowerDimension(
function AddVerticalBorrowerDimension( function AddVerticalBorrowerDimension(
xDim: number, xDim: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
depth: number, depth: number,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number scale: number
): void { ): void {
const it = MakeRecursionDFSIterator(container, depth, currentTransform); const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const marks = []; // list of vertical lines for the dimension const marks = []; // list of vertical lines for the dimension
for (const { for (const {
container: childContainer, currentTransform: childCurrentTransform container: childContainer, currentTransform: childCurrentTransform

View file

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { ReactSVGPanZoom, Tool, TOOL_PAN, Value } from 'react-svg-pan-zoom'; import { ReactSVGPanZoom, Tool, TOOL_PAN, Value } from 'react-svg-pan-zoom';
import { Container } from './Elements/Container'; import { Container } from './Elements/Container';
import { ContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
import { Selector } from './Elements/Selector/Selector'; import { Selector } from './Elements/Selector/Selector';
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer'; import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default'; import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
@ -15,17 +15,13 @@ interface ISVGProps {
viewerHeight: number viewerHeight: number
width: number width: number
height: number height: number
containers: Map<string, IContainerModel>
children: ContainerModel children: ContainerModel
selected?: ContainerModel selected?: ContainerModel
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
selectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
} }
interface Viewer {
viewerWidth: number
viewerHeight: number
}
export const ID = 'svg'; export const ID = 'svg';
export function SVG(props: ISVGProps): JSX.Element { export function SVG(props: ISVGProps): JSX.Element {
@ -55,6 +51,7 @@ export function SVG(props: ISVGProps): JSX.Element {
let children: React.ReactNode | React.ReactNode[] = []; let children: React.ReactNode | React.ReactNode[] = [];
children = <Container children = <Container
key={`container-${props.children.properties.id}`} key={`container-${props.children.properties.id}`}
containers={props.containers}
model={props.children} model={props.children}
depth={0} depth={0}
scale={scale} scale={scale}
@ -97,9 +94,9 @@ export function SVG(props: ISVGProps): JSX.Element {
<svg {...properties}> <svg {...properties}>
{children} {children}
{SHOW_DIMENSIONS_PER_DEPTH {SHOW_DIMENSIONS_PER_DEPTH
? <DepthDimensionLayer scale={scale} roots={props.children} /> ? <DepthDimensionLayer containers={props.containers} scale={scale} roots={props.children} />
: null} : null}
<DimensionLayer scale={scale} root={props.children} /> <DimensionLayer containers={props.containers} scale={scale} root={props.children} />
<SymbolLayer scale={scale} symbols={props.symbols} /> <SymbolLayer scale={scale} symbols={props.symbols} />
<Selector scale={scale} selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */} <Selector scale={scale} selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */}
</svg> </svg>

View file

@ -18,6 +18,7 @@ import { Settings } from '../Settings/Settings';
import { IMessage } from '../../Interfaces/IMessage'; import { IMessage } from '../../Interfaces/IMessage';
import { DISABLE_API } from '../../utils/default'; import { DISABLE_API } from '../../utils/default';
import { UseWorker, UseAsync } from './UseWorker'; import { UseWorker, UseAsync } from './UseWorker';
import { FindContainerById } from '../../utils/itertools';
export interface IUIProps { export interface IUIProps {
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
@ -89,6 +90,12 @@ export function UI(props: IUIProps): JSX.Element {
let leftChildren: JSX.Element = (<></>); let leftChildren: JSX.Element = (<></>);
let rightChildren: 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) { switch (selectedSidebar) {
case SidebarType.Components: case SidebarType.Components:
leftSidebarTitle = 'Components'; leftSidebarTitle = 'Components';
@ -100,7 +107,8 @@ export function UI(props: IUIProps): JSX.Element {
/>; />;
rightSidebarTitle = 'Elements'; rightSidebarTitle = 'Elements';
rightChildren = <ElementsList rightChildren = <ElementsList
mainContainer={props.current.mainContainer} containers={props.current.containers}
mainContainer={mainContainer}
symbols={props.current.symbols} symbols={props.current.symbols}
selectedContainer={props.selectedContainer} selectedContainer={props.selectedContainer}
onPropertyChange={props.onPropertyChange} onPropertyChange={props.onPropertyChange}

View file

@ -4,6 +4,7 @@ import { IGetFeedbackRequest } from '../../Interfaces/IGetFeedbackRequest';
import { IGetFeedbackResponse } from '../../Interfaces/IGetFeedbackResponse'; import { IGetFeedbackResponse } from '../../Interfaces/IGetFeedbackResponse';
import { IMessage } from '../../Interfaces/IMessage'; import { IMessage } from '../../Interfaces/IMessage';
import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload'; import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload';
import { API_GET_FEEDBACK_URL } from '../../../public/svgld-settings';
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
const myWorker = window.Worker && new Worker('workers/message_worker.js'); const myWorker = window.Worker && new Worker('workers/message_worker.js');
@ -15,7 +16,7 @@ export function UseWorker(
// use webworker for the stringify to avoid freezing // use webworker for the stringify to avoid freezing
myWorker.postMessage({ myWorker.postMessage({
state, state,
url: import.meta.env.VITE_API_GET_FEEDBACK_URL url: API_GET_FEEDBACK_URL
}); });
return () => { return () => {
@ -37,7 +38,7 @@ export function UseAsync(
ApplicationState: state ApplicationState: state
}; };
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure()); const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
fetch(import.meta.env.VITE_API_GET_FEEDBACK_URL, { fetch(API_GET_FEEDBACK_URL, {
method: 'POST', method: 'POST',
headers: new Headers({ headers: new Headers({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention

View file

@ -3,7 +3,7 @@ import { IContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IPoint } from '../../Interfaces/IPoint'; import { IPoint } from '../../Interfaces/IPoint';
import { DIMENSION_MARGIN, USE_EXPERIMENTAL_CANVAS_API } from '../../utils/default'; 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 { BAR_WIDTH } from '../Bar/Bar';
import { Canvas } from '../Canvas/Canvas'; import { Canvas } from '../Canvas/Canvas';
import { AddDimensions } from '../Canvas/DimensionLayer'; import { AddDimensions } from '../Canvas/DimensionLayer';
@ -96,22 +96,32 @@ export function Viewer({
viewerHeight: window.innerHeight viewerHeight: window.innerHeight
}); });
const mainContainer = FindContainerById(current.containers, current.mainContainer);
if (mainContainer === undefined) {
return <></>;
}
UseSVGAutoResizerOnWindowResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer); UseSVGAutoResizerOnWindowResize(isLeftSidebarOpen, isRightSidebarOpen, setViewer);
UseSVGAutoResizerOnSidebar(isLeftSidebarOpen, isRightSidebarOpen, setViewer); UseSVGAutoResizerOnSidebar(isLeftSidebarOpen, isRightSidebarOpen, setViewer);
if (USE_EXPERIMENTAL_CANVAS_API) { if (USE_EXPERIMENTAL_CANVAS_API) {
function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void { function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): void {
const topDim = current.mainContainer.properties.y; if (mainContainer === undefined) {
const leftDim = current.mainContainer.properties.x; return;
const rightDim = current.mainContainer.properties.x + current.mainContainer.properties.width; }
const bottomDim = current.mainContainer.properties.y + current.mainContainer.properties.height;
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.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save(); ctx.save();
ctx.setTransform(scale, 0, 0, scale, translatePos.x, translatePos.y); ctx.setTransform(scale, 0, 0, scale, translatePos.x, translatePos.y);
ctx.fillStyle = '#000000'; 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) { for (const { container, depth, currentTransform } of it) {
const [x, y] = [ const [x, y] = [
container.properties.x + currentTransform[0], container.properties.x + currentTransform[0],
@ -130,6 +140,7 @@ export function Viewer({
rightDim, rightDim,
depth, depth,
scale, scale,
current.containers,
container, container,
currentTransform currentTransform
); );
@ -160,13 +171,14 @@ export function Viewer({
className={marginClasses} className={marginClasses}
viewerWidth={viewer.viewerWidth} viewerWidth={viewer.viewerWidth}
viewerHeight={viewer.viewerHeight} viewerHeight={viewer.viewerHeight}
width={current.mainContainer?.properties.width} width={mainContainer.properties.width}
height={current.mainContainer?.properties.height} height={mainContainer.properties.height}
containers={current.containers}
selected={selectedContainer} selected={selectedContainer}
symbols={current.symbols} symbols={current.symbols}
selectContainer={selectContainer} selectContainer={selectContainer}
> >
{current.mainContainer} {mainContainer}
</SVG> </SVG>
); );
} }
@ -188,6 +200,7 @@ function RenderDimensions(
rightDim: number, rightDim: number,
depth: number, depth: number,
scale: number, scale: number,
containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number] currentTransform: [number, number]
): void { ): void {
@ -197,7 +210,7 @@ function RenderDimensions(
const containerBottomDim = bottomDim + (DIMENSION_MARGIN * (depth + 1)) / scale; const containerBottomDim = bottomDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
const containerRightDim = rightDim + (DIMENSION_MARGIN * (depth + 1)) / scale; const containerRightDim = rightDim + (DIMENSION_MARGIN * (depth + 1)) / scale;
const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim]; const dimMapped = [containerLeftDim, containerBottomDim, containerTopDim, containerRightDim];
AddDimensions(ctx, container, dimMapped, currentTransform, scale, depth); AddDimensions(ctx, containers, container, dimMapped, currentTransform, scale, depth);
ctx.restore(); ctx.restore();
} }

View file

@ -174,7 +174,7 @@ function AppendContainer(root: Element | Document,
const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep); const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep);
const currentState = history[editorState.historyCurrentStep]; const currentState = history[editorState.historyCurrentStep];
const parent = FindContainerById(currentState.mainContainer, parentId); const parent = FindContainerById(currentState.containers, parentId);
const newHistory = AddContainerAction( const newHistory = AddContainerAction(
parent?.children.length ?? 0, parent?.children.length ?? 0,
@ -203,7 +203,7 @@ function AppendContainerToSelectedContainer(root: Element | Document,
const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep); const history = GetCurrentHistory(editorState.history, editorState.historyCurrentStep);
const currentState = 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( const newHistory = AddContainerToSelectedContainerAction(
type, type,

View file

@ -1,7 +1,7 @@
import { IContainerProperties } from './IContainerProperties'; import { IContainerProperties } from './IContainerProperties';
export interface IContainerModel { export interface IContainerModel {
children: IContainerModel[] children: string[]
parent: IContainerModel | null parent: IContainerModel | null
properties: IContainerProperties properties: IContainerProperties
userData: Record<string, string | number> userData: Record<string, string | number>
@ -12,7 +12,7 @@ export interface IContainerModel {
* Do not add methods since they will be lost during serialization * Do not add methods since they will be lost during serialization
*/ */
export class ContainerModel implements IContainerModel { export class ContainerModel implements IContainerModel {
public children: IContainerModel[]; public children: string[];
public parent: IContainerModel | null; public parent: IContainerModel | null;
public properties: IContainerProperties; public properties: IContainerProperties;
public userData: Record<string, string | number>; public userData: Record<string, string | number>;
@ -20,7 +20,7 @@ export class ContainerModel implements IContainerModel {
constructor( constructor(
parent: IContainerModel | null, parent: IContainerModel | null,
properties: IContainerProperties, properties: IContainerProperties,
children: IContainerModel[] = [], children: string[] = [],
userData = {}) { userData = {}) {
this.parent = parent; this.parent = parent;
this.properties = properties; this.properties = properties;

View file

@ -6,11 +6,12 @@ export interface IHistoryState {
lastAction: string lastAction: string
/** Reference to the main container */ /** Reference to the main container */
mainContainer: IContainerModel mainContainer: string
// TODO: Add hashmap<string, IContainerModel> to optimize FincContainerById from worst O(n) to O(1) // TODO: Add hashmap<string, IContainerModel> 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: 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 // TODO: Update addContainers and deleteContainer to update the hashmap
containers: Map<string, IContainerModel>
/** Id of the selected container */ /** Id of the selected container */
selectedContainerId: string selectedContainerId: string

View file

@ -124,6 +124,8 @@ export function GetDefaultEditorState(configuration: IConfiguration): IEditorSta
null, null,
mainContainerConfig mainContainerConfig
); );
const containers = new Map<string, IContainerModel>();
containers.set(mainContainer.properties.id, mainContainer);
const typeCounters = {}; const typeCounters = {};
(typeCounters as any)[mainContainer.properties.type] = 0; (typeCounters as any)[mainContainer.properties.type] = 0;
@ -133,7 +135,8 @@ export function GetDefaultEditorState(configuration: IConfiguration): IEditorSta
history: [ history: [
{ {
lastAction: '', lastAction: '',
mainContainer, mainContainer: mainContainer.properties.id,
containers,
selectedContainerId: mainContainer.properties.id, selectedContainerId: mainContainer.properties.id,
typeCounters, typeCounters,
symbols: new Map(), symbols: new Map(),
@ -175,6 +178,13 @@ export const DEFAULT_CONFIG: IConfiguration = {
/* eslint-enable */ /* eslint-enable */
}; };
const DEFAULT_CONTAINER_STYLE = {
stroke: 'black',
fillOpacity: 1,
fill: 'white',
strokeWidth: 2
}
/** /**
* Default Main container properties * Default Main container properties
*/ */
@ -203,10 +213,7 @@ export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = {
showDimensionWithMarks: [Position.Down, Position.Right], showDimensionWithMarks: [Position.Down, Position.Right],
markPosition: [], markPosition: [],
warning: '', warning: '',
style: { style: DEFAULT_CONTAINER_STYLE
stroke: 'black',
fillOpacity: 0
}
}; };
/** /**
@ -256,7 +263,7 @@ export function GetDefaultContainerProps(type: string,
showDimensionWithMarks: containerConfig.ShowDimensionWithMarks ?? [], showDimensionWithMarks: containerConfig.ShowDimensionWithMarks ?? [],
warning: '', warning: '',
customSVG: containerConfig.CustomSVG, customSVG: containerConfig.CustomSVG,
style: structuredClone(containerConfig.Style), style: Object.assign(structuredClone(DEFAULT_CONTAINER_STYLE), structuredClone(containerConfig.Style)),
userData: structuredClone(containerConfig.UserData) userData: structuredClone(containerConfig.UserData)
}); });
} }

View file

@ -1,9 +1,22 @@
import { IContainerModel } from '../Interfaces/IContainerModel'; import { IContainerModel } from '../Interfaces/IContainerModel';
export function * MakeChildrenIterator(containers: Map<string, IContainerModel>, childrenIds: string[]): Generator<IContainerModel, void, unknown> {
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 * Returns a Generator iterating of over the children depth-first
*/ */
export function * MakeDFSIterator(root: IContainerModel, enableHideChildrenInTreeview = false): Generator<IContainerModel, void, unknown> { export function * MakeDFSIterator(root: IContainerModel, containers: Map<string, IContainerModel>, enableHideChildrenInTreeview = false): Generator<IContainerModel, void, unknown> {
const queue: IContainerModel[] = [root]; const queue: IContainerModel[] = [root];
const visited = new Set<IContainerModel>(queue); const visited = new Set<IContainerModel>(queue);
while (queue.length > 0) { while (queue.length > 0) {
@ -16,8 +29,9 @@ export function * MakeDFSIterator(root: IContainerModel, enableHideChildrenInTre
} }
for (let i = container.children.length - 1; i >= 0; i--) { for (let i = container.children.length - 1; i >= 0; i--) {
const child = container.children[i]; const childId = container.children[i];
if (visited.has(child)) { const child = FindContainerById(containers, childId);
if (child === undefined || visited.has(child)) {
continue; continue;
} }
visited.add(child); visited.add(child);
@ -38,7 +52,7 @@ export interface ContainerAndDepthAndTransform extends ContainerAndDepth {
/** /**
* Returns a Generator iterating of over the children depth-first * Returns a Generator iterating of over the children depth-first
*/ */
export function * MakeBFSIterator(root: IContainerModel): Generator<ContainerAndDepth, void, unknown> { export function * MakeBFSIterator(root: IContainerModel, containers: Map<string, IContainerModel>): Generator<ContainerAndDepth, void, unknown> {
const queue: IContainerModel[] = [root]; const queue: IContainerModel[] = [root];
let depth = 0; let depth = 0;
while (queue.length > 0) { while (queue.length > 0) {
@ -51,7 +65,13 @@ export function * MakeBFSIterator(root: IContainerModel): Generator<ContainerAnd
}; };
for (let i = container.children.length - 1; i >= 0; i--) { for (let i = container.children.length - 1; i >= 0; i--) {
const child = container.children[i]; const childId = container.children[i];
const child = FindContainerById(containers, childId);
if (child === undefined) {
continue;
}
queue.push(child); queue.push(child);
} }
} }
@ -61,6 +81,7 @@ export function * MakeBFSIterator(root: IContainerModel): Generator<ContainerAnd
export function * MakeRecursionDFSIterator( export function * MakeRecursionDFSIterator(
root: IContainerModel | null, root: IContainerModel | null,
containers: Map<string, IContainerModel>,
depth: number, depth: number,
currentTransform: [number, number], currentTransform: [number, number],
enableHideChildrenInTreeview: boolean = false enableHideChildrenInTreeview: boolean = false
@ -79,9 +100,16 @@ export function * MakeRecursionDFSIterator(
return; return;
} }
for (const container of root.children) { for (const containerId of root.children) {
const container = FindContainerById(containers, containerId);
if (container === undefined) {
continue;
}
yield * MakeRecursionDFSIterator( yield * MakeRecursionDFSIterator(
container, container,
containers,
depth + 1, depth + 1,
[ [
currentTransform[0] + root.properties.x, currentTransform[0] + root.properties.x,
@ -185,8 +213,15 @@ export function ApplyParentTransform(
return [x, y]; 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<string, IContainerModel>, id: string): IContainerModel | undefined {
const it = MakeDFSIterator(root, containers);
for (const container of it) { for (const container of it) {
if (container.properties.id === id) { if (container.properties.id === id) {
return container; return container;
@ -195,6 +230,17 @@ export function FindContainerById(root: IContainerModel, id: string): IContainer
return undefined; 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<string, IContainerModel>, id: string): IContainerModel | undefined {
return containers.get(id);
}
export interface IPair<T> { export interface IPair<T> {
cur: T cur: T
next: T next: T

View file

@ -32,15 +32,22 @@ export function ReviveState(state: IHistoryState): void {
for (const symbol of state.symbols.values()) { for (const symbol of state.symbols.values()) {
symbol.linkedContainers = new Set(symbol.linkedContainers); 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) { for (const container of it) {
const parentId = container.properties.parentId; const parentId = container.properties.parentId;
if (parentId === null) { if (parentId === null) {
container.parent = null; container.parent = null;
continue; continue;
} }
const parent = FindContainerById(state.mainContainer, parentId); const parent = FindContainerById(state.containers, parentId);
if (parent === undefined) { if (parent === undefined) {
continue; continue;
} }
@ -54,6 +61,10 @@ export function GetCircularReplacer(): (key: any, value: object | Map<string, an
return; return;
} }
if (key === 'containers') {
return Array.from((value as Map<string, any>).entries());
}
if (key === 'symbols') { if (key === 'symbols') {
return Array.from((value as Map<string, any>).entries()); return Array.from((value as Map<string, any>).entries());
} }

3
src/vite-env.d.ts vendored
View file

@ -1,9 +1,6 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { 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... // more env variables...
} }

View file

@ -16,7 +16,7 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": ["src"], "include": ["src", "public/svgld-settings.d.ts", "public/svgld-settings.ts"],
"exclude": ["test-server"], "exclude": ["test-server"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }