From 1af3c8a72eed6e8be2d9740219630aaafbe105e9 Mon Sep 17 00:00:00 2001
From: KingOfDog <kingofdog@web.de>
Date: Mon, 4 Mar 2019 10:29:32 +0100
Subject: [PATCH] Implement possibility to export current states and
 connections and re-import these later

---
 index.html             |  4 +-
 js/components/state.js | 27 ++++++++++++-
 js/export.js           | 65 ++++++++++++++++++++++++++++++
 js/main.js             | 89 +++++++++++++++++++++++++++++++++++-------
 4 files changed, 168 insertions(+), 17 deletions(-)

diff --git a/index.html b/index.html
index b2b115f..80f265f 100644
--- a/index.html
+++ b/index.html
@@ -2,18 +2,20 @@
 <html lang="en">
 <head>
     <meta charset="UTF-8">
-    <title>FMS Designer</title>
+    <title>FSM Designer</title>
 </head>
 <body>
 
 <canvas id="canvas"></canvas>
 
 <script src="js/math.js"></script>
+<script src="js/export.js"></script>
 <script src="js/components/connection.js"></script>
 <script src="js/components/start-connection.js"></script>
 <script src="js/components/self-connection.js"></script>
 <script src="js/components/temporary-connection.js"></script>
 <script src="js/components/state.js"></script>
 <script src="js/main.js"></script>
+<script src="js/simulate.js"></script>
 </body>
 </html>
