From 616fe3e9ac19f71f0a2d86ed3ba809a16df77385 Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Thu, 11 Aug 2022 08:43:10 +0000 Subject: [PATCH] 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 --- .eslintrc.cjs | 1 + README.md | 2 + package.json | 1 + pnpm-lock.yaml | 7 + src/Components/App/App.tsx | 19 +- src/Components/App/MenuActions.ts | 2 + src/Components/Editor/ContainerOperations.ts | 259 +++++++++-- src/Components/Editor/Editor.tsx | 17 +- src/Components/Editor/Shortcuts.ts | 2 +- .../ElementsSidebar/ElementsSidebar.test.tsx | 31 +- .../ElementsSidebar/ElementsSidebar.tsx | 54 +-- src/Components/Properties/Properties.test.tsx | 3 +- src/Components/Properties/Properties.tsx | 31 +- .../Properties/PropertiesInputTypes.tsx | 7 + src/Components/SVG/SVG.tsx | 5 +- src/Components/UI/UI.tsx | 6 +- src/Interfaces/HistoryState.ts | 1 + src/Interfaces/Properties.ts | 1 + src/Interfaces/SizePointer.ts | 4 + src/tests/resources/rigidbodytest.json | 436 ++++++++++++++++++ src/utils/default.ts | 1 + src/utils/saveload.ts | 9 + 22 files changed, 804 insertions(+), 95 deletions(-) create mode 100644 src/Components/Properties/PropertiesInputTypes.tsx create mode 100644 src/Interfaces/SizePointer.ts create mode 100644 src/tests/resources/rigidbodytest.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b72be75..33736e7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -17,6 +17,7 @@ module.exports = { project: './tsconfig.json' }, plugins: [ + 'only-warn', 'react', '@typescript-eslint' ], diff --git a/README.md b/README.md index 635d01b..895f9b3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index 148f302..1c45135 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9bce2d..10ead3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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} diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx index fa725cf..c25c37e 100644 --- a/src/Components/App/App.tsx +++ b/src/Components/App/App.tsx @@ -23,6 +23,7 @@ export const App: React.FunctionComponent = (props) => { const [editorState, setEditorState] = useState({ configuration: DEFAULT_CONFIG, history: [{ + LastAction: '', MainContainer: defaultMainContainer, SelectedContainer: defaultMainContainer, SelectedContainerId: defaultMainContainer.properties.id, @@ -40,14 +41,16 @@ export const App: React.FunctionComponent = (props) => { return; } - fetch(state) - .then( - async(response) => await response.json(), - (error) => { throw new Error(error); } - ) - .then((data: IEditorState) => { - LoadState(data, setEditorState, setLoaded); - }, (error) => { throw new Error(error); }); + if (!isLoaded) { + fetch(state) + .then( + async(response) => await response.json(), + (error) => { throw new Error(error); } + ) + .then((data: IEditorState) => { + LoadState(data, setEditorState, setLoaded); + }, (error) => { throw new Error(error); }); + } }); if (isLoaded) { diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts index ce0cd7d..2669c1a 100644 --- a/src/Components/App/MenuActions.ts +++ b/src/Components/App/MenuActions.ts @@ -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, diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index d9bdea5..ea4db42 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -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); @@ -62,7 +56,7 @@ export function DeleteContainer( if (container === mainContainerClone) { // TODO: Implement alert - throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed !'); + throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!'); } if (container === null || container === undefined) { @@ -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>, @@ -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; +} diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index cf67ed7..ad27c22 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -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 = (props) => { - const [history, setHistory] = React.useState([...props.history]); - const [historyCurrentStep, setHistoryCurrentStep] = React.useState(0); + const [history, setHistory] = React.useState(structuredClone(props.history)); + const [historyCurrentStep, setHistoryCurrentStep] = React.useState(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); }; }); diff --git a/src/Components/Editor/Shortcuts.ts b/src/Components/Editor/Shortcuts.ts index 00b449e..8ac6790 100644 --- a/src/Components/Editor/Shortcuts.ts +++ b/src/Components/Editor/Shortcuts.ts @@ -1,5 +1,5 @@ import { Dispatch, SetStateAction } from 'react'; -import { HistoryState } from "../../Interfaces/HistoryState"; +import { HistoryState } from '../../Interfaces/HistoryState'; export function onKeyDown( event: KeyboardEvent, diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index 230949d..c1372b1 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -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={() => {}} diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index f449960..873161d 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -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 = (props: IElement // Event listeners React.useEffect(() => { + const onContextMenu = (event: MouseEvent): void => handleRightClick( + event, + setIsContextMenuOpen, + setOnClickContainerId, + setContextMenuPosition + ); + + const onLeftClick = (): void => handleLeftClick( + isContextMenuOpen, + setIsContextMenuOpen, + setOnClickContainerId + ); + elementRef.current?.addEventListener( 'contextmenu', - (event) => handleRightClick( - event, - setIsContextMenuOpen, - setOnClickContainerId, - setContextMenuPosition - )); + 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 = (props: IElement y={contextMenuPosition.y} isOpen={isContextMenuOpen} > - props.DeleteContainer(onClickContainerId)} /> + { + setIsContextMenuOpen(false); + props.DeleteContainer(onClickContainerId); + }} /> - + ); }; diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 86a80e2..9f0fe13 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -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) => { diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index ca2056a..c1ac966 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -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 = (props: IPropertiesProps) => { @@ -26,11 +27,24 @@ export const Properties: React.FC = (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(
@@ -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} />
diff --git a/src/Components/Properties/PropertiesInputTypes.tsx b/src/Components/Properties/PropertiesInputTypes.tsx new file mode 100644 index 0000000..c62a8fd --- /dev/null +++ b/src/Components/Properties/PropertiesInputTypes.tsx @@ -0,0 +1,7 @@ +export const INPUT_TYPES: Record = { + x: 'number', + y: 'number', + width: 'number', + height: 'number', + isRigidBody: 'checkbox' +}; diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index aaa3b22..94fb746 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -35,10 +35,11 @@ export const SVG: React.FC = (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); }; }); diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 7422471..6d6adbe 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -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 = (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} diff --git a/src/Interfaces/HistoryState.ts b/src/Interfaces/HistoryState.ts index aa51638..da1d74b 100644 --- a/src/Interfaces/HistoryState.ts +++ b/src/Interfaces/HistoryState.ts @@ -1,6 +1,7 @@ import { IContainerModel } from './ContainerModel'; export interface HistoryState { + LastAction: string MainContainer: IContainerModel SelectedContainer: IContainerModel | null SelectedContainerId: string diff --git a/src/Interfaces/Properties.ts b/src/Interfaces/Properties.ts index ed185f2..ea5f54e 100644 --- a/src/Interfaces/Properties.ts +++ b/src/Interfaces/Properties.ts @@ -5,4 +5,5 @@ export default interface Properties extends React.CSSProperties { parentId: string | null x: number y: number + isRigidBody: boolean } diff --git a/src/Interfaces/SizePointer.ts b/src/Interfaces/SizePointer.ts new file mode 100644 index 0000000..9a80057 --- /dev/null +++ b/src/Interfaces/SizePointer.ts @@ -0,0 +1,4 @@ +export interface SizePointer { + x: number + width: number +} diff --git a/src/tests/resources/rigidbodytest.json b/src/tests/resources/rigidbodytest.json new file mode 100644 index 0000000..8430870 --- /dev/null +++ b/src/tests/resources/rigidbodytest.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/src/utils/default.ts b/src/utils/default.ts index 60371fc..552b8a0 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -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, diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts index de8b835..a33652e 100644 --- a/src/utils/saveload.ts +++ b/src/utils/saveload.ts @@ -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;