diff --git a/js/FSMDocument.js b/js/FSMDocument.js new file mode 100644 index 0000000..17826df --- /dev/null +++ b/js/FSMDocument.js @@ -0,0 +1,80 @@ +class FSMDocument { + + constructor(name) { + this.id = guid(); + this.name = null; + this.createdAt = Date.now(); + this.lastModified = Date.now(); + this.element = null; + this.unsaved = true; + this.lastSavedHash = ''; + this.states = []; + this.connections = []; + } + + tick() { + this.states.forEach(stateA => { + this.states.forEach(stateB => { + if (stateA !== stateB && stateA.intersects(stateB)) { + const inter = stateA.intersection(stateB); + const angle1 = stateA.directionTo(stateB); + const angle2 = angle1 + Math.PI; + + const x1 = Math.cos(angle1) * inter + stateA.x; + const y1 = Math.sin(angle1) * inter + stateA.y; + const x2 = Math.cos(angle2) * inter + stateB.x; + const y2 = Math.sin(angle2) * inter + stateB.y; + + stateA.moveTo(x1, y1); + stateB.moveTo(x2, y2); + } + }); + }); + + this.states.forEach(state => { + if (state.v) { + state.moveToStep(); + } + }); + + if(!this.unsaved) { + if(this.hashCode() !== this.lastSavedHash) { + this.unsaved = true; + this.element.classList.add('unsaved'); + } + } + } + + draw() { + this.states.forEach(state => state.draw()); + this.connections.forEach(connection => connection.draw()); + } + + onDblClick(x, y) { + if (!selectedObject) { + selectedObject = new State(x, y); + this.states.push(selectedObject); + resetCaret(); + } else if (selectedObject instanceof State) { + selectedObject.isAcceptState = !selectedObject.isAcceptState; + } + } + + deleteCurrentObject() { + if (!!selectedObject) { + if (selectedObject instanceof State) { + this.states.splice(this.states.findIndex(state => state === selectedObject), 1); + } + + for (let i = 0; i < this.connections.length; i++) { + const con = this.connections[i]; + if (con === selectedObject || con.state === selectedObject || con.stateA === selectedObject || con.stateB === selectedObject) { + this.connections.splice(i--, 1); + } + } + + selectedObject = null; + } + } + +} \ No newline at end of file diff --git a/js/main.js b/js/main.js index ddd7300..2df2ade 100644 --- a/js/main.js +++ b/js/main.js @@ -14,14 +14,79 @@ const settings = { } }; +const elements = { + documents: document.getElementById('document-tabs'), +}; + const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); canvas.width = width; canvas.height = height; -const states = []; -const connections = []; +const documents = []; +let activeDocument = null; + +function switchDocument(doc) { + if (doc instanceof FSMDocument) { + doc = documents.findIndex(document => document === doc); + } + + if (typeof doc === 'number' && doc >= 0 && doc < documents.length) { + activeDocument = doc; + + const docElements = document.getElementsByClassName('document-tab'); + for (let el of docElements) { + el.classList.remove('active'); + } + docElements[doc].classList.add('active'); + + resetCaret(); + selectedObject = null; + currentConnection = null; + movingObject = false; + originalClick = null; + deltaMouseX = 0; + deltaMouseY = 0; + shiftPressed = false; + } +} + +function addDocument(doc) { + documents.push(doc); + + const elem = document.createElement('li'); + elem.classList.add('document-tab'); + + elem.addChild('span', 'document-name', doc.name || 'Unbenannt'); + const btn = elem.addChild('button', ['btn', 'btn-close'], 'X'); + + elem.addEventListener('click', () => { + switchDocument(doc); + }); + + btn.addEventListener('click', () => { + closeDocument(doc); + }); + + doc.element = elem; + + elements.documents.appendChild(elem); +} + +function closeDocument(doc) { + if (doc instanceof Document) { + doc = documents.findIndex(document => document === doc); + } + + if (typeof doc === 'number') { + documents.splice(doc, 1); + + const docElements = document.getElementsByClassName('document-tab'); + docElements[doc].remove(); + } +} + const animations = []; let isPaused = false; @@ -46,29 +111,9 @@ function resetCaret() { } function tick() { - states.forEach(stateA => { - states.forEach(stateB => { - if (stateA !== stateB && stateA.intersects(stateB)) { - const inter = stateA.intersection(stateB); - const angle1 = stateA.directionTo(stateB); - const angle2 = angle1 + Math.PI; - - const x1 = Math.cos(angle1) * inter + stateA.x; - const y1 = Math.sin(angle1) * inter + stateA.y; - const x2 = Math.cos(angle2) * inter + stateB.x; - const y2 = Math.sin(angle2) * inter + stateB.y; - - stateA.moveTo(x1, y1); - stateB.moveTo(x2, y2); - } - }); - }); - - states.forEach(state => { - if (state.v) { - state.moveToStep(); - } - }); + if (activeDocument !== null) { + documents[activeDocument].tick(); + } if (!isPaused) setTimeout(tick, 1000 / 30); @@ -79,9 +124,9 @@ function draw() { ctx.save(); ctx.translate(0.5, 0.5); - states.forEach(state => state.draw()); - - connections.forEach(connection => connection.draw()); + if (activeDocument !== null) { + documents[activeDocument].draw(); + } if (!!currentConnection) { currentConnection.draw(); @@ -96,13 +141,19 @@ function draw() { } function selectObject(x, y) { - for (let state of states) { + if (activeDocument === null) { + return null; + } + + const doc = documents[activeDocument]; + + for (let state of doc.states) { if (state.containsPoint(x, y)) { return state; } } - for (let connection of connections) { + for (let connection of doc.connections) { if (connection.containsPoint(x, y)) { return connection; } @@ -184,13 +235,13 @@ canvas.addEventListener('mousemove', (event) => { }); -canvas.addEventListener('mouseup', (event) => { +canvas.addEventListener('mouseup', () => { movingObject = false; - if (!!currentConnection) { + if (!!currentConnection && activeDocument !== null) { if (!(currentConnection instanceof TemporaryConnection)) { selectedObject = currentConnection; - connections.push(currentConnection); + documents[activeDocument].connections.push(currentConnection); resetCaret(); } currentConnection = null; @@ -199,15 +250,13 @@ canvas.addEventListener('mouseup', (event) => { canvas.addEventListener('dblclick', (event) => { const mouse = cbRelMousePos(event); + + if (activeDocument === null) + return; + selectedObject = selectObject(mouse.x, mouse.y); - if (!selectedObject) { - selectedObject = new State(mouse.x, mouse.y); - states.push(selectedObject); - resetCaret(); - } else if (selectedObject instanceof State) { - selectedObject.isAcceptState = !selectedObject.isAcceptState; - } + documents[activeDocument].onDblClick(mouse.x, mouse.y); }); document.addEventListener('keydown', (event) => { @@ -217,8 +266,6 @@ document.addEventListener('keydown', (event) => { return true; } - console.log(key); - switch (key) { case 16: // Shift shiftPressed = true; @@ -233,31 +280,27 @@ document.addEventListener('keydown', (event) => { return false; case 37: // Left Arrow - caretPos--; - caretPos = Math.max(caretPos, 0); - resetCaret(); + if (!(event.ctrlKey || event.metaKey) && !!selectedObject) { + caretPos--; + caretPos = Math.max(caretPos, 0); + resetCaret(); - return false; + return false; + } + + break; case 39: // Right Arrow - caretPos++; - caretPos = Math.min(caretPos, selectedObject.text.length); - resetCaret(); + if (!(event.ctrlKey || event.metaKey) && !!selectedObject) { + caretPos++; + caretPos = Math.min(caretPos, selectedObject.text.length); + resetCaret(); - return false; + return false; + } + break; case 46: // Delete - if (!!selectedObject) { - if (selectedObject instanceof State) { - states.splice(states.findIndex(state => state === selectedObject), 1); - } - - for (let i = 0; i < connections.length; i++) { - const con = connections[i]; - if (con === selectedObject || con.state === selectedObject || con.stateA === selectedObject || con.stateB === selectedObject) { - connections.splice(i--, 1); - } - } - - selectedObject = null; + if (activeDocument !== null) { + documents[activeDocument].deleteCurrentObject(); } break; } @@ -281,8 +324,6 @@ document.addEventListener('keypress', (event) => { if (!!selectedObject && 'text' in selectedObject) { const text = selectedObject.text; - 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); @@ -291,8 +332,6 @@ document.addEventListener('keypress', (event) => { return false; } - - console.log(caretPos); } } @@ -302,8 +341,43 @@ document.addEventListener('keypress', (event) => { }); window.addEventListener('keydown', (event) => { - if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') { - exportToFile(); + const keyCode = crossBrowserKey(event); + const key = String.fromCharCode(keyCode).toLowerCase(); + if ((event.ctrlKey || event.metaKey) && key === 's') { // Ctrl + S + if (event.altKey || event.shiftKey) { // Ctrl + Alt + S + modalExport.open(); + } else { + saveToLocalStorage(); + } + + event.preventDefault(); + return false; + } + + if ((event.ctrlKey || event.metaKey) && key === 'e') { // Ctrl + E + modalExport.open(); + + event.preventDefault(); + return false; + } + + if ((event.ctrlKey || event.metaKey) && keyCode === 37) { // Ctrl + LeftArrow + let activeDoc = activeDocument; + activeDoc--; + activeDoc = activeDoc < 0 ? documents.length - 1 : activeDoc % documents.length; + + switchDocument(activeDoc); + + event.preventDefault(); + return false; + } + + if ((event.ctrlKey || event.metaKey) && keyCode === 39) { // Ctrl + RightArrow + let activeDoc = activeDocument; + activeDoc++; + activeDoc = activeDoc < 0 ? documents.length - 1 : activeDoc % documents.length; + + switchDocument(activeDoc); event.preventDefault(); return false; @@ -361,8 +435,26 @@ CanvasRenderingContext2D.prototype.drawText = function (originalText, x, y, angl } }; +Node.prototype.addChild = function (name, classList, text) { + const elem = document.createElement(name); + + if (classList instanceof Array) { + elem.classList.add(...classList); + } else if (!!classList) { + elem.classList.add(classList); + } + + if (!!text) { + elem.innerText = text; + } + + this.appendChild(elem); + + return elem; +}; + function snapNode(targetState) { - for (let state of states) { + for (let state of documents[activeDocument].states) { if (state === targetState) continue; @@ -415,15 +507,43 @@ 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 +} + +window.addEventListener('load', () => { + const doc = new FSMDocument('Dokument1'); + addDocument(doc); + switchDocument(doc); + + tick(); + draw(); +}); + +Object.prototype.hashCode = function () { + let baseString = ''; + Object.values(this).forEach(entry => { + if (entry === null) + return; + + if (typeof entry === 'object' && !(entry instanceof Node)) { + baseString += entry.hashCode(); + } else { + baseString += entry; + } + }); + let hash = 0; + for (let i = 0; i < baseString.length; i++) { + const char = baseString.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return hash; +}; \ No newline at end of file diff --git a/js/simulate.js b/js/simulate.js index 2b46f1b..28b003f 100644 --- a/js/simulate.js +++ b/js/simulate.js @@ -3,6 +3,10 @@ let singleCharMode = true; let simulationStepDuration = 500; function simulate(word) { + if(activeDocument !== null) { + return false; + } + let steps = []; if (singleCharMode) { @@ -16,7 +20,7 @@ function simulate(word) { }); simulationStates.splice(); - const startConnections = connections.filter(conn => conn instanceof StartConnection); + const startConnections = documents[activeDocument].connections.filter(conn => conn instanceof StartConnection); startConnections.forEach(connection => animations.push(new ConnectionAnimation(connection))); simulationStates = startConnections.map(conn => conn.state); @@ -78,7 +82,9 @@ function simulationStep(input) { } function getOutgoingConnections(state) { - return connections.filter(connection => connection instanceof SelfConnection ? connection.state === state : connection.stateA === state); + if(activeDocument === null) + return []; + return documents[activeDocument].connections.filter(connection => connection instanceof SelfConnection ? connection.state === state : connection.stateA === state); } class ConnectionAnimation {