/** * @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 = 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( container: IContainerModel ): IContainerModel { if (container.parent === null) { return container; } // Get the available spaces of the parent const availableWidths = getAvailableWidths(container.parent, container); const containerX = container.properties.x; const containerWidth = 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, 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 = 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 = 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 => container.properties.width <= sizePointer.width;