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:
pnpm_config_cache: $(Pipeline.Workspace)/.pnpm-store
CSWebProjectLocation: '$(System.DefaultWorkingDirectory)/csharp/SVGLDLibs/SVGLDWebAPI/SVGLDWebAPI.csproj'
CSLibsProjectLocation: '$(System.DefaultWorkingDirectory)/csharp/SVGLDLibs/SVGLDLibs/SVGLDLibs.csproj'
CSLibsProjectModelsLocation: '$(System.DefaultWorkingDirectory)/csharp/SVGLDLibs/SVGLDLibs/Models'
buildConfiguration: 'Release'
steps:
- task: Cache@2
@ -47,7 +51,7 @@ steps:
set -euo pipefail
node --version
node ./test-server/http.js &
dotnet run --project=./csharp/SVGLDLibs/SVGLDWebAPI/SVGLDWebAPI.csproj &
dotnet run --project=$(CSWebProjectLocation) &
jobs
sleep 10
pnpm i
@ -58,3 +62,12 @@ steps:
- publish: $(System.DefaultWorkingDirectory)/dist
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;
[DataMember(EmitDefaultValue = false)]
public ContainerModel mainContainer;
public string mainContainer;
[DataMember(EmitDefaultValue = false)]
public Dictionary<string, ContainerModel> containers;
[DataMember(EmitDefaultValue = false)]
public string selectedContainerId;

View file

