From 3e7af02071f83859be3950fe8c471210a95786e7 Mon Sep 17 00:00:00 2001 From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de> Date: Fri, 16 Sep 2022 14:08:23 +0200 Subject: [PATCH] Partially refactored the node types editor. --- src/common/graph/graph.ts | 18 +- src/editor/components/nodedetails.tsx | 1 + src/editor/components/nodetypeentry.tsx | 214 ++++++++-------------- src/editor/components/nodetypeseditor.tsx | 79 ++++---- src/editor/components/sidepanel.tsx | 23 ++- src/editor/editor.tsx | 60 +++++- src/editor/graph.ts | 2 +- 7 files changed, 204 insertions(+), 193 deletions(-) diff --git a/src/common/graph/graph.ts b/src/common/graph/graph.ts index 13d135d..f0c61c6 100644 --- a/src/common/graph/graph.ts +++ b/src/common/graph/graph.ts @@ -47,9 +47,9 @@ export class Graph private idToNode: Map<number, Node>; private idToLink: Map<number, Link>; - private nextNodeId = 0; - private nextLinkId = 0; - private nextObjectGroupId = 0; + protected nextNodeId = 0; + protected nextLinkId = 0; + protected nextObjectGroupId = 0; /** * Creates a new Graph object. @@ -184,6 +184,14 @@ export class Graph return this.idToNode.get(id); } + public nodeType(id: number): NodeType { + return this.idToObjectGroup.get(id); + } + + public link(id: number): Link { + return this.idToLink.get(id); + } + private addNode(node: Node) { if (this.idToNode.has(node.id)) { node.id = ++this.nextNodeId; @@ -283,12 +291,16 @@ export class Graph } const nodeType = this.idToObjectGroup.get(id); + this.idToObjectGroup.delete(id); for (const node of this.nodes) { if (node.type.id === nodeType.id) { node.type = this.objectGroups[0]; } } + this.objectGroups = this.objectGroups.filter( + (group) => group.id !== id + ); return true; } diff --git a/src/editor/components/nodedetails.tsx b/src/editor/components/nodedetails.tsx index 70083d9..6e29bd4 100644 --- a/src/editor/components/nodedetails.tsx +++ b/src/editor/components/nodedetails.tsx @@ -59,6 +59,7 @@ function NodeDetails({ Object.assign(referenceData, { [key]: value }); onNodeDataChange( + // Generate changes for individual nodes if multiple nodes are selected selectedNodes.map((node) => { const update = Object.fromEntries( Object.entries(referenceData).filter( diff --git a/src/editor/components/nodetypeentry.tsx b/src/editor/components/nodetypeentry.tsx index ca8b6f7..93fb425 100644 --- a/src/editor/components/nodetypeentry.tsx +++ b/src/editor/components/nodetypeentry.tsx @@ -1,155 +1,89 @@ -import React from "react"; -import { ReactNode } from "react"; -import { NodeType } from "../../common/graph/nodetype"; +import React, { useState } from "react"; +import { NodeType, NodeTypeData } from "../../common/graph/nodetype"; import "./nodetypeentry.css"; -import { DynamicGraph } from "../graph"; -type propTypes = { - graph: DynamicGraph; - type: NodeType; - onChange: { (): void }; - onSelectAll: (type: NodeType) => void; +type NodeTypeEntryProps = { + nodeType: NodeType; + onNodeTypeDelete: (id: number[]) => void; + onNodeTypeDataChange: ( + requests: NodeTypeData[], + createCheckpoint?: boolean + ) => void; + onNodeTypeSelect: (type: NodeType) => void; + enableDelete: boolean; }; -type stateTypes = { - temporaryColor: string; -}; - -export class NodeTypeEntry extends React.Component<propTypes, stateTypes> { - debounceTimeout: NodeJS.Timeout; - debounceArgs: any[]; - debounceFunc: any; - - constructor(props: propTypes) { - super(props); - this.deleteType = this.deleteType.bind(this); - this.handleTextChange = this.handleTextChange.bind(this); - this.handleColorChange = this.handleColorChange.bind(this); - - this.state = { - temporaryColor: undefined, - }; - } - private deleteType() { - //this.props.type.delete(); - } +function NodeTypeEntry({ + nodeType, + onNodeTypeDataChange, + onNodeTypeDelete, + onNodeTypeSelect, + enableDelete, +}: NodeTypeEntryProps) { + const [tmpColor, setTmpColor] = useState<string>(undefined); - private isValidColor(color: string): boolean { + const isValidColor = (color: string) => { if (color.length <= 0) { return false; } // Source: https://stackoverflow.com/a/8027444 return /^#([0-9A-F]{3}){1,2}$/i.test(color); - } - - private handleColorChange(event: any) { - const newColor = event.target.value; - - if (this.isValidColor(newColor)) { - // Is actual change? - if (this.props.type.color !== newColor) { - // Update proper color - this.props.type.color = newColor; - // this.props.type.graph.storeCurrentData( // TODO: Reimplement - // "Changed color of type [" + this.props.type + "]" - // ); - } - - // Reset temporary value - this.setState({ - temporaryColor: undefined, - }); + }; + + const referenceData: NodeTypeData = { + id: nodeType.id, + color: nodeType.color, + name: nodeType.name, + }; + + const handleDataChange = function <ValueType>( + key: keyof NodeTypeData, + value: ValueType + ) { + // if (!changed) { TODO: Move to nodetypeseditor + // setChanged(true); + // } + + Object.assign(referenceData, { [key]: value }); + onNodeTypeDataChange([referenceData], false); + }; + + const handleColorChange = (color: string) => { + if (isValidColor(color)) { + handleDataChange("color", color); + setTmpColor(undefined); } else { - // Only set as temporary value - this.setState({ - temporaryColor: newColor, - }); - } - - this.props.onChange(); - } - - /** - * Generic function for handeling a changing text input and applying the new value to the node type. - * @param event Change event of text input. - * @param property Property to give new value. - */ - private handleTextChange(event: any, property: string) { - const newValue = event.target.value; - - // Actual change? - if ((this.props.type as any)[property] == newValue) { - return; + setTmpColor(color); } - - (this.props.type as any)[property] = newValue; - this.props.onChange(); - - // Save change, but debounce, so it doesn't trigger too quickly - this.props.onChange(); - // this.debounce( - // (property: string) => { - // // this.props.type.graph.storeCurrentData( // TODO: Reimplement - // // "Changed " + property + " of type [" + this.props.type + "]" - // // ); - // this.props.onChange(); - // }, - // 500, - // property - // ); - } - - // debounce(func: any, wait: number, ...args: any[]) { - // // It works, don't question it - // const later = () => { - // this.debounceTimeout = null; - // this.debounceFunc(...this.debounceArgs); - // }; - // - // clearTimeout(this.debounceTimeout); - // if ( - // this.debounceArgs !== undefined && - // args[0] !== this.debounceArgs[0] && - // this.debounceFunc !== undefined - // ) { - // this.debounceFunc(...this.debounceArgs); - // } - // - // this.debounceArgs = args; - // this.debounceFunc = func; - // this.debounceTimeout = setTimeout(later, wait); - // } - - render(): ReactNode { - return ( - <li className="node-type"> - <input - className="node-type-name" - type={"text"} - value={this.props.type.name} - onChange={(event) => this.handleTextChange(event, "name")} - /> - <input - className="node-type-color" - type={"text"} - value={ - this.state.temporaryColor !== undefined - ? this.state.temporaryColor - : this.props.type.color - } - onChange={(event) => this.handleColorChange(event)} - /> - <button onClick={() => this.props.onSelectAll(this.props.type)}> - Select nodes + }; + + return ( + <li className="node-type"> + <input + className="node-type-name" + type={"text"} + value={nodeType.name} + onChange={(event) => + handleDataChange("name", event.target.value) + } + /> + <input + className="node-type-color" + type={"text"} + value={tmpColor !== undefined ? tmpColor : nodeType.color} + onChange={(event) => handleColorChange(event.target.value)} + /> + <button onClick={() => onNodeTypeSelect(nodeType)}> + Select nodes + </button> + {enableDelete && ( + <button onClick={() => onNodeTypeDelete([nodeType.id])}> + Delete </button> - {this.props.graph && - this.props.graph.objectGroups.length > 1 ? ( - <button onClick={this.deleteType}>Delete</button> - ) : ( - "" - )} - </li> - ); - } + )} + </li> + ); } + +export default NodeTypeEntry; diff --git a/src/editor/components/nodetypeseditor.tsx b/src/editor/components/nodetypeseditor.tsx index 65321bf..5eca976 100644 --- a/src/editor/components/nodetypeseditor.tsx +++ b/src/editor/components/nodetypeseditor.tsx @@ -1,47 +1,44 @@ import React from "react"; -import { ReactNode } from "react"; import "./nodetypeseditor.css"; -import { NodeTypeEntry } from "./nodetypeentry"; -import { NodeType } from "../../common/graph/nodetype"; -import { DynamicGraph } from "../graph"; +import NodeTypeEntry from "./nodetypeentry"; +import { NodeType, NodeTypeData } from "../../common/graph/nodetype"; -type propTypes = { - graph: DynamicGraph; - onChange: { (): void }; - onSelectAll: (type: NodeType) => void; +type NodeTypesEditorProps = { + nodeTypes: NodeType[]; + onNodeTypeSelect: (type: NodeType) => void; + onNodeTypeCreation: () => NodeType; + onNodeTypeDelete: (id: number[]) => void; + onNodeTypeDataChange: ( + requests: NodeTypeData[], + createCheckpoint?: boolean + ) => void; }; -export class NodeTypesEditor extends React.Component<propTypes> { - constructor(props: propTypes) { - super(props); - this.addType = this.addType.bind(this); - } - - private addType() { - this.props.graph.createObjectGroup(); - } - - render(): ReactNode { - if (this.props.graph === undefined) { - return "No graph selected."; - } - - return ( - <div id="node-types-editor"> - <h3>Node types</h3> - <ul> - {this.props.graph.objectGroups.map((type) => ( - <NodeTypeEntry - onChange={this.props.onChange} - key={type.id} - type={type} - graph={this.props.graph} - onSelectAll={this.props.onSelectAll} - /> - ))} - </ul> - <button onClick={this.addType}>Add type</button> - </div> - ); - } +function NodeTypesEditor({ + nodeTypes, + onNodeTypeSelect, + onNodeTypeCreation, + onNodeTypeDelete, + onNodeTypeDataChange, +}: NodeTypesEditorProps) { + return ( + <div id="node-types-editor"> + <h3>Node types</h3> + <ul> + {nodeTypes.map((nodeType) => ( + <NodeTypeEntry + key={nodeType.id} + nodeType={nodeType} + onNodeTypeSelect={onNodeTypeSelect} + onNodeTypeDelete={onNodeTypeDelete} + onNodeTypeDataChange={onNodeTypeDataChange} + enableDelete={nodeTypes.length > 1} + /> + ))} + </ul> + <button onClick={onNodeTypeCreation}>Add type</button> + </div> + ); } + +export default NodeTypesEditor; diff --git a/src/editor/components/sidepanel.tsx b/src/editor/components/sidepanel.tsx index 4022b64..16e8786 100644 --- a/src/editor/components/sidepanel.tsx +++ b/src/editor/components/sidepanel.tsx @@ -4,11 +4,11 @@ import "./sidepanel.css"; import HistoryNavigator from "./historynavigator"; import { DynamicGraph } from "../graph"; import NodeDetails from "./nodedetails"; -import { NodeTypesEditor } from "./nodetypeseditor"; +import NodeTypesEditor from "./nodetypeseditor"; import Settings from "./settings"; import Instructions from "./instructions"; import { Node } from "../../common/graph/node"; -import { NodeType } from "../../common/graph/nodetype"; +import { NodeType, NodeTypeData } from "../../common/graph/nodetype"; import { EditorSettings, NodeDataChangeRequest } from "../editor"; interface SidepanelProps { @@ -17,6 +17,12 @@ interface SidepanelProps { onRedo: () => void; onUndo: () => void; onNodeTypeSelect: (type: NodeType) => void; + onNodeTypeCreation: () => NodeType; + onNodeTypeDelete: (id: number[]) => void; + onNodeTypeDataChange: ( + requests: NodeTypeData[], + createCheckpoint?: boolean + ) => void; onSettingsChange: (settings: EditorSettings) => void; onNodeDataChange: ( requests: NodeDataChangeRequest[], @@ -34,6 +40,9 @@ function Sidepanel({ onUndo, onRedo, onNodeTypeSelect, + onNodeTypeCreation, + onNodeTypeDelete, + onNodeTypeDataChange, onSettingsChange, onNodeDataChange, onSave, @@ -61,11 +70,11 @@ function Sidepanel({ /> <hr /> <NodeTypesEditor - onChange={() => - console.log("Refactor onChange for nodetypes editor!") - } // TODO: Refactor - graph={graph} - onSelectAll={onNodeTypeSelect} + nodeTypes={graph.objectGroups} + onNodeTypeSelect={onNodeTypeSelect} + onNodeTypeCreation={onNodeTypeCreation} + onNodeTypeDelete={onNodeTypeDelete} + onNodeTypeDataChange={onNodeTypeDataChange} /> <hr /> <Settings settings={settings} onSettingsChange={onSettingsChange} /> diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx index 556bfe9..2e3a9ee 100644 --- a/src/editor/editor.tsx +++ b/src/editor/editor.tsx @@ -13,7 +13,7 @@ import { Node, NodeProperties } from "../common/graph/node"; import { SpaceManager } from "./components/spacemanager"; import SelectLayer from "./components/selectlayer"; import { Coordinate2D, GraphData, SimGraphData } from "../common/graph/graph"; -import { NodeType } from "../common/graph/nodetype"; +import { NodeType, NodeTypeData } from "../common/graph/nodetype"; import { GraphRenderer2D } from "./renderer"; import * as Config from "../config"; import Sidepanel from "./components/sidepanel"; @@ -80,6 +80,10 @@ export class Editor extends React.PureComponent<any, stateTypes> { this.saveSpace = this.saveSpace.bind(this); this.forceUpdate = this.forceUpdate.bind(this); this.handleNodeTypeSelect = this.handleNodeTypeSelect.bind(this); + this.handleNodeTypeDataChange = + this.handleNodeTypeDataChange.bind(this); + this.handleNodeTypeDeletion = this.handleNodeTypeDeletion.bind(this); + this.handleNodeTypeCreation = this.handleNodeTypeCreation.bind(this); this.handleBoxSelect = this.handleBoxSelect.bind(this); this.selectNodes = this.selectNodes.bind(this); this.handleNodeDataChange = this.handleNodeDataChange.bind(this); @@ -259,6 +263,34 @@ export class Editor extends React.PureComponent<any, stateTypes> { this.setState({ graph: graph }); } + private handleNodeTypeDataChange( + nodeTypeData: NodeTypeData[], + createCheckpoint = true + ) { + if (nodeTypeData.length == 0) { + return; + } + + // Create a shallow copy of the graph object to trigger an update over setState + const graph = Object.assign(new DynamicGraph(), this.state.graph); + + // Modify node type + for (const request of nodeTypeData) { + const node = graph.nodeType(request.id); + Object.assign(node, request); + } + + // Create checkpoint + if (createCheckpoint) { + graph.createCheckpoint( + `Modified ${nodeTypeData.length} node(s) data.` + ); + } + + // Push shallow copy to state + this.setState({ graph: graph }); + } + private handleNodeCreation( position?: Coordinate2D, createCheckpoint = true @@ -326,6 +358,29 @@ export class Editor extends React.PureComponent<any, stateTypes> { this.setState({ graph: graph }); } + private handleNodeTypeCreation(createCheckpoint = true): NodeType { + const graph = Object.assign(new DynamicGraph(), this.state.graph); + const nodeType = graph.createObjectGroup(); + + if (createCheckpoint) { + graph.createCheckpoint("Created new node type."); + } + + this.setState({ graph: graph }); + return nodeType; + } + + private handleNodeTypeDeletion(ids: number[], createCheckpoint = true) { + const graph = Object.assign(new DynamicGraph(), this.state.graph); + ids.forEach((id) => graph.deleteNodeType(id)); + + if (createCheckpoint) { + graph.createCheckpoint(`Deleted ${ids.length} node type(s).`); + } + + this.setState({ graph: graph }); + } + private loadGraphFromCheckpoint(checkpoint: Checkpoint<SimGraphData>) { const graph = new DynamicGraph(); graph.fromSerializedObject(checkpoint.data); @@ -425,6 +480,9 @@ export class Editor extends React.PureComponent<any, stateTypes> { onUndo={this.handleUndo} onRedo={this.handleRedo} onNodeTypeSelect={this.handleNodeTypeSelect} + onNodeTypeCreation={this.handleNodeTypeCreation} + onNodeTypeDelete={this.handleNodeTypeDeletion} + onNodeTypeDataChange={this.handleNodeTypeDataChange} onSettingsChange={(settings) => this.setState({ settings: settings }) } diff --git a/src/editor/graph.ts b/src/editor/graph.ts index 5d71e9f..0c87a85 100644 --- a/src/editor/graph.ts +++ b/src/editor/graph.ts @@ -42,7 +42,7 @@ export class DynamicGraph extends Common.Graph { public createObjectGroup(data?: NodeTypeData): NodeType { if (data == undefined) { data = { - id: 0, + id: ++this.nextObjectGroupId, name: "Unnamed", color: "#000000", }; -- GitLab