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 { IConfiguration } from '../../Interfaces/IConfiguration';
import { ContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel } from '../../Interfaces/IContainerModel';
import { fetchConfiguration } from '../API/api'; import { fetchConfiguration } from '../API/api';
import { IEditorState } from "../../Interfaces/IEditorState"; import { IEditorState } from '../../Interfaces/IEditorState';
import { LoadState } from './Load'; import { LoadState } from './Load';
export function NewEditor( export function NewEditor(
@ -23,6 +23,7 @@ export function NewEditor(
width: configuration.MainContainer.Width, width: configuration.MainContainer.Width,
height: configuration.MainContainer.Height, height: configuration.MainContainer.Height,
isRigidBody: false, isRigidBody: false,
isAnchor: false,
fillOpacity: 0, fillOpacity: 0,
stroke: 'black' 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" * "Transform the container into a rigid body"
@ -82,7 +90,7 @@ function constraintBodyInsideSpace(
if (containerX < x) { if (containerX < x) {
containerProperties.x = x; containerProperties.x = x;
} }
if (containerX + containerWidth > width) { if (containerX + containerWidth > x + width) {
containerProperties.x = x + width - containerWidth; containerProperties.x = x + width - containerWidth;
} }
@ -90,7 +98,7 @@ function constraintBodyInsideSpace(
if (containerY < y) { if (containerY < y) {
containerProperties.y = y; containerProperties.y = y;
} }
if (containerY + containerHeight > height) { if (containerY + containerHeight > y + height) {
containerProperties.y = y + height - containerHeight; containerProperties.y = y + height - containerHeight;
} }
@ -104,7 +112,7 @@ function constraintBodyInsideSpace(
* @param container * @param container
* @returns Updated container * @returns Updated container
*/ */
function constraintBodyInsideUnallocatedWidth( export function constraintBodyInsideUnallocatedWidth(
container: IContainerModel container: IContainerModel
): IContainerModel { ): IContainerModel {
if (container.parent === null) { if (container.parent === null) {
@ -114,12 +122,7 @@ function constraintBodyInsideUnallocatedWidth(
// Get the available spaces of the parent // Get the available spaces of the parent
const availableWidths = getAvailableWidths(container.parent, container); const availableWidths = getAvailableWidths(container.parent, container);
const containerX = Number(container.properties.x); const containerX = Number(container.properties.x);
const containerWidth = Number(container.properties.width);
// 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 // Check if there is still some space
if (availableWidths.length === 0) { 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 // Check if the container actually fit inside
// It will usually fit if it was alrady fitting // It will usually fit if it was alrady fitting
const availableWidthFound = availableWidths.find((width) => const availableWidthFound = availableWidths.find((width) =>
@ -179,17 +200,18 @@ function getAvailableWidths(
const width = Number(container.properties.width); const width = Number(container.properties.width);
let unallocatedSpaces: ISizePointer[] = [{ x, width }]; let unallocatedSpaces: ISizePointer[] = [{ x, width }];
// We will only uses containers that also have the rigid bodies // We will only uses containers that also are rigid or are anchors
// as out-of-bound or enormouse containers should be ignored const solidBodies = container.children.filter(
const rigidBodies = container.children.filter( (child) => child.properties.isRigidBody || child.properties.isAnchor
(child) => child.properties.isRigidBody
); );
for (const child of rigidBodies) { for (const child of solidBodies) {
// Ignore the exception // Ignore the exception
if (child === exception) { if (child === exception) {
continue; continue;
} }
const childX = child.properties.x;
const childWidth = Number(child.properties.width);
// get the space of the child that is inside the parent // get the space of the child that is inside the parent
let newUnallocatedSpace: ISizePointer[] = []; let newUnallocatedSpace: ISizePointer[] = [];
@ -202,8 +224,8 @@ function getAvailableWidths(
const newUnallocatedWidths = getAvailableWidthsTwoLines( const newUnallocatedWidths = getAvailableWidthsTwoLines(
unallocatedSpace.x, unallocatedSpace.x,
unallocatedSpace.x + unallocatedSpace.width, unallocatedSpace.x + unallocatedSpace.width,
child.properties.x, childX,
child.properties.x + Number(child.properties.width) childX + childWidth
); );
// Concat the new list of SizePointer pointing to availables spaces // Concat the new list of SizePointer pointing to availables spaces
@ -262,7 +284,7 @@ function getAvailableWidthsTwoLines(
width: min2 - min1 width: min2 - min1
}, },
{ {
x: min2, x: max2,
width: max1 - max2 width: max1 - max2
} }
]; ];

View file

@ -4,6 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
import { findContainerById } from '../../utils/itertools'; import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor'; import { getCurrentHistory } from './Editor';
import IProperties from '../../Interfaces/IProperties';
/** /**
* Select a container * Select a container
@ -203,20 +204,23 @@ export function AddContainer(
} }
} }
const defaultProperties: IProperties = {
id: `${type}-${count}`,
parentId: parentClone.properties.id,
x,
y: 0,
width: properties.Width,
height: parentClone.properties.height,
isRigidBody: false,
isAnchor: false,
XPositionReference: properties.XPositionReference,
...properties.Style
};
// Create the container // Create the container
const newContainer = new ContainerModel( const newContainer = new ContainerModel(
parentClone, parentClone,
{ defaultProperties,
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 type

View file

@ -4,7 +4,9 @@ import { IHistoryState } from '../../Interfaces/IHistoryState';
import IProperties from '../../Interfaces/IProperties'; import IProperties from '../../Interfaces/IProperties';
import { findContainerById } from '../../utils/itertools'; import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor'; 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 * 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'); 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 mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); 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!'); throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
} }
(container.properties as any)[key] = value; 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) { if (container.properties.isRigidBody) {
RecalculatePhysics(container); RecalculatePhysics(container);
@ -88,25 +84,6 @@ export function OnPropertiesSubmit(
throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); 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 mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); 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}`); const input = (event.target as HTMLFormElement).querySelector(`#${property}`);
if (input instanceof HTMLInputElement) { if (input instanceof HTMLInputElement) {
(container.properties as any)[property] = input.value; (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, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false isRigidBody: false,
isAnchor: false
}, },
userData: {} userData: {}
}} }}
@ -47,7 +48,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false isRigidBody: false,
isAnchor: false
}, },
userData: {} userData: {}
}; };
@ -103,7 +105,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false isRigidBody: false,
isAnchor: false
}, },
userData: {} userData: {}
}; };
@ -119,7 +122,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 0, width: 0,
height: 0, height: 0,
isRigidBody: false isRigidBody: false,
isAnchor: false
}, },
userData: {} userData: {}
} }
@ -136,7 +140,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 0, width: 0,
height: 0, height: 0,
isRigidBody: false isRigidBody: false,
isAnchor: false
}, },
userData: {} userData: {}
} }
@ -173,7 +178,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false isRigidBody: false,
isAnchor: false
}, },
userData: {} userData: {}
}; };
@ -188,7 +194,8 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 0, width: 0,
height: 0, height: 0,
isRigidBody: false isRigidBody: false,
isAnchor: false
}, },
userData: {} userData: {}
}; };

View file

@ -23,7 +23,8 @@ describe.concurrent('Properties', () => {
parentId: 'parentId', parentId: 'parentId',
x: 1, x: 1,
y: 1, y: 1,
isRigidBody: false isRigidBody: false,
isAnchor: false
}; };
const handleChange = vi.fn((key, value) => { const handleChange = vi.fn((key, value) => {

View file

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

View file

@ -1,11 +1,21 @@
import * as React from 'react'; import * as React from 'react';
import { XPositionReference } from '../Enums/XPositionReference'; 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 { export default interface IProperties extends React.CSSProperties {
id: string id: string
parentId: string | null parentId: string | null
x: number x: number
y: number y: number
isRigidBody: boolean isRigidBody: boolean
isAnchor: boolean
XPositionReference?: XPositionReference XPositionReference?: XPositionReference
} }

View file

@ -33,6 +33,7 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
width: DEFAULT_CONFIG.MainContainer.Width, width: DEFAULT_CONFIG.MainContainer.Width,
height: DEFAULT_CONFIG.MainContainer.Height, height: DEFAULT_CONFIG.MainContainer.Height,
isRigidBody: false, isRigidBody: false,
isAnchor: false,
fillOpacity: 0, fillOpacity: 0,
stroke: 'black' stroke: 'black'
}; };