From effc46719be24e503336cc3222c2a2c75c559248 Mon Sep 17 00:00:00 2001
From: Max <m.giller.dev@gmail.com>
Date: Mon, 19 Jul 2021 13:42:06 +0200
Subject: [PATCH] Unod/redo prototype

---
 editor/editor.html           |   3 +
 editor/images/tools/redo.png | Bin 0 -> 3114 bytes
 editor/images/tools/undo.png | Bin 0 -> 3142 bytes
 editor/js/editor.js          |   9 +-
 editor/js/graph.js           | 156 +++++++++++++++++++++++------------
 editor/js/manageddata.js     |  55 ++++++++++++
 editor/js/state.js           |   2 +
 editor/js/tools/redotool.js  |  10 +++
 editor/js/tools/undotool.js  |  10 +++
 9 files changed, 188 insertions(+), 57 deletions(-)
 create mode 100644 editor/images/tools/redo.png
 create mode 100644 editor/images/tools/undo.png
 create mode 100644 editor/js/manageddata.js
 create mode 100644 editor/js/tools/redotool.js
 create mode 100644 editor/js/tools/undotool.js

diff --git a/editor/editor.html b/editor/editor.html
index 69be217..e490ec0 100644
--- a/editor/editor.html
+++ b/editor/editor.html
@@ -24,8 +24,11 @@
         const PLUGIN_PATH = "%WWW%";
     </script>
 
+    <script src="%WWW%editor/js/manageddata.js"></script>
     <script src="%WWW%editor/js/graph.js"></script>
     <script src="%WWW%editor/js/tools/tool.js"></script>
+    <script src="%WWW%editor/js/tools/undotool.js"></script>
+    <script src="%WWW%editor/js/tools/redotool.js"></script>
     <script src="%WWW%editor/js/tools/selecttool.js"></script>
     <script src="%WWW%editor/js/tools/collecttool.js"></script>
     <script src="%WWW%editor/js/tools/deletetool.js"></script>
diff --git a/editor/images/tools/redo.png b/editor/images/tools/redo.png
new file mode 100644
index 0000000000000000000000000000000000000000..a0937d23d5ccfe9dc7b4eee4252a9fd55b1277aa
GIT binary patch
literal 3114
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
Ef(;JS)&Kwi

literal 0
HcmV?d00001

diff --git a/editor/images/tools/undo.png b/editor/images/tools/undo.png
new file mode 100644
index 0000000000000000000000000000000000000000..954ac5f4ae8c75c2c2e23cb4d115828af8284e55
GIT binary patch
literal 3142
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

literal 0
HcmV?d00001

diff --git a/editor/js/editor.js b/editor/js/editor.js
index be00eea..493f70b 100644
--- a/editor/js/editor.js
+++ b/editor/js/editor.js
@@ -1,4 +1,5 @@
 var state;
