diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts index c849ca8..c144acc 100644 --- a/src/Components/App/MenuActions.ts +++ b/src/Components/App/MenuActions.ts @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction } from 'react'; import { IConfiguration } from '../../Interfaces/IConfiguration'; import { ContainerModel } from '../../Interfaces/IContainerModel'; import { fetchConfiguration } from '../API/api'; -import { IEditorState } from "../../Interfaces/IEditorState"; +import { IEditorState } from '../../Interfaces/IEditorState'; import { LoadState } from './Load'; export function NewEditor( @@ -23,6 +23,7 @@ export function NewEditor( width: configuration.MainContainer.Width, height: configuration.MainContainer.Height, isRigidBody: false, + isAnchor: false, fillOpacity: 0, stroke: 'black' } diff --git a/src/Components/Editor/Behaviors/AnchorBehaviors.ts b/src/Components/Editor/Behaviors/AnchorBehaviors.ts new file mode 100644 index 0000000..d9731e8 --- /dev/null +++ b/src/Components/Editor/Behaviors/AnchorBehaviors.ts @@ -0,0 +1,70 @@ +/** + * @module AnchorBehavior + * + * An anchor is a container that takes physical priority in the representation : + * - It cannot be moved by other rigid siblings container + * - It cannot be resized by any other siblings container + * - It cannot overlap any other siblings rigid container : + * - overlapping container 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 no available space left) + * Meaning that: + * - Moving an anchor container will resize the width of an overlapping container + * or make them lose their property as a rigid body + */ + +import { IContainerModel } from '../../../Interfaces/IContainerModel'; +import { constraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors'; + +/** + * Impose the container position to its siblings + * Apply the following modification to the overlapping rigid body container : + * @param container Container to impose its position + */ +export function ImposePosition(container: IContainerModel): IContainerModel { + if (container.parent === undefined || + container.parent === null) { + return container; + } + + const rigidBodies = container.parent.children.filter( + child => child.properties.isRigidBody && !child.properties.isAnchor + ); + + const overlappingContainers = getOverlappingContainers(container, rigidBodies); + for (const overlappingContainer of overlappingContainers) { + constraintBodyInsideUnallocatedWidth(overlappingContainer); + } + return container; +} + +/** + * Returns the overlapping containers with container + * @param container A container + * @param containers A list of containers + * @returns A list of overlapping containers + */ +function getOverlappingContainers( + container: IContainerModel, + containers: IContainerModel[] +): IContainerModel[] { + const min1 = container.properties.x; + const max1 = container.properties.x + Number(container.properties.width); + const overlappingContainers: IContainerModel[] = []; + for (const other of containers) { + if (other === container) { + continue; + } + + const min2 = other.properties.x; + const max2 = other.properties.x + Number(other.properties.width); + const isOverlapping = Math.min(max1, max2) - Math.max(min1, min2) > 0; + + if (!isOverlapping) { + continue; + } + + overlappingContainers.push(other); + } + return overlappingContainers; +} diff --git a/src/Components/Editor/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts similarity index 72% rename from src/Components/Editor/RigidBodyBehaviors.ts rename to src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index 116fbc4..eabec85 100644 --- a/src/Components/Editor/RigidBodyBehaviors.ts +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -1,5 +1,13 @@ -import { IContainerModel } from '../../Interfaces/IContainerModel'; -import { ISizePointer } from '../../Interfaces/ISizePointer'; +/** + * @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" @@ -82,7 +90,7 @@ function constraintBodyInsideSpace( if (containerX < x) { containerProperties.x = x; } - if (containerX + containerWidth > width) { + if (containerX + containerWidth > x + width) { containerProperties.x = x + width - containerWidth; } @@ -90,7 +98,7 @@ function constraintBodyInsideSpace( if (containerY < y) { containerProperties.y = y; } - if (containerY + containerHeight > height) { + if (containerY + containerHeight > y + height) { containerProperties.y = y + height - containerHeight; } @@ -104,7 +112,7 @@ function constraintBodyInsideSpace( * @param container * @returns Updated container */ -function constraintBodyInsideUnallocatedWidth( +export function constraintBodyInsideUnallocatedWidth( container: IContainerModel ): IContainerModel { if (container.parent === null) { @@ -114,12 +122,7 @@ function constraintBodyInsideUnallocatedWidth( // Get the available spaces of the parent const availableWidths = getAvailableWidths(container.parent, container); const containerX = Number(container.properties.x); - - // Sort the available width to find the closest one - availableWidths.sort( - (width1, width2) => - Math.abs(width1.x - containerX) - Math.abs(width2.x - containerX) - ); + const containerWidth = Number(container.properties.width); // Check if there is still some space if (availableWidths.length === 0) { @@ -128,6 +131,24 @@ function constraintBodyInsideUnallocatedWidth( ); } + 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) => @@ -179,17 +200,18 @@ function getAvailableWidths( const width = Number(container.properties.width); let unallocatedSpaces: ISizePointer[] = [{ x, width }]; - // We will only uses containers that also have the rigid bodies - // as out-of-bound or enormouse containers should be ignored - const rigidBodies = container.children.filter( - (child) => child.properties.isRigidBody + // 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 rigidBodies) { + 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[] = []; @@ -202,8 +224,8 @@ function getAvailableWidths( const newUnallocatedWidths = getAvailableWidthsTwoLines( unallocatedSpace.x, unallocatedSpace.x + unallocatedSpace.width, - child.properties.x, - child.properties.x + Number(child.properties.width) + childX, + childX + childWidth ); // Concat the new list of SizePointer pointing to availables spaces @@ -218,39 +240,49 @@ function getAvailableWidths( /** * Returns the unallocated widths between two lines in 1D - * @param min1 left of the first line - * @param max1 rigth of the first line - * @param min2 left of the second line - * @param max2 right of the second line + * @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( - min1: number, - max1: number, - min2: number, - max2: number + unalloctedSpaceLeft: number, + unallocatedSpaceRight: number, + rectLeft: number, + rectRight: number ): ISizePointer[] { - if (min2 < min1 && max2 > max1) { + 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 (min1 >= min2) { + if (unalloctedSpaceLeft >= rectLeft) { // object 2 is partially overlapping on the left return [ { - x: max2, - width: max1 - max2 + x: rectRight, + width: unallocatedSpaceRight - rectRight } ]; } - if (max2 >= max1) { + if (rectRight >= unallocatedSpaceRight) { // object 2 is partially overlapping on the right return [ { - x: min2, - width: max2 - min1 + x: unalloctedSpaceLeft, + width: rectRight - unalloctedSpaceLeft } ]; } @@ -258,12 +290,12 @@ function getAvailableWidthsTwoLines( // object 2 is overlapping in the middle return [ { - x: min1, - width: min2 - min1 + x: unalloctedSpaceLeft, + width: rectLeft - unalloctedSpaceLeft }, { - x: min2, - width: max1 - max2 + x: rectRight, + width: unallocatedSpaceRight - rectRight } ]; } diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index 40391c5..63d90a0 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -4,6 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; +import IProperties from '../../Interfaces/IProperties'; /** * Select a container @@ -203,20 +204,23 @@ export function AddContainer( } } + const defaultProperties: IProperties = { + id: `${type}-${count}`, + parentId: parentClone.properties.id, + x, + y: 0, + width: properties.Width, + height: parentClone.properties.height, + isRigidBody: false, + isAnchor: false, + XPositionReference: properties.XPositionReference, + ...properties.Style + }; + // Create the container const newContainer = new ContainerModel( parentClone, - { - id: `${type}-${count}`, - parentId: parentClone.properties.id, - x, - y: 0, - width: properties?.Width, - height: parentClone.properties.height, - isRigidBody: false, - XPositionReference: properties.XPositionReference, - ...properties.Style - }, + defaultProperties, [], { type diff --git a/src/Components/Editor/PropertiesOperations.ts b/src/Components/Editor/PropertiesOperations.ts index 11c1b62..ed5a4db 100644 --- a/src/Components/Editor/PropertiesOperations.ts +++ b/src/Components/Editor/PropertiesOperations.ts @@ -4,7 +4,9 @@ import { IHistoryState } from '../../Interfaces/IHistoryState'; import IProperties from '../../Interfaces/IProperties'; import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; -import { RecalculatePhysics } from './RigidBodyBehaviors'; +import { RecalculatePhysics } from './Behaviors/RigidBodyBehaviors'; +import { INPUT_TYPES } from '../Properties/PropertiesInputTypes'; +import { ImposePosition } from './Behaviors/AnchorBehaviors'; /** * Handled the property change event in the properties form @@ -28,20 +30,6 @@ export function OnPropertyChange( throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } - if (parent === null) { - const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer); - (selectedContainerClone.properties as any)[key] = value; - setHistory(history.concat([{ - LastAction: 'Change property of main', - MainContainer: selectedContainerClone, - SelectedContainer: selectedContainerClone, - SelectedContainerId: selectedContainerClone.properties.id, - TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); - return; - } - const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); @@ -49,7 +37,15 @@ export function OnPropertyChange( throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } - (container.properties as any)[key] = value; + if (INPUT_TYPES[key] === 'number') { + (container.properties as any)[key] = Number(value); + } else { + (container.properties as any)[key] = value; + } + + if (container.properties.isAnchor) { + ImposePosition(container); + } if (container.properties.isRigidBody) { RecalculatePhysics(container); @@ -88,25 +84,6 @@ export function OnPropertiesSubmit( throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } - if (parent === null) { - const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer); - for (const property in properties) { - const input = (event.target as HTMLFormElement).querySelector(`#${property}`); - if (input instanceof HTMLInputElement) { - (selectedContainerClone.properties as any)[property] = input.value; - } - } - setHistory(history.concat([{ - LastAction: 'Change property of main', - MainContainer: selectedContainerClone, - SelectedContainer: selectedContainerClone, - SelectedContainerId: selectedContainerClone.properties.id, - TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); - return; - } - const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); @@ -118,6 +95,11 @@ export function OnPropertiesSubmit( const input = (event.target as HTMLFormElement).querySelector(`#${property}`); if (input instanceof HTMLInputElement) { (container.properties as any)[property] = input.value; + if (INPUT_TYPES[property] === 'number') { + (container.properties as any)[property] = Number(input.value); + } else { + (container.properties as any)[property] = input.value; + } } } diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index f1776c0..8d34480 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -17,7 +17,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }} @@ -47,7 +48,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -103,7 +105,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -119,7 +122,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} } @@ -136,7 +140,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} } @@ -173,7 +178,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -188,7 +194,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 2afc364..9fbef37 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -23,7 +23,8 @@ describe.concurrent('Properties', () => { parentId: 'parentId', x: 1, y: 1, - isRigidBody: false + isRigidBody: false, + isAnchor: false }; const handleChange = vi.fn((key, value) => { diff --git a/src/Components/Properties/PropertiesInputTypes.tsx b/src/Components/Properties/PropertiesInputTypes.tsx index c62a8fd..d91ddbc 100644 --- a/src/Components/Properties/PropertiesInputTypes.tsx +++ b/src/Components/Properties/PropertiesInputTypes.tsx @@ -3,5 +3,6 @@ export const INPUT_TYPES: Record = { y: 'number', width: 'number', height: 'number', - isRigidBody: 'checkbox' + isRigidBody: 'checkbox', + isAnchor: 'checkbox' }; diff --git a/src/Enums/AddingBehavior.ts b/src/Enums/AddingBehavior.ts deleted file mode 100644 index fb6ae67..0000000 --- a/src/Enums/AddingBehavior.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum AddingBehavior { - InsertInto, - Replace -} diff --git a/src/Interfaces/IEditorState.tsx b/src/Interfaces/IEditorState.ts similarity index 100% rename from src/Interfaces/IEditorState.tsx rename to src/Interfaces/IEditorState.ts diff --git a/src/Interfaces/IProperties.ts b/src/Interfaces/IProperties.ts index 4996ce5..ef2db7e 100644 --- a/src/Interfaces/IProperties.ts +++ b/src/Interfaces/IProperties.ts @@ -1,11 +1,21 @@ import * as React from 'react'; import { XPositionReference } from '../Enums/XPositionReference'; +/** + * Properties of a container + * @property id id of the container + * @property parentId id of the parent container + * @property x horizontal offset of the container + * @property y vertical offset of the container + * @property isRigidBody if true apply rigid body behaviors + * @property isAnchor if true apply anchor behaviors + */ export default interface IProperties extends React.CSSProperties { id: string parentId: string | null x: number y: number isRigidBody: boolean + isAnchor: boolean XPositionReference?: XPositionReference } diff --git a/src/utils/default.ts b/src/utils/default.ts index 785a840..2bd22dc 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -33,6 +33,7 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = { width: DEFAULT_CONFIG.MainContainer.Width, height: DEFAULT_CONFIG.MainContainer.Height, isRigidBody: false, + isAnchor: false, fillOpacity: 0, stroke: 'black' };