Implement possibility to export current states and connections and re-import these later
This commit is contained in:
parent
e5cdf549d2
commit
1af3c8a72e
|
@ -2,18 +2,20 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>FMS Designer</title>
|
<title>FSM Designer</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
|
|
||||||
<script src="js/math.js"></script>
|
<script src="js/math.js"></script>
|
||||||
|
<script src="js/export.js"></script>
|
||||||
<script src="js/components/connection.js"></script>
|
<script src="js/components/connection.js"></script>
|
||||||
<script src="js/components/start-connection.js"></script>
|
<script src="js/components/start-connection.js"></script>
|
||||||
<script src="js/components/self-connection.js"></script>
|
<script src="js/components/self-connection.js"></script>
|
||||||
<script src="js/components/temporary-connection.js"></script>
|
<script src="js/components/temporary-connection.js"></script>
|
||||||
<script src="js/components/state.js"></script>
|
<script src="js/components/state.js"></script>
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
|
<script src="js/simulate.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
class State {
|
class State {
|
||||||
|
|
||||||
constructor(x, y) {
|
constructor(x, y) {
|
||||||
|
this.id = guid();
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.mouseOffsetX = 0;
|
this.mouseOffsetX = 0;
|
||||||
this.mouseOffsetY = 0;
|
this.mouseOffsetY = 0;
|
||||||
this.color = '#f0f';
|
this.color = '#f0f';
|
||||||
|
this.isActive = false;
|
||||||
|
this.activeTime = 0;
|
||||||
this.isAcceptState = false;
|
this.isAcceptState = false;
|
||||||
this.text = '';
|
this.text = '';
|
||||||
}
|
}
|
||||||
|
@ -21,7 +24,9 @@ class State {
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
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.strokeStyle = settings.colors.getColor(this);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
@ -32,6 +37,26 @@ class State {
|
||||||
|
|
||||||
ctx.closePath();
|
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.fillStyle = settings.colors.getColor(this);
|
||||||
ctx.drawText(this.text, this.x, this.y, null, selectedObject === this);
|
ctx.drawText(this.text, this.x, this.y, null, selectedObject === this);
|
||||||
|
|
||||||
|
|
65
js/export.js
65
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);
|
||||||
|
}
|
||||||
|
|
89
js/main.js
89
js/main.js
|
@ -22,11 +22,22 @@ canvas.height = height;
|
||||||
|
|
||||||
const states = [];
|
const states = [];
|
||||||
const connections = [];
|
const connections = [];
|
||||||
|
const animations = [];
|
||||||
|
|
||||||
let isPaused = false;
|
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,
|
let caretTimer,
|
||||||
caretVisible = true;
|
caretVisible = true,
|
||||||
|
caretPos = 0;
|
||||||
|
|
||||||
function resetCaret() {
|
function resetCaret() {
|
||||||
clearInterval(caretTimer);
|
clearInterval(caretTimer);
|
||||||
|
@ -76,6 +87,8 @@ function draw() {
|
||||||
currentConnection.draw();
|
currentConnection.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
animations.forEach(animation => animation.draw());
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
if (!isPaused)
|
if (!isPaused)
|
||||||
|
@ -204,20 +217,36 @@ 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;
|
||||||
break;
|
break;
|
||||||
case 8: // Backspace
|
case 8: // Backspace
|
||||||
if (!!selectedObject) {
|
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();
|
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;
|
return false;
|
||||||
case 46: // Delete
|
case 46: // Delete
|
||||||
if (!!selectedObject) {
|
if (!!selectedObject) {
|
||||||
if(selectedObject instanceof State) {
|
if (selectedObject instanceof State) {
|
||||||
states.splice(states.findIndex(state => state === selectedObject), 1);
|
states.splice(states.findIndex(state => state === selectedObject), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,18 +274,38 @@ document.addEventListener('keyup', (event) => {
|
||||||
document.addEventListener('keypress', (event) => {
|
document.addEventListener('keypress', (event) => {
|
||||||
const key = crossBrowserKey(event);
|
const key = crossBrowserKey(event);
|
||||||
|
|
||||||
if(!canvasHasFocus()) {
|
if (!canvasHasFocus()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(key >= 0x20 && key <= 0x7E && !event.metaKey && !event.altKey && !event.ctrlKey && !!selectedObject && 'text' in selectedObject) {
|
if (!!selectedObject && 'text' in selectedObject) {
|
||||||
selectedObject.text += String.fromCharCode(key);
|
const text = selectedObject.text;
|
||||||
resetCaret();
|
|
||||||
|
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -274,14 +323,14 @@ CanvasRenderingContext2D.prototype.drawArrow = function (x, y, angle) {
|
||||||
this.fill();
|
this.fill();
|
||||||
};
|
};
|
||||||
|
|
||||||
CanvasRenderingContext2D.prototype.drawText = function(originalText, x, y, angleOrNull, isSelected) {
|
CanvasRenderingContext2D.prototype.drawText = function (originalText, x, y, angleOrNull, isSelected) {
|
||||||
const text = originalText;
|
const text = convertLatexShortcuts(originalText);
|
||||||
this.font = '20px Roboto';
|
this.font = '20px Roboto';
|
||||||
const width = this.measureText(text).width;
|
const width = this.measureText(text).width;
|
||||||
|
|
||||||
x -= width / 2; // Centers the text
|
x -= width / 2; // Centers the text
|
||||||
|
|
||||||
if(!!angleOrNull) {
|
if (!!angleOrNull) {
|
||||||
const dx = Math.cos(angleOrNull),
|
const dx = Math.cos(angleOrNull),
|
||||||
dy = Math.sin(angleOrNull),
|
dy = Math.sin(angleOrNull),
|
||||||
cornerPointX = (width / 2 + 5) * (dx > 0 ? 1 : -1),
|
cornerPointX = (width / 2 + 5) * (dx > 0 ? 1 : -1),
|
||||||
|
@ -292,14 +341,14 @@ CanvasRenderingContext2D.prototype.drawText = function(originalText, x, y, angle
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw text and caret
|
// Draw text and caret
|
||||||
if('advancedFillText' in this) {
|
if ('advancedFillText' in this) {
|
||||||
// this.advancedFillText()
|
// this.advancedFillText()
|
||||||
} else {
|
} else {
|
||||||
x = Math.round(x);
|
x = Math.round(x);
|
||||||
y = Math.round(y);
|
y = Math.round(y);
|
||||||
this.fillText(text, x, y + 6);
|
this.fillText(text, x, y + 6);
|
||||||
if(isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) {
|
if (isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) {
|
||||||
x += width;
|
x += this.measureText(text.substring(0, caretPos)).width;
|
||||||
|
|
||||||
this.beginPath();
|
this.beginPath();
|
||||||
|
|
||||||
|
@ -368,3 +417,13 @@ function cbRelMousePos(event) {
|
||||||
|
|
||||||
tick();
|
tick();
|
||||||
draw();
|
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();
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user