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