@ -6,7 +6,7 @@ namespace SVGLDLibs.Models
public class ContainerModel
{
[DataMember(EmitDefaultValue = false)]
public List<ContainerModel> children;
public List<string> children;
[DataMember(EmitDefaultValue = false)]
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.
## Configurer les options de build
## Configurer l'API
Il y a deux fichiers principaux à configurer :
- `.env.production.local`: pour configurer les URLs d'API
- `src/assets/svgld-settings.js`: pour configurer les URLs d'API
- `src/utils/default.ts`: pour configurer les fonctionnalités
Copiez `.env.production` vers `.env.production.local` et modifiez-le comme bon vous semble :
Modifiez `public/svgld-settings.js`
```
VITE_API_FETCH_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetSVGLayoutConfiguration
VITE_API_SET_CONTAINER_LIST_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/SetContainerList
VITE_API_GET_FEEDBACK_URL=https://localhost/SmartMenuiserieTemplate/Service.svc/GetFeedback
```js
export const API_FETCH_URL = 'http://localhost:5000';
export const API_SET_CONTAINER_LIST_URL = 'http://localhost:5000/SetContainerList';
export const API_GET_FEEDBACK_URL = 'http://localhost:5000/GetFeedback';
```
Vous pouvez modifiez `src/utils/default.ts` mais ne le committez pas.

View file

@ -9,6 +9,7 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/svgld-settings.js"></script>
<script type="module" src="./src/main.tsx"></script>
</body>
</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;
}
if (key === 'containers') {
return Array.from(value.entries());
}
if (key === 'symbols') {
return Array.from(value.entries());
}

View file

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { describe, it, expect } from 'vitest';
import { API_FETCH_URL } from '../../../public/svgld-settings';
import { AddMethod } from '../../Enums/AddMethod';
import { Orientation } from '../../Enums/Orientation';
import { Position } from '../../Enums/Position';
@ -10,7 +11,6 @@ import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
import { ICategory } from '../../Interfaces/ICategory';
import { IConfiguration } from '../../Interfaces/IConfiguration';
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
import { IEditorState } from '../../Interfaces/IEditorState';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IPattern } from '../../Interfaces/IPattern';
import { DEFAULT_MAINCONTAINER_PROPS, GetDefaultContainerProps } from '../../utils/default';
@ -21,9 +21,9 @@ const CHARP_WEB_API_RESOURCE_URL = 'SVGLD';
const CSHARP_WEB_API_URL = CSHARP_WEB_API_BASE_URL + CHARP_WEB_API_RESOURCE_URL + '/';
// TODO: Migrate this test to SVGLDWebAPI rather than using test-server/
describe.concurrent('Test server test', () => {
describe.concurrent('Test server test', async() => {
it('Load environment', () => {
const url = import.meta.env.VITE_API_FETCH_URL;
const url = API_FETCH_URL;
expect(url).toBe('http://localhost:5000');
});
@ -103,9 +103,11 @@ describe.concurrent('Models test suite', () => {
DEFAULT_MAINCONTAINER_PROPS
);
const containers = new Map<string, IContainerModel>();
const historyState: IHistoryState = {
lastAction: 'string',
mainContainer,
mainContainer: mainContainer.properties.id,
containers,
selectedContainerId: '3',
typeCounters: {
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 { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest';
import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse';
@ -8,7 +9,7 @@ import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload';
* @returns {Configation} The model of the configuration for the application
*/
export async function FetchConfiguration(): Promise<IConfiguration> {
const url = import.meta.env.VITE_API_FETCH_URL;
const url = API_FETCH_URL;
// The test library cannot use the Fetch API
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
@ -34,7 +35,7 @@ export async function FetchConfiguration(): Promise<IConfiguration> {
}
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());
// The test library cannot use the Fetch API
// @ts-expect-error

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,6 +18,7 @@ import { Settings } from '../Settings/Settings';
import { IMessage } from '../../Interfaces/IMessage';
import { DISABLE_API } from '../../utils/default';
import { UseWorker, UseAsync } from './UseWorker';
import { FindContainerById } from '../../utils/itertools';
export interface IUIProps {
selectedContainer: IContainerModel | undefined
@ -89,6 +90,12 @@ export function UI(props: IUIProps): JSX.Element {
let leftChildren: JSX.Element = (<></>);
let rightChildren: JSX.Element = (<></>);
const mainContainer = FindContainerById(props.current.containers, props.current.mainContainer)
if (mainContainer === undefined) {
throw new Error('Tried to initialized UI but there is no main container!');
}
switch (selectedSidebar) {
case SidebarType.Components:
leftSidebarTitle = 'Components';
@ -100,7 +107,8 @@ export function UI(props: IUIProps): JSX.Element {
/>;
rightSidebarTitle = 'Elements';
rightChildren = <ElementsList
mainContainer={props.current.mainContainer}
containers={props.current.containers}
mainContainer={mainContainer}
symbols={props.current.symbols}
selectedContainer={props.selectedContainer}
onPropertyChange={props.onPropertyChange}

View file

@ -4,6 +4,7 @@ import { IGetFeedbackRequest } from '../../Interfaces/IGetFeedbackRequest';
import { IGetFeedbackResponse } from '../../Interfaces/IGetFeedbackResponse';
import { IMessage } from '../../Interfaces/IMessage';
import { GetCircularReplacerKeepDataStructure } from '../../utils/saveload';
import { API_GET_FEEDBACK_URL } from '../../../public/svgld-settings';
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
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
myWorker.postMessage({
state,
url: import.meta.env.VITE_API_GET_FEEDBACK_URL
url: API_GET_FEEDBACK_URL
});
return () => {
@ -37,7 +38,7 @@ export function UseAsync(
ApplicationState: state
};
const dataParsed = JSON.stringify(request, GetCircularReplacerKeepDataStructure());
fetch(import.meta.env.VITE_API_GET_FEEDBACK_URL, {
fetch(API_GET_FEEDBACK_URL, {
method: 'POST',
headers: new Headers({
// eslint-disable-next-line @typescript-eslint/naming-convention

View file

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

View file

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

View file

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

View file

@ -6,11 +6,12 @@ export interface IHistoryState {
lastAction: string
/** 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: this hashmap will not be serialized, modify it in the replacer and reviver in saveload.ts + worker.js
// TODO: Update addContainers and deleteContainer to update the hashmap
containers: Map<string, IContainerModel>
/** Id of the selected container */
selectedContainerId: string

View file

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

View file

@ -1,9 +1,22 @@
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
*/
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 visited = new Set<IContainerModel>(queue);
while (queue.length > 0) {
@ -16,8 +29,9 @@ export function * MakeDFSIterator(root: IContainerModel, enableHideChildrenInTre
}
for (let i = container.children.length - 1; i >= 0; i--) {
const child = container.children[i];
if (visited.has(child)) {
const childId = container.children[i];
const child = FindContainerById(containers, childId);
if (child === undefined || visited.has(child)) {
continue;
}
visited.add(child);
@ -38,7 +52,7 @@ export interface ContainerAndDepthAndTransform extends ContainerAndDepth {
/**
* Returns a Generator iterating of over the children depth-first
*/
export function * MakeBFSIterator(root: IContainerModel): Generator<ContainerAndDepth, void, unknown> {
export function * MakeBFSIterator(root: IContainerModel, containers: Map<string, IContainerModel>): Generator<ContainerAndDepth, void, unknown> {
const queue: IContainerModel[] = [root];
let depth = 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--) {
const child = container.children[i];
const childId = container.children[i];
const child = FindContainerById(containers, childId);
if (child === undefined) {
continue;
}
queue.push(child);
}
}
@ -61,6 +81,7 @@ export function * MakeBFSIterator(root: IContainerModel): Generator<ContainerAnd
export function * MakeRecursionDFSIterator(
root: IContainerModel | null,
containers: Map<string, IContainerModel>,
depth: number,
currentTransform: [number, number],
enableHideChildrenInTreeview: boolean = false
@ -79,9 +100,16 @@ export function * MakeRecursionDFSIterator(
return;
}
for (const container of root.children) {
for (const containerId of root.children) {
const container = FindContainerById(containers, containerId);
if (container === undefined) {
continue;
}
yield * MakeRecursionDFSIterator(
container,
containers,
depth + 1,
[
currentTransform[0] + root.properties.x,
@ -185,8 +213,15 @@ export function ApplyParentTransform(
return [x, y];
}
export function FindContainerById(root: IContainerModel, id: string): IContainerModel | undefined {
const it = MakeDFSIterator(root);
/**
* Returns the container by id
* @deprecated Please use FindContainerById
* @param root Root of the container tree
* @param id Id of the container to find
* @returns The container found or undefined if not found
*/
export function FindContainerByIdDFS(root: IContainerModel, containers: Map<string, IContainerModel>, id: string): IContainerModel | undefined {
const it = MakeDFSIterator(root, containers);
for (const container of it) {
if (container.properties.id === id) {
return container;
@ -195,6 +230,17 @@ export function FindContainerById(root: IContainerModel, id: string): IContainer
return undefined;
}
/**
* Returns the container by id
* For now, does the same as containers.get(id)
* @param containers Map of containers
* @param id id of container
* @returns Container by id
*/
export function FindContainerById(containers: Map<string, IContainerModel>, id: string): IContainerModel | undefined {
return containers.get(id);
}
export interface IPair<T> {
cur: T
next: T

View file

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

View file

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