General improvements
This commit is contained in:
parent
1bf43ef28d
commit
23c24ded96
145
index.html
145
index.html
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
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;
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
389
js/main.js
389
js/main.js
|
@ -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,23 +14,41 @@ 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);
|
||||||
|
@ -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,7 +392,11 @@ 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();
|
||||||
|
@ -289,9 +404,17 @@ canvas.addEventListener('contextmenu', (event) => {
|
||||||
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 => {
|
||||||
|
|
60
js/menu.js
60
js/menu.js
|
@ -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">');
|
||||||
|
@ -70,6 +72,7 @@ modalImport.addFooterBtn('Laden', 'btn', () => {
|
||||||
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,11 +92,12 @@ 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', () => {
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user