import React, { Dispatch, SetStateAction } from 'react'; import { HistoryState } from '../../Interfaces/HistoryState'; import { Configuration } from '../../Interfaces/Configuration'; import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel'; import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; import { SizePointer } from '../../Interfaces/SizePointer'; import Properties from '../../Interfaces/Properties'; /** * Select a container * @param container Selected container */ export function SelectContainer( container: ContainerModel, fullHistory: HistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; const mainContainerClone = structuredClone(current.MainContainer); const selectedContainer = findContainerById(mainContainerClone, container.properties.id); if (selectedContainer === undefined) { throw new Error('[SelectContainer] Cannot find container among children of main container!'); } setHistory(history.concat([{ LastAction: `Select container ${selectedContainer.properties.id}`, MainContainer: mainContainerClone, SelectedContainer: selectedContainer, SelectedContainerId: selectedContainer.properties.id, TypeCounters: Object.assign({}, current.TypeCounters) }])); setHistoryCurrentStep(history.length); } export function DeleteContainer( containerId: string, fullHistory: HistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[historyCurrentStep]; const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); const container = findContainerById(mainContainerClone, containerId); if (container === undefined) { throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); } if (container === mainContainerClone || container.parent === undefined || container.parent === null) { // TODO: Implement alert throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!'); } if (container === null || container === undefined) { throw new Error('[DeleteContainer] Container model was not found among children of the main container!'); } const index = container.parent.children.indexOf(container); if (index > -1) { container.parent.children.splice(index, 1); } else { throw new Error('[DeleteContainer] Could not find container among parent\'s children'); } // Select the previous container // or select the one above const SelectedContainer = findContainerById(mainContainerClone, current.SelectedContainerId) ?? container.parent.children.at(index - 1) ?? container.parent; const SelectedContainerId = SelectedContainer.properties.id; setHistory(history.concat([{ LastAction: `Delete container ${containerId}`, MainContainer: mainContainerClone, SelectedContainer, SelectedContainerId, TypeCounters: Object.assign({}, current.TypeCounters) }])); setHistoryCurrentStep(history.length); } /** * Add a new container to a selected container * @param type The type of container * @returns void */ export function AddContainerToSelectedContainer( type: string, configuration: Configuration, fullHistory: HistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; if (current.SelectedContainer === null || current.SelectedContainer === undefined) { return; } const parent = current.SelectedContainer; AddContainer( parent.children.length, type, parent.properties.id, configuration, fullHistory, historyCurrentStep, setHistory, setHistoryCurrentStep ); } export function AddContainer( index: number, type: string, parentId: string, configuration: Configuration, fullHistory: HistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; if (current.MainContainer === null || current.MainContainer === undefined) { return; } // Get the preset properties from the API const properties = configuration.AvailableContainers .find(option => option.Type === type); if (properties === undefined) { throw new Error(`[AddContainer] Object type not found. Found: ${type}`); } // Set the counter of the object type in order to assign an unique id const newCounters = Object.assign({}, current.TypeCounters); if (newCounters[type] === null || newCounters[type] === undefined) { newCounters[type] = 0; } else { newCounters[type]++; } const count = newCounters[type]; // Create maincontainer model const clone: IContainerModel = structuredClone(current.MainContainer); // Find the parent const parentClone: IContainerModel | undefined = findContainerById( clone, parentId ); if (parentClone === null || parentClone === undefined) { throw new Error('[AddContainer] Container model was not found among children of the main container!'); } let x = 0; if (index > 0) { const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1); if (lastChild !== undefined) { x = lastChild.properties.x + Number(lastChild.properties.width); } } // 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 }, [], { type } ); // And push it the the parent children if (index === parentClone.children.length) { parentClone.children.push(newContainer); } else { parentClone.children.splice(index, 0, newContainer); } // Update the state setHistory(history.concat([{ LastAction: 'Add container', MainContainer: clone, SelectedContainer: parentClone, SelectedContainerId: parentClone.properties.id, TypeCounters: newCounters }])); setHistoryCurrentStep(history.length); } /** * Handled the property change event in the properties form * @param key Property name * @param value New value of the property * @returns void */ export function OnPropertyChange( key: string, value: string | number | boolean, fullHistory: HistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; if (current.SelectedContainer === null || current.SelectedContainer === undefined) { 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); if (container === null || container === undefined) { throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } (container.properties as any)[key] = value; if (container.properties.isRigidBody) { RecalculatePhysics(container); } setHistory(history.concat([{ LastAction: `Change property of container ${container.properties.id}`, MainContainer: mainContainerClone, SelectedContainer: container, SelectedContainerId: container.properties.id, TypeCounters: Object.assign({}, current.TypeCounters) }])); setHistoryCurrentStep(history.length); } /** * Handled the property change event in the properties form * @param key Property name * @param value New value of the property * @returns void */ export function OnPropertiesSubmit( event: React.SyntheticEvent, properties: Properties, fullHistory: HistoryState[], historyCurrentStep: number, setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { event.preventDefault(); const history = getCurrentHistory(fullHistory, historyCurrentStep); const current = history[history.length - 1]; if (current.SelectedContainer === null || current.SelectedContainer === undefined) { 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); if (container === null || container === undefined) { throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } for (const property in properties) { const input = (event.target as HTMLFormElement).querySelector(`#${property}`); if (input instanceof HTMLInputElement) { (container.properties as any)[property] = input.value; } } if (container.properties.isRigidBody) { RecalculatePhysics(container); } setHistory(history.concat([{ LastAction: `Change property of container ${container.properties.id}`, MainContainer: mainContainerClone, SelectedContainer: container, SelectedContainerId: container.properties.id, TypeCounters: Object.assign({}, current.TypeCounters) }])); setHistoryCurrentStep(history.length); } // TODO put this in a different file 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 * @param container * @returns */ 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); } 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 > width) { containerProperties.x = x + width - containerWidth; } // Check vertical out of bound if (containerY < y) { containerProperties.y = y; } if (containerY + containerHeight > height) { containerProperties.y = y + height - containerHeight; } return container; } /** * 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 * (except the fact that disk space is divided by block). * @param container * @returns {SizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space) */ function getAvailableWidths(container: IContainerModel, exception: IContainerModel): SizePointer[] { const x = 0; const width = Number(container.properties.width); let unallocatedSpaces: SizePointer[] = [{ x, width }]; const rigidBodies = container.children.filter(child => child.properties.isRigidBody); for (const child of rigidBodies) { if (child === exception) { continue; } // get the space of the child that is inside the parent let newUnallocatedSpace: SizePointer[] = []; for (const unallocatedSpace of unallocatedSpaces) { const newUnallocatedWidths = getAvailableWidthsTwoLines( unallocatedSpace.x, unallocatedSpace.x + unallocatedSpace.width, child.properties.x, child.properties.x + Number(child.properties.width)); newUnallocatedSpace = newUnallocatedSpace.concat(newUnallocatedWidths); } unallocatedSpaces = newUnallocatedSpace; } return unallocatedSpaces; } /** * 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 * @returns Available widths */ function getAvailableWidthsTwoLines(min1: number, max1: number, min2: number, max2: number): SizePointer[] { if (min2 < min1 && max2 > max1) { // object 2 is overlapping full width return []; } if (min1 >= min2) { // object 2 is partially overlapping on the left return [{ x: max2, width: max1 - max2 }]; } if (max2 >= max1) { // object 2 is partially overlapping on the right return [{ x: min2, width: max2 - min1 }]; } // object 2 is overlapping in the middle return [ { x: min1, width: min2 - min1 }, { x: min2, width: max1 - max2 } ]; } /** * * @param container * @returns */ function constraintBodyInsideUnallocatedWidth(container: IContainerModel): IContainerModel { if (container.parent === null) { return container; } const availableWidths = getAvailableWidths(container.parent, container); const containerX = Number(container.properties.x); // Sort the available width availableWidths .sort((width1, width2) => Math.abs(width1.x - containerX) - Math.abs(width2.x - containerX)); 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 availableWidthFound = availableWidths.find( width => isFitting(container, width) ); if (availableWidthFound === undefined) { // There is two way to reach this part of the code // 1) toggle the 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) ); } function isFitting(container: IContainerModel, sizePointer: SizePointer): boolean { const containerWidth = Number(container.properties.width); return containerWidth <= sizePointer.width; }