diff --git a/js/components/state.js b/js/components/state.js
index 34aa6e2..b41a4ab 100644
--- a/js/components/state.js
+++ b/js/components/state.js
@@ -1,11 +1,14 @@
 class State {
 
     constructor(x, y) {
+        this.id = guid();
         this.x = x;
         this.y = y;
         this.mouseOffsetX = 0;
         this.mouseOffsetY = 0;
         this.color = '#f0f';
+        this.isActive = false;
+        this.activeTime = 0;
         this.isAcceptState = false;
         this.text = '';
     }
@@ -21,7 +24,9 @@ class State {
     }
 
     draw() {
-        ctx.fillStyle = this.color;
+        const activeTimeDuration = Date.now() - this.activeTime;
+
+        ctx.fillStyle = this.isActive && activeTimeDuration > simulationStepDuration ? '#0f0' : this.color;
         ctx.strokeStyle = settings.colors.getColor(this);
 
         ctx.beginPath();
@@ -32,6 +37,26 @@ class State {
 
         ctx.closePath();
 
+        if(activeTimeDuration < simulationStepDuration) {
+            let size = 0;
+            const percent = Math.min(Math.max(activeTimeDuration / simulationStepDuration, 0), 1);
+
+            if(this.isActive) {
+                size = easeInOutCubic(percent) * radius;
+            } else {
+                size = easeInOutCubic(1 - percent) * radius;
+            }
+
+            ctx.fillStyle = '#0f0';
+
+            ctx.beginPath();
+
+            ctx.arc(this.x, this.y, size, 0, 2 * Math.PI);
+            ctx.fill();
+
+            ctx.closePath();
+        }
+
         ctx.fillStyle = settings.colors.getColor(this);
         ctx.drawText(this.text, this.x, this.y, null, selectedObject === this);
 
diff --git a/js/export.js b/js/export.js
index e69de29..f7e7e31 100644
--- a/js/export.js
+++ b/js/export.js
@@ -0,0 +1,65 @@
+function exportToJson() {
+    const _states = JSON.parse(JSON.stringify(states));
+    const _connections = JSON.parse(JSON.stringify(connections)).map((conn, index) => {
+        conn.type = connections[index].constructor.name;
+        return conn;
+    });
+    const data = {
+        states: _states,
+        connections: _connections.filter(conn => conn.type === 'Connection').map(conn => {
+            conn.stateA = conn.stateA.id;
+            conn.stateB = conn.stateB.id;
+            return conn;
+        }),
+        startConnections: _connections.filter(conn => conn.type === 'StartConnection').map(conn => {
+            conn.state = conn.state.id;
+            return conn;
+        }),
+        selfConnections: _connections.filter(conn => conn.type === 'SelfConnection').map(conn => {
+            conn.state = conn.state.id;
+            return conn;
+        }),
+        settings,
+    };
+    return JSON.stringify(data);
+}
+
+function importFromJson(json) {
+    const data = JSON.parse(json);
+
+    states.push(...data.states.map(state => Object.setPrototypeOf(state, State.prototype)));
+    connections.push(...data.connections.map(conn => Object.setPrototypeOf(conn, Connection.prototype)).map(conn => {
+        conn.stateA = states.find(state => state.id === conn.stateA);
+        conn.stateB = states.find(state => state.id === conn.stateB);
+        return conn;
+    }));
+    connections.push(...data.startConnections.map(conn => Object.setPrototypeOf(conn, StartConnection.prototype)).map(conn => {
+        conn.state = states.find(state => state.id === conn.state);
+        return conn;
+    }));
+    connections.push(...data.selfConnections.map(conn => Object.setPrototypeOf(conn, SelfConnection.prototype)).map(conn => {
+        conn.state = states.find(state => state.id === conn.state);
+        return conn;
+    }));
+}
+
+function exportToFile() {
+    const name = 'placeholder.fsm';
+    const json = exportToJson();
+    console.log(json);
+    downloadFile(name, json, 'application/json');
+}
+
+function downloadFile(name, content, type) {
+    const element = document.createElement('a');
+
+    element.setAttribute('href', `data:${type},charset=utf-8,${content}`);
+    element.setAttribute('download', name);
+
+    element.style.display = 'none';
+    document.body.appendChild(element);
+
+    element.click();
+    document.body.removeChild(element);
+}
+
diff --git a/js/main.js b/js/main.js
index 323a65d..ddd7300 100644
--- a/js/main.js
+++ b/js/main.js
@@ -22,11 +22,22 @@ canvas.height = height;
 
 const states = [];
 const connections = [];
+const animations = [];
 
 let isPaused = false;
 
+function convertLatexShortcuts(text) {
+    for (let i = 0; i < 10; i++) {
+        // TODO: Replace with more general way
+        text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));
+    }
+
+    return text;
+}
+
 let caretTimer,
-    caretVisible = true;
+    caretVisible = true,
+    caretPos = 0;
 
 function resetCaret() {
     clearInterval(caretTimer);
@@ -76,6 +87,8 @@ function draw() {
         currentConnection.draw();
     }
 
+    animations.forEach(animation => animation.draw());
+
     ctx.restore();
 
     if (!isPaused)
@@ -204,20 +217,36 @@ document.addEventListener('keydown', (event) => {
         return true;
     }
 
+    console.log(key);
+
     switch (key) {
         case 16: // Shift
             shiftPressed = true;
             break;
         case 8: // Backspace
             if (!!selectedObject) {
-                selectedObject.text = selectedObject.text.substr(0, selectedObject.text.length - 1);
+                selectedObject.text = selectedObject.text.substr(0, caretPos - 1) + selectedObject.text.substr(caretPos, selectedObject.text.length);
+                caretPos--;
+                caretPos = Math.max(caretPos, 0);
                 resetCaret();
             }
 
+            return false;
+        case 37: // Left Arrow
+            caretPos--;
+            caretPos = Math.max(caretPos, 0);
+            resetCaret();
+
+            return false;
+        case 39: // Right Arrow
+            caretPos++;
+            caretPos = Math.min(caretPos, selectedObject.text.length);
+            resetCaret();
+
             return false;
         case 46: // Delete
             if (!!selectedObject) {
-                if(selectedObject instanceof State) {
+                if (selectedObject instanceof State) {
                     states.splice(states.findIndex(state => state === selectedObject), 1);
                 }
 
@@ -245,18 +274,38 @@ document.addEventListener('keyup', (event) => {
 document.addEventListener('keypress', (event) => {
     const key = crossBrowserKey(event);
 
-    if(!canvasHasFocus()) {
+    if (!canvasHasFocus()) {
         return true;
     }
 
-    if(key >= 0x20 && key <= 0x7E && !event.metaKey && !event.altKey && !event.ctrlKey && !!selectedObject && 'text' in selectedObject) {
-        selectedObject.text += String.fromCharCode(key);
-        resetCaret();
+    if (!!selectedObject && 'text' in selectedObject) {
+        const text = selectedObject.text;
 
-        return false;
+        console.log(key, event);
+
+        if (!event.metaKey && !event.altKey && !event.ctrlKey) {
+            if (key >= 0x20 && key <= 0x7E) {
+                selectedObject.text = text.substring(0, caretPos) + String.fromCharCode(key) + text.substring(caretPos, text.length);
+                caretPos++;
+                resetCaret();
+
+                return false;
+            }
+
+            console.log(caretPos);
+        }
     }
 
-    if(key === 8) {
+    if (key === 8) {
+        return false;
+    }
+});
+
+window.addEventListener('keydown', (event) => {
+    if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') {
+        exportToFile();
+
+        event.preventDefault();
         return false;
     }
 });
@@ -274,14 +323,14 @@ CanvasRenderingContext2D.prototype.drawArrow = function (x, y, angle) {
     this.fill();
 };
 
-CanvasRenderingContext2D.prototype.drawText = function(originalText, x, y, angleOrNull, isSelected) {
-    const text = originalText;
+CanvasRenderingContext2D.prototype.drawText = function (originalText, x, y, angleOrNull, isSelected) {
+    const text = convertLatexShortcuts(originalText);
     this.font = '20px Roboto';
     const width = this.measureText(text).width;
 
     x -= width / 2; // Centers the text
 
-    if(!!angleOrNull) {
+    if (!!angleOrNull) {
         const dx = Math.cos(angleOrNull),
             dy = Math.sin(angleOrNull),
             cornerPointX = (width / 2 + 5) * (dx > 0 ? 1 : -1),
@@ -292,14 +341,14 @@ CanvasRenderingContext2D.prototype.drawText = function(originalText, x, y, angle
     }
 
     // Draw text and caret
-    if('advancedFillText' in this) {
+    if ('advancedFillText' in this) {
         // this.advancedFillText()
     } else {
         x = Math.round(x);
         y = Math.round(y);
         this.fillText(text, x, y + 6);
-        if(isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) {
-            x += width;
+        if (isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) {
+            x += this.measureText(text.substring(0, caretPos)).width;
 
             this.beginPath();
 
@@ -368,3 +417,13 @@ function cbRelMousePos(event) {
 
 tick();
 draw();
+
+function guid() {
+    function s4() {
+        return Math.floor((1 + Math.random()) * 0x10000)
+            .toString(16)
+            .substring(1);
+    }
+    return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
+        s4() + '-' + s4() + s4() + s4();
+}
\ No newline at end of file