From f161b127e43dfbd5af83e41c6fc9087a2e95133c Mon Sep 17 00:00:00 2001
From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de>
Date: Fri, 9 Sep 2022 14:46:04 +0200
Subject: [PATCH] Prepared editor main component for the new data structure

---
 src/common/graph/node.ts |  22 +--
 src/common/history.ts    |   5 +-
 src/editor/editor.tsx    | 362 ++++++++++++++++-----------------------
 src/editor/graph.ts      |   6 +-
 4 files changed, 163 insertions(+), 232 deletions(-)

diff --git a/src/common/graph/node.ts b/src/common/graph/node.ts
index c3b8171..31a84be 100644
--- a/src/common/graph/node.ts
+++ b/src/common/graph/node.ts
@@ -52,10 +52,13 @@ export interface GraphNode extends NodeProperties {
     index?: number;
     x?: number;
     y?: number;
+    z?: number;
     vx?: number;
     vy?: number;
+    vz?: number;
     fx?: number;
     fy?: number;
+    fz?: number;
 }
 
 export class Node
@@ -77,10 +80,13 @@ export class Node
     public index?: number;
     public x?: number;
     public y?: number;
+    public z?: number;
     public vx?: number;
     public vy?: number;
+    public vz?: number;
     public fx?: number;
     public fy?: number;
