Compare commits

..

5 commits

Author SHA1 Message Date
Eric NGUYEN
ab867b6b5c Fix rigid body wrong sorting due to not using the middle and the theorical position of the container
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-12 15:55:14 +02:00
Eric NGUYEN
10d13b246d Implement impose position 2022-08-12 14:57:35 +02:00
Eric NGUYEN
0b41f7ac2c Implement isAnchor basics properties + fix IsRigidbody 2022-08-12 14:16:44 +02:00
Eric NGUYEN
42d6d30763 Fix x, y not a number (partially) 2022-08-12 14:13:33 +02:00
Eric NGUYEN
d4abe8966e Change extension of EditorState 2022-08-12 13:52:51 +02:00
11 changed files with 168 additions and 75 deletions

View file

@ -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'
}

View file

@ -0,0 +1,64 @@
/**
* @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
* Apply the following modification to the overlapping rigid body container :
*/
export function ImposePosition(container: IContainerModel): IContainerModel {
if (container.parent === undefined ||
container.parent === null) {
return container;
}
// Get the closest one
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;
}
function getOverlappingContainers(
container: IContainerModel,
siblings: IContainerModel[]
): IContainerModel[] {
const min1 = container.properties.x;
const max1 = container.properties.x + Number(container.properties.width);
const overlappingContainers: IContainerModel[] = [];
for (const sibling of siblings) {
if (sibling === container) {
continue;
}
const min2 = sibling.properties.x;
const max2 = sibling.properties.x + Number(sibling.properties.width);
const isOverlapping = Math.min(max1, max2) - Math.max(min1, min2) > 0;
if (!isOverlapping) {
continue;
}
overlappingContainers.push(sibling);
}
return overlappingContainers;
}

View file

@ -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
@ -262,7 +284,7 @@ function getAvailableWidthsTwoLines(
width: min2 - min1
},
{
x: min2,
x: max2,
width: max1 - max2
}
];

View file

@ -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(
}
}
// Create the container
const newContainer = new ContainerModel(
parentClone,
{
const defaultProperties: IProperties = {
id: `${type}-${count}`,
parentId: parentClone.properties.id,
x,
y: 0,
width: properties?.Width,
width: properties.Width,
height: parentClone.properties.height,
isRigidBody: false,
isAnchor: false,
XPositionReference: properties.XPositionReference,
...properties.Style
},
};
// Create the container
const newContainer = new ContainerModel(
parentClone,
defaultProperties,
[],
{
type

View file

@ -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!');
}
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;
}
}
}

View file

@ -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: {}
};

View file

@ -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) => {

View file

@ -3,5 +3,6 @@ export const INPUT_TYPES: Record<string, string> = {
y: 'number',
width: 'number',
height: 'number',
isRigidBody: 'checkbox'
isRigidBody: 'checkbox',
isAnchor: 'checkbox'
};

View file

@ -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
}

View file

@ -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'
};