svg-layout-designer-react/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts

421 lines
13 KiB
TypeScript

/**
* @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';
import { Orientation } from '../../../Enums/Orientation';
import { ENABLE_HARD_RIGID } from '../../../utils/default';
import { FindContainerById, MakeChildrenIterator } from '../../../utils/itertools';
/**
* "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 ApplyRigidBody(
containers: Map<string, IContainerModel>,
container: IContainerModel,
parent: IContainerModel
): IContainerModel {
container = ConstraintBodyInsideParent(container, parent);
if (ENABLE_HARD_RIGID) {
container = ConstraintBodyInsideUnallocatedWidth(containers, 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,
parent: IContainerModel
): IContainerModel {
const parentProperties = parent.properties;
const parentWidth = parentProperties.width;
const parentHeight = 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 = containerProperties.x;
const containerY = containerProperties.y;
const containerWidth = containerProperties.width;
const containerHeight = 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(
containers: Map<string, IContainerModel>,
container: IContainerModel
): IContainerModel {
const parent = FindContainerById(containers, container.properties.parentId);
if (parent === null || parent === undefined) {
return container;
}
// Get the available spaces of the parent
const isHorizontal =
parent.properties.orientation === Orientation.Horizontal;
const children: IContainerModel[] = [...MakeChildrenIterator(containers, parent.children)];
const availableWidths = GetAvailableWidths(
0,
parent.properties.width,
children,
container,
isHorizontal
);
// 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 bit.'
);
}
const containerId = container.properties.id;
if (!isHorizontal) {
const containerY = container.properties.y;
const containerHeight = container.properties.height;
const containerMinHeight = container.properties.minHeight;
SortAvailableWidthsByClosest(containerY, containerHeight, availableWidths);
// Check if the container actually fit inside
// It will usually fit if it was alrady fitting
const availableWidthFound = availableWidths.find((width) =>
IsFitting(containerHeight, width)
);
if (availableWidthFound === undefined) {
const { x, width } = TrySqueeze(containerY, containerHeight, containerMinHeight, containerId, availableWidths);
container.properties.y = x;
container.properties.height = width;
return container;
}
ConstraintBodyInsideSpace(
container,
0,
availableWidthFound.x,
parent.properties.width,
availableWidthFound.width
);
return container;
}
const containerX = container.properties.x;
const containerWidth = container.properties.width;
const containerMinWidth = container.properties.minWidth;
SortAvailableWidthsByClosest(containerX, containerWidth, availableWidths);
// Check if the container actually fit inside
// It will usually fit if it was alrady fitting
const availableWidthFound = availableWidths.find((width) =>
IsFitting(containerWidth, width)
);
if (availableWidthFound === undefined) {
const { x, width } = TrySqueeze(containerX, containerWidth, containerMinWidth, containerId, availableWidths);
container.properties.x = x;
container.properties.width = width;
return container;
}
ConstraintBodyInsideSpace(
container,
availableWidthFound.x,
0,
availableWidthFound.width,
parent.properties.height
);
return container;
}
function TrySqueeze(
containerX: number,
containerWidth: number,
containerMinWidth: number,
containerId: string,
availableWidths: ISizePointer[]
): { x: number, width: number } {
// 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
const availableWidth: ISizePointer | undefined = availableWidths.find((width) => {
return IsFitting(containerMinWidth, width);
});
if (availableWidth === undefined) {
console.debug(`Container ${containerId} cannot fit in any space due to its minimum width being to large.`);
return {
x: containerX,
width: containerWidth
};
}
return {
x: availableWidth.x,
width: availableWidth.width
};
}
function SortAvailableWidthsByClosest(containerX: number, containerWidth: number, availableWidths: ISizePointer[]): void {
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 a container can fit inside a size space
* @param container Container to check
* @param sizePointer Size space to check
* @returns
*/
function IsFitting(containerWidth: number,
sizePointer: ISizePointer): boolean {
return containerWidth <= sizePointer.width;
}
/**
* 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(
x: number,
width: number,
children: IContainerModel[],
exception: IContainerModel,
isHorizontal: boolean
): ISizePointer[] {
// Initialize the first size pointer
// which takes full width of the available space
let unallocatedSpaces: ISizePointer[] = [{ x, width }];
for (const child of children) {
if (unallocatedSpaces.length < 1) {
return unallocatedSpaces;
}
// Ignore the exception
// And we will also only uses containers that also are rigid or are anchors
if (child === exception) {
continue;
}
const childX = isHorizontal ? child.properties.x : child.properties.y;
const childWidth = isHorizontal ? child.properties.width : child.properties.height;
// 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,
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 unalloctedSpace unallocated space
* @param rectX left of the second line
* @param rectWidth width of the second line
* @returns Available widths
*/
function GetAvailableWidthsTwoLines(
unallocatedSpace: ISizePointer,
rectX: number,
rectWidth: number
): ISizePointer[] {
const unallocatedSpaceRight = unallocatedSpace.x + unallocatedSpace.width;
const rectRight = rectX + rectWidth;
const isNotOverlapping = unallocatedSpaceRight < rectX ||
unallocatedSpace.x > rectRight;
if (isNotOverlapping) {
return [unallocatedSpace];
}
const isOverlappingFullWidth = rectX < unallocatedSpace.x && rectRight > unallocatedSpaceRight;
if (isOverlappingFullWidth) {
return [];
}
const isOverlappingOnTheLeft = unallocatedSpace.x >= rectX;
if (isOverlappingOnTheLeft) {
return GetAvailableWidthsLeft(unallocatedSpaceRight, rectRight);
}
const isOverlappingOnTheRight = rectRight >= unallocatedSpaceRight;
if (isOverlappingOnTheRight) {
return GetAvailableWidthsRight(unallocatedSpace.x, rectX);
}
return GetAvailableWidthsMiddle(unallocatedSpace.x, unallocatedSpaceRight, rectX, rectRight);
}
function GetAvailableWidthsLeft(unallocatedSpaceRight: number, rectRight: number): ISizePointer[] {
if (unallocatedSpaceRight - rectRight <= 0) {
return [];
}
return [
{
x: rectRight,
width: unallocatedSpaceRight - rectRight
}
];
}
function GetAvailableWidthsRight(unallocatedSpaceX: number, rectX: number): ISizePointer[] {
if (rectX - unallocatedSpaceX <= 0) {
return [];
}
return [
{
x: unallocatedSpaceX,
width: rectX - unallocatedSpaceX
}
];
}
function GetAvailableWidthsMiddle(
unallocatedSpaceX: number,
unallocatedSpaceRight: number,
rectX: number,
rectRight: number
): ISizePointer[] {
const sizePointers: ISizePointer[] = [];
if (rectX - unallocatedSpaceX > 0) {
sizePointers.push({
x: unallocatedSpaceX,
width: rectX - unallocatedSpaceX
});
}
if (unallocatedSpaceRight - rectRight > 0) {
sizePointers.push({
x: rectRight,
width: unallocatedSpaceRight - rectRight
});
}
return sizePointers;
}