All checks were successful
continuous-integration/drone/push Build is passing
- Make Dimension an actual svg line - Implement XPositionReference - Select the container above after delete - Remove DimensionLayer - Improve form properties Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/25
555 lines
18 KiB
TypeScript
555 lines
18 KiB
TypeScript
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<SetStateAction<HistoryState[]>>,
|
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
): 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<SetStateAction<HistoryState[]>>,
|
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
): 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<SetStateAction<HistoryState[]>>,
|
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
): 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<SetStateAction<HistoryState[]>>,
|
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
): 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<SetStateAction<HistoryState[]>>,
|
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
): 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<HTMLFormElement>,
|
|
properties: Properties,
|
|
fullHistory: HistoryState[],
|
|
historyCurrentStep: number,
|
|
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
|
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
|
): 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;
|
|
}
|