Sort documentation
This commit is contained in:
parent
b64bc8cb6a
commit
3627a9718e
23 changed files with 18 additions and 1 deletions
441
docs/#Project/Pages/Behaviors.md
Normal file
441
docs/#Project/Pages/Behaviors.md
Normal file
|
@ -0,0 +1,441 @@
|
|||
# Comportements des conteneurs
|
||||
|
||||
Ce document traite des comportements spéciaux et uniques qu'un conteneur peut avoir.
|
||||
|
||||
Chaque comportement est documenté dans une section avec 3 sous-sections :
|
||||
- Règles
|
||||
- Applications
|
||||
- Références de code et algorithmes
|
||||
|
||||
|
||||
# Comportement par défaut
|
||||
|
||||
Le comportement par défaut est le panneau flottant. Il peut se déplacer et se redimensionner mais pas ses frères et soeurs.
|
||||
|
||||
|
||||
## Règles
|
||||
|
||||
Le comportement par défaut n'a pas de règles particulières qui s'appliquent à lui-même.
|
||||
|
||||
Cependant, il a une règle commune pour tout comportement qui s'applique à ses enfants.
|
||||
|
||||
Il s'agit d'appliquer les comportements spéciaux de ses enfants (rigide ou ancré).
|
||||
|
||||
Traduit avec www.DeepL.com/Translator (version gratuite)
|
||||
|
||||
|
||||
## Applications
|
||||
|
||||
The default behavior is important to have a good user experience when adding object.
|
||||
|
||||
The golden rule is to never oppose the user which is why we don't want to applies rigid body by default
|
||||
as it can block the addition of container.
|
||||
Allowing freedom of movement can help for better precision if not the same as the rigid property.
|
||||
|
||||
An example would be trying to overlap an element in order to use it as a layer.
|
||||
|
||||
|
||||
## Références de code et algorithmes
|
||||
|
||||
Dans le module `PropertiesOperations.ts` dans les fonctions suivantes :
|
||||
- `OnPropertyChange()`
|
||||
- `OnPropertiesSubmit()`
|
||||
|
||||
et dans le module `ContainerOperation.ts` dans `AddContainer()`,
|
||||
|
||||
il utilise la fonction `ApplyBehaviors` du module `Behaviors.ts` pour appliquer les comportements spéciaux de ses enfants.
|
||||
|
||||
|
||||
|
||||
# Comportement du corps rigide
|
||||
|
||||
Le comportement de corps rigide est un comportement spécial
|
||||
qui permet de restreindre un conteneur dans un espace donné.
|
||||
|
||||
|
||||
## Règles
|
||||
|
||||
Les principales règles sont :
|
||||
- Le conteneur rigide doit être maintenu à l'intérieur de son conteneur parent.
|
||||
- Le conteneur rigide doit se trouver à l'intérieur d'un espace non alloué de son parent. C'est-à-dire qu'il ne peut pas se superposer à un autre frère ou sœur.
|
||||
|
||||
|
||||
## Applications
|
||||
|
||||
Ce comportement a de nombreuses applications. Principalement sur les recalculs.
|
||||
|
||||
Vous pouvez vouloir redimensionner/déplacer rapidement et être certain qu'il ne déborde pas de son parent.
|
||||
|
||||
Vous pouvez vouloir redimensionner son parent et faire en sorte qu'il redimensionne ses enfants.
|
||||
|
||||
Vous pouvez vouloir que les frères et sœurs interagissent les uns avec les autres.
|
||||
|
||||
|
||||
## Références de code et algorithmes
|
||||
|
||||
Son algorithme peut être un peu compliqué en raison des nombreux cas d'utilisation.
|
||||
|
||||
|
||||
### Première règle
|
||||
|
||||
Commençons par la première règle : *Le conteneur rigide doit être maintenu à l'intérieur de son conteneur parent*.
|
||||
|
||||
Dans le fichier `RigidBodyBehaviors.ts`, voyez `constraintBodyInsideParent()` et `constraintBodyInsideSpace()`.
|
||||
|
||||
Comme vous pouvez le voir, `constraintBodyInsideParent()` n'est qu'une enveloppe pour `constraintBodyInsideSpace()`, donc étudions juste cette dernière fonction.
|
||||
|
||||
C'est un problème simple de deux rectangles.
|
||||
|
||||
Afin de restreindre l'enfant à son parent,
|
||||
nous devons d'abord savoir si l'enfant n'est pas plus grand que son parent.
|
||||
|
||||
Si c'est le cas, il suffit de placer l'enfant au début et de lui faire prendre la taille complète de son parent.
|
||||
|
||||
Si ce n'est pas le cas, nous devons vérifier si l'enfant est hors limites (en dehors de son parent). Et si c'est le cas, nous devons le ramener à l'intérieur.
|
||||
|
||||
Pour vérifier s'il est plus grand que son parent, il suffit de comparer leurs tailles : `childWidth > parentWidth` et verticalement `childHeight > parentHeight`.
|
||||
|
||||
Si c'est faux, nous devons vérifier la sortie de l'objet, vérifier pour x (et y) : `child.x < parent.x` pour le côté gauche ou `child.x + child.width > parent.x + parent.width` pour le côté droit. Nous ne voulons pas non plus de chevauchement, c'est pourquoi nous utilisons `child.width`.
|
||||
|
||||
La condition est également équivalente à `child.x > parent.x + parent.width - child.width` qui pourrait être plus logique puisque l'espace requis doit être plus petit à cause de la taille de l'enfant.
|
||||
|
||||
|
||||
Dans mon algorithme, j'ai décidé de les placer près du bord où ils sont sortis de la limite :
|
||||
|
||||
```
|
||||
left oob: child.x = parent.x
|
||||
right oob: child.x = parent.x + parent.width - child.width
|
||||
```
|
||||
|
||||
Pseudo-code :
|
||||
|
||||
```c
|
||||
constraintBodyInsideSpace(child, parent) {
|
||||
if (child is bigger than parent) {
|
||||
if (child is larger) {
|
||||
set child x and width;
|
||||
}
|
||||
|
||||
if (child is taller) {
|
||||
set child y and height;
|
||||
}
|
||||
} else {
|
||||
if (child is to the left of parent) {
|
||||
set child x at the left side of parent
|
||||
}
|
||||
|
||||
if (child is to the right of parent) {
|
||||
set child x at the right side of parent
|
||||
}
|
||||
|
||||
if (child is higher than parent) {
|
||||
set child y at the top of parent
|
||||
}
|
||||
|
||||
if (child is lower than parent) {
|
||||
set child y at the bottom of parent
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Deuxième règle
|
||||
|
||||
La deuxième règle est la plus importante et la plus compliquée car elle doit interagir avec ses frères et sœurs.
|
||||
|
||||
*Le conteneur rigide doit se trouver dans un espace non alloué de son parent. Ce qui signifie qu'il ne peut pas chevaucher un autre frère ou une autre soeur.
|
||||
|
||||
Définissons d'abord ce qu'est un *espace* : un *espace* est la largeur d'un conteneur. Ce qui signifie donc que la règle ne s'applique que sur la vue horizontale. Pour simplifier la chose, cela signifie également que nous ne devons travailler que sur une seule dimension.
|
||||
|
||||
Pour résoudre ce problème, comme pour le parent, nous pourrions utiliser la détection de collision entre ses frères et sœurs. Cependant, cela pourrait être très lent car le pire scénario est un produit cartésien : O(n2). En effet, pour chaque conteneur, nous devons rechercher les autres conteneurs qui entrent en collision avec lui. Lorsqu'il entre en collision, nous devons le déplacer et recommencer la recherche.
|
||||
|
||||
Rappelez-vous, cette règle est appliquée chaque fois que vous changez une propriété du conteneur, c'est le *lag*. Nous ne pouvons pas nous permettre des boucles inefficaces.
|
||||
|
||||
Utilisons un "système d'espace" qui a des "conteneurs" qui ne peuvent pas "se chevaucher".
|
||||
|
||||
La mémoire.
|
||||
|
||||
La mémoire, la RAM, l'espace du disque dur, gèrent leur espace par un système d'adresses et de morceaux d'espaces (mots, octets...). Dans notre cas, nous n'avons pas de morceaux d'espaces, mais des nombres flottants (qui peuvent être un casse-tête à cause des cas limites).
|
||||
|
||||
Ce système est particulièrement utile car il se souvient de l'espace utilisé après chaque itération d'allocation, ce qui signifie que nous pouvons savoir exactement quand il n'y a plus d'espace à l'intérieur du parent et quand un conteneur doit se redimensionner afin de tenir à l'intérieur.
|
||||
|
||||
Bien, commençons l'algorithme. Voir `constraintBodyInsideUnallocatedWidth()`, `getAvailableWidths()` et `getAvailableWidthsTwoLines()` dans `RigidBodyBehaviors.ts` pour les références de l'implémentation.
|
||||
|
||||
Nous avons initialement tout l'espace disponible : laissez `space` être cet espace disponible dans le parent.
|
||||
|
||||
`space` est un pointeur, donc au début il a `0` à son adresse de pointeur et `parent.width` comme espace.
|
||||
|
||||
Pour simplifier l'algorithme lors de l'ajout d'un conteneur, comparons cela à manger une bûche de Noël.
|
||||
|
||||

|
||||
|
||||
Comme pour le gâteau, il faut le couper et en prendre une part.
|
||||
|
||||
Il y a 5 façons possibles de le couper :
|
||||
- Ne pas manger le gâteau (on préfère peut-être manger un autre gâteau/une autre part)
|
||||
- Manger le gâteau entier
|
||||
- Couper le gâteau à gauche
|
||||
- Couper le gâteau sur le côté droit
|
||||
- Couper le gâteau au milieu
|
||||
|
||||
Ne pas couper le gâteau signifie rendre le gâteau entier tel quel.
|
||||
|
||||
Manger le gâteau entier, c'est ne rien rendre.
|
||||
|
||||
Couper à gauche ou à droite signifie laisser une partie.
|
||||
|
||||
Couper au milieu signifie laisser deux parts.
|
||||
|
||||
Après avoir coupé le gâteau, *pendant* qu'il en reste encore, on peut continuer l'opération. (c'est une boucle *pour* dans le code cependant pour des raisons de syntaxe)
|
||||
|
||||
Cependant, après avoir servi les frères et sœurs, nous pouvons remarquer qu'il n'en reste plus pour nous. Nous nous mettons en colère, nous *jetons* une crise de colère.
|
||||
|
||||
```c
|
||||
// si vous n'avez pas compris la blague
|
||||
if (there no more cake) {
|
||||
throw tantrum
|
||||
}
|
||||
```
|
||||
|
||||
Attends, il y a vraiment du gâteau !
|
||||
|
||||
Mais il est laissé en plusieurs morceaux, nous allons juste prendre le plus proche qui correspond à notre faim.
|
||||
|
||||
S'il y en a un qui correspond à notre faim, prenons-le !
|
||||
|
||||
Pourtant ! Il y a des gâteaux mais aucun ne correspond à notre faim. Mais nous avons une acceptation *minimale*, soyons humbles, nous prendrons quand même la petite part. D'ailleurs, prendre plusieurs parts serait mauvais pour nous. Néanmoins, si mon acceptation *minimale* devait être supérieure à ce qui reste, je lancerais un *avertissement* pour la prochaine fois.
|
||||
|
||||
|
||||
Traduisons cela en pseudo-code.
|
||||
|
||||
Commençons par obtenir les espaces disponibles :
|
||||
|
||||
```typescript
|
||||
getAvailableSpaces(parent, me) {
|
||||
spaces = [{ x: 0, size: parent.width }]
|
||||
|
||||
let i = 0
|
||||
while (spaces.length > 0 and i < parent.length) {
|
||||
let sibling = parent.children[i];
|
||||
|
||||
if (sibling is me or is neither rigid nor is anchor) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let spacesLeft be an array
|
||||
|
||||
foreach(space in spaces) {
|
||||
spacesLeftOfSpace = allocate(sibling, space);
|
||||
spacesLeft.concat(spacesLeftOfSpace)
|
||||
}
|
||||
|
||||
spaces = spacesLeft
|
||||
i++
|
||||
}
|
||||
|
||||
return spaces;
|
||||
}
|
||||
```
|
||||
|
||||
Pour allouer :
|
||||
|
||||
```typescript
|
||||
allocate(sibling, space) {
|
||||
if (sibling is not overlapping the space) {
|
||||
return [space]
|
||||
}
|
||||
|
||||
if (sibling overlap the space entirely) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (sibling overlap at the left side) {
|
||||
return [{
|
||||
x: right side of sibling
|
||||
size: right side of space - right side of sibling // "cut the left part"
|
||||
}]
|
||||
}
|
||||
|
||||
if (sibling overlap at the right side) {
|
||||
return [{
|
||||
x: left side of space
|
||||
size: leftSide of sibling - leftSide of space
|
||||
}]
|
||||
}
|
||||
|
||||
// if (sibling overlap in the middle)
|
||||
return [
|
||||
{
|
||||
x: left side of space
|
||||
size: left side of sibling - left side of space
|
||||
},
|
||||
{
|
||||
x: right side of sibling
|
||||
size: right side of space - right side of sibling
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Enfin pour l'appelant :
|
||||
|
||||
```typescript
|
||||
constraintBodyInsideUnallocatedWidth(parent, container) {
|
||||
spaces = getAvailableSpaces(parent, container)
|
||||
if (there is no more spaces) {
|
||||
throw error
|
||||
}
|
||||
|
||||
spaces = sort spaces by closest from the middle of container
|
||||
|
||||
spaceFound = spaces.find(space that fit container.space)
|
||||
|
||||
if (no spaceFound) {
|
||||
|
||||
spaceFound = spaces.find(space that fit container.minimumSpace)
|
||||
|
||||
if (no spaceFound) {
|
||||
show warning
|
||||
return container
|
||||
}
|
||||
|
||||
set container x and width to make it fit
|
||||
return container;
|
||||
}
|
||||
|
||||
constraintBodyInsideSpace(container, spaceFound)
|
||||
}
|
||||
```
|
||||
|
||||
Cet algorithme est génial, mais certains problèmes subsistent :
|
||||
- Trouver le plus proche prend O(nlogn), n étant le nombre d'espaces. Ce n'est généralement pas mauvais puisque l'objectif du corps rigide est de **remplir** l'espace. Mais il y a toujours un très mauvais cas de figure.
|
||||
- Il y a 2 recherches pour l'espace, même problème mais le tri précédent aide à le rendre plus rapide pour les meilleurs cas.
|
||||
|
||||
|
||||
|
||||
# Comportement d'ancrage
|
||||
|
||||
Le comportement d'ancrage permet à un conteneur d'être prioritaire sur ses frères et sœurs.
|
||||
|
||||
|
||||
## Règles
|
||||
|
||||
Il a les règles suivantes :
|
||||
- Le conteneur ne peut pas être déplacé par un autre conteneur frère ou sœur rigide.
|
||||
- Le conteneur ne peut pas être redimensionné par un autre conteneur de la même famille.
|
||||
- Le conteneur ne peut pas chevaucher un autre conteneur rigide de la même famille :
|
||||
- les conteneurs qui se chevauchent sont déplacés vers l'espace/largeur disponible le plus proche
|
||||
- ou redimensionnés lorsqu'il n'y a plus d'autre espace disponible que le leur
|
||||
- ou perdent leurs propriétés de corps rigide lorsqu'il n'y a absolument plus d'espace disponible (même pas le leur).
|
||||
|
||||
|
||||
## Applications
|
||||
|
||||
Le gain de priorité permet de s'assurer qu'un objet rigide ne bougera pas, quoi qu'il arrive, et qu'il bougera absolument, peu importe ce qui se trouve sous lui.
|
||||
|
||||
|
||||
## Références de code et algorithmes
|
||||
|
||||
Bien qu'il y ait plusieurs règles appliquées à ce comportement, la plupart d'entre elles ne sont que des conditions.
|
||||
|
||||
Ces trois règles :
|
||||
- Le conteneur ne peut pas être déplacé par d'autres conteneurs frères et sœurs rigides.
|
||||
- Le conteneur ne peut pas être redimensionné par un autre conteneur de la même famille.
|
||||
- Il ne peut pas chevaucher un autre conteneur rigide de la même famille.
|
||||
|
||||
Il peut être traduit en un seul : "Le conteneur est un espace alloué, donc tout conteneur en contact se déplacera ou sera redimensionné".
|
||||
|
||||
Ce qui signifie que l'application des propriétés du corps rigide du frère ou de la sœur appliquera également cette règle. La différence entre le comportement par défaut et le comportement d'ancrage est que le conteneur d'ancrage sera pris en compte lors du calcul de l'espace disponible.
|
||||
|
||||
Vous pouvez considérer le conteneur par défaut comme un panneau flottant et le conteneur d'ancrage comme un mur. Vous pouvez passer sous le panneau flottant mais pas par-dessus le mur.
|
||||
|
||||
Pour optimiser l'algorithme, il suffit de trouver les frères et sœurs qui se chevauchent puisque l'ancre n'est pas appliquée à ceux qui ne sont pas en collision.
|
||||
|
||||
Pseudo-code :
|
||||
|
||||
```c#
|
||||
ImposePosition(container) {
|
||||
let rigidBodies be the rigid siblings that are not anchor
|
||||
let overlappingRigidBodies be the overlapping rigid siblings of rigidBodies
|
||||
|
||||
foreach(overlappingRigidBody of overlappingRigidBodies) {
|
||||
constraintBodyInsideUnallocatedWidth(overlappingRigidBody)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
De plus, nous devons modifier `getAvailableSpaces()` pour qu'il prenne en compte les conteneurs d'ancrage.
|
||||
|
||||
|
||||
# Le comportement Flex
|
||||
|
||||
Le comportement flex est un comportement qui modifie à la fois la position et la taille d'un conteneur tout en interagissant avec ses frères et sœurs.
|
||||
|
||||
|
||||
## Application
|
||||
|
||||
Le comportement flex est utile pour le redimensionnement automatique d'un conteneur en fonction de son parent et de ses frères et sœurs.
|
||||
|
||||
|
||||
## Références de code et algorithmes
|
||||
|
||||
Tout d'abord, nous devons déterminer ce qu'est un espace flexible. Un espace flexible est la zone située entre deux objets d'ancrage, un objet d'ancrage pouvant être un conteneur ancré ou les bords du conteneur parent.
|
||||
|
||||
L'algorithme pour trouver un espace flexible est d'itérer sur les conteneurs et de créer un groupe à chaque fois qu'une ancre est trouvée (voir `GetFlexibleGroups`).
|
||||
|
||||
Ensuite, pour appliquer le flex dans ces groupes, il y a trois scénarios principaux :
|
||||
- Il n'y a pas assez d'espace même si l'on comprime tous les conteneurs à leur largeur minimale.
|
||||
- Il y a suffisamment d'espace pour que tous les conteneurs aient la même taille.
|
||||
- Il n'y a pas assez d'espace pour que tous les conteneurs aient la même taille, mais nous pouvons les comprimer.
|
||||
|
||||
Dans le premier scénario, il suffit de renvoyer une erreur.
|
||||
|
||||
Dans le second scénario, nous devons changer la taille de chaque conteneur pour qu'ils aient la même largeur. Ainsi, `wantedWidth = sum(space_width) / n`. La position sera alors `containerIndex * wantedWidth`.
|
||||
|
||||
Enfin, dans le troisième scénario, il existe plusieurs façons de procéder. Nous pourrions simplement appliquer le comportement de poussée tout en redimensionnant un par un chaque conteneur jusqu'à ce qu'ils s'adaptent, mais cela peut ne pas avoir de solution et coûtera un total de O(n2) de complexité.
|
||||
|
||||
L'autre moyen est d'utiliser la programmation linéaire puisque nous pouvons traduire ce problème comme un programme linéaire : la fonction objectif serait de maximiser la largeur de tous les conteneurs sans déborder du conteneur parent `max sum(width_i) <= wantedWidth`. Les inéquations supplémentaires sont les contraintes de largeur minimale et maximale.
|
||||
|
||||
L'algorithme que nous allons utiliser est l'algorithme du simplexe. Il s'agit d'un algorithme populaire dans le domaine de l'optimisation mathématique.
|
||||
|
||||
|
||||
|
||||
# Comportement de poussée
|
||||
|
||||
Lorsqu'on a un conteneur à droite et qu'on en ajoute un nouveau à sa droite, on pousse ce conteneur à gauche s'il y a assez de place.
|
||||
|
||||
|
||||
## Application
|
||||
|
||||
Permet d'ajouter un nouveau conteneur sans empiéter sur les conteneurs existants lorsqu'il y a suffisamment d'espace à leur gauche.
|
||||
|
||||
|
||||
## Références de code et algorithmes
|
||||
|
||||
Afin de pousser, nous devons trouver les trous à la gauche du nouveau conteneur. Lorsque nous trouvons un trou, nous devons pousser le conteneur à la droite du trou jusqu'à ce qu'il n'y ait plus de place.
|
||||
|
||||
Pour trouver un trou, nous devons itérer par paire (deux par deux) de la droite vers la gauche.
|
||||
|
||||
Pour pousser le conteneur de droite, nous pouvons simplement soustraire sa position de la taille du trou.
|
||||
|
||||
Lorsque l'espace restant est égal à 0, nous pouvons arrêter de pousser.
|
||||
|
||||
Comme vous pouvez le constater, cet algorithme est très lent et peut coûter jusqu'à O(n2). Et comme les calculs se chevauchent, il est également possible qu'en raison de la précision de la virgule flottante, il reste un espace minuscule.
|
||||
|
||||
|
||||
# Comportement de swap
|
||||
|
||||
Lorsque deux conteneurs sont en collision, la position des deux conteneurs est permutée.
|
||||
|
||||
|
||||
## Application
|
||||
|
||||
Lorsqu'il n'y a plus de place et que le corps rigide est activé, pour déplacer un conteneur dans la ligne, nous pouvons permuter deux conteneurs en augmentant x.
|
||||
|
||||
|
||||
## Références de code et algorithmes
|
||||
|
||||
Le code pour ce comportement est très simple :
|
||||
- Lorsque deux conteneurs se chevauchent, on permute leur position.
|
13
docs/#Project/Pages/CICD.md
Normal file
13
docs/#Project/Pages/CICD.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Azure Pipelines
|
||||
|
||||
This project uses Azure Pipelines to runs automatic tests.
|
||||
|
||||
Its `azure-pipelines.yml` configuration file can be found at the root project folder.
|
||||
|
||||
|
||||
# Drone.io
|
||||
|
||||
Due to the limitations of Azure Pipelines (limited free usage, no parallel, no dockerhub...), it might be more useful to use Drone.io.
|
||||
However `pnpm` will not be as useful as in Azure Pipelines since we cannot cache on the parent machine.
|
||||
|
||||
Its config file can be found in `.drone.yml`.
|
BIN
docs/#Project/Pages/ComponentStructure.drawio
(Stored with Git LFS)
Normal file
BIN
docs/#Project/Pages/ComponentStructure.drawio
(Stored with Git LFS)
Normal file
Binary file not shown.
217
docs/#Project/Pages/DataStructure.md
Normal file
217
docs/#Project/Pages/DataStructure.md
Normal 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.
|
80
docs/#Project/Pages/Dependencies.md
Normal file
80
docs/#Project/Pages/Dependencies.md
Normal file
|
@ -0,0 +1,80 @@
|
|||
# Dependencies
|
||||
|
||||
This document briefly explains the different dependencies located in the `package.json`.
|
||||
|
||||
This document will not explain how to use them. You can read their documentation for that and the codebase have exemples for references.
|
||||
|
||||
|
||||
# [React](https://reactjs.org/)
|
||||
|
||||
Main framework to build the js application.
|
||||
|
||||
It depends on Vite in order to build the project.
|
||||
|
||||
Others dependencies:
|
||||
- [react-dom](https://reactjs.org/docs/react-dom.html): library used to inject the app to `#root` html element.
|
||||
- [react-window](https://www.npmjs.com/package/react-window): component that offers component dynamic loading over scroll (very useful++)
|
||||
- [react-svg-pan-zoom](https://www.npmjs.com/package/react-svg-pan-zoom): component that offers pan + zoom to a svg element (if this gets deprecated, please try to migrate to HTML Canvas before trying a new library)
|
||||
|
||||
|
||||
# [Vite](https://vitejs.dev/)
|
||||
|
||||
Vite is the main tool to develop the react application.
|
||||
|
||||
Its uses the following files to configure the project :
|
||||
- `vite.config.ts`
|
||||
- `.env*`
|
||||
- `src/vite-env.d.ts`
|
||||
|
||||
Others dependencies:
|
||||
- @vitejs/plugin-react
|
||||
|
||||
|
||||
# [Tailwind CSS](https://tailwindcss.com/)
|
||||
|
||||
CSS framework designed around constraints with utility classes in order to reduce dead css code.
|
||||
|
||||
Its uses the following files to configure the project :
|
||||
- `src/index.scss`
|
||||
- `tailwind.config.cjs`
|
||||
- `postcss.config.cjs`
|
||||
|
||||
Other dependencies:
|
||||
- postcss
|
||||
- sass
|
||||
- autoprefixer
|
||||
|
||||
|
||||
# [Heroicons](https://heroicons.com/)
|
||||
|
||||
SVG Icons that can be used as JSX elements with Tailwind CSS
|
||||
|
||||
# [Interweave](https://interweave.dev/)
|
||||
|
||||
React library to render HTML from string.
|
||||
In this project, it is particularly used for the CustomSVG property.
|
||||
|
||||
If this dependencies gets deprecated please revert [PR#18 e96e4f12](https://dev.azure.com/enguyen0660/SVGLayoutDesignerReact/_git/SVGLayoutDesignerReact/commit/e96e4f123b4aa4c9cdb327d4d617ab0e63dc4d0f?refName=refs%2Fheads%2Fdev)
|
||||
|
||||
# Testing
|
||||
|
||||
- [Vitest](https://vitest.dev/)
|
||||
- [Testing Library](https://testing-library.com/)
|
||||
- [jsdom](https://github.com/jsdom/jsdom)
|
||||
|
||||
|
||||
# [eslint](https://eslint.org/)
|
||||
|
||||
A Linter. Used for error checking, syntax checking and code style enforcing.
|
||||
|
||||
Currently using `standard-with-typescript` with a few modification.
|
||||
|
||||
See the `.eslintrc.cjs` for more informations.
|
||||
|
||||
Other dependencies:
|
||||
- typescript-eslint/eslint-plugin
|
||||
- typescript-eslint/parser
|
||||
- eslint-plugin-import
|
||||
- eslint-plugin-n
|
||||
- eslint-plugin-promise
|
||||
- eslint-plugin-react
|
50
docs/#Project/Pages/Project_Structure.md
Normal file
50
docs/#Project/Pages/Project_Structure.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
# Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── .vscode/ VSCode config folder
|
||||
│ ├── launch.json VSCode debug config
|
||||
│ └── settings.json VSCode project config
|
||||
├── csharp/ C# models and test project
|
||||
├── docs/ Documentation folder
|
||||
├── public/ Public folder in which the index.html
|
||||
│ │ import its resources
|
||||
│ ├── smartcomponent/ SmartComponent folder
|
||||
│ └── workers/ Webworkers folder
|
||||
├── src/ Source folder for the react app
|
||||
│ ├── assets/ Assets folder in which the react app
|
||||
│ │ import its resources
|
||||
│ ├── Components/ Components folder
|
||||
│ ├── Enums/ Enums folder
|
||||
│ ├── Events/ API Events folder
|
||||
│ ├── Interfaces/ Interface (+ types folder)
|
||||
│ ├── test/ Setup folder for the tests
|
||||
│ ├── tests/ Other tests + resources
|
||||
│ ├── utils/ Utilities folder
|
||||
│ ├── index.scss Tailwind CSS extends
|
||||
│ ├── main.tsx Entrypoint for App injection
|
||||
│ └── vite-env.d.ts Types for .env files
|
||||
├── test-server/ Tests servers to test the API
|
||||
│ ├── http.js Test server for bun.sh
|
||||
│ └── node-http.js Test server for Node.js
|
||||
├── .drone.yml Drone.io ci configuration
|
||||
├── .env.development Vite development environment config
|
||||
├── .env.production Vite production environment config
|
||||
├── .env.test Vite test environment config
|
||||
├── .eslintrc.cjs eslint config file
|
||||
├── .gitattributes git-lfs config file
|
||||
├── .gitignore git ignore config file
|
||||
├── azure-pipelines.yml Azure Pipelines YAML config file
|
||||
├── index.html HTML Page
|
||||
├── package-lock.json Describe the node_modules tree for npm
|
||||
├── package.json Node.JS config file
|
||||
├── pnpm-lock.yaml Describe the node_modules tree for pnpm
|
||||
├── postcss.config.cjs Postcss config file for SCSS processing
|
||||
├── README.md
|
||||
├── tailwind.config.cjs Tailwind CSS config file
|
||||
├── tsconfig.json Typescript config file
|
||||
├── tsconfig.node.json Typescript config file for Node modules
|
||||
├── vite.config.ts Vite config file
|
||||
└── vitest.config.ts Vitest config file
|
||||
```
|
||||
|
146
docs/#Project/Pages/SmartComponent.md
Normal file
146
docs/#Project/Pages/SmartComponent.md
Normal file
|
@ -0,0 +1,146 @@
|
|||
- [Préface](#préface)
|
||||
- [Customiser et build le projet (recommandé)](#customiser-et-build-le-projet-(recommandé))
|
||||
* [Configurer les options de build](#configurer-les-options-de-build)
|
||||
* [Build le projet](#build-le-projet)
|
||||
- [Télécharger les défauts](#télécharger-les-défauts)
|
||||
* [Télécharger les ressources](#télécharger-les-ressources)
|
||||
- [Utiliser dans modeler](#utiliser-dans-modeler)
|
||||
|
||||
# Préface
|
||||
|
||||
Ce document a pour but d'expliquer comment intégrer SVGLayoutDesigner dans un projet.
|
||||
|
||||
Le projet se compile avec `npm run build` grace à Vite.
|
||||
Avec les fichiers générés dans `dist` nous allons créer un nouveau composant modeler par injection.
|
||||
(Plus tard les fichiers "Release" seront fournies par azure-pipeline par artefacts).
|
||||
|
||||
Il utilise le framework JS React pour faire le rendu.
|
||||
|
||||
Les projets modeler vont communiquer avec le composant par [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent) et par REST via un webservice dans les options de build.
|
||||
|
||||
Il y deux manières de récupérer les builds du projets:
|
||||
- Rebuild le projet (recommandé)
|
||||
- Récupérer des prébuild
|
||||
|
||||
# Customiser et build le projet (recommandé)
|
||||
|
||||
Customiser le build du projet permet de modifier les urls de l'API et de personnaliser des fonctionnalités.
|
||||
|
||||
## Configurer les options de build
|
||||
|
||||
Il y a deux fichiers principaux à configurer :
|
||||
- `.env.production.local`: 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 :
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
Vous pouvez modifiez `src/utils/default.ts` mais ne le committez pas.
|
||||
|
||||
## Build le projet
|
||||
|
||||
Lancer la commande `npm run build` dans le projet.
|
||||
|
||||
Les fichiers seront fournis dans `dist`.
|
||||
|
||||
|
||||
# Télécharger les défauts
|
||||
|
||||
Les defauts utilise l'API de SmartMenuiserieTemplate sur `localhost`
|
||||
|
||||
## Télécharger les ressources
|
||||
|
||||
Sur Azure DevOps, dans ce projet *SmartConfigurator*, allez dans **Pipelines**. Cliquez sur la pipeline [SVGLayoutDesignerReact](https://techformsa.visualstudio.com/SmartConfigurator/_build?definitionId=9)
|
||||
|
||||
Cliquez sur le dernier run de la branche `dev` ou (`master` pour une version stable).
|
||||
|
||||
Cliquez sur Job (s'il a un statut **Success**)
|
||||
|
||||
Cliquez sur `1 artifact`:
|
||||

|
||||
|
||||
Cliquez sur le menu hamburger et cliquez sur **Download Artifacts** :
|
||||

|
||||
|
||||
|
||||
# Utiliser dans modeler
|
||||
|
||||
- Créez un dossier `svg-layout-designer` dans le dossier `Component`.
|
||||
|
||||
- Puis copier les fichiers de build dans le dossier. (copiez l'entièreté du dossier `dist` ou extrayez le zip selon la méthode).
|
||||
|
||||
- Modifiez le fichier `index.html` du composant pour changer les chemins absolus en relatifs:
|
||||
|
||||
```diff
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
- <script type="module" crossorigin src="/assets/index.acd801d1.js"></script>
|
||||
+ <script type="module" crossorigin src="./assets/index.acd801d1.js"></script>
|
||||
- <link rel="stylesheet" href="/assets/index.859481c4.css">
|
||||
+ <link rel="stylesheet" href="./assets/index.859481c4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
- Si **besoin**, modifier le fichier `smartcomponent/svg-layout-designer.html` pour avoir la bonne url relative à la basename de l'url :
|
||||
|
||||
```diff
|
||||
-<iframe src="./Components/svg-layout-designer/index.html" style="border:none; width:100%; height:100%">
|
||||
+<iframe src="./Components/SVGLayoutDesigner/index.html" style="border:none; width:100%; height:100%">
|
||||
</iframe>
|
||||
```
|
||||
|
||||
- Ensuite dans Visual Studio, incluez les fichiers et compilez.
|
||||
|
||||
- Enfin sur Modeler, un nouveau composant devrait être disponible.
|
||||
|
||||
- Pour l'utiliser dans un viewModel, créez un observable de type `SVGLayoutDesigner` et assignez-le dans les propriétés du composant :
|
||||
|
||||
```typescript
|
||||
ConfiguratorViewModel.ts
|
||||
...
|
||||
|
||||
private OSVGLayoutDesigner: KnockoutObservable<Components.SVGLayoutDesigner> = ko.observable<Components.SVGLayoutDesigner>(null);
|
||||
|
||||
...
|
||||
```
|
||||
|
||||

|
||||
|
||||
- Exemple d'utilisation:
|
||||
|
||||
```typescript
|
||||
public AddContainer() {
|
||||
// Récupère la configuration de l'éditeur
|
||||
this.OSVGLayoutDesigner().GetEditorState((state: SVGLD.IEditorState) => {
|
||||
// Création d'un nouveau conteneur du premier type de container de la config dans le conteneur sélectionné
|
||||
const config = state.configuration;
|
||||
const containerConfig = config.AvailableContainers.shift();
|
||||
const type = containerConfig.Type;
|
||||
this.OSVGLayoutDesigner().AppendContainerToSelectedContainer(type);
|
||||
});
|
||||
}
|
||||
|
||||
public DeleteContainer() {
|
||||
// Supprime le conteneur sélectionné
|
||||
this.OSVGLayoutDesigner().GetCurrentHistoryState((state: SVGLD.IHistoryState) => {
|
||||
const selectedContainer = state.selectedContainerId;
|
||||
this.OSVGLayoutDesigner().DeleteContainer(selectedContainer);
|
||||
})
|
||||
}
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue