Implement anchor and fix bugs with rigid body (#27)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Co-authored-by: Eric NGUYEN <enguyen@techform.fr> Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/27
This commit is contained in:
parent
c81a6fe44b
commit
704dab7307
12 changed files with 202 additions and 97 deletions
312
src/Components/Editor/Behaviors/RigidBodyBehaviors.ts
Normal file
312
src/Components/Editor/Behaviors/RigidBodyBehaviors.ts
Normal file
|
@ -0,0 +1,312 @@
|
|||
/**
|
||||
* @module RigidBodyBehaviors
|
||||
* Apply the following contraints to the `container` :
|
||||
* - The container must be kept inside its parent
|
||||
* - The container must find an unallocated space within the parent
|
||||
* If the contraints fails, an error message will be returned
|
||||
*/
|
||||
|
||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||
import { ISizePointer } from '../../../Interfaces/ISizePointer';
|
||||
|
||||
/**
|
||||
* "Transform the container into a rigid body"
|
||||
* Apply the following contraints to the `container` :
|
||||
* - The container must be kept inside its parent
|
||||
* - The container must find an unallocated space within the parent
|
||||
* If the contraints fails, an error message will be returned
|
||||
* @param container Container to apply its rigid body properties
|
||||
* @returns A rigid body container
|
||||
*/
|
||||
export function RecalculatePhysics(
|
||||
container: IContainerModel
|
||||
): IContainerModel {
|
||||
container = constraintBodyInsideParent(container);
|
||||
container = constraintBodyInsideUnallocatedWidth(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit a rect inside a parent rect by applying the following rules :
|
||||
* it cannot be bigger than the parent
|
||||
* it cannot go out of bound
|
||||
* Mutates and returns the container
|
||||
* @param container
|
||||
* @returns Updated container
|
||||
*/
|
||||
function constraintBodyInsideParent(
|
||||
container: IContainerModel
|
||||
): IContainerModel {
|
||||
if (container.parent === null || container.parent === undefined) {
|
||||
return container;
|
||||
}
|
||||
|
||||
const parentProperties = container.parent.properties;
|
||||
const parentWidth = Number(parentProperties.width);
|
||||
const parentHeight = Number(parentProperties.height);
|
||||
|
||||
return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit a container inside a rectangle
|
||||
* Mutates and returns the container
|
||||
* @param container A container
|
||||
* @param x x of top left of the rectangle
|
||||
* @param y y of top left of the rectangle
|
||||
* @param width width of the rectangle
|
||||
* @param height height of the rectangle
|
||||
* @returns Updated container
|
||||
*/
|
||||
function constraintBodyInsideSpace(
|
||||
container: IContainerModel,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
): IContainerModel {
|
||||
const containerProperties = container.properties;
|
||||
const containerX = Number(containerProperties.x);
|
||||
const containerY = Number(containerProperties.y);
|
||||
const containerWidth = Number(containerProperties.width);
|
||||
const containerHeight = Number(containerProperties.height);
|
||||
|
||||
// Check size bigger than parent
|
||||
const isBodyLargerThanParent = containerWidth > width;
|
||||
const isBodyTallerThanParentHeight = containerHeight > height;
|
||||
if (isBodyLargerThanParent || isBodyTallerThanParentHeight) {
|
||||
if (isBodyLargerThanParent) {
|
||||
containerProperties.x = x;
|
||||
containerProperties.width = width;
|
||||
}
|
||||
if (isBodyTallerThanParentHeight) {
|
||||
containerProperties.y = y;
|
||||
containerProperties.height = height;
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
// Check horizontal out of bound
|
||||
if (containerX < x) {
|
||||
containerProperties.x = x;
|
||||
}
|
||||
if (containerX + containerWidth > x + width) {
|
||||
containerProperties.x = x + width - containerWidth;
|
||||
}
|
||||
|
||||
// Check vertical out of bound
|
||||
if (containerY < y) {
|
||||
containerProperties.y = y;
|
||||
}
|
||||
if (containerY + containerHeight > y + height) {
|
||||
containerProperties.y = y + height - containerHeight;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constraint the container inside unallocated width/space of the parent container
|
||||
* If there is no unalloacted width/space, an error will be thrown
|
||||
* Mutates and returns the container
|
||||
* @param container
|
||||
* @returns Updated container
|
||||
*/
|
||||
export function constraintBodyInsideUnallocatedWidth(
|
||||
container: IContainerModel
|
||||
): IContainerModel {
|
||||
if (container.parent === null) {
|
||||
return container;
|
||||
}
|
||||
|
||||
// Get the available spaces of the parent
|
||||
const availableWidths = getAvailableWidths(container.parent, container);
|
||||
const containerX = Number(container.properties.x);
|
||||
const containerWidth = Number(container.properties.width);
|
||||
|
||||
// Check if there is still some space
|
||||
if (availableWidths.length === 0) {
|
||||
throw new Error(
|
||||
'No available space found on the parent container. Try to free the parent a little before placing it inside.'
|
||||
);
|
||||
}
|
||||
|
||||
const middle = containerX + containerWidth / 2;
|
||||
// Sort the available width to find the space with the closest position
|
||||
availableWidths.sort(
|
||||
(width1, width2) => {
|
||||
let compared1X = width1.x;
|
||||
if (width1.x < containerX) {
|
||||
compared1X = width1.x + width1.width - containerWidth;
|
||||
}
|
||||
|
||||
let compared2X = width2.x;
|
||||
if (width2.x < containerX) {
|
||||
compared2X = width2.x + width2.width - containerWidth;
|
||||
}
|
||||
|
||||
return Math.abs(compared1X - middle) - Math.abs(compared2X - middle);
|
||||
}
|
||||
);
|
||||
|
||||
// Check if the container actually fit inside
|
||||
// It will usually fit if it was alrady fitting
|
||||
const availableWidthFound = availableWidths.find((width) =>
|
||||
isFitting(container, width)
|
||||
);
|
||||
|
||||
if (availableWidthFound === undefined) {
|
||||
// Otherwise, it is possible that it does not fit
|
||||
// There is two way to reach this part of the code
|
||||
// 1) Enable isRigidBody such as width > availableWidth.width
|
||||
// 2) Resize a container such as width > availableWidth.width
|
||||
|
||||
// We want the container to fit automatically inside the available space
|
||||
// even if it means to resize the container
|
||||
// The end goal is that the code never show the error message no matter what action is done
|
||||
// TODO: Actually give an option to not fit and show the error message shown below
|
||||
const availableWidth = availableWidths[0];
|
||||
container.properties.x = availableWidth.x;
|
||||
container.properties.width = availableWidth.width;
|
||||
// throw new Error('[constraintBodyInsideUnallocatedWidth] BIGERR: No available space found on the parent container, even though there is some.');
|
||||
return container;
|
||||
}
|
||||
|
||||
return constraintBodyInsideSpace(
|
||||
container,
|
||||
availableWidthFound.x,
|
||||
0,
|
||||
availableWidthFound.width,
|
||||
Number(container.parent.properties.height)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unallocated widths inside a container
|
||||
* An allocated width is defined by its the widths of the children that are rigid bodies.
|
||||
* An example of this allocation system is the disk space of an hard drive
|
||||
* (except the fact that disk space is divided by block).
|
||||
* @param container Container where to find an available width
|
||||
* @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded)
|
||||
* @returns {ISizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space)
|
||||
*/
|
||||
function getAvailableWidths(
|
||||
container: IContainerModel,
|
||||
exception: IContainerModel
|
||||
): ISizePointer[] {
|
||||
// Initialize the first size pointer
|
||||
// which takes full width of the available space
|
||||
const x = 0;
|
||||
const width = Number(container.properties.width);
|
||||
let unallocatedSpaces: ISizePointer[] = [{ x, width }];
|
||||
|
||||
// We will only uses containers that also are rigid or are anchors
|
||||
const solidBodies = container.children.filter(
|
||||
(child) => child.properties.isRigidBody || child.properties.isAnchor
|
||||
);
|
||||
|
||||
for (const child of solidBodies) {
|
||||
// Ignore the exception
|
||||
if (child === exception) {
|
||||
continue;
|
||||
}
|
||||
const childX = child.properties.x;
|
||||
const childWidth = Number(child.properties.width);
|
||||
|
||||
// get the space of the child that is inside the parent
|
||||
let newUnallocatedSpace: ISizePointer[] = [];
|
||||
|
||||
// We will iterate on a mutable variable in order to divide it
|
||||
for (const unallocatedSpace of unallocatedSpaces) {
|
||||
// In order to find unallocated space,
|
||||
// We need to calculate the overlap between the two containers
|
||||
// We only works with widths meaning in 1D (with lines)
|
||||
const newUnallocatedWidths = getAvailableWidthsTwoLines(
|
||||
unallocatedSpace.x,
|
||||
unallocatedSpace.x + unallocatedSpace.width,
|
||||
childX,
|
||||
childX + childWidth
|
||||
);
|
||||
|
||||
// Concat the new list of SizePointer pointing to availables spaces
|
||||
newUnallocatedSpace = newUnallocatedSpace.concat(newUnallocatedWidths);
|
||||
}
|
||||
// Finally update the availables spaces found, loop again with it
|
||||
unallocatedSpaces = newUnallocatedSpace;
|
||||
}
|
||||
|
||||
return unallocatedSpaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unallocated widths between two lines in 1D
|
||||
* @param unalloctedSpaceLeft left of the first line
|
||||
* @param unallocatedSpaceRight rigth of the first line
|
||||
* @param rectLeft left of the second line
|
||||
* @param rectRight right of the second line
|
||||
* @returns Available widths
|
||||
*/
|
||||
function getAvailableWidthsTwoLines(
|
||||
unalloctedSpaceLeft: number,
|
||||
unallocatedSpaceRight: number,
|
||||
rectLeft: number,
|
||||
rectRight: number
|
||||
): ISizePointer[] {
|
||||
if (unallocatedSpaceRight < rectLeft ||
|
||||
unalloctedSpaceLeft > rectRight
|
||||
) {
|
||||
// object 1 and 2 are not overlapping
|
||||
return [{
|
||||
x: unalloctedSpaceLeft,
|
||||
width: unallocatedSpaceRight - unalloctedSpaceLeft
|
||||
}];
|
||||
}
|
||||
|
||||
if (rectLeft < unalloctedSpaceLeft && rectRight > unallocatedSpaceRight) {
|
||||
// object 2 is overlapping full width
|
||||
return [];
|
||||
}
|
||||
|
||||
if (unalloctedSpaceLeft >= rectLeft) {
|
||||
// object 2 is partially overlapping on the left
|
||||
return [
|
||||
{
|
||||
x: rectRight,
|
||||
width: unallocatedSpaceRight - rectRight
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (rectRight >= unallocatedSpaceRight) {
|
||||
// object 2 is partially overlapping on the right
|
||||
return [
|
||||
{
|
||||
x: unalloctedSpaceLeft,
|
||||
width: rectRight - unalloctedSpaceLeft
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// object 2 is overlapping in the middle
|
||||
return [
|
||||
{
|
||||
x: unalloctedSpaceLeft,
|
||||
width: rectLeft - unalloctedSpaceLeft
|
||||
},
|
||||
{
|
||||
x: rectRight,
|
||||
width: unallocatedSpaceRight - rectRight
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a container can fit inside a size space
|
||||
* @param container Container to check
|
||||
* @param sizePointer Size space to check
|
||||
* @returns
|
||||
*/
|
||||
const isFitting = (
|
||||
container: IContainerModel,
|
||||
sizePointer: ISizePointer
|
||||
): boolean => Number(container.properties.width) <= sizePointer.width;
|
Loading…
Add table
Add a link
Reference in a new issue