Merged PR 17: Implement rigid body Fix multiple bugs

Implement rigid body

Fix saveload bug: having null elements
Fix events being duplicated and not being removed
This commit is contained in:
Eric Nguyen 2022-08-11 08:43:10 +00:00
parent d2e1d9f0a4
commit 616fe3e9ac
22 changed files with 804 additions and 95 deletions

View file

@ -1,9 +1,10 @@
import { Dispatch, SetStateAction } from 'react';
import { HistoryState } from "../../Interfaces/HistoryState";
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';
/**
* Select a container
@ -19,22 +20,19 @@ export function SelectContainer(
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
if (current.MainContainer === null) {
throw new Error('[SelectContainer] Tried to select a container while there is no main container!');
}
const mainContainerClone = structuredClone(current.MainContainer);
const SelectedContainer = findContainerById(mainContainerClone, container.properties.id);
const selectedContainer = findContainerById(mainContainerClone, container.properties.id);
if (SelectedContainer === undefined) {
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,
TypeCounters: Object.assign({}, current.TypeCounters),
SelectedContainer,
SelectedContainerId: SelectedContainer.properties.id
SelectedContainer: selectedContainer,
SelectedContainerId: selectedContainer.properties.id,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
}
@ -49,10 +47,6 @@ export function DeleteContainer(
const history = getCurrentHistory(fullHistory, historyCurrentStep);
const current = history[historyCurrentStep];
if (current.MainContainer === null) {
throw new Error('[DeleteContainer] Error: Tried to delete a container without a main container');
}
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container = findContainerById(mainContainerClone, containerId);
@ -62,7 +56,7 @@ export function DeleteContainer(
if (container === mainContainerClone) {
// TODO: Implement alert
throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed !');
throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!');
}
if (container === null || container === undefined) {
@ -77,9 +71,10 @@ export function DeleteContainer(
}
setHistory(history.concat([{
LastAction: `Delete container ${containerId}`,
MainContainer: mainContainerClone,
SelectedContainer: null,
SelectedContainerId: '',
MainContainer: mainContainerClone,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
@ -168,7 +163,7 @@ export function AddContainer(
}
let x = 0;
if (index !== 0) {
if (index > 0) {
const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1);
if (lastChild !== undefined) {
x = lastChild.properties.x + Number(lastChild.properties.width);
@ -185,6 +180,7 @@ export function AddContainer(
y: 0,
width: properties?.Width,
height: parentClone.properties.height,
isRigidBody: false,
...properties.Style
},
[],
@ -202,10 +198,11 @@ export function AddContainer(
// Update the state
setHistory(history.concat([{
LastAction: 'Add container',
MainContainer: clone,
TypeCounters: newCounters,
SelectedContainer: parentClone,
SelectedContainerId: parentClone.properties.id
SelectedContainerId: parentClone.properties.id,
TypeCounters: newCounters
}]));
setHistoryCurrentStep(history.length);
}
@ -218,7 +215,7 @@ export function AddContainer(
*/
export function OnPropertyChange(
key: string,
value: string | number,
value: string | number | boolean,
fullHistory: HistoryState[],
historyCurrentStep: number,
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
@ -232,18 +229,14 @@ export function OnPropertyChange(
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
}
if (current.MainContainer === null ||
current.MainContainer === undefined) {
throw new Error('[OnPropertyChange] Property was changed before the main container was added');
}
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,
MainContainer: selectedContainerClone,
TypeCounters: Object.assign({}, current.TypeCounters)
}]));
setHistoryCurrentStep(history.length);
@ -259,11 +252,223 @@ export function OnPropertyChange(
(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,
MainContainer: mainContainerClone,
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;
}