/** * @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, 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, 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; }