Merge pull request 'Separated the model and the Container entity in order to remove any mutation operation' (#3) from dev.containermodel into dev
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/3
This commit is contained in:
Siklos 2022-08-04 07:52:16 -04:00
commit 86535f9940
7 changed files with 206 additions and 178 deletions

View file

@ -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<string, number>
}
@ -56,19 +57,16 @@ class App extends React.Component<IAppProps> {
// 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<IAppProps> {
* 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<IAppProps> {
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<string, string | number>;
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<IAppProps> {
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<IAppProps> {
} 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<IAppProps> {
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)}
/>
<button className='fixed z-10 top-4 right-12 text-lg bg-slate-200 hover:bg-slate-300 transition-all drop-shadow-md hover:drop-shadow-lg py-2 px-3 rounded-lg' onClick={() => this.ToggleElementsSidebar()}>&#9776; Elements</button>
<SVG selected={current.SelectedContainer}>

View file

@ -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<IElementsSidebarProps> {
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<IElementsSidebarProps> {
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(
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 1.2 }}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.150
}}
className={
`w-full elements-sidebar-row whitespace-pre
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 1.2 }}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 0.150
}}
className={
`w-full elements-sidebar-row whitespace-pre
text-left text-sm font-medium transition-all ${selectedClass}`
}
key={key}
onClick={() => this.props.selectContainer(container)}>
}
key={key}
onClick={() => this.props.selectContainer(container)}>
{ text }
</motion.button>
);
@ -67,7 +67,7 @@ export class ElementsSidebar extends React.Component<IElementsSidebarProps> {
<div className='overflow-y-auto overflow-x-hidden text-slate-200 flex-grow divide-y divide-solid divide-slate-500'>
{ containerRows }
</div>
<Properties properties={this.props.SelectedContainer?.GetProperties()} onChange={this.props.onPropertyChange}></Properties>
<Properties properties={this.props.SelectedContainer?.properties} onChange={this.props.onPropertyChange}></Properties>
</div>
);
}

View file

@ -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<string, string | number>
export interface IContainerProps {
model: IContainerModel
}
const GAP = 50;
export class Container extends React.Component<IContainerProps> {
/**
* 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<Container, void, unknown> {
const queue: Container[] = [this];
const visited = new Set<Container>(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<IContainerProps> {
// 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<IContainerProps> {
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 (
<g
style={defaultStyle}
transform={`translate(${this.props.properties.x}, ${this.props.properties.y})`}
key={`container-${this.props.properties.id}`}
transform={`translate(${this.props.model.properties.x}, ${this.props.model.properties.y})`}
key={`container-${this.props.model.properties.id}`}
>
<Dimension
id={id}
@ -128,8 +58,8 @@ export class Container extends React.Component<IContainerProps> {
text={text}
/>
<rect
width={this.props.properties.width}
height={this.props.properties.height}
width={this.props.model.properties.width}
height={this.props.model.properties.height}
style={style}
>
</rect>
@ -137,7 +67,7 @@ export class Container extends React.Component<IContainerProps> {
x={xText}
y={yText}
>
{this.props.properties.id}
{this.props.model.properties.id}
</text>
{ containersElements }
</g>

View file

@ -0,0 +1,80 @@
import Properties from '../../../Interfaces/Properties';
export interface IContainerModel {
children: IContainerModel[],
parent: IContainerModel | null,
properties: Properties,
userData: Record<string, string | number>
}
export class ContainerModel implements IContainerModel {
public children: IContainerModel[];
public parent: IContainerModel | null;
public properties: Properties;
public userData: Record<string, string | number>;
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<IContainerModel, void, unknown> {
const queue: IContainerModel[] = [root];
const visited = new Set<IContainerModel>(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];
}

View file

@ -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({

View file

@ -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<ISelectorProps> = (props) => {
@ -13,10 +13,10 @@ export const Selector: React.FC<ISelectorProps> = (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

View file

@ -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<ISVGProps> {
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 (