+var graph;
 var graphObj;
 
 window.onload = function () {
@@ -8,9 +9,7 @@ window.onload = function () {
         })
         .then((graphConfig) => {
             state = new State();
-
-            graph.data = graphConfig;
-            graph.addIdentifiers();
+            graph = new Graph(graphConfig);
             load();
 
             // Deactivate physics after a short delay
@@ -64,7 +63,7 @@ function load() {
         .nodeCanvasObject((node, ctx) => state.nodeCanvasObject(node, ctx))
         .onLinkClick((link) => state.onLinkClick(link));
 
-    graph.externUpdate.push(() => {
-        graphObj.graphData(graph.data);
+    graph.onChangeCallbacks.push((data) => {
+        graphObj.graphData(data);
     });
 }
diff --git a/editor/js/graph.js b/editor/js/graph.js
index 0f98608..a11bdb4 100644
--- a/editor/js/graph.js
+++ b/editor/js/graph.js
@@ -22,54 +22,77 @@ const JSON_CONFIG = PLUGIN_PATH + "datasets/aud1.json";
 
 const STOP_PHYSICS_DELAY = 5000; // ms
 
-const graph = {
-    data: undefined,
-    externUpdate: [],   // Register callbacks in this list
+class Graph extends ManagedData {
+    constructor(data) {
+        super(Graph.addIdentifiers(data));
 
-    update() {
-        graph.externUpdate.forEach((fn) => fn());
-    },
+        this.onChangeCallbacks = [];
+    }
+
+    triggerOnChange() {
+        this.onChangeCallbacks.forEach((fn) => fn(this.data));
+    }
+
+    onRedo() {
+        this.triggerOnChange();
+    }
+
+    onUndo() {
+        this.triggerOnChange();
+    }
+
+    formatData(data) {
+        return this.getCleanData(data, positions = true);
+    }
 
     deleteNode(nodeId) {
         // Delete node from nodes
-        graph.data[GRAPH_NODES] = graph.data[GRAPH_NODES].filter(
+        this.data[GRAPH_NODES] = this.data[GRAPH_NODES].filter(
             (n) => n[NODE_ID] !== nodeId
         );
 
         // Delete links with node
-        graph.data[GRAPH_LINKS] = graph.data[GRAPH_LINKS].filter(
+        this.data[GRAPH_LINKS] = this.data[GRAPH_LINKS].filter(
             (l) =>
                 l[LINK_SOURCE][NODE_ID] !== nodeId &&
                 l[LINK_TARGET][NODE_ID] !== nodeId
         );
-    },
+
+        this.storeCurrentData("Deleted node with id [" + nodeId + "]");
+    }
 
     stopPhysics() {
-        graph.data[GRAPH_NODES].forEach((n) => {
+        this.data[GRAPH_NODES].forEach((n) => {
             n.fx = n.x;
             n.fy = n.y;
         });
-    },
+    }
 
-    addIdentifiers() {
-        graph.data[GRAPH_NODES].forEach((n) => {
+    static addIdentifiers(data) {
+        data[GRAPH_NODES].forEach((n) => {
             n.node = true;
             n.link = false;
         });
-        graph.data[GRAPH_LINKS].forEach((l) => {
+        data[GRAPH_LINKS].forEach((l) => {
             l.node = false;
             l.link = true;
         });
-    },
+
+        return data;
+    }
 
     deleteLink(sourceId, targetId) {
         // Only keep links, of one of the nodes is different
-        graph.data[GRAPH_LINKS] = graph.data[GRAPH_LINKS].filter(
+        this.data[GRAPH_LINKS] = this.data[GRAPH_LINKS].filter(
             (l) =>
                 l[LINK_SOURCE][NODE_ID] !== sourceId ||
                 l[LINK_TARGET][NODE_ID] !== targetId
         );
-    },
+
+        this.storeCurrentData(
+            "Deleted link connecting [" + sourceId + "] with [" + targetId + "]"
+        );
+    }
 
     isLinkOnNode(link, node) {
         if (link === undefined || node === undefined) {
@@ -84,10 +107,10 @@ const graph = {
             link[LINK_SOURCE][NODE_ID] === node[NODE_ID] ||
             link[LINK_TARGET][NODE_ID] === node[NODE_ID]
         );
-    },
+    }
 
     existsLink(sourceId, targetId) {
-        const links = graph.data[GRAPH_LINKS];
+        const links = this.data[GRAPH_LINKS];
 
         for (var i = 0; i < links.length; i++) {
             var link = links[i];
@@ -100,13 +123,13 @@ const graph = {
         }
 
         return false;
-    },
+    }
 
     connectNodes(sourceId, targetIds) {
         targetIds.forEach((targetId) => {
             if (
-                graph.existsLink(sourceId, targetId) ||
-                graph.existsLink(targetId, sourceId)
+                this.existsLink(sourceId, targetId) ||
+                this.existsLink(targetId, sourceId)
             ) {
                 return;
             }
@@ -116,36 +139,53 @@ const graph = {
             link[LINK_SOURCE] = sourceId;
             link[LINK_TARGET] = targetId;
 
-            graph.data[GRAPH_LINKS].push(link);
+            this.data[GRAPH_LINKS].push(link);
         });
-    },
 
-    getCleanData() {
+        this.storeCurrentData(
+            "Created link connecting [" +
+                sourceId +
+                "] with [" +
+                targetIds.join() +
+                "]"
+        );
+    }
+
+    getCleanData(data = undefined, positions = false) {
+        if (data === undefined) {
+            data = this.data;
+        }
+
         var cleanData = {};
         cleanData[GRAPH_LINKS] = [];
         cleanData[GRAPH_NODES] = [];
 
-        graph.data[GRAPH_LINKS].forEach((link) =>
-            cleanData[GRAPH_LINKS].push(graph.getCleanLink(link))
+        data[GRAPH_LINKS].forEach((link) =>
+            cleanData[GRAPH_LINKS].push(this.getCleanLink(link))
         );
 
-        graph.data[GRAPH_NODES].forEach((node) =>
-            cleanData[GRAPH_NODES].push(graph.getCleanNode(node))
+        data[GRAPH_NODES].forEach((node) =>
+            cleanData[GRAPH_NODES].push(this.getCleanNode(node, positions))
         );
 
         console.log(cleanData);
         return cleanData;
-    },
+    }
 
-    getCleanNode(node) {
+    getCleanNode(node, positions) {
         var cleanNode = {};
 
         NODE_PARAMS.forEach((param) => {
             cleanNode[param] = node[param];
         });
 
+        if (positions) {
+            cleanNode.fx = node.fx;
+            cleanNode.fy = node.fy;
+        }
+
         return cleanNode;
-    },
+    }
 
     getCleanLink(link) {
         var cleanLink = {};
@@ -161,27 +201,28 @@ const graph = {
         });
 
         return cleanLink;
-    },
+    }
 
     existsNodeId(nodeId) {
-        var nodes = graph.data[GRAPH_NODES];
+        var nodes = this.data[GRAPH_NODES];
         for (var i = 0; i < nodes.length; i++) {
             if (nodes[i][NODE_ID] === nodeId) {
                 return true;
             }
         }
         return false;
-    },
+    }
 
     getUnusedNodeId() {
         var id;
         do {
-            id = graph.getRandomString();
-        } while (graph.existsNodeId(id));
+            id = this.getRandomString();
+        } while (this.existsNodeId(id));
         return id;
-    },
+    }
 
     getRandomString(length = 8) {
+        // Move to global helpers
         // Based on: https://stackoverflow.com/a/1349426/7376120
         var characters =
             "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -194,7 +235,7 @@ const graph = {
             );
         }
         return result;
-    },
+    }
 
     addLink(sourceId, targetId, linkDetails = {}) {
         // Copy params
@@ -204,15 +245,14 @@ const graph = {
         if (
             sourceId === undefined ||
             targetId === undefined ||
-            graph.existsNodeId(sourceId) === false ||
-            graph.existsNodeId(targetId) === false
+            this.existsNodeId(sourceId) === false ||
+            this.existsNodeId(targetId) === false
         ) {
             return;
         }
 
-
         // Make sure the link is unique
-        if (graph.existsLink(sourceId, targetId)) {
+        if (this.existsLink(sourceId, targetId)) {
             return;
         }
 
@@ -224,11 +264,19 @@ const graph = {
         newLink.node = false;
 
         // Add node
-        graph.data[GRAPH_LINKS].push(newLink);
-        graph.update();
+        this.data[GRAPH_LINKS].push(newLink);
+        this.triggerOnChange();
+
+        this.storeCurrentData(
+            "Added custom link connecting [" +
+                sourceId +
+                "] with [" +
+                targetId +
+                "]"
+        );
 
         return newLink;
-    },
+    }
 
     addNode(nodeDetails) {
         // Copy params
@@ -236,8 +284,8 @@ const graph = {
 
         // Make sure the ID is set and unique
         if (newNode[NODE_ID] === undefined) {
-            newNode[NODE_ID] = graph.getUnusedNodeId();
-        } else if (graph.existsNodeId(newNode[NODE_ID])) {
+            newNode[NODE_ID] = this.getUnusedNodeId();
+        } else if (this.existsNodeId(newNode[NODE_ID])) {
             return;
         }
 
@@ -246,9 +294,13 @@ const graph = {
         newNode.link = false;
 
         // Add node
-        graph.data[GRAPH_NODES].push(newNode);
-        graph.update();
+        this.data[GRAPH_NODES].push(newNode);
+        this.triggerOnChange();
+
+        this.storeCurrentData(
+            "Added custom node with id [" + newNode[NODE_ID] + "]"
+        );
 
         return newNode;
-    },
-};
+    }
+}
diff --git a/editor/js/manageddata.js b/editor/js/manageddata.js
new file mode 100644
index 0000000..c83ddd7
--- /dev/null
+++ b/editor/js/manageddata.js
@@ -0,0 +1,55 @@
+class ManagedData {
+    constructor(data) {
+        this.data = data;
+        this.history = [];
+        this.historyPosition = 0;
+
+        this.storeCurrentData("Initial state");
+    }
+
+    onUndo() {}
+    onRedo() {}
+
+    undo() {
+        if (this.historyPosition + 1 >= this.history.length) {
+            return false;
+        }
+
+        this.historyPosition += 1;
+        this.data = JSON.parse(this.history[this.historyPosition].data);
+
+        this.onUndo();
+
+        return true;
+    }
+
+    redo() {
+        if (this.historyPosition <= 0) {
+            return false;
+        }
+
+        this.historyPosition -= 1;
+        this.data = JSON.parse(this.history[this.historyPosition].data);
+
+        this.onRedo();
+
+        return true;
+    }
+
+    formatData(data) {
+        return data;
+    }
+
+    storeCurrentData(description) {
+        var formattedData = this.formatData(this.data);
+
+        this.history.unshift({
+            description: description,
+            data: JSON.stringify(formattedData), // Creating a deep copy
+        });
+
+        // Forget about the currently stored potential future
+        this.history.splice(0, this.historyPosition);
+        this.historyPosition = 0;
+    }
+}
diff --git a/editor/js/state.js b/editor/js/state.js
index 553509d..5f6cdfa 100644
--- a/editor/js/state.js
+++ b/editor/js/state.js
@@ -1,4 +1,6 @@
 const TOOLS = {
+    undo: new UndoTool("undo"),
+    redo: new RedoTool("redo"),
     select: new SelectTool("select"),
     collect: new CollectTool("collect"),
     delete: new DeleteTool("delete"),
diff --git a/editor/js/tools/redotool.js b/editor/js/tools/redotool.js
new file mode 100644
index 0000000..2cbe00d
--- /dev/null
+++ b/editor/js/tools/redotool.js
@@ -0,0 +1,10 @@
+class RedoTool extends Tool {
+    constructor(key) {
+        super("Redo", "redo", key);
+    }
+    
+    onToolActivate() {
+        graph.redo();
+        state.setTool(state.previousTool)
+    }
+}
\ No newline at end of file
diff --git a/editor/js/tools/undotool.js b/editor/js/tools/undotool.js
new file mode 100644
index 0000000..98af6eb
--- /dev/null
+++ b/editor/js/tools/undotool.js
@@ -0,0 +1,10 @@
+class UndoTool extends Tool {
+    constructor(key) {
+        super("Undo", "undo", key);
+    }
+    
+    onToolActivate() {
+        graph.undo();
+        state.setTool(state.previousTool)
+    }
+}
\ No newline at end of file
-- 
GitLab