Refactoring code in order to allow multiple documents and new class for Documents
This commit is contained in:
parent
a79bc8347c
commit
4d4f470de5
80
js/FSMDocument.js
Normal file
80
js/FSMDocument.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
248
js/main.js
248
js/main.js
|
@ -14,14 +14,79 @@ const settings = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const elements = {
|
||||||
|
documents: document.getElementById('document-tabs'),
|
||||||
|
};
|
||||||
|
|
||||||
const canvas = document.getElementById('canvas');
|
const canvas = document.getElementById('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
|
||||||
const states = [];
|
const documents = [];
|
||||||
const connections = [];
|
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 = [];
|
const animations = [];
|
||||||
|
|
||||||
let isPaused = false;
|
let isPaused = false;
|
||||||
|
@ -46,29 +111,9 @@ function resetCaret() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
states.forEach(stateA => {
|
if (activeDocument !== null) {
|
||||||
states.forEach(stateB => {
|
documents[activeDocument].tick();
|
||||||
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 (!isPaused)
|
if (!isPaused)
|
||||||
setTimeout(tick, 1000 / 30);
|
setTimeout(tick, 1000 / 30);
|
||||||
|
@ -79,9 +124,9 @@ function draw() {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(0.5, 0.5);
|
ctx.translate(0.5, 0.5);
|
||||||
|
|
||||||
states.forEach(state => state.draw());
|
if (activeDocument !== null) {
|
||||||
|
documents[activeDocument].draw();
|
||||||
connections.forEach(connection => connection.draw());
|
}
|
||||||
|
|
||||||
if (!!currentConnection) {
|
if (!!currentConnection) {
|
||||||
currentConnection.draw();
|
currentConnection.draw();
|
||||||
|
@ -96,13 +141,19 @@ function draw() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectObject(x, y) {
|
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)) {
|
if (state.containsPoint(x, y)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let connection of connections) {
|
for (let connection of doc.connections) {
|
||||||
if (connection.containsPoint(x, y)) {
|
if (connection.containsPoint(x, y)) {
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
@ -184,13 +235,13 @@ canvas.addEventListener('mousemove', (event) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('mouseup', (event) => {
|
canvas.addEventListener('mouseup', () => {
|
||||||
movingObject = false;
|
movingObject = false;
|
||||||
|
|
||||||
if (!!currentConnection) {
|
if (!!currentConnection && activeDocument !== null) {
|
||||||
if (!(currentConnection instanceof TemporaryConnection)) {
|
if (!(currentConnection instanceof TemporaryConnection)) {
|
||||||
selectedObject = currentConnection;
|
selectedObject = currentConnection;
|
||||||
connections.push(currentConnection);
|
documents[activeDocument].connections.push(currentConnection);
|
||||||
resetCaret();
|
resetCaret();
|
||||||
}
|
}
|
||||||
currentConnection = null;
|
currentConnection = null;
|
||||||
|
@ -199,15 +250,13 @@ canvas.addEventListener('mouseup', (event) => {
|
||||||
|
|
||||||
canvas.addEventListener('dblclick', (event) => {
|
canvas.addEventListener('dblclick', (event) => {
|
||||||
const mouse = cbRelMousePos(event);
|
const mouse = cbRelMousePos(event);
|
||||||
|
|
||||||
|
if (activeDocument === null)
|
||||||
|
return;
|
||||||
|
|
||||||
selectedObject = selectObject(mouse.x, mouse.y);
|
selectedObject = selectObject(mouse.x, mouse.y);
|
||||||
|
|
||||||
if (!selectedObject) {
|
documents[activeDocument].onDblClick(mouse.x, mouse.y);
|
||||||
selectedObject = new State(mouse.x, mouse.y);
|
|
||||||
states.push(selectedObject);
|
|
||||||
resetCaret();
|
|
||||||
} else if (selectedObject instanceof State) {
|
|
||||||
selectedObject.isAcceptState = !selectedObject.isAcceptState;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
|
@ -217,8 +266,6 @@ document.addEventListener('keydown', (event) => {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(key);
|
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 16: // Shift
|
case 16: // Shift
|
||||||
shiftPressed = true;
|
shiftPressed = true;
|
||||||
|
@ -233,31 +280,27 @@ document.addEventListener('keydown', (event) => {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
case 37: // Left Arrow
|
case 37: // Left Arrow
|
||||||
|
if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
|
||||||
caretPos--;
|
caretPos--;
|
||||||
caretPos = Math.max(caretPos, 0);
|
caretPos = Math.max(caretPos, 0);
|
||||||
resetCaret();
|
resetCaret();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
case 39: // Right Arrow
|
case 39: // Right Arrow
|
||||||
|
if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
|
||||||
caretPos++;
|
caretPos++;
|
||||||
caretPos = Math.min(caretPos, selectedObject.text.length);
|
caretPos = Math.min(caretPos, selectedObject.text.length);
|
||||||
resetCaret();
|
resetCaret();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 46: // Delete
|
case 46: // Delete
|
||||||
if (!!selectedObject) {
|
if (activeDocument !== null) {
|
||||||
if (selectedObject instanceof State) {
|
documents[activeDocument].deleteCurrentObject();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -281,8 +324,6 @@ document.addEventListener('keypress', (event) => {
|
||||||
if (!!selectedObject && 'text' in selectedObject) {
|
if (!!selectedObject && 'text' in selectedObject) {
|
||||||
const text = selectedObject.text;
|
const text = selectedObject.text;
|
||||||
|
|
||||||
console.log(key, event);
|
|
||||||
|
|
||||||
if (!event.metaKey && !event.altKey && !event.ctrlKey) {
|
if (!event.metaKey && !event.altKey && !event.ctrlKey) {
|
||||||
if (key >= 0x20 && key <= 0x7E) {
|
if (key >= 0x20 && key <= 0x7E) {
|
||||||
selectedObject.text = text.substring(0, caretPos) + String.fromCharCode(key) + text.substring(caretPos, text.length);
|
selectedObject.text = text.substring(0, caretPos) + String.fromCharCode(key) + text.substring(caretPos, text.length);
|
||||||
|
@ -291,8 +332,6 @@ document.addEventListener('keypress', (event) => {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(caretPos);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,8 +341,43 @@ document.addEventListener('keypress', (event) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keydown', (event) => {
|
window.addEventListener('keydown', (event) => {
|
||||||
if ((event.ctrlKey || event.metaKey) && String.fromCharCode(event.which).toLowerCase() === 's') {
|
const keyCode = crossBrowserKey(event);
|
||||||
exportToFile();
|
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();
|
event.preventDefault();
|
||||||
return false;
|
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) {
|
function snapNode(targetState) {
|
||||||
for (let state of states) {
|
for (let state of documents[activeDocument].states) {
|
||||||
if (state === targetState)
|
if (state === targetState)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
@ -415,15 +507,43 @@ function cbRelMousePos(event) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
tick();
|
|
||||||
draw();
|
|
||||||
|
|
||||||
function guid() {
|
function guid() {
|
||||||
function s4() {
|
function s4() {
|
||||||
return Math.floor((1 + Math.random()) * 0x10000)
|
return Math.floor((1 + Math.random()) * 0x10000)
|
||||||
.toString(16)
|
.toString(16)
|
||||||
.substring(1);
|
.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
||||||
s4() + '-' + s4() + s4() + s4();
|
s4() + '-' + s4() + s4() + s4();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
|
@ -3,6 +3,10 @@ let singleCharMode = true;
|
||||||
let simulationStepDuration = 500;
|
let simulationStepDuration = 500;
|
||||||
|
|
||||||
function simulate(word) {
|
function simulate(word) {
|
||||||
|
if(activeDocument !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let steps = [];
|
let steps = [];
|
||||||
|
|
||||||
if (singleCharMode) {
|
if (singleCharMode) {
|
||||||
|
@ -16,7 +20,7 @@ function simulate(word) {
|
||||||
});
|
});
|
||||||
simulationStates.splice();
|
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)));
|
startConnections.forEach(connection => animations.push(new ConnectionAnimation(connection)));
|
||||||
simulationStates = startConnections.map(conn => conn.state);
|
simulationStates = startConnections.map(conn => conn.state);
|
||||||
|
|
||||||
|
@ -78,7 +82,9 @@ function simulationStep(input) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOutgoingConnections(state) {
|
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 {
|
class ConnectionAnimation {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user