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

@ -17,6 +17,7 @@ module.exports = {
project: './tsconfig.json'
},
plugins: [
'only-warn',
'react',
'@typescript-eslint'
],

View file

@ -4,6 +4,8 @@
[![Build Status](https://drone.siklos-chaneru.duckdns.org/api/badges/Siklos/svg-layout-designer-react/status.svg?ref=refs/heads/dev)](https://drone.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react)
[![Build Status](https://dev.azure.com/enguyen0660/SVGLayoutDesignerReact/_apis/build/status/SVGLayoutDesignerReact?branchName=dev)](https://dev.azure.com/enguyen0660/SVGLayoutDesignerReact/_build/latest?definitionId=4&branchName=dev)
An svg layout designer.
# Getting Started

View file

@ -37,6 +37,7 @@
"eslint-config-standard-with-typescript": "^22.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.4",
"eslint-plugin-only-warn": "^1.0.3",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.30.1",
"jsdom": "^20.0.0",

7
pnpm-lock.yaml generated
View file

@ -19,6 +19,7 @@ specifiers:
eslint-config-standard-with-typescript: ^22.0.0
eslint-plugin-import: ^2.26.0
eslint-plugin-n: ^15.2.4
eslint-plugin-only-warn: ^1.0.3
eslint-plugin-promise: ^6.0.0
eslint-plugin-react: ^7.30.1
framer-motion: ^6.5.1
@ -58,6 +59,7 @@ devDependencies:
eslint-config-standard-with-typescript: 22.0.0_mfupvx5msz6are6ggwiepter3m
eslint-plugin-import: 2.26.0_wuikv5nqgdfyng42xxm7lklfmi
eslint-plugin-n: 15.2.4_eslint@8.21.0
eslint-plugin-only-warn: 1.0.3
eslint-plugin-promise: 6.0.0_eslint@8.21.0
eslint-plugin-react: 7.30.1_eslint@8.21.0
jsdom: 20.0.0
@ -1724,6 +1726,11 @@ packages:
semver: 7.3.7
dev: true
/eslint-plugin-only-warn/1.0.3:
resolution: {integrity: sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==}
engines: {node: '>=6'}
dev: true
/eslint-plugin-promise/6.0.0_eslint@8.21.0:
resolution: {integrity: sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}

View file

@ -23,6 +23,7 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
const [editorState, setEditorState] = useState<IEditorState>({
configuration: DEFAULT_CONFIG,
history: [{
LastAction: '',
MainContainer: defaultMainContainer,
SelectedContainer: defaultMainContainer,
SelectedContainerId: defaultMainContainer.properties.id,
@ -40,6 +41,7 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
return;
}
if (!isLoaded) {
fetch(state)
.then(
async(response) => await response.json(),
@ -48,6 +50,7 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
.then((data: IEditorState) => {
LoadState(data, setEditorState, setLoaded);
}, (error) => { throw new Error(error); });
}
});
if (isLoaded) {

View file

@ -22,6 +22,7 @@ export function NewEditor(
y: 0,
width: configuration.MainContainer.Width,
height: configuration.MainContainer.Height,
isRigidBody: false,
fillOpacity: 0,
stroke: 'black'
}
@ -34,6 +35,7 @@ export function NewEditor(
history:
[
{
LastAction: '',
MainContainer,
SelectedContainer: MainContainer,
SelectedContainerId: MainContainer.properties.id,

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

View file

@ -24,24 +24,21 @@ export const getCurrentHistory = (history: HistoryState[], historyCurrentStep: n
export const getCurrentHistoryState = (history: HistoryState[], historyCurrentStep: number): HistoryState => history[historyCurrentStep];
const Editor: React.FunctionComponent<IEditorProps> = (props) => {
const [history, setHistory] = React.useState<HistoryState[]>([...props.history]);
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(0);
const [history, setHistory] = React.useState<HistoryState[]>(structuredClone(props.history));
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
React.useEffect(() => {
window.addEventListener('keyup', (event) => onKeyDown(
const onKeyUp = (event: KeyboardEvent): void => onKeyDown(
event,
history,
historyCurrentStep,
setHistoryCurrentStep
));
);
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keyup', (event) => onKeyDown(
event,
history,
historyCurrentStep,
setHistoryCurrentStep
));
window.removeEventListener('keyup', onKeyUp);
};
});

View file

@ -1,5 +1,5 @@
import { Dispatch, SetStateAction } from 'react';
import { HistoryState } from "../../Interfaces/HistoryState";
import { HistoryState } from '../../Interfaces/HistoryState';
export function onKeyDown(
event: KeyboardEvent,

View file

@ -16,14 +16,15 @@ describe.concurrent('Elements sidebar', () => {
x: 0,
y: 0,
width: 2000,
height: 100
height: 100,
isRigidBody: false
},
userData: {}
}}
isOpen={true}
isHistoryOpen={false}
SelectedContainer={null}
onPropertyChange={() => {}}
OnPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
@ -44,7 +45,8 @@ describe.concurrent('Elements sidebar', () => {
x: 0,
y: 0,
width: 2000,
height: 100
height: 100,
isRigidBody: false
},
userData: {}
};
@ -54,7 +56,7 @@ describe.concurrent('Elements sidebar', () => {
isOpen={true}
isHistoryOpen={false}
SelectedContainer={MainContainer}
onPropertyChange={() => {}}
OnPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
@ -98,7 +100,8 @@ describe.concurrent('Elements sidebar', () => {
x: 0,
y: 0,
width: 2000,
height: 100
height: 100,
isRigidBody: false
},
userData: {}
};
@ -113,7 +116,8 @@ describe.concurrent('Elements sidebar', () => {
x: 0,
y: 0,
width: 0,
height: 0
height: 0,
isRigidBody: false
},
userData: {}
}
@ -129,7 +133,8 @@ describe.concurrent('Elements sidebar', () => {
x: 0,
y: 0,
width: 0,
height: 0
height: 0,
isRigidBody: false
},
userData: {}
}
@ -140,7 +145,7 @@ describe.concurrent('Elements sidebar', () => {
isOpen={true}
isHistoryOpen={false}
SelectedContainer={MainContainer}
onPropertyChange={() => {}}
OnPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
@ -164,7 +169,8 @@ describe.concurrent('Elements sidebar', () => {
x: 0,
y: 0,
width: 2000,
height: 100
height: 100,
isRigidBody: false
},
userData: {}
};
@ -178,7 +184,8 @@ describe.concurrent('Elements sidebar', () => {
x: 0,
y: 0,
width: 0,
height: 0
height: 0,
isRigidBody: false
},
userData: {}
};
@ -194,7 +201,7 @@ describe.concurrent('Elements sidebar', () => {
isOpen={true}
isHistoryOpen={false}
SelectedContainer={SelectedContainer}
onPropertyChange={() => {}}
OnPropertyChange={() => {}}
SelectContainer={selectContainer}
DeleteContainer={() => {}}
AddContainer={() => {}}
@ -217,7 +224,7 @@ describe.concurrent('Elements sidebar', () => {
isOpen={true}
isHistoryOpen={false}
SelectedContainer={SelectedContainer}
onPropertyChange={() => {}}
OnPropertyChange={() => {}}
SelectContainer={selectContainer}
DeleteContainer={() => {}}
AddContainer={() => {}}

View file

@ -13,7 +13,7 @@ interface IElementsSidebarProps {
isOpen: boolean
isHistoryOpen: boolean
SelectedContainer: IContainerModel | null
onPropertyChange: (key: string, value: string) => void
OnPropertyChange: (key: string, value: string | number | boolean) => void
SelectContainer: (container: IContainerModel) => void
DeleteContainer: (containerid: string) => void
AddContainer: (index: number, type: string, parent: string) => void
@ -71,40 +71,39 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
// Event listeners
React.useEffect(() => {
elementRef.current?.addEventListener(
'contextmenu',
(event) => handleRightClick(
const onContextMenu = (event: MouseEvent): void => handleRightClick(
event,
setIsContextMenuOpen,
setOnClickContainerId,
setContextMenuPosition
));
);
const onLeftClick = (): void => handleLeftClick(
isContextMenuOpen,
setIsContextMenuOpen,
setOnClickContainerId
);
elementRef.current?.addEventListener(
'contextmenu',
onContextMenu
);
window.addEventListener(
'click',
(event) => handleLeftClick(
isContextMenuOpen,
setIsContextMenuOpen,
setOnClickContainerId
));
onLeftClick
);
return () => {
elementRef.current?.addEventListener(
elementRef.current?.removeEventListener(
'contextmenu',
(event) => handleRightClick(
event,
setIsContextMenuOpen,
setOnClickContainerId,
setContextMenuPosition
));
onContextMenu
);
window.removeEventListener(
'click',
(event) => handleLeftClick(
isContextMenuOpen,
setIsContextMenuOpen,
setOnClickContainerId
));
onLeftClick
);
};
}, []);
@ -141,9 +140,12 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
y={contextMenuPosition.y}
isOpen={isContextMenuOpen}
>
<MenuItem className='contextmenu-item' text='Delete' onClick={() => props.DeleteContainer(onClickContainerId)} />
<MenuItem className='contextmenu-item' text='Delete' onClick={() => {
setIsContextMenuOpen(false);
props.DeleteContainer(onClickContainerId);
}} />
</Menu>
<Properties properties={props.SelectedContainer?.properties} onChange={props.onPropertyChange}></Properties>
<Properties properties={props.SelectedContainer?.properties} onChange={props.OnPropertyChange}></Properties>
</div>
);
};

View file

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

View file

@ -1,9 +1,10 @@
import * as React from 'react';
import ContainerProperties from '../../Interfaces/Properties';
import { INPUT_TYPES } from './PropertiesInputTypes';
interface IPropertiesProps {
properties?: ContainerProperties
onChange: (key: string, value: string) => void
onChange: (key: string, value: string | number | boolean) => void
}
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
@ -26,11 +27,24 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
const handleProperties = (
[key, value]: [string, string | number],
groupInput: React.ReactNode[],
onChange: (key: string, value: string) => void
onChange: (key: string, value: string | number | boolean) => void
): void => {
const id = `property-${key}`;
const type = 'text';
const isDisabled = key === 'id' || key === 'parentId'; // hardcoded
let type = 'text';
let checked;
/// hardcoded stuff for ergonomy ///
if (typeof value === 'boolean') {
checked = value;
}
if (key in INPUT_TYPES) {
type = INPUT_TYPES[key];
}
const isDisabled = ['id', 'parentId'].includes(key);
///
groupInput.push(
<div key={id} className='mt-4'>
<label className='text-sm font-medium text-gray-800' htmlFor={id}>{key}</label>
@ -43,7 +57,14 @@ const handleProperties = (
type={type}
id={id}
value={value}
onChange={(event) => onChange(key, event.target.value)}
checked={checked}
onChange={(event) => {
if (type === 'checkbox') {
onChange(key, event.target.checked);
return;
}
onChange(key, event.target.value);
}}
disabled={isDisabled}
/>
</div>

View file

@ -0,0 +1,7 @@
export const INPUT_TYPES: Record<string, string> = {
x: 'number',
y: 'number',
width: 'number',
height: 'number',
isRigidBody: 'checkbox'
};

View file

@ -35,10 +35,11 @@ export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
});
React.useEffect(() => {
window.addEventListener('resize', () => resizeViewBox(setViewer));
const onResize = (): void => resizeViewBox(setViewer);
window.addEventListener('resize', onResize);
return () => {
window.addEventListener('resize', () => resizeViewBox(setViewer));
window.removeEventListener('resize', onResize);
};
});

View file

@ -4,7 +4,7 @@ import { Sidebar } from '../Sidebar/Sidebar';
import { History } from '../History/History';
import { AvailableContainer } from '../../Interfaces/AvailableContainer';
import { ContainerModel } from '../../Interfaces/ContainerModel';
import { HistoryState } from "../../Interfaces/HistoryState";
import { HistoryState } from '../../Interfaces/HistoryState';
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
import { FloatingButton } from '../FloatingButton/FloatingButton';
import { Bar } from '../Bar/Bar';
@ -16,7 +16,7 @@ interface IUIProps {
AvailableContainers: AvailableContainer[]
SelectContainer: (container: ContainerModel) => void
DeleteContainer: (containerId: string) => void
OnPropertyChange: (key: string, value: string) => void
OnPropertyChange: (key: string, value: string | number | boolean) => void
AddContainerToSelectedContainer: (type: string) => void
AddContainer: (index: number, type: string, parentId: string) => void
SaveEditorAsJSON: () => void
@ -58,7 +58,7 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
SelectedContainer={props.current.SelectedContainer}
isOpen={isElementsSidebarOpen}
isHistoryOpen={isHistoryOpen}
onPropertyChange={props.OnPropertyChange}
OnPropertyChange={props.OnPropertyChange}
SelectContainer={props.SelectContainer}
DeleteContainer={props.DeleteContainer}
AddContainer={props.AddContainer}

View file

@ -1,6 +1,7 @@
import { IContainerModel } from './ContainerModel';
export interface HistoryState {
LastAction: string
MainContainer: IContainerModel
SelectedContainer: IContainerModel | null
SelectedContainerId: string

View file

@ -5,4 +5,5 @@ export default interface Properties extends React.CSSProperties {
parentId: string | null
x: number
y: number
isRigidBody: boolean
}

View file

@ -0,0 +1,4 @@
export interface SizePointer {
x: number
width: number
}

View file

@ -0,0 +1,436 @@
{
"history": [
{
"MainContainer": {
"children": [],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "main",
"TypeCounters": {}
},
{
"LastAction": "Add container",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": 75,
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "main",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Select container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": 75,
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "7",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "2",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "20",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "200",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "2000",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "20000",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"isRigidBody": true,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
}
],
"historyCurrentStep": 10,
"configuration": {
"AvailableContainers": [
{
"Type": "Container",
"Width": 75,
"Height": 100,
"Style": {
"fillOpacity": 0,
"stroke": "green"
}
}
],
"AvailableSymbols": [],
"MainContainer": {
"Type": "Container",
"Width": 2000,
"Height": 100,
"Style": {
"fillOpacity": 0,
"stroke": "black"
}
}
}
}

View file

@ -30,6 +30,7 @@ export const DEFAULT_MAINCONTAINER_PROPS: Properties = {
parentId: 'null',
x: 0,
y: 0,
isRigidBody: false,
width: DEFAULT_CONFIG.MainContainer.Width,
height: DEFAULT_CONFIG.MainContainer.Height,
fillOpacity: 0,

View file

@ -8,6 +8,11 @@ import { IEditorState } from '../Components/Editor/Editor';
*/
export function Revive(editorState: IEditorState): void {
const history = editorState.history;
// restore last step
editorState.historyCurrentStep = history.length - 1;
// restore the parents and the selected container
for (const state of history) {
if (state.MainContainer === null || state.MainContainer === undefined) {
continue;
@ -43,6 +48,10 @@ export const getCircularReplacer = (): (key: any, value: object | null) => objec
return;
}
if (key === 'SelectedContainer') {
return;
}
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;