svg-layout-designer-react/docs/Behaviors.md

17 KiB

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 :

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.

buche

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.

// 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 :

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 :

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 :

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 :

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.