arcs nicely curve when they connect a rountangle to itself

This commit is contained in:
Joeri Exelmans 2025-10-06 17:15:51 +02:00
parent e009f718d2
commit da0e56e17c
11 changed files with 526 additions and 153 deletions

View file

@ -82,6 +82,7 @@ text.highlight {
}
.arrow {
fill: none;
stroke: black;
stroke-width: 2px;
}

View file

@ -1,5 +1,5 @@
import { MouseEventHandler, SetStateAction, useEffect, useRef, useState } from "react";
import { Line2D, Rect2D, Vec2D, addV2D, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, euclideanDistance, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
import "./VisualEditor.css";
@ -396,38 +396,65 @@ export function VisualEditor() {
};
}, [selectingState, dragging]);
// whenever an arrow is selected, highlight the rountangle sides it connects to
// just for visual feedback
let sidesToHighlight: {[key: string]: RountanglePart[]} = {};
let arrowsToHighlight: {[key: string]: Arrow} = {};
let textsToHighlight: {[key: string]: Text} = {};
// detect what is 'connected'
const arrow2SideMap = new Map<string,[{ uid: string; part: RountanglePart; } | undefined, { uid: string; part: RountanglePart; } | undefined]>();
const side2ArrowMap = new Map<string, Set<["start"|"end", string]>>();
const text2ArrowMap = new Map<string,string>();
const arrow2TextMap = new Map<string,string[]>();
for (const arrow of state.arrows) {
const startSide = findNearestRountangleSide(arrow, "start", state.rountangles);
const endSide = findNearestRountangleSide(arrow, "end", state.rountangles);
if (startSide || endSide) {
arrow2SideMap.set(arrow.uid, [startSide, endSide]);
}
if (startSide) {
const arrowConns = side2ArrowMap.get(startSide.uid) || new Set();
arrowConns.add(["start", arrow.uid]);
side2ArrowMap.set(startSide.uid, arrowConns);
}
if (endSide) {
const arrowConns = side2ArrowMap.get(endSide.uid) || new Set();
arrowConns.add(["end", arrow.uid]);
side2ArrowMap.set(endSide.uid, arrowConns);
}
}
for (const text of state.texts) {
const nearestArrow = findNearestArrow(text.topLeft, state.arrows);
if (nearestArrow) {
text2ArrowMap.set(text.uid, nearestArrow.uid);
const textsOfArrow = arrow2TextMap.get(nearestArrow.uid) || [];
textsOfArrow.push(text.uid);
arrow2TextMap.set(nearestArrow.uid, textsOfArrow);
}
}
// for visual feedback, when selecting/moving one thing, we also highlight (in green) all the things that belong to the thing we selected.
const sidesToHighlight: {[key: string]: RountanglePart[]} = {};
const arrowsToHighlight: {[key: string]: boolean} = {};
const textsToHighlight: {[key: string]: boolean} = {};
for (const selected of selection) {
for (const arrow of state.arrows) {
if (arrow.uid === selected.uid) {
const rSideStart = findNearestRountangleSide(arrow, "start", state.rountangles);
if (rSideStart) {
sidesToHighlight[rSideStart.uid] = [...(sidesToHighlight[rSideStart.uid] || []), rSideStart.part];
}
const rSideEnd = findNearestRountangleSide(arrow, "end", state.rountangles);
if (rSideEnd) {
sidesToHighlight[rSideEnd.uid] = [...(sidesToHighlight[rSideEnd.uid] || []), rSideEnd.part];
}
for (const text of state.texts) {
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
if (belongsToArrow === arrow) {
textsToHighlight[text.uid] = text;
}
}
const sides = arrow2SideMap.get(selected.uid);
if (sides) {
const [startSide, endSide] = sides;
if (startSide) sidesToHighlight[startSide.uid] = [...sidesToHighlight[startSide.uid]||[], startSide.part];
if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part];
}
const texts = arrow2TextMap.get(selected.uid);
if (texts) {
for (const textUid of texts) {
textsToHighlight[textUid] = true;
}
}
for (const text of state.texts) {
if (text.uid === selected.uid) {
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
if (belongsToArrow) {
arrowsToHighlight[belongsToArrow.uid] = belongsToArrow;
}
const arrows = side2ArrowMap.get(selected.uid);
if (arrows) {
for (const [arrowPart, arrowUid] of arrows) {
arrowsToHighlight[arrowUid] = true;
}
}
const arrow2 = text2ArrowMap.get(selected.uid);
if (arrow2) {
arrowsToHighlight[arrow2] = true;
}
}
const rootErrors = errors.filter(([uid]) => uid === "root").map(err=>err[1]);
@ -461,13 +488,22 @@ export function VisualEditor() {
errors={errors.filter(([uid,msg])=>uid===rountangle.uid).map(err=>err[1])}
/>)}
{state.arrows.map(arrow => <ArrowSVG
key={arrow.uid}
arrow={arrow}
selected={selection.find(a => a.uid === arrow.uid)?.parts || []}
errors={errors.filter(([uid,msg])=>uid===arrow.uid).map(err=>err[1])}
highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)}
/>
{state.arrows.map(arrow => {
const sides = arrow2SideMap.get(arrow.uid);
console.log(sides, arrow);
let arc = "no" as ArcDirection;
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0].uid !== undefined) {
arc = arcDirection(sides[0]?.part, sides[1]?.part);
}
return <ArrowSVG
key={arrow.uid}
arrow={arrow}
selected={selection.find(a => a.uid === arrow.uid)?.parts || []}
errors={errors.filter(([uid,msg])=>uid===arrow.uid).map(err=>err[1])}
highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)}
arc={arc}
/>;
}
)}
{state.texts.map(txt => {
@ -479,6 +515,7 @@ export function VisualEditor() {
{txt.text.slice(0, start.offset)}
<tspan className="error" data-uid={txt.uid} data-parts="text">
{txt.text.slice(start.offset, end.offset)}
{start.offset === end.offset && <>_</>}
</tspan>
{txt.text.slice(end.offset)}
</>;
@ -486,18 +523,11 @@ export function VisualEditor() {
else {
markedText = <>{txt.text}</>;
}
// const annotatedText = err ? [...txt.text].map((char,i) => {
// if (i >= err.location.start.offset && i < err.location.end.offset) {
// return char+'\u0332';
// }
// return char;
// }).join('') : txt.text;
return <text
key={txt.uid}
className={
(selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"")
+(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":"")
// +(errors.some(([uid]) => uid === txt.uid)?" error":"")
}
x={txt.topLeft.x}
y={txt.topLeft.y}
@ -677,20 +707,23 @@ export function RountangleSVG(props: {rountangle: Rountangle, selected: string[]
</g>;
}
export function ArrowSVG(props: {arrow: Arrow, selected: string[], errors: string[], highlight: boolean}) {
export function ArrowSVG(props: {arrow: Arrow, selected: string[], errors: string[], highlight: boolean, arc: ArcDirection}) {
const {start, end, uid} = props.arrow;
const radius = euclideanDistance(start, end)/1.6;
const largeArc = "1";
const arcOrLine = props.arc === "no" ? "L" :
`A ${radius} ${radius} 0 ${largeArc} ${props.arc === "ccw" ? "0" : "1"}`;
return <g>
<line
<path
className={"arrow"
+(props.selected.length===2?" selected":"")
+(props.errors.length>0?" error":"")
+(props.highlight?" highlight":"")
}
markerEnd='url(#arrowEnd)'
x1={start.x}
y1={start.y}
x2={end.x}
y2={end.y}
d={`M ${start.x} ${start.y}
${arcOrLine}
${end.x} ${end.y}`}
data-uid={uid}
data-parts="start end"
/>

View file

@ -26,6 +26,12 @@ export type Transition = {
}
export type Statechart = {
root: ConcreteState;
root: OrState;
transitions: Map<string, Transition[]>; // key: source state uid
variables: Set<string>;
inputEvents: Set<string>;
internalEvents: Set<string>;
outputEvents: Set<string>;
}

View file

@ -1,3 +1,5 @@
import { RountanglePart } from "./editor_types";
export type Vec2D = {
x: number;
y: number;
@ -161,3 +163,36 @@ export function getBottomSide(rect: Rect2D): Line2D {
end: { x: rect.topLeft.x + rect.size.x, y: rect.topLeft.y + rect.size.y },
};
}
export type ArcDirection = "no" | "cw" | "ccw";
export function arcDirection(start: RountanglePart, end: RountanglePart): ArcDirection {
if (start === end) {
if (start === "left" || start === "top") {
return "ccw";
}
else {
return "cw";
}
}
const both = [start, end];
if (both.includes("top") && both.includes("bottom")) {
return "no";
}
if (both.includes("left") && both.includes("right")) {
return "no";
}
if (start === "top" && end === "left") {
return "ccw";
}
if (start === "left" && end === "bottom") {
return "ccw";
}
if (start === "bottom" && end === "right") {
return "ccw";
}
if (start === "right" && end === "top") {
return "ccw";
}
return "cw";
}

View file

@ -0,0 +1,26 @@
import { ConcreteState, Statechart } from "./ast";
export function initialize(ast: Statechart): RT_Statechart {
const rt_root = recursiveEnter(ast.root) as RT_OrState;
return {
root: rt_root,
variables: new Map(),
};
}
export function recursiveEnter(state: ConcreteState): RT_ConcreteState {
if (state.kind === "and") {
return {
kind: "and",
children: state.children.map(child => recursiveEnter(child)),
};
}
else {
const currentState = state.initial[0][1];
return {
kind: "or",
current: currentState.uid,
current_rt: recursiveEnter(currentState),
};
}
}

View file

@ -32,7 +32,7 @@ export type RaiseEvent = {
}
export type Expression = BinaryExpression | UnaryExpression | VarRef;
export type Expression = BinaryExpression | UnaryExpression | VarRef | Literal;
export type BinaryExpression = {
kind: "binaryExpr";
@ -51,3 +51,8 @@ export type VarRef = {
kind: "ref";
variable: string;
}
export type Literal = {
kind: "literal";
value: any;
}

View file

@ -168,40 +168,54 @@ function peg$parse(input, options) {
const peg$c1 = "]";
const peg$c2 = "/";
const peg$c3 = "after";
const peg$c4 = "ms";
const peg$c5 = "s";
const peg$c6 = ";";
const peg$c7 = "=";
const peg$c8 = "(";
const peg$c9 = ")";
const peg$c10 = "true";
const peg$c11 = "false";
const peg$c12 = "^";
const peg$c4 = "entry";
const peg$c5 = "exit";
const peg$c6 = "ms";
const peg$c7 = "s";
const peg$c8 = ";";
const peg$c9 = "=";
const peg$c10 = "==";
const peg$c11 = "!=";
const peg$c12 = "<=";
const peg$c13 = ">=";
const peg$c14 = "(";
const peg$c15 = ")";
const peg$c16 = "true";
const peg$c17 = "false";
const peg$c18 = "^";
const peg$r0 = /^[a-zA-Z0-9]/;
const peg$r1 = /^[0-9]/;
const peg$r2 = /^[ \t\n\r]/;
const peg$r3 = /^[+\-]/;
const peg$r4 = /^[*\/]/;
const peg$r3 = /^[<>]/;
const peg$r4 = /^[+\-]/;
const peg$r5 = /^[*\/]/;
const peg$e0 = peg$literalExpectation("[", false);
const peg$e1 = peg$literalExpectation("]", false);
const peg$e2 = peg$literalExpectation("/", false);
const peg$e3 = peg$literalExpectation("after", false);
const peg$e4 = peg$literalExpectation("ms", false);
const peg$e5 = peg$literalExpectation("s", false);
const peg$e6 = peg$literalExpectation(";", false);
const peg$e7 = peg$literalExpectation("=", false);
const peg$e8 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false, false);
const peg$e9 = peg$classExpectation([["0", "9"]], false, false, false);
const peg$e10 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false);
const peg$e11 = peg$classExpectation(["+", "-"], false, false, false);
const peg$e12 = peg$classExpectation(["*", "/"], false, false, false);
const peg$e13 = peg$literalExpectation("(", false);
const peg$e14 = peg$literalExpectation(")", false);
const peg$e15 = peg$literalExpectation("true", false);
const peg$e16 = peg$literalExpectation("false", false);
const peg$e17 = peg$literalExpectation("^", false);
const peg$e4 = peg$literalExpectation("entry", false);
const peg$e5 = peg$literalExpectation("exit", false);
const peg$e6 = peg$literalExpectation("ms", false);
const peg$e7 = peg$literalExpectation("s", false);
const peg$e8 = peg$literalExpectation(";", false);
const peg$e9 = peg$literalExpectation("=", false);
const peg$e10 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false, false);
const peg$e11 = peg$classExpectation([["0", "9"]], false, false, false);
const peg$e12 = peg$classExpectation([" ", "\t", "\n", "\r"], false, false, false);
const peg$e13 = peg$literalExpectation("==", false);
const peg$e14 = peg$literalExpectation("!=", false);
const peg$e15 = peg$classExpectation(["<", ">"], false, false, false);
const peg$e16 = peg$literalExpectation("<=", false);
const peg$e17 = peg$literalExpectation(">=", false);
const peg$e18 = peg$classExpectation(["+", "-"], false, false, false);
const peg$e19 = peg$classExpectation(["*", "/"], false, false, false);
const peg$e20 = peg$literalExpectation("(", false);
const peg$e21 = peg$literalExpectation(")", false);
const peg$e22 = peg$literalExpectation("true", false);
const peg$e23 = peg$literalExpectation("false", false);
const peg$e24 = peg$literalExpectation("^", false);
function peg$f0(trigger, guard, actions) {
return {
@ -216,59 +230,76 @@ function peg$parse(input, options) {
function peg$f2(dur) {
return {kind: "after", durationMs: dur};
}
function peg$f3(num, u) {
return num * (u === "s" ? 1000 : 1);
function peg$f3() {
return {kind: "entry"};
}
function peg$f4() {
return {kind: "exit"};
}
function peg$f5(num, u) {
return num * (u === "s" ? 1000 : 1);
}
function peg$f6() {
return text();
}
function peg$f5(head, tail) {
function peg$f7(head, tail) {
return [head, ...tail.map(t => t[3])];
}
function peg$f6(lhs, rhs) {
function peg$f8(lhs, rhs) {
return {kind: "assignment", lhs, rhs};
}
function peg$f7() {
function peg$f9() {
return text();
}
function peg$f8() {
function peg$f10() {
return parseInt(text());
}
function peg$f9(prod, rest) {
function peg$f11(sum, rest) {
if (rest === null) {
return sum;
}
return {
kind: "binaryExpr",
operator: rest[0][1],
lhs: sum,
rhs: rest[1],
};
}
function peg$f12(prod, rest) {
if (rest === null) {
return prod;
}
return {
kind:"binaryExpr",
kind: "binaryExpr",
operator: rest[0][1],
lhs: prod,
rhs: rest[1],
};
}
function peg$f10(atom, rest) {
function peg$f13(atom, rest) {
if (rest === null) {
return atom;
}
return {
kind:"binaryExpr",
kind: "binaryExpr",
operator: rest[0][1],
lhs: atom,
rhs: rest[1],
};
}
function peg$f11(expr) {
function peg$f14(expr) {
return expr;
}
function peg$f12(value) {
function peg$f15(value) {
return {kind: "literal", value}
}
function peg$f13(variable) {
function peg$f16(variable) {
return {kind: "ref", variable}
}
function peg$f14() {
function peg$f17() {
return text() === "true";
}
function peg$f15(event) {
function peg$f18(event) {
return {kind: "raise", event};
}
let peg$currPos = options.peg$currPos | 0;
@ -459,7 +490,7 @@ function peg$parse(input, options) {
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
s7 = peg$parsesum();
s7 = peg$parsecompare();
if (s7 !== peg$FAILED) {
s8 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 93) {
@ -529,7 +560,13 @@ function peg$parse(input, options) {
s0 = peg$parseafterTrigger();
if (s0 === peg$FAILED) {
s0 = peg$parseeventTrigger();
s0 = peg$parseentryTrigger();
if (s0 === peg$FAILED) {
s0 = peg$parseexitTrigger();
if (s0 === peg$FAILED) {
s0 = peg$parseeventTrigger();
}
}
}
return s0;
@ -578,6 +615,46 @@ function peg$parse(input, options) {
return s0;
}
function peg$parseentryTrigger() {
let s0, s1;
s0 = peg$currPos;
if (input.substr(peg$currPos, 5) === peg$c4) {
s1 = peg$c4;
peg$currPos += 5;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e4); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f3();
}
s0 = s1;
return s0;
}
function peg$parseexitTrigger() {
let s0, s1;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c5) {
s1 = peg$c5;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e5); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f4();
}
s0 = s1;
return s0;
}
function peg$parsedurationMs() {
let s0, s1, s2, s3;
@ -588,7 +665,7 @@ function peg$parse(input, options) {
s3 = peg$parsetimeUnit();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f3(s1, s3);
s0 = peg$f5(s1, s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@ -604,25 +681,25 @@ function peg$parse(input, options) {
function peg$parsetimeUnit() {
let s0, s1;
if (input.substr(peg$currPos, 2) === peg$c4) {
s0 = peg$c4;
if (input.substr(peg$currPos, 2) === peg$c6) {
s0 = peg$c6;
peg$currPos += 2;
} else {
s0 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e4); }
if (peg$silentFails === 0) { peg$fail(peg$e6); }
}
if (s0 === peg$FAILED) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 115) {
s1 = peg$c5;
s1 = peg$c7;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e5); }
if (peg$silentFails === 0) { peg$fail(peg$e7); }
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f4();
s1 = peg$f6();
}
s0 = s1;
}
@ -640,11 +717,11 @@ function peg$parse(input, options) {
s3 = peg$currPos;
s4 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 59) {
s5 = peg$c6;
s5 = peg$c8;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e6); }
if (peg$silentFails === 0) { peg$fail(peg$e8); }
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
@ -665,11 +742,11 @@ function peg$parse(input, options) {
s3 = peg$currPos;
s4 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 59) {
s5 = peg$c6;
s5 = peg$c8;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e6); }
if (peg$silentFails === 0) { peg$fail(peg$e8); }
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
@ -688,17 +765,17 @@ function peg$parse(input, options) {
}
s3 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 59) {
s4 = peg$c6;
s4 = peg$c8;
peg$currPos++;
} else {
s4 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e6); }
if (peg$silentFails === 0) { peg$fail(peg$e8); }
}
if (s4 === peg$FAILED) {
s4 = null;
}
peg$savedPos = s0;
s0 = peg$f5(s1, s2);
s0 = peg$f7(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@ -726,18 +803,18 @@ function peg$parse(input, options) {
if (s1 !== peg$FAILED) {
s2 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 61) {
s3 = peg$c7;
s3 = peg$c9;
peg$currPos++;
} else {
s3 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e7); }
if (peg$silentFails === 0) { peg$fail(peg$e9); }
}
if (s3 !== peg$FAILED) {
s4 = peg$parse_();
s5 = peg$parsesum();
s5 = peg$parsecompare();
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f6(s1, s5);
s0 = peg$f8(s1, s5);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@ -764,7 +841,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e8); }
if (peg$silentFails === 0) { peg$fail(peg$e10); }
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
@ -774,7 +851,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e8); }
if (peg$silentFails === 0) { peg$fail(peg$e10); }
}
}
} else {
@ -782,7 +859,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f7();
s1 = peg$f9();
}
s0 = s1;
@ -799,7 +876,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e9); }
if (peg$silentFails === 0) { peg$fail(peg$e11); }
}
if (s2 !== peg$FAILED) {
while (s2 !== peg$FAILED) {
@ -809,7 +886,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s2 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e9); }
if (peg$silentFails === 0) { peg$fail(peg$e11); }
}
}
} else {
@ -817,7 +894,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f8();
s1 = peg$f10();
}
s0 = s1;
@ -834,7 +911,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e10); }
if (peg$silentFails === 0) { peg$fail(peg$e12); }
}
while (s1 !== peg$FAILED) {
s0.push(s1);
@ -843,7 +920,7 @@ function peg$parse(input, options) {
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e10); }
if (peg$silentFails === 0) { peg$fail(peg$e12); }
}
}
peg$silentFails--;
@ -851,6 +928,92 @@ function peg$parse(input, options) {
return s0;
}
function peg$parsecompare() {
let s0, s1, s2, s3, s4, s5, s6;
s0 = peg$currPos;
s1 = peg$parsesum();
if (s1 !== peg$FAILED) {
s2 = peg$currPos;
s3 = peg$currPos;
s4 = peg$parse_();
if (input.substr(peg$currPos, 2) === peg$c10) {
s5 = peg$c10;
peg$currPos += 2;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e13); }
}
if (s5 === peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c11) {
s5 = peg$c11;
peg$currPos += 2;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e14); }
}
if (s5 === peg$FAILED) {
s5 = input.charAt(peg$currPos);
if (peg$r3.test(s5)) {
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e15); }
}
if (s5 === peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c12) {
s5 = peg$c12;
peg$currPos += 2;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e16); }
}
if (s5 === peg$FAILED) {
if (input.substr(peg$currPos, 2) === peg$c13) {
s5 = peg$c13;
peg$currPos += 2;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
}
}
}
}
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
s4 = [s4, s5, s6];
s3 = s4;
} else {
peg$currPos = s3;
s3 = peg$FAILED;
}
if (s3 !== peg$FAILED) {
s4 = peg$parsecompare();
if (s4 !== peg$FAILED) {
s3 = [s3, s4];
s2 = s3;
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
} else {
peg$currPos = s2;
s2 = peg$FAILED;
}
if (s2 === peg$FAILED) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f11(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
}
return s0;
}
function peg$parsesum() {
let s0, s1, s2, s3, s4, s5, s6;
@ -861,11 +1024,11 @@ function peg$parse(input, options) {
s3 = peg$currPos;
s4 = peg$parse_();
s5 = input.charAt(peg$currPos);
if (peg$r3.test(s5)) {
if (peg$r4.test(s5)) {
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e11); }
if (peg$silentFails === 0) { peg$fail(peg$e18); }
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
@ -892,7 +1055,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f9(s1, s2);
s0 = peg$f12(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@ -911,11 +1074,11 @@ function peg$parse(input, options) {
s3 = peg$currPos;
s4 = peg$parse_();
s5 = input.charAt(peg$currPos);
if (peg$r4.test(s5)) {
if (peg$r5.test(s5)) {
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e12); }
if (peg$silentFails === 0) { peg$fail(peg$e19); }
}
if (s5 !== peg$FAILED) {
s6 = peg$parse_();
@ -942,7 +1105,7 @@ function peg$parse(input, options) {
s2 = null;
}
peg$savedPos = s0;
s0 = peg$f10(s1, s2);
s0 = peg$f13(s1, s2);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@ -970,27 +1133,27 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 40) {
s1 = peg$c8;
s1 = peg$c14;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e13); }
if (peg$silentFails === 0) { peg$fail(peg$e20); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parse_();
s3 = peg$parsesum();
s3 = peg$parsecompare();
if (s3 !== peg$FAILED) {
s4 = peg$parse_();
if (input.charCodeAt(peg$currPos) === 41) {
s5 = peg$c9;
s5 = peg$c15;
peg$currPos++;
} else {
s5 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e14); }
if (peg$silentFails === 0) { peg$fail(peg$e21); }
}
if (s5 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f11(s3);
s0 = peg$f14(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;
@ -1017,7 +1180,7 @@ function peg$parse(input, options) {
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f12(s1);
s1 = peg$f15(s1);
}
s0 = s1;
@ -1031,7 +1194,7 @@ function peg$parse(input, options) {
s1 = peg$parseidentifier();
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f13(s1);
s1 = peg$f16(s1);
}
s0 = s1;
@ -1042,25 +1205,25 @@ function peg$parse(input, options) {
let s0, s1;
s0 = peg$currPos;
if (input.substr(peg$currPos, 4) === peg$c10) {
s1 = peg$c10;
if (input.substr(peg$currPos, 4) === peg$c16) {
s1 = peg$c16;
peg$currPos += 4;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e15); }
if (peg$silentFails === 0) { peg$fail(peg$e22); }
}
if (s1 === peg$FAILED) {
if (input.substr(peg$currPos, 5) === peg$c11) {
s1 = peg$c11;
if (input.substr(peg$currPos, 5) === peg$c17) {
s1 = peg$c17;
peg$currPos += 5;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e16); }
if (peg$silentFails === 0) { peg$fail(peg$e23); }
}
}
if (s1 !== peg$FAILED) {
peg$savedPos = s0;
s1 = peg$f14();
s1 = peg$f17();
}
s0 = s1;
@ -1072,18 +1235,18 @@ function peg$parse(input, options) {
s0 = peg$currPos;
if (input.charCodeAt(peg$currPos) === 94) {
s1 = peg$c12;
s1 = peg$c18;
peg$currPos++;
} else {
s1 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e17); }
if (peg$silentFails === 0) { peg$fail(peg$e24); }
}
if (s1 !== peg$FAILED) {
s2 = peg$parse_();
s3 = peg$parseidentifier();
if (s3 !== peg$FAILED) {
peg$savedPos = s0;
s0 = peg$f15(s3);
s0 = peg$f18(s3);
} else {
peg$currPos = s0;
s0 = peg$FAILED;

View file

@ -1,6 +1,6 @@
export const ARROW_SNAP_THRESHOLD = 20;
export const TEXT_SNAP_THRESHOLD = 20;
export const TEXT_SNAP_THRESHOLD = 30;
export const ROUNTANGLE_RADIUS = 20;
export const MIN_ROUNTANGLE_SIZE = { x: ROUNTANGLE_RADIUS*2, y: ROUNTANGLE_RADIUS*2 };

View file

@ -1,9 +1,10 @@
import { act } from "react";
import { ConcreteState, OrState, Statechart, Transition } from "./ast";
import { findNearestArrow, findNearestRountangleSide, Rountangle, VisualEditorState } from "./editor_types";
import { isEntirelyWithin } from "./geometry";
import { TransitionLabel } from "./label_ast";
import { isEntirelyWithin, transformLine } from "./geometry";
import { Action, Expression, TransitionLabel } from "./label_ast";
import { parse as parseLabel } from "./label_parser";
import { parse as parseLabel, SyntaxError } from "./label_parser";
export function parseStatechart(state: VisualEditorState): [Statechart, [string,string][]] {
const errorShapes: [string, string][] = [];
@ -113,6 +114,11 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
}
}
let variables = new Set<string>();
const inputEvents = new Set<string>();
const outputEvents = new Set<string>();
const internalEvents = new Set<string>();
// step 3: figure out labels
for (const text of state.texts) {
@ -125,10 +131,42 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
try {
transitionLabel = parseLabel(text.text); // may throw
belongsToTransition.label.push(transitionLabel);
// collect events
if (transitionLabel.trigger.kind === "event") {
const {event} = transitionLabel.trigger;
if (event.startsWith("_")) {
internalEvents.add(event);
}
else {
inputEvents.add(event);
}
}
for (const action of transitionLabel.actions) {
if (action.kind === "raise") {
const {event} = action;
if (event.startsWith("_")) {
internalEvents.add(event);
}
else {
outputEvents.add(event);
}
}
}
// collect variables
variables = variables
.union(findVariables(transitionLabel.guard));
for (const action of transitionLabel.actions) {
variables = variables.union(findVariablesAction(action));
}
}
catch (e) {
console.log({e});
errorShapes.push([text.uid, e]);
if (e instanceof SyntaxError) {
belongsToTransition.label.push(null);
errorShapes.push([text.uid, e]);
}
else {
throw e;
}
}
}
}
@ -146,5 +184,32 @@ export function parseStatechart(state: VisualEditorState): [Statechart, [string,
return [{
root,
transitions,
variables,
inputEvents,
internalEvents,
outputEvents,
}, errorShapes];
}
}
function findVariables(expr: Expression): Set<string> {
if (expr.kind === "ref") {
return new Set([expr.variable]);
}
else if (expr.kind === "unaryExpr") {
return findVariables(expr.expr);
}
else if (expr.kind === "binaryExpr") {
return findVariables(expr.lhs).union(findVariables(expr.rhs));
}
else if (expr.kind === "literal") {
return new Set();
}
}
function findVariablesAction(action: Action): Set<string> {
if (action.kind === "assignment") {
return new Set([action.lhs, ...findVariables(action.rhs)]);
}
return new Set();
}

View file

@ -0,0 +1,18 @@
type RT_ConcreteState = RT_OrState | RT_AndState;
type RT_OrState = {
kind: "or";
current: string;
current_rt: RT_ConcreteState; // keep the runtime configuration only of the current state
}
type RT_AndState = {
kind: "and";
children: RT_ConcreteState[]; // keep the runtime configuration of every child
}
type RT_Statechart = {
root: RT_OrState;
variables: Map<string, any>;
}

View file

@ -6,7 +6,7 @@ start = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions
};
}
trigger = afterTrigger / eventTrigger
trigger = afterTrigger / entryTrigger / exitTrigger / eventTrigger
eventTrigger = event:identifier {
return {kind: "event", event};
@ -16,6 +16,15 @@ afterTrigger = "after" _ dur:durationMs {
return {kind: "after", durationMs: dur};
}
entryTrigger = "entry" {
return {kind: "entry"};
}
exitTrigger = "exit" {
return {kind: "exit"};
}
durationMs = num:number _ u:timeUnit {
return num * (u === "s" ? 1000 : 1);
}
@ -46,14 +55,26 @@ number = [0-9]+ {
_ "whitespace"
= [ \t\n\r]*
expr = sum
expr = compare
compare = sum:sum rest:((_ ("==" / "!=" / "<" / ">" / "<=" / ">=") _) compare)? {
if (rest === null) {
return sum;
}
return {
kind: "binaryExpr",
operator: rest[0][1],
lhs: sum,
rhs: rest[1],
};
}
sum = prod:product rest:((_ ("+" / "-") _) sum)? {
if (rest === null) {
return prod;
}
return {
kind:"binaryExpr",
kind: "binaryExpr",
operator: rest[0][1],
lhs: prod,
rhs: rest[1],
@ -65,7 +86,7 @@ product = atom:atom rest:((_ ("*" / "/") _) product)? {
return atom;
}
return {
kind:"binaryExpr",
kind: "binaryExpr",
operator: rest[0][1],
lhs: atom,
rhs: rest[1],