General improvements

This commit is contained in:
KingOfDog 2019-04-03 16:15:15 +02:00 committed by Marcel
parent 1bf43ef28d
commit 23c24ded96
11 changed files with 828 additions and 152 deletions

View File

@ -30,7 +30,7 @@
padding: 0 40px; padding: 0 40px;
} }
input { input:not([type=checkbox]) {
width: 100%; width: 100%;
font-size: 16px; font-size: 16px;
padding: 8px 16px; padding: 8px 16px;
@ -44,6 +44,11 @@
background-color: rgba(0, 0, 0, .2); background-color: rgba(0, 0, 0, .2);
} }
label {
display: block;
margin-bottom: 8px;
}
/** /**
* Buttons * Buttons
*/ */
@ -127,7 +132,7 @@
.action-button { .action-button {
background: #fff; background: #fff;
margin: 0 8px; margin: 4px 8px;
} }
.action-button .fa { .action-button .fa {
@ -199,11 +204,109 @@
margin: 0 auto; margin: 0 auto;
background: #fff; background: #fff;
border-radius: 10px; border-radius: 10px;
width: calc(100% - 20px);
} }
#previewImage { #previewImage {
width: 100%; width: 100%;
} }
.explanation {
max-width: 800px;
margin: 10px auto;
padding: 0 20px;
}
/**
* Context Menu
*/
.contextmenu {
position: absolute;
top: 0;
left: 0;
display: none;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, .33);
box-shadow: 0 0 4px rgba(0, 0, 0, .15);
margin: 0;
padding: 0;
list-style: none;
min-width: 150px;
background: #fff;
}
.contextmenu > li {
padding: 8px 16px;
cursor: pointer;
}
.contextmenu > li:first-child {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.contextmenu > li:last-child {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.contextmenu > li:hover {
background: rgba(0, 0, 0, .1);
}
.contextmenu > li:active {
background: rgba(0, 0, 0, .2);
}
/**
* Drag Overlay
*/
#dragOverlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, .75);
padding: 20px;
color: #fff;
pointer-events: none;
opacity: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
-webkit-transition: opacity .25s;
-moz-transition: opacity .25s;
-ms-transition: opacity .25s;
-o-transition: opacity .25s;
transition: opacity .25s;
}
#dragOverlay.visible {
opacity: 1;
pointer-events: all;
cursor: pointer;
}
#dragOverlay .border {
width: 100%;
height: 100%;
border-radius: 10px;
border: 4px dashed white;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
#dragOverlay .content {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
</style> </style>
</head> </head>
<body> <body>
@ -235,7 +338,43 @@
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<div class="explanation">
<p>Oben befindet sich der sogenannter FSM-Designer. Dieser ermöglicht es unter anderem deterministische endliche Automaten zu erstellen.</p>
<p>Funktionsweise:</p>
<ul>
<li><b>Zustand hinzufügen:</b> Doppelklick</li>
<li><b>Übergang hinzufügen:</b> <code>Shift</code> gedrückt halten und mit linker Maustaste ziehen</li>
<li><b>Verschieben:</b> klassisches Drag'n'drop</li>
<li><b>Endzustand:</b> Doppelklick auf einen bestehenden Zustand</li>
<li><b>Index:</b> Unterstrich vor eine Zahl hinzufügen (z.B. <code>q_0</code>)</li>
<li><b>Griechischer Buchstabe:</b> Name des Buchstaben mit Backslash (z.B. <code>\epsilon</code>)</li>
</ul>
<p>Diese Anwendung wurde mithilfe von HTML5 Canvas und JavaScript ECMA2016 erstellt.</p>
<p>Der Source Code ist open source auf <a href="https://git.kingofdog.eu/KingOfDog/fsm-designer">Gitea</a> verfügbar (demnächst womöglich auch über Github).</p>
<p>Made with <i class="fa fa-heart"></i> by <a href="https://kingofdog.eu">KingOfDog</a>.</p>
</div>
<ul id="contextmenuCanvas" class="contextmenu">
<li>Test</li>
</ul>
<div id="dragOverlay">
<div class="border"></div>
<div class="content">
<h1>Datei ablegen (.fsm)</h1>
<h3>Dokument importieren und weiterarbeiten!</h3>
</div>
</div>
<script src="lib/tingle/tingle.min.js"></script> <script src="lib/tingle/tingle.min.js"></script>
<script src="lib/contextmenu.js/contextMenu.min.js"></script>
<script src="js/math.js"></script> <script src="js/math.js"></script>
<script src="js/export/export.js"></script> <script src="js/export/export.js"></script>
@ -245,7 +384,7 @@
<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/FSMDocument.js"></script> <script src="js/fsm-document.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script src="js/simulate.js"></script> <script src="js/simulate.js"></script>
<script src="js/menu.js"></script> <script src="js/menu.js"></script>

View File

@ -1,86 +0,0 @@
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;
}
}
onRightClick(x, y) {
if(!selectedObject) {
}
}
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;
}
}
}

View File

@ -1,15 +1,26 @@
class Connection { class Connection {
constructor(stateA, stateB) { constructor(stateA, stateB) {
this.id = guid();
this.stateA = stateA; this.stateA = stateA;
this.stateB = stateB; this.stateB = stateB;
this.text = ''; this._text = '';
this.lineAngleAdjust = 0; this.lineAngleAdjust = 0;
this.parallelPart = 0.5; this.parallelPart = 0.5;
this.perpendicularPart = 0; this.perpendicularPart = 0;
} }
get text() {
return this._text;
}
set text(value) {
documents[activeDocument].addChange('connection', this.id, 'edit', ['_text'], [this._text], true);
this._text = value;
documents[activeDocument].addChange('connection', this.id, 'edit', ['_text'], [this._text]);
}
getAnchorPoint() { getAnchorPoint() {
const dx = this.stateB.x - this.stateA.x; const dx = this.stateB.x - this.stateA.x;
const dy = this.stateB.y - this.stateA.y; const dy = this.stateB.y - this.stateA.y;
@ -103,11 +114,11 @@ class Connection {
ctx.closePath(); ctx.closePath();
// Draws the text onto the line // Draws the text onto the line
if(stuff.isCircle) { if (stuff.isCircle) {
let startAngle = stuff.startAngle, let startAngle = stuff.startAngle,
endAngle = stuff.endAngle; endAngle = stuff.endAngle;
if(endAngle < startAngle) { if (endAngle < startAngle) {
endAngle += Math.PI * 2; endAngle += Math.PI * 2;
} }

View File

@ -1,16 +1,27 @@
class SelfConnection { class SelfConnection {
constructor(state, mouse) { constructor(state, mouse) {
this.id = guid();
this.state = state; this.state = state;
this.anchorAngle = 0; this.anchorAngle = 0;
this.mouseOffsetAngle = 0; this.mouseOffsetAngle = 0;
this.text = ''; this._text = '';
if(mouse) { if(mouse) {
this.setAnchorPoint(mouse.x, mouse.y); this.setAnchorPoint(mouse.x, mouse.y);
} }
} }
get text() {
return this._text;
}
set text(value) {
documents[activeDocument].addChange('selfconnection', this.id, 'edit', ['_text'], [this._text], true);
this._text = value;
documents[activeDocument].addChange('selfconnection', this.id, 'edit', ['_text'], [this._text]);
}
setMouseStart(x, y) { setMouseStart(x, y) {
this.mouseOffsetAngle = this.anchorAngle - Math.atan2(y - this.state.y, x - this.state.x); this.mouseOffsetAngle = this.anchorAngle - Math.atan2(y - this.state.y, x - this.state.x);
} }

View File

@ -1,25 +1,36 @@
class StartConnection { class StartConnection {
constructor(state, start) { constructor(state, start) {
this.id = guid();
this.state = state; this.state = state;
this.deltaX = 0; this.deltaX = 0;
this.deltaY = 0; this.deltaY = 0;
this.text = ''; this._text = '';
if(start) { if (start) {
this.setAnchorPoint(start.x, start.y); this.setAnchorPoint(start.x, start.y);
} }
} }
get text() {
return this._text;
}
set text(value) {
documents[activeDocument].addChange('startconnection', this.id, 'edit', ['_text'], [this._text], true);
this._text = value;
documents[activeDocument].addChange('startconnection', this.id, 'edit', ['_text'], [this._text]);
}
setAnchorPoint(x, y) { setAnchorPoint(x, y) {
this.deltaX = x - this.state.x; this.deltaX = x - this.state.x;
this.deltaY = y - this.state.y; this.deltaY = y - this.state.y;
if(Math.abs(this.deltaX) < settings.snapToPadding) { if (Math.abs(this.deltaX) < settings.snapToPadding) {
this.deltaX = 0; this.deltaX = 0;
} }
if(Math.abs(this.deltaY) < settings.snapToPadding) { if (Math.abs(this.deltaY) < settings.snapToPadding) {
this.deltaY = 0; this.deltaY = 0;
} }
} }

View File

@ -10,7 +10,17 @@ class State {
this.isActive = false; this.isActive = false;
this.activeTime = 0; this.activeTime = 0;
this.isAcceptState = false; this.isAcceptState = false;
this.text = ''; this._text = '';
}
get text() {
return this._text;
}
set text(value) {
documents[activeDocument].addChange('state', this.id, 'edit', ['_text'], [this._text], true);
this._text = value;
documents[activeDocument].addChange('state', this.id, 'edit', ['_text'], [this._text]);
} }
setMouseStart(x, y) { setMouseStart(x, y) {
@ -58,7 +68,7 @@ class State {
} }
ctx.fillStyle = settings.colors.getColor(this); ctx.fillStyle = settings.colors.getColor(this);
const width = ctx.measureText(this.text).width; const width = ctx.measureText(convertLatexShortcuts(this.text)).width;
if(width < radius * .9) if(width < radius * .9)
ctx.drawText(this.text, this.x, this.y, null, selectedObject === this); ctx.drawText(this.text, this.x, this.y, null, selectedObject === this);
else else
@ -134,5 +144,4 @@ class State {
containsPoint(x, y) { containsPoint(x, y) {
return (x - this.x) ** 2 + (y - this.y) ** 2 < radius ** 2; return (x - this.x) ** 2 + (y - this.y) ** 2 < radius ** 2;
} }
} }

View File

@ -67,7 +67,7 @@ function parseFromJson(json) {
break; break;
case 'SelfConnection': case 'SelfConnection':
connection = Object.assign(new SelfConnection(null, {x: 0, y: 0}), connection); connection = Object.assign(new SelfConnection(null), connection);
connection.state = doc.states.find(state => state.id === connection.state); connection.state = doc.states.find(state => state.id === connection.state);

187
js/fsm-document.js Normal file
View File

@ -0,0 +1,187 @@
class FSMDocument {
constructor(name) {
this.id = guid();
this.name = name || null;
this.createdAt = Date.now();
this.lastModified = Date.now();
this.element = null;
this.unsaved = true;
this.lastSavedHash = '';
this.states = [];
this.connections = [];
this.changes = [];
this.changesIndex = -1;
}
physicsTick() {
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;
stateA.x += Math.cos(angle1) * inter;
stateA.y += Math.sin(angle1) * inter;
stateB.x += Math.cos(angle2) * inter;
stateB.y += Math.sin(angle2) * inter;
}
});
});
}
tick() {
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);
this.addChange('state', selectedObject.id, 'add', Object.keys(selectedObject), Object.values(selectedObject));
resetCaret();
} else if (selectedObject instanceof State) {
selectedObject.isAcceptState = !selectedObject.isAcceptState;
this.addChange('state', selectedObject.id, 'edit', ['isAcceptState'], [!selectedObject.isAcceptState]);
}
}
onRightClick(x, y) {
if (!selectedObject) {
}
}
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;
}
}
addChange(objectType, id, action, fields, oldValues, tryCombiningChanges) {
if (this.changesIndex < this.changes.length - 1) {
this.changes.splice(this.changesIndex + 1, this.changes.length - this.changesIndex - 1);
}
if (tryCombiningChanges) {
let prevChange = this.changes[this.changesIndex];
while (!!prevChange && prevChange.type === objectType && prevChange.id === id && prevChange.action === action && prevChange.fields.length === fields.length) {
oldValues = prevChange.prevValues;
this.changes.splice(this.changesIndex, 1);
this.changesIndex--;
prevChange = this.changes[this.changesIndex];
}
}
this.changes.push({
type: objectType,
id,
action,
fields,
prevValues: oldValues,
date: Date.now()
});
this.changesIndex++;
}
undo(stepAmount) {
if (stepAmount === 0)
return;
const loopAmount = Math.abs(stepAmount);
const isRedo = stepAmount < 0;
for (let i = 0; i < loopAmount; i++) {
if (this.changes.length === 0 || (!isRedo && this.changesIndex < 0) || (isRedo && this.changesIndex >= this.changes.length - 1)) {
return;
}
let change;
if (isRedo) {
this.changesIndex++;
change = this.changes[this.changesIndex];
} else {
change = Object.assign({}, this.changes[this.changesIndex]);
this.changesIndex--;
change.action = change.action === 'add' ? 'remove' : change.action === 'remove' ? 'add' : 'edit';
}
if (change.type === 'state') {
if (change.action === 'remove') {
this.states.splice(this.states.findIndex(item => item.id === change.id), 1);
return;
}
let state;
if (change.action === 'add')
state = new State();
else if (change.action === 'edit')
state = this.states.find(item => item.id === change.id);
change.fields.forEach((field, index) => {
state[field] = change.prevValues[index];
});
if (change.action === 'add')
this.states.push(state);
}
if (change.type.indexOf('connection') !== -1) {
if (change.action === 'remove') {
this.connections.splice(this.connections.findIndex(item => item.id === change.id), 1);
return;
}
let connection;
if (change.action === 'add') {
if(change.type === 'startconnection')
connection = new StartConnection();
else if(change.type === 'selfconnection')
connection = new SelfConnection();
else
connection = new Connection();
} else if (change.action === 'edit') {
connection = this.connections.find(item => item.id === change.id);
}
change.fields.forEach((field, index) => {
connection[field] = change.prevValues[index];
});
if (change.action === 'add')
this.connections.push(connection);
}
}
}
}

View File

@ -1,7 +1,11 @@
const width = 800; const width = 800;
const height = 600; const height = 600;
let realWidth = 800;
let realHeight = 600;
const radius = 25; 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 = { const settings = {
physics: true, physics: true,
speed: 2, speed: 2,
@ -10,29 +14,47 @@ const settings = {
colors: { colors: {
default: '#000', default: '#000',
active: '#00f', 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 getColor: (object) => selectedObject === object ? settings.colors.active : settings.colors.default
} },
drawGrid: true,
snapToGrid: true,
gridSize: 100,
}; };
const elements = { const elements = {
documents: document.getElementById('document-tabs'), documents: document.getElementById('document-tabs'),
dragOverlay: document.getElementById('dragOverlay'),
}; };
const contextmenu = document.getElementById('contextmenuCanvas');
const canvas = document.getElementById('canvas'); const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
canvas.width = width; function onResize() {
canvas.height = height; 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 = []; const documents = [];
let activeDocument = null; let activeDocument = null;
let assistantLines = [];
function switchDocument(doc) { function switchDocument(doc) {
if (doc instanceof FSMDocument) { if (doc instanceof FSMDocument) {
doc = documents.findIndex(document => document.id === doc.id); doc = documents.findIndex(document => document.id === doc.id);
} }
if(doc < 0) { if (doc < 0) {
activeDocument = null; activeDocument = null;
} }
@ -66,7 +88,7 @@ function addDocument(doc) {
const btn = elem.addChild('button', ['btn', 'btn-close'], 'X'); const btn = elem.addChild('button', ['btn', 'btn-close'], 'X');
elem.addEventListener('click', (e) => { elem.addEventListener('click', (e) => {
if(e.target === elem || e.target === label) if (e.target === elem || e.target === label)
switchDocument(doc); switchDocument(doc);
}); });
@ -90,10 +112,10 @@ function closeDocument(doc) {
const docElements = document.getElementsByClassName('document-tab'); const docElements = document.getElementsByClassName('document-tab');
docElements[doc].remove(); docElements[doc].remove();
if(doc < activeDocument) { if (doc < activeDocument) {
activeDocument--; activeDocument--;
} else if(doc === activeDocument) { } else if (doc === activeDocument) {
if(activeDocument >= documents.length) { if (activeDocument >= documents.length) {
activeDocument = documents.length - 1; activeDocument = documents.length - 1;
} }
} }
@ -106,6 +128,11 @@ const animations = [];
let isPaused = false; let isPaused = false;
function convertLatexShortcuts(text) { 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++) { for (let i = 0; i < 10; i++) {
// TODO: Replace with more general way // TODO: Replace with more general way
text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i)); text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));
@ -116,7 +143,8 @@ function convertLatexShortcuts(text) {
let caretTimer, let caretTimer,
caretVisible = true, caretVisible = true,
caretPos = 0; caretPos = 0,
caretSelection = null;
function resetCaret() { function resetCaret() {
clearInterval(caretTimer); clearInterval(caretTimer);
@ -126,6 +154,9 @@ function resetCaret() {
function tick() { function tick() {
if (activeDocument !== null) { if (activeDocument !== null) {
for (let i = 0; i < 10; i++) {
documents[activeDocument].physicsTick();
}
documents[activeDocument].tick(); documents[activeDocument].tick();
} }
@ -139,6 +170,22 @@ function draw() {
ctx.translate(0.5, 0.5); ctx.translate(0.5, 0.5);
if (activeDocument !== null) { 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(); documents[activeDocument].draw();
if (!!currentConnection) { if (!!currentConnection) {
@ -160,6 +207,24 @@ function draw() {
setTimeout(draw, 1000 / 60); 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) { function selectObject(x, y) {
if (activeDocument === null) { if (activeDocument === null) {
return null; return null;
@ -169,12 +234,14 @@ function selectObject(x, y) {
for (let state of doc.states) { for (let state of doc.states) {
if (state.containsPoint(x, y)) { if (state.containsPoint(x, y)) {
caretPos = state.text.length;
return state; return state;
} }
} }
for (let connection of doc.connections) { for (let connection of doc.connections) {
if (connection.containsPoint(x, y)) { if (connection.containsPoint(x, y)) {
caretPos = connection.text.length;
return connection; return connection;
} }
} }
@ -191,7 +258,10 @@ let cursorVisible = true,
originalClick = null, originalClick = null,
deltaMouseX = 0, deltaMouseX = 0,
deltaMouseY = 0, deltaMouseY = 0,
shiftPressed = false; shiftPressed = false,
ctrlPressed = false,
changedKeys = [],
changedValues = [];
canvas.addEventListener('mousedown', (event) => { canvas.addEventListener('mousedown', (event) => {
const mouse = cbRelMousePos(event); const mouse = cbRelMousePos(event);
@ -200,7 +270,7 @@ canvas.addEventListener('mousedown', (event) => {
originalClick = mouse; originalClick = mouse;
if (!!selectedObject) { if (!!selectedObject) {
if (shiftPressed && selectedObject instanceof State) { if ((shiftPressed || event.shiftKey) && selectedObject instanceof State) {
currentConnection = new SelfConnection(selectedObject, mouse); currentConnection = new SelfConnection(selectedObject, mouse);
} else { } else {
movingObject = true; movingObject = true;
@ -210,7 +280,21 @@ canvas.addEventListener('mousedown', (event) => {
} }
} }
resetCaret(); resetCaret();
} else if (shiftPressed) {
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); currentConnection = new TemporaryConnection(mouse, mouse);
} }
@ -249,25 +333,52 @@ canvas.addEventListener('mousemove', (event) => {
} }
if (movingObject) { if (movingObject) {
selectedObject.setAnchorPoint(mouse.x, mouse.y);
if (selectedObject instanceof State) { 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); snapNode(selectedObject);
} }
} else {
selectedObject.setAnchorPoint(mouse.x, mouse.y);
}
} }
}); });
canvas.addEventListener('mouseup', () => { document.addEventListener('mouseup', () => {
movingObject = false; movingObject = false;
if (!!selectedObject && activeDocument !== null) {
if (selectedObject instanceof State) {
documents[activeDocument].addChange('state', selectedObject.id, 'edit', changedKeys, changedValues);
} else if (selectedObject instanceof Connection || selectedObject instanceof SelfConnection || selectedObject instanceof StartConnection) {
documents[activeDocument].addChange(selectedObject.constructor.name.toLowerCase(), selectedObject.id, 'edit', changedKeys, changedValues);
}
}
if (!!currentConnection && activeDocument !== null) { if (!!currentConnection && activeDocument !== null) {
if (!(currentConnection instanceof TemporaryConnection)) { if (!(currentConnection instanceof TemporaryConnection)) {
selectedObject = currentConnection; selectedObject = currentConnection;
documents[activeDocument].connections.push(currentConnection); documents[activeDocument].connections.push(currentConnection);
resetCaret(); resetCaret();
documents[activeDocument].addChange(selectedObject.constructor.name.toLowerCase(), selectedObject.id, 'add', Object.keys(selectedObject), Object.values(selectedObject))
} }
currentConnection = null; currentConnection = null;
} }
assistantLines = [];
}); });
canvas.addEventListener('dblclick', (event) => { canvas.addEventListener('dblclick', (event) => {
@ -281,17 +392,29 @@ canvas.addEventListener('dblclick', (event) => {
documents[activeDocument].onDblClick(mouse.x, mouse.y); documents[activeDocument].onDblClick(mouse.x, mouse.y);
}); });
canvas.addEventListener('contextmenu', (event) => { document.addEventListener('click', (event) => {
contextmenu.style.display = 'none';
});
document.addEventListener('contextmenu', (event) => {
const mouse = cbRelMousePos(event); const mouse = cbRelMousePos(event);
event.preventDefault(); event.preventDefault();
if(activeDocument === null) if (activeDocument === null)
return; return;
selectedObject = selectObject(mouse.x, mouse.y); // selectedObject = selectObject(mouse.x, mouse.y);
documents[activeDocument].onRightClick(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) => { document.addEventListener('keydown', (event) => {
@ -307,9 +430,24 @@ document.addEventListener('keydown', (event) => {
break; break;
case 8: // Backspace case 8: // Backspace
if (!!selectedObject) { if (!!selectedObject) {
selectedObject.text = selectedObject.text.substr(0, caretPos - 1) + selectedObject.text.substr(caretPos, selectedObject.text.length); 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--;
caretPos = Math.max(caretPos, 0); caretPos = Math.max(caretPos, 0);
}
selectedObject.text = selectedObject.text.slice(0, start) + selectedObject.text.slice(end, selectedObject.text.length);
resetCaret(); resetCaret();
} }
@ -318,36 +456,60 @@ document.addEventListener('keydown', (event) => {
return false; return false;
case 37: // Left Arrow case 37: // Left Arrow
if (!(event.ctrlKey || event.metaKey) && !!selectedObject) { if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
if ((shiftPressed || event.shiftKey) && caretSelection === null)
caretSelection = caretPos;
caretPos--; caretPos--;
caretPos = Math.max(caretPos, 0); caretPos = Math.max(caretPos, 0);
resetCaret(); resetCaret();
if (!(shiftPressed || event.shiftKey) || caretSelection === caretPos)
caretSelection = null;
return false; return false;
} }
break; break;
case 39: // Right Arrow case 39: // Right Arrow
if (!(event.ctrlKey || event.metaKey) && !!selectedObject) { if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
if ((shiftPressed || event.shiftKey) && caretSelection === null)
caretSelection = caretPos;
caretPos++; caretPos++;
caretPos = Math.min(caretPos, selectedObject.text.length); caretPos = Math.min(caretPos, selectedObject.text.length);
resetCaret(); resetCaret();
if (!(shiftPressed || event.shiftKey) || caretSelection === caretPos)
caretSelection = null;
return false; return false;
} }
break; break;
case 36: // Home case 36: // Home
if(!(event.ctrlKey || event.metaKey) && !!selectedObject) { if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
if ((shiftPressed || event.shiftKey) && caretSelection === null)
caretSelection = caretPos;
caretPos = 0; caretPos = 0;
resetCaret(); resetCaret();
if (!(shiftPressed || event.shiftKey) || caretSelection === caretPos)
caretSelection = null;
return false; return false;
} }
break; break;
case 35: // End case 35: // End
if(!(event.ctrlKey || event.metaKey) && !!selectedObject) { if (!(event.ctrlKey || event.metaKey) && !!selectedObject) {
if ((shiftPressed || event.shiftKey) && caretSelection === null)
caretSelection = caretPos;
caretPos = selectedObject.text.length; caretPos = selectedObject.text.length;
if (!(shiftPressed || event.shiftKey) || caretSelection === caretPos)
caretSelection = null;
return false; return false;
} }
@ -376,13 +538,23 @@ document.addEventListener('keypress', (event) => {
} }
if (!!selectedObject && 'text' in selectedObject) { if (!!selectedObject && 'text' in selectedObject) {
const text = selectedObject.text;
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); 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++; caretPos++;
resetCaret(); resetCaret();
selectedObject.text = text;
return false; return false;
} }
@ -390,10 +562,34 @@ document.addEventListener('keypress', (event) => {
} }
if (key === 8) { if (key === 8) {
selectedObject = null;
return false; 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) => { window.addEventListener('keydown', (event) => {
const keyCode = crossBrowserKey(event); const keyCode = crossBrowserKey(event);
const key = String.fromCharCode(keyCode).toLowerCase(); const key = String.fromCharCode(keyCode).toLowerCase();
@ -408,6 +604,34 @@ window.addEventListener('keydown', (event) => {
return false; 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 if ((event.ctrlKey || event.metaKey) && key === 'e') { // Ctrl + E
modalExport.open(); modalExport.open();
@ -415,6 +639,15 @@ window.addEventListener('keydown', (event) => {
return false; 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 if ((event.ctrlKey || event.metaKey) && keyCode === 37) { // Ctrl + LeftArrow
let activeDoc = activeDocument; let activeDoc = activeDocument;
activeDoc--; activeDoc--;
@ -438,6 +671,19 @@ window.addEventListener('keydown', (event) => {
} }
}); });
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) { CanvasRenderingContext2D.prototype.drawArrow = function (x, y, angle) {
const dx = Math.cos(angle); const dx = Math.cos(angle);
const dy = Math.sin(angle); const dy = Math.sin(angle);
@ -474,19 +720,65 @@ CanvasRenderingContext2D.prototype.drawText = function (originalText, x, y, angl
} else { } else {
x = Math.round(x); x = Math.round(x);
y = Math.round(y); y = Math.round(y);
this.fillText(text, x, y + 6);
if (isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) {
x += this.measureText(text.substring(0, caretPos)).width;
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.beginPath();
this.moveTo(x, y - 10); this.moveTo(selectionX, y - 10);
this.lineTo(x, 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.stroke();
this.closePath(); this.closePath();
} }
} }
if (drawText)
this.fillText(text, x, y + 6);
}
}; };
Node.prototype.addChild = function (name, classList, text) { Node.prototype.addChild = function (name, classList, text) {
@ -508,16 +800,51 @@ Node.prototype.addChild = function (name, classList, text) {
}; };
function snapNode(targetState) { function snapNode(targetState) {
let snapX = false,
snapY = false;
assistantLines = [];
for (let state of documents[activeDocument].states) { for (let state of documents[activeDocument].states) {
if (state === targetState) if (state === targetState)
continue; continue;
if (Math.abs(targetState.x - state.x) < settings.snapToPadding) { if (!snapX && Math.abs(targetState.x - state.x) < settings.snapToPadding) {
targetState.x = state.x; targetState.x = state.x;
assistantLines.push({x: state.x});
snapX = true;
} }
if (Math.abs(targetState.y - state.y) < settings.snapToPadding) { if (!snapY && Math.abs(targetState.y - state.y) < settings.snapToPadding) {
targetState.y = state.y; 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;
} }
} }
} }
@ -556,8 +883,8 @@ function cbRelMousePos(event) {
const elem = cbElementPos(event), const elem = cbElementPos(event),
mouse = cbMousePos(event); mouse = cbMousePos(event);
return { return {
x: mouse.x - elem.x, x: (mouse.x - elem.x) / realWidth * width,
y: mouse.y - elem.y, y: (mouse.y - elem.y) / realHeight * height,
}; };
} }
@ -573,6 +900,7 @@ function guid() {
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
onResize();
const doc = new FSMDocument('Dokument1'); const doc = new FSMDocument('Dokument1');
addDocument(doc); addDocument(doc);
switchDocument(doc); switchDocument(doc);
@ -581,6 +909,17 @@ window.addEventListener('load', () => {
draw(); draw();
}); });
window.addEventListener('beforeunload', (e) => {
if (!!documents.find(item => item.unsaved)) {
e.preventDefault();
e.returnValue('');
}
});
window.addEventListener('resize', () => {
onResize();
});
Object.prototype.hashCode = function () { Object.prototype.hashCode = function () {
let baseString = ''; let baseString = '';
Object.values(this).forEach(entry => { Object.values(this).forEach(entry => {

View File

@ -11,7 +11,7 @@ modalSave.addFooterBtn('Speichern', 'btn', () => {
const input = document.getElementById('saveName'); const input = document.getElementById('saveName');
const name = input.value; const name = input.value;
if(name.trim().length > 0) { if (name.trim().length > 0) {
documents[activeDocument].name = name; documents[activeDocument].name = name;
saveToLocalStorage(); saveToLocalStorage();
modalSave.close(); modalSave.close();
@ -30,7 +30,7 @@ const modalOpen = new tingle.modal({
html += '</ul>'; html += '</ul>';
modalOpen.setContent(html); modalOpen.setContent(html);
for(let element of document.getElementsByClassName('file-list-item')) { for (let element of document.getElementsByClassName('file-list-item')) {
element.addEventListener('click', () => { element.addEventListener('click', () => {
const entry = entries.find(entry => entry.document.id === element.getAttribute('data-id')); const entry = entries.find(entry => entry.document.id === element.getAttribute('data-id'));
addDocument(entry.document); addDocument(entry.document);
@ -44,8 +44,10 @@ const modalOpen = new tingle.modal({
const modalExport = new tingle.modal({ const modalExport = new tingle.modal({
footer: true, footer: true,
beforeOpen: () => { beforeOpen: () => {
setTimeout(() => {
const el = document.getElementById('previewImage'); const el = document.getElementById('previewImage');
el.src = canvas.toDataURL('image/png'); el.src = canvas.toDataURL('image/png');
}, 100);
} }
}); });
modalExport.setContent('<h3>Dokument exportieren</h3><img src="" id="previewImage">'); modalExport.setContent('<h3>Dokument exportieren</h3><img src="" id="previewImage">');
@ -69,7 +71,8 @@ modalImport.addFooterBtn('Laden', 'btn', () => {
const el = document.getElementById('importUpload'); const el = document.getElementById('importUpload');
const file = el.files[0]; const file = el.files[0];
if(file) { if (file) {
console.log(file.type);
const reader = new FileReader(); const reader = new FileReader();
// TODO: Check for file type etc. // TODO: Check for file type etc.
reader.readAsText(file, 'UTF-8'); reader.readAsText(file, 'UTF-8');
@ -89,19 +92,20 @@ modalImport.addFooterBtn('Laden', 'btn', () => {
const modalSimulate = new tingle.modal({ const modalSimulate = new tingle.modal({
footer: true, footer: true,
}); });
modalSimulate.setContent('<h3>Maschine simulieren</h3> <label for="simulationInput">Eingabe</label> <input type="text" id="simulationInput">'); modalSimulate.setContent('<h3>Maschine simulieren</h3> <label for="singleCharMode"><input type="checkbox" id="singleCharMode" checked> Eingabe einzelner Buchstaben</label> <br> <label for="simulationInput">Eingabe</label> <input type="text" id="simulationInput">');
modalSimulate.addFooterBtn('Simulation starten', 'btn', () => { modalSimulate.addFooterBtn('Simulation starten', 'btn', () => {
modalSimulate.close(); modalSimulate.close();
const input = document.getElementById('simulationInput').value; const input = document.getElementById('simulationInput').value;
simulate(input); const singleCharMode = document.getElementById('singleCharMode').checked;
simulate(input, singleCharMode);
}); });
document.getElementById('saveBtn').addEventListener('click', () => { document.getElementById('saveBtn').addEventListener('click', () => {
if(activeDocument === null) { if (activeDocument === null) {
return; return;
} }
if(documents[activeDocument].name !== null) { if (documents[activeDocument].name !== null) {
saveToLocalStorage(); saveToLocalStorage();
} else { } else {
modalSave.open(); modalSave.open();
@ -109,23 +113,75 @@ document.getElementById('saveBtn').addEventListener('click', () => {
}); });
document.getElementById('addBtn').addEventListener('click', () => { document.getElementById('addBtn').addEventListener('click', () => {
selectedObject = null;
currentConnection = null;
const doc = new FSMDocument(null); const doc = new FSMDocument(null);
addDocument(doc); addDocument(doc);
switchDocument(doc); switchDocument(doc);
}); });
document.getElementById('openBtn').addEventListener('click', () => { document.getElementById('openBtn').addEventListener('click', () => {
selectedObject = null;
currentConnection = null;
modalOpen.open(); modalOpen.open();
}); });
document.getElementById('importBtn').addEventListener('click', () => { document.getElementById('importBtn').addEventListener('click', () => {
selectedObject = null;
currentConnection = null;
modalImport.open(); modalImport.open();
}); });
document.getElementById('exportBtn').addEventListener('click', () => { document.getElementById('exportBtn').addEventListener('click', () => {
selectedObject = null;
currentConnection = null;
modalExport.open(); modalExport.open();
}); });
document.getElementById('simulateBtn').addEventListener('click', () => { document.getElementById('simulateBtn').addEventListener('click', () => {
selectedObject = null;
currentConnection = null;
modalSimulate.open(); modalSimulate.open();
}); });
document.addEventListener('dragover', (event) => {
elements.dragOverlay.classList.add('visible');
event.preventDefault();
});
document.addEventListener('dragexit', (event) => {
elements.dragOverlay.classList.remove('visible');
});
document.addEventListener('drop', (event) => {
const item = event.dataTransfer.items[0];
if (item.kind === 'file') {
const file = item.getAsFile();
const lastDot = file.name.lastIndexOf('.');
const ext = file.name.slice(lastDot + 1);
if (ext === 'fsm') {
const reader = new FileReader();
reader.readAsText(file, 'UTF-8');
reader.onload = (evt) => {
const json = evt.target.result;
// TODO: Add error handling
try {
importFromJson(json);
} catch (e) {
alert('Ein Fehler ist beim Einlesen der Datei aufgetreten. Die Datei scheint nicht im richtigen Format zu sein.')
}
};
reader.onerror = (evt) => {
alert('Ein Fehler beim Einlesen der Datei ist aufgetreten. Womöglich muss hier ein Bug behoben werden... :(');
};
} else {
alert('Dieses Dateiformat wird nicht unterstützt.')
}
}
elements.dragOverlay.classList.remove('visible');
event.preventDefault();
return false;
});

View File

@ -1,8 +1,7 @@
let simulationStates = []; let simulationStates = [];
let singleCharMode = true;
let simulationStepDuration = 500; let simulationStepDuration = 500;
function simulate(word) { function simulate(word, singleCharMode = true) {
if(activeDocument === null) { if(activeDocument === null) {
return false; return false;
} }