+    public fz?: number;
 
     constructor(graph?: Graph) {
         super(0, graph);
@@ -89,12 +95,7 @@ export class Node
     }
 
     public setType(typeId: number) {
-        // Is it even different?
-        if (this.type.id === typeId) {
-            return;
-        }
-
-        const newType = this.graph.getType(typeId);
+        const newType = this.graph.nameToObjectGroup.get(typeId); // TODO
 
         // Exists?
         if (newType === undefined) {
@@ -102,15 +103,6 @@ export class Node
         }
 
         this.type = newType;
-
-        // Store change
-        this.graph.storeCurrentData(
-            "Set type [" +
-                newType.toString() +
-                "] for [" +
-                this.toString() +
-                "]"
-        );
     }
 
     public delete() {
diff --git a/src/common/history.ts b/src/common/history.ts
index 6e18bc9..2249bc0 100644
--- a/src/common/history.ts
+++ b/src/common/history.ts
@@ -14,13 +14,14 @@ export class History<HistoryDataType> {
 
     constructor(
         data: SerializableItem<unknown, HistoryDataType>,
-        maxCheckpoints = 20
+        maxCheckpoints = 20,
+        initialMessage = "New History"
     ) {
         this.data = data;
         this.maxCheckpoints = maxCheckpoints;
         this.checkpoints = [];
         this.currentCheckpoint = -1;
-        this.checkpoint("New History");
+        this.checkpoint(initialMessage);
     }
 
     checkpoint(description: string) {
diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx
index 5858bf5..8ef5ba1 100644
--- a/src/editor/editor.tsx
+++ b/src/editor/editor.tsx
@@ -1,5 +1,5 @@
 import React from "react";
-import { Graph } from "./graph";
+import { DynamicGraph } from "./graph";
 import { loadGraphJson } from "../common/datasets";
 import { NodeDetails } from "./components/nodedetails";
 import { SpaceSelect } from "./components/spaceselect";
@@ -12,7 +12,8 @@ import { Link } from "../common/graph/link";
 import { NodeTypesEditor } from "./components/nodetypeseditor";
 import { SpaceManager } from "./components/spacemanager";
 import { SelectLayer } from "./components/selectlayer";
-import { NodeType } from "../structures/graph/nodetype";
+import { GraphData } from "../common/graph/graph";
+import { NodeType } from "../common/graph/nodetype";
 
 type propTypes = {
     spaceId: string;
@@ -21,7 +22,7 @@ type stateTypes = {
     /**
      * Graph structure holding the basic information.
      */
-    graph: Graph;
+    graph: DynamicGraph;
 
     /**
      * Should labels on nodes be rendered, or none at all.
@@ -71,8 +72,8 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
     private maxDistanceToConnect = 15;
     private defaultWarmupTicks = 100;
     private warmupTicks = 100;
-    private renderer: any;
-    private graphContainer: any;
+    private renderer: React.RefObject<any>;
+    private graphContainer: React.RefObject<HTMLDivElement>;
 
     /**
      * True, if the graph was the target of the most recent click event.
@@ -87,12 +88,11 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         this.loadSpace = this.loadSpace.bind(this);
         this.extractPositions = this.extractPositions.bind(this);
         this.handleNodeClick = this.handleNodeClick.bind(this);
-        this.onHistoryChange = this.onHistoryChange.bind(this);
+        this.onGraphDataChange = this.onGraphDataChange.bind(this);
         this.handleEngineStop = this.handleEngineStop.bind(this);
         this.handleKeyDown = this.handleKeyDown.bind(this);
         this.handleKeyUp = this.handleKeyUp.bind(this);
         this.forceUpdate = this.forceUpdate.bind(this);
-        this.isHighlighted = this.isHighlighted.bind(this);
         this.handleNodeCanvasObject = this.handleNodeCanvasObject.bind(this);
         this.handleLinkCanvasObject = this.handleLinkCanvasObject.bind(this);
         this.handleBackgroundClick = this.handleBackgroundClick.bind(this);
@@ -111,7 +111,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
             graph: undefined,
             visibleLabels: true,
             connectOnDrag: false,
-            selectedNodes: undefined,
+            selectedNodes: [], // TODO: Why was undefined allowed here?
             keys: {},
             graphWidth: 1000,
         };
@@ -141,27 +141,23 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
      * @param data Serialized graph data.
      * @returns True, if successful.
      */
-    public loadGraph(data: any): boolean {
+    public loadGraph(data: GraphData): boolean {
         console.log("Starting to load new graph ...");
         console.log(data);
 
         // Create graph
-        const newGraph = Graph.parse(data);
+        const graph = new DynamicGraph();
+        graph.fromSerializedObject(data);
 
         this.warmupTicks = this.defaultWarmupTicks; // Should only run for each newly initialized graph, but then never again
 
-        // Is valid and parsed successfully?
-        if (newGraph === undefined) {
-            return false;
-        }
-
         // Set as new state
-        console.log(newGraph);
+        console.log(graph);
         this.setState({
-            graph: newGraph,
+            graph: graph,
         });
 
-        newGraph.onChangeCallbacks.push(this.onHistoryChange);
+        graph.onChangeCallbacks.push(this.onGraphDataChange);
 
         // Subscribe to global events
         document.onkeydown = this.handleKeyDown;
@@ -212,26 +208,15 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
      * Deletes all nodes currently selected. Handles store points accordingly of the number of deleted nodes.
      */
     private deleteSelectedNodes() {
-        if (this.selectedNodes === undefined) {
-            return; // Nothing to delete
-        }
-
-        if (this.selectedNodes.length == 1) {
-            this.selectedNodes[0].delete();
-            return;
-        }
+        const selectedNodes = this.state.selectedNodes;
 
-        // Delete multiple connected nodes
-        const count: number = this.selectedNodes.length;
-        try {
-            // Disable storing temporarily to create just one big change.
-            this.state.graph.disableStoring();
-            this.selectedNodes.forEach((node: Node) => node.delete());
-        } finally {
-            this.state.graph.enableStoring();
-            this.state.graph.storeCurrentData(
-                "Deleted " + count + " nodes and all connected links"
-            );
+        if (selectedNodes.length == 1) {
+            selectedNodes[0].delete();
+            selectedNodes.pop();
+            this.selectNodes(selectedNodes);
+        } else {
+            selectedNodes.forEach((node: Node) => node.delete());
+            this.deselect();
         }
     }
 
@@ -270,17 +255,14 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
      * Handler for background click event on force graph. Adds new node by default.
      * @param event Click event.
      */
-    private handleBackgroundClick(event: any, position: clickPosition) {
+    private handleBackgroundClick(event: MouseEvent, position: clickPosition) {
         this.graphInFocus = true;
 
         // Is there really no node there? Trying to prevent small error, where this event is triggered, even if there is a node.
-        const placeholderNode: Node = {
-            id: undefined,
-            x: position.graph.x,
-            y: position.graph.y,
-        } as unknown as Node;
-        const nearestNode =
-            this.state.graph.getClosestOtherNode(placeholderNode);
+        const nearestNode = this.state.graph.getClosestNode(
+            position.graph.x,
+            position.graph.y
+        );
         if (nearestNode !== undefined && nearestNode.distance < 4) {
             this.handleNodeClick(nearestNode.node);
             return;
@@ -293,66 +275,33 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         }
 
         // Add new node
-        const newNode = new Node();
-
-        newNode.name = "Unnamed";
-        (newNode as any).x = position.graph.x;
-        (newNode as any).y = position.graph.y;
-        (newNode as any).vx = 0;
-        (newNode as any).vy = 0;
-
-        newNode.add(this.state.graph);
-        this.forceUpdate();
+        const node = this.state.graph.createNode(
+            undefined,
+            position.graph.x,
+            position.graph.y,
+            0,
+            0
+        );
+        this.forceUpdate(); // TODO: Remove?
 
         // Select newly created node
         if (this.state.keys["Shift"]) {
             // Simply add to current selection of shift is pressed
-            this.toggleNodeSelection(newNode);
+            this.toggleNodeSelection(node);
         } else {
-            this.selectNode(newNode);
+            this.selectNode(node);
         }
     }
 
     /**
      * Propagates the changed state of the graph.
      */
-    private onHistoryChange() {
-        if (this.selectedNodes === undefined) {
-            this.selectNode(undefined);
-            this.forceUpdate();
-            return;
-        }
-
-        const nodes: Node[] = this.selectedNodes.map((node: Node) =>
-            this.state.graph.getNode(node.id)
+    private onGraphDataChange() {
+        const nodes: Node[] = this.state.selectedNodes.map((node: Node) =>
+            this.state.graph.node(node.id)
         );
         this.selectNodes(nodes);
-        this.forceUpdate();
-    }
-
-    /**
-     * Should a given element be highlighted in rendering or not.
-     * @param element Element that should, or should not be highlighted.
-     * @returns True, if element should be highlighted.
-     */
-    private isHighlighted(element: GraphElement): boolean {
-        if (this.selectedNodes == undefined || element == undefined) {
-            // Default to false if nothing selected.
-            return false;
-        }
-
-        if (element.node) {
-            // Is one of nodes
-            return this.selectedNodes.includes(element as Node);
-        } else if (element.link) {
-            // Is link
-            // Is it one of the adjacent links?
-            return this.selectedNodes.some((node: Node) =>
-                node.links.find(element.equals)
-            );
-        } else {
-            return false;
-        }
+        this.forceUpdate(); // TODO
     }
 
     /**
@@ -363,13 +312,17 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
     private extractPositions(event: any): clickPosition {
         return {
             graph: this.renderer.current.screen2GraphCoords(
-                event.layerX,
+                event.layerX, // TODO: Replace layerx/layery non standard properties and fix typing
                 event.layerY
             ),
             window: { x: event.clientX, y: event.clientY },
         };
     }
 
+    private deselect() {
+        this.setState({ selectedNodes: [] });
+    }
+
     /**
      * Selects a single node, or clears selection if given undefined.
      * @param node Single node to select, or undefined.
@@ -388,38 +341,36 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         });
     }
 
-    /**
-     * Makes sure to always offer a valid format of the selected nodes. Is either undefined or contains at least one valid node. An empty array is never returned.
-     */
-    private get selectedNodes(): Node[] {
-        if (this.state.selectedNodes === undefined) {
-            return undefined;
-        }
-
-        // Remove undefines
-        let selectedNodes = this.state.selectedNodes.filter(
-            (n: Node) => n !== undefined
-        );
-
-        // Remove duplicates
-        selectedNodes = [...new Set(selectedNodes)];
-
-        if (selectedNodes.length > 0) {
-            return selectedNodes;
-        }
-
-        return undefined;
-    }
+    // /**
+    //  * Makes sure to always offer a valid format of the selected nodes. Is either undefined or contains at least one valid node. An empty array is never returned.
+    //  */
+    // private get selectedNodes(): Node[] {
+    //     // TODO: Here are a lot of things that should not be possible by design
+    //
+    //     // Remove undefines
+    //     let selectedNodes = this.state.selectedNodes.filter(
+    //         (n: Node) => n !== undefined
+    //     );
+    //
+    //     // Remove duplicates
+    //     selectedNodes = [...new Set(selectedNodes)];
+    //
+    //     if (selectedNodes.length > 0) {
+    //         return selectedNodes;
+    //     }
+    //
+    //     return undefined;
+    // }
 
     private handleNodeClick(node: Node) {
         this.graphInFocus = true;
 
         if (this.state.keys["Control"]) {
             // Connect to clicked node as parent while control is pressed
-            if (this.selectedNodes == undefined) {
+            if (this.state.selectedNodes.length == 0) {
                 // Have no node connected, so select
                 this.selectNode(node);
-            } else if (!this.selectedNodes.includes(node)) {
+            } else if (!this.state.selectedNodes.includes(node)) {
                 // Already have *other* node/s selected, so connect
                 this.connectSelectionToNode(node);
             }
@@ -429,43 +380,26 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
             // By default, simply select node
             this.selectNode(node);
         }
-        this.forceUpdate();
+        this.forceUpdate(); // TODO: Remove?
     }
 
     private connectSelectionToNode(node: Node) {
-        if (this.selectedNodes === undefined) {
-            return;
-        }
-
-        if (this.selectedNodes.length == 1) {
-            node.connect(this.selectedNodes[0]);
+        if (this.state.selectedNodes.length == 0) {
             return;
         }
 
-        // More than one new link => custom save point handling
-        try {
-            this.state.graph.disableStoring();
-            this.selectedNodes.forEach((selectedNode: Node) =>
+        if (this.state.selectedNodes.length == 1) {
+            node.connect(this.state.selectedNodes[0]);
+        } else {
+            this.state.selectedNodes.forEach((selectedNode: Node) =>
                 node.connect(selectedNode)
             );
-        } finally {
-            this.state.graph.enableStoring();
-            this.state.graph.storeCurrentData(
-                "Added " +
-                    this.selectedNodes.length +
-                    " links on [" +
-                    node.toString() +
-                    "]"
-            );
         }
     }
 
     private toggleNodeSelection(node: Node) {
         // Convert selection to array as basis
-        let selection = this.selectedNodes;
-        if (selection === undefined) {
-            selection = [];
-        }
+        let selection = this.state.selectedNodes;
 
         // Add/Remove node
         if (selection.includes(node)) {
@@ -478,32 +412,24 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         this.selectNodes(selection);
     }
 
-    private handleNodeCanvasObject(node: Node, ctx: any, globalScale: any) {
+    private handleNodeCanvasObject(
+        node: Node,
+        ctx: CanvasRenderingContext2D,
+        globalScale: number
+    ) {
+        // TODO: Refactor
+
         // add ring just for highlighted nodes
-        if (this.isHighlighted(node)) {
+        if (this.state.selectedNodes.includes(node)) {
             // Outer circle
             ctx.beginPath();
-            ctx.arc(
-                (node as any).x,
-                (node as any).y,
-                4 * 0.7,
-                0,
-                2 * Math.PI,
-                false
-            );
+            ctx.arc(node.x, node.y, 4 * 0.7, 0, 2 * Math.PI, false);
             ctx.fillStyle = "white";
             ctx.fill();
 
             // Inner circle
             ctx.beginPath();
-            ctx.arc(
-                (node as any).x,
-                (node as any).y,
-                4 * 0.3,
-                0,
-                2 * Math.PI,
-                false
-            );
+            ctx.arc(node.x, node.y, 4 * 0.3, 0, 2 * Math.PI, false);
             ctx.fillStyle = node.type.color;
             ctx.fill();
         }
@@ -516,8 +442,8 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
 
             ctx.drawImage(
                 img,
-                (node as any).x - imageSize / 2,
-                (node as any).y - imageSize / 2,
+                node.x - imageSize / 2,
+                node.y - imageSize / 2,
                 imageSize,
                 imageSize
             );
@@ -529,47 +455,52 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
          * If this nodes is considered highlighted => Draw label
          * If this node is a neighbor of a selected node => Draw label
          */
-        const isNodeRelatedToSelection: boolean =
-            this.selectedNodes === undefined ||
-            this.isHighlighted(node) ||
-            this.selectedNodes.some((selectedNode: Node) =>
-                selectedNode.neighbors.includes(node)
-            );
-
-        if (this.state.visibleLabels && isNodeRelatedToSelection) {
-            const label = node.name;
-            const fontSize = 11 / globalScale;
-            ctx.font = `${fontSize}px Sans-Serif`;
-            const textWidth = ctx.measureText(label).width;
-            const bckgDimensions = [textWidth, fontSize].map(
-                (n) => n + fontSize * 0.2
-            ); // some padding
-
-            const nodeHeightOffset = imageSize / 3 + bckgDimensions[1];
-            ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
-            ctx.fillRect(
-                (node as any).x - bckgDimensions[0] / 2,
-                (node as any).y - bckgDimensions[1] / 2 + nodeHeightOffset,
-                ...bckgDimensions
-            );
-
-            ctx.textAlign = "center";
-            ctx.textBaseline = "middle";
-            ctx.fillStyle = "white";
-            ctx.fillText(
-                label,
-                (node as any).x,
-                (node as any).y + nodeHeightOffset
-            );
-        }
+        // TODO: Reenable node label rendering
+        // const isNodeRelatedToSelection: boolean =
+        //     this.state.selectedNodes.length != 0 ||
+        //     this.isHighlighted(node) ||
+        //     this.selectedNodes.some((selectedNode: Node) =>
+        //         selectedNode.neighbors.includes(node)
+        //     );
+        //
+        // if (this.state.visibleLabels && isNodeRelatedToSelection) {
+        //     const label = node.name;
+        //     const fontSize = 11 / globalScale;
+        //     ctx.font = `${fontSize}px Sans-Serif`;
+        //     const textWidth = ctx.measureText(label).width;
+        //     const bckgDimensions = [textWidth, fontSize].map(
+        //         (n) => n + fontSize * 0.2
+        //     ); // some padding
+        //
+        //     const nodeHeightOffset = imageSize / 3 + bckgDimensions[1];
+        //     ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
+        //     ctx.fillRect(
+        //         (node as any).x - bckgDimensions[0] / 2,
+        //         (node as any).y - bckgDimensions[1] / 2 + nodeHeightOffset,
+        //         ...bckgDimensions
+        //     );
+        //
+        //     ctx.textAlign = "center";
+        //     ctx.textBaseline = "middle";
+        //     ctx.fillStyle = "white";
+        //     ctx.fillText(
+        //         label,
+        //         (node as any).x,
+        //         (node as any).y + nodeHeightOffset
+        //     );
+        // }
 
         // TODO: Render label as always visible
     }
 
-    private handleLinkCanvasObject(link: any, ctx: any, globalScale: any): any {
+    private handleLinkCanvasObject(
+        link: Link,
+        ctx: CanvasRenderingContext2D,
+        globalScale: number
+    ) {
         // Links already initialized?
         if (link.source.x === undefined) {
-            return undefined;
+            return;
         }
 
         // Draw gradient link
@@ -581,11 +512,15 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         );
         // Have reversed colors
         // Color at source node referencing the target node and vice versa
-        gradient.addColorStop("0", link.target.type.color);
-        gradient.addColorStop("1", link.source.type.color);
+        gradient.addColorStop(0, link.target.type.color);
+        gradient.addColorStop(1, link.source.type.color);
 
         let lineWidth = 0.5;
-        if (this.isHighlighted(link)) {
+        if (
+            this.state.selectedNodes.some((node: Node) =>
+                node.links.find(link.equals)
+            )
+        ) {
             lineWidth = 2;
         }
         lineWidth /= globalScale; // Scale with zoom
@@ -596,8 +531,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         ctx.strokeStyle = gradient;
         ctx.lineWidth = lineWidth;
         ctx.stroke();
-
-        return undefined;
     }
 
     private handleNodeTypeSelect(type: NodeType) {
@@ -610,7 +543,10 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
     private handleNodeDrag(node: Node) {
         this.graphInFocus = true;
 
-        if (!this.selectedNodes || !this.selectedNodes.includes(node)) {
+        if (
+            !this.state.selectedNodes ||
+            !this.state.selectedNodes.includes(node)
+        ) {
             this.selectNode(node);
         }
 
@@ -619,7 +555,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
             return;
         }
 
-        const closest = this.state.graph.getClosestOtherNode(node);
+        const closest = this.state.graph.getClosestNode(node.x, node.y, node);
 
         // Is close enough for new link?
         if (closest.distance > this.maxDistanceToConnect) {
@@ -639,11 +575,11 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
     /**
      * Processes right-click event on graph elements by deleting them.
      */
-    private handleElementRightClick(element: GraphElement) {
+    private handleElementRightClick(element: GraphElement<unknown, unknown>) {
         this.graphInFocus = true;
 
         element.delete();
-        this.forceUpdate();
+        this.forceUpdate(); // TODO: Necessary?
     }
 
     private handleEngineStop() {
@@ -653,7 +589,6 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
         }
 
         this.warmupTicks = 0; // Only warm up once, so stop warming up after the first freeze
-        this.state.graph.storeCurrentData("Initial state", false);
 
         this.forceUpdate();
     }
@@ -663,7 +598,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
             return;
         }
 
-        this.selectNodes(selectedNodes.concat(this.selectedNodes));
+        this.selectNodes(selectedNodes.concat(this.state.selectedNodes));
     }
 
     render(): React.ReactNode {
@@ -690,10 +625,7 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
                                 <ForceGraph2D
                                     ref={this.renderer}
                                     width={this.state.graphWidth}
-                                    graphData={{
-                                        nodes: this.state.graph.data.nodes,
-                                        links: this.state.graph.links,
-                                    }}
+                                    graphData={this.state.graph}
                                     onNodeClick={this.handleNodeClick}
                                     autoPauseRedraw={false}
                                     cooldownTicks={0}
@@ -729,13 +661,15 @@ export class Editor extends React.PureComponent<propTypes, stateTypes> {
                         <HistoryNavigator
                             spaceId="space"
                             historyObject={this.state.graph}
-                            onChange={this.onHistoryChange}
+                            onChange={this.onGraphDataChange}
                         />
                         <hr />
                         <NodeDetails
-                            selectedNodes={this.selectedNodes}
+                            selectedNodes={this.state.selectedNodes}
                             allTypes={
-                                this.state.graph ? this.state.graph.types : []
+                                this.state.graph
+                                    ? this.state.graph.objectGroups
+                                    : []
                             }
                             onChange={this.forceUpdate}
                         />
diff --git a/src/editor/graph.ts b/src/editor/graph.ts
index 3c5bbc3..4a74b5f 100644
--- a/src/editor/graph.ts
+++ b/src/editor/graph.ts
@@ -14,7 +14,11 @@ export class DynamicGraph extends Common.Graph {
     constructor(data?: GraphContent) {
         super(data);
         this.onChangeCallbacks = [];
-        this.history = new History<SimGraphData>(this);
+        this.history = new History<SimGraphData>(
+            this,
+            20,
+            "Created new graph."
+        );
     }
 
     /**
-- 
GitLab