From da0e56e17c323790c81fd66725d9a7e9c9b90510 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Mon, 6 Oct 2025 17:15:51 +0200 Subject: [PATCH] arcs nicely curve when they connect a rountangle to itself --- src/VisualEditor/VisualEditor.css | 1 + src/VisualEditor/VisualEditor.tsx | 129 +++++--- src/VisualEditor/ast.ts | 8 +- src/VisualEditor/geometry.ts | 35 +++ src/VisualEditor/interpreter.ts | 26 ++ src/VisualEditor/label_ast.ts | 7 +- src/VisualEditor/label_parser.js | 347 ++++++++++++++++------ src/VisualEditor/parameters.ts | 2 +- src/VisualEditor/parser.ts | 77 ++++- src/VisualEditor/runtime_types.ts | 18 ++ src/VisualEditor/transition_label.grammar | 29 +- 11 files changed, 526 insertions(+), 153 deletions(-) create mode 100644 src/VisualEditor/interpreter.ts create mode 100644 src/VisualEditor/runtime_types.ts diff --git a/src/VisualEditor/VisualEditor.css b/src/VisualEditor/VisualEditor.css index 5b52cc4..c275e26 100644 --- a/src/VisualEditor/VisualEditor.css +++ b/src/VisualEditor/VisualEditor.css @@ -82,6 +82,7 @@ text.highlight { } .arrow { + fill: none; stroke: black; stroke-width: 2px; } diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index 951ef32..62ce237 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -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(); + const side2ArrowMap = new Map>(); + const text2ArrowMap = new Map(); + const arrow2TextMap = new Map(); + 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 => 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 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)} {txt.text.slice(start.offset, end.offset)} + {start.offset === end.offset && <>_} {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 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[] ; } -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 - 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" /> diff --git a/src/VisualEditor/ast.ts b/src/VisualEditor/ast.ts index 72be756..63612e4 100644 --- a/src/VisualEditor/ast.ts +++ b/src/VisualEditor/ast.ts @@ -26,6 +26,12 @@ export type Transition = { } export type Statechart = { - root: ConcreteState; + root: OrState; transitions: Map; // key: source state uid + + variables: Set; + + inputEvents: Set; + internalEvents: Set; + outputEvents: Set; } diff --git a/src/VisualEditor/geometry.ts b/src/VisualEditor/geometry.ts index 81f3684..a272284 100644 --- a/src/VisualEditor/geometry.ts +++ b/src/VisualEditor/geometry.ts @@ -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"; +} \ No newline at end of file diff --git a/src/VisualEditor/interpreter.ts b/src/VisualEditor/interpreter.ts new file mode 100644 index 0000000..f95279c --- /dev/null +++ b/src/VisualEditor/interpreter.ts @@ -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), + }; + } +} \ No newline at end of file diff --git a/src/VisualEditor/label_ast.ts b/src/VisualEditor/label_ast.ts index a7b9579..6bec803 100644 --- a/src/VisualEditor/label_ast.ts +++ b/src/VisualEditor/label_ast.ts @@ -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; +} \ No newline at end of file diff --git a/src/VisualEditor/label_parser.js b/src/VisualEditor/label_parser.js index b275b41..a334518 100644 --- a/src/VisualEditor/label_parser.js +++ b/src/VisualEditor/label_parser.js @@ -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; diff --git a/src/VisualEditor/parameters.ts b/src/VisualEditor/parameters.ts index 7b47a33..2015ccd 100644 --- a/src/VisualEditor/parameters.ts +++ b/src/VisualEditor/parameters.ts @@ -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 }; diff --git a/src/VisualEditor/parser.ts b/src/VisualEditor/parser.ts index cd003a2..2b5aaf5 100644 --- a/src/VisualEditor/parser.ts +++ b/src/VisualEditor/parser.ts @@ -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(); + const inputEvents = new Set(); + const outputEvents = new Set(); + const internalEvents = new Set(); + // 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]; -} \ No newline at end of file +} + +function findVariables(expr: Expression): Set { + 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 { + if (action.kind === "assignment") { + return new Set([action.lhs, ...findVariables(action.rhs)]); + } + return new Set(); +} + diff --git a/src/VisualEditor/runtime_types.ts b/src/VisualEditor/runtime_types.ts new file mode 100644 index 0000000..d7af957 --- /dev/null +++ b/src/VisualEditor/runtime_types.ts @@ -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; +} diff --git a/src/VisualEditor/transition_label.grammar b/src/VisualEditor/transition_label.grammar index b2f723c..9198b9a 100644 --- a/src/VisualEditor/transition_label.grammar +++ b/src/VisualEditor/transition_label.grammar @@ -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],