Implement anchor and fix bugs with rigid body #27

Merged
Siklos merged 9 commits from dev.anchor into dev 2022-08-12 11:47:22 -04:00
12 changed files with 202 additions and 97 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,70 @@
/**
* @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 to its siblings
* Apply the following modification to the overlapping rigid body container :
* @param container Container to impose its position
*/
export function ImposePosition(container: IContainerModel): IContainerModel {
if (container.parent === undefined ||
container.parent === null) {
return container;
}
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;
}
/**
* Returns the overlapping containers with container
* @param container A container
* @param containers A list of containers
* @returns A list of overlapping containers
*/
function getOverlappingContainers(
container: IContainerModel,
containers: IContainerModel[]
): IContainerModel[] {
const min1 = container.properties.x;
const max1 = container.properties.x + Number(container.properties.width);
const overlappingContainers: IContainerModel[] = [];
for (const other of containers) {
if (other === container) {
continue;
}
const min2 = other.properties.x;
const max2 = other.properties.x + Number(other.properties.width);
const isOverlapping = Math.min(max1, max2) - Math.max(min1, min2) > 0;
if (!isOverlapping) {
continue;
}
overlappingContainers.push(other);
}
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
@ -218,39 +240,49 @@ function getAvailableWidths(
/** /**
* Returns the unallocated widths between two lines in 1D * Returns the unallocated widths between two lines in 1D
* @param min1 left of the first line * @param unalloctedSpaceLeft left of the first line
* @param max1 rigth of the first line * @param unallocatedSpaceRight rigth of the first line
* @param min2 left of the second line * @param rectLeft left of the second line
* @param max2 right of the second line * @param rectRight right of the second line
* @returns Available widths * @returns Available widths
*/ */
function getAvailableWidthsTwoLines( function getAvailableWidthsTwoLines(
min1: number, unalloctedSpaceLeft: number,
max1: number, unallocatedSpaceRight: number,
min2: number, rectLeft: number,
max2: number rectRight: number
): ISizePointer[] { ): ISizePointer[] {
if (min2 < min1 && max2 > max1) { if (unallocatedSpaceRight < rectLeft ||
unalloctedSpaceLeft > rectRight
) {
// object 1 and 2 are not overlapping
return [{
x: unalloctedSpaceLeft,
width: unallocatedSpaceRight - unalloctedSpaceLeft
}];
}
if (rectLeft < unalloctedSpaceLeft && rectRight > unallocatedSpaceRight) {
// object 2 is overlapping full width // object 2 is overlapping full width
return []; return [];
} }
if (min1 >= min2) { if (unalloctedSpaceLeft >= rectLeft) {
// object 2 is partially overlapping on the left // object 2 is partially overlapping on the left
return [ return [
{ {
x: max2, x: rectRight,
width: max1 - max2 width: unallocatedSpaceRight - rectRight
} }
]; ];
} }
if (max2 >= max1) { if (rectRight >= unallocatedSpaceRight) {
// object 2 is partially overlapping on the right // object 2 is partially overlapping on the right
return [ return [
{ {
x: min2, x: unalloctedSpaceLeft,
width: max2 - min1 width: rectRight - unalloctedSpaceLeft
} }
]; ];
} }
@ -258,12 +290,12 @@ function getAvailableWidthsTwoLines(
// object 2 is overlapping in the middle // object 2 is overlapping in the middle
return [ return [
{ {
x: min1, x: unalloctedSpaceLeft,
width: min2 - min1 width: rectLeft - unalloctedSpaceLeft
}, },
{ {
x: min2, x: rectRight,
width: max1 - max2 width: unallocatedSpaceRight - rectRight
} }
]; ];
} }

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(
} }
} }
// Create the container const defaultProperties: IProperties = {
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,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!');
} }
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);
@ -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,4 +0,0 @@
export enum AddingBehavior {
InsertInto,
Replace
}

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