From b55cba198e9cb61e226d83afed48e6a004cb204c Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Fri, 17 Oct 2025 14:37:26 +0200 Subject: [PATCH] parser and editor use same code for figuring out what is connected to what --- src/VisualEditor/HistorySVG.tsx | 16 ++- src/VisualEditor/VisualEditor.css | 7 +- src/VisualEditor/VisualEditor.tsx | 135 +++++++++++--------------- src/statecharts/concrete_syntax.ts | 20 +++- src/statecharts/detect_connections.ts | 84 ++++++++++++++++ src/statecharts/parser.ts | 115 +++++++++++----------- 6 files changed, 228 insertions(+), 149 deletions(-) create mode 100644 src/statecharts/detect_connections.ts diff --git a/src/VisualEditor/HistorySVG.tsx b/src/VisualEditor/HistorySVG.tsx index 2e031d9..6ca1be1 100644 --- a/src/VisualEditor/HistorySVG.tsx +++ b/src/VisualEditor/HistorySVG.tsx @@ -1,11 +1,10 @@ import { Vec2D } from "./geometry"; import { HISTORY_RADIUS } from "./parameters"; -export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|"deep", selected: boolean}) { +export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|"deep", selected: boolean, highlight: boolean}) { const text = props.kind === "shallow" ? "H" : "H*"; return <> {text} @@ -29,5 +28,14 @@ export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"| data-uid={props.uid} data-parts="history" /> + {(props.selected || props.highlight) && + } ; -} \ No newline at end of file +} diff --git a/src/VisualEditor/VisualEditor.css b/src/VisualEditor/VisualEditor.css index d9ee73d..62268c1 100644 --- a/src/VisualEditor/VisualEditor.css +++ b/src/VisualEditor/VisualEditor.css @@ -44,7 +44,6 @@ .rountangle.active { fill: darkorange; fill-opacity: 0.2; - /* filter: drop-shadow( 3px 3px 2px rgba(0, 0, 0, .7)); */ stroke-width: 3px; } @@ -131,8 +130,6 @@ text.helper:hover { } .draggableText, .draggableText.highlight { - /* text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff, 1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff; */ - /* -webkit-text-stroke: 4px white; */ paint-order: stroke; stroke: white; stroke-width: 4px; @@ -140,7 +137,6 @@ text.helper:hover { stroke-linejoin: miter; stroke-opacity: 1; fill-opacity:1; - /* font-weight: 800; */ } .draggableText.highlight:not(.selected) { @@ -148,9 +144,10 @@ text.helper:hover { font-weight: 600; } -.highlight:not(.selected) { +.highlight:not(.selected):not(text) { stroke: green; stroke-width: 3px; + fill: none; } .arrow.error { diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index 4e3ce6d..098ef44 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -1,20 +1,21 @@ import * as lz4 from "@nick/lz4"; -import { Dispatch, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react"; +import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from "react"; import { Statechart } from "../statecharts/abstract_syntax"; -import { Arrow, ArrowPart, Diamond, Rountangle, RountanglePart, Text, VisualEditorState, emptyState, findNearestArrow, findNearestSide, findRountangle } from "../statecharts/concrete_syntax"; +import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text, VisualEditorState, emptyState } from "../statecharts/concrete_syntax"; import { parseStatechart, TraceableError } from "../statecharts/parser"; import { BigStep } from "../statecharts/runtime_types"; import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry"; import { MIN_ROUNTANGLE_SIZE } from "./parameters"; import { getBBoxInSvgCoords } from "./svg_helper"; - -import "./VisualEditor.css"; import { ArrowSVG } from "./ArrowSVG"; import { RountangleSVG } from "./RountangleSVG"; import { TextSVG } from "./TextSVG"; import { DiamondSVG } from "./DiamondSVG"; import { HistorySVG } from "./HistorySVG"; +import { detectConnections } from "../statecharts/detect_connections"; + +import "./VisualEditor.css"; type DraggingState = { @@ -142,28 +143,22 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor }, []); useEffect(() => { - // delay is necessary for 2 reasons: - // 1) it's a hack - prevents us from writing the initial state to localstorage (before having recovered the state that was in localstorage) - // 2) performance: only save when the user does nothing const timeout = setTimeout(() => { const stateBuffer = new TextEncoder().encode(JSON.stringify(state)); const compressedStateBuffer = lz4.compress(stateBuffer); const compressedStateString = compressedStateBuffer.toBase64(); window.location.hash = "#"+compressedStateString; - - // const [statechart, errors] = parseStatechart(state); - // setErrors(errors); - // setAST(statechart); }, 200); return () => clearTimeout(timeout); }, [state]); + const conns = useMemo(() => detectConnections(state), [state]); + useEffect(() => { - const [statechart, errors] = parseStatechart(state); + const [statechart, errors] = parseStatechart(state, conns); setErrors(errors); setAST(statechart); }, [state]) - function getCurrentPointer(e: {pageX: number, pageY: number}) { const bbox = refSVG.current!.getBoundingClientRect(); @@ -514,90 +509,50 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor }; }, [selectingState, dragging]); - // detect what is 'connected' - const arrow2SideMap = new Map(); - const side2ArrowMap = new Map>(); - const text2ArrowMap = new Map(); - const arrow2TextMap = new Map(); - const text2RountangleMap = new Map(); - const rountangle2TextMap = new Map(); - - // arrow <-> (rountangle | diamond) - for (const arrow of state.arrows) { - const sides = [...state.rountangles, ...state.diamonds]; - const startSide = findNearestSide(arrow, "start", sides); - const endSide = findNearestSide(arrow, "end", sides); - if (startSide || endSide) { - arrow2SideMap.set(arrow.uid, [startSide, endSide]); - } - if (startSide) { - const arrowConns = side2ArrowMap.get(startSide.uid + '/' + startSide.part) || new Set(); - arrowConns.add(["start", arrow.uid]); - side2ArrowMap.set(startSide.uid + '/' + startSide.part, arrowConns); - } - if (endSide) { - const arrowConns = side2ArrowMap.get(endSide.uid + '/' + endSide.part) || new Set(); - arrowConns.add(["end", arrow.uid]); - side2ArrowMap.set(endSide.uid + '/' + endSide.part, arrowConns); - } - } - // text <-> arrow - for (const text of state.texts) { - const nearestArrow = findNearestArrow(text.topLeft, state.arrows); - if (nearestArrow) { - // prioritize text belonging to arrows: - text2ArrowMap.set(text.uid, nearestArrow.uid); - const textsOfArrow = arrow2TextMap.get(nearestArrow.uid) || []; - textsOfArrow.push(text.uid); - arrow2TextMap.set(nearestArrow.uid, textsOfArrow); - } - else { - // text <-> rountangle - const rountangle = findRountangle(text.topLeft, state.rountangles); - if (rountangle) { - text2RountangleMap.set(text.uid, rountangle.uid); - const texts = rountangle2TextMap.get(rountangle.uid) || []; - texts.push(text.uid); - rountangle2TextMap.set(rountangle.uid, texts); - } - } - } - // 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} = {}; const rountanglesToHighlight: {[key: string]: boolean} = {}; + const historyToHighlight: {[key: string]: boolean} = {}; for (const selected of selection) { - const sides = arrow2SideMap.get(selected.uid); + const sides = conns.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) || []), - ...(rountangle2TextMap.get(selected.uid) || []), + ...(conns.arrow2TextMap.get(selected.uid) || []), + ...(conns.rountangle2TextMap.get(selected.uid) || []), ]; for (const textUid of texts) { textsToHighlight[textUid] = true; } for (const part of selected.parts) { - const arrows = side2ArrowMap.get(selected.uid + '/' + part) || []; + const arrows = conns.side2ArrowMap.get(selected.uid + '/' + part) || []; if (arrows) { for (const [arrowPart, arrowUid] of arrows) { arrowsToHighlight[arrowUid] = true; } } } - const arrow2 = text2ArrowMap.get(selected.uid); + const arrow2 = conns.text2ArrowMap.get(selected.uid); if (arrow2) { arrowsToHighlight[arrow2] = true; } - const rountangleUid = text2RountangleMap.get(selected.uid) + const rountangleUid = conns.text2RountangleMap.get(selected.uid) if (rountangleUid) { rountanglesToHighlight[rountangleUid] = true; } + const history = conns.arrow2HistoryMap.get(selected.uid); + if (history) { + historyToHighlight[history] = true; + } + const arrow3 = conns.history2ArrowMap.get(selected.uid) || []; + for (const arrow of arrow3) { + arrowsToHighlight[arrow] = true; + } } function onPaste(e: ClipboardEvent) { @@ -619,6 +574,11 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor uid: (nextID++).toString(), topLeft: addV2D(r.topLeft, offset), } as Rountangle)); + const copiedDiamonds: Diamond[] = parsed.diamonds.map((r: Diamond) => ({ + ...r, + uid: (nextID++).toString(), + topLeft: addV2D(r.topLeft, offset), + } as Diamond)); const copiedArrows: Arrow[] = parsed.arrows.map((a: Arrow) => ({ ...a, uid: (nextID++).toString(), @@ -630,18 +590,27 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor uid: (nextID++).toString(), topLeft: addV2D(t.topLeft, offset), } as Text)); + const copiedHistories: History[] = parsed.history.map((h: History) => ({ + ...h, + uid: (nextID++).toString(), + topLeft: addV2D(h.topLeft, offset), + })) setState(state => ({ ...state, rountangles: [...state.rountangles, ...copiedRountangles], + diamonds: [...state.diamonds, ...copiedDiamonds], arrows: [...state.arrows, ...copiedArrows], texts: [...state.texts, ...copiedTexts], + history: [...state.history, ...copiedHistories], nextID: nextID, })); // @ts-ignore const newSelection: Selection = [ ...copiedRountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})), + ...copiedDiamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})), ...copiedArrows.map(a => ({uid: a.uid, parts: ["start", "end"]})), ...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})), + ...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})), ]; setSelection(newSelection); // copyInternal(newSelection, e); // doesn't work @@ -655,10 +624,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor function copyInternal(selection: Selection, e: ClipboardEvent) { const uidsToCopy = new Set(selection.map(shape => shape.uid)); const rountanglesToCopy = state.rountangles.filter(r => uidsToCopy.has(r.uid)); + const diamondsToCopy = state.diamonds.filter(d => uidsToCopy.has(d.uid)); + const historiesToCopy = state.history.filter(h => uidsToCopy.has(h.uid)); const arrowsToCopy = state.arrows.filter(a => uidsToCopy.has(a.uid)); const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid)); e.clipboardData?.setData("text/plain", JSON.stringify({ rountangles: rountanglesToCopy, + diamonds: diamondsToCopy, + history: historiesToCopy, arrows: arrowsToCopy, texts: textsToCopy, })); @@ -739,15 +712,16 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor {(rootErrors.length>0) && {rootErrors.join(' ')}} - {state.rountangles.map(rountangle => r.uid === rountangle.uid)?.parts || []} - highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]} - errors={errors - .filter(({shapeUid}) => shapeUid === rountangle.uid) - .map(({message}) => message)} - active={active.has(rountangle.uid)} + {state.rountangles.map(rountangle => + r.uid === rountangle.uid)?.parts || []} + highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]} + errors={errors + .filter(({shapeUid}) => shapeUid === rountangle.uid) + .map(({message}) => message)} + active={active.has(rountangle.uid)} />)} {state.diamonds.map(diamond => <> @@ -763,11 +737,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor )} {state.history.map(history => <> - h.uid === history.uid)} /> + h.uid === history.uid))} + highlight={Boolean(historyToHighlight[history.uid])} + /> )} {state.arrows.map(arrow => { - const sides = arrow2SideMap.get(arrow.uid); + const sides = conns.arrow2SideMap.get(arrow.uid); 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); diff --git a/src/statecharts/concrete_syntax.ts b/src/statecharts/concrete_syntax.ts index 1e35785..b4b45af 100644 --- a/src/statecharts/concrete_syntax.ts +++ b/src/statecharts/concrete_syntax.ts @@ -1,5 +1,5 @@ -import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox } from "../VisualEditor/geometry"; -import { ARROW_SNAP_THRESHOLD, TEXT_SNAP_THRESHOLD } from "../VisualEditor/parameters"; +import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox, subtractV2D } from "../VisualEditor/geometry"; +import { ARROW_SNAP_THRESHOLD, HISTORY_RADIUS, TEXT_SNAP_THRESHOLD } from "../VisualEditor/parameters"; import { sides } from "../VisualEditor/VisualEditor"; export type Rountangle = { @@ -115,3 +115,19 @@ export function findRountangle(point: Vec2D, candidates: Rountangle[]): Rountang } } } + +export function findNearestHistory(point: Vec2D, candidates: History[]): History | undefined { + let best; + let bestDistance = Infinity; + for (const h of candidates) { + const diff = subtractV2D(point, {x: h.topLeft.x+HISTORY_RADIUS, y: h.topLeft.y+HISTORY_RADIUS}); + const euclideanDistance = Math.hypot(diff.x, diff.y) - HISTORY_RADIUS; + if (euclideanDistance < ARROW_SNAP_THRESHOLD) { + if (euclideanDistance < bestDistance) { + best = h; + bestDistance = euclideanDistance; + } + } + } + return best; +} diff --git a/src/statecharts/detect_connections.ts b/src/statecharts/detect_connections.ts new file mode 100644 index 0000000..b5bbb4d --- /dev/null +++ b/src/statecharts/detect_connections.ts @@ -0,0 +1,84 @@ +import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RountanglePart, VisualEditorState } from "./concrete_syntax"; + +export type Connections = { + arrow2SideMap: Map, + side2ArrowMap: Map>, + text2ArrowMap: Map, + arrow2TextMap: Map, + arrow2HistoryMap: Map, + text2RountangleMap: Map, + rountangle2TextMap: Map, + history2ArrowMap: Map, +} + +export function detectConnections(state: VisualEditorState): Connections { + // detect what is 'connected' + const arrow2SideMap = new Map(); + const side2ArrowMap = new Map>(); + const text2ArrowMap = new Map(); + const arrow2TextMap = new Map(); + const arrow2HistoryMap = new Map(); + const text2RountangleMap = new Map(); + const rountangle2TextMap = new Map(); + const history2ArrowMap = new Map(); + + // arrow <-> (rountangle | diamond) + for (const arrow of state.arrows) { + // snap to history: + const historyTarget = findNearestHistory(arrow.end, state.history); + if (historyTarget) { + arrow2HistoryMap.set(arrow.uid, historyTarget.uid); + history2ArrowMap.set(historyTarget.uid, [...(history2ArrowMap.get(historyTarget.uid) || []), arrow.uid]); + } + + // snap to rountangle/diamon side: + const sides = [...state.rountangles, ...state.diamonds]; + const startSide = findNearestSide(arrow, "start", sides); + const endSide = historyTarget ? undefined : findNearestSide(arrow, "end", sides); + if (startSide || endSide) { + arrow2SideMap.set(arrow.uid, [startSide, endSide]); + } + if (startSide) { + const arrowConns = side2ArrowMap.get(startSide.uid + '/' + startSide.part) || new Set(); + arrowConns.add(["start", arrow.uid]); + side2ArrowMap.set(startSide.uid + '/' + startSide.part, arrowConns); + } + if (endSide) { + const arrowConns = side2ArrowMap.get(endSide.uid + '/' + endSide.part) || new Set(); + arrowConns.add(["end", arrow.uid]); + side2ArrowMap.set(endSide.uid + '/' + endSide.part, arrowConns); + } + } + // text <-> arrow + for (const text of state.texts) { + const nearestArrow = findNearestArrow(text.topLeft, state.arrows); + if (nearestArrow) { + // prioritize text belonging to arrows: + text2ArrowMap.set(text.uid, nearestArrow.uid); + const textsOfArrow = arrow2TextMap.get(nearestArrow.uid) || []; + textsOfArrow.push(text.uid); + arrow2TextMap.set(nearestArrow.uid, textsOfArrow); + } + else { + // text <-> rountangle + const rountangle = findRountangle(text.topLeft, state.rountangles); + if (rountangle) { + text2RountangleMap.set(text.uid, rountangle.uid); + const texts = rountangle2TextMap.get(rountangle.uid) || []; + texts.push(text.uid); + rountangle2TextMap.set(rountangle.uid, texts); + } + } + } + + return { + arrow2SideMap, + side2ArrowMap, + text2ArrowMap, + arrow2TextMap, + arrow2HistoryMap, + text2RountangleMap, + rountangle2TextMap, + history2ArrowMap, + }; +} diff --git a/src/statecharts/parser.ts b/src/statecharts/parser.ts index 8fe35f0..eca8fa0 100644 --- a/src/statecharts/parser.ts +++ b/src/statecharts/parser.ts @@ -1,9 +1,9 @@ -import { AbstractState, ConcreteState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax"; -import { findNearestArrow, findNearestSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax"; +import { ConcreteState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax"; +import { Rountangle, VisualEditorState } from "./concrete_syntax"; import { isEntirelyWithin } from "../VisualEditor/geometry"; import { Action, EventTrigger, Expression, ParsedText } from "./label_ast"; - import { parse as parseLabel, SyntaxError } from "./label_parser"; +import { Connections } from "./detect_connections"; export type TraceableError = { shapeUid: string; @@ -29,7 +29,7 @@ function addEvent(events: EventTrigger[], e: EventTrigger, textUid: string) { } } -export function parseStatechart(state: VisualEditorState): [Statechart, TraceableError[]] { +export function parseStatechart(state: VisualEditorState, conns: Connections): [Statechart, TraceableError[]] { const errors: TraceableError[] = []; // implicitly, the root is always an Or-state @@ -120,9 +120,8 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl const uid2Transition = new Map(); for (const arr of state.arrows) { - const sides = [...state.rountangles, ...state.diamonds]; - const srcUID = findNearestSide(arr, "start", sides)?.uid; - const tgtUID = findNearestSide(arr, "end", sides)?.uid; + const srcUID = conns.arrow2SideMap.get(arr.uid)?.[0]?.uid; + const tgtUID = conns.arrow2SideMap.get(arr.uid)?.[1]?.uid; if (!srcUID) { if (!tgtUID) { // dangling edge @@ -222,73 +221,71 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl throw e; } } - const belongsToArrow = findNearestArrow(text.topLeft, state.arrows); - if (belongsToArrow) { - const belongsToTransition = uid2Transition.get(belongsToArrow.uid); - if (belongsToTransition) { - const {src} = belongsToTransition; - belongsToTransition.label.push(parsed); - if (parsed.kind === "transitionLabel") { - // collect events - // triggers - if (parsed.trigger.kind === "event") { - if (src.kind === "pseudo") { - errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have event trigger"}); + const belongsToArrowUID = conns.text2ArrowMap.get(text.uid); + const belongsToTransition = uid2Transition.get(belongsToArrowUID!); + if (belongsToTransition) { + const {src} = belongsToTransition; + belongsToTransition.label.push(parsed); + if (parsed.kind === "transitionLabel") { + // collect events + // triggers + if (parsed.trigger.kind === "event") { + if (src.kind === "pseudo") { + errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have event trigger"}); + } + else { + const {event} = parsed.trigger; + if (event.startsWith("_")) { + errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid)); } else { - const {event} = parsed.trigger; - if (event.startsWith("_")) { - errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid)); - } - else { - errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid)); - } + errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid)); } } - else if (parsed.trigger.kind === "after") { - if (src.kind === "pseudo") { - errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have after-trigger"}); - } - else { - src.timers.push(parsed.trigger.durationMs); - src.timers.sort(); - } + } + else if (parsed.trigger.kind === "after") { + if (src.kind === "pseudo") { + errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have after-trigger"}); } - else if (["entry", "exit"].includes(parsed.trigger.kind)) { - errors.push({shapeUid: text.uid, message: "entry/exit trigger not allowed on transitions"}); + else { + src.timers.push(parsed.trigger.durationMs); + src.timers.sort(); } - else if (parsed.trigger.kind === "triggerless") { - if (src.kind !== "pseudo") { - errors.push({shapeUid: text.uid, message: "triggerless transitions only allowed on pseudo-states"}); - } + } + else if (["entry", "exit"].includes(parsed.trigger.kind)) { + errors.push({shapeUid: text.uid, message: "entry/exit trigger not allowed on transitions"}); + } + else if (parsed.trigger.kind === "triggerless") { + if (src.kind !== "pseudo") { + errors.push({shapeUid: text.uid, message: "triggerless transitions only allowed on pseudo-states"}); } + } - // // raise-actions - // for (const action of parsed.actions) { - // if (action.kind === "raise") { - // const {event} = action; - // if (event.startsWith("_")) { - // internalEvents.add(event); - // } - // else { - // outputEvents.add(event); - // } - // } - // } + // // raise-actions + // for (const action of parsed.actions) { + // if (action.kind === "raise") { + // const {event} = action; + // if (event.startsWith("_")) { + // internalEvents.add(event); + // } + // else { + // outputEvents.add(event); + // } + // } + // } - // collect variables - variables = variables.union(findVariables(parsed.guard)); - for (const action of parsed.actions) { - variables = variables.union(findVariablesAction(action)); - } + // collect variables + variables = variables.union(findVariables(parsed.guard)); + for (const action of parsed.actions) { + variables = variables.union(findVariablesAction(action)); } } } else { // text does not belong to transition... // so it belongs to a rountangle (a state) - const rountangle = findRountangle(text.topLeft, state.rountangles); - const belongsToState = rountangle ? uid2State.get(rountangle.uid)! as ConcreteState : root; + const rountangleUID = conns.text2RountangleMap.get(text.uid); + const belongsToState = uid2State.get(rountangleUID!) as ConcreteState || root; if (parsed.kind === "transitionLabel") { // labels belonging to a rountangle (= a state) must by entry/exit actions // if we cannot find a containing state, then it belong to the root