12 KiB
Container behaviors
This document is about the special and unique behaviors that a container can have.
Each behavior is documented into a section with 3 subsections :
- Rules
- Applications
- Code reference and algorithms
Default behavior
The default behavior is the floating panel. It can move and resize itself but not its siblings.
Rules
The default behavior does not have any particular rules that applies to itself.
However it does have a common rule for any behavior that applies to its children.
Which is to apply the specials behaviors of its children (rigid or anchor).
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.
Code references and algorithms
In the module PropertiesOperations.ts
in the following functions:
OnPropertyChange()
OnPropertiesSubmit()
and in ContainerOperation.ts
in AddContainer()
,
it uses the ApplyBehaviors
function of the Behaviors.ts
module to apply the specials behaviors of its children.
Rigid body behavior
The rigid body behavior is a special behavior that allows a container to be restricted into a space.
Rules
The main rules are :
- The rigid container must be kept inside its parent container
- The rigid container must be inside an unallocated space of its parent. Meaning, that it cannot overlap with another sibling.
Applications
This behavior has many applications to it. Mainly about recalculations.
You may want to resize/move quickly and be certain that it does not overflow its parent.
You may want to resize its parent and makes its resize its children.
You may want siblings to interact with each other.
Code references and algorithms
Its algorithm can be a little complicated due to the numerous uses cases.
First rule
Lets start with the first rule : The rigid container must be kept inside its parent container
Inside the RigidBodyBehaviors.ts
, see constraintBodyInsideParent()
and constraintBodyInsideSpace()
.
As you can see constraintBodyInsideParent()
is just a wrapper for constraintBodyInsideSpace()
, so lets just study the last function.
This is a simple problem of two rectangle.
In order to restrict the child to its parent, we need to know firstly, if the children is not bigger than its parent.
If it is, we just need to set the child at the beginning and makes it takes the full size of its parent.
If it is not, we need to check if the children is out of bound (outside its parent). And if it is out of bound, we need to move it back inside.
To check if it bigger than its parent we just need to compare their sizes : childWidth > parentWidth
and vertically childHeight > parentHeight
.
If false we need to check out of bound, check for x (and y): child.x < parent.x
for the left side or child.x + child.width > parent.x + parent.width
for the right side. We don't want the overlap either which is why we uses child.width
.
The condition is also equivalent to child.x > parent.x + parent.width - child.width
which could makes more sense as the required space must be smaller because of the child size.
In my algorithm, I decided to put them near the edge where they went out of bound :
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
}
}
}
Second rule
The second rule is the most important and complicated as it must interact with its siblings.
The rigid container must be inside an unallocated space of its parent. Meaning, that it cannot overlap with another sibling.
Let's first define what is a space : a space is the width of a container. Which consequently means that the rule only applies on the horizontal view. To simply the matter that also means that we only need to work on one dimension.
To solve this problem, like the parent, we could use collision detection between its siblings. However this could be very slow as the worst case scenario is a cartesian product: O(n2). Because for each container we need to search for other container that collide with use. When it collide we need to move it and search again.
Remember, this rule is applied every time you change a property of container, this is lag. We cannot afford to inefficient loops.
Let us use a "system of space" that has "containers" that cannot "overlap".
Memory.
Memory, RAM, Hard drive space, handles their space through a system of adresses and chunks of spaces (words, bytes...). In our case we don't have chunk of spaces but floating numbers (which can be a pain the work with because of the edge cases).
This system is particularly useful as it remember the space used after every iteration of allocation, meaning that we can know exactly when there is no more space inside parent and when a container must resized itself in order to fit inside.
Alright let us start the algorithm. See constraintBodyInsideUnallocatedWidth()
, getAvailableWidths()
and getAvailableWidthsTwoLines()
in RigidBodyBehaviors.ts
for implementation reference.
We have initially the whole space available: let space
be this available space in the parent.
space
is a pointer, thus at the beginning it has 0
to its pointer address and parent.width
as it space.
To simplify the algorithm when adding a container, let us compare it to eating a bûche de Noël (yule log).
Like eating the cake, we need to cut it and take a part.
There is 5 possible way to cut it :
- Not eating the cake (maybe we prefer to eat a different cake/part)
- Eating the whole cake
- Cutting the cake at the left side
- Cutting the cake at the right right
- Cutting the cake in the middle
Not cutting the cake means returning the whole cake as is.
Eating the whole cake is to not returns anything.
Cutting at the left or the right side means leave 1 part.
Cutting the middle means leaving two part.
After cutting the cake, while there is still some left, we can continue the operation. (it is a for loop in the code though for syntax reasons)
However after serving for the siblings we may notice that there is no more left for us. We get angry, we throw a tantrum.
// if you did not understood the joke
if (there no more cake) {
throw tantrum
}
Wait, there is actually cake!
But it is left in multiple parts, we will just takes the closest one that fits our hunger.
If there is one that fits our hunger let's take it!
Yet! There is cake but none fits our hunger. But we do have a minimum acceptance, let us be humble, we will still take the small bit. By the way, taking multiple part would look bad for us. Nonetheless, if my minimum acceptance were to be higher than what is left, I would throw a warning for next time.
Alright lets translate this in pseudo-code.
Let us start with getting the available spaces :
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;
}
To allocate:
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
}
]
}
Finally the top part:
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)
}
This algorithm is great but, some problems remains:
- Finding the closest takes O(nlogn) with n being the number of spaces. This is usually not bad since the objective of the rigid body is to fill space. But it still does have a very bad worst case.
- There is 2 searches for space, same problem but the previous sort helps to make it faster for the best cases
Anchor behavior
The anchor behavior allows a container to gain priority over its siblings.
Rules
It has the following rules:
- The container cannot be moved by other rigid siblings container
- The container cannot be resized by any other siblings container
- The container cannot overlap any other siblings rigid container :
- overlapping containers are shifted to the nearest available space/width
- or resized when there is no available space left other than theirs
- or lose their rigid body properties when there is absolutely no available space left (not even theirs)
Applications
Gaining priority can helps makes sure that a rigid object won't move no matter what and will absolutly move no matter what is under it.
Code references and algorithms
While there is a more rules applied to this behavior, most of them are just conditions.
These three rules:
- The container cannot be moved by other rigid siblings container
- The container cannot be resized by any other siblings container
- It cannot overlap any other siblings rigid container
Can be translate into a single one: "The container is an allocated space so any container is in contact will move or be resized"
Meaning that applying the rigid body properties of the sibling will also apply this rule. The difference with the default behavior and the anchor behavior is that the anchor container will be taken into account during the calculation of available space.
You can think of the default container as a floating panel and the anchor container as a wall. You can go under the floating panel but cannot go over the wall.
To optimize the algorithm, we just need to find the overlapping siblings since the anchor is not applied to those who are not in 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)
}
}
Also, modify getAvailableSpaces()
so it takes into account anchor containers.
That's it.