General improvements
This commit is contained in:
parent
1bf43ef28d
commit
23c24ded96
145
index.html
145
index.html
|
@ -30,7 +30,7 @@
|
|||
padding: 0 40px;
|
||||
}
|
||||
|
||||
input {
|
||||
input:not([type=checkbox]) {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
padding: 8px 16px;
|
||||
|
@ -44,6 +44,11 @@
|
|||
background-color: rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buttons
|
||||
*/
|
||||
|
@ -127,7 +132,7 @@
|
|||
|
||||
.action-button {
|
||||
background: #fff;
|
||||
margin: 0 8px;
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
.action-button .fa {
|
||||
|
@ -199,11 +204,109 @@
|
|||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
#previewImage {
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
|
@ -235,7 +338,43 @@
|
|||
|
||||
<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/contextmenu.js/contextMenu.min.js"></script>
|
||||
|
||||
<script src="js/math.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/temporary-connection.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/simulate.js"></script>
|
||||
<script src="js/menu.js"></script>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,26 @@
|
|||
class Connection {
|
||||
|
||||
constructor(stateA, stateB) {
|
||||
this.id = guid();
|
||||
this.stateA = stateA;
|
||||
this.stateB = stateB;
|
||||
this.text = '';
|
||||
this._text = '';
|
||||
this.lineAngleAdjust = 0;
|
||||
|
||||
this.parallelPart = 0.5;
|
||||
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() {
|
||||
const dx = this.stateB.x - this.stateA.x;
|
||||
const dy = this.stateB.y - this.stateA.y;
|
||||
|
@ -103,11 +114,11 @@ class Connection {
|
|||
ctx.closePath();
|
||||
|
||||
// Draws the text onto the line
|
||||
if(stuff.isCircle) {
|
||||
if (stuff.isCircle) {
|
||||
let startAngle = stuff.startAngle,
|
||||
endAngle = stuff.endAngle;
|
||||
|
||||
if(endAngle < startAngle) {
|
||||
if (endAngle < startAngle) {
|
||||
endAngle += Math.PI * 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
class SelfConnection {
|
||||
|
||||
constructor(state, mouse) {
|
||||
this.id = guid();
|
||||
this.state = state;
|
||||
this.anchorAngle = 0;
|
||||
this.mouseOffsetAngle = 0;
|
||||
this.text = '';
|
||||
this._text = '';
|
||||
|
||||
if(mouse) {
|
||||
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) {
|
||||
this.mouseOffsetAngle = this.anchorAngle - Math.atan2(y - this.state.y, x - this.state.x);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,36 @@
|
|||
class StartConnection {
|
||||
|
||||
constructor(state, start) {
|
||||
this.id = guid();
|
||||
this.state = state;
|
||||
this.deltaX = 0;
|
||||
this.deltaY = 0;
|
||||
this.text = '';
|
||||
this._text = '';
|
||||
|
||||
if(start) {
|
||||
if (start) {
|
||||
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) {
|
||||
this.deltaX = x - this.state.x;
|
||||
this.deltaY = y - this.state.y;
|
||||
|
||||
if(Math.abs(this.deltaX) < settings.snapToPadding) {
|
||||
if (Math.abs(this.deltaX) < settings.snapToPadding) {
|
||||
this.deltaX = 0;
|
||||
}
|
||||
|
||||
if(Math.abs(this.deltaY) < settings.snapToPadding) {
|
||||
if (Math.abs(this.deltaY) < settings.snapToPadding) {
|
||||
this.deltaY = 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,17 @@ class State {
|
|||
this.isActive = false;
|
||||
this.activeTime = 0;
|
||||
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) {
|
||||
|
@ -58,7 +68,7 @@ class State {
|
|||
}
|
||||
|
||||
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)
|
||||
ctx.drawText(this.text, this.x, this.y, null, selectedObject === this);
|
||||
else
|
||||
|
@ -134,5 +144,4 @@ class State {
|
|||
containsPoint(x, y) {
|
||||
return (x - this.x) ** 2 + (y - this.y) ** 2 < radius ** 2;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ function parseFromJson(json) {
|
|||
|
||||
break;
|
||||
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);
|
||||
|
||||
|
|
187
js/fsm-document.js
Normal file
187
js/fsm-document.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
417
js/main.js
417
js/main.js
|
@ -1,7 +1,11 @@
|
|||
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,
|
||||
|
@ -10,29 +14,47 @@ const settings = {
|
|||
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');
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
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) {
|
||||
if (doc < 0) {
|
||||
activeDocument = null;
|
||||
}
|
||||
|
||||
|
@ -66,7 +88,7 @@ function addDocument(doc) {
|
|||
const btn = elem.addChild('button', ['btn', 'btn-close'], 'X');
|
||||
|
||||
elem.addEventListener('click', (e) => {
|
||||
if(e.target === elem || e.target === label)
|
||||
if (e.target === elem || e.target === label)
|
||||
switchDocument(doc);
|
||||
});
|
||||
|
||||
|
@ -90,10 +112,10 @@ function closeDocument(doc) {
|
|||
const docElements = document.getElementsByClassName('document-tab');
|
||||
docElements[doc].remove();
|
||||
|
||||
if(doc < activeDocument) {
|
||||
if (doc < activeDocument) {
|
||||
activeDocument--;
|
||||
} else if(doc === activeDocument) {
|
||||
if(activeDocument >= documents.length) {
|
||||
} else if (doc === activeDocument) {
|
||||
if (activeDocument >= documents.length) {
|
||||
activeDocument = documents.length - 1;
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +128,11 @@ 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));
|
||||
|
@ -116,7 +143,8 @@ function convertLatexShortcuts(text) {
|
|||
|
||||
let caretTimer,
|
||||
caretVisible = true,
|
||||
caretPos = 0;
|
||||
caretPos = 0,
|
||||
caretSelection = null;
|
||||
|
||||
function resetCaret() {
|
||||
clearInterval(caretTimer);
|
||||
|
@ -126,6 +154,9 @@ function resetCaret() {
|
|||
|
||||
function tick() {
|
||||
if (activeDocument !== null) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
documents[activeDocument].physicsTick();
|
||||
}
|
||||
documents[activeDocument].tick();
|
||||
}
|
||||
|
||||
|
@ -139,6 +170,22 @@ function draw() {
|
|||
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) {
|
||||
|
@ -160,6 +207,24 @@ function draw() {
|
|||
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;
|
||||
|
@ -169,12 +234,14 @@ function selectObject(x, y) {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +258,10 @@ let cursorVisible = true,
|
|||
originalClick = null,
|
||||
deltaMouseX = 0,
|
||||
deltaMouseY = 0,
|
||||
shiftPressed = false;
|
||||
shiftPressed = false,
|
||||
ctrlPressed = false,
|
||||
changedKeys = [],
|
||||
changedValues = [];
|
||||
|
||||
canvas.addEventListener('mousedown', (event) => {
|
||||
const mouse = cbRelMousePos(event);
|
||||
|
@ -200,7 +270,7 @@ canvas.addEventListener('mousedown', (event) => {
|
|||
originalClick = mouse;
|
||||
|
||||
if (!!selectedObject) {
|
||||
if (shiftPressed && selectedObject instanceof State) {
|
||||
if ((shiftPressed || event.shiftKey) && selectedObject instanceof State) {
|
||||
currentConnection = new SelfConnection(selectedObject, mouse);
|
||||
} else {
|
||||
movingObject = true;
|
||||
|
@ -210,7 +280,21 @@ canvas.addEventListener('mousedown', (event) => {
|
|||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -249,25 +333,52 @@ canvas.addEventListener('mousemove', (event) => {
|
|||
}
|
||||
|
||||
if (movingObject) {
|
||||
selectedObject.setAnchorPoint(mouse.x, mouse.y);
|
||||
if (selectedObject instanceof State) {
|
||||
snapNode(selectedObject);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
document.addEventListener('mouseup', () => {
|
||||
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 instanceof TemporaryConnection)) {
|
||||
selectedObject = currentConnection;
|
||||
documents[activeDocument].connections.push(currentConnection);
|
||||
resetCaret();
|
||||
|
||||
documents[activeDocument].addChange(selectedObject.constructor.name.toLowerCase(), selectedObject.id, 'add', Object.keys(selectedObject), Object.values(selectedObject))
|
||||
}
|
||||
currentConnection = null;
|
||||
}
|
||||
|
||||
assistantLines = [];
|
||||
});
|
||||
|
||||
canvas.addEventListener('dblclick', (event) => {
|
||||
|
@ -281,17 +392,29 @@ canvas.addEventListener('dblclick', (event) => {
|
|||
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);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if(activeDocument === null)
|
||||
if (activeDocument === null)
|
||||
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) => {
|
||||
|
@ -307,9 +430,24 @@ document.addEventListener('keydown', (event) => {
|
|||
break;
|
||||
case 8: // Backspace
|
||||
if (!!selectedObject) {
|
||||
selectedObject.text = selectedObject.text.substr(0, caretPos - 1) + selectedObject.text.substr(caretPos, selectedObject.text.length);
|
||||
caretPos--;
|
||||
caretPos = Math.max(caretPos, 0);
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -318,36 +456,60 @@ document.addEventListener('keydown', (event) => {
|
|||
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 (!(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 (!(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;
|
||||
}
|
||||
|
||||
|
@ -376,13 +538,23 @@ document.addEventListener('keypress', (event) => {
|
|||
}
|
||||
|
||||
if (!!selectedObject && 'text' in selectedObject) {
|
||||
const text = selectedObject.text;
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
@ -390,10 +562,34 @@ document.addEventListener('keypress', (event) => {
|
|||
}
|
||||
|
||||
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();
|
||||
|
@ -408,6 +604,34 @@ window.addEventListener('keydown', (event) => {
|
|||
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();
|
||||
|
||||
|
@ -415,6 +639,15 @@ window.addEventListener('keydown', (event) => {
|
|||
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--;
|
||||
|
@ -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) {
|
||||
const dx = Math.cos(angle);
|
||||
const dy = Math.sin(angle);
|
||||
|
@ -474,18 +720,64 @@ CanvasRenderingContext2D.prototype.drawText = function (originalText, x, y, angl
|
|||
} else {
|
||||
x = Math.round(x);
|
||||
y = Math.round(y);
|
||||
this.fillText(text, x, y + 6);
|
||||
if (isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) {
|
||||
x += this.measureText(text.substring(0, caretPos)).width;
|
||||
|
||||
this.beginPath();
|
||||
let drawText = true;
|
||||
|
||||
this.moveTo(x, y - 10);
|
||||
this.lineTo(x, y + 10);
|
||||
if (isSelected && canvasHasFocus() && document.hasFocus()) {
|
||||
const caretX = x + this.measureText(text.substring(0, caretPos)).width;
|
||||
|
||||
this.stroke();
|
||||
this.closePath();
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -508,16 +800,51 @@ Node.prototype.addChild = function (name, classList, text) {
|
|||
};
|
||||
|
||||
function snapNode(targetState) {
|
||||
let snapX = false,
|
||||
snapY = false;
|
||||
assistantLines = [];
|
||||
|
||||
for (let state of documents[activeDocument].states) {
|
||||
if (state === targetState)
|
||||
continue;
|
||||
|
||||
if (Math.abs(targetState.x - state.x) < settings.snapToPadding) {
|
||||
if (!snapX && Math.abs(targetState.x - state.x) < settings.snapToPadding) {
|
||||
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;
|
||||
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),
|
||||
mouse = cbMousePos(event);
|
||||
return {
|
||||
x: mouse.x - elem.x,
|
||||
y: mouse.y - elem.y,
|
||||
x: (mouse.x - elem.x) / realWidth * width,
|
||||
y: (mouse.y - elem.y) / realHeight * height,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -573,6 +900,7 @@ function guid() {
|
|||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
onResize();
|
||||
const doc = new FSMDocument('Dokument1');
|
||||
addDocument(doc);
|
||||
switchDocument(doc);
|
||||
|
@ -581,6 +909,17 @@ window.addEventListener('load', () => {
|
|||
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 => {
|
||||
|
|
74
js/menu.js
74
js/menu.js
|
@ -11,7 +11,7 @@ modalSave.addFooterBtn('Speichern', 'btn', () => {
|
|||
const input = document.getElementById('saveName');
|
||||
const name = input.value;
|
||||
|
||||
if(name.trim().length > 0) {
|
||||
if (name.trim().length > 0) {
|
||||
documents[activeDocument].name = name;
|
||||
saveToLocalStorage();
|
||||
modalSave.close();
|
||||
|
@ -30,7 +30,7 @@ const modalOpen = new tingle.modal({
|
|||
html += '</ul>';
|
||||
modalOpen.setContent(html);
|
||||
|
||||
for(let element of document.getElementsByClassName('file-list-item')) {
|
||||
for (let element of document.getElementsByClassName('file-list-item')) {
|
||||
element.addEventListener('click', () => {
|
||||
const entry = entries.find(entry => entry.document.id === element.getAttribute('data-id'));
|
||||
addDocument(entry.document);
|
||||
|
@ -44,8 +44,10 @@ const modalOpen = new tingle.modal({
|
|||
const modalExport = new tingle.modal({
|
||||
footer: true,
|
||||
beforeOpen: () => {
|
||||
const el = document.getElementById('previewImage');
|
||||
el.src = canvas.toDataURL('image/png');
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById('previewImage');
|
||||
el.src = canvas.toDataURL('image/png');
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
modalExport.setContent('<h3>Dokument exportieren</h3><img src="" id="previewImage">');
|
||||
|
@ -69,7 +71,8 @@ modalImport.addFooterBtn('Laden', 'btn', () => {
|
|||
const el = document.getElementById('importUpload');
|
||||
const file = el.files[0];
|
||||
|
||||
if(file) {
|
||||
if (file) {
|
||||
console.log(file.type);
|
||||
const reader = new FileReader();
|
||||
// TODO: Check for file type etc.
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
|
@ -89,19 +92,20 @@ modalImport.addFooterBtn('Laden', 'btn', () => {
|
|||
const modalSimulate = new tingle.modal({
|
||||
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.close();
|
||||
const input = document.getElementById('simulationInput').value;
|
||||
simulate(input);
|
||||
const singleCharMode = document.getElementById('singleCharMode').checked;
|
||||
simulate(input, singleCharMode);
|
||||
});
|
||||
|
||||
document.getElementById('saveBtn').addEventListener('click', () => {
|
||||
if(activeDocument === null) {
|
||||
if (activeDocument === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(documents[activeDocument].name !== null) {
|
||||
if (documents[activeDocument].name !== null) {
|
||||
saveToLocalStorage();
|
||||
} else {
|
||||
modalSave.open();
|
||||
|
@ -109,23 +113,75 @@ document.getElementById('saveBtn').addEventListener('click', () => {
|
|||
});
|
||||
|
||||
document.getElementById('addBtn').addEventListener('click', () => {
|
||||
selectedObject = null;
|
||||
currentConnection = null;
|
||||
const doc = new FSMDocument(null);
|
||||
addDocument(doc);
|
||||
switchDocument(doc);
|
||||
});
|
||||
|
||||
document.getElementById('openBtn').addEventListener('click', () => {
|
||||
selectedObject = null;
|
||||
currentConnection = null;
|
||||
modalOpen.open();
|
||||
});
|
||||
|
||||
document.getElementById('importBtn').addEventListener('click', () => {
|
||||
selectedObject = null;
|
||||
currentConnection = null;
|
||||
modalImport.open();
|
||||
});
|
||||
|
||||
document.getElementById('exportBtn').addEventListener('click', () => {
|
||||
selectedObject = null;
|
||||
currentConnection = null;
|
||||
modalExport.open();
|
||||
});
|
||||
|
||||
document.getElementById('simulateBtn').addEventListener('click', () => {
|
||||
selectedObject = null;
|
||||
currentConnection = null;
|
||||
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;
|
||||
});
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
let simulationStates = [];
|
||||
let singleCharMode = true;
|
||||
let simulationStepDuration = 500;
|
||||
|
||||
function simulate(word) {
|
||||
function simulate(word, singleCharMode = true) {
|
||||
if(activeDocument === null) {
|
||||
return false;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user