Compare commits

..

No commits in common. "ab867b6b5cec26af815a48969218e3420006a3c4" and "d60410f7f2c41b67b428e45c71e5c3535279d9ef" have entirely different histories.

11 changed files with 75 additions and 168 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,7 +23,6 @@ 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

@ -1,64 +0,0 @@
/**
* @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

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

View file

@ -4,9 +4,7 @@ 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 './Behaviors/RigidBodyBehaviors'; import { RecalculatePhysics } from './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
@ -30,6 +28,20 @@ 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);
@ -37,15 +49,7 @@ 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!');
} }
if (INPUT_TYPES[key] === 'number') {
(container.properties as any)[key] = Number(value);
} else {
(container.properties as any)[key] = value; (container.properties as any)[key] = value;
}
if (container.properties.isAnchor) {
ImposePosition(container);
}
if (container.properties.isRigidBody) { if (container.properties.isRigidBody) {
RecalculatePhysics(container); RecalculatePhysics(container);
@ -84,6 +88,25 @@ 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);
@ -95,11 +118,6 @@ 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

@ -1,13 +1,5 @@
/** import { IContainerModel } from '../../Interfaces/IContainerModel';
* @module RigidBodyBehaviors import { ISizePointer } from '../../Interfaces/ISizePointer';
* 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"
@ -90,7 +82,7 @@ function constraintBodyInsideSpace(
if (containerX < x) { if (containerX < x) {
containerProperties.x = x; containerProperties.x = x;
} }
if (containerX + containerWidth > x + width) { if (containerX + containerWidth > width) {
containerProperties.x = x + width - containerWidth; containerProperties.x = x + width - containerWidth;
} }
@ -98,7 +90,7 @@ function constraintBodyInsideSpace(
if (containerY < y) { if (containerY < y) {
containerProperties.y = y; containerProperties.y = y;
} }
if (containerY + containerHeight > y + height) { if (containerY + containerHeight > height) {
containerProperties.y = y + height - containerHeight; containerProperties.y = y + height - containerHeight;
} }
@ -112,7 +104,7 @@ function constraintBodyInsideSpace(
* @param container * @param container
* @returns Updated container * @returns Updated container
*/ */
export function constraintBodyInsideUnallocatedWidth( function constraintBodyInsideUnallocatedWidth(
container: IContainerModel container: IContainerModel
): IContainerModel { ): IContainerModel {
if (container.parent === null) { if (container.parent === null) {
@ -122,7 +114,12 @@ export 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) {
@ -131,24 +128,6 @@ export 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) =>
@ -200,18 +179,17 @@ 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 are rigid or are anchors // We will only uses containers that also have the rigid bodies
const solidBodies = container.children.filter( // as out-of-bound or enormouse containers should be ignored
(child) => child.properties.isRigidBody || child.properties.isAnchor const rigidBodies = container.children.filter(
(child) => child.properties.isRigidBody
); );
for (const child of solidBodies) { for (const child of rigidBodies) {
// 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[] = [];
@ -224,8 +202,8 @@ function getAvailableWidths(
const newUnallocatedWidths = getAvailableWidthsTwoLines( const newUnallocatedWidths = getAvailableWidthsTwoLines(
unallocatedSpace.x, unallocatedSpace.x,
unallocatedSpace.x + unallocatedSpace.width, unallocatedSpace.x + unallocatedSpace.width,
childX, child.properties.x,
childX + childWidth child.properties.x + Number(child.properties.width)
); );
// Concat the new list of SizePointer pointing to availables spaces // Concat the new list of SizePointer pointing to availables spaces
@ -284,7 +262,7 @@ function getAvailableWidthsTwoLines(
width: min2 - min1 width: min2 - min1
}, },
{ {
x: max2, x: min2,
width: max1 - max2 width: max1 - max2
} }
]; ];

View file

@ -17,8 +17,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false, isRigidBody: false
isAnchor: false
}, },
userData: {} userData: {}
}} }}
@ -48,8 +47,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false, isRigidBody: false
isAnchor: false
}, },
userData: {} userData: {}
}; };
@ -105,8 +103,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false, isRigidBody: false
isAnchor: false
}, },
userData: {} userData: {}
}; };
@ -122,8 +119,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 0, width: 0,
height: 0, height: 0,
isRigidBody: false, isRigidBody: false
isAnchor: false
}, },
userData: {} userData: {}
} }
@ -140,8 +136,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 0, width: 0,
height: 0, height: 0,
isRigidBody: false, isRigidBody: false
isAnchor: false
}, },
userData: {} userData: {}
} }
@ -178,8 +173,7 @@ describe.concurrent('Elements sidebar', () => {
y: 0, y: 0,
width: 2000, width: 2000,
height: 100, height: 100,
isRigidBody: false, isRigidBody: false
isAnchor: false
}, },
userData: {} userData: {}
}; };
@ -194,8 +188,7 @@ 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,8 +23,7 @@ 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,6 +3,5 @@ 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,21 +1,11 @@
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,7 +33,6 @@ 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'
}; };