From e2a099457c5ec2888af83092e4b4e916bb52bf2c Mon Sep 17 00:00:00 2001 From: Siklos Date: Thu, 4 Aug 2022 12:57:34 +0200 Subject: [PATCH] Separated the model and the Container entity in order to remove any mutation operation --- src/App.tsx | 119 ++++++++++-------- .../ElementsSidebar/ElementsSidebar.tsx | 48 +++---- src/Components/SVG/Elements/Container.tsx | 102 +++------------ src/Components/SVG/Elements/ContainerModel.ts | 80 ++++++++++++ .../SVG/Elements/DimensionLayer.tsx | 16 +-- src/Components/SVG/Elements/Selector.tsx | 10 +- src/Components/SVG/SVG.tsx | 9 +- 7 files changed, 206 insertions(+), 178 deletions(-) create mode 100644 src/Components/SVG/Elements/ContainerModel.ts diff --git a/src/App.tsx b/src/App.tsx index ac29d48..ed36af1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,20 @@ -import React, { Children } from 'react'; +import React from 'react'; import './App.scss'; import Sidebar from './Components/Sidebar/Sidebar'; import { ElementsSidebar } from './Components/ElementsSidebar/ElementsSidebar'; import { AvailableContainer } from './Interfaces/AvailableContainer'; import { Configuration } from './Interfaces/Configuration'; -import { Container } from './Components/SVG/Elements/Container'; import { SVG } from './Components/SVG/SVG'; import { History } from './Components/History/History'; +import { ContainerModel, IContainerModel, MakeIterator } from './Components/SVG/Elements/ContainerModel'; +import Properties from './Interfaces/Properties'; interface IAppProps { } export interface IHistoryState { - MainContainer: Container | null, - SelectedContainer: Container | null, + MainContainer: IContainerModel | null, + SelectedContainer: IContainerModel | null, TypeCounters: Record } @@ -56,19 +57,16 @@ class App extends React.Component { // Fetch the configuration from the API fetchConfiguration().then((configuration: Configuration) => { // Set the main container from the given properties of the API - const MainContainer = new Container( + const MainContainer = new ContainerModel( + null, { - parent: null, - properties: { - id: 'main', - x: 0, - y: 0, - width: configuration.MainContainer.Width, - height: configuration.MainContainer.Height, - fillOpacity: 0, - stroke: 'black' - }, - children: [] + id: 'main', + x: 0, + y: 0, + width: configuration.MainContainer.Width, + height: configuration.MainContainer.Height, + fillOpacity: 0, + stroke: 'black' } ); @@ -116,7 +114,7 @@ class App extends React.Component { * Select a container * @param container Selected container */ - public SelectContainer(container: Container) { + public SelectContainer(container: ContainerModel) { const history = this.getCurrentHistory(); const current = history[history.length - 1]; this.setState({ @@ -141,30 +139,21 @@ class App extends React.Component { if (current.SelectedContainer === null || current.SelectedContainer === undefined) { - throw new Error('Property was changed before selecting a Container'); + throw new Error('[OnPropertyChange] Property was changed before selecting a Container'); } if (current.MainContainer === null || current.MainContainer === undefined) { - throw new Error('Property was changed before the main container was added'); + throw new Error('[OnPropertyChange] Property was changed before the main container was added'); } - const pair = {} as Record; - pair[key] = value; - const properties = Object.assign(current.SelectedContainer.props.properties, pair); - const props = { - ...current.SelectedContainer.props, - properties - }; - - const newSelectedContainer = new Container(props); - - const parent = current.SelectedContainer.props.parent; if (parent === null) { + const clone: IContainerModel = structuredClone(current.SelectedContainer); + (clone.properties as any)[key] = value; this.setState({ history: history.concat([{ - SelectedContainer: newSelectedContainer, - MainContainer: newSelectedContainer, + SelectedContainer: clone, + MainContainer: clone, TypeCounters: current.TypeCounters }]), historyCurrentStep: history.length @@ -172,15 +161,27 @@ class App extends React.Component { return; } - const index = parent.props.children.indexOf(current.SelectedContainer); - parent.props.children[index] = newSelectedContainer; + const clone: IContainerModel = structuredClone(current.MainContainer); + const it = MakeIterator(clone); + let container: ContainerModel | null = null; + for (const child of it) { + if (child.properties.id === current.SelectedContainer.properties.id) { + container = child as ContainerModel; + break; + } + } + + if (container === null) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + (container.properties as any)[key] = value; - const newMainContainer = new Container(Object.assign({}, current.MainContainer.props)); this.setState( { history: history.concat([{ - SelectedContainer: newSelectedContainer, - MainContainer: newMainContainer, + SelectedContainer: container, + MainContainer: clone, TypeCounters: current.TypeCounters }]), historyCurrentStep: history.length @@ -221,36 +222,52 @@ class App extends React.Component { } else { newCounters[type]++; } + const count = newCounters[type]; + + // Create maincontainer model + const structure: IContainerModel = structuredClone(current.MainContainer); + const clone = Object.assign(new ContainerModel(null, {} as Properties), structure); + + // Find the parent + const it = MakeIterator(clone); + let parent: ContainerModel | null = null; + for (const child of it) { + if (child.properties.id === current.SelectedContainer.properties.id) { + parent = child as ContainerModel; + break; + } + } + + if (parent === null) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } // Create the container - const parent = current.SelectedContainer; - const count = newCounters[type]; - const container = new Container({ + const newContainer = new ContainerModel( parent, - properties: { + { id: `${type}-${count}`, x: 0, y: 0, width: properties?.Width, - height: parent.props.properties.height, + height: parent.properties.height, ...properties.Style - }, - children: [], - userData: { + } as Properties, + [], + { type } - }); + ); // And push it the the parent children - parent.props.children.push(container); + parent.children.push(newContainer); // Update the state - const newMainContainer = new Container(Object.assign({}, current.MainContainer.props)); this.setState({ history: history.concat([{ - MainContainer: newMainContainer, + MainContainer: clone, TypeCounters: newCounters, - SelectedContainer: current.SelectedContainer + SelectedContainer: parent }]), historyCurrentStep: history.length } as IAppState); @@ -283,7 +300,7 @@ class App extends React.Component { isOpen={this.state.isSVGSidebarOpen} onClick={() => this.ToggleElementsSidebar()} onPropertyChange={(key: string, value: string) => this.OnPropertyChange(key, value)} - selectContainer={(container: Container) => this.SelectContainer(container)} + selectContainer={(container: ContainerModel) => this.SelectContainer(container)} /> diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index afec90a..3caea25 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,26 +1,26 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; -import { Container } from '../SVG/Elements/Container'; +import { IContainerModel, getDepth, MakeIterator } from '../SVG/Elements/ContainerModel'; interface IElementsSidebarProps { - MainContainer: Container | null, + MainContainer: IContainerModel | null, isOpen: boolean, - SelectedContainer: Container | null, + SelectedContainer: IContainerModel | null, onClick: () => void, onPropertyChange: (key: string, value: string) => void, - selectContainer: (container: Container) => void + selectContainer: (container: IContainerModel) => void } export class ElementsSidebar extends React.Component { - public iterateChilds(handleContainer: (container: Container) => void): React.ReactNode { + public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { if (!this.props.MainContainer) { return null; } - const it = this.props.MainContainer.MakeIterator(); + const it = MakeIterator(this.props.MainContainer); for (const container of it) { - handleContainer(container); + handleContainer(container as IContainerModel); } } @@ -28,29 +28,29 @@ export class ElementsSidebar extends React.Component { const isOpenClasses = this.props.isOpen ? 'right-0' : '-right-64'; const containerRows: React.ReactNode[] = []; - this.iterateChilds((container: Container) => { - const depth: number = container.getDepth(); - const key = container.props.properties.id.toString(); + this.iterateChilds((container: IContainerModel) => { + const depth: number = getDepth(container); + const key = container.properties.id.toString(); const text = '|\t'.repeat(depth) + key; const selectedClass: string = this.props.SelectedContainer !== null && - this.props.SelectedContainer.props.properties.id === container.props.properties.id + this.props.SelectedContainer.properties.id === container.properties.id ? 'bg-blue-500 hover:bg-blue-600' : 'bg-slate-400 hover:bg-slate-600'; containerRows.push( this.props.selectContainer(container)}> + } + key={key} + onClick={() => this.props.selectContainer(container)}> { text } ); @@ -67,7 +67,7 @@ export class ElementsSidebar extends React.Component {
{ containerRows }
- + ); } diff --git a/src/Components/SVG/Elements/Container.tsx b/src/Components/SVG/Elements/Container.tsx index 1094588..ab8d92c 100644 --- a/src/Components/SVG/Elements/Container.tsx +++ b/src/Components/SVG/Elements/Container.tsx @@ -1,92 +1,22 @@ import * as React from 'react'; -import Properties from '../../../Interfaces/Properties'; +import { getDepth, IContainerModel } from './ContainerModel'; import { Dimension } from './Dimension'; -interface IContainerProps { - // eslint-disable-next-line no-use-before-define - parent: Container | null, - // eslint-disable-next-line no-use-before-define - children: Container[], - properties: Properties, - userData?: Record +export interface IContainerProps { + model: IContainerModel } const GAP = 50; export class Container extends React.Component { - /** - * Returns A copy of the properties of the Container - * @returns A copy of the properties of the Container - */ - public GetProperties(): Properties { - const properties : Properties = { - ...this.props.properties - }; - return properties; - } - - /** - * Returns a Generator iterating of over the children depth-first - */ - public * MakeIterator(): Generator { - const queue: Container[] = [this]; - const visited = new Set(queue); - while (queue.length > 0) { - const container = queue.pop() as Container; - - yield container; - - // if this reverse() gets costly, replace it by a simple for - container.props.children.reverse().forEach((child) => { - if (visited.has(child)) { - return; - } - visited.add(child); - queue.push(child); - }); - } - } - - /** - * Returns the depth of the container - * @returns The depth of the container - */ - public getDepth() { - let depth = 0; - - let current: Container | null = this.props.parent; - while (current != null) { - depth++; - current = current.props.parent; - } - - return depth; - } - - /** - * Returns the absolute position by iterating to the parent - * @returns The absolute position of the container - */ - public getAbsolutePosition(): [number, number] { - let x = Number(this.props.properties.x); - let y = Number(this.props.properties.y); - let current = this.props.parent; - while (current != null) { - x += Number(current.props.properties.x); - y += Number(current.props.properties.y); - current = current.props.parent; - } - return [x, y]; - } - /** * Render the container * @returns Render the container */ public render(): React.ReactNode { - const containersElements = this.props.children.map(child => child.render()); - const xText = Number(this.props.properties.width) / 2; - const yText = Number(this.props.properties.height) / 2; + const containersElements = this.props.model.children.map(child => new Container({ model: child } as IContainerProps).render()); + const xText = Number(this.props.model.properties.width) / 2; + const yText = Number(this.props.model.properties.height) / 2; // g style const defaultStyle = { @@ -98,7 +28,7 @@ export class Container extends React.Component { // Rect style const style = Object.assign( JSON.parse(JSON.stringify(defaultStyle)), - this.props.properties + this.props.model.properties ); style.x = 0; style.y = 0; @@ -106,18 +36,18 @@ export class Container extends React.Component { delete style.width; // Dimension props - const id = `dim-${this.props.properties.id}`; + const id = `dim-${this.props.model.properties.id}`; const xStart: number = 0; - const xEnd = Number(this.props.properties.width); - const y = -(GAP * (this.getDepth() + 1)); + const xEnd = Number(this.props.model.properties.width); + const y = -(GAP * (getDepth(this.props.model) + 1)); const strokeWidth = 1; - const text = (this.props.properties.width ?? 0).toString(); + const text = (this.props.model.properties.width ?? 0).toString(); return ( { text={text} /> @@ -137,7 +67,7 @@ export class Container extends React.Component { x={xText} y={yText} > - {this.props.properties.id} + {this.props.model.properties.id} { containersElements } diff --git a/src/Components/SVG/Elements/ContainerModel.ts b/src/Components/SVG/Elements/ContainerModel.ts new file mode 100644 index 0000000..4b35082 --- /dev/null +++ b/src/Components/SVG/Elements/ContainerModel.ts @@ -0,0 +1,80 @@ +import Properties from '../../../Interfaces/Properties'; + +export interface IContainerModel { + children: IContainerModel[], + parent: IContainerModel | null, + properties: Properties, + userData: Record +} + +export class ContainerModel implements IContainerModel { + public children: IContainerModel[]; + public parent: IContainerModel | null; + public properties: Properties; + public userData: Record; + + constructor( + parent: IContainerModel | null, + properties: Properties, + children: IContainerModel[] = [], + userData = {}) { + this.parent = parent; + this.properties = properties; + this.children = children; + this.userData = userData; + } +}; + +/** + * Returns a Generator iterating of over the children depth-first + */ +export function * MakeIterator(root: IContainerModel): Generator { + const queue: IContainerModel[] = [root]; + const visited = new Set(queue); + while (queue.length > 0) { + const container = queue.pop() as IContainerModel; + + yield container; + + // if this reverse() gets costly, replace it by a simple for + container.children.forEach((child) => { + if (visited.has(child)) { + return; + } + visited.add(child); + queue.push(child); + }); + } +} + +/** + * Returns the depth of the container + * @returns The depth of the container + */ +export function getDepth(parent: IContainerModel) { + let depth = 0; + + let current: IContainerModel | null = parent; + while (current != null) { + depth++; + current = current.parent; + } + + return depth; +} + +/** + * Returns the absolute position by iterating to the parent + * @returns The absolute position of the container + */ +export function getAbsolutePosition(container: IContainerModel): [number, number] { + let x = Number(container.properties.x); + let y = Number(container.properties.y); + let current = container.parent; + while (current != null) { + x += Number(current.properties.x); + y += Number(current.properties.y); + current = current.parent; + } + return [x, y]; +} diff --git a/src/Components/SVG/Elements/DimensionLayer.tsx b/src/Components/SVG/Elements/DimensionLayer.tsx index 601fedd..610177b 100644 --- a/src/Components/SVG/Elements/DimensionLayer.tsx +++ b/src/Components/SVG/Elements/DimensionLayer.tsx @@ -1,25 +1,25 @@ import * as React from 'react'; -import { Container } from './Container'; +import { ContainerModel, getDepth, MakeIterator } from './ContainerModel'; import { Dimension } from './Dimension'; interface IDimensionLayerProps { isHidden: boolean, - roots: Container | Container[] | null, + roots: ContainerModel | ContainerModel[] | null, } const GAP: number = 50; -const getDimensionsNodes = (root: Container): React.ReactNode[] => { - const it = root.MakeIterator(); +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.props.properties.width); + const width = Number(container.properties.width); - const id = `dim-${container.props.properties.id}`; - const xStart: number = container.props.properties.x; + const id = `dim-${container.properties.id}`; + const xStart: number = container.properties.x; const xEnd = xStart + width; - const y = -(GAP * (container.getDepth() + 1)); + const y = -(GAP * (getDepth(container) + 1)); const strokeWidth = 1; const text = width.toString(); const dimension = new Dimension({ diff --git a/src/Components/SVG/Elements/Selector.tsx b/src/Components/SVG/Elements/Selector.tsx index 6268d2f..6fc1b9b 100644 --- a/src/Components/SVG/Elements/Selector.tsx +++ b/src/Components/SVG/Elements/Selector.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; -import { Container } from './Container'; +import { IContainerModel, getAbsolutePosition } from './ContainerModel'; interface ISelectorProps { - selected: Container | null + selected: IContainerModel | null } export const Selector: React.FC = (props) => { @@ -13,10 +13,10 @@ export const Selector: React.FC = (props) => { ); } - const [x, y] = props.selected.getAbsolutePosition(); + const [x, y] = getAbsolutePosition(props.selected); const [width, height] = [ - props.selected.props.properties.width, - props.selected.props.properties.height + props.selected.properties.width, + props.selected.properties.height ]; const style = { stroke: '#3B82F6', // tw blue-500 diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 64a3246..9583571 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { ReactSVGPanZoom, Tool, Value, TOOL_PAN } from 'react-svg-pan-zoom'; import { Container } from './Elements/Container'; +import { ContainerModel } from './Elements/ContainerModel'; import { Selector } from './Elements/Selector'; interface ISVGProps { - children: Container | Container[] | null, - selected: Container | null + children: ContainerModel | ContainerModel[] | null, + selected: ContainerModel | null } interface ISVGState { @@ -65,9 +66,9 @@ export class SVG extends React.Component { let children: React.ReactNode | React.ReactNode[] = []; if (Array.isArray(this.props.children)) { - children = this.props.children.map(child => child.render()); + children = this.props.children.map(child => new Container({ model: child }).render()); } else if (this.props.children !== null) { - children = this.props.children.render(); + children = new Container({ model: this.props.children }).render(); } return (