From ac56f8419687b14520bffb35da4b54330667e011 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 08:37:10 -0400 Subject: [PATCH 01/12] Add option for the properties form to only update on submit (#23) Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/23 --- src/Components/Editor/ContainerOperations.ts | 74 ++++++++++++++- src/Components/Editor/Editor.tsx | 10 +- .../ElementsSidebar/ElementsSidebar.test.tsx | 21 +++-- .../ElementsSidebar/ElementsSidebar.tsx | 7 +- src/Components/MainMenu/MainMenu.tsx | 9 +- src/Components/Properties/Properties.test.tsx | 21 +++-- src/Components/Properties/Properties.tsx | 93 ++++++++++++++----- src/Components/ToggleButton/ToggleButton.scss | 8 ++ src/Components/ToggleButton/ToggleButton.tsx | 52 +++++++++++ src/Components/UI/UI.tsx | 2 + src/index.scss | 10 ++ src/utils/default.ts | 2 +- 12 files changed, 256 insertions(+), 53 deletions(-) create mode 100644 src/Components/ToggleButton/ToggleButton.scss create mode 100644 src/Components/ToggleButton/ToggleButton.tsx diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index ea4db42..8ad34ec 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction } from 'react'; +import React, { Dispatch, SetStateAction } from 'react'; import { HistoryState } from '../../Interfaces/HistoryState'; import { Configuration } from '../../Interfaces/Configuration'; import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel'; @@ -60,7 +60,7 @@ export function DeleteContainer( } if (container === null || container === undefined) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + throw new Error('[DeleteContainer] Container model was not found among children of the main container!'); } if (container.parent != null) { @@ -266,6 +266,76 @@ export function OnPropertyChange( setHistoryCurrentStep(history.length); } +/** + * Handled the property change event in the properties form + * @param key Property name + * @param value New value of the property + * @returns void + */ +export function OnPropertiesSubmit( + event: React.SyntheticEvent, + refs: Array>, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + event.preventDefault(); + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { + throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); + } + + if (parent === null) { + const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer); + for (const ref of refs) { + const input = ref.current; + if (input instanceof HTMLInputElement) { + (selectedContainerClone.properties as any)[input.id] = 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 container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + for (const ref of refs) { + const input = ref.current; + if (input instanceof HTMLInputElement) { + (container.properties as any)[input.id] = input.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, + TypeCounters: Object.assign({}, current.TypeCounters) + }])); + setHistoryCurrentStep(history.length); +} + // TODO put this in a different file export function RecalculatePhysics(container: IContainerModel): IContainerModel { diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index ad27c22..b008607 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -4,7 +4,7 @@ import { Configuration } from '../../Interfaces/Configuration'; import { SVG } from '../SVG/SVG'; import { HistoryState } from '../../Interfaces/HistoryState'; import { UI } from '../UI/UI'; -import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations'; +import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer, OnPropertiesSubmit } from './ContainerOperations'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save'; import { onKeyDown } from './Shortcuts'; @@ -72,6 +72,14 @@ const Editor: React.FunctionComponent = (props) => { setHistory, setHistoryCurrentStep )} + OnPropertiesSubmit={(event, refs) => OnPropertiesSubmit( + event, + refs, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + )} AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer( type, configuration, diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index c1372b1..fac312b 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -25,6 +25,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={null} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -57,6 +58,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -70,12 +72,12 @@ describe.concurrent('Elements sidebar', () => { expect(screen.queryByText('y')).toBeDefined(); expect(screen.queryByText('width')).toBeDefined(); expect(screen.queryByText('height')).toBeDefined(); - const propertyId = container.querySelector('#property-id'); - const propertyParentId = container.querySelector('#property-parentId'); - const propertyX = container.querySelector('#property-x'); - const propertyY = container.querySelector('#property-y'); - const propertyWidth = container.querySelector('#property-width'); - const propertyHeight = container.querySelector('#property-height'); + const propertyId = container.querySelector('#id'); + const propertyParentId = container.querySelector('#parentId'); + const propertyX = container.querySelector('#x'); + const propertyY = container.querySelector('#y'); + const propertyWidth = container.querySelector('#width'); + const propertyHeight = container.querySelector('#height'); expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); expect(propertyParentId).toBeDefined(); expect((propertyParentId as HTMLInputElement).value).toBe(''); @@ -146,6 +148,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={() => {}} DeleteContainer={() => {}} AddContainer={() => {}} @@ -202,6 +205,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} AddContainer={() => {}} @@ -212,8 +216,8 @@ describe.concurrent('Elements sidebar', () => { expect(screen.getByText(/main/i)); const child1 = screen.getByText(/child-1/i); expect(child1); - const propertyId = container.querySelector('#property-id'); - const propertyParentId = container.querySelector('#property-parentId'); + const propertyId = container.querySelector('#id'); + const propertyParentId = container.querySelector('#parentId'); expect((propertyId as HTMLInputElement).value).toBe(MainContainer.properties.id.toString()); expect((propertyParentId as HTMLInputElement).value).toBe(''); @@ -225,6 +229,7 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} OnPropertyChange={() => {}} + OnPropertiesSubmit={() => {}} SelectContainer={selectContainer} DeleteContainer={() => {}} AddContainer={() => {}} diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 873161d..96be3fe 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -14,6 +14,7 @@ interface IElementsSidebarProps { isHistoryOpen: boolean SelectedContainer: IContainerModel | null OnPropertyChange: (key: string, value: string | number | boolean) => void + OnPropertiesSubmit: (event: React.FormEvent, refs: Array>) => void SelectContainer: (container: IContainerModel) => void DeleteContainer: (containerid: string) => void AddContainer: (index: number, type: string, parent: string) => void @@ -145,7 +146,11 @@ export const ElementsSidebar: React.FC = (props: IElement props.DeleteContainer(onClickContainerId); }} /> - + ); }; diff --git a/src/Components/MainMenu/MainMenu.tsx b/src/Components/MainMenu/MainMenu.tsx index 1d84f9a..9c8b27f 100644 --- a/src/Components/MainMenu/MainMenu.tsx +++ b/src/Components/MainMenu/MainMenu.tsx @@ -38,13 +38,8 @@ export const MainMenu: React.FC = (props) => { diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 9f0fe13..2afc364 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -8,6 +8,7 @@ describe.concurrent('Properties', () => { render( {}} + onSubmit={() => {}} />); expect(screen.queryByText('id')).toBeNull(); @@ -16,7 +17,7 @@ describe.concurrent('Properties', () => { expect(screen.queryByText('y')).toBeNull(); }); - it('Some properties', () => { + it('Some properties, change values with dynamic input', () => { const prop = { id: 'stuff', parentId: 'parentId', @@ -32,6 +33,7 @@ describe.concurrent('Properties', () => { const { container, rerender } = render( {}} />); expect(screen.queryByText('id')).toBeDefined(); @@ -39,10 +41,10 @@ describe.concurrent('Properties', () => { expect(screen.queryByText('x')).toBeDefined(); expect(screen.queryByText('y')).toBeDefined(); - let propertyId = container.querySelector('#property-id'); - let propertyParentId = container.querySelector('#property-parentId'); - let propertyX = container.querySelector('#property-x'); - let propertyY = container.querySelector('#property-y'); + let propertyId = container.querySelector('#id'); + let propertyParentId = container.querySelector('#parentId'); + let propertyX = container.querySelector('#x'); + let propertyY = container.querySelector('#y'); expect(propertyId).toBeDefined(); expect((propertyId as HTMLInputElement).value).toBe('stuff'); expect(propertyParentId).toBeDefined(); @@ -65,12 +67,13 @@ describe.concurrent('Properties', () => { rerender( {}} />); - propertyId = container.querySelector('#property-id'); - propertyParentId = container.querySelector('#property-parentId'); - propertyX = container.querySelector('#property-x'); - propertyY = container.querySelector('#property-y'); + propertyId = container.querySelector('#id'); + propertyParentId = container.querySelector('#parentId'); + propertyX = container.querySelector('#x'); + propertyY = container.querySelector('#y'); expect(propertyId).toBeDefined(); expect((propertyId as HTMLInputElement).value).toBe('stuffed'); expect(propertyParentId).toBeDefined(); diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index c1ac966..7477542 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -1,25 +1,50 @@ -import * as React from 'react'; +import React, { useState } from 'react'; import ContainerProperties from '../../Interfaces/Properties'; +import { ToggleButton } from '../ToggleButton/ToggleButton'; import { INPUT_TYPES } from './PropertiesInputTypes'; interface IPropertiesProps { properties?: ContainerProperties onChange: (key: string, value: string | number | boolean) => void + onSubmit: (event: React.FormEvent, refs: Array>) => void } export const Properties: React.FC = (props: IPropertiesProps) => { + const [isDynamicInput, setIsDynamicInput] = useState(true); + if (props.properties === undefined) { return
; } const groupInput: React.ReactNode[] = []; + const refs: Array> = []; Object .entries(props.properties) - .forEach((pair) => handleProperties(pair, groupInput, props.onChange)); + .forEach((pair) => handleProperties(pair, groupInput, refs, isDynamicInput, props.onChange)); + + const form = isDynamicInput + ?
+ { groupInput } +
+ :
props.onSubmit(event, refs)} + > + + { groupInput } +
+ ; return ( -
- { groupInput } +
+ setIsDynamicInput(!isDynamicInput)} + /> + { form }
); }; @@ -27,6 +52,8 @@ export const Properties: React.FC = (props: IPropertiesProps) const handleProperties = ( [key, value]: [string, string | number], groupInput: React.ReactNode[], + refs: Array>, + isDynamicInput: boolean, onChange: (key: string, value: string | number | boolean) => void ): void => { const id = `property-${key}`; @@ -42,31 +69,49 @@ const handleProperties = ( type = INPUT_TYPES[key]; } - const isDisabled = ['id', 'parentId'].includes(key); - /// + const ref: React.RefObject = React.useRef(null); + refs.push(ref); - groupInput.push( -
- - { - if (type === 'checkbox') { - onChange(key, event.target.checked); - return; - } - onChange(key, event.target.value); - }} - disabled={isDisabled} - /> + type={type} + id={key} + ref={ref} + value={value} + checked={checked} + onChange={(event) => { + if (type === 'checkbox') { + onChange(key, event.target.checked); + return; + } + onChange(key, event.target.value); + }} + disabled={isDisabled} + /> + : ; + + groupInput.push( +
+ + {input}
); }; diff --git a/src/Components/ToggleButton/ToggleButton.scss b/src/Components/ToggleButton/ToggleButton.scss new file mode 100644 index 0000000..0948f52 --- /dev/null +++ b/src/Components/ToggleButton/ToggleButton.scss @@ -0,0 +1,8 @@ +input:checked ~ .dot { + transform: translateX(100%); +} +input:checked ~ .line { + background-color: #3B82F6; +} + + diff --git a/src/Components/ToggleButton/ToggleButton.tsx b/src/Components/ToggleButton/ToggleButton.tsx new file mode 100644 index 0000000..198bf99 --- /dev/null +++ b/src/Components/ToggleButton/ToggleButton.tsx @@ -0,0 +1,52 @@ +import React, { FC } from 'react'; +import './ToggleButton.scss'; + +interface IToggleButtonProps { + id: string + text: string + type?: TOGGLE_TYPE + title: string + checked: boolean + onChange: React.ChangeEventHandler +} + +export enum TOGGLE_TYPE { + MATERIAL, + IOS +} + +export const ToggleButton: FC = (props) => { + const id = `toggle-${props.id}`; + const type = props.type ?? TOGGLE_TYPE.MATERIAL; + let classLine = 'line w-10 h-4 bg-gray-400 rounded-full shadow-inner'; + let classDot = 'dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition'; + if (type === TOGGLE_TYPE.IOS) { + classLine = 'line block bg-gray-600 w-14 h-8 rounded-full'; + classDot = 'dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition'; + } + + return ( +
+
+ +
+
+ ); +}; diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 6d6adbe..5c7012e 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -17,6 +17,7 @@ interface IUIProps { SelectContainer: (container: ContainerModel) => void DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string | number | boolean) => void + OnPropertiesSubmit: (event: React.FormEvent, refs: Array>) => void AddContainerToSelectedContainer: (type: string) => void AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void @@ -59,6 +60,7 @@ export const UI: React.FunctionComponent = (props: IUIProps) => { isOpen={isElementsSidebarOpen} isHistoryOpen={isHistoryOpen} OnPropertyChange={props.OnPropertyChange} + OnPropertiesSubmit={props.OnPropertiesSubmit} SelectContainer={props.SelectContainer} DeleteContainer={props.DeleteContainer} AddContainer={props.AddContainer} diff --git a/src/index.scss b/src/index.scss index 1bc3361..f653990 100644 --- a/src/index.scss +++ b/src/index.scss @@ -23,6 +23,16 @@ @apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg } + .normal-btn { + @apply text-sm + py-2 px-4 + rounded-full border-0 + font-semibold + transition-all + bg-blue-100 text-blue-700 + hover:bg-blue-200 + } + .floating-btn { @apply h-full w-full text-white align-middle items-center justify-center } diff --git a/src/utils/default.ts b/src/utils/default.ts index 552b8a0..a117ef0 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -30,9 +30,9 @@ export const DEFAULT_MAINCONTAINER_PROPS: Properties = { parentId: 'null', x: 0, y: 0, - isRigidBody: false, width: DEFAULT_CONFIG.MainContainer.Width, height: DEFAULT_CONFIG.MainContainer.Height, + isRigidBody: false, fillOpacity: 0, stroke: 'black' }; From d11dfec22b9af05b905485cef620aa22bca1eb7f Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 09:10:06 -0400 Subject: [PATCH 02/12] Fix misuse of Hooks with useRef (#24) Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/24 --- .eslintrc.cjs | 3 ++ package-lock.json | 49 ++++++++++++++++--- package.json | 1 + pnpm-lock.yaml | 11 +++++ src/Components/Editor/ContainerOperations.ts | 15 +++--- src/Components/Editor/Editor.tsx | 4 +- .../ElementsSidebar/ElementsSidebar.tsx | 4 +- src/Components/Properties/Properties.tsx | 13 ++--- src/Components/UI/UI.tsx | 3 +- 9 files changed, 76 insertions(+), 27 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 33736e7..13ac012 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -19,6 +19,7 @@ module.exports = { plugins: [ 'only-warn', 'react', + 'react-hooks', '@typescript-eslint' ], rules: { @@ -29,5 +30,7 @@ module.exports = { '@typescript-eslint/semi': ['warn', 'always'], 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': 'error', + 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks + 'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies } }; diff --git a/package-lock.json b/package-lock.json index 285432c..8573881 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "react-svg-pan-zoom": "^3.11.0" }, "devDependencies": { + "@testing-library/dom": "^8.16.1", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^14.4.1", @@ -31,8 +32,10 @@ "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", + "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^20.0.0", "postcss": "^8.4.14", "sass": "^1.54.0", @@ -745,9 +748,9 @@ "dev": true }, "node_modules/@testing-library/dom": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz", - "integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.17.1.tgz", + "integrity": "sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", @@ -2879,6 +2882,15 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-only-warn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.3.tgz", + "integrity": "sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/eslint-plugin-promise": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", @@ -2919,6 +2931,18 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -6708,9 +6732,9 @@ "dev": true }, "@testing-library/dom": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.16.0.tgz", - "integrity": "sha512-uxF4zmnLHHDlmW4l+0WDjcgLVwCvH+OVLpD8Dfp+Bjfz85prwxWGbwXgJdLtkgjD0qfOzkJF9SmA6YZPsMYX4w==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.17.1.tgz", + "integrity": "sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", @@ -8254,6 +8278,12 @@ "semver": "^7.3.7" } }, + "eslint-plugin-only-warn": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-only-warn/-/eslint-plugin-only-warn-1.0.3.tgz", + "integrity": "sha512-XQOX/TfLoLw6h8ky51d29uUjXRTQHqBGXPylDEmy5fe/w7LIOnp8MA24b1OSMEn9BQoKow1q3g1kLe5/9uBTvw==", + "dev": true + }, "eslint-plugin-promise": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz", @@ -8311,6 +8341,13 @@ } } }, + "eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "dev": true, + "requires": {} + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/package.json b/package.json index 1c45135..0f0d27f 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-plugin-only-warn": "^1.0.3", "eslint-plugin-promise": "^6.0.0", "eslint-plugin-react": "^7.30.1", + "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^20.0.0", "postcss": "^8.4.14", "sass": "^1.54.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10ead3b..469528f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,7 @@ specifiers: eslint-plugin-only-warn: ^1.0.3 eslint-plugin-promise: ^6.0.0 eslint-plugin-react: ^7.30.1 + eslint-plugin-react-hooks: ^4.6.0 framer-motion: ^6.5.1 jsdom: ^20.0.0 postcss: ^8.4.14 @@ -62,6 +63,7 @@ devDependencies: 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 + eslint-plugin-react-hooks: 4.6.0_eslint@8.21.0 jsdom: 20.0.0 postcss: 8.4.16 sass: 1.54.3 @@ -1740,6 +1742,15 @@ packages: eslint: 8.21.0 dev: true + /eslint-plugin-react-hooks/4.6.0_eslint@8.21.0: + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.21.0 + dev: true + /eslint-plugin-react/7.30.1_eslint@8.21.0: resolution: {integrity: sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==} engines: {node: '>=4'} diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index 8ad34ec..ec5a28a 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -5,6 +5,7 @@ import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; import { SizePointer } from '../../Interfaces/SizePointer'; +import Properties from '../../Interfaces/Properties'; /** * Select a container @@ -274,7 +275,7 @@ export function OnPropertyChange( */ export function OnPropertiesSubmit( event: React.SyntheticEvent, - refs: Array>, + properties: Properties, fullHistory: HistoryState[], historyCurrentStep: number, setHistory: Dispatch>, @@ -291,10 +292,10 @@ export function OnPropertiesSubmit( if (parent === null) { const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer); - for (const ref of refs) { - const input = ref.current; + for (const property in properties) { + const input = (event.target as HTMLFormElement).querySelector(`#${property}`); if (input instanceof HTMLInputElement) { - (selectedContainerClone.properties as any)[input.id] = input.value; + (selectedContainerClone.properties as any)[property] = input.value; } } setHistory(history.concat([{ @@ -315,10 +316,10 @@ export function OnPropertiesSubmit( throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); } - for (const ref of refs) { - const input = ref.current; + for (const property in properties) { + const input = (event.target as HTMLFormElement).querySelector(`#${property}`); if (input instanceof HTMLInputElement) { - (container.properties as any)[input.id] = input.value; + (container.properties as any)[property] = input.value; } } diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index b008607..f07d5e8 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -72,9 +72,9 @@ const Editor: React.FunctionComponent = (props) => { setHistory, setHistoryCurrentStep )} - OnPropertiesSubmit={(event, refs) => OnPropertiesSubmit( + OnPropertiesSubmit={(event, properties) => OnPropertiesSubmit( event, - refs, + properties, history, historyCurrentStep, setHistory, diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 96be3fe..c0a89c6 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; +import ContainerProperties from '../../Interfaces/Properties'; import { IContainerModel } from '../../Interfaces/ContainerModel'; import { getDepth, MakeIterator } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; @@ -8,13 +9,14 @@ import { MenuItem } from '../Menu/MenuItem'; import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers'; import { Point } from '../../Interfaces/Point'; + interface IElementsSidebarProps { MainContainer: IContainerModel isOpen: boolean isHistoryOpen: boolean SelectedContainer: IContainerModel | null OnPropertyChange: (key: string, value: string | number | boolean) => void - OnPropertiesSubmit: (event: React.FormEvent, refs: Array>) => void + OnPropertiesSubmit: (event: React.FormEvent, properties: ContainerProperties) => void SelectContainer: (container: IContainerModel) => void DeleteContainer: (containerid: string) => void AddContainer: (index: number, type: string, parent: string) => void diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 7477542..3720188 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -6,7 +6,7 @@ import { INPUT_TYPES } from './PropertiesInputTypes'; interface IPropertiesProps { properties?: ContainerProperties onChange: (key: string, value: string | number | boolean) => void - onSubmit: (event: React.FormEvent, refs: Array>) => void + onSubmit: (event: React.FormEvent, properties: ContainerProperties) => void } export const Properties: React.FC = (props: IPropertiesProps) => { @@ -17,10 +17,9 @@ export const Properties: React.FC = (props: IPropertiesProps) } const groupInput: React.ReactNode[] = []; - const refs: Array> = []; Object .entries(props.properties) - .forEach((pair) => handleProperties(pair, groupInput, refs, isDynamicInput, props.onChange)); + .forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange)); const form = isDynamicInput ?
@@ -28,7 +27,7 @@ export const Properties: React.FC = (props: IPropertiesProps)
:
props.onSubmit(event, refs)} + onSubmit={(event) => props.onSubmit(event, props.properties as ContainerProperties)} > { groupInput } @@ -52,7 +51,6 @@ export const Properties: React.FC = (props: IPropertiesProps) const handleProperties = ( [key, value]: [string, string | number], groupInput: React.ReactNode[], - refs: Array>, isDynamicInput: boolean, onChange: (key: string, value: string | number | boolean) => void ): void => { @@ -69,9 +67,6 @@ const handleProperties = ( type = INPUT_TYPES[key]; } - const ref: React.RefObject = React.useRef(null); - refs.push(ref); - const isDisabled = ['id', 'parentId'].includes(key); const input = isDynamicInput ? { @@ -102,7 +96,6 @@ const handleProperties = ( ' type={type} id={key} - ref={ref} defaultValue={value} defaultChecked={checked} disabled={isDisabled} diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 5c7012e..ffe9fb3 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -8,6 +8,7 @@ import { HistoryState } from '../../Interfaces/HistoryState'; import { PhotographIcon, UploadIcon } from '@heroicons/react/outline'; import { FloatingButton } from '../FloatingButton/FloatingButton'; import { Bar } from '../Bar/Bar'; +import Properties from '../../Interfaces/Properties'; interface IUIProps { current: HistoryState @@ -17,7 +18,7 @@ interface IUIProps { SelectContainer: (container: ContainerModel) => void DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string | number | boolean) => void - OnPropertiesSubmit: (event: React.FormEvent, refs: Array>) => void + OnPropertiesSubmit: (event: React.FormEvent, properties: Properties) => void AddContainerToSelectedContainer: (type: string) => void AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void From faa058e57de1b9417d2e5f6b7552325ed529d4a1 Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 11 Aug 2022 11:48:31 -0400 Subject: [PATCH 03/12] Implement new features for svg components + improve form properties (#25) - Make Dimension an actual svg line - Implement XPositionReference - Select the container above after delete - Remove DimensionLayer - Improve form properties Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/25 --- src/Components/Editor/ContainerOperations.ts | 26 +++++--- src/Components/Editor/Save.ts | 2 +- .../ElementsSidebar/ElementsSidebar.tsx | 3 +- src/Components/Properties/Properties.tsx | 44 +++++++------ src/Components/SVG/Elements/Container.tsx | 23 ++++++- src/Components/SVG/Elements/Dimension.tsx | 59 +++++++++++++---- .../SVG/Elements/DimensionLayer.tsx | 66 ------------------- src/Interfaces/AvailableContainer.ts | 2 + src/Interfaces/Properties.ts | 2 + src/utils/default.ts | 2 + test-server/http.js | 3 - 11 files changed, 119 insertions(+), 113 deletions(-) delete mode 100644 src/Components/SVG/Elements/DimensionLayer.tsx diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index ec5a28a..cbc0a9c 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -55,7 +55,9 @@ export function DeleteContainer( throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); } - if (container === mainContainerClone) { + if (container === mainContainerClone || + container.parent === undefined || + container.parent === null) { // TODO: Implement alert throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!'); } @@ -64,18 +66,25 @@ export function DeleteContainer( throw new Error('[DeleteContainer] Container model was not found among children of the main container!'); } - if (container.parent != null) { - const index = container.parent.children.indexOf(container); - if (index > -1) { - container.parent.children.splice(index, 1); - } + const index = container.parent.children.indexOf(container); + if (index > -1) { + container.parent.children.splice(index, 1); + } else { + throw new Error('[DeleteContainer] Could not find container among parent\'s children'); } + // Select the previous container + // or select the one above + const SelectedContainer = findContainerById(mainContainerClone, current.SelectedContainerId) ?? + container.parent.children.at(index - 1) ?? + container.parent; + const SelectedContainerId = SelectedContainer.properties.id; + setHistory(history.concat([{ LastAction: `Delete container ${containerId}`, MainContainer: mainContainerClone, - SelectedContainer: null, - SelectedContainerId: '', + SelectedContainer, + SelectedContainerId, TypeCounters: Object.assign({}, current.TypeCounters) }])); setHistoryCurrentStep(history.length); @@ -182,6 +191,7 @@ export function AddContainer( width: properties?.Width, height: parentClone.properties.height, isRigidBody: false, + XPositionReference: properties.XPositionReference, ...properties.Style }, [], diff --git a/src/Components/Editor/Save.ts b/src/Components/Editor/Save.ts index 91cee2f..3aa201a 100644 --- a/src/Components/Editor/Save.ts +++ b/src/Components/Editor/Save.ts @@ -1,4 +1,4 @@ -import { HistoryState } from "../../Interfaces/HistoryState"; +import { HistoryState } from '../../Interfaces/HistoryState'; import { Configuration } from '../../Interfaces/Configuration'; import { getCircularReplacer } from '../../utils/saveload'; import { ID } from '../SVG/SVG'; diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index c0a89c6..e9d5e59 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -9,7 +9,6 @@ import { MenuItem } from '../Menu/MenuItem'; import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers'; import { Point } from '../../Interfaces/Point'; - interface IElementsSidebarProps { MainContainer: IContainerModel isOpen: boolean @@ -108,7 +107,7 @@ export const ElementsSidebar: React.FC = (props: IElement onLeftClick ); }; - }, []); + }); // Render let isOpenClasses = '-right-64'; diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 3720188..49f5028 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -22,15 +22,17 @@ export const Properties: React.FC = (props: IPropertiesProps) .forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange)); const form = isDynamicInput - ?
+ ?
{ groupInput }
: props.onSubmit(event, props.properties as ContainerProperties)} > - - { groupInput } + +
+ { groupInput } +
; @@ -67,16 +69,19 @@ const handleProperties = ( type = INPUT_TYPES[key]; } + const className = ` + w-full + text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2 + bg-white border-2 border-white rounded-lg placeholder-gray-800 + focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 + disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`; const isDisabled = ['id', 'parentId'].includes(key); const input = isDynamicInput ? { @@ -89,22 +94,23 @@ const handleProperties = ( disabled={isDisabled} /> : ; groupInput.push( -
- - {input} -
+ ); + groupInput.push(input); }; diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index 02dea6f..25a45c9 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { XPositionReference } from '../../../Enums/XPositionReference'; import { IContainerModel } from '../../../Interfaces/ContainerModel'; import { getDepth } from '../../../utils/itertools'; import { Dimension } from './Dimension'; @@ -17,7 +18,14 @@ export const Container: React.FC = (props: IContainerProps) => const containersElements = props.model.children.map(child => ); const xText = Number(props.model.properties.width) / 2; const yText = Number(props.model.properties.height) / 2; - const transform = `translate(${Number(props.model.properties.x)}, ${Number(props.model.properties.y)})`; + + const [transformedX, transformedY] = transformPosition( + Number(props.model.properties.x), + Number(props.model.properties.y), + Number(props.model.properties.width), + props.model.properties.XPositionReference + ); + const transform = `translate(${transformedX}, ${transformedY})`; // g style const defaultStyle: React.CSSProperties = { @@ -54,7 +62,8 @@ export const Container: React.FC = (props: IContainerProps) => id={id} xStart={xStart} xEnd={xEnd} - y={y} + yStart={y} + yEnd={y} strokeWidth={strokeWidth} text={text} /> @@ -74,3 +83,13 @@ export const Container: React.FC = (props: IContainerProps) => ); }; + +function transformPosition(x: number, y: number, width: number, xPositionReference = XPositionReference.Left): [number, number] { + let transformedX = x; + if (xPositionReference === XPositionReference.Center) { + transformedX -= width / 2; + } else if (xPositionReference === XPositionReference.Right) { + transformedX -= width; + } + return [transformedX, y]; +} diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index ec51873..08f0a22 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -3,45 +3,80 @@ import * as React from 'react'; interface IDimensionProps { id: string xStart: number + yStart: number xEnd: number - y: number + yEnd: number text: string strokeWidth: number } +/** + * 2D Parametric function. Returns a new coordinate from the origin coordinate + * See for more details https://en.wikipedia.org/wiki/Parametric_equation. + * TL;DR a parametric function is a function with a parameter + * @param x0 Origin coordinate + * @param t The parameter + * @param vx Transform vector + * @returns Returns a new coordinate from the origin coordinate + */ +const applyParametric = (x0: number, t: number, vx: number): number => x0 + t * vx; + export const Dimension: React.FC = (props: IDimensionProps) => { const style: React.CSSProperties = { stroke: 'black' }; + + /// We need to find the points of the notches + // Get the vector of the line + const [deltaX, deltaY] = [(props.xEnd - props.xStart), (props.yEnd - props.yStart)]; + + // Get the unit vector + const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + const [unitX, unitY] = [deltaX / norm, deltaY / norm]; + + // Get the perpandicular vector + const [perpVecX, perpVecY] = [unitY, -unitX]; + + // Use the parametric function to get the coordinates (x = x0 + t * v.x) + const startTopX = applyParametric(props.xStart, 4, perpVecX); + const startTopY = applyParametric(props.yStart, 4, perpVecY); + const startBottomX = applyParametric(props.xStart, -4, perpVecX); + const startBottomY = applyParametric(props.yStart, -4, perpVecY); + + const endTopX = applyParametric(props.xEnd, 4, perpVecX); + const endTopY = applyParametric(props.yEnd, 4, perpVecY); + const endBottomX = applyParametric(props.xEnd, -4, perpVecX); + const endBottomY = applyParametric(props.yEnd, -4, perpVecY); + return ( {props.text} diff --git a/src/Components/SVG/Elements/DimensionLayer.tsx b/src/Components/SVG/Elements/DimensionLayer.tsx deleted file mode 100644 index 85f1a43..0000000 --- a/src/Components/SVG/Elements/DimensionLayer.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import * as React from 'react'; -import { ContainerModel } from '../../../Interfaces/ContainerModel'; -import { getDepth, MakeIterator } from '../../../utils/itertools'; -import { Dimension } from './Dimension'; - -interface IDimensionLayerProps { - isHidden: boolean - roots: ContainerModel | ContainerModel[] | null -} - -const GAP: number = 50; - -const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => { - const it = MakeIterator(root); - const dimensions: React.ReactNode[] = []; - for (const container of it) { - // WARN: this might be dangerous later when using other units/rules - const width = Number(container.properties.width); - - const id = `dim-${container.properties.id}`; - const xStart: number = container.properties.x; - const xEnd = xStart + width; - const y = -(GAP * (getDepth(container) + 1)); - const strokeWidth = 1; - const text = width.toString(); - dimensions.push( - - ); - } - return dimensions; -}; - -/** - * A layer containing all dimension - * - * @deprecated In order to avoid adding complexity - * with computing the position in a group hierarchy, - * use Dimension directly inside the Container, - * Currently it is glitched as - * it does not take parents into account, - * and will not work correctly - * @param props - * @returns - */ -export const DimensionLayer: React.FC = (props: IDimensionLayerProps) => { - let dimensions: React.ReactNode[] = []; - if (Array.isArray(props.roots)) { - props.roots.forEach(child => { - dimensions.concat(getDimensionsNodes(child)); - }); - } else if (props.roots !== null) { - dimensions = getDimensionsNodes(props.roots); - } - return ( - - { dimensions } - - ); -}; diff --git a/src/Interfaces/AvailableContainer.ts b/src/Interfaces/AvailableContainer.ts index d7bb22f..b328b0a 100644 --- a/src/Interfaces/AvailableContainer.ts +++ b/src/Interfaces/AvailableContainer.ts @@ -1,9 +1,11 @@ import React from 'react'; +import { XPositionReference } from '../Enums/XPositionReference'; /** Model of available container used in application configuration */ export interface AvailableContainer { Type: string Width: number Height: number + XPositionReference?: XPositionReference Style: React.CSSProperties } diff --git a/src/Interfaces/Properties.ts b/src/Interfaces/Properties.ts index ea5f54e..6f2f32e 100644 --- a/src/Interfaces/Properties.ts +++ b/src/Interfaces/Properties.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { XPositionReference } from '../Enums/XPositionReference'; export default interface Properties extends React.CSSProperties { id: string @@ -6,4 +7,5 @@ export default interface Properties extends React.CSSProperties { x: number y: number isRigidBody: boolean + XPositionReference?: XPositionReference } diff --git a/src/utils/default.ts b/src/utils/default.ts index a117ef0..51c1d7a 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -36,3 +36,5 @@ export const DEFAULT_MAINCONTAINER_PROPS: Properties = { fillOpacity: 0, stroke: 'black' }; + +export const NOTCHES_LENGTH = 4; diff --git a/test-server/http.js b/test-server/http.js index c4f3238..69088c9 100644 --- a/test-server/http.js +++ b/test-server/http.js @@ -76,9 +76,6 @@ const GetSVGLayoutConfiguration = () => { fillOpacity: 0, borderWidth: 2, stroke: 'blue', - transform: 'translateX(-50%)', - transformOrigin: 'center', - transformBox: 'fill-box' } } ], From 61b72f6a35ed2bdb73d9ebe670e3c3033cfb5123 Mon Sep 17 00:00:00 2001 From: Hydroxycarbamide Date: Thu, 11 Aug 2022 23:35:40 +0200 Subject: [PATCH 04/12] Separate properties operations and rigid body behaviors in different modules + Doc --- src/Components/Editor/ContainerOperations.ts | 365 ++---------------- src/Components/Editor/Editor.tsx | 3 +- src/Components/Editor/PropertiesOperations.ts | 136 +++++++ src/Components/Editor/RigidBodyBehaviors.ts | 280 ++++++++++++++ src/Interfaces/SizePointer.ts | 5 + 5 files changed, 449 insertions(+), 340 deletions(-) create mode 100644 src/Components/Editor/PropertiesOperations.ts create mode 100644 src/Components/Editor/RigidBodyBehaviors.ts diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index cbc0a9c..fde4a72 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -1,11 +1,9 @@ -import React, { Dispatch, SetStateAction } from 'react'; +import { Dispatch, SetStateAction } from 'react'; 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'; -import Properties from '../../Interfaces/Properties'; /** * Select a container @@ -38,6 +36,14 @@ export function SelectContainer( setHistoryCurrentStep(history.length); } +/** + * Delete a container + * @param containerId containerId of the container to delete + * @param fullHistory History of the editor + * @param historyCurrentStep Current step + * @param setHistory State setter for History + * @param setHistoryCurrentStep State setter for current step + */ export function DeleteContainer( containerId: string, fullHistory: HistoryState[], @@ -93,6 +99,11 @@ export function DeleteContainer( /** * Add a new container to a selected container * @param type The type of container + * @param configuration Configuration of the App + * @param fullHistory History of the editor + * @param historyCurrentStep Current step + * @param setHistory State setter for History + * @param setHistoryCurrentStep State setter for current step * @returns void */ export function AddContainerToSelectedContainer( @@ -124,6 +135,18 @@ export function AddContainerToSelectedContainer( ); } +/** + * Create and add a new container at `index` in children of parent of `parentId` + * @param index Index where to insert to the new container + * @param type Type of container + * @param parentId Parent in which to insert the new container + * @param configuration Configuration of the app + * @param fullHistory History of the editor + * @param historyCurrentStep Current step + * @param setHistory State setter of History + * @param setHistoryCurrentStep State setter of the current step + * @returns void + */ export function AddContainer( index: number, type: string, @@ -217,339 +240,3 @@ export function AddContainer( }])); setHistoryCurrentStep(history.length); } - -/** - * Handled the property change event in the properties form - * @param key Property name - * @param value New value of the property - * @returns void - */ -export function OnPropertyChange( - key: string, - value: string | number | boolean, - fullHistory: HistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { - const history = getCurrentHistory(fullHistory, historyCurrentStep); - const current = history[history.length - 1]; - - if (current.SelectedContainer === null || - current.SelectedContainer === undefined) { - 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 container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); - - if (container === null || container === undefined) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); - } - - (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, - TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); -} - -/** - * Handled the property change event in the properties form - * @param key Property name - * @param value New value of the property - * @returns void - */ -export function OnPropertiesSubmit( - event: React.SyntheticEvent, - properties: Properties, - fullHistory: HistoryState[], - historyCurrentStep: number, - setHistory: Dispatch>, - setHistoryCurrentStep: Dispatch> -): void { - event.preventDefault(); - const history = getCurrentHistory(fullHistory, historyCurrentStep); - const current = history[history.length - 1]; - - if (current.SelectedContainer === null || - current.SelectedContainer === undefined) { - 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 container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); - - if (container === null || container === undefined) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); - } - - for (const property in properties) { - const input = (event.target as HTMLFormElement).querySelector(`#${property}`); - if (input instanceof HTMLInputElement) { - (container.properties as any)[property] = input.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, - 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 f07d5e8..09627e4 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -4,9 +4,10 @@ import { Configuration } from '../../Interfaces/Configuration'; import { SVG } from '../SVG/SVG'; import { HistoryState } from '../../Interfaces/HistoryState'; import { UI } from '../UI/UI'; -import { SelectContainer, DeleteContainer, OnPropertyChange, AddContainerToSelectedContainer, AddContainer, OnPropertiesSubmit } from './ContainerOperations'; +import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save'; import { onKeyDown } from './Shortcuts'; +import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations'; interface IEditorProps { configuration: Configuration diff --git a/src/Components/Editor/PropertiesOperations.ts b/src/Components/Editor/PropertiesOperations.ts new file mode 100644 index 0000000..f97e1cb --- /dev/null +++ b/src/Components/Editor/PropertiesOperations.ts @@ -0,0 +1,136 @@ +import { Dispatch, SetStateAction } from 'react'; +import { IContainerModel, ContainerModel } from '../../Interfaces/ContainerModel'; +import { HistoryState } from '../../Interfaces/HistoryState'; +import Properties from '../../Interfaces/Properties'; +import { findContainerById } from '../../utils/itertools'; +import { getCurrentHistory } from './Editor'; +import { RecalculatePhysics } from './RigidBodyBehaviors'; + +/** + * Handled the property change event in the properties form + * @param key Property name + * @param value New value of the property + * @returns void + */ +export function OnPropertyChange( + key: string, + value: string | number | boolean, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { + 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 container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + (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, + TypeCounters: Object.assign({}, current.TypeCounters) + }])); + setHistoryCurrentStep(history.length); +} + +/** + * Handled the property change event in the properties form + * @param key Property name + * @param properties Properties of the selected container + * @returns void + */ +export function OnPropertiesSubmit( + event: React.SyntheticEvent, + properties: Properties, + fullHistory: HistoryState[], + historyCurrentStep: number, + setHistory: Dispatch>, + setHistoryCurrentStep: Dispatch> +): void { + event.preventDefault(); + const history = getCurrentHistory(fullHistory, historyCurrentStep); + const current = history[history.length - 1]; + + if (current.SelectedContainer === null || + current.SelectedContainer === undefined) { + 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 container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + for (const property in properties) { + const input = (event.target as HTMLFormElement).querySelector(`#${property}`); + if (input instanceof HTMLInputElement) { + (container.properties as any)[property] = input.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, + TypeCounters: Object.assign({}, current.TypeCounters) + }])); + setHistoryCurrentStep(history.length); +} diff --git a/src/Components/Editor/RigidBodyBehaviors.ts b/src/Components/Editor/RigidBodyBehaviors.ts new file mode 100644 index 0000000..95ce545 --- /dev/null +++ b/src/Components/Editor/RigidBodyBehaviors.ts @@ -0,0 +1,280 @@ +import { IContainerModel } from '../../Interfaces/ContainerModel'; +import { SizePointer } from '../../Interfaces/SizePointer'; + +/** + * "Transform the container into a rigid body" + * 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 + * @param container Container to apply its rigid body properties + * @returns A rigid body container + */ +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 + * Mutates and returns the container + * @param container + * @returns Updated container + */ +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); +} + +/** + * Limit a container inside a rectangle + * Mutates and returns the container + * @param container A container + * @param x x of top left of the rectangle + * @param y y of top left of the rectangle + * @param width width of the rectangle + * @param height height of the rectangle + * @returns Updated container + */ +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; +} + +/** + * Constraint the container inside unallocated width/space of the parent container + * If there is no unalloacted width/space, an error will be thrown + * Mutates and returns the container + * @param container + * @returns Updated container + */ +function constraintBodyInsideUnallocatedWidth( + container: IContainerModel +): IContainerModel { + if (container.parent === null) { + return container; + } + + // Get the available spaces of the parent + const availableWidths = getAvailableWidths(container.parent, container); + const containerX = Number(container.properties.x); + + // 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 + 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.' + ); + } + + // Check if the container actually fit inside + // It will usually fit if it was alrady fitting + const availableWidthFound = availableWidths.find((width) => + isFitting(container, width) + ); + + if (availableWidthFound === undefined) { + // Otherwise, it is possible that it does not fit + // There is two way to reach this part of the code + // 1) Enable 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) + ); +} + +/** + * 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 of an hard drive + * (except the fact that disk space is divided by block). + * @param container Container where to find an available width + * @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded) + * @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[] { + // Initialize the first size pointer + // which takes full width of the available space + const x = 0; + const width = Number(container.properties.width); + let unallocatedSpaces: SizePointer[] = [{ x, width }]; + + // We will only uses containers that also have the rigid bodies + // as out-of-bound or enormouse containers should be ignored + const rigidBodies = container.children.filter( + (child) => child.properties.isRigidBody + ); + + for (const child of rigidBodies) { + // Ignore the exception + if (child === exception) { + continue; + } + + // get the space of the child that is inside the parent + let newUnallocatedSpace: SizePointer[] = []; + + // We will iterate on a mutable variable in order to divide it + for (const unallocatedSpace of unallocatedSpaces) { + // In order to find unallocated space, + // We need to calculate the overlap between the two containers + // We only works with widths meaning in 1D (with lines) + const newUnallocatedWidths = getAvailableWidthsTwoLines( + unallocatedSpace.x, + unallocatedSpace.x + unallocatedSpace.width, + child.properties.x, + child.properties.x + Number(child.properties.width) + ); + + // Concat the new list of SizePointer pointing to availables spaces + newUnallocatedSpace = newUnallocatedSpace.concat(newUnallocatedWidths); + } + // Finally update the availables spaces found, loop again with it + 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 + } + ]; +} + +/** + * Check if a container can fit inside a size space + * @param container Container to check + * @param sizePointer Size space to check + * @returns + */ +const isFitting = ( + container: IContainerModel, + sizePointer: SizePointer +): boolean => Number(container.properties.width) <= sizePointer.width; diff --git a/src/Interfaces/SizePointer.ts b/src/Interfaces/SizePointer.ts index 9a80057..e5ec599 100644 --- a/src/Interfaces/SizePointer.ts +++ b/src/Interfaces/SizePointer.ts @@ -1,3 +1,8 @@ +/** + * A SizePointer is a pointer in a 1 dimensional array of width/space + * x being the address where the pointer is pointing + * width being the overall (un)allocated space affected to the address + */ export interface SizePointer { x: number width: number From 8e41fdad46724f86b02411a98de6aff16a74fc25 Mon Sep 17 00:00:00 2001 From: Hydroxycarbamide Date: Thu, 11 Aug 2022 23:41:18 +0200 Subject: [PATCH 05/12] App.scss: Move public style to public css file --- index.html | 1 + public/style.css | 6 ++++++ src/Components/App/App.scss | 6 ------ 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 public/style.css diff --git a/index.html b/index.html index e0d1c84..d9f5a08 100644 --- a/index.html +++ b/index.html @@ -3,6 +3,7 @@ + Vite + React + TS diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..a732b5f --- /dev/null +++ b/public/style.css @@ -0,0 +1,6 @@ +html, +body, +#root { + width: 100%; + height: 100%; +} diff --git a/src/Components/App/App.scss b/src/Components/App/App.scss index 2cb61ee..e69de29 100644 --- a/src/Components/App/App.scss +++ b/src/Components/App/App.scss @@ -1,6 +0,0 @@ -html, -body, -#root { - width: 100%; - height: 100%; -} \ No newline at end of file From 6c601429b9bb1479aa658e4ac074f5d600e25a16 Mon Sep 17 00:00:00 2001 From: Hydroxycarbamide Date: Thu, 11 Aug 2022 23:45:16 +0200 Subject: [PATCH 06/12] Import NOTCHES_LENGTH for the default value of the notches --- src/Components/SVG/Elements/Dimension.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index 08f0a22..c5f6f86 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { NOTCHES_LENGTH } from '../../../utils/default'; interface IDimensionProps { id: string @@ -38,15 +39,15 @@ export const Dimension: React.FC = (props: IDimensionProps) => const [perpVecX, perpVecY] = [unitY, -unitX]; // Use the parametric function to get the coordinates (x = x0 + t * v.x) - const startTopX = applyParametric(props.xStart, 4, perpVecX); - const startTopY = applyParametric(props.yStart, 4, perpVecY); - const startBottomX = applyParametric(props.xStart, -4, perpVecX); - const startBottomY = applyParametric(props.yStart, -4, perpVecY); + const startTopX = applyParametric(props.xStart, NOTCHES_LENGTH, perpVecX); + const startTopY = applyParametric(props.yStart, NOTCHES_LENGTH, perpVecY); + const startBottomX = applyParametric(props.xStart, -NOTCHES_LENGTH, perpVecX); + const startBottomY = applyParametric(props.yStart, -NOTCHES_LENGTH, perpVecY); - const endTopX = applyParametric(props.xEnd, 4, perpVecX); - const endTopY = applyParametric(props.yEnd, 4, perpVecY); - const endBottomX = applyParametric(props.xEnd, -4, perpVecX); - const endBottomY = applyParametric(props.yEnd, -4, perpVecY); + const endTopX = applyParametric(props.xEnd, NOTCHES_LENGTH, perpVecX); + const endTopY = applyParametric(props.yEnd, NOTCHES_LENGTH, perpVecY); + const endBottomX = applyParametric(props.xEnd, -NOTCHES_LENGTH, perpVecX); + const endBottomY = applyParametric(props.yEnd, -NOTCHES_LENGTH, perpVecY); return ( From c81a6fe44b8fc323b41ca9c210ad95bc232d0a56 Mon Sep 17 00:00:00 2001 From: Siklos Date: Fri, 12 Aug 2022 06:36:14 -0400 Subject: [PATCH 07/12] Implement events for external use + Rename interfaces with a I prefix + add some documentation (#26) Implement events for external use Rename interfaces with a I prefix Add some documentation Co-authored-by: Eric NGUYEN Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/26 --- .gitattributes | 1 + docs/ComponentStructure.drawio | 3 + public/Interfaces.d.ts | 65 +++++++++++++++++++ src/Components/API/api.ts | 6 +- src/Components/App/App.tsx | 5 +- src/Components/App/Load.ts | 2 +- src/Components/App/MenuActions.ts | 8 +-- src/Components/Editor/ContainerOperations.ts | 26 ++++---- src/Components/Editor/Editor.tsx | 38 ++++++----- src/Components/Editor/PropertiesOperations.ts | 16 ++--- src/Components/Editor/RigidBodyBehaviors.ts | 16 ++--- src/Components/Editor/Save.ts | 10 +-- src/Components/Editor/Shortcuts.ts | 4 +- .../ElementsSidebar/ElementsSidebar.test.tsx | 2 +- .../ElementsSidebar/ElementsSidebar.tsx | 8 +-- .../ElementsSidebar/MouseEventHandlers.ts | 8 +-- src/Components/History/History.tsx | 4 +- src/Components/Properties/Properties.tsx | 2 +- src/Components/SVG/Elements/Container.tsx | 4 +- src/Components/SVG/Elements/Selector.tsx | 2 +- src/Components/SVG/SVG.tsx | 2 +- src/Components/Sidebar/Sidebar.tsx | 4 +- src/Components/UI/UI.tsx | 16 ++--- src/Events/EditorEvents.ts | 26 ++++++++ src/Interfaces/Configuration.ts | 9 --- ...bleContainer.ts => IAvailableContainer.ts} | 2 +- ...AvailableSymbol.ts => IAvailableSymbol.ts} | 6 +- src/Interfaces/IConfiguration.ts | 9 +++ .../{ContainerModel.ts => IContainerModel.ts} | 8 +-- src/Interfaces/IEditorState.tsx | 8 +++ .../{HistoryState.ts => IHistoryState.ts} | 4 +- src/Interfaces/{Image.ts => IImage.ts} | 2 +- src/Interfaces/{Point.ts => IPoint.ts} | 2 +- .../{Properties.ts => IProperties.ts} | 2 +- .../{SizePointer.ts => ISizePointer.ts} | 2 +- src/utils/default.ts | 8 +-- src/utils/itertools.ts | 2 +- src/utils/saveload.ts | 2 +- 38 files changed, 228 insertions(+), 116 deletions(-) create mode 100644 .gitattributes create mode 100644 docs/ComponentStructure.drawio create mode 100644 public/Interfaces.d.ts create mode 100644 src/Events/EditorEvents.ts delete mode 100644 src/Interfaces/Configuration.ts rename src/Interfaces/{AvailableContainer.ts => IAvailableContainer.ts} (87%) rename src/Interfaces/{AvailableSymbol.ts => IAvailableSymbol.ts} (71%) create mode 100644 src/Interfaces/IConfiguration.ts rename src/Interfaces/{ContainerModel.ts => IContainerModel.ts} (81%) create mode 100644 src/Interfaces/IEditorState.tsx rename src/Interfaces/{HistoryState.ts => IHistoryState.ts} (66%) rename src/Interfaces/{Image.ts => IImage.ts} (81%) rename src/Interfaces/{Point.ts => IPoint.ts} (50%) rename src/Interfaces/{Properties.ts => IProperties.ts} (77%) rename src/Interfaces/{SizePointer.ts => ISizePointer.ts} (87%) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2def8a6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.drawio filter=lfs diff=lfs merge=lfs -text diff --git a/docs/ComponentStructure.drawio b/docs/ComponentStructure.drawio new file mode 100644 index 0000000..2fdcefc --- /dev/null +++ b/docs/ComponentStructure.drawio @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59a34bc3428280ecd661efa7b4756bf9f4930f36c077984a40e7fdc6983aeeff +size 2063 diff --git a/public/Interfaces.d.ts b/public/Interfaces.d.ts new file mode 100644 index 0000000..5a466a1 --- /dev/null +++ b/public/Interfaces.d.ts @@ -0,0 +1,65 @@ +declare interface IHistoryState { + LastAction: string + MainContainer: IContainerModel + SelectedContainer: IContainerModel | null + SelectedContainerId: string + TypeCounters: Record +} + +declare interface IAvailableContainer { + Type: string + Width: number + Height: number + XPositionReference?: XPositionReference + Style: React.CSSProperties +} + +declare interface IEditorState { + history: IHistoryState[] + historyCurrentStep: number + configuration: IConfiguration +} + +declare interface IConfiguration { + AvailableContainers: IAvailableContainer[] + AvailableSymbols: IAvailableSymbol[] + MainContainer: IAvailableContainer +} + +declare interface IContainerModel { + children: IContainerModel[] + parent: IContainerModel | null + properties: IProperties + userData: Record +} + +declare interface IProperties extends React.CSSProperties { + id: string + parentId: string | null + x: number + y: number + isRigidBody: boolean + XPositionReference?: XPositionReference +} + +declare enum XPositionReference { + Left, + Center, + Right +} + +declare interface IAvailableSymbol { + Name: string + XPositionReference: XPositionReference + Image: IImage + Width: number + Height: number +} + +declare interface IImage { + Name: string + Url: string + Base64Image: string + Svg: string +} + diff --git a/src/Components/API/api.ts b/src/Components/API/api.ts index e113cc9..9acea24 100644 --- a/src/Components/API/api.ts +++ b/src/Components/API/api.ts @@ -1,10 +1,10 @@ -import { Configuration } from '../../Interfaces/Configuration'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; /** * Fetch the configuration from the API * @returns {Configation} The model of the configuration for the application */ -export async function fetchConfiguration(): Promise { +export async function fetchConfiguration(): Promise { const url = `${import.meta.env.VITE_API_URL}`; // The test library cannot use the Fetch API // @ts-expect-error @@ -15,7 +15,7 @@ export async function fetchConfiguration(): Promise { }) .then(async(response) => await response.json() - ) as Configuration; + ) as IConfiguration; } return await new Promise((resolve) => { const xhr = new XMLHttpRequest(); diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx index c25c37e..c85d322 100644 --- a/src/Components/App/App.tsx +++ b/src/Components/App/App.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; import './App.scss'; import { MainMenu } from '../MainMenu/MainMenu'; -import { ContainerModel } from '../../Interfaces/ContainerModel'; -import Editor, { IEditorState } from '../Editor/Editor'; +import { ContainerModel } from '../../Interfaces/IContainerModel'; +import Editor from '../Editor/Editor'; +import { IEditorState } from '../../Interfaces/IEditorState'; import { LoadState } from './Load'; import { LoadEditor, NewEditor } from './MenuActions'; import { DEFAULT_CONFIG, DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default'; diff --git a/src/Components/App/Load.ts b/src/Components/App/Load.ts index b5459c7..909dfc9 100644 --- a/src/Components/App/Load.ts +++ b/src/Components/App/Load.ts @@ -1,6 +1,6 @@ import { Dispatch, SetStateAction } from 'react'; import { Revive } from '../../utils/saveload'; -import { IEditorState } from '../Editor/Editor'; +import { IEditorState } from '../../Interfaces/IEditorState'; export function LoadState( editorState: IEditorState, diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts index 2669c1a..c849ca8 100644 --- a/src/Components/App/MenuActions.ts +++ b/src/Components/App/MenuActions.ts @@ -1,8 +1,8 @@ import { Dispatch, SetStateAction } from 'react'; -import { Configuration } from '../../Interfaces/Configuration'; -import { ContainerModel } from '../../Interfaces/ContainerModel'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; +import { ContainerModel } from '../../Interfaces/IContainerModel'; import { fetchConfiguration } from '../API/api'; -import { IEditorState } from '../Editor/Editor'; +import { IEditorState } from "../../Interfaces/IEditorState"; import { LoadState } from './Load'; export function NewEditor( @@ -11,7 +11,7 @@ export function NewEditor( ): void { // Fetch the configuration from the API fetchConfiguration() - .then((configuration: Configuration) => { + .then((configuration: IConfiguration) => { // Set the main container from the given properties of the API const MainContainer = new ContainerModel( null, diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index fde4a72..40391c5 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -1,7 +1,7 @@ import { Dispatch, SetStateAction } from 'react'; -import { HistoryState } from '../../Interfaces/HistoryState'; -import { Configuration } from '../../Interfaces/Configuration'; -import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; +import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; @@ -11,9 +11,9 @@ import { getCurrentHistory } from './Editor'; */ export function SelectContainer( container: ContainerModel, - fullHistory: HistoryState[], + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); @@ -46,9 +46,9 @@ export function SelectContainer( */ export function DeleteContainer( containerId: string, - fullHistory: HistoryState[], + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); @@ -108,10 +108,10 @@ export function DeleteContainer( */ export function AddContainerToSelectedContainer( type: string, - configuration: Configuration, - fullHistory: HistoryState[], + configuration: IConfiguration, + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); @@ -151,10 +151,10 @@ export function AddContainer( index: number, type: string, parentId: string, - configuration: Configuration, - fullHistory: HistoryState[], + configuration: IConfiguration, + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 09627e4..5c8e334 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -1,32 +1,29 @@ -import React from 'react'; +import React, { useRef } from 'react'; import './Editor.scss'; -import { Configuration } from '../../Interfaces/Configuration'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; import { SVG } from '../SVG/SVG'; -import { HistoryState } from '../../Interfaces/HistoryState'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; import { UI } from '../UI/UI'; import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, AddContainer } from './ContainerOperations'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Save'; import { onKeyDown } from './Shortcuts'; import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations'; +import EditorEvents from '../../Events/EditorEvents'; +import { IEditorState } from '../../Interfaces/IEditorState'; interface IEditorProps { - configuration: Configuration - history: HistoryState[] + configuration: IConfiguration + history: IHistoryState[] historyCurrentStep: number } -export interface IEditorState { - history: HistoryState[] - historyCurrentStep: number - configuration: Configuration -} - -export const getCurrentHistory = (history: HistoryState[], historyCurrentStep: number): HistoryState[] => history.slice(0, historyCurrentStep + 1); -export const getCurrentHistoryState = (history: HistoryState[], historyCurrentStep: number): HistoryState => history[historyCurrentStep]; +export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] => history.slice(0, historyCurrentStep + 1); +export const getCurrentHistoryState = (history: IHistoryState[], historyCurrentStep: number): IHistoryState => history[historyCurrentStep]; const Editor: React.FunctionComponent = (props) => { - const [history, setHistory] = React.useState(structuredClone(props.history)); + const [history, setHistory] = React.useState(structuredClone(props.history)); const [historyCurrentStep, setHistoryCurrentStep] = React.useState(props.historyCurrentStep); + const editorRef = useRef(null); React.useEffect(() => { const onKeyUp = (event: KeyboardEvent): void => onKeyDown( @@ -38,6 +35,17 @@ const Editor: React.FunctionComponent = (props) => { window.addEventListener('keyup', onKeyUp); + const events = EditorEvents; + const editorState: IEditorState = { + history, + historyCurrentStep, + configuration: props.configuration + }; + + for (const event of events) { + editorRef.current?.addEventListener(event.name, () => event.func(editorState)); + } + return () => { window.removeEventListener('keyup', onKeyUp); }; @@ -46,7 +54,7 @@ const Editor: React.FunctionComponent = (props) => { const configuration = props.configuration; const current = getCurrentHistoryState(history, historyCurrentStep); return ( -
+
>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { const history = getCurrentHistory(fullHistory, historyCurrentStep); @@ -73,10 +73,10 @@ export function OnPropertyChange( */ export function OnPropertiesSubmit( event: React.SyntheticEvent, - properties: Properties, - fullHistory: HistoryState[], + properties: IProperties, + fullHistory: IHistoryState[], historyCurrentStep: number, - setHistory: Dispatch>, + setHistory: Dispatch>, setHistoryCurrentStep: Dispatch> ): void { event.preventDefault(); diff --git a/src/Components/Editor/RigidBodyBehaviors.ts b/src/Components/Editor/RigidBodyBehaviors.ts index 95ce545..116fbc4 100644 --- a/src/Components/Editor/RigidBodyBehaviors.ts +++ b/src/Components/Editor/RigidBodyBehaviors.ts @@ -1,5 +1,5 @@ -import { IContainerModel } from '../../Interfaces/ContainerModel'; -import { SizePointer } from '../../Interfaces/SizePointer'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; +import { ISizePointer } from '../../Interfaces/ISizePointer'; /** * "Transform the container into a rigid body" @@ -167,17 +167,17 @@ function constraintBodyInsideUnallocatedWidth( * (except the fact that disk space is divided by block). * @param container Container where to find an available width * @param exception Container to exclude of the widths (since a container will be moved, it might need to be excluded) - * @returns {SizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space) + * @returns {ISizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space) */ function getAvailableWidths( container: IContainerModel, exception: IContainerModel -): SizePointer[] { +): ISizePointer[] { // Initialize the first size pointer // which takes full width of the available space const x = 0; const width = Number(container.properties.width); - let unallocatedSpaces: SizePointer[] = [{ x, width }]; + let unallocatedSpaces: ISizePointer[] = [{ x, width }]; // We will only uses containers that also have the rigid bodies // as out-of-bound or enormouse containers should be ignored @@ -192,7 +192,7 @@ function getAvailableWidths( } // get the space of the child that is inside the parent - let newUnallocatedSpace: SizePointer[] = []; + let newUnallocatedSpace: ISizePointer[] = []; // We will iterate on a mutable variable in order to divide it for (const unallocatedSpace of unallocatedSpaces) { @@ -229,7 +229,7 @@ function getAvailableWidthsTwoLines( max1: number, min2: number, max2: number -): SizePointer[] { +): ISizePointer[] { if (min2 < min1 && max2 > max1) { // object 2 is overlapping full width return []; @@ -276,5 +276,5 @@ function getAvailableWidthsTwoLines( */ const isFitting = ( container: IContainerModel, - sizePointer: SizePointer + sizePointer: ISizePointer ): boolean => Number(container.properties.width) <= sizePointer.width; diff --git a/src/Components/Editor/Save.ts b/src/Components/Editor/Save.ts index 3aa201a..5717a41 100644 --- a/src/Components/Editor/Save.ts +++ b/src/Components/Editor/Save.ts @@ -1,13 +1,13 @@ -import { HistoryState } from '../../Interfaces/HistoryState'; -import { Configuration } from '../../Interfaces/Configuration'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; +import { IConfiguration } from '../../Interfaces/IConfiguration'; import { getCircularReplacer } from '../../utils/saveload'; import { ID } from '../SVG/SVG'; -import { IEditorState } from './Editor'; +import { IEditorState } from '../../Interfaces/IEditorState'; export function SaveEditorAsJSON( - history: HistoryState[], + history: IHistoryState[], historyCurrentStep: number, - configuration: Configuration + configuration: IConfiguration ): void { const exportName = 'state'; const spaces = import.meta.env.DEV ? 4 : 0; diff --git a/src/Components/Editor/Shortcuts.ts b/src/Components/Editor/Shortcuts.ts index 8ac6790..aa20d96 100644 --- a/src/Components/Editor/Shortcuts.ts +++ b/src/Components/Editor/Shortcuts.ts @@ -1,9 +1,9 @@ import { Dispatch, SetStateAction } from 'react'; -import { HistoryState } from '../../Interfaces/HistoryState'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; export function onKeyDown( event: KeyboardEvent, - history: HistoryState[], + history: IHistoryState[], historyCurrentStep: number, setHistoryCurrentStep: Dispatch> ): void { diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index fac312b..f1776c0 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import * as React from 'react'; import { fireEvent, render, screen } from '../../utils/test-utils'; import { ElementsSidebar } from './ElementsSidebar'; -import { IContainerModel } from '../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; describe.concurrent('Elements sidebar', () => { it('With a MainContainer', () => { diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index e9d5e59..e62f3db 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; -import ContainerProperties from '../../Interfaces/Properties'; -import { IContainerModel } from '../../Interfaces/ContainerModel'; +import ContainerProperties from '../../Interfaces/IProperties'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; import { getDepth, MakeIterator } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; import { MenuItem } from '../Menu/MenuItem'; import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers'; -import { Point } from '../../Interfaces/Point'; +import { IPoint } from '../../Interfaces/IPoint'; interface IElementsSidebarProps { MainContainer: IContainerModel @@ -64,7 +64,7 @@ export const ElementsSidebar: React.FC = (props: IElement // States const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); const [onClickContainerId, setOnClickContainerId] = React.useState(''); - const [contextMenuPosition, setContextMenuPosition] = React.useState({ + const [contextMenuPosition, setContextMenuPosition] = React.useState({ x: 0, y: 0 }); diff --git a/src/Components/ElementsSidebar/MouseEventHandlers.ts b/src/Components/ElementsSidebar/MouseEventHandlers.ts index 53fb14e..1a814d2 100644 --- a/src/Components/ElementsSidebar/MouseEventHandlers.ts +++ b/src/Components/ElementsSidebar/MouseEventHandlers.ts @@ -1,12 +1,12 @@ -import { IContainerModel } from '../../Interfaces/ContainerModel'; -import { Point } from '../../Interfaces/Point'; +import { IContainerModel } from '../../Interfaces/IContainerModel'; +import { IPoint } from '../../Interfaces/IPoint'; import { findContainerById } from '../../utils/itertools'; export function handleRightClick( event: MouseEvent, setIsContextMenuOpen: React.Dispatch>, setOnClickContainerId: React.Dispatch>, - setContextMenuPosition: React.Dispatch> + setContextMenuPosition: React.Dispatch> ): void { event.preventDefault(); @@ -16,7 +16,7 @@ export function handleRightClick( return; } - const contextMenuPosition: Point = { x: event.pageX, y: event.pageY }; + const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY }; setIsContextMenuOpen(true); setOnClickContainerId(event.target.id); setContextMenuPosition(contextMenuPosition); diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx index 067a867..dd8e676 100644 --- a/src/Components/History/History.tsx +++ b/src/Components/History/History.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { HistoryState } from "../../Interfaces/HistoryState"; +import { IHistoryState } from "../../Interfaces/IHistoryState"; interface IHistoryProps { - history: HistoryState[] + history: IHistoryState[] historyCurrentStep: number isOpen: boolean jumpTo: (move: number) => void diff --git a/src/Components/Properties/Properties.tsx b/src/Components/Properties/Properties.tsx index 49f5028..516f23d 100644 --- a/src/Components/Properties/Properties.tsx +++ b/src/Components/Properties/Properties.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import ContainerProperties from '../../Interfaces/Properties'; +import ContainerProperties from '../../Interfaces/IProperties'; import { ToggleButton } from '../ToggleButton/ToggleButton'; import { INPUT_TYPES } from './PropertiesInputTypes'; diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index 25a45c9..6417570 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { XPositionReference } from '../../../Enums/XPositionReference'; -import { IContainerModel } from '../../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { getDepth } from '../../../utils/itertools'; import { Dimension } from './Dimension'; -export interface IContainerProps { +interface IContainerProps { model: IContainerModel } diff --git a/src/Components/SVG/Elements/Selector.tsx b/src/Components/SVG/Elements/Selector.tsx index c5937e0..e70ca79 100644 --- a/src/Components/SVG/Elements/Selector.tsx +++ b/src/Components/SVG/Elements/Selector.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { IContainerModel } from '../../../Interfaces/ContainerModel'; +import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { getAbsolutePosition } from '../../../utils/itertools'; interface ISelectorProps { diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 94fb746..5758d5f 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; -import { ContainerModel } from '../../Interfaces/ContainerModel'; +import { ContainerModel } from '../../Interfaces/IContainerModel'; import { Selector } from './Elements/Selector'; import { BAR_WIDTH } from '../Bar/Bar'; diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index dc2f1a7..440908f 100644 --- a/src/Components/Sidebar/Sidebar.tsx +++ b/src/Components/Sidebar/Sidebar.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { AvailableContainer } from '../../Interfaces/AvailableContainer'; +import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { truncateString } from '../../utils/stringtools'; interface ISidebarProps { - componentOptions: AvailableContainer[] + componentOptions: IAvailableContainer[] isOpen: boolean buttonOnClick: (type: string) => void } diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index ffe9fb3..4fa36e6 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -2,23 +2,23 @@ import * as React from 'react'; import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar'; 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 { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; +import { ContainerModel } from '../../Interfaces/IContainerModel'; +import { IHistoryState } from '../../Interfaces/IHistoryState'; import { PhotographIcon, UploadIcon } from '@heroicons/react/outline'; import { FloatingButton } from '../FloatingButton/FloatingButton'; import { Bar } from '../Bar/Bar'; -import Properties from '../../Interfaces/Properties'; +import IProperties from '../../Interfaces/IProperties'; interface IUIProps { - current: HistoryState - history: HistoryState[] + current: IHistoryState + history: IHistoryState[] historyCurrentStep: number - AvailableContainers: AvailableContainer[] + AvailableContainers: IAvailableContainer[] SelectContainer: (container: ContainerModel) => void DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string | number | boolean) => void - OnPropertiesSubmit: (event: React.FormEvent, properties: Properties) => void + OnPropertiesSubmit: (event: React.FormEvent, properties: IProperties) => void AddContainerToSelectedContainer: (type: string) => void AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void diff --git a/src/Events/EditorEvents.ts b/src/Events/EditorEvents.ts new file mode 100644 index 0000000..316b7b8 --- /dev/null +++ b/src/Events/EditorEvents.ts @@ -0,0 +1,26 @@ +import { IEditorState } from '../Interfaces/IEditorState'; +import { IHistoryState } from '../Interfaces/IHistoryState'; + +const getEditorState = (editorState: IEditorState): void => { + const customEvent = new CustomEvent('getEditorState', { detail: editorState }); + document.dispatchEvent(customEvent); +}; + +const getCurrentHistoryState = (editorState: IEditorState): void => { + const customEvent = new CustomEvent( + 'getCurrentHistoryState', + { detail: editorState.history[editorState.historyCurrentStep] }); + document.dispatchEvent(customEvent); +}; + +export interface IEditorEvent { + name: string + func: (editorState: IEditorState) => void +} + +const events: IEditorEvent[] = [ + { name: 'getEditorState', func: getEditorState }, + { name: 'getCurrentHistoryState', func: getCurrentHistoryState } +]; + +export default events; diff --git a/src/Interfaces/Configuration.ts b/src/Interfaces/Configuration.ts deleted file mode 100644 index f8d4854..0000000 --- a/src/Interfaces/Configuration.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AvailableContainer } from './AvailableContainer'; -import { AvailableSymbolModel } from './AvailableSymbol'; - -/** Model of configuration for the application to configure it */ -export interface Configuration { - AvailableContainers: AvailableContainer[] - AvailableSymbols: AvailableSymbolModel[] - MainContainer: AvailableContainer -} diff --git a/src/Interfaces/AvailableContainer.ts b/src/Interfaces/IAvailableContainer.ts similarity index 87% rename from src/Interfaces/AvailableContainer.ts rename to src/Interfaces/IAvailableContainer.ts index b328b0a..c7ad3c1 100644 --- a/src/Interfaces/AvailableContainer.ts +++ b/src/Interfaces/IAvailableContainer.ts @@ -2,7 +2,7 @@ import React from 'react'; import { XPositionReference } from '../Enums/XPositionReference'; /** Model of available container used in application configuration */ -export interface AvailableContainer { +export interface IAvailableContainer { Type: string Width: number Height: number diff --git a/src/Interfaces/AvailableSymbol.ts b/src/Interfaces/IAvailableSymbol.ts similarity index 71% rename from src/Interfaces/AvailableSymbol.ts rename to src/Interfaces/IAvailableSymbol.ts index e1d518d..3f3176a 100644 --- a/src/Interfaces/AvailableSymbol.ts +++ b/src/Interfaces/IAvailableSymbol.ts @@ -1,12 +1,12 @@ import { XPositionReference } from '../Enums/XPositionReference'; -import { Image } from './Image'; +import { IImage } from './IImage'; /** * Model of available symbol to configure the application */ -export interface AvailableSymbolModel { +export interface IAvailableSymbol { Name: string XPositionReference: XPositionReference - Image: Image + Image: IImage Width: number Height: number } diff --git a/src/Interfaces/IConfiguration.ts b/src/Interfaces/IConfiguration.ts new file mode 100644 index 0000000..a37647d --- /dev/null +++ b/src/Interfaces/IConfiguration.ts @@ -0,0 +1,9 @@ +import { IAvailableContainer } from './IAvailableContainer'; +import { IAvailableSymbol } from './IAvailableSymbol'; + +/** Model of configuration for the application to configure it */ +export interface IConfiguration { + AvailableContainers: IAvailableContainer[] + AvailableSymbols: IAvailableSymbol[] + MainContainer: IAvailableContainer +} diff --git a/src/Interfaces/ContainerModel.ts b/src/Interfaces/IContainerModel.ts similarity index 81% rename from src/Interfaces/ContainerModel.ts rename to src/Interfaces/IContainerModel.ts index 1c70ae3..b180486 100644 --- a/src/Interfaces/ContainerModel.ts +++ b/src/Interfaces/IContainerModel.ts @@ -1,21 +1,21 @@ -import Properties from './Properties'; +import IProperties from './IProperties'; export interface IContainerModel { children: IContainerModel[] parent: IContainerModel | null - properties: Properties + properties: IProperties userData: Record } export class ContainerModel implements IContainerModel { public children: IContainerModel[]; public parent: IContainerModel | null; - public properties: Properties; + public properties: IProperties; public userData: Record; constructor( parent: IContainerModel | null, - properties: Properties, + properties: IProperties, children: IContainerModel[] = [], userData = {}) { this.parent = parent; diff --git a/src/Interfaces/IEditorState.tsx b/src/Interfaces/IEditorState.tsx new file mode 100644 index 0000000..495a868 --- /dev/null +++ b/src/Interfaces/IEditorState.tsx @@ -0,0 +1,8 @@ +import { IConfiguration } from './IConfiguration'; +import { IHistoryState } from './IHistoryState'; + +export interface IEditorState { + history: IHistoryState[] + historyCurrentStep: number + configuration: IConfiguration +} diff --git a/src/Interfaces/HistoryState.ts b/src/Interfaces/IHistoryState.ts similarity index 66% rename from src/Interfaces/HistoryState.ts rename to src/Interfaces/IHistoryState.ts index da1d74b..fd46fbc 100644 --- a/src/Interfaces/HistoryState.ts +++ b/src/Interfaces/IHistoryState.ts @@ -1,6 +1,6 @@ -import { IContainerModel } from './ContainerModel'; +import { IContainerModel } from './IContainerModel'; -export interface HistoryState { +export interface IHistoryState { LastAction: string MainContainer: IContainerModel SelectedContainer: IContainerModel | null diff --git a/src/Interfaces/Image.ts b/src/Interfaces/IImage.ts similarity index 81% rename from src/Interfaces/Image.ts rename to src/Interfaces/IImage.ts index b839b09..7432440 100644 --- a/src/Interfaces/Image.ts +++ b/src/Interfaces/IImage.ts @@ -1,5 +1,5 @@ /** Model of an image with multiple source */ -export interface Image { +export interface IImage { Name: string Url: string Base64Image: string diff --git a/src/Interfaces/Point.ts b/src/Interfaces/IPoint.ts similarity index 50% rename from src/Interfaces/Point.ts rename to src/Interfaces/IPoint.ts index 43fd673..d2e202a 100644 --- a/src/Interfaces/Point.ts +++ b/src/Interfaces/IPoint.ts @@ -1,4 +1,4 @@ -export interface Point { +export interface IPoint { x: number y: number } diff --git a/src/Interfaces/Properties.ts b/src/Interfaces/IProperties.ts similarity index 77% rename from src/Interfaces/Properties.ts rename to src/Interfaces/IProperties.ts index 6f2f32e..4996ce5 100644 --- a/src/Interfaces/Properties.ts +++ b/src/Interfaces/IProperties.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { XPositionReference } from '../Enums/XPositionReference'; -export default interface Properties extends React.CSSProperties { +export default interface IProperties extends React.CSSProperties { id: string parentId: string | null x: number diff --git a/src/Interfaces/SizePointer.ts b/src/Interfaces/ISizePointer.ts similarity index 87% rename from src/Interfaces/SizePointer.ts rename to src/Interfaces/ISizePointer.ts index e5ec599..05f880c 100644 --- a/src/Interfaces/SizePointer.ts +++ b/src/Interfaces/ISizePointer.ts @@ -3,7 +3,7 @@ * x being the address where the pointer is pointing * width being the overall (un)allocated space affected to the address */ -export interface SizePointer { +export interface ISizePointer { x: number width: number } diff --git a/src/utils/default.ts b/src/utils/default.ts index 51c1d7a..785a840 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -1,7 +1,7 @@ -import { Configuration } from '../Interfaces/Configuration'; -import Properties from '../Interfaces/Properties'; +import { IConfiguration } from '../Interfaces/IConfiguration'; +import IProperties from '../Interfaces/IProperties'; -export const DEFAULT_CONFIG: Configuration = { +export const DEFAULT_CONFIG: IConfiguration = { AvailableContainers: [ { Type: 'Container', @@ -25,7 +25,7 @@ export const DEFAULT_CONFIG: Configuration = { } }; -export const DEFAULT_MAINCONTAINER_PROPS: Properties = { +export const DEFAULT_MAINCONTAINER_PROPS: IProperties = { id: 'main', parentId: 'null', x: 0, diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts index 67cd40c..d50a089 100644 --- a/src/utils/itertools.ts +++ b/src/utils/itertools.ts @@ -1,4 +1,4 @@ -import { IContainerModel } from '../Interfaces/ContainerModel'; +import { IContainerModel } from '../Interfaces/IContainerModel'; /** * Returns a Generator iterating of over the children depth-first diff --git a/src/utils/saveload.ts b/src/utils/saveload.ts index a33652e..356dd50 100644 --- a/src/utils/saveload.ts +++ b/src/utils/saveload.ts @@ -1,5 +1,5 @@ import { findContainerById, MakeIterator } from './itertools'; -import { IEditorState } from '../Components/Editor/Editor'; +import { IEditorState } from '../Interfaces/IEditorState'; /** * Revive the Editor state From 704dab7307fb98a0418cdd6df5c43052ee89ad36 Mon Sep 17 00:00:00 2001 From: Siklos Date: Fri, 12 Aug 2022 11:47:21 -0400 Subject: [PATCH 08/12] Implement anchor and fix bugs with rigid body (#27) Co-authored-by: Eric NGUYEN Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/27 --- src/Components/App/MenuActions.ts | 3 +- .../Editor/Behaviors/AnchorBehaviors.ts | 70 ++++++++++++ .../{ => Behaviors}/RigidBodyBehaviors.ts | 106 ++++++++++++------ src/Components/Editor/ContainerOperations.ts | 26 +++-- src/Components/Editor/PropertiesOperations.ts | 52 +++------ .../ElementsSidebar/ElementsSidebar.test.tsx | 21 ++-- src/Components/Properties/Properties.test.tsx | 3 +- .../Properties/PropertiesInputTypes.tsx | 3 +- src/Enums/AddingBehavior.ts | 4 - .../{IEditorState.tsx => IEditorState.ts} | 0 src/Interfaces/IProperties.ts | 10 ++ src/utils/default.ts | 1 + 12 files changed, 202 insertions(+), 97 deletions(-) create mode 100644 src/Components/Editor/Behaviors/AnchorBehaviors.ts rename src/Components/Editor/{ => Behaviors}/RigidBodyBehaviors.ts (72%) delete mode 100644 src/Enums/AddingBehavior.ts rename src/Interfaces/{IEditorState.tsx => IEditorState.ts} (100%) diff --git a/src/Components/App/MenuActions.ts b/src/Components/App/MenuActions.ts index c849ca8..c144acc 100644 --- a/src/Components/App/MenuActions.ts +++ b/src/Components/App/MenuActions.ts @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction } from 'react'; import { IConfiguration } from '../../Interfaces/IConfiguration'; import { ContainerModel } from '../../Interfaces/IContainerModel'; import { fetchConfiguration } from '../API/api'; -import { IEditorState } from "../../Interfaces/IEditorState"; +import { IEditorState } from '../../Interfaces/IEditorState'; import { LoadState } from './Load'; export function NewEditor( @@ -23,6 +23,7 @@ export function NewEditor( width: configuration.MainContainer.Width, height: configuration.MainContainer.Height, isRigidBody: false, + isAnchor: false, fillOpacity: 0, stroke: 'black' } diff --git a/src/Components/Editor/Behaviors/AnchorBehaviors.ts b/src/Components/Editor/Behaviors/AnchorBehaviors.ts new file mode 100644 index 0000000..d9731e8 --- /dev/null +++ b/src/Components/Editor/Behaviors/AnchorBehaviors.ts @@ -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; +} diff --git a/src/Components/Editor/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts similarity index 72% rename from src/Components/Editor/RigidBodyBehaviors.ts rename to src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index 116fbc4..eabec85 100644 --- a/src/Components/Editor/RigidBodyBehaviors.ts +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -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" @@ -82,7 +90,7 @@ function constraintBodyInsideSpace( if (containerX < x) { containerProperties.x = x; } - if (containerX + containerWidth > width) { + if (containerX + containerWidth > x + width) { containerProperties.x = x + width - containerWidth; } @@ -90,7 +98,7 @@ function constraintBodyInsideSpace( if (containerY < y) { containerProperties.y = y; } - if (containerY + containerHeight > height) { + if (containerY + containerHeight > y + height) { containerProperties.y = y + height - containerHeight; } @@ -104,7 +112,7 @@ function constraintBodyInsideSpace( * @param container * @returns Updated container */ -function constraintBodyInsideUnallocatedWidth( +export function constraintBodyInsideUnallocatedWidth( container: IContainerModel ): IContainerModel { if (container.parent === null) { @@ -114,12 +122,7 @@ function constraintBodyInsideUnallocatedWidth( // Get the available spaces of the parent const availableWidths = getAvailableWidths(container.parent, container); const containerX = Number(container.properties.x); - - // Sort the available width to find the closest one - availableWidths.sort( - (width1, width2) => - Math.abs(width1.x - containerX) - Math.abs(width2.x - containerX) - ); + const containerWidth = Number(container.properties.width); // Check if there is still some space 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 // It will usually fit if it was alrady fitting const availableWidthFound = availableWidths.find((width) => @@ -179,17 +200,18 @@ function getAvailableWidths( const width = Number(container.properties.width); let unallocatedSpaces: ISizePointer[] = [{ x, width }]; - // We will only uses containers that also have the rigid bodies - // as out-of-bound or enormouse containers should be ignored - const rigidBodies = container.children.filter( - (child) => child.properties.isRigidBody + // We will only uses containers that also are rigid or are anchors + const solidBodies = container.children.filter( + (child) => child.properties.isRigidBody || child.properties.isAnchor ); - for (const child of rigidBodies) { + for (const child of solidBodies) { // Ignore the exception if (child === exception) { continue; } + const childX = child.properties.x; + const childWidth = Number(child.properties.width); // get the space of the child that is inside the parent let newUnallocatedSpace: ISizePointer[] = []; @@ -202,8 +224,8 @@ function getAvailableWidths( const newUnallocatedWidths = getAvailableWidthsTwoLines( unallocatedSpace.x, unallocatedSpace.x + unallocatedSpace.width, - child.properties.x, - child.properties.x + Number(child.properties.width) + childX, + childX + childWidth ); // 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 - * @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 + * @param unalloctedSpaceLeft left of the first line + * @param unallocatedSpaceRight rigth of the first line + * @param rectLeft left of the second line + * @param rectRight right of the second line * @returns Available widths */ function getAvailableWidthsTwoLines( - min1: number, - max1: number, - min2: number, - max2: number + unalloctedSpaceLeft: number, + unallocatedSpaceRight: number, + rectLeft: number, + rectRight: number ): 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 return []; } - if (min1 >= min2) { + if (unalloctedSpaceLeft >= rectLeft) { // object 2 is partially overlapping on the left return [ { - x: max2, - width: max1 - max2 + x: rectRight, + width: unallocatedSpaceRight - rectRight } ]; } - if (max2 >= max1) { + if (rectRight >= unallocatedSpaceRight) { // object 2 is partially overlapping on the right return [ { - x: min2, - width: max2 - min1 + x: unalloctedSpaceLeft, + width: rectRight - unalloctedSpaceLeft } ]; } @@ -258,12 +290,12 @@ function getAvailableWidthsTwoLines( // object 2 is overlapping in the middle return [ { - x: min1, - width: min2 - min1 + x: unalloctedSpaceLeft, + width: rectLeft - unalloctedSpaceLeft }, { - x: min2, - width: max1 - max2 + x: rectRight, + width: unallocatedSpaceRight - rectRight } ]; } diff --git a/src/Components/Editor/ContainerOperations.ts b/src/Components/Editor/ContainerOperations.ts index 40391c5..63d90a0 100644 --- a/src/Components/Editor/ContainerOperations.ts +++ b/src/Components/Editor/ContainerOperations.ts @@ -4,6 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { findContainerById } from '../../utils/itertools'; import { getCurrentHistory } from './Editor'; +import IProperties from '../../Interfaces/IProperties'; /** * Select a container @@ -203,20 +204,23 @@ export function AddContainer( } } + const defaultProperties: IProperties = { + id: `${type}-${count}`, + parentId: parentClone.properties.id, + x, + y: 0, + width: properties.Width, + height: parentClone.properties.height, + isRigidBody: false, + isAnchor: false, + XPositionReference: properties.XPositionReference, + ...properties.Style + }; + // Create the container const newContainer = new ContainerModel( parentClone, - { - id: `${type}-${count}`, - parentId: parentClone.properties.id, - x, - y: 0, - width: properties?.Width, - height: parentClone.properties.height, - isRigidBody: false, - XPositionReference: properties.XPositionReference, - ...properties.Style - }, + defaultProperties, [], { type diff --git a/src/Components/Editor/PropertiesOperations.ts b/src/Components/Editor/PropertiesOperations.ts index 11c1b62..ed5a4db 100644 --- a/src/Components/Editor/PropertiesOperations.ts +++ b/src/Components/Editor/PropertiesOperations.ts @@ -4,7 +4,9 @@ import { IHistoryState } from '../../Interfaces/IHistoryState'; import IProperties from '../../Interfaces/IProperties'; import { findContainerById } from '../../utils/itertools'; 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 @@ -28,20 +30,6 @@ export function OnPropertyChange( 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 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!'); } - (container.properties as any)[key] = value; + if (INPUT_TYPES[key] === 'number') { + (container.properties as any)[key] = Number(value); + } else { + (container.properties as any)[key] = value; + } + + if (container.properties.isAnchor) { + ImposePosition(container); + } if (container.properties.isRigidBody) { RecalculatePhysics(container); @@ -88,25 +84,6 @@ export function OnPropertiesSubmit( 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 container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id); @@ -118,6 +95,11 @@ export function OnPropertiesSubmit( const input = (event.target as HTMLFormElement).querySelector(`#${property}`); if (input instanceof HTMLInputElement) { (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; + } } } diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index f1776c0..8d34480 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -17,7 +17,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }} @@ -47,7 +48,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -103,7 +105,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -119,7 +122,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} } @@ -136,7 +140,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} } @@ -173,7 +178,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 2000, height: 100, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; @@ -188,7 +194,8 @@ describe.concurrent('Elements sidebar', () => { y: 0, width: 0, height: 0, - isRigidBody: false + isRigidBody: false, + isAnchor: false }, userData: {} }; diff --git a/src/Components/Properties/Properties.test.tsx b/src/Components/Properties/Properties.test.tsx index 2afc364..9fbef37 100644 --- a/src/Components/Properties/Properties.test.tsx +++ b/src/Components/Properties/Properties.test.tsx @@ -23,7 +23,8 @@ describe.concurrent('Properties', () => { parentId: 'parentId', x: 1, y: 1, - isRigidBody: false + isRigidBody: false, + isAnchor: false }; const handleChange = vi.fn((key, value) => { diff --git a/src/Components/Properties/PropertiesInputTypes.tsx b/src/Components/Properties/PropertiesInputTypes.tsx index c62a8fd..d91ddbc 100644 --- a/src/Components/Properties/PropertiesInputTypes.tsx +++ b/src/Components/Properties/PropertiesInputTypes.tsx @@ -3,5 +3,6 @@ export const INPUT_TYPES: Record = { y: 'number', width: 'number', height: 'number', - isRigidBody: 'checkbox' + isRigidBody: 'checkbox', + isAnchor: 'checkbox' }; diff --git a/src/Enums/AddingBehavior.ts b/src/Enums/AddingBehavior.ts deleted file mode 100644 index fb6ae67..0000000 --- a/src/Enums/AddingBehavior.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum AddingBehavior { - InsertInto, - Replace -} diff --git a/src/Interfaces/IEditorState.tsx b/src/Interfaces/IEditorState.ts similarity index 100% rename from src/Interfaces/IEditorState.tsx rename to src/Interfaces/IEditorState.ts diff --git a/src/Interfaces/IProperties.ts b/src/Interfaces/IProperties.ts index 4996ce5..ef2db7e 100644 --- a/src/Interfaces/IProperties.ts +++ b/src/Interfaces/IProperties.ts @@ -1,11 +1,21 @@ import * as React from 'react'; 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 { id: string parentId: string | null x: number y: number isRigidBody: boolean + isAnchor: boolean XPositionReference?: XPositionReference } diff --git a/src/utils/default.ts b/src/utils/default.ts index 785a840..2bd22dc 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -33,6 +33,7 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = { width: DEFAULT_CONFIG.MainContainer.Width, height: DEFAULT_CONFIG.MainContainer.Height, isRigidBody: false, + isAnchor: false, fillOpacity: 0, stroke: 'black' }; From d6eb9ea3641130c428b04adfd6ed61d99656648d Mon Sep 17 00:00:00 2001 From: Siklos Date: Fri, 12 Aug 2022 16:31:37 -0400 Subject: [PATCH 09/12] Optimize history and fix nodes pollution + fix css + removes motion.framer (#28) Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/28 --- package-lock.json | 420 +++--------------- package.json | 5 +- pnpm-lock.yaml | 139 ++---- src/Components/Bar/Bar.tsx | 2 +- src/Components/Editor/ContainerOperations.ts | 25 +- src/Components/Editor/Editor.tsx | 13 +- src/Components/Editor/PropertiesOperations.ts | 18 +- .../ElementsSidebar/ElementsSidebar.tsx | 96 ++-- src/Components/History/History.tsx | 49 +- src/Components/SVG/SVG.tsx | 2 +- src/Components/Sidebar/Sidebar.tsx | 2 +- 11 files changed, 196 insertions(+), 575 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8573881..b7e9f41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "0.0.0", "dependencies": { "@heroicons/react": "^1.0.6", - "framer-motion": "^6.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-svg-pan-zoom": "^3.11.0" + "react-svg-pan-zoom": "^3.11.0", + "react-window": "^1.8.7" }, "devDependencies": { "@testing-library/dom": "^8.16.1", @@ -22,6 +22,7 @@ "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@types/react-svg-pan-zoom": "^3.3.5", + "@types/react-window": "^1.8.5", "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", "@vitejs/plugin-react": "^2.0.0", @@ -424,7 +425,6 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.13.4" }, @@ -480,21 +480,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "node_modules/@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -617,89 +602,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@motionone/animation": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.13.1.tgz", - "integrity": "sha512-dxQ+1wWxL6iFHDy1uv6hhcPjIdOg36eDT56jN4LI7Z5HZRyLpq8x1t7JFQclo/IEIb+6Bk4atmyinGFdXVECuA==", - "dependencies": { - "@motionone/easing": "^10.13.1", - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/animation/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/@motionone/dom": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", - "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", - "dependencies": { - "@motionone/animation": "^10.12.0", - "@motionone/generators": "^10.12.0", - "@motionone/types": "^10.12.0", - "@motionone/utils": "^10.12.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/dom/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/@motionone/easing": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.13.1.tgz", - "integrity": "sha512-INEsInHHDHVgx0dp5qlXi1lMXBqYicgLMMSn3zfGzaIvcaEbI1Uz8BoyNV4BiclTupG7RYIh+T6BU83ZcEe74g==", - "dependencies": { - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/easing/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/@motionone/generators": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.13.1.tgz", - "integrity": "sha512-+HK5u2YcNJCckTTqfOLgSVcrWv2z1dVwrSZEMVJuAh0EnWEWGDJRvMBoPc0cFf/osbkA2Rq9bH2+vP0Ex/D8uw==", - "dependencies": { - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/generators/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/@motionone/types": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.13.0.tgz", - "integrity": "sha512-qegk4qg8U1N9ZwAJ187BG3TkZz1k9LP/pvNtCSlqdq/PMUDKlCFG4ZnjJ481P0IOH/vIw1OzIbKIuyg0A3rk9g==" - }, - "node_modules/@motionone/utils": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.13.1.tgz", - "integrity": "sha512-TjDPTIppaf3ofBXQv4ZzAketJgN0sclALXfZ6mfrkjJkOy83mLls9744F+6S+VKCpBmvbZcBY4PQfrfhAfeMtA==", - "dependencies": { - "@motionone/types": "^10.13.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/utils/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1082,6 +984,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", + "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -3355,44 +3266,6 @@ "url": "https://www.patreon.com/infusion" } }, - "node_modules/framer-motion": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", - "dependencies": { - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, - "peerDependencies": { - "react": ">=16.8 || ^17.0.0 || ^18.0.0", - "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/framer-motion/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, - "node_modules/framesync": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", - "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/framesync/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3630,11 +3503,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -4452,6 +4320,11 @@ "node": ">=12" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4860,22 +4733,6 @@ "node": ">=0.10.0" } }, - "node_modules/popmotion": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", - "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", - "dependencies": { - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - } - }, - "node_modules/popmotion/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, "node_modules/postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -5153,6 +5010,22 @@ "react": ">=17.0.0" } }, + "node_modules/react-window": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz", + "integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -5190,8 +5063,7 @@ "node_modules/regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", @@ -5560,20 +5432,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/style-value-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", - "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", - "dependencies": { - "hey-listen": "^1.0.8", - "tslib": "^2.1.0" - } - }, - "node_modules/style-value-types/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6443,7 +6301,6 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -6487,21 +6344,6 @@ "to-fast-properties": "^2.0.0" } }, - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "requires": { - "@emotion/memoize": "0.7.4" - } - }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "@eslint/eslintrc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", @@ -6600,99 +6442,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "@motionone/animation": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.13.1.tgz", - "integrity": "sha512-dxQ+1wWxL6iFHDy1uv6hhcPjIdOg36eDT56jN4LI7Z5HZRyLpq8x1t7JFQclo/IEIb+6Bk4atmyinGFdXVECuA==", - "requires": { - "@motionone/easing": "^10.13.1", - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "@motionone/dom": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", - "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", - "requires": { - "@motionone/animation": "^10.12.0", - "@motionone/generators": "^10.12.0", - "@motionone/types": "^10.12.0", - "@motionone/utils": "^10.12.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "@motionone/easing": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.13.1.tgz", - "integrity": "sha512-INEsInHHDHVgx0dp5qlXi1lMXBqYicgLMMSn3zfGzaIvcaEbI1Uz8BoyNV4BiclTupG7RYIh+T6BU83ZcEe74g==", - "requires": { - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "@motionone/generators": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.13.1.tgz", - "integrity": "sha512-+HK5u2YcNJCckTTqfOLgSVcrWv2z1dVwrSZEMVJuAh0EnWEWGDJRvMBoPc0cFf/osbkA2Rq9bH2+vP0Ex/D8uw==", - "requires": { - "@motionone/types": "^10.13.0", - "@motionone/utils": "^10.13.1", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "@motionone/types": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.13.0.tgz", - "integrity": "sha512-qegk4qg8U1N9ZwAJ187BG3TkZz1k9LP/pvNtCSlqdq/PMUDKlCFG4ZnjJ481P0IOH/vIw1OzIbKIuyg0A3rk9g==" - }, - "@motionone/utils": { - "version": "10.13.1", - "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.13.1.tgz", - "integrity": "sha512-TjDPTIppaf3ofBXQv4ZzAketJgN0sclALXfZ6mfrkjJkOy83mLls9744F+6S+VKCpBmvbZcBY4PQfrfhAfeMtA==", - "requires": { - "@motionone/types": "^10.13.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7000,6 +6749,15 @@ "@types/react": "*" } }, + "@types/react-window": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz", + "integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -8547,42 +8305,6 @@ "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", "dev": true }, - "framer-motion": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", - "requires": { - "@emotion/is-prop-valid": "^0.8.2", - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, - "framesync": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", - "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", - "requires": { - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8747,11 +8469,6 @@ "has-symbols": "^1.0.2" } }, - "hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" - }, "html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -9350,6 +9067,11 @@ "sourcemap-codec": "^1.4.8" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -9647,24 +9369,6 @@ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, - "popmotion": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", - "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", - "requires": { - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -9833,6 +9537,15 @@ "transformation-matrix": "^2.11.1" } }, + "react-window": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.7.tgz", + "integrity": "sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==", + "requires": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -9864,8 +9577,7 @@ "regenerator-runtime": { "version": "0.13.9", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", - "dev": true + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" }, "regexp.prototype.flags": { "version": "1.4.3", @@ -10122,22 +9834,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "style-value-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", - "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", - "requires": { - "hey-listen": "^1.0.8", - "tslib": "^2.1.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index 0f0d27f..5fdcfa7 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ }, "dependencies": { "@heroicons/react": "^1.0.6", - "framer-motion": "^6.5.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-svg-pan-zoom": "^3.11.0" + "react-svg-pan-zoom": "^3.11.0", + "react-window": "^1.8.7" }, "devDependencies": { "@testing-library/dom": "^8.16.1", @@ -27,6 +27,7 @@ "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", "@types/react-svg-pan-zoom": "^3.3.5", + "@types/react-window": "^1.8.5", "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", "@vitejs/plugin-react": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 469528f..be1cc19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ specifiers: '@types/react': ^18.0.15 '@types/react-dom': ^18.0.6 '@types/react-svg-pan-zoom': ^3.3.5 + '@types/react-window': ^1.8.5 '@typescript-eslint/eslint-plugin': ^5.31.0 '@typescript-eslint/parser': ^5.31.0 '@vitejs/plugin-react': ^2.0.0 @@ -23,12 +24,12 @@ specifiers: eslint-plugin-promise: ^6.0.0 eslint-plugin-react: ^7.30.1 eslint-plugin-react-hooks: ^4.6.0 - framer-motion: ^6.5.1 jsdom: ^20.0.0 postcss: ^8.4.14 react: ^18.2.0 react-dom: ^18.2.0 react-svg-pan-zoom: ^3.11.0 + react-window: ^1.8.7 sass: ^1.54.0 tailwindcss: ^3.1.7 typescript: ^4.6.4 @@ -37,10 +38,10 @@ specifiers: dependencies: '@heroicons/react': 1.0.6_react@18.2.0 - framer-motion: 6.5.1_biqbaboplfbrettd7655fr4n2y react: 18.2.0 react-dom: 18.2.0_react@18.2.0 react-svg-pan-zoom: 3.11.0_react@18.2.0 + react-window: 1.8.7_biqbaboplfbrettd7655fr4n2y devDependencies: '@testing-library/dom': 8.16.1 @@ -50,6 +51,7 @@ devDependencies: '@types/react': 18.0.17 '@types/react-dom': 18.0.6 '@types/react-svg-pan-zoom': 3.3.5 + '@types/react-window': 1.8.5 '@typescript-eslint/eslint-plugin': 5.32.0_iosr3hrei2tubxveewluhu5lhy '@typescript-eslint/parser': 5.32.0_qugx7qdu5zevzvxaiqyxfiwquq '@vitejs/plugin-react': 2.0.0_vite@3.0.4 @@ -314,7 +316,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.13.9 - dev: true /@babel/template/7.18.10: resolution: {integrity: sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==} @@ -352,19 +353,6 @@ packages: to-fast-properties: 2.0.0 dev: true - /@emotion/is-prop-valid/0.8.8: - resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} - requiresBuild: true - dependencies: - '@emotion/memoize': 0.7.4 - dev: false - optional: true - - /@emotion/memoize/0.7.4: - resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} - dev: false - optional: true - /@esbuild/linux-loong64/0.14.54: resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} engines: {node: '>=12'} @@ -463,53 +451,6 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true - /@motionone/animation/10.13.2: - resolution: {integrity: sha512-YGWss58IR2X4lOjW89rv1Q+/Nq/QhfltaggI7i8sZTpKC1yUvM+XYDdvlRpWc6dk8LviMBrddBJAlLdbaqeRmw==} - dependencies: - '@motionone/easing': 10.13.2 - '@motionone/types': 10.13.2 - '@motionone/utils': 10.13.2 - tslib: 2.4.0 - dev: false - - /@motionone/dom/10.12.0: - resolution: {integrity: sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==} - dependencies: - '@motionone/animation': 10.13.2 - '@motionone/generators': 10.13.2 - '@motionone/types': 10.13.2 - '@motionone/utils': 10.13.2 - hey-listen: 1.0.8 - tslib: 2.4.0 - dev: false - - /@motionone/easing/10.13.2: - resolution: {integrity: sha512-3HqctS5NyDfDQ+8+cZqc3Pu7I6amFCt9zDUjcozHyFXHh4PKYHK4+GJDFjJIS8bCAF2BrJmpmduDQ2V7lFEYeQ==} - dependencies: - '@motionone/utils': 10.13.2 - tslib: 2.4.0 - dev: false - - /@motionone/generators/10.13.2: - resolution: {integrity: sha512-QMoXV1MXEEhR6D3dct/RMMS1FwJlAsW+kMPbFGzBA4NbweblgeYQCft9DcDAVpV9wIwD6qvlBG9u99sOXLfHiA==} - dependencies: - '@motionone/types': 10.13.2 - '@motionone/utils': 10.13.2 - tslib: 2.4.0 - dev: false - - /@motionone/types/10.13.2: - resolution: {integrity: sha512-yYV4q5v5F0iADhab4wHfqaRJnM/eVtQLjUPhyEcS72aUz/xyOzi09GzD/Gu+K506BDfqn5eULIilUI77QNaqhw==} - dev: false - - /@motionone/utils/10.13.2: - resolution: {integrity: sha512-6Lw5bDA/w7lrPmT/jYWQ76lkHlHs9fl2NZpJ22cVy1kKDdEH+Cl1U6hMTpdphO6VQktQ6v2APngag91WBKLqlA==} - dependencies: - '@motionone/types': 10.13.2 - hey-listen: 1.0.8 - tslib: 2.4.0 - dev: false - /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -645,6 +586,12 @@ packages: '@types/react': 18.0.17 dev: true + /@types/react-window/1.8.5: + resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==} + dependencies: + '@types/react': 18.0.17 + dev: true + /@types/react/18.0.17: resolution: {integrity: sha512-38ETy4tL+rn4uQQi7mB81G7V1g0u2ryquNmsVIOKUAEIDK+3CUjZ6rSRpdvS99dNBnkLFL83qfmtLacGOTIhwQ==} dependencies: @@ -1997,30 +1944,6 @@ packages: resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} dev: true - /framer-motion/6.5.1_biqbaboplfbrettd7655fr4n2y: - resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==} - peerDependencies: - react: '>=16.8 || ^17.0.0 || ^18.0.0' - react-dom: '>=16.8 || ^17.0.0 || ^18.0.0' - dependencies: - '@motionone/dom': 10.12.0 - framesync: 6.0.1 - hey-listen: 1.0.8 - popmotion: 11.0.3 - react: 18.2.0 - react-dom: 18.2.0_react@18.2.0 - style-value-types: 5.0.0 - tslib: 2.4.0 - optionalDependencies: - '@emotion/is-prop-valid': 0.8.8 - dev: false - - /framesync/6.0.1: - resolution: {integrity: sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==} - dependencies: - tslib: 2.4.0 - dev: false - /fs.realpath/1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -2172,10 +2095,6 @@ packages: function-bind: 1.1.1 dev: true - /hey-listen/1.0.8: - resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} - dev: false - /html-encoding-sniffer/3.0.0: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} @@ -2560,6 +2479,10 @@ packages: sourcemap-codec: 1.4.8 dev: true + /memoize-one/5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2830,15 +2753,6 @@ packages: engines: {node: '>=0.10.0'} dev: true - /popmotion/11.0.3: - resolution: {integrity: sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==} - dependencies: - framesync: 6.0.1 - hey-listen: 1.0.8 - style-value-types: 5.0.0 - tslib: 2.4.0 - dev: false - /postcss-import/14.1.0_postcss@8.4.16: resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} engines: {node: '>=10.0.0'} @@ -2998,6 +2912,19 @@ packages: transformation-matrix: 2.12.0 dev: false + /react-window/1.8.7_biqbaboplfbrettd7655fr4n2y: + resolution: {integrity: sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.18.9 + memoize-one: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + dev: false + /react/18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -3027,7 +2954,6 @@ packages: /regenerator-runtime/0.13.9: resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} - dev: true /regexp.prototype.flags/1.4.3: resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} @@ -3238,13 +3164,6 @@ packages: engines: {node: '>=8'} dev: true - /style-value-types/5.0.0: - resolution: {integrity: sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==} - dependencies: - hey-listen: 1.0.8 - tslib: 2.4.0 - dev: false - /supports-color/5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -3363,10 +3282,6 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /tslib/2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - dev: false - /tsutils/3.21.0_typescript@4.7.4: resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} diff --git a/src/Components/Bar/Bar.tsx b/src/Components/Bar/Bar.tsx index 1738a3f..77cb259 100644 --- a/src/Components/Bar/Bar.tsx +++ b/src/Components/Bar/Bar.tsx @@ -15,7 +15,7 @@ export const BAR_WIDTH = 64; // 4rem export const Bar: React.FC = (props) => { return ( -
+
= (props) => { configuration: props.configuration }; + const funcs = new Map void>(); for (const event of events) { - editorRef.current?.addEventListener(event.name, () => event.func(editorState)); + const func = (): void => event.func(editorState); + editorRef.current?.addEventListener(event.name, func); + funcs.set(event.name, func); } return () => { window.removeEventListener('keyup', onKeyUp); + + for (const event of events) { + const func = funcs.get(event.name); + if (func === undefined) { + continue; + } + editorRef.current?.removeEventListener(event.name, func); + } }; }); diff --git a/src/Components/Editor/PropertiesOperations.ts b/src/Components/Editor/PropertiesOperations.ts index ed5a4db..4bd4c2d 100644 --- a/src/Components/Editor/PropertiesOperations.ts +++ b/src/Components/Editor/PropertiesOperations.ts @@ -51,14 +51,15 @@ export function OnPropertyChange( RecalculatePhysics(container); } - setHistory(history.concat([{ - LastAction: `Change property of container ${container.properties.id}`, + history.push({ + LastAction: `Change ${key} of ${container.properties.id}`, MainContainer: mainContainerClone, SelectedContainer: container, SelectedContainerId: container.properties.id, TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); } /** @@ -107,12 +108,13 @@ export function OnPropertiesSubmit( RecalculatePhysics(container); } - setHistory(history.concat([{ - LastAction: `Change property of container ${container.properties.id}`, + history.push({ + LastAction: `Change properties of ${container.properties.id}`, MainContainer: mainContainerClone, SelectedContainer: container, SelectedContainerId: container.properties.id, TypeCounters: Object.assign({}, current.TypeCounters) - }])); - setHistoryCurrentStep(history.length); + }); + setHistory(history); + setHistoryCurrentStep(history.length - 1); } diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index e62f3db..30f1617 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { motion } from 'framer-motion'; +import { FixedSizeList as List } from 'react-window'; import { Properties } from '../Properties/Properties'; import ContainerProperties from '../../Interfaces/IProperties'; import { IContainerModel } from '../../Interfaces/IContainerModel'; @@ -21,45 +21,6 @@ interface IElementsSidebarProps { AddContainer: (index: number, type: string, parent: string) => void } -function createRows( - container: IContainerModel, - props: IElementsSidebarProps, - containerRows: React.ReactNode[] -): void { - const depth: number = getDepth(container); - const key = container.properties.id.toString(); - const text = '|\t'.repeat(depth) + key; - const selectedClass: string = props.SelectedContainer !== undefined && - props.SelectedContainer !== null && - props.SelectedContainer.properties.id === container.properties.id - ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' - : 'bg-slate-300/60 hover:bg-slate-300'; - - containerRows.push( - handleOnDrop(event, props.MainContainer, props.AddContainer)} - onDragOver={(event) => handleDragOver(event, props.MainContainer)} - onDragLeave={(event) => handleDragLeave(event)} - onClick={() => props.SelectContainer(container)} - > - { text } - - ); -}; - export const ElementsSidebar: React.FC = (props: IElementsSidebarProps): JSX.Element => { // States const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); @@ -117,24 +78,55 @@ export const ElementsSidebar: React.FC = (props: IElement : 'right-0'; } - const containerRows: React.ReactNode[] = []; - const it = MakeIterator(props.MainContainer); - for (const container of it) { - createRows( - container, - props, - containerRows - ); - } + const containers = [...it]; + const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { + const container = containers[index]; + const depth: number = getDepth(container); + const key = container.properties.id.toString(); + const text = '|\t'.repeat(depth) + key; + const selectedClass: string = props.SelectedContainer !== undefined && + props.SelectedContainer !== null && + props.SelectedContainer.properties.id === container.properties.id + ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' + : 'bg-slate-300/60 hover:bg-slate-300'; + return ( + + ); + }; + + const ROW_HEIGHT = 35; + const NUMBERS_OF_ROWS = 10; return ( -
+
Elements
-
- { containerRows } +
+ + { Row } +
= (props: IHistoryProps) => { const isOpenClasses = props.isOpen ? 'right-0' : '-right-64'; - - const states = props.history.map((step, move) => { - const desc = move > 0 - ? `Go to modification n°${move}` + const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { + const reversedIndex = (props.history.length - 1) - index; + const step = props.history[reversedIndex]; + const desc = reversedIndex > 0 + ? `${reversedIndex}: ${step.LastAction}` : 'Go to the beginning'; - const isCurrent = move === props.historyCurrentStep; - - const selectedClass = isCurrent + const selectedClass = reversedIndex === props.historyCurrentStep ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-500 hover:bg-slate-700'; - const isCurrentText = isCurrent - ? ' (current)' - : ''; return ( - ); - }); - - // recent first - states.reverse(); + }; return ( -
+
Timeline
-
- { states } -
+ + { Row } +
); }; diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 5758d5f..652258f 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -30,7 +30,7 @@ function resizeViewBox( export const SVG: React.FC = (props: ISVGProps) => { const [viewer, setViewer] = React.useState({ - viewerWidth: window.innerWidth, + viewerWidth: window.innerWidth - BAR_WIDTH, viewerHeight: window.innerHeight }); diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index 440908f..c39b235 100644 --- a/src/Components/Sidebar/Sidebar.tsx +++ b/src/Components/Sidebar/Sidebar.tsx @@ -30,7 +30,7 @@ export const Sidebar: React.FC = (props: ISidebarProps) => { const isOpenClasses = props.isOpen ? 'left-16' : '-left-64'; return (
Components From 497d2173e889a29364d7ab431e582e1d556303a3 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 15 Aug 2022 15:41:38 +0200 Subject: [PATCH 10/12] Replace sqrt in Dimension by Fast inverse square root --- src/Components/SVG/Elements/Dimension.tsx | 5 +++-- src/utils/math.ts | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/utils/math.ts diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index c5f6f86..3a20b9c 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { NOTCHES_LENGTH } from '../../../utils/default'; +import { Qrsqrt } from '../../../utils/math'; interface IDimensionProps { id: string @@ -32,8 +33,8 @@ export const Dimension: React.FC = (props: IDimensionProps) => const [deltaX, deltaY] = [(props.xEnd - props.xStart), (props.yEnd - props.yStart)]; // Get the unit vector - const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - const [unitX, unitY] = [deltaX / norm, deltaY / norm]; + const inv = Qrsqrt(deltaX * deltaX + deltaY * deltaY); + const [unitX, unitY] = [deltaX * inv, deltaY * inv]; // Get the perpandicular vector const [perpVecX, perpVecY] = [unitY, -unitX]; diff --git a/src/utils/math.ts b/src/utils/math.ts new file mode 100644 index 0000000..38a1039 --- /dev/null +++ b/src/utils/math.ts @@ -0,0 +1,21 @@ +const bytes = new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT); +const floatView = new Float32Array(bytes); +const intView = new Uint32Array(bytes); +const threehalfs = 1.5; + +/** + * Fast inverse square root + * http://en.wikipedia.org/wiki/Fast_inverse_square_root + * https://youtu.be/p8u_k2LIZyo + * @param number Number to square root + * @returns Approximation of the squqre root of the number + */ +export function Qrsqrt(number: number): number { + const x2 = number * 0.5; + floatView[0] = number; + intView[0] = 0x5f3759df - (intView[0] >> 1); + let y = floatView[0]; + y = y * (threehalfs - (x2 * y * y)); + + return y; +} From 5b20e4f2dccb93b1cf16d28838824b986575ec00 Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 15 Aug 2022 16:00:04 +0200 Subject: [PATCH 11/12] Revert "Replace sqrt in Dimension by Fast inverse square root" This reverts commit 497d2173e889a29364d7ab431e582e1d556303a3. --- src/Components/SVG/Elements/Dimension.tsx | 5 ++--- src/utils/math.ts | 21 --------------------- 2 files changed, 2 insertions(+), 24 deletions(-) delete mode 100644 src/utils/math.ts diff --git a/src/Components/SVG/Elements/Dimension.tsx b/src/Components/SVG/Elements/Dimension.tsx index 3a20b9c..c5f6f86 100644 --- a/src/Components/SVG/Elements/Dimension.tsx +++ b/src/Components/SVG/Elements/Dimension.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { NOTCHES_LENGTH } from '../../../utils/default'; -import { Qrsqrt } from '../../../utils/math'; interface IDimensionProps { id: string @@ -33,8 +32,8 @@ export const Dimension: React.FC = (props: IDimensionProps) => const [deltaX, deltaY] = [(props.xEnd - props.xStart), (props.yEnd - props.yStart)]; // Get the unit vector - const inv = Qrsqrt(deltaX * deltaX + deltaY * deltaY); - const [unitX, unitY] = [deltaX * inv, deltaY * inv]; + const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + const [unitX, unitY] = [deltaX / norm, deltaY / norm]; // Get the perpandicular vector const [perpVecX, perpVecY] = [unitY, -unitX]; diff --git a/src/utils/math.ts b/src/utils/math.ts deleted file mode 100644 index 38a1039..0000000 --- a/src/utils/math.ts +++ /dev/null @@ -1,21 +0,0 @@ -const bytes = new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT); -const floatView = new Float32Array(bytes); -const intView = new Uint32Array(bytes); -const threehalfs = 1.5; - -/** - * Fast inverse square root - * http://en.wikipedia.org/wiki/Fast_inverse_square_root - * https://youtu.be/p8u_k2LIZyo - * @param number Number to square root - * @returns Approximation of the squqre root of the number - */ -export function Qrsqrt(number: number): number { - const x2 = number * 0.5; - floatView[0] = number; - intView[0] = 0x5f3759df - (intView[0] >> 1); - let y = floatView[0]; - y = y * (threehalfs - (x2 * y * y)); - - return y; -} From 7ff411b98845ec2481bad433c2f92afb552b99ab Mon Sep 17 00:00:00 2001 From: Siklos Date: Mon, 15 Aug 2022 11:52:17 -0400 Subject: [PATCH 12/12] Implement webworker for save operation + Limit the history size (#29) - Implement webworker for save operation - Limit the history size Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/29 --- src/Components/Editor/Editor.tsx | 8 +++++- src/Components/Editor/Save.ts | 41 ++++++++++++++++++++---------- src/Components/History/History.tsx | 4 +-- src/utils/default.ts | 2 ++ src/workers/worker.js | 25 ++++++++++++++++++ tsconfig.json | 2 +- 6 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 src/workers/worker.js diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 4337365..3a728e3 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -10,6 +10,7 @@ import { onKeyDown } from './Shortcuts'; import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations'; import EditorEvents from '../../Events/EditorEvents'; import { IEditorState } from '../../Interfaces/IEditorState'; +import { MAX_HISTORY } from '../../utils/default'; interface IEditorProps { configuration: IConfiguration @@ -17,7 +18,12 @@ interface IEditorProps { historyCurrentStep: number } -export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] => history.slice(0, historyCurrentStep + 1); +export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] => + history.slice( + Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow) + historyCurrentStep + 1 + ); + export const getCurrentHistoryState = (history: IHistoryState[], historyCurrentStep: number): IHistoryState => history[historyCurrentStep]; const Editor: React.FunctionComponent = (props) => { diff --git a/src/Components/Editor/Save.ts b/src/Components/Editor/Save.ts index 5717a41..159959a 100644 --- a/src/Components/Editor/Save.ts +++ b/src/Components/Editor/Save.ts @@ -3,27 +3,38 @@ import { IConfiguration } from '../../Interfaces/IConfiguration'; import { getCircularReplacer } from '../../utils/saveload'; import { ID } from '../SVG/SVG'; import { IEditorState } from '../../Interfaces/IEditorState'; +import Worker from '../../workers/worker?worker'; export function SaveEditorAsJSON( history: IHistoryState[], historyCurrentStep: number, configuration: IConfiguration ): void { - const exportName = 'state'; + const exportName = 'state.json'; const spaces = import.meta.env.DEV ? 4 : 0; const editorState: IEditorState = { history, historyCurrentStep, configuration }; + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (window.Worker) { + // use webworker for the stringify to avoid freezing + const myWorker = new Worker(); + myWorker.postMessage({ editorState, spaces }); + myWorker.onmessage = (event) => { + const data = event.data; + const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; + createDownloadNode(exportName, dataStr); + myWorker.terminate(); + }; + return; + } + const data = JSON.stringify(editorState, getCircularReplacer(), spaces); const dataStr = `data:text/json;charset=utf-8,${encodeURIComponent(data)}`; - const downloadAnchorNode = document.createElement('a'); - downloadAnchorNode.setAttribute('href', dataStr); - downloadAnchorNode.setAttribute('download', `${exportName}.json`); - document.body.appendChild(downloadAnchorNode); // required for firefox - downloadAnchorNode.click(); - downloadAnchorNode.remove(); + createDownloadNode(exportName, dataStr); } export function SaveEditorAsSVG(): void { @@ -32,10 +43,14 @@ export function SaveEditorAsSVG(): void { const preface = '\r\n'; const svgBlob = new Blob([preface, svg.outerHTML], { type: 'image/svg+xml;charset=utf-8' }); const svgUrl = URL.createObjectURL(svgBlob); - const downloadLink = document.createElement('a'); - downloadLink.href = svgUrl; - downloadLink.download = 'newesttree.svg'; - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); + createDownloadNode('state.svg', svgUrl); +} + +function createDownloadNode(filename: string, datastring: string) { + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.href = datastring; + downloadAnchorNode.download = filename; + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); } diff --git a/src/Components/History/History.tsx b/src/Components/History/History.tsx index 30778bd..36e8e80 100644 --- a/src/Components/History/History.tsx +++ b/src/Components/History/History.tsx @@ -14,9 +14,7 @@ export const History: React.FC = (props: IHistoryProps) => { const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => { const reversedIndex = (props.history.length - 1) - index; const step = props.history[reversedIndex]; - const desc = reversedIndex > 0 - ? `${reversedIndex}: ${step.LastAction}` - : 'Go to the beginning'; + const desc = step.LastAction; const selectedClass = reversedIndex === props.historyCurrentStep ? 'bg-blue-500 hover:bg-blue-600' diff --git a/src/utils/default.ts b/src/utils/default.ts index 2bd22dc..027c540 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -39,3 +39,5 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = { }; export const NOTCHES_LENGTH = 4; + +export const MAX_HISTORY = 200; diff --git a/src/workers/worker.js b/src/workers/worker.js new file mode 100644 index 0000000..c11fa7d --- /dev/null +++ b/src/workers/worker.js @@ -0,0 +1,25 @@ +onmessage = (e) => { + const data = JSON.stringify(e.data.editorState, getCircularReplacer(), e.data.spaces); + postMessage(data); +}; + +const getCircularReplacer = () => { + const seen = new WeakSet(); + return (key, value) => { + if (key === 'parent') { + return; + } + + if (key === 'SelectedContainer') { + return; + } + + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return; + } + seen.add(value); + } + return value; + }; +}; diff --git a/tsconfig.json b/tsconfig.json index 034ee0e..2fe4d5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["src"], + "include": ["src", "src/workers"], "exclude": ["test-server"], "references": [{ "path": "./tsconfig.node.json" }] }