From 4e84cf7e4736c8db5427c221e3276b5b168bef4d Mon Sep 17 00:00:00 2001
From: RareDrops <54132759+RareDrops@users.noreply.github.com>
Date: Thu, 3 Apr 2025 20:14:48 +1300
Subject: [PATCH 01/18] Update build.py
---
build.py | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/build.py b/build.py
index 51ac835..fa98119 100755
--- a/build.py
+++ b/build.py
@@ -1,4 +1,5 @@
#!/usr/bin/python
+# -*- coding: utf-8 -*-
import os, time, sys
@@ -8,10 +9,18 @@ def sources():
def build():
path = './www/fsm.js'
- data = '\n'.join(open(file, 'r').read() for file in sources())
- with open(path, 'w') as f:
- f.write(data)
- print 'built %s (%u bytes)' % (path, len(data))
+ try:
+ data = '\n'.join(open(file, 'r', encoding='utf-8').read() for file in sources())
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(data)
+ print('built %s (%u bytes)' % (path, len(data)))
+ except Exception as e:
+ print('Error:', str(e))
+ # Try alternative encoding if utf-8 fails
+ data = '\n'.join(open(file, 'r', encoding='latin-1').read() for file in sources())
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write(data)
+ print('built %s (%u bytes) using latin-1 fallback' % (path, len(data)))
def stat():
return [os.stat(file).st_mtime for file in sources()]
From 287c39b5ea99ac38a4876eb523b314f5a55d8054 Mon Sep 17 00:00:00 2001
From: RareDrops <54132759+RareDrops@users.noreply.github.com>
Date: Thu, 3 Apr 2025 20:14:52 +1300
Subject: [PATCH 02/18] Update fsm.js
---
src/main/fsm.js | 193 +++++++++++++++++++++++++++---------------------
1 file changed, 107 insertions(+), 86 deletions(-)
diff --git a/src/main/fsm.js b/src/main/fsm.js
index 39b03fe..5e33d0f 100644
--- a/src/main/fsm.js
+++ b/src/main/fsm.js
@@ -1,34 +1,55 @@
-var 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' ];
-
function convertLatexShortcuts(text) {
- // html greek characters
- for(var i = 0; i < greekLetterNames.length; i++) {
- var name = greekLetterNames[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)));
+ // Greek letters
+ const greekLetters = {
+ '\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ', '\\epsilon': 'ε',
+ '\\zeta': 'ζ', '\\eta': 'η', '\\theta': 'θ', '\\iota': 'ι', '\\kappa': 'κ',
+ '\\lambda': 'λ', '\\mu': 'μ', '\\nu': 'ν', '\\xi': 'ξ', '\\pi': 'π',
+ '\\rho': 'ρ', '\\sigma': 'σ', '\\tau': 'τ', '\\upsilon': 'υ', '\\phi': 'φ',
+ '\\chi': 'χ', '\\psi': 'ψ', '\\omega': 'ω',
+ '\\Gamma': 'Γ', '\\Delta': 'Δ', '\\Theta': 'Θ', '\\Lambda': 'Λ', '\\Xi': 'Ξ',
+ '\\Pi': 'Π', '\\Sigma': 'Σ', '\\Phi': 'Φ', '\\Psi': 'Ψ', '\\Omega': 'Ω'
+ };
+
+ // Basic mathematical operators
+ const operators = {
+ '\\times': '×', '\\div': '÷', '\\pm': '±', '\\mp': '∓',
+ '\\leq': '≤', '\\geq': '≥', '\\neq': '≠', '\\approx': '≈',
+ '\\infty': '∞', '\\sum': '∑', '\\prod': '∏', '\\int': '∫',
+ // Set theory symbols
+ '\\cup': '∪', '\\cap': '∩', '\\subset': '⊂', '\\supset': '⊃',
+ '\\subseteq': '⊆', '\\supseteq': '⊇', '\\in': '∈', '\\notin': '∉',
+ '\\emptyset': '∅', '\\varnothing': '∅',
+ // Additional operators
+ '\\cdot': '·', '\\bullet': '•', '\\circ': '∘', '\\oplus': '⊕',
+ '\\otimes': '⊗', '\\setminus': '∖', '\\subsetneq': '⊊', '\\supsetneq': '⊋'
+ };
+
+ // Replace Greek letters
+ for (const [latex, unicode] of Object.entries(greekLetters)) {
+ text = text.replace(new RegExp(latex, 'g'), unicode);
}
- // subscripts
- for(var i = 0; i < 10; i++) {
- text = text.replace(new RegExp('_' + i, 'g'), String.fromCharCode(8320 + i));
+ // Replace operators
+ for (const [latex, unicode] of Object.entries(operators)) {
+ text = text.replace(new RegExp(latex, 'g'), unicode);
}
return text;
}
-
-function textToXML(text) {
- text = text.replace(/&/g, '&').replace(//g, '>');
- var result = '';
- for(var i = 0; i < text.length; i++) {
- var c = text.charCodeAt(i);
- if(c >= 0x20 && c <= 0x7E) {
- result += text[i];
- } else {
- result += '' + c + ';';
- }
- }
- return result;
-}
+
+function textToXML(text) {
+ text = text.replace(/&/g, '&').replace(//g, '>');
+ var result = '';
+ for(var i = 0; i < text.length; i++) {
+ var c = text.charCodeAt(i);
+ if(c >= 0x20 && c <= 0x7E) {
+ result += text[i];
+ } else {
+ result += '' + c + ';';
+ }
+ }
+ return result;
+}
function drawArrow(c, x, y, angle) {
var dx = Math.cos(angle);
@@ -39,12 +60,12 @@ function drawArrow(c, x, y, angle) {
c.lineTo(x - 8 * dx - 5 * dy, y - 8 * dy + 5 * dx);
c.fill();
}
-
-function canvasHasFocus() {
- return (document.activeElement || document.body) == document.body;
-}
-function drawText(c, originalText, x, y, angleOrNull, isSelected) {
+function canvasHasFocus() {
+ return (document.activeElement || document.body) == document.body;
+}
+
+function drawText(c, originalText, x, y, angleOrNull, isSelected) {
text = convertLatexShortcuts(originalText);
c.font = '20px "Times New Roman", serif';
var width = c.measureText(text).width;
@@ -63,13 +84,13 @@ function drawText(c, originalText, x, y, angleOrNull, isSelected) {
y += cornerPointY + cos * slide;
}
- // draw text and caret (round the coordinates so the caret falls on a pixel)
+ // draw text and caret (round the coordinates so the caret falls on a pixel)
if('advancedFillText' in c) {
- c.advancedFillText(text, originalText, x + width / 2, y, angleOrNull);
+ c.advancedFillText(text, originalText, x + width / 2, y, angleOrNull);
} else {
x = Math.round(x);
- y = Math.round(y);
- c.fillText(text, x, y + 6);
+ y = Math.round(y);
+ c.fillText(text, x, y + 6);
if(isSelected && caretVisible && canvasHasFocus() && document.hasFocus()) {
x += width;
c.beginPath();
@@ -101,8 +122,8 @@ var selectedObject = null; // either a Link or a Node
var currentLink = null; // a Link
var movingObject = false;
var originalClick;
-
-function drawUsing(c) {
+
+function drawUsing(c) {
c.clearRect(0, 0, canvas.width, canvas.height);
c.save();
c.translate(0.5, 0.5);
@@ -124,12 +145,12 @@ function drawUsing(c) {
}
c.restore();
-}
-
-function draw() {
- drawUsing(canvas.getContext('2d'));
- saveBackup();
-}
+}
+
+function draw() {
+ drawUsing(canvas.getContext('2d'));
+ saveBackup();
+}
function selectObject(x, y) {
for(var i = 0; i < nodes.length; i++) {
@@ -160,7 +181,7 @@ function snapNode(node) {
}
window.onload = function() {
- canvas = document.getElementById('canvas');
+ canvas = document.getElementById('canvas');
restoreBackup();
draw();
@@ -186,14 +207,14 @@ window.onload = function() {
}
draw();
-
- if(canvasHasFocus()) {
- // disable drag-and-drop only if the canvas is already focused
- return false;
+
+ if(canvasHasFocus()) {
+ // disable drag-and-drop only if the canvas is already focused
+ return false;
} else {
- // otherwise, let the browser switch the focus away from wherever it was
+ // otherwise, let the browser switch the focus away from wherever it was
resetCaret();
- return true;
+ return true;
}
};
@@ -265,13 +286,13 @@ window.onload = function() {
var shift = false;
-document.onkeydown = function(e) {
- var key = crossBrowserKey(e);
+document.onkeydown = function(e) {
+ var key = crossBrowserKey(e);
if(key == 16) {
shift = true;
- } else if(!canvasHasFocus()) {
- // don't read keystrokes when other things have focus
+ } else if(!canvasHasFocus()) {
+ // don't read keystrokes when other things have focus
return true;
} else if(key == 8) { // backspace key
if(selectedObject != null && 'text' in selectedObject) {
@@ -279,7 +300,7 @@ document.onkeydown = function(e) {
resetCaret();
draw();
}
-
+
// backspace is a shortcut for the back button, but do NOT want to change pages
return false;
} else if(key == 46) { // delete key
@@ -310,9 +331,9 @@ document.onkeyup = function(e) {
document.onkeypress = function(e) {
// don't read keystrokes when other things have focus
- var key = crossBrowserKey(e);
- if(!canvasHasFocus()) {
- // don't read keystrokes when other things have focus
+ var key = crossBrowserKey(e);
+ if(!canvasHasFocus()) {
+ // don't read keystrokes when other things have focus
return true;
} else if(key >= 0x20 && key <= 0x7E && !e.metaKey && !e.altKey && !e.ctrlKey && selectedObject != null && 'text' in selectedObject) {
selectedObject.text += String.fromCharCode(key);
@@ -321,7 +342,7 @@ document.onkeypress = function(e) {
// don't let keys do their actions (like space scrolls down the page)
return false;
- } else if(key == 8) {
+ } else if(key == 8) {
// backspace is a shortcut for the back button, but do NOT want to change pages
return false;
}
@@ -360,40 +381,40 @@ function crossBrowserRelativeMousePos(e) {
'y': mouse.y - element.y
};
}
-
-function output(text) {
- var element = document.getElementById('output');
- element.style.display = 'block';
- element.value = text;
-}
+
+function output(text) {
+ var element = document.getElementById('output');
+ element.style.display = 'block';
+ element.value = text;
+}
function saveAsPNG() {
- var oldSelectedObject = selectedObject;
- selectedObject = null;
- drawUsing(canvas.getContext('2d'));
- selectedObject = oldSelectedObject;
+ var oldSelectedObject = selectedObject;
+ selectedObject = null;
+ drawUsing(canvas.getContext('2d'));
+ selectedObject = oldSelectedObject;
var pngData = canvas.toDataURL('image/png');
document.location.href = pngData;
-}
-
-function saveAsSVG() {
+}
+
+function saveAsSVG() {
var exporter = new ExportAsSVG();
- var oldSelectedObject = selectedObject;
- selectedObject = null;
- drawUsing(exporter);
- selectedObject = oldSelectedObject;
- var svgData = exporter.toSVG();
- output(svgData);
- // Chrome isn't ready for this yet, the 'Save As' menu item is disabled
- // document.location.href = 'data:image/svg+xml;base64,' + btoa(svgData);
+ var oldSelectedObject = selectedObject;
+ selectedObject = null;
+ drawUsing(exporter);
+ selectedObject = oldSelectedObject;
+ var svgData = exporter.toSVG();
+ output(svgData);
+ // Chrome isn't ready for this yet, the 'Save As' menu item is disabled
+ // document.location.href = 'data:image/svg+xml;base64,' + btoa(svgData);
}
-
-function saveAsLaTeX() {
+
+function saveAsLaTeX() {
var exporter = new ExportAsLaTeX();
- var oldSelectedObject = selectedObject;
- selectedObject = null;
- drawUsing(exporter);
- selectedObject = oldSelectedObject;
- var texData = exporter.toLaTeX();
- output(texData);
+ var oldSelectedObject = selectedObject;
+ selectedObject = null;
+ drawUsing(exporter);
+ selectedObject = oldSelectedObject;
+ var texData = exporter.toLaTeX();
+ output(texData);
}
From 1137c9579a47629f9d7a83eb2224620d0657b787 Mon Sep 17 00:00:00 2001
From: RareDrops <54132759+RareDrops@users.noreply.github.com>
Date: Thu, 3 Apr 2025 20:14:55 +1300
Subject: [PATCH 03/18] Create fsm.js
---
www/fsm.js | 1113 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 1113 insertions(+)
create mode 100644 www/fsm.js
diff --git a/www/fsm.js b/www/fsm.js
new file mode 100644
index 0000000..662e4c7
--- /dev/null
+++ b/www/fsm.js
@@ -0,0 +1,1113 @@
+/*
+ Finite State Machine Designer (http://madebyevan.com/fsm/)
+ License: MIT License (see below)
+
+ Copyright (c) 2010 Evan Wallace
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+function Link(a, b) {
+ this.nodeA = a;
+ this.nodeB = b;
+ this.text = '';
+ this.lineAngleAdjust = 0; // value to add to textAngle when link is straight line
+
+ // make anchor point relative to the locations of nodeA and nodeB
+ this.parallelPart = 0.5; // percentage from nodeA to nodeB
+ this.perpendicularPart = 0; // pixels from line between nodeA and nodeB
+}
+
+Link.prototype.getAnchorPoint = function() {
+ var dx = this.nodeB.x - this.nodeA.x;
+ var dy = this.nodeB.y - this.nodeA.y;
+ var scale = Math.sqrt(dx * dx + dy * dy);
+ return {
+ 'x': this.nodeA.x + dx * this.parallelPart - dy * this.perpendicularPart / scale,
+ 'y': this.nodeA.y + dy * this.parallelPart + dx * this.perpendicularPart / scale
+ };
+};
+
+Link.prototype.setAnchorPoint = function(x, y) {
+ var dx = this.nodeB.x - this.nodeA.x;
+ var dy = this.nodeB.y - this.nodeA.y;
+ var scale = Math.sqrt(dx * dx + dy * dy);
+ this.parallelPart = (dx * (x - this.nodeA.x) + dy * (y - this.nodeA.y)) / (scale * scale);
+ this.perpendicularPart = (dx * (y - this.nodeA.y) - dy * (x - this.nodeA.x)) / scale;
+ // snap to a straight line
+ if(this.parallelPart > 0 && this.parallelPart < 1 && Math.abs(this.perpendicularPart) < snapToPadding) {
+ this.lineAngleAdjust = (this.perpendicularPart < 0) * Math.PI;
+ this.perpendicularPart = 0;
+ }
+};
+
+Link.prototype.getEndPointsAndCircle = function() {
+ if(this.perpendicularPart == 0) {
+ var midX = (this.nodeA.x + this.nodeB.x) / 2;
+ var midY = (this.nodeA.y + this.nodeB.y) / 2;
+ var start = this.nodeA.closestPointOnCircle(midX, midY);
+ var end = this.nodeB.closestPointOnCircle(midX, midY);
+ return {
+ 'hasCircle': false,
+ 'startX': start.x,
+ 'startY': start.y,
+ 'endX': end.x,
+ 'endY': end.y,
+ };
+ }
+ var anchor = this.getAnchorPoint();
+ var circle = circleFromThreePoints(this.nodeA.x, this.nodeA.y, this.nodeB.x, this.nodeB.y, anchor.x, anchor.y);
+ var isReversed = (this.perpendicularPart > 0);
+ var reverseScale = isReversed ? 1 : -1;
+ var startAngle = Math.atan2(this.nodeA.y - circle.y, this.nodeA.x - circle.x) - reverseScale * nodeRadius / circle.radius;
+ var endAngle = Math.atan2(this.nodeB.y - circle.y, this.nodeB.x - circle.x) + reverseScale * nodeRadius / circle.radius;
+ var startX = circle.x + circle.radius * Math.cos(startAngle);
+ var startY = circle.y + circle.radius * Math.sin(startAngle);
+ var endX = circle.x + circle.radius * Math.cos(endAngle);
+ var endY = circle.y + circle.radius * Math.sin(endAngle);
+ return {
+ 'hasCircle': true,
+ 'startX': startX,
+ 'startY': startY,
+ 'endX': endX,
+ 'endY': endY,
+ 'startAngle': startAngle,
+ 'endAngle': endAngle,
+ 'circleX': circle.x,
+ 'circleY': circle.y,
+ 'circleRadius': circle.radius,
+ 'reverseScale': reverseScale,
+ 'isReversed': isReversed,
+ };
+};
+
+Link.prototype.draw = function(c) {
+ var stuff = this.getEndPointsAndCircle();
+ // draw arc
+ c.beginPath();
+ if(stuff.hasCircle) {
+ c.arc(stuff.circleX, stuff.circleY, stuff.circleRadius, stuff.startAngle, stuff.endAngle, stuff.isReversed);
+ } else {
+ c.moveTo(stuff.startX, stuff.startY);
+ c.lineTo(stuff.endX, stuff.endY);
+ }
+ c.stroke();
+ // draw the head of the arrow
+ if(stuff.hasCircle) {
+ drawArrow(c, stuff.endX, stuff.endY, stuff.endAngle - stuff.reverseScale * (Math.PI / 2));
+ } else {
+ drawArrow(c, stuff.endX, stuff.endY, Math.atan2(stuff.endY - stuff.startY, stuff.endX - stuff.startX));
+ }
+ // draw the text
+ if(stuff.hasCircle) {
+ var startAngle = stuff.startAngle;
+ var endAngle = stuff.endAngle;
+ if(endAngle < startAngle) {
+ endAngle += Math.PI * 2;
+ }
+ var textAngle = (startAngle + endAngle) / 2 + stuff.isReversed * Math.PI;
+ var textX = stuff.circleX + stuff.circleRadius * Math.cos(textAngle);
+ var textY = stuff.circleY + stuff.circleRadius * Math.sin(textAngle);
+ drawText(c, this.text, textX, textY, textAngle, selectedObject == this);
+ } else {
+ var textX = (stuff.startX + stuff.endX) / 2;
+ var textY = (stuff.startY + stuff.endY) / 2;
+ var textAngle = Math.atan2(stuff.endX - stuff.startX, stuff.startY - stuff.endY);
+ drawText(c, this.text, textX, textY, textAngle + this.lineAngleAdjust, selectedObject == this);
+ }
+};
+
+Link.prototype.containsPoint = function(x, y) {
+ var stuff = this.getEndPointsAndCircle();
+ if(stuff.hasCircle) {
+ var dx = x - stuff.circleX;
+ var dy = y - stuff.circleY;
+ var distance = Math.sqrt(dx*dx + dy*dy) - stuff.circleRadius;
+ if(Math.abs(distance) < hitTargetPadding) {
+ var angle = Math.atan2(dy, dx);
+ var startAngle = stuff.startAngle;
+ var endAngle = stuff.endAngle;
+ if(stuff.isReversed) {
+ var temp = startAngle;
+ startAngle = endAngle;
+ endAngle = temp;
+ }
+ if(endAngle < startAngle) {
+ endAngle += Math.PI * 2;
+ }
+ if(angle < startAngle) {
+ angle += Math.PI * 2;
+ } else if(angle > endAngle) {
+ angle -= Math.PI * 2;
+ }
+ return (angle > startAngle && angle < endAngle);
+ }
+ } else {
+ var dx = stuff.endX - stuff.startX;
+ var dy = stuff.endY - stuff.startY;
+ var length = Math.sqrt(dx*dx + dy*dy);
+ var percent = (dx * (x - stuff.startX) + dy * (y - stuff.startY)) / (length * length);
+ var distance = (dx * (y - stuff.startY) - dy * (x - stuff.startX)) / length;
+ return (percent > 0 && percent < 1 && Math.abs(distance) < hitTargetPadding);
+ }
+ return false;
+};
+
+function Node(x, y) {
+ this.x = x;
+ this.y = y;
+ this.mouseOffsetX = 0;
+ this.mouseOffsetY = 0;
+ this.isAcceptState = false;
+ this.text = '';
+}
+
+Node.prototype.setMouseStart = function(x, y) {
+ this.mouseOffsetX = this.x - x;
+ this.mouseOffsetY = this.y - y;
+};
+
+Node.prototype.setAnchorPoint = function(x, y) {
+ this.x = x + this.mouseOffsetX;
+ this.y = y + this.mouseOffsetY;
+};
+
+Node.prototype.draw = function(c) {
+ // draw the circle
+ c.beginPath();
+ c.arc(this.x, this.y, nodeRadius, 0, 2 * Math.PI, false);
+ c.stroke();
+
+ // draw the text
+ drawText(c, this.text, this.x, this.y, null, selectedObject == this);
+
+ // draw a double circle for an accept state
+ if(this.isAcceptState) {
+ c.beginPath();
+ c.arc(this.x, this.y, nodeRadius - 6, 0, 2 * Math.PI, false);
+ c.stroke();
+ }
+};
+
+Node.prototype.closestPointOnCircle = function(x, y) {
+ var dx = x - this.x;
+ var dy = y - this.y;
+ var scale = Math.sqrt(dx * dx + dy * dy);
+ return {
+ 'x': this.x + dx * nodeRadius / scale,
+ 'y': this.y + dy * nodeRadius / scale,
+ };
+};
+
+Node.prototype.containsPoint = function(x, y) {
+ return (x - this.x)*(x - this.x) + (y - this.y)*(y - this.y) < nodeRadius*nodeRadius;
+};
+
+function SelfLink(node, mouse) {
+ this.node = node;
+ this.anchorAngle = 0;
+ this.mouseOffsetAngle = 0;
+ this.text = '';
+
+ if(mouse) {
+ this.setAnchorPoint(mouse.x, mouse.y);
+ }
+}
+
+SelfLink.prototype.setMouseStart = function(x, y) {
+ this.mouseOffsetAngle = this.anchorAngle - Math.atan2(y - this.node.y, x - this.node.x);
+};
+
+SelfLink.prototype.setAnchorPoint = function(x, y) {
+ this.anchorAngle = Math.atan2(y - this.node.y, x - this.node.x) + this.mouseOffsetAngle;
+ // snap to 90 degrees
+ var snap = Math.round(this.anchorAngle / (Math.PI / 2)) * (Math.PI / 2);
+ if(Math.abs(this.anchorAngle - snap) < 0.1) this.anchorAngle = snap;
+ // keep in the range -pi to pi so our containsPoint() function always works
+ if(this.anchorAngle < -Math.PI) this.anchorAngle += 2 * Math.PI;
+ if(this.anchorAngle > Math.PI) this.anchorAngle -= 2 * Math.PI;
+};
+
+SelfLink.prototype.getEndPointsAndCircle = function() {
+ var circleX = this.node.x + 1.5 * nodeRadius * Math.cos(this.anchorAngle);
+ var circleY = this.node.y + 1.5 * nodeRadius * Math.sin(this.anchorAngle);
+ var circleRadius = 0.75 * nodeRadius;
+ var startAngle = this.anchorAngle - Math.PI * 0.8;
+ var endAngle = this.anchorAngle + Math.PI * 0.8;
+ var startX = circleX + circleRadius * Math.cos(startAngle);
+ var startY = circleY + circleRadius * Math.sin(startAngle);
+ var endX = circleX + circleRadius * Math.cos(endAngle);
+ var endY = circleY + circleRadius * Math.sin(endAngle);
+ return {
+ 'hasCircle': true,
+ 'startX': startX,
+ 'startY': startY,
+ 'endX': endX,
+ 'endY': endY,
+ 'startAngle': startAngle,
+ 'endAngle': endAngle,
+ 'circleX': circleX,
+ 'circleY': circleY,
+ 'circleRadius': circleRadius
+ };
+};
+
+SelfLink.prototype.draw = function(c) {
+ var stuff = this.getEndPointsAndCircle();
+ // draw arc
+ c.beginPath();
+ c.arc(stuff.circleX, stuff.circleY, stuff.circleRadius, stuff.startAngle, stuff.endAngle, false);
+ c.stroke();
+ // draw the text on the loop farthest from the node
+ var textX = stuff.circleX + stuff.circleRadius * Math.cos(this.anchorAngle);
+ var textY = stuff.circleY + stuff.circleRadius * Math.sin(this.anchorAngle);
+ drawText(c, this.text, textX, textY, this.anchorAngle, selectedObject == this);
+ // draw the head of the arrow
+ drawArrow(c, stuff.endX, stuff.endY, stuff.endAngle + Math.PI * 0.4);
+};
+
+SelfLink.prototype.containsPoint = function(x, y) {
+ var stuff = this.getEndPointsAndCircle();
+ var dx = x - stuff.circleX;
+ var dy = y - stuff.circleY;
+ var distance = Math.sqrt(dx*dx + dy*dy) - stuff.circleRadius;
+ return (Math.abs(distance) < hitTargetPadding);
+};
+
+function StartLink(node, start) {
+ this.node = node;
+ this.deltaX = 0;
+ this.deltaY = 0;
+ this.text = '';
+
+ if(start) {
+ this.setAnchorPoint(start.x, start.y);
+ }
+}
+
+StartLink.prototype.setAnchorPoint = function(x, y) {
+ this.deltaX = x - this.node.x;
+ this.deltaY = y - this.node.y;
+
+ if(Math.abs(this.deltaX) < snapToPadding) {
+ this.deltaX = 0;
+ }
+
+ if(Math.abs(this.deltaY) < snapToPadding) {
+ this.deltaY = 0;
+ }
+};
+
+StartLink.prototype.getEndPoints = function() {
+ var startX = this.node.x + this.deltaX;
+ var startY = this.node.y + this.deltaY;
+ var end = this.node.closestPointOnCircle(startX, startY);
+ return {
+ 'startX': startX,
+ 'startY': startY,
+ 'endX': end.x,
+ 'endY': end.y,
+ };
+};
+
+StartLink.prototype.draw = function(c) {
+ var stuff = this.getEndPoints();
+
+ // draw the line
+ c.beginPath();
+ c.moveTo(stuff.startX, stuff.startY);
+ c.lineTo(stuff.endX, stuff.endY);
+ c.stroke();
+
+ // draw the text at the end without the arrow
+ var textAngle = Math.atan2(stuff.startY - stuff.endY, stuff.startX - stuff.endX);
+ drawText(c, this.text, stuff.startX, stuff.startY, textAngle, selectedObject == this);
+
+ // draw the head of the arrow
+ drawArrow(c, stuff.endX, stuff.endY, Math.atan2(-this.deltaY, -this.deltaX));
+};
+
+StartLink.prototype.containsPoint = function(x, y) {
+ var stuff = this.getEndPoints();
+ var dx = stuff.endX - stuff.startX;
+ var dy = stuff.endY - stuff.startY;
+ var length = Math.sqrt(dx*dx + dy*dy);
+ var percent = (dx * (x - stuff.startX) + dy * (y - stuff.startY)) / (length * length);
+ var distance = (dx * (y - stuff.startY) - dy * (x - stuff.startX)) / length;
+ return (percent > 0 && percent < 1 && Math.abs(distance) < hitTargetPadding);
+};
+
+function TemporaryLink(from, to) {
+ this.from = from;
+ this.to = to;
+}
+
+TemporaryLink.prototype.draw = function(c) {
+ // draw the line
+ c.beginPath();
+ c.moveTo(this.to.x, this.to.y);
+ c.lineTo(this.from.x, this.from.y);
+ c.stroke();
+
+ // draw the head of the arrow
+ drawArrow(c, this.to.x, this.to.y, Math.atan2(this.to.y - this.from.y, this.to.x - this.from.x));
+};
+
+// draw using this instead of a canvas and call toLaTeX() afterward
+function ExportAsLaTeX() {
+ this._points = [];
+ this._texData = '';
+ this._scale = 0.1; // to convert pixels to document space (TikZ breaks if the numbers get too big, above 500?)
+
+ this.toLaTeX = function() {
+ return '\\documentclass[12pt]{article}\n' +
+ '\\usepackage{tikz}\n' +
+ '\n' +
+ '\\begin{document}\n' +
+ '\n' +
+ '\\begin{center}\n' +
+ '\\begin{tikzpicture}[scale=0.2]\n' +
+ '\\tikzstyle{every node}+=[inner sep=0pt]\n' +
+ this._texData +
+ '\\end{tikzpicture}\n' +
+ '\\end{center}\n' +
+ '\n' +
+ '\\end{document}\n';
+ };
+
+ this.beginPath = function() {
+ this._points = [];
+ };
+ this.arc = function(x, y, radius, startAngle, endAngle, isReversed) {
+ x *= this._scale;
+ y *= this._scale;
+ radius *= this._scale;
+ if(endAngle - startAngle == Math.PI * 2) {
+ this._texData += '\\draw [' + this.strokeStyle + '] (' + fixed(x, 3) + ',' + fixed(-y, 3) + ') circle (' + fixed(radius, 3) + ');\n';
+ } else {
+ if(isReversed) {
+ var temp = startAngle;
+ startAngle = endAngle;
+ endAngle = temp;
+ }
+ if(endAngle < startAngle) {
+ endAngle += Math.PI * 2;
+ }
+ // TikZ needs the angles to be in between -2pi and 2pi or it breaks
+ if(Math.min(startAngle, endAngle) < -2*Math.PI) {
+ startAngle += 2*Math.PI;
+ endAngle += 2*Math.PI;
+ } else if(Math.max(startAngle, endAngle) > 2*Math.PI) {
+ startAngle -= 2*Math.PI;
+ endAngle -= 2*Math.PI;
+ }
+ startAngle = -startAngle;
+ endAngle = -endAngle;
+ this._texData += '\\draw [' + this.strokeStyle + '] (' + fixed(x + radius * Math.cos(startAngle), 3) + ',' + fixed(-y + radius * Math.sin(startAngle), 3) + ') arc (' + fixed(startAngle * 180 / Math.PI, 5) + ':' + fixed(endAngle * 180 / Math.PI, 5) + ':' + fixed(radius, 3) + ');\n';
+ }
+ };
+ this.moveTo = this.lineTo = function(x, y) {
+ x *= this._scale;
+ y *= this._scale;
+ this._points.push({ 'x': x, 'y': y });
+ };
+ this.stroke = function() {
+ if(this._points.length == 0) return;
+ this._texData += '\\draw [' + this.strokeStyle + ']';
+ for(var i = 0; i < this._points.length; i++) {
+ var p = this._points[i];
+ this._texData += (i > 0 ? ' --' : '') + ' (' + fixed(p.x, 2) + ',' + fixed(-p.y, 2) + ')';
+ }
+ this._texData += ';\n';
+ };
+ this.fill = function() {
+ if(this._points.length == 0) return;
+ this._texData += '\\fill [' + this.strokeStyle + ']';
+ for(var i = 0; i < this._points.length; i++) {
+ var p = this._points[i];
+ this._texData += (i > 0 ? ' --' : '') + ' (' + fixed(p.x, 2) + ',' + fixed(-p.y, 2) + ')';
+ }
+ this._texData += ';\n';
+ };
+ this.measureText = function(text) {
+ var c = canvas.getContext('2d');
+ c.font = '20px "Times New Romain", serif';
+ return c.measureText(text);
+ };
+ this.advancedFillText = function(text, originalText, x, y, angleOrNull) {
+ if(text.replace(' ', '').length > 0) {
+ var nodeParams = '';
+ // x and y start off as the center of the text, but will be moved to one side of the box when angleOrNull != null
+ if(angleOrNull != null) {
+ var width = this.measureText(text).width;
+ var dx = Math.cos(angleOrNull);
+ var dy = Math.sin(angleOrNull);
+ if(Math.abs(dx) > Math.abs(dy)) {
+ if(dx > 0) nodeParams = '[right] ', x -= width / 2;
+ else nodeParams = '[left] ', x += width / 2;
+ } else {
+ if(dy > 0) nodeParams = '[below] ', y -= 10;
+ else nodeParams = '[above] ', y += 10;
+ }
+ }
+ x *= this._scale;
+ y *= this._scale;
+ this._texData += '\\draw (' + fixed(x, 2) + ',' + fixed(-y, 2) + ') node ' + nodeParams + '{$' + originalText.replace(/ /g, '\\mbox{ }') + '$};\n';
+ }
+ };
+
+ this.translate = this.save = this.restore = this.clearRect = function(){};
+}
+
+// draw using this instead of a canvas and call toSVG() afterward
+function ExportAsSVG() {
+ this.fillStyle = 'black';
+ this.strokeStyle = 'black';
+ this.lineWidth = 1;
+ this.font = '12px Arial, sans-serif';
+ this._points = [];
+ this._svgData = '';
+ this._transX = 0;
+ this._transY = 0;
+
+ this.toSVG = function() {
+ return '\n\n\n\n';
+ };
+
+ this.beginPath = function() {
+ this._points = [];
+ };
+ this.arc = function(x, y, radius, startAngle, endAngle, isReversed) {
+ x += this._transX;
+ y += this._transY;
+ var style = 'stroke="' + this.strokeStyle + '" stroke-width="' + this.lineWidth + '" fill="none"';
+
+ if(endAngle - startAngle == Math.PI * 2) {
+ this._svgData += '\t
The big white box above is the FSM designer. Here's how to use it:
-This was made in HTML5 and JavaScript using the canvas element.
-Created by Evan Wallace in 2010
- + + +The big white box above is the FSM designer. Here's how to use it:
+This was made in HTML5 and JavaScript using the canvas element.
+Created by Evan Wallace in 2010
+ From 68f0934c83aeba84c78f938aaf34a0e558b0f94c Mon Sep 17 00:00:00 2001 From: RareDrops <54132759+RareDrops@users.noreply.github.com> Date: Thu, 3 Apr 2025 20:27:21 +1300 Subject: [PATCH 05/18] moved to public folder --- index.html | 12 ++++++++++++ {docs => public}/fsm.js | 0 {docs => public}/index.html | 0 3 files changed, 12 insertions(+) create mode 100644 index.html rename {docs => public}/fsm.js (100%) rename {docs => public}/index.html (100%) diff --git a/index.html b/index.html new file mode 100644 index 0000000..9575166 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + +