967 lines
28 KiB
JavaScript
967 lines
28 KiB
JavaScript
const width = 800;
|
|
const height = 600;
|
|
let realWidth = 800;
|
|
let realHeight = 600;
|
|
const radius = 25;
|
|
|
|
const greekLetterNames = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa', 'Lambda', 'Mu', 'Nu', 'Xi', 'Omicron', 'Pi', 'Rho', 'Sigma', 'Tau', 'Upsilon', 'Phi', 'Chi', 'Psi', 'Omega'];
|
|
|
|
const settings = {
|
|
physics: true,
|
|
speed: 2,
|
|
snapToPadding: 6,
|
|
hitTargetPadding: 6,
|
|
colors: {
|
|
default: '#000',
|
|
active: '#00f',
|
|
selectionBg: 'rgba(0,0,255,.75)',
|
|
selectionText: '#ffffff',
|
|
gridColor: 'rgba(0,0,0,.15)',
|
|
getColor: (object) => selectedObject === object ? settings.colors.active : settings.colors.default
|
|
},
|
|
drawGrid: true,
|
|
snapToGrid: true,
|
|
gridSize: 100,
|
|
};
|
|
|
|
const elements = {
|
|
documents: document.getElementById('document-tabs'),
|
|
dragOverlay: document.getElementById('dragOverlay'),
|
|
};
|
|
|
|
const contextmenu = document.getElementById('contextmenuCanvas');
|
|
const canvas = document.getElementById('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
function onResize() {
|
|
canvas.width = width * 2;
|
|
canvas.height = height * 2;
|
|
canvas.style.maxWidth = width + 'px';
|
|
canvas.style.height = canvas.clientWidth * height / width + 'px';
|
|
ctx.scale(2, 2);
|
|
|
|
realWidth = canvas.clientWidth;
|
|
realHeight = canvas.clientHeight;
|
|
}
|
|
|
|
const documents = [];
|
|
let activeDocument = null;
|
|
|
|
let assistantLines = [];
|
|
|
|
function switchDocument(doc) {
|
|
if (doc instanceof FSMDocument) {
|
|
doc = documents.findIndex(document => document.id === doc.id);
|
|
}
|
|
|
|
if (doc < 0) {
|
|
activeDocument = null;
|
|
}
|
|
|
|
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');
|
|
|
|
const label = elem.addChild('span', 'document-name', doc.name || 'Unbenannt');
|
|
const btn = elem.addChild('button', ['btn', 'btn-close'], 'X');
|
|
|
|
elem.addEventListener('click', (e) => {
|
|
if (e.target === elem || e.target === label)
|
|
switchDocument(doc);
|
|
});
|
|
|
|
btn.addEventListener('click', () => {
|
|
closeDocument(doc);
|
|
});
|
|
|
|
doc.element = elem;
|
|
|
|
elements.documents.appendChild(elem);
|
|
}
|
|
|
|
function closeDocument(doc) {
|
|
if (doc instanceof FSMDocument) {
|
|
doc = documents.findIndex(document => document.id === doc.id);
|
|
}
|
|
|
|
if (typeof doc === 'number') {
|
|
documents.splice(doc, 1);
|
|
|
|
const docElements = document.getElementsByClassName('document-tab');
|
|
docElements[doc].remove();
|
|
|
|
if (doc < activeDocument) {
|
|
activeDocument--;
|
|
} else if (doc === activeDocument) {
|
|
if (activeDocument >= documents.length) {
|
|
activeDocument = documents.length - 1;
|
|
}
|
|
}
|
|
switchDocument(activeDocument);
|
|
}
|
|
}
|
|
|
|
const animations = [];
|
|
|
|
let isPaused = false;
|
|
|
|
function convertLatexShortcuts(text) {
|
|
greekLetterNames.forEach((name, i) => {
|
|
text = text.replace(new RegExp('\\\\' + name, 'g'), String.fromCharCode(913 + i + (i > 16)));
|
|
text = text.replace(new RegExp('\\\\' + name.toLowerCase(), 'g'), String.fromCharCode(945 + i + (i > 16)));
|
|
});
|
|
|
|
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,
|
|
caretPos = 0,
|
|
caretSelection = null;
|
|
|
|
function resetCaret() {
|
|
clearInterval(caretTimer);
|
|
caretTimer = setInterval(() => caretVisible = !caretVisible, 500);
|
|
caretVisible = true;
|
|
}
|
|
|
|
function tick() {
|
|
if (activeDocument !== null) {
|
|
for (let i = 0; i < 10; i++) {
|
|
documents[activeDocument].physicsTick();
|
|
}
|
|
documents[activeDocument].tick();
|
|
}
|
|
|
|
if (!isPaused)
|
|
setTimeout(tick, 1000 / 30);
|
|
}
|
|
|
|
function draw() {
|
|
ctx.clearRect(0, 0, width, height);
|
|
ctx.save();
|
|
ctx.translate(0.5, 0.5);
|
|
|
|
if (activeDocument !== null) {
|
|
if (settings.drawGrid)
|
|
drawGrid();
|
|
|
|
assistantLines.forEach(line => {
|
|
ctx.strokeStyle = '#f0f';
|
|
ctx.beginPath();
|
|
if (!!line.x) {
|
|
ctx.moveTo(line.x, 0);
|
|
ctx.lineTo(line.x, height);
|
|
} else {
|
|
ctx.moveTo(0, line.y);
|
|
ctx.lineTo(width, line.y);
|
|
}
|
|
ctx.stroke();
|
|
});
|
|
|
|
documents[activeDocument].draw();
|
|
|
|
if (!!currentConnection) {
|
|
currentConnection.draw();
|
|
}
|
|
|
|
animations.forEach(animation => animation.draw());
|
|
} else {
|
|
ctx.fillStyle = '#eee';
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
ctx.fillStyle = '#000';
|
|
ctx.drawText('Bitte öffne ein Dokument oder erstelle ein neues...', width / 2, height / 2, null, false);
|
|
}
|
|
|
|
ctx.restore();
|
|
|
|
if (!isPaused)
|
|
setTimeout(draw, 1000 / 60);
|
|
}
|
|
|
|
function drawGrid() {
|
|
for (let i = settings.gridSize; i < width; i += settings.gridSize) {
|
|
ctx.strokeStyle = settings.colors.gridColor;
|
|
ctx.beginPath();
|
|
ctx.moveTo(i, 0);
|
|
ctx.lineTo(i, height);
|
|
ctx.stroke();
|
|
}
|
|
|
|
for (let i = settings.gridSize; i < height; i += settings.gridSize) {
|
|
ctx.strokeStyle = settings.colors.gridColor;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, i);
|
|
ctx.lineTo(width, i);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function selectObject(x, y) {
|
|
if (activeDocument === null) {
|
|
return null;
|
|
}
|
|
|
|
const doc = documents[activeDocument];
|
|
|
|
for (let state of doc.states) {
|
|
if (state.containsPoint(x, y)) {
|
|
caretPos = state.text.length;
|
|
return state;
|
|
}
|
|
}
|
|
|
|
for (let connection of doc.connections) {
|
|
if (connection.containsPoint(x, y)) {
|
|
caretPos = connection.text.length;
|
|
return connection;
|
|
}
|
|
}
|
|
|
|
caretPos = 0;
|
|
|
|
return null;
|
|
}
|
|
|
|
let cursorVisible = true,
|
|
selectedObject = null,
|
|
currentConnection = null,
|
|
movingObject = false,
|
|
originalClick = null,
|
|
deltaMouseX = 0,
|
|
deltaMouseY = 0,
|
|
shiftPressed = false,
|
|
ctrlPressed = false,
|
|
changedKeys = [],
|
|
changedValues = [];
|
|
|
|
canvas.addEventListener('mousedown', (event) => {
|
|
const mouse = cbRelMousePos(event);
|
|
selectedObject = selectObject(mouse.x, mouse.y);
|
|
movingObject = false;
|
|
originalClick = mouse;
|
|
|
|
if (!!selectedObject) {
|
|
if ((shiftPressed || event.shiftKey) && selectedObject instanceof State) {
|
|
currentConnection = new SelfConnection(selectedObject, mouse);
|
|
} else {
|
|
movingObject = true;
|
|
deltaMouseX = deltaMouseY = 0;
|
|
if (selectedObject.setMouseStart) {
|
|
selectedObject.setMouseStart(mouse.x, mouse.y);
|
|
}
|
|
}
|
|
resetCaret();
|
|
|
|
if (selectedObject instanceof SelfConnection) {
|
|
changedKeys = ['anchorAngle'];
|
|
changedValues = [selectedObject.anchorAngle];
|
|
} else if (selectedObject instanceof Connection) {
|
|
changedKeys = ['parallelPart', 'perpendicularPart'];
|
|
changedValues = [selectedObject.parallelPart, selectedObject.perpendicularPart];
|
|
} else if (selectedObject instanceof StartConnection) {
|
|
changedKeys = ['deltaX', 'deltaY'];
|
|
changedValues = [selectedObject.deltaX, selectedObject.deltaY];
|
|
} else if (selectedObject instanceof State) {
|
|
changedKeys = ['x', 'y'];
|
|
changedValues = [selectedObject.x, selectedObject.y];
|
|
}
|
|
} else if (shiftPressed || event.shiftKey) {
|
|
currentConnection = new TemporaryConnection(mouse, mouse);
|
|
}
|
|
|
|
if (canvasHasFocus()) {
|
|
return false;
|
|
} else {
|
|
resetCaret();
|
|
return true;
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', (event) => {
|
|
const mouse = cbRelMousePos(event);
|
|
|
|
if (!!currentConnection) {
|
|
let targetNode = selectObject(mouse.x, mouse.y);
|
|
if (!(targetNode instanceof State)) {
|
|
targetNode = null;
|
|
}
|
|
|
|
if (!selectedObject) {
|
|
if (!!targetNode) {
|
|
currentConnection = new StartConnection(targetNode, originalClick);
|
|
} else {
|
|
currentConnection = new TemporaryConnection(originalClick, mouse);
|
|
}
|
|
} 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (movingObject) {
|
|
if (selectedObject instanceof State) {
|
|
if (ctrlPressed || event.ctrlKey) {
|
|
const prevX = changedValues[0];
|
|
const prevY = changedValues[1];
|
|
selectedObject.setAnchorPoint(mouse.x, mouse.y);
|
|
const deltaX = Math.abs(selectedObject.x - prevX);
|
|
const deltaY = Math.abs(selectedObject.y - prevY);
|
|
if (deltaX > deltaY) {
|
|
selectedObject.y = prevY;
|
|
} else {
|
|
selectedObject.x = prevX;
|
|
}
|
|
} else {
|
|
selectedObject.setAnchorPoint(mouse.x, mouse.y);
|
|
snapNode(selectedObject);
|
|
}
|
|
} else {
|
|
selectedObject.setAnchorPoint(mouse.x, mouse.y);
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
movingObject = false;
|
|
|
|
if (!!selectedObject && activeDocument !== null) {
|
|
documents[activeDocument].addChange(selectedObject.constructor.name.toLowerCase(), selectedObject.id, 'edit', changedKeys, changedValues);
|
|
}
|
|
|
|
if (!!currentConnection && activeDocument !== null) {
|
|
if (!(currentConnection instanceof TemporaryConnection)) {
|
|
selectedObject = currentConnection;
|
|
documents[activeDocument].connections.push(currentConnection);
|
|
resetCaret();
|
|
|
|
if (currentConnection instanceof Connection) {
|
|
const parallelPart = currentConnection.parallelPart;
|
|
const perpendicularPart = currentConnection.perpendicularPart;
|
|
const connections = documents[activeDocument].getConnectionsBetweenStates(currentConnection.stateA, currentConnection.stateB).filter(conn => {
|
|
return conn.parallelPart === parallelPart && conn.perpendicularPart === perpendicularPart;
|
|
});
|
|
|
|
if (connections.length > 1) {
|
|
let cx = (currentConnection.stateB.x + currentConnection.stateA.x) / 2;
|
|
let cy = (currentConnection.stateB.y + currentConnection.stateA.y) / 2;
|
|
const dx = currentConnection.stateB.x - currentConnection.stateA.x;
|
|
const dy = currentConnection.stateB.y - currentConnection.stateA.y;
|
|
const factorX = Math.sin(dx);
|
|
const factorY = Math.cos(dy);
|
|
const step = Math.sqrt(dx ** 2 + dy ** 2) * .25;
|
|
|
|
cx -= (connections.length / 2 - .5) * step * factorX;
|
|
cy -= (connections.length / 2 - .5) * step * factorY;
|
|
|
|
connections.forEach(connection => {
|
|
connection.setAnchorPoint(cx, cy);
|
|
|
|
cx += step * factorX;
|
|
cy += step * factorY;
|
|
});
|
|
}
|
|
}
|
|
|
|
documents[activeDocument].addChange(selectedObject.constructor.name.toLowerCase(), selectedObject.id, 'add', Object.keys(selectedObject), Object.values(selectedObject))
|
|
}
|
|
currentConnection = null;
|
|
}
|
|
|
|
assistantLines = [];
|
|
});
|
|
|
|
canvas.addEventListener('dblclick', (event) => {
|
|
const mouse = cbRelMousePos(event);
|
|
|
|
if (activeDocument === null)
|
|
return;
|
|
|
|
selectedObject = selectObject(mouse.x, mouse.y);
|
|
|
|
documents[activeDocument].onDblClick(mouse.x, mouse.y);
|
|
});
|
|
|
|
document.addEventListener('click', (event) => {
|
|
contextmenu.style.display = 'none';
|
|
});
|
|
|
|
document.addEventListener('contextmenu', (event) => {
|
|
const mouse = cbRelMousePos(event);
|
|
|
|
event.preventDefault();
|
|
|
|
if (activeDocument === null)
|
|
return;
|
|
|
|
// selectedObject = selectObject(mouse.x, mouse.y);
|
|
|
|
if (event.target === contextmenu || event.target === canvas) {
|
|
contextmenu.style.top = event.y + 'px';
|
|
contextmenu.style.left = event.x + 'px';
|
|
contextmenu.style.display = 'block';
|
|
} else {
|
|
contextmenu.style.display = 'none';
|
|
}
|
|
|
|
// documents[activeDocument].onRightClick(mouse.x, mouse.y);
|
|
});
|
|
|
|
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) {
|
|
let start = caretPos - 1;
|
|
let end = caretPos;
|
|
|
|
if (caretSelection !== null) {
|
|
if (caretSelection < caretPos) {
|
|
start = caretSelection;
|
|
} else {
|
|
start = caretPos;
|
|
end = caretSelection;
|
|
}
|
|
|
|
caretSelection = null;
|
|
} else {
|
|
caretPos--;
|
|
caretPos = Math.max(caretPos, 0);
|
|
}
|
|
|
|
selectedObject.text = selectedObject.text.slice(0, start) + selectedObject.text.slice(end, selectedObject.text.length);
|
|
resetCaret();
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
return false;
|
|
case 37: // Left Arrow
|
|
if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
|
|
if ((shiftPressed || event.shiftKey) && caretSelection === null)
|
|
caretSelection = caretPos;
|
|
|
|
caretPos--;
|
|
caretPos = Math.max(caretPos, 0);
|
|
resetCaret();
|
|
|
|
if (!(shiftPressed || event.shiftKey) || caretSelection === caretPos)
|
|
caretSelection = null;
|
|
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
case 39: // Right Arrow
|
|
if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
|
|
if ((shiftPressed || event.shiftKey) && caretSelection === null)
|
|
caretSelection = caretPos;
|
|
|
|
caretPos++;
|
|
caretPos = Math.min(caretPos, selectedObject.text.length);
|
|
resetCaret();
|
|
|
|
if (!(shiftPressed || event.shiftKey) || caretSelection === caretPos)
|
|
caretSelection = null;
|
|
|
|
return false;
|
|
}
|
|
break;
|
|
case 36: // Home
|
|
if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
|
|
if ((shiftPressed || event.shiftKey) && caretSelection === null)
|
|
caretSelection = caretPos;
|
|
|
|
caretPos = 0;
|
|
resetCaret();
|
|
|
|
if (!(shiftPressed || event.shiftKey) || caretSelection === caretPos)
|
|
caretSelection = null;
|
|
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
case 35: // End
|
|
if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
|
|
if ((shiftPressed || event.shiftKey) && caretSelection === null)
|
|
caretSelection = caretPos;
|
|
|
|
caretPos = selectedObject.text.length;
|
|
|
|
if (!(shiftPressed || event.shiftKey) || caretSelection === caretPos)
|
|
caretSelection = null;
|
|
|
|
return false;
|
|
}
|
|
|
|
break;
|
|
case 46: // Delete
|
|
if (activeDocument !== null) {
|
|
documents[activeDocument].deleteCurrentObject();
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keyup', (event) => {
|
|
const key = crossBrowserKey(event);
|
|
|
|
if (key === 16) {
|
|
shiftPressed = false;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('keypress', (event) => {
|
|
const key = crossBrowserKey(event);
|
|
|
|
if (!canvasHasFocus()) {
|
|
return true;
|
|
}
|
|
|
|
if (!!selectedObject && 'text' in selectedObject) {
|
|
if (!event.metaKey && !event.altKey && !event.ctrlKey) {
|
|
if (key >= 0x20 && key <= 0x7E) {
|
|
let text = selectedObject.text;
|
|
if (caretSelection !== null) {
|
|
if (caretSelection > caretPos) {
|
|
text = text.slice(0, caretPos) + text.slice(caretSelection, text.length);
|
|
} else {
|
|
text = text.slice(0, caretSelection) + text.slice(caretPos, text.length);
|
|
caretPos = caretSelection;
|
|
}
|
|
caretSelection = null;
|
|
}
|
|
|
|
text = text.substring(0, caretPos) + String.fromCharCode(key) + text.substring(caretPos, text.length);
|
|
caretPos++;
|
|
resetCaret();
|
|
selectedObject.text = text;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (key === 8) {
|
|
selectedObject = null;
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
document.addEventListener('paste', (event) => {
|
|
if (activeDocument !== null && selectedObject !== null) {
|
|
const pastedText = event.clipboardData.getData('text/plain');
|
|
|
|
let start = caretPos;
|
|
let end = caretPos;
|
|
|
|
if (caretSelection !== null) {
|
|
if (caretSelection < caretPos) {
|
|
start = caretSelection;
|
|
caretPos = caretSelection;
|
|
} else {
|
|
end = caretSelection;
|
|
}
|
|
caretSelection = null;
|
|
}
|
|
|
|
selectedObject.text = selectedObject.text.slice(0, start) + pastedText + selectedObject.text.slice(end, selectedObject.text.length);
|
|
caretPos += pastedText.length;
|
|
}
|
|
});
|
|
|
|
window.addEventListener('keydown', (event) => {
|
|
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 === 'z') { // Ctrl + Z
|
|
if (activeDocument !== null) {
|
|
documents[activeDocument].undo(1);
|
|
|
|
event.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ((event.ctrlKey || event.metaKey) && key === 'y') { // Ctrl + Y
|
|
if (activeDocument !== null) {
|
|
documents[activeDocument].undo(-1);
|
|
|
|
event.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ((event.ctrlKey || event.metaKey) && key === 'a') { // Ctrl + A
|
|
if (activeDocument !== null && selectedObject !== null) {
|
|
caretPos = selectedObject.text.length;
|
|
caretSelection = 0;
|
|
|
|
event.preventDefault();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ((event.ctrlKey || event.metaKey) && key === 'e') { // Ctrl + E
|
|
modalExport.open();
|
|
|
|
event.preventDefault();
|
|
return false;
|
|
}
|
|
|
|
if ((event.ctrlKey || event.metaKey) && key === 'n') { // Ctrl + N
|
|
const doc = new FSMDocument();
|
|
addDocument(doc);
|
|
switchDocument(doc);
|
|
|
|
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;
|
|
}
|
|
});
|
|
|
|
let prevSelectedObject;
|
|
window.addEventListener('blur', () => {
|
|
prevSelectedObject = selectedObject;
|
|
currentConnection = null;
|
|
selectedObject = null;
|
|
shiftPressed = false;
|
|
});
|
|
|
|
window.addEventListener('focus', (s) => {
|
|
selectedObject = prevSelectedObject;
|
|
resetCaret();
|
|
});
|
|
|
|
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 = convertLatexShortcuts(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);
|
|
|
|
let drawText = true;
|
|
|
|
if (isSelected && canvasHasFocus() && document.hasFocus()) {
|
|
const caretX = x + this.measureText(text.substring(0, caretPos)).width;
|
|
|
|
if (caretSelection !== null && caretPos !== caretSelection) {
|
|
drawText = false;
|
|
const selectionX = x + this.measureText(text.substring(0, caretSelection)).width;
|
|
|
|
ctx.fillStyle = settings.colors.selectionBg;
|
|
this.beginPath();
|
|
|
|
this.moveTo(selectionX, y - 10);
|
|
this.lineTo(selectionX, y + 10);
|
|
this.lineTo(caretX, y + 10);
|
|
this.lineTo(caretX, y - 10);
|
|
|
|
this.closePath();
|
|
this.fill();
|
|
|
|
let start = caretPos;
|
|
let end = caretSelection;
|
|
if (caretSelection < caretPos) {
|
|
[start, end] = [end, start];
|
|
}
|
|
const prevText = text.substring(0, start);
|
|
const selectedText = text.substring(start, end);
|
|
const followingText = text.substring(end, text.length);
|
|
|
|
let textX = x;
|
|
const textY = y + 6;
|
|
|
|
ctx.fillStyle = settings.colors.active;
|
|
this.fillText(prevText, textX, textY);
|
|
textX += this.measureText(prevText).width;
|
|
|
|
ctx.fillStyle = settings.colors.selectionText;
|
|
this.fillText(selectedText, textX, textY);
|
|
textX += this.measureText(selectedText).width;
|
|
|
|
ctx.fillStyle = settings.colors.active;
|
|
this.fillText(followingText, textX, textY);
|
|
}
|
|
|
|
if (caretVisible) {
|
|
this.beginPath();
|
|
|
|
this.moveTo(caretX, y - 10);
|
|
this.lineTo(caretX, y + 10);
|
|
|
|
this.stroke();
|
|
this.closePath();
|
|
}
|
|
}
|
|
|
|
if (drawText)
|
|
this.fillText(text, x, y + 6);
|
|
}
|
|
};
|
|
|
|
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) {
|
|
let snapX = false,
|
|
snapY = false;
|
|
assistantLines = [];
|
|
|
|
for (let state of documents[activeDocument].states) {
|
|
if (state === targetState)
|
|
continue;
|
|
|
|
if (!snapX && Math.abs(targetState.x - state.x) < settings.snapToPadding) {
|
|
targetState.x = state.x;
|
|
assistantLines.push({x: state.x});
|
|
snapX = true;
|
|
}
|
|
|
|
if (!snapY && Math.abs(targetState.y - state.y) < settings.snapToPadding) {
|
|
targetState.y = state.y;
|
|
assistantLines.push({y: state.y});
|
|
snapY = true;
|
|
}
|
|
|
|
if (snapX && snapY)
|
|
return;
|
|
}
|
|
|
|
if (settings.drawGrid && settings.snapToGrid) {
|
|
for (let i = 100; i < width; i += 100) {
|
|
if (!snapX && Math.abs(targetState.x - i) < settings.snapToPadding) {
|
|
targetState.x = i;
|
|
assistantLines.push({x: i});
|
|
snapX = true;
|
|
}
|
|
|
|
if (snapX && snapY)
|
|
return;
|
|
}
|
|
|
|
for (let i = settings.gridSize; i < height; i += settings.gridSize) {
|
|
if (!snapY && Math.abs(targetState.y - i) < settings.snapToPadding) {
|
|
targetState.y = i;
|
|
assistantLines.push({y: i});
|
|
snapY = true;
|
|
}
|
|
|
|
if (snapX && snapY)
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
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) / realWidth * width,
|
|
y: (mouse.y - elem.y) / realHeight * height,
|
|
};
|
|
}
|
|
|
|
function guid() {
|
|
function s4() {
|
|
return Math.floor((1 + Math.random()) * 0x10000)
|
|
.toString(16)
|
|
.substring(1);
|
|
}
|
|
|
|
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
|
|
s4() + '-' + s4() + s4() + s4();
|
|
}
|
|
|
|
window.addEventListener('load', () => {
|
|
onResize();
|
|
const doc = new FSMDocument('Dokument1');
|
|
addDocument(doc);
|
|
switchDocument(doc);
|
|
|
|
tick();
|
|
draw();
|
|
});
|
|
|
|
window.addEventListener('beforeunload', (e) => {
|
|
if (!!documents.find(item => item.unsaved)) {
|
|
e.preventDefault();
|
|
e.returnValue('');
|
|
}
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
onResize();
|
|
});
|
|
|
|
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;
|
|
};
|