449 lines
No EOL
18 KiB
Markdown
449 lines
No EOL
18 KiB
Markdown
> TL;DR
|
|
> Behaviors.ts définis les comportements qui sont utilisés.
|
|
|
|
> Actuellement sont activés par défaut :
|
|
> - Corps rigide (simple) : Est restraint dans le parent
|
|
> - Ancrage : Impose la priorité de position et de taille
|
|
> - Flex : se redimensionne automatiquement
|
|
|
|
> Désactivés:
|
|
> - Corps rigide (complet) : Est restraint par les parents et par ses voisins (fonctionne mais pas intuitif)
|
|
> - Poussé : quand un conteneur est ajouté au bout de la bande filante, pousse tous les conteneurs à sa gauche (fonctionne mais pas intuitif)
|
|
> - Swap : échange de place avec un autre conteneur lorsqu'ils sont superposés (buggué ne pas utiliser)
|
|
|
|
# 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é).
|
|
|
|
|
|
## 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 `ContainerOperations.ts` dans les fonctions suivantes :
|
|
- `OnPropertyChange()`
|
|
|
|
et dans le module `AddContainer.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. |