From ac80b943a22cdde0252e1a46aea72293a3c1ac21 Mon Sep 17 00:00:00 2001
From: Matthias Konitzny <konitzny@ibr.cs.tu-bs.de>
Date: Wed, 14 Sep 2022 14:46:35 +0200
Subject: [PATCH] Reworking select layer - WIP commit

---
 src/editor/components/selectlayer.tsx | 361 +++++++++++++++++---------
 src/editor/editor.tsx                 |  12 +-
 src/editor/renderer.tsx               |  22 +-
 3 files changed, 245 insertions(+), 150 deletions(-)

diff --git a/src/editor/components/selectlayer.tsx b/src/editor/components/selectlayer.tsx
index 9b7872d..9b3a86e 100644
--- a/src/editor/components/selectlayer.tsx
+++ b/src/editor/components/selectlayer.tsx
@@ -1,70 +1,44 @@
-import React from "react";
-import { ReactNode } from "react";
+import React, { useState } from "react";
 import { Node } from "../../common/graph/node";
 import "./selectlayer.css";
+import { Coordinate2D } from "../../common/graph/graph";
 
-type propTypes = {
-    children: any;
-    allNodes: Node[];
+interface SelectLayerProps {
+    children: React.ReactNode | React.ReactNode[];
+    nodes: Node[];
     isEnabled: boolean;
-    screen2GraphCoords: (x: number, y: number) => any;
+    screen2GraphCoords: (x: number, y: number) => Coordinate2D;
     onBoxSelect: (nodes: Node[]) => void;
-};
-
-type layerCoordinates = {
-    x: number;
-    y: number;
-};
-
-export class SelectLayer extends React.Component<propTypes> {
-    private layerContainer: any;
-    private layerBox: any;
-    private initialSelectPoint: layerCoordinates = undefined;
-
-    constructor(props: propTypes) {
-        super(props);
-
-        this.isSelecting = this.isSelecting.bind(this);
-        this.onBoxSelect = this.onBoxSelect.bind(this);
-        this.boxSelectOnPointerDown = this.boxSelectOnPointerDown.bind(this);
-        this.boxSelectOnPointerMove = this.boxSelectOnPointerMove.bind(this);
-        this.boxSelectOnPointerUp = this.boxSelectOnPointerUp.bind(this);
-
-        this.layerContainer = React.createRef();
-        this.layerBox = React.createRef();
-    }
-
-    componentDidMount(): void {
-        this.setupBoxSelect();
-    }
-
-    setupBoxSelect() {
-        // Source: https://github.com/vasturiano/force-graph/issues/151#issuecomment-735850938
-        this.layerContainer.current.onpointerdown = this.boxSelectOnPointerDown;
-        this.layerContainer.current.onpointermove = this.boxSelectOnPointerMove;
-        this.layerContainer.current.onpointerup = this.boxSelectOnPointerUp;
-    }
+}
 
-    private isSelecting(): boolean {
-        if (!this.initialSelectPoint) {
-            return false;
-        }
+interface Box {
+    bottom: number;
+    top: number;
+    left: number;
+    right: number;
+}
 
-        if (!this.props.isEnabled) {
-            this.initialSelectPoint = undefined;
-            this.layerBox.current.className = "";
-            return false;
-        }
+function SelectLayer({
+    children,
+    nodes,
+    onBoxSelect,
+    isEnabled,
+    screen2GraphCoords,
+}: SelectLayerProps) {
+    const [selectionStart, setSelectionStart] =
+        useState<Coordinate2D>(undefined);
+    const [selectionEnd, setSelectionEnd] = useState<Coordinate2D>(undefined);
 
-        return true;
+    if (!isEnabled) {
+        return <>{children}</>;
     }
 
-    onBoxSelect(left: number, bottom: number, top: number, right: number) {
+    const makeSelection = (bb: Box) => {
         // Filter out selected nodes
         const hitNodes: Node[] = [];
-        const tl = this.props.screen2GraphCoords(left, top);
-        const br = this.props.screen2GraphCoords(right, bottom);
-        this.props.allNodes.forEach((node: any) => {
+        const tl = screen2GraphCoords(bb.left, bb.top);
+        const br = screen2GraphCoords(bb.right, bb.bottom);
+        nodes.forEach((node: Node) => {
             if (
                 tl.x < node.x &&
                 node.x < br.x &&
@@ -75,87 +49,216 @@ export class SelectLayer extends React.Component<propTypes> {
                 hitNodes.push(node);
             }
         });
+        onBoxSelect(hitNodes);
+    };
 
-        this.props.onBoxSelect(hitNodes);
-    }
-
-    boxSelectOnPointerDown(e: any) {
-        if (!this.props.isEnabled) {
-            return;
-        }
-
-        e.preventDefault();
-        this.layerBox.current.style.left = e.offsetX.toString() + "px";
-        this.layerBox.current.style.top = e.offsetY.toString() + "px";
-        this.layerBox.current.style.width = "0px";
-        this.layerBox.current.style.height = "0px";
-        this.initialSelectPoint = {
-            x: e.offsetX,
-            y: e.offsetY,
+    const getBoundingBox = (p1: Coordinate2D, p2: Coordinate2D): Box => {
+        return {
+            left: Math.min(p1.x, p2.x),
+            top: Math.min(p1.y, p2.y),
+            right: Math.max(p1.x, p2.x),
+            bottom: Math.max(p1.x, p2.x),
         };
-        this.layerBox.current.className = "visible";
-    }
+    };
 
-    boxSelectOnPointerMove(e: any) {
-        if (!this.isSelecting()) {
-            return;
-        }
+    const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
+        e.preventDefault();
+        makeSelection(getBoundingBox(selectionStart, selectionEnd));
+        setSelectionStart(undefined);
+        setSelectionEnd(undefined);
+    };
 
+    const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
         e.preventDefault();
-        if (e.offsetX < this.initialSelectPoint.x) {
-            this.layerBox.current.style.left = e.offsetX.toString() + "px";
-            this.layerBox.current.style.width =
-                (this.initialSelectPoint.x - e.offsetX).toString() + "px";
-        } else {
-            this.layerBox.current.style.left =
-                this.initialSelectPoint.x.toString() + "px";
-            this.layerBox.current.style.width =
-                (e.offsetX - this.initialSelectPoint.x).toString() + "px";
-        }
-        if (e.offsetY < this.initialSelectPoint.y) {
-            this.layerBox.current.style.top = e.offsetY.toString() + "px";
-            this.layerBox.current.style.height =
-                (this.initialSelectPoint.y - e.offsetY).toString() + "px";
-        } else {
-            this.layerBox.current.style.top =
-                this.initialSelectPoint.y.toString() + "px";
-            this.layerBox.current.style.height =
-                (e.offsetY - this.initialSelectPoint.y).toString() + "px";
-        }
-    }
+        setSelectionStart({ x: e.screenX, y: e.screenY });
+        setSelectionEnd({ x: e.screenX, y: e.screenY });
+    };
 
-    boxSelectOnPointerUp(e: any) {
-        if (!this.isSelecting()) {
-            return;
-        }
+    const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
+        setSelectionEnd({ x: e.screenX, y: e.screenY });
+    };
 
-        e.preventDefault();
-        let left, bottom, top, right;
-        if (e.offsetX < this.initialSelectPoint.x) {
-            left = e.offsetX;
-            right = this.initialSelectPoint.x;
-        } else {
-            left = this.initialSelectPoint.x;
-            right = e.offsetX;
-        }
-        if (e.offsetY < this.initialSelectPoint.y) {
-            top = e.offsetY;
-            bottom = this.initialSelectPoint.y;
-        } else {
-            top = this.initialSelectPoint.y;
-            bottom = e.offsetY;
-        }
-        this.initialSelectPoint = undefined;
-        this.layerBox.current.className = "";
-        this.onBoxSelect(left, bottom, top, right);
-    }
+    let width = 0;
+    let height = 0;
+    let left = 0;
+    let top = 0;
 
-    render(): ReactNode {
-        return (
-            <div ref={this.layerContainer} id="select-layer">
-                <div ref={this.layerBox} id="box-select"></div>
-                {this.props.children}
-            </div>
-        );
+    if (selectionStart && selectionEnd) {
+        const box = getBoundingBox(selectionStart, selectionEnd);
+        width = box.right - box.left;
+        height = box.bottom - box.top;
+        left = box.left;
+        top = box.top;
     }
+
+    return (
+        <div
+            onPointerDown={handlePointerDown}
+            onPointerMove={handlePointerMove}
+            onPointerUp={handlePointerUp}
+            id="select-layer"
+        >
+            <div
+                id="box-select"
+                className={selectionStart ? "visible" : ""}
+                style={{ width: width, height: height, left: left, top: top }}
+            ></div>
+            {children}
+        </div>
+    );
 }
+
+export default SelectLayer;
+
+// export class SelectLayer extends React.Component<propTypes> {
+//     private layerContainer: React.RefObject<HTMLDivElement>;
+//     private layerBox: React.RefObject<HTMLDivElement>;
+//     private initialSelectPoint: layerCoordinates = undefined;
+//
+//     constructor(props: propTypes) {
+//         super(props);
+//
+//         this.isSelecting = this.isSelecting.bind(this);
+//         this.onBoxSelect = this.onBoxSelect.bind(this);
+//         this.boxSelectOnPointerDown = this.boxSelectOnPointerDown.bind(this);
+//         this.boxSelectOnPointerMove = this.boxSelectOnPointerMove.bind(this);
+//         this.boxSelectOnPointerUp = this.boxSelectOnPointerUp.bind(this);
+//
+//         this.layerContainer = React.createRef();
+//         this.layerBox = React.createRef();
+//     }
+//
+//     componentDidMount(): void {
+//         this.setupBoxSelect();
+//     }
+//
+//     setupBoxSelect() {
+//         // Source: https://github.com/vasturiano/force-graph/issues/151#issuecomment-735850938
+//         this.layerContainer.current.addEventListener(
+//             "pointerdown",
+//             this.boxSelectOnPointerDown
+//         );
+//         this.layerContainer.current.addEventListener(
+//             "pointermove",
+//             this.boxSelectOnPointerMove
+//         );
+//         this.layerContainer.current.addEventListener(
+//             "pointerup",
+//             this.boxSelectOnPointerUp
+//         );
+//     }
+//
+//     private isSelecting(): boolean {
+//         if (!this.initialSelectPoint) {
+//             return false;
+//         }
+//
+//         if (!this.props.isEnabled) {
+//             this.initialSelectPoint = undefined;
+//             this.layerBox.current.className = "";
+//             return false;
+//         }
+//
+//         return true;
+//     }
+//
+//     onBoxSelect(left: number, bottom: number, top: number, right: number) {
+//         // Filter out selected nodes
+//         const hitNodes: Node[] = [];
+//         const tl = this.props.screen2GraphCoords(left, top);
+//         const br = this.props.screen2GraphCoords(right, bottom);
+//         this.props.allNodes.forEach((node: Node) => {
+//             if (
+//                 tl.x < node.x &&
+//                 node.x < br.x &&
+//                 br.y > node.y &&
+//                 node.y > tl.y
+//             ) {
+//                 // Add node if in box area
+//                 hitNodes.push(node);
+//             }
+//         });
+//
+//         this.props.onBoxSelect(hitNodes);
+//     }
+//
+//     boxSelectOnPointerDown(e: PointerEvent) {
+//         if (!this.props.isEnabled) {
+//             return;
+//         }
+//
+//         e.preventDefault();
+//         this.layerBox.current.style.left = e.offsetX.toString() + "px";
+//         this.layerBox.current.style.top = e.offsetY.toString() + "px";
+//         this.layerBox.current.style.width = "0px";
+//         this.layerBox.current.style.height = "0px";
+//         this.initialSelectPoint = {
+//             x: e.offsetX,
+//             y: e.offsetY,
+//         };
+//         this.layerBox.current.className = "visible";
+//     }
+//
+//     boxSelectOnPointerMove(e: PointerEvent) {
+//         if (!this.isSelecting()) {
+//             return;
+//         }
+//
+//         e.preventDefault();
+//         if (e.offsetX < this.initialSelectPoint.x) {
+//             this.layerBox.current.style.left = e.offsetX.toString() + "px";
+//             this.layerBox.current.style.width =
+//                 (this.initialSelectPoint.x - e.offsetX).toString() + "px";
+//         } else {
+//             this.layerBox.current.style.left =
+//                 this.initialSelectPoint.x.toString() + "px";
+//             this.layerBox.current.style.width =
+//                 (e.offsetX - this.initialSelectPoint.x).toString() + "px";
+//         }
+//         if (e.offsetY < this.initialSelectPoint.y) {
+//             this.layerBox.current.style.top = e.offsetY.toString() + "px";
+//             this.layerBox.current.style.height =
+//                 (this.initialSelectPoint.y - e.offsetY).toString() + "px";
+//         } else {
+//             this.layerBox.current.style.top =
+//                 this.initialSelectPoint.y.toString() + "px";
+//             this.layerBox.current.style.height =
+//                 (e.offsetY - this.initialSelectPoint.y).toString() + "px";
+//         }
+//     }
+//
+//     boxSelectOnPointerUp(e: PointerEvent) {
+//         if (!this.isSelecting()) {
+//             return;
+//         }
+//
+//         e.preventDefault();
+//         let left, bottom, top, right;
+//         if (e.offsetX < this.initialSelectPoint.x) {
+//             left = e.offsetX;
+//             right = this.initialSelectPoint.x;
+//         } else {
+//             left = this.initialSelectPoint.x;
+//             right = e.offsetX;
+//         }
+//         if (e.offsetY < this.initialSelectPoint.y) {
+//             top = e.offsetY;
+//             bottom = this.initialSelectPoint.y;
+//         } else {
+//             top = this.initialSelectPoint.y;
+//             bottom = e.offsetY;
+//         }
+//         this.initialSelectPoint = undefined;
+//         this.layerBox.current.className = "";
+//         this.onBoxSelect(left, bottom, top, right);
+//     }
+//
+//     render(): ReactNode {
+//         return (
+//             <div ref={this.layerContainer} id="select-layer">
+//                 <div ref={this.layerBox} id="box-select"></div>
+//                 {this.props.children}
+//             </div>
+//         );
+//     }
+// }
diff --git a/src/editor/editor.tsx b/src/editor/editor.tsx
index 73807c4..da04c04 100644
--- a/src/editor/editor.tsx
+++ b/src/editor/editor.tsx
@@ -7,7 +7,7 @@ import * as Helpers from "../common/helpers";
 import { Node, NodeProperties } from "../common/graph/node";
 
 import { SpaceManager } from "./components/spacemanager";
-import { SelectLayer } from "./components/selectlayer";
+import SelectLayer from "./components/selectlayer";
 import { GraphData } from "../common/graph/graph";
 import { NodeType } from "../common/graph/nodetype";
 import { GraphRenderer2D } from "./renderer";
@@ -107,13 +107,13 @@ export class Editor extends React.PureComponent<any, stateTypes> {
     keyPressed(key: string) {
         const keys = this.state.keys;
         keys[key] = true;
-        this.setState({ keys: keys });
+        this.setState({ keys: { ...keys } });
     }
 
     keyReleased(key: string) {
         const keys = this.state.keys;
         keys[key] = false;
-        this.setState({ keys: keys });
+        this.setState({ keys: { ...keys } });
     }
 
     /**
@@ -230,11 +230,7 @@ export class Editor extends React.PureComponent<any, stateTypes> {
                             ref={this.graphContainer}
                         >
                             <SelectLayer
-                                allNodes={
-                                    this.state.graph
-                                        ? this.state.graph.nodes
-                                        : []
-                                }
+                                nodes={this.state.graph.nodes}
                                 screen2GraphCoords={
                                     this.rendererRef
                                         ? this.rendererRef.current
diff --git a/src/editor/renderer.tsx b/src/editor/renderer.tsx
index 67be19d..831d396 100644
--- a/src/editor/renderer.tsx
+++ b/src/editor/renderer.tsx
@@ -108,10 +108,10 @@ export class GraphRenderer2D extends React.PureComponent<
 
         if (this.keys["Control"]) {
             // Connect to clicked node as parent while control is pressed
-            if (this.state.selectedNodes.length == 0) {
+            if (this.props.selectedNodes.length == 0) {
                 // Have no node connected, so select
                 this.props.onNodeSelectionChanged([node]);
-            } else if (!this.state.selectedNodes.includes(node)) {
+            } else if (!this.props.selectedNodes.includes(node)) {
                 // Already have *other* node/s selected, so connect
                 this.connectSelectionToNode(node);
             }
@@ -121,7 +121,6 @@ export class GraphRenderer2D extends React.PureComponent<
             // By default, simply select node
             this.props.onNodeSelectionChanged([node]);
         }
-        //this.forceUpdate(); // TODO: Remove?
     }
 
     /**
@@ -216,7 +215,7 @@ export class GraphRenderer2D extends React.PureComponent<
             // Add node to selection
             selection.push(node);
         }
-        this.props.onNodeSelectionChanged(selection);
+        this.props.onNodeSelectionChanged([...selection]);
     }
 
     private handleEngineStop() {
@@ -231,19 +230,16 @@ export class GraphRenderer2D extends React.PureComponent<
     private handleNodeDrag(node: Node) {
         this.graphInFocus = true;
 
-        if (
-            !this.state.selectedNodes ||
-            !this.state.selectedNodes.includes(node)
-        ) {
-            this.props.onNodeSelectionChanged([node]);
-        }
+        // if (!this.props.selectedNodes.includes(node)) {
+        //     this.props.onNodeSelectionChanged([...this.props.selectedNodes, node]);
+        // }
 
         // Should run connect logic?
-        if (!this.state.connectOnDrag) {
+        if (!this.props.connectOnDrag) {
             return;
         }
 
-        const closest = this.state.graph.getClosestNode(node.x, node.y, node);
+        const closest = this.props.graph.getClosestNode(node.x, node.y, node);
 
         // Is close enough for new link?
         if (closest.distance > this.maxDistanceToConnect) {
@@ -256,7 +252,7 @@ export class GraphRenderer2D extends React.PureComponent<
         }
 
         // Add link
-        node.connect(closest.node);
+        node.connect(closest.node); // TODO: Change must propagate
         // this.forceUpdate(); TODO: Remove?
     }
 
-- 
GitLab