Separate properties operations and rigid body behaviors in different modules + Doc
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
faa058e57d
commit
61b72f6a35
5 changed files with 449 additions and 340 deletions
|
@ -1,11 +1,9 @@
|
||||||
import React, { Dispatch, SetStateAction } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { HistoryState } from '../../Interfaces/HistoryState';
|
import { HistoryState } from '../../Interfaces/HistoryState';
|
||||||
import { Configuration } from '../../Interfaces/Configuration';
|
import { Configuration } from '../../Interfaces/Configuration';
|
||||||
import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel';
|
import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel';
|
||||||
import { findContainerById } from '../../utils/itertools';
|
import { findContainerById } from '../../utils/itertools';
|
||||||
import { getCurrentHistory } from './Editor';
|
import { getCurrentHistory } from './Editor';
|
||||||
import { SizePointer } from '../../Interfaces/SizePointer';
|
|
||||||
import Properties from '../../Interfaces/Properties';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a container
|
* Select a container
|
||||||
|
@ -38,6 +36,14 @@ export function SelectContainer(
|
||||||
setHistoryCurrentStep(history.length);
|
setHistoryCurrentStep(history.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a container
|
||||||
|
* @param containerId containerId of the container to delete
|
||||||
|
* @param fullHistory History of the editor
|
||||||
|
* @param historyCurrentStep Current step
|
||||||
|
* @param setHistory State setter for History
|
||||||
|
* @param setHistoryCurrentStep State setter for current step
|
||||||
|
*/
|
||||||
export function DeleteContainer(
|
export function DeleteContainer(
|
||||||
containerId: string,
|
containerId: string,
|
||||||
fullHistory: HistoryState[],
|
fullHistory: HistoryState[],
|
||||||
|
@ -93,6 +99,11 @@ export function DeleteContainer(
|
||||||
/**
|
/**
|
||||||
* Add a new container to a selected container
|
* Add a new container to a selected container
|
||||||
* @param type The type of container
|
* @param type The type of container
|
||||||
|
* @param configuration Configuration of the App
|
||||||
|
* @param fullHistory History of the editor
|
||||||
|
* @param historyCurrentStep Current step
|
||||||
|
* @param setHistory State setter for History
|
||||||
|
* @param setHistoryCurrentStep State setter for current step
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
export function AddContainerToSelectedContainer(
|
export function AddContainerToSelectedContainer(
|
||||||
|
@ -124,6 +135,18 @@ export function AddContainerToSelectedContainer(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and add a new container at `index` in children of parent of `parentId`
|
||||||
|
* @param index Index where to insert to the new container
|
||||||
|
* @param type Type of container
|
||||||
|
* @param parentId Parent in which to insert the new container
|
||||||
|
* @param configuration Configuration of the app
|
||||||
|
* @param fullHistory History of the editor
|
||||||
|
* @param historyCurrentStep Current step
|
||||||
|
* @param setHistory State setter of History
|
||||||
|
* @param setHistoryCurrentStep State setter of the current step
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
export function AddContainer(
|
export function AddContainer(
|
||||||
index: number,
|
index: number,
|
||||||
type: string,
|
type: string,
|
||||||
|
@ -217,339 +240,3 @@ export function AddContainer(
|
||||||
}]));
|
}]));
|
||||||
setHistoryCurrentStep(history.length);
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,9 +4,10 @@ import { Configuration } from '../../Interfaces/Configuration';
|
||||||
import { SVG } from '../SVG/SVG';
|
import { SVG } from '../SVG/SVG';
|
||||||
import { HistoryState } from '../../Interfaces/HistoryState';
|
import { HistoryState } from '../../Interfaces/HistoryState';
|
||||||
import { UI } from '../UI/UI';
|
import { UI } from '../UI/UI';
|
||||||
import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer, OnPropertiesSubmit } from './ContainerOperations';
|
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations';
|
||||||
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save';
|
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save';
|
||||||
import { onKeyDown } from './Shortcuts';
|
import { onKeyDown } from './Shortcuts';
|
||||||
|
import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations';
|
||||||
|
|
||||||
interface IEditorProps {
|
interface IEditorProps {
|
||||||
configuration: Configuration
|
configuration: Configuration
|
||||||
|
|
136
src/Components/Editor/PropertiesOperations.ts
Normal file
136
src/Components/Editor/PropertiesOperations.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { IContainerModel, ContainerModel } from '../../Interfaces/ContainerModel';
|
||||||
|
import { HistoryState } from '../../Interfaces/HistoryState';
|
||||||
|
import Properties from '../../Interfaces/Properties';
|
||||||
|
import { findContainerById } from '../../utils/itertools';
|
||||||
|
import { getCurrentHistory } from './Editor';
|
||||||
|
import { RecalculatePhysics } from './RigidBodyBehaviors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 properties Properties of the selected container
|
||||||
|
* @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);
|
||||||
|
}
|
280
src/Components/Editor/RigidBodyBehaviors.ts
Normal file
280
src/Components/Editor/RigidBodyBehaviors.ts
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
import { IContainerModel } from '../../Interfaces/ContainerModel';
|
||||||
|
import { SizePointer } from '../../Interfaces/SizePointer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "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 = Number(parentProperties.width);
|
||||||
|
const parentHeight = Number(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 = 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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 = 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)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
Number(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 {SizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space)
|
||||||
|
*/
|
||||||
|
function getAvailableWidths(
|
||||||
|
container: IContainerModel,
|
||||||
|
exception: IContainerModel
|
||||||
|
): SizePointer[] {
|
||||||
|
// Initialize the first size pointer
|
||||||
|
// which takes full width of the available space
|
||||||
|
const x = 0;
|
||||||
|
const width = Number(container.properties.width);
|
||||||
|
let unallocatedSpaces: SizePointer[] = [{ 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
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const child of rigidBodies) {
|
||||||
|
// Ignore the exception
|
||||||
|
if (child === exception) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the space of the child that is inside the parent
|
||||||
|
let newUnallocatedSpace: SizePointer[] = [];
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
child.properties.x,
|
||||||
|
child.properties.x + Number(child.properties.width)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: SizePointer
|
||||||
|
): boolean => Number(container.properties.width) <= sizePointer.width;
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* A SizePointer is a pointer in a 1 dimensional array of width/space
|
||||||
|
* x being the address where the pointer is pointing
|
||||||
|
* width being the overall (un)allocated space affected to the address
|
||||||
|
*/
|
||||||
export interface SizePointer {
|
export interface SizePointer {
|
||||||
x: number
|
x: number
|
||||||
width: number
|
width: number
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue