fsm-designer/js/main.js

371 lines
9.6 KiB
JavaScript
Raw Normal View History

const width = 800;
const height = 600;
2019-02-27 20:22:38 +00:00
const radius = 25;
const settings = {
physics: true,
speed: 2,
snapToPadding: 6,
hitTargetPadding: 6,
colors: {
default: '#000',
active: '#00f',
getColor: (object) => selectedObject === object ? settings.colors.active : settings.colors.default
}
2019-02-27 20:22:38 +00:00
};
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
const states = [];
const connections = [];
let isPaused = false;
let caretTimer,
caretVisible = true;
function resetCaret() {
clearInterval(caretTimer);
caretTimer = setInterval(() => caretVisible = !caretVisible, 500);
caretVisible = true;
}
2019-02-27 20:22:38 +00:00
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 (!isPaused)
setTimeout(tick, 1000 / 30);
2019-02-27 20:22:38 +00:00
}
function draw() {
ctx.clearRect(0, 0, width, height);
ctx.save();
ctx.translate(0.5, 0.5);
states.forEach(state => state.draw());
2019-02-27 20:22:38 +00:00
connections.forEach(connection => connection.draw());
if (!!currentConnection) {
currentConnection.draw();
}
ctx.restore();
2019-02-27 20:22:38 +00:00
if (!isPaused)
setTimeout(draw, 1000 / 60);
2019-02-27 20:22:38 +00:00
}
function selectObject(x, y) {
for (let state of states) {
if (state.containsPoint(x, y)) {
2019-02-27 20:22:38 +00:00
return state;
}
}
for (let connection of connections) {
if (connection.containsPoint(x, y)) {
return connection;
}
}
2019-02-27 20:22:38 +00:00
return null;
2019-02-27 20:22:38 +00:00
}
let cursorVisible = true,
2019-02-27 20:22:38 +00:00
selectedObject = null,
currentConnection = null,
movingObject = false,
originalClick = null,
deltaMouseX = 0,
deltaMouseY = 0,
shiftPressed = false;
2019-02-27 20:22:38 +00:00
canvas.addEventListener('mousedown', (event) => {
const mouse = cbRelMousePos(event);
selectedObject = selectObject(mouse.x, mouse.y);
movingObject = false;
originalClick = mouse;
2019-02-27 20:22:38 +00:00
if (!!selectedObject) {
if (shiftPressed && selectedObject instanceof State) {
currentConnection = new SelfConnection(selectedObject, mouse);
2019-02-27 20:22:38 +00:00
} else {
movingObject = true;
deltaMouseX = deltaMouseY = 0;
if (selectedObject.setMouseStart) {
selectedObject.setMouseStart(mouse.x, mouse.y);
}
2019-02-27 20:22:38 +00:00
}
resetCaret();
} else if (shiftPressed) {
currentConnection = new TemporaryConnection(mouse, mouse);
}
if (canvasHasFocus()) {
return false;
} else {
resetCaret();
return true;
2019-02-27 20:22:38 +00:00
}
});
canvas.addEventListener('mousemove', (event) => {
const mouse = cbRelMousePos(event);
2019-02-27 20:22:38 +00:00
if (!!currentConnection) {
let targetNode = selectObject(mouse.x, mouse.y);
if (!(targetNode instanceof State)) {
targetNode = null;
2019-02-27 20:22:38 +00:00
}
if (!selectedObject) {
if (!!targetNode) {
currentConnection = new StartConnection(targetNode, originalClick);
2019-02-27 20:22:38 +00:00
} else {
currentConnection = new TemporaryConnection(originalClick, mouse);
2019-02-27 20:22:38 +00:00
}
} else {
if (targetNode === selectedObject) {
currentConnection = new SelfConnection(selectedObject, mouse);
} else if (!!targetNode) {
currentConnection = new Connection(selectedObject, targetNode);
} else {
currentConnection = new TemporaryConnection(selectedObject.closestPointOnCircle(mouse.x, mouse.y), mouse)
}
}
}
2019-02-27 20:22:38 +00:00
if (movingObject) {
selectedObject.setAnchorPoint(mouse.x, mouse.y);
if (selectedObject instanceof State) {
snapNode(selectedObject);
2019-02-27 20:22:38 +00:00
}
}
});
canvas.addEventListener('mouseup', (event) => {
movingObject = false;
2019-02-27 20:22:38 +00:00
if (!!currentConnection) {
if (!(currentConnection instanceof TemporaryConnection)) {
selectedObject = currentConnection;
connections.push(currentConnection);
resetCaret();
}
currentConnection = null;
2019-02-27 20:22:38 +00:00
}
});
2019-02-27 20:22:38 +00:00
canvas.addEventListener('dblclick', (event) => {
const mouse = cbRelMousePos(event);
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;
}
2019-02-27 20:22:38 +00:00
});
document.addEventListener('keydown', (event) => {
const key = crossBrowserKey(event);
if (!canvasHasFocus()) {
return true;
}
switch (key) {
case 16: // Shift
shiftPressed = true;
break;
case 8: // Backspace
if (!!selectedObject) {
selectedObject.text = selectedObject.text.substr(0, selectedObject.text.length - 1);
resetCaret();
}
2019-02-27 20:22:38 +00:00
return false;
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;
}
break;
2019-02-27 20:22:38 +00:00
}
});
document.addEventListener('keyup', (event) => {
const key = crossBrowserKey(event);
2019-02-27 20:22:38 +00:00
if (key === 16) {
shiftPressed = false;
2019-02-27 20:22:38 +00:00
}
});
document.addEventListener('keypress', (event) => {
const key = crossBrowserKey(event);
if(!canvasHasFocus()) {
return true;
}
2019-02-27 20:22:38 +00:00
if(key >= 0x20 && key <= 0x7E && !event.metaKey && !event.altKey && !event.ctrlKey && !!selectedObject && 'text' in selectedObject) {
selectedObject.text += String.fromCharCode(key);
resetCaret();
return false;
}
if(key === 8) {
return false;
}
2019-02-27 20:22:38 +00:00
});
CanvasRenderingContext2D.prototype.drawArrow = function (x, y, angle) {
const dx = Math.cos(angle);
const dy = Math.sin(angle);
this.beginPath();
this.moveTo(x, y);
this.lineTo(x - 8 * dx + 5 * dy, y - 8 * dy - 5 * dx);
this.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);
this.fill();
};
CanvasRenderingContext2D.prototype.drawText = function(originalText, x, y, angleOrNull, isSelected) {
const text = originalText;
this.font = '20px Roboto';
const width = this.measureText(text).width;
x -= width / 2; // Centers the text
if(!!angleOrNull) {
const dx = Math.cos(angleOrNull),
dy = Math.sin(angleOrNull),
cornerPointX = (width / 2 + 5) * (dx > 0 ? 1 : -1),
cornerPointY = (10 + 5) * (dy > 0 ? 1 : -1),
slide = dy * (Math.abs(dy) ** 40) * cornerPointX - dx * (Math.abs(dx) ** 10) * cornerPointY;
x += cornerPointX - dy * slide;
y += cornerPointY + dx * slide;
}
// Draw text and caret
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;
this.beginPath();
this.moveTo(x, y - 10);
this.lineTo(x, y + 10);
this.stroke();
this.closePath();
}
}
};
function snapNode(targetState) {
for (let state of states) {
if (state === targetState)
continue;
if (Math.abs(targetState.x - state.x) < settings.snapToPadding) {
targetState.x = state.x;
}
if (Math.abs(targetState.y - state.y) < settings.snapToPadding) {
targetState.y = state.y;
}
}
}
function canvasHasFocus() {
return (document.activeElement || document.body) === document.body;
}
function crossBrowserKey(event) {
event = event || window.event;
return event.which || event.keyCode;
}
function cbElementPos(event) {
event = event || window.event;
let obj = event.target || event.srcElement,
x = 0,
y = 0;
while (obj.offsetParent) {
x += obj.offsetLeft;
y += obj.offsetTop;
obj = obj.offsetParent;
}
return {x, y};
}
function cbMousePos(event) {
event = event || window.event;
return {
x: event.pageX || event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft,
y: event.pageY || event.clientY + document.body.scrollTop + document.documentElement.scrollTop,
}
}
function cbRelMousePos(event) {
const elem = cbElementPos(event),
mouse = cbMousePos(event);
return {
x: mouse.x - elem.x,
y: mouse.y - elem.y,
};
}
2019-02-27 20:22:38 +00:00
tick();
draw();