Compare commits
2 commits
6dc7a2e9a7
...
c07489080a
| Author | SHA1 | Date | |
|---|---|---|---|
| c07489080a | |||
| b55cba198e |
11 changed files with 519 additions and 370 deletions
|
|
@ -52,6 +52,7 @@ export function App() {
|
||||||
function appendNewConfig(inputEvent: string, simtime: number, config: BigStepOutput) {
|
function appendNewConfig(inputEvent: string, simtime: number, config: BigStepOutput) {
|
||||||
setRT([...rt.slice(0, rtIdx!+1), {inputEvent, simtime, ...config}]);
|
setRT([...rt.slice(0, rtIdx!+1), {inputEvent, simtime, ...config}]);
|
||||||
setRTIdx(rtIdx!+1);
|
setRTIdx(rtIdx!+1);
|
||||||
|
console.log('new config:', config);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,12 @@ export function ShowAST(props: {root: ConcreteState | PseudoState, transitions:
|
||||||
return <details open={true} className={props.rt?.mode.has(props.root.uid) ? "active" : ""}>
|
return <details open={true} className={props.rt?.mode.has(props.root.uid) ? "active" : ""}>
|
||||||
<summary>{props.root.kind}: {description}</summary>
|
<summary>{props.root.kind}: {description}</summary>
|
||||||
|
|
||||||
{props.root.entryActions.length>0 &&
|
{props.root.kind !== "pseudo" && props.root.entryActions.length>0 &&
|
||||||
props.root.entryActions.map(action =>
|
props.root.entryActions.map(action =>
|
||||||
<div> entry / <ShowAction action={action}/></div>
|
<div> entry / <ShowAction action={action}/></div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{props.root.exitActions.length>0 &&
|
{props.root.kind !== "pseudo" && props.root.exitActions.length>0 &&
|
||||||
props.root.exitActions.map(action =>
|
props.root.exitActions.map(action =>
|
||||||
<div> exit / <ShowAction action={action}/></div>
|
<div> exit / <ShowAction action={action}/></div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { Vec2D } from "./geometry";
|
import { Vec2D } from "./geometry";
|
||||||
import { HISTORY_RADIUS } from "./parameters";
|
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*";
|
const text = props.kind === "shallow" ? "H" : "H*";
|
||||||
return <>
|
return <>
|
||||||
<circle
|
<circle
|
||||||
className={props.selected ? "selected":""}
|
|
||||||
cx={props.topLeft.x+HISTORY_RADIUS}
|
cx={props.topLeft.x+HISTORY_RADIUS}
|
||||||
cy={props.topLeft.y+HISTORY_RADIUS}
|
cy={props.topLeft.y+HISTORY_RADIUS}
|
||||||
r={HISTORY_RADIUS}
|
r={HISTORY_RADIUS}
|
||||||
|
|
@ -17,7 +16,7 @@ export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
x={props.topLeft.x+HISTORY_RADIUS}
|
x={props.topLeft.x+HISTORY_RADIUS}
|
||||||
y={props.topLeft.y+HISTORY_RADIUS+4}
|
y={props.topLeft.y+HISTORY_RADIUS+5}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fontWeight={500}
|
fontWeight={500}
|
||||||
>{text}</text>
|
>{text}</text>
|
||||||
|
|
@ -29,5 +28,14 @@ export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|
|
||||||
data-uid={props.uid}
|
data-uid={props.uid}
|
||||||
data-parts="history"
|
data-parts="history"
|
||||||
/>
|
/>
|
||||||
|
{(props.selected || props.highlight) &&
|
||||||
|
<circle
|
||||||
|
className={props.selected ? "selected" : props.highlight ? "highlight" : ""}
|
||||||
|
cx={props.topLeft.x+HISTORY_RADIUS}
|
||||||
|
cy={props.topLeft.y+HISTORY_RADIUS}
|
||||||
|
r={HISTORY_RADIUS}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="history"
|
||||||
|
/>}
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +44,6 @@
|
||||||
.rountangle.active {
|
.rountangle.active {
|
||||||
fill: darkorange;
|
fill: darkorange;
|
||||||
fill-opacity: 0.2;
|
fill-opacity: 0.2;
|
||||||
/* filter: drop-shadow( 3px 3px 2px rgba(0, 0, 0, .7)); */
|
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,22 +116,21 @@ line.selected, circle.selected {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.draggableText:hover:not(:active) {
|
.draggableText:hover:not(:active) {
|
||||||
fill: blue;
|
/* fill: blue; */
|
||||||
cursor: grab;
|
/* cursor: grab; */
|
||||||
}
|
}
|
||||||
text.helper {
|
text.helper {
|
||||||
fill: rgba(0,0,0,0);
|
fill: rgba(0,0,0,0);
|
||||||
stroke: rgba(0,0,0,0);
|
stroke: rgba(0,0,0,0);
|
||||||
stroke-width: 8px;
|
stroke-width: 16px;
|
||||||
}
|
}
|
||||||
text.helper:hover {
|
text.helper:hover {
|
||||||
stroke: blue;
|
stroke: blue;
|
||||||
stroke-opacity: 0.2;
|
stroke-opacity: 0.2;
|
||||||
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableText, .draggableText.highlight {
|
.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;
|
paint-order: stroke;
|
||||||
stroke: white;
|
stroke: white;
|
||||||
stroke-width: 4px;
|
stroke-width: 4px;
|
||||||
|
|
@ -140,7 +138,6 @@ text.helper:hover {
|
||||||
stroke-linejoin: miter;
|
stroke-linejoin: miter;
|
||||||
stroke-opacity: 1;
|
stroke-opacity: 1;
|
||||||
fill-opacity:1;
|
fill-opacity:1;
|
||||||
/* font-weight: 800; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableText.highlight:not(.selected) {
|
.draggableText.highlight:not(.selected) {
|
||||||
|
|
@ -148,9 +145,10 @@ text.helper:hover {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight:not(.selected) {
|
.highlight:not(.selected):not(text) {
|
||||||
stroke: green;
|
stroke: green;
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
|
fill: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.error {
|
.arrow.error {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
import * as lz4 from "@nick/lz4";
|
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 { 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 { parseStatechart, TraceableError } from "../statecharts/parser";
|
||||||
import { BigStep } from "../statecharts/runtime_types";
|
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 { 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 { MIN_ROUNTANGLE_SIZE } from "./parameters";
|
||||||
import { getBBoxInSvgCoords } from "./svg_helper";
|
import { getBBoxInSvgCoords } from "./svg_helper";
|
||||||
|
|
||||||
import "./VisualEditor.css";
|
|
||||||
import { ArrowSVG } from "./ArrowSVG";
|
import { ArrowSVG } from "./ArrowSVG";
|
||||||
import { RountangleSVG } from "./RountangleSVG";
|
import { RountangleSVG } from "./RountangleSVG";
|
||||||
import { TextSVG } from "./TextSVG";
|
import { TextSVG } from "./TextSVG";
|
||||||
import { DiamondSVG } from "./DiamondSVG";
|
import { DiamondSVG } from "./DiamondSVG";
|
||||||
import { HistorySVG } from "./HistorySVG";
|
import { HistorySVG } from "./HistorySVG";
|
||||||
|
import { detectConnections } from "../statecharts/detect_connections";
|
||||||
|
|
||||||
|
import "./VisualEditor.css";
|
||||||
|
|
||||||
|
|
||||||
type DraggingState = {
|
type DraggingState = {
|
||||||
|
|
@ -142,29 +143,23 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
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 timeout = setTimeout(() => {
|
||||||
const stateBuffer = new TextEncoder().encode(JSON.stringify(state));
|
const stateBuffer = new TextEncoder().encode(JSON.stringify(state));
|
||||||
const compressedStateBuffer = lz4.compress(stateBuffer);
|
const compressedStateBuffer = lz4.compress(stateBuffer);
|
||||||
const compressedStateString = compressedStateBuffer.toBase64();
|
const compressedStateString = compressedStateBuffer.toBase64();
|
||||||
window.location.hash = "#"+compressedStateString;
|
window.location.hash = "#"+compressedStateString;
|
||||||
|
|
||||||
// const [statechart, errors] = parseStatechart(state);
|
|
||||||
// setErrors(errors);
|
|
||||||
// setAST(statechart);
|
|
||||||
}, 200);
|
}, 200);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
|
const conns = useMemo(() => detectConnections(state), [state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const [statechart, errors] = parseStatechart(state);
|
const [statechart, errors] = parseStatechart(state, conns);
|
||||||
setErrors(errors);
|
setErrors(errors);
|
||||||
setAST(statechart);
|
setAST(statechart);
|
||||||
}, [state])
|
}, [state])
|
||||||
|
|
||||||
|
|
||||||
function getCurrentPointer(e: {pageX: number, pageY: number}) {
|
function getCurrentPointer(e: {pageX: number, pageY: number}) {
|
||||||
const bbox = refSVG.current!.getBoundingClientRect();
|
const bbox = refSVG.current!.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
|
|
@ -514,90 +509,50 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
};
|
};
|
||||||
}, [selectingState, dragging]);
|
}, [selectingState, dragging]);
|
||||||
|
|
||||||
// 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[]>();
|
|
||||||
const text2RountangleMap = new Map<string, string>();
|
|
||||||
const rountangle2TextMap = new Map<string, string[]>();
|
|
||||||
|
|
||||||
// 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.
|
// 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 sidesToHighlight: {[key: string]: RountanglePart[]} = {};
|
||||||
const arrowsToHighlight: {[key: string]: boolean} = {};
|
const arrowsToHighlight: {[key: string]: boolean} = {};
|
||||||
const textsToHighlight: {[key: string]: boolean} = {};
|
const textsToHighlight: {[key: string]: boolean} = {};
|
||||||
const rountanglesToHighlight: {[key: string]: boolean} = {};
|
const rountanglesToHighlight: {[key: string]: boolean} = {};
|
||||||
|
const historyToHighlight: {[key: string]: boolean} = {};
|
||||||
for (const selected of selection) {
|
for (const selected of selection) {
|
||||||
const sides = arrow2SideMap.get(selected.uid);
|
const sides = conns.arrow2SideMap.get(selected.uid);
|
||||||
if (sides) {
|
if (sides) {
|
||||||
const [startSide, endSide] = sides;
|
const [startSide, endSide] = sides;
|
||||||
if (startSide) sidesToHighlight[startSide.uid] = [...sidesToHighlight[startSide.uid]||[], startSide.part];
|
if (startSide) sidesToHighlight[startSide.uid] = [...sidesToHighlight[startSide.uid]||[], startSide.part];
|
||||||
if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part];
|
if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part];
|
||||||
}
|
}
|
||||||
const texts = [
|
const texts = [
|
||||||
...(arrow2TextMap.get(selected.uid) || []),
|
...(conns.arrow2TextMap.get(selected.uid) || []),
|
||||||
...(rountangle2TextMap.get(selected.uid) || []),
|
...(conns.rountangle2TextMap.get(selected.uid) || []),
|
||||||
];
|
];
|
||||||
for (const textUid of texts) {
|
for (const textUid of texts) {
|
||||||
textsToHighlight[textUid] = true;
|
textsToHighlight[textUid] = true;
|
||||||
}
|
}
|
||||||
for (const part of selected.parts) {
|
for (const part of selected.parts) {
|
||||||
const arrows = side2ArrowMap.get(selected.uid + '/' + part) || [];
|
const arrows = conns.side2ArrowMap.get(selected.uid + '/' + part) || [];
|
||||||
if (arrows) {
|
if (arrows) {
|
||||||
for (const [arrowPart, arrowUid] of arrows) {
|
for (const [arrowPart, arrowUid] of arrows) {
|
||||||
arrowsToHighlight[arrowUid] = true;
|
arrowsToHighlight[arrowUid] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const arrow2 = text2ArrowMap.get(selected.uid);
|
const arrow2 = conns.text2ArrowMap.get(selected.uid);
|
||||||
if (arrow2) {
|
if (arrow2) {
|
||||||
arrowsToHighlight[arrow2] = true;
|
arrowsToHighlight[arrow2] = true;
|
||||||
}
|
}
|
||||||
const rountangleUid = text2RountangleMap.get(selected.uid)
|
const rountangleUid = conns.text2RountangleMap.get(selected.uid)
|
||||||
if (rountangleUid) {
|
if (rountangleUid) {
|
||||||
rountanglesToHighlight[rountangleUid] = true;
|
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) {
|
function onPaste(e: ClipboardEvent) {
|
||||||
|
|
@ -619,6 +574,11 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
uid: (nextID++).toString(),
|
uid: (nextID++).toString(),
|
||||||
topLeft: addV2D(r.topLeft, offset),
|
topLeft: addV2D(r.topLeft, offset),
|
||||||
} as Rountangle));
|
} 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) => ({
|
const copiedArrows: Arrow[] = parsed.arrows.map((a: Arrow) => ({
|
||||||
...a,
|
...a,
|
||||||
uid: (nextID++).toString(),
|
uid: (nextID++).toString(),
|
||||||
|
|
@ -630,18 +590,27 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
uid: (nextID++).toString(),
|
uid: (nextID++).toString(),
|
||||||
topLeft: addV2D(t.topLeft, offset),
|
topLeft: addV2D(t.topLeft, offset),
|
||||||
} as Text));
|
} as Text));
|
||||||
|
const copiedHistories: History[] = parsed.history.map((h: History) => ({
|
||||||
|
...h,
|
||||||
|
uid: (nextID++).toString(),
|
||||||
|
topLeft: addV2D(h.topLeft, offset),
|
||||||
|
}))
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: [...state.rountangles, ...copiedRountangles],
|
rountangles: [...state.rountangles, ...copiedRountangles],
|
||||||
|
diamonds: [...state.diamonds, ...copiedDiamonds],
|
||||||
arrows: [...state.arrows, ...copiedArrows],
|
arrows: [...state.arrows, ...copiedArrows],
|
||||||
texts: [...state.texts, ...copiedTexts],
|
texts: [...state.texts, ...copiedTexts],
|
||||||
|
history: [...state.history, ...copiedHistories],
|
||||||
nextID: nextID,
|
nextID: nextID,
|
||||||
}));
|
}));
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const newSelection: Selection = [
|
const newSelection: Selection = [
|
||||||
...copiedRountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
|
...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"]})),
|
...copiedArrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
||||||
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
|
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
|
||||||
|
...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})),
|
||||||
];
|
];
|
||||||
setSelection(newSelection);
|
setSelection(newSelection);
|
||||||
// copyInternal(newSelection, e); // doesn't work
|
// 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) {
|
function copyInternal(selection: Selection, e: ClipboardEvent) {
|
||||||
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
||||||
const rountanglesToCopy = state.rountangles.filter(r => uidsToCopy.has(r.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 arrowsToCopy = state.arrows.filter(a => uidsToCopy.has(a.uid));
|
||||||
const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid));
|
const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid));
|
||||||
e.clipboardData?.setData("text/plain", JSON.stringify({
|
e.clipboardData?.setData("text/plain", JSON.stringify({
|
||||||
rountangles: rountanglesToCopy,
|
rountangles: rountanglesToCopy,
|
||||||
|
diamonds: diamondsToCopy,
|
||||||
|
history: historiesToCopy,
|
||||||
arrows: arrowsToCopy,
|
arrows: arrowsToCopy,
|
||||||
texts: textsToCopy,
|
texts: textsToCopy,
|
||||||
}));
|
}));
|
||||||
|
|
@ -739,7 +712,8 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
|
|
||||||
{(rootErrors.length>0) && <text className="error" x={5} y={20}>{rootErrors.join(' ')}</text>}
|
{(rootErrors.length>0) && <text className="error" x={5} y={20}>{rootErrors.join(' ')}</text>}
|
||||||
|
|
||||||
{state.rountangles.map(rountangle => <RountangleSVG
|
{state.rountangles.map(rountangle =>
|
||||||
|
<RountangleSVG
|
||||||
key={rountangle.uid}
|
key={rountangle.uid}
|
||||||
rountangle={rountangle}
|
rountangle={rountangle}
|
||||||
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
|
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
|
||||||
|
|
@ -763,11 +737,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
{state.history.map(history => <>
|
{state.history.map(history => <>
|
||||||
<HistorySVG {...history} selected={selection.find(h => h.uid === history.uid)} />
|
<HistorySVG {...history}
|
||||||
|
selected={Boolean(selection.find(h => h.uid === history.uid))}
|
||||||
|
highlight={Boolean(historyToHighlight[history.uid])}
|
||||||
|
/>
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
{state.arrows.map(arrow => {
|
{state.arrows.map(arrow => {
|
||||||
const sides = arrow2SideMap.get(arrow.uid);
|
const sides = conns.arrow2SideMap.get(arrow.uid);
|
||||||
let arc = "no" as ArcDirection;
|
let arc = "no" as ArcDirection;
|
||||||
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0]!.uid !== undefined) {
|
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0]!.uid !== undefined) {
|
||||||
arc = arcDirection(sides[0]!.part, sides[1]!.part);
|
arc = arcDirection(sides[0]!.part, sides[1]!.part);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export type AbstractState = {
|
||||||
export type StableState = {
|
export type StableState = {
|
||||||
kind: "and" | "or";
|
kind: "and" | "or";
|
||||||
children: ConcreteState[];
|
children: ConcreteState[];
|
||||||
|
history: HistoryState[];
|
||||||
timers: number[]; // list of timeouts (e.g., the state having an outgoing transition with trigger "after 4s" would appear as the number 4000 in this list)
|
timers: number[]; // list of timeouts (e.g., the state having an outgoing transition with trigger "after 4s" would appear as the number 4000 in this list)
|
||||||
} & AbstractState;
|
} & AbstractState;
|
||||||
|
|
||||||
|
|
@ -32,12 +33,19 @@ export type PseudoState = {
|
||||||
comments: [string, string][];
|
comments: [string, string][];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HistoryState = {
|
||||||
|
kind: "shallow" | "deep";
|
||||||
|
parent: ConcreteState;
|
||||||
|
uid: string;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type ConcreteState = AndState | OrState;
|
export type ConcreteState = AndState | OrState;
|
||||||
|
|
||||||
export type Transition = {
|
export type Transition = {
|
||||||
uid: string; // uid of arrow in concrete syntax
|
uid: string; // uid of arrow in concrete syntax
|
||||||
src: ConcreteState | PseudoState;
|
src: ConcreteState | PseudoState;
|
||||||
tgt: ConcreteState | PseudoState;
|
tgt: ConcreteState | PseudoState | HistoryState;
|
||||||
label: ParsedText[];
|
label: ParsedText[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +60,8 @@ export type Statechart = {
|
||||||
outputEvents: Set<string>;
|
outputEvents: Set<string>;
|
||||||
|
|
||||||
uid2State: Map<string, ConcreteState|PseudoState>;
|
uid2State: Map<string, ConcreteState|PseudoState>;
|
||||||
|
|
||||||
|
historyStates: HistoryState[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyRoot: OrState = {
|
const emptyRoot: OrState = {
|
||||||
|
|
@ -60,6 +70,7 @@ const emptyRoot: OrState = {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
initial: [],
|
initial: [],
|
||||||
children:[],
|
children:[],
|
||||||
|
history: [],
|
||||||
comments: [],
|
comments: [],
|
||||||
entryActions: [],
|
entryActions: [],
|
||||||
exitActions: [],
|
exitActions: [],
|
||||||
|
|
@ -74,6 +85,7 @@ export const emptyStatechart: Statechart = {
|
||||||
internalEvents: [],
|
internalEvents: [],
|
||||||
outputEvents: new Set(),
|
outputEvents: new Set(),
|
||||||
uid2State: new Map([["root", emptyRoot]]),
|
uid2State: new Map([["root", emptyRoot]]),
|
||||||
|
historyStates: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// reflexive, transitive relation
|
// reflexive, transitive relation
|
||||||
|
|
@ -98,7 +110,7 @@ export function isOverlapping(a: ConcreteState, b: ConcreteState): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function computeLCA(a: ConcreteState, b: ConcreteState): ConcreteState {
|
export function computeLCA(a: (ConcreteState|HistoryState), b: (ConcreteState|HistoryState)): (ConcreteState|HistoryState) {
|
||||||
if (a === b) {
|
if (a === b) {
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +120,7 @@ export function computeLCA(a: ConcreteState, b: ConcreteState): ConcreteState {
|
||||||
return computeLCA(a, b.parent!);
|
return computeLCA(a, b.parent!);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeLCA2(states: ConcreteState[]): ConcreteState {
|
export function computeLCA2(states: (ConcreteState|HistoryState)[]): (ConcreteState|HistoryState) {
|
||||||
if (states.length === 0) {
|
if (states.length === 0) {
|
||||||
throw new Error("cannot compute LCA of empty set of states");
|
throw new Error("cannot compute LCA of empty set of states");
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +131,7 @@ export function computeLCA2(states: ConcreteState[]): ConcreteState {
|
||||||
return states.reduce((acc, cur) => computeLCA(acc, cur));
|
return states.reduce((acc, cur) => computeLCA(acc, cur));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPossibleTargets(t: Transition, ts: Map<string, Transition[]>): ConcreteState[] {
|
export function getPossibleTargets(t: Transition, ts: Map<string, Transition[]>): (ConcreteState|HistoryState)[] {
|
||||||
if (t.tgt.kind !== "pseudo") {
|
if (t.tgt.kind !== "pseudo") {
|
||||||
return [t.tgt];
|
return [t.tgt];
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +153,7 @@ export function computeArena2(t: Transition, ts: Map<string, Transition[]>): OrS
|
||||||
// root > A > B > C > D
|
// root > A > B > C > D
|
||||||
// computePath({ancestor: A, descendant: A}) = []
|
// computePath({ancestor: A, descendant: A}) = []
|
||||||
// computePath({ancestor: A, descendant: C}) = [B, C]
|
// computePath({ancestor: A, descendant: C}) = [B, C]
|
||||||
export function computePath({ancestor, descendant}: {ancestor: ConcreteState, descendant: ConcreteState}): ConcreteState[] {
|
export function computePath({ancestor, descendant}: {ancestor: ConcreteState, descendant: (ConcreteState|HistoryState)}): (ConcreteState|HistoryState)[] {
|
||||||
if (ancestor === descendant) {
|
if (ancestor === descendant) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +199,21 @@ export function getDescendants(state: ConcreteState): Set<string> {
|
||||||
// the 'description' of a state is a human-readable string that (hopefully) identifies the state.
|
// the 'description' of a state is a human-readable string that (hopefully) identifies the state.
|
||||||
// if the state contains a comment, we take the 'first' (= visually topmost) comment
|
// if the state contains a comment, we take the 'first' (= visually topmost) comment
|
||||||
// otherwise we fall back to the state's UID.
|
// otherwise we fall back to the state's UID.
|
||||||
export function stateDescription(state: ConcreteState | PseudoState) {
|
export function stateDescription(state: ConcreteState | PseudoState | HistoryState): string {
|
||||||
|
if (state.kind === "shallow") {
|
||||||
|
return `shallow(${stateDescription(state.parent)})`;
|
||||||
|
}
|
||||||
|
else if (state.kind === "deep") {
|
||||||
|
return `deep(${stateDescription(state.parent)})`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// @ts-ignore
|
||||||
const description = state.comments.length > 0 ? state.comments[0][1] : state.uid;
|
const description = state.comments.length > 0 ? state.comments[0][1] : state.uid;
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transitionDescription(t: Transition) {
|
||||||
|
return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox } from "../VisualEditor/geometry";
|
import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox, subtractV2D } from "../VisualEditor/geometry";
|
||||||
import { ARROW_SNAP_THRESHOLD, TEXT_SNAP_THRESHOLD } from "../VisualEditor/parameters";
|
import { ARROW_SNAP_THRESHOLD, HISTORY_RADIUS, TEXT_SNAP_THRESHOLD } from "../VisualEditor/parameters";
|
||||||
import { sides } from "../VisualEditor/VisualEditor";
|
import { sides } from "../VisualEditor/VisualEditor";
|
||||||
|
|
||||||
export type Rountangle = {
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
84
src/statecharts/detect_connections.ts
Normal file
84
src/statecharts/detect_connections.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RountanglePart, VisualEditorState } from "./concrete_syntax";
|
||||||
|
|
||||||
|
export type Connections = {
|
||||||
|
arrow2SideMap: Map<string,[{ uid: string; part: RountanglePart; } | undefined, { uid: string; part: RountanglePart; } | undefined]>,
|
||||||
|
side2ArrowMap: Map<string, Set<["start"|"end", string]>>,
|
||||||
|
text2ArrowMap: Map<string,string>,
|
||||||
|
arrow2TextMap: Map<string,string[]>,
|
||||||
|
arrow2HistoryMap: Map<string,string>,
|
||||||
|
text2RountangleMap: Map<string, string>,
|
||||||
|
rountangle2TextMap: Map<string, string[]>,
|
||||||
|
history2ArrowMap: Map<string, string[]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function detectConnections(state: VisualEditorState): Connections {
|
||||||
|
// 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[]>();
|
||||||
|
const arrow2HistoryMap = new Map<string,string>();
|
||||||
|
const text2RountangleMap = new Map<string, string>();
|
||||||
|
const rountangle2TextMap = new Map<string, string[]>();
|
||||||
|
const history2ArrowMap = new Map<string, string[]>();
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,170 +1,26 @@
|
||||||
import { computeArena2, computePath, ConcreteState, getDescendants, isOverlapping, OrState, StableState, Statechart, stateDescription, Transition } from "./abstract_syntax";
|
import { computeArena2, computePath, ConcreteState, getDescendants, HistoryState, isOverlapping, OrState, StableState, Statechart, stateDescription, Transition, transitionDescription } from "./abstract_syntax";
|
||||||
import { evalExpr } from "./actionlang_interpreter";
|
import { evalExpr } from "./actionlang_interpreter";
|
||||||
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
|
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
|
||||||
import { BigStepOutput, Environment, initialRaised, Mode, RaisedEvents, RT_Event, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types";
|
import { BigStepOutput, Environment, initialRaised, Mode, RaisedEvents, RT_Event, RT_History, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types";
|
||||||
|
|
||||||
export function initialize(ast: Statechart): BigStepOutput {
|
export function initialize(ast: Statechart): BigStepOutput {
|
||||||
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
|
let history = new Map();
|
||||||
|
let enteredStates, environment, raised;
|
||||||
|
({enteredStates, environment, history, ...raised} = enterDefault(0, ast.root, {
|
||||||
environment: new Environment([new Map([["_timers", []]])]),
|
environment: new Environment([new Map([["_timers", []]])]),
|
||||||
|
history,
|
||||||
...initialRaised,
|
...initialRaised,
|
||||||
});
|
}));
|
||||||
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
|
return handleInternalEvents(0, ast, {mode: enteredStates, environment, history, ...raised});
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionScope = {
|
type ActionScope = {
|
||||||
environment: Environment,
|
environment: Environment,
|
||||||
|
history: RT_History,
|
||||||
} & RaisedEvents;
|
} & RaisedEvents;
|
||||||
|
|
||||||
type EnteredScope = { enteredStates: Mode } & ActionScope;
|
type EnteredScope = { enteredStates: Mode } & ActionScope;
|
||||||
|
|
||||||
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
|
|
||||||
// console.log('enter', stateDescription(state), '...');
|
|
||||||
let {environment, ...rest} = actionScope;
|
|
||||||
// environment = environment.pushScope();
|
|
||||||
for (const action of state.entryActions) {
|
|
||||||
({environment, ...rest} = execAction(action, {environment, ...rest}));
|
|
||||||
}
|
|
||||||
// schedule timers
|
|
||||||
// we store timers in the environment (dirty!)
|
|
||||||
environment = environment.transform<Timers>("_timers", oldTimers => {
|
|
||||||
const newTimers = [
|
|
||||||
...oldTimers,
|
|
||||||
...state.timers.map(timeOffset => {
|
|
||||||
const futureSimTime = simtime + timeOffset;
|
|
||||||
return [futureSimTime, {kind: "timer", state: state.uid, timeDurMs: timeOffset}] as [number, TimerElapseEvent];
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
newTimers.sort((a,b) => a[0] - b[0]);
|
|
||||||
return newTimers;
|
|
||||||
}, []);
|
|
||||||
// new nested scope
|
|
||||||
return {environment, ...rest};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function exitActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
|
|
||||||
// console.log('exit', stateDescription(state), '...');
|
|
||||||
for (const action of state.exitActions) {
|
|
||||||
(actionScope = execAction(action, actionScope));
|
|
||||||
}
|
|
||||||
let environment = actionScope.environment;
|
|
||||||
// cancel timers
|
|
||||||
environment = environment.transform<Timers>("_timers", oldTimers => {
|
|
||||||
// remove all timers of 'state':
|
|
||||||
return oldTimers.filter(([_, {state: s}]) => s !== state.uid);
|
|
||||||
}, []);
|
|
||||||
// environment = environment.popScope();
|
|
||||||
return {...actionScope, environment};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enterDefault(simtime: number, state: ConcreteState, rt: ActionScope): EnteredScope {
|
|
||||||
let actionScope = rt;
|
|
||||||
|
|
||||||
// execute entry actions
|
|
||||||
actionScope = entryActions(simtime, state, actionScope);
|
|
||||||
|
|
||||||
// enter children...
|
|
||||||
let enteredStates = new Set([state.uid]);
|
|
||||||
if (state.kind === "and") {
|
|
||||||
// enter every child
|
|
||||||
for (const child of state.children) {
|
|
||||||
let enteredChildren;
|
|
||||||
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
|
|
||||||
enteredStates = enteredStates.union(enteredChildren);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (state.kind === "or") {
|
|
||||||
// same as AND-state, but we only enter the initial state(s)
|
|
||||||
if (state.initial.length > 0) {
|
|
||||||
if (state.initial.length > 1) {
|
|
||||||
console.warn(state.uid + ': multiple initial states, only entering one of them');
|
|
||||||
}
|
|
||||||
let enteredChildren;
|
|
||||||
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, state.initial[0][1], actionScope));
|
|
||||||
enteredStates = enteredStates.union(enteredChildren);
|
|
||||||
}
|
|
||||||
console.warn(state.uid + ': no initial state');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {enteredStates, ...actionScope};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enterPath(simtime: number, path: ConcreteState[], rt: ActionScope): EnteredScope {
|
|
||||||
let actionScope = rt;
|
|
||||||
|
|
||||||
const [state, ...rest] = path;
|
|
||||||
|
|
||||||
// execute entry actions
|
|
||||||
actionScope = entryActions(simtime, state, actionScope);
|
|
||||||
|
|
||||||
// enter children...
|
|
||||||
let enteredStates = new Set([state.uid]);
|
|
||||||
if (state.kind === "and") {
|
|
||||||
// enter every child
|
|
||||||
for (const child of state.children) {
|
|
||||||
let enteredChildren;
|
|
||||||
if (rest.length > 0 && child.uid === rest[0].uid) {
|
|
||||||
({enteredStates: enteredChildren, ...actionScope} = enterPath(simtime, rest, actionScope));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
|
|
||||||
}
|
|
||||||
enteredStates = enteredStates.union(enteredChildren);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (state.kind === "or") {
|
|
||||||
if (rest.length > 0) {
|
|
||||||
let enteredChildren;
|
|
||||||
({enteredStates: enteredChildren, ...actionScope} = enterPath(simtime, rest, actionScope));
|
|
||||||
enteredStates = enteredStates.union(enteredChildren);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// same as AND-state, but we only enter the initial state(s)
|
|
||||||
for (const [_, child] of state.initial) {
|
|
||||||
let enteredChildren;
|
|
||||||
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
|
|
||||||
enteredStates = enteredStates.union(enteredChildren);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { enteredStates, ...actionScope };
|
|
||||||
}
|
|
||||||
|
|
||||||
// exit the given state and all its active descendants
|
|
||||||
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
|
|
||||||
let {enteredStates, ...actionScope} = rt;
|
|
||||||
|
|
||||||
if (enteredStates.has(state.uid)) {
|
|
||||||
// exit all active children...
|
|
||||||
for (const child of state.children) {
|
|
||||||
actionScope = exitCurrent(simtime, child, {enteredStates, ...actionScope});
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute exit actions
|
|
||||||
actionScope = exitActions(simtime, state, actionScope);
|
|
||||||
}
|
|
||||||
|
|
||||||
return actionScope;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function exitPath(simtime: number, path: ConcreteState[], rt: EnteredScope): ActionScope {
|
|
||||||
let {enteredStates, ...actionScope} = rt;
|
|
||||||
|
|
||||||
const toExit = enteredStates.difference(new Set(path.map(s=>s.uid)));
|
|
||||||
|
|
||||||
const [state, ...rest] = path;
|
|
||||||
|
|
||||||
// exit state and all its children, *except* states along the rest of the path
|
|
||||||
actionScope = exitCurrent(simtime, state, {enteredStates: toExit, ...actionScope});
|
|
||||||
if (rest.length > 0) {
|
|
||||||
actionScope = exitPath(simtime, rest, {enteredStates, ...actionScope});
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute exit actions
|
|
||||||
actionScope = exitActions(simtime, state, actionScope);
|
|
||||||
return actionScope;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
if (action.kind === "assignment") {
|
if (action.kind === "assignment") {
|
||||||
const rhs = evalExpr(action.rhs, rt.environment);
|
const rhs = evalExpr(action.rhs, rt.environment);
|
||||||
|
|
@ -197,7 +53,156 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
throw new Error("should never reach here");
|
throw new Error("should never reach here");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
|
||||||
|
// console.log('enter', stateDescription(state), '...');
|
||||||
|
let {environment, ...rest} = actionScope;
|
||||||
|
// environment = environment.pushScope();
|
||||||
|
for (const action of state.entryActions) {
|
||||||
|
({environment, ...rest} = execAction(action, {environment, ...rest}));
|
||||||
|
}
|
||||||
|
// schedule timers
|
||||||
|
// we store timers in the environment (dirty!)
|
||||||
|
environment = environment.transform<Timers>("_timers", oldTimers => {
|
||||||
|
const newTimers = [
|
||||||
|
...oldTimers,
|
||||||
|
...state.timers.map(timeOffset => {
|
||||||
|
const futureSimTime = simtime + timeOffset;
|
||||||
|
return [futureSimTime, {kind: "timer", state: state.uid, timeDurMs: timeOffset}] as [number, TimerElapseEvent];
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
newTimers.sort((a,b) => a[0] - b[0]);
|
||||||
|
return newTimers;
|
||||||
|
}, []);
|
||||||
|
// new nested scope
|
||||||
|
return {environment, ...rest};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitActions(simtime: number, state: ConcreteState, {enteredStates, ...actionScope}: EnteredScope): ActionScope {
|
||||||
|
// console.log('exit', stateDescription(state), '...');
|
||||||
|
for (const action of state.exitActions) {
|
||||||
|
(actionScope = execAction(action, actionScope));
|
||||||
|
}
|
||||||
|
let environment = actionScope.environment;
|
||||||
|
// cancel timers
|
||||||
|
environment = environment.transform<Timers>("_timers", oldTimers => {
|
||||||
|
// remove all timers of 'state':
|
||||||
|
return oldTimers.filter(([_, {state: s}]) => s !== state.uid);
|
||||||
|
}, []);
|
||||||
|
// environment = environment.popScope();
|
||||||
|
return {...actionScope, environment};
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively enter the given state's default state
|
||||||
|
export function enterDefault(simtime: number, state: ConcreteState, rt: ActionScope): EnteredScope {
|
||||||
|
let actionScope = rt;
|
||||||
|
|
||||||
|
// execute entry actions
|
||||||
|
actionScope = entryActions(simtime, state, actionScope);
|
||||||
|
|
||||||
|
// enter children...
|
||||||
|
let enteredStates = new Set([state.uid]);
|
||||||
|
if (state.kind === "and") {
|
||||||
|
// enter every child
|
||||||
|
for (const child of state.children) {
|
||||||
|
let enteredChildren;
|
||||||
|
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, child, actionScope));
|
||||||
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (state.kind === "or") {
|
||||||
|
// same as AND-state, but we only enter the initial state(s)
|
||||||
|
if (state.initial.length > 0) {
|
||||||
|
if (state.initial.length > 1) {
|
||||||
|
console.warn(state.uid + ': multiple initial states, only entering one of them');
|
||||||
|
}
|
||||||
|
let enteredChildren;
|
||||||
|
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, state.initial[0][1], actionScope));
|
||||||
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
|
}
|
||||||
|
console.warn(state.uid + ': no initial state');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {enteredStates, ...actionScope};
|
||||||
|
}
|
||||||
|
|
||||||
|
// recursively enter the given state and, if children need to be entered, preferrably those occurring in 'toEnter' will be entered. If no child occurs in 'toEnter', the default child will be entered.
|
||||||
|
export function enterStates(simtime: number, state: ConcreteState, toEnter: Set<string>, actionScope: ActionScope): EnteredScope {
|
||||||
|
|
||||||
|
// execute entry actions
|
||||||
|
actionScope = entryActions(simtime, state, actionScope);
|
||||||
|
|
||||||
|
// enter children...
|
||||||
|
let enteredStates = new Set([state.uid]);
|
||||||
|
|
||||||
|
if (state.kind === "and") {
|
||||||
|
// every child must be entered
|
||||||
|
for (const child of state.children) {
|
||||||
|
let enteredChildren;
|
||||||
|
({enteredStates: enteredChildren, ...actionScope} = enterStates(simtime, child, toEnter, actionScope));
|
||||||
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (state.kind === "or") {
|
||||||
|
// only one child can be entered
|
||||||
|
const childToEnter = state.children.filter(child => toEnter.has(child.uid));
|
||||||
|
if (childToEnter.length === 1) {
|
||||||
|
// good
|
||||||
|
let enteredChildren;
|
||||||
|
({enteredStates: enteredChildren, ...actionScope} = enterStates(simtime, childToEnter[0], toEnter, actionScope));
|
||||||
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
|
}
|
||||||
|
else if (childToEnter.length === 0) {
|
||||||
|
// also good, enter default child
|
||||||
|
for (const [_, defaultChild] of state.initial) {
|
||||||
|
let enteredChildren;
|
||||||
|
({enteredStates: enteredChildren, ...actionScope} = enterDefault(simtime, defaultChild, actionScope));
|
||||||
|
enteredStates = enteredStates.union(enteredChildren);
|
||||||
|
break; // one is enough
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("can only enter one child of an OR-state, stupid!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { enteredStates, ...actionScope };
|
||||||
|
}
|
||||||
|
|
||||||
|
// exit the given state and all its active descendants
|
||||||
|
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
|
||||||
|
let {enteredStates, history, ...actionScope} = rt;
|
||||||
|
|
||||||
|
if (enteredStates.has(state.uid)) {
|
||||||
|
// exit all active children...
|
||||||
|
for (const child of state.children) {
|
||||||
|
({history, ...actionScope} = exitCurrent(simtime, child, {enteredStates, history, ...actionScope}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute exit actions
|
||||||
|
({history, ...actionScope} = exitActions(simtime, state, {enteredStates, history, ...actionScope}));
|
||||||
|
|
||||||
|
// record history
|
||||||
|
for (const h of state.history) {
|
||||||
|
if (h.kind === "shallow") {
|
||||||
|
history.set(h.uid, new Set(state.children
|
||||||
|
.filter(child => enteredStates.has(child.uid))
|
||||||
|
.map(child => child.uid)));
|
||||||
|
}
|
||||||
|
else if (h.kind === "deep") {
|
||||||
|
// horribly inefficient (i don't care)
|
||||||
|
history.set(h.uid,
|
||||||
|
getDescendants(state)
|
||||||
|
.difference(new Set([state.uid]))
|
||||||
|
.intersection(enteredStates)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {history, ...actionScope};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {environment, mode, history, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||||
const arenasFired = new Set<OrState>();
|
const arenasFired = new Set<OrState>();
|
||||||
for (const state of activeParent.children) {
|
for (const state of activeParent.children) {
|
||||||
if (mode.has(state.uid)) {
|
if (mode.has(state.uid)) {
|
||||||
|
|
@ -249,7 +254,7 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
|
||||||
event.param,
|
event.param,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
({mode, environment, ...raised} = fireTransition2(simtime, t, statechart.transitions, l, arena, {mode, environment, ...raised}));
|
({mode, environment, history, ...raised} = fireTransition(simtime, t, statechart.transitions, l, arena, {mode, environment, history, ...raised}));
|
||||||
if (event.kind === "input" && event.param !== undefined) {
|
if (event.kind === "input" && event.param !== undefined) {
|
||||||
environment = environment.popScope();
|
environment = environment.popScope();
|
||||||
}
|
}
|
||||||
|
|
@ -261,57 +266,53 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// no enabled outgoing transitions, try the children:
|
// no enabled outgoing transitions, try the children:
|
||||||
({environment, mode, ...raised} = handleEvent(simtime, event, statechart, state, {environment, mode, ...raised}));
|
({environment, mode, history, ...raised} = handleEvent(simtime, event, statechart, state, {environment, mode, history, ...raised}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {environment, mode, ...raised};
|
return {environment, mode, history, ...raised};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment}: {mode: Mode, environment: Environment}): BigStepOutput {
|
export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStepOutput {
|
||||||
let raised = initialRaised;
|
let raised = initialRaised;
|
||||||
|
|
||||||
({mode, environment, ...raised} = handleEvent(simtime, event, statechart, statechart.root, {mode, environment, ...raised}));
|
({mode, environment, ...raised} = handleEvent(simtime, event, statechart, statechart.root, {mode, environment, history, ...raised}));
|
||||||
|
|
||||||
return handleInternalEvents(simtime, statechart, {mode, environment, ...raised});
|
return handleInternalEvents(simtime, statechart, {mode, environment, history, ...raised});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleInternalEvents(simtime: number, statechart: Statechart, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): BigStepOutput {
|
export function handleInternalEvents(simtime: number, statechart: Statechart, {mode, environment, history, ...raised}: RT_Statechart & RaisedEvents): BigStepOutput {
|
||||||
while (raised.internalEvents.length > 0) {
|
while (raised.internalEvents.length > 0) {
|
||||||
const [internalEvent, ...rest] = raised.internalEvents;
|
const [internalEvent, ...rest] = raised.internalEvents;
|
||||||
({mode, environment, ...raised} = handleEvent(simtime,
|
({mode, environment, ...raised} = handleEvent(simtime,
|
||||||
{kind: "input", ...internalEvent}, // internal event becomes input event
|
{kind: "input", ...internalEvent}, // internal event becomes input event
|
||||||
statechart, statechart.root, {mode, environment, internalEvents: rest, outputEvents: raised.outputEvents}));
|
statechart, statechart.root, {mode, environment, history, internalEvents: rest, outputEvents: raised.outputEvents}));
|
||||||
}
|
}
|
||||||
return {mode, environment, outputEvents: raised.outputEvents};
|
return {mode, environment, history, outputEvents: raised.outputEvents};
|
||||||
}
|
}
|
||||||
|
|
||||||
function transitionDescription(t: Transition) {
|
export function fireTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, history, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||||
return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fireTransition2(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
|
||||||
console.log('fire', transitionDescription(t));
|
console.log('fire', transitionDescription(t));
|
||||||
|
|
||||||
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}).reverse();
|
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}).reverse() as ConcreteState[];
|
||||||
|
|
||||||
// exit src and other states up to arena
|
// exit src and other states up to arena
|
||||||
({environment, ...raised} = exitPath(simtime, srcPath, {environment, enteredStates: mode, ...raised}));
|
({environment, history, ...raised} = exitCurrent(simtime, srcPath[0], {environment, enteredStates: mode, history, ...raised}))
|
||||||
const toExit = getDescendants(arena);
|
const toExit = getDescendants(arena);
|
||||||
toExit.delete(arena.uid); // do not exit the arena itself
|
toExit.delete(arena.uid); // do not exit the arena itself
|
||||||
const exitedMode = mode.difference(toExit); // active states after exiting the states we need to exit
|
const exitedMode = mode.difference(toExit); // active states after exiting the states we need to exit
|
||||||
|
|
||||||
// console.log({exitedMode});
|
// console.log({exitedMode});
|
||||||
|
|
||||||
return fireSecondHalfOfTransition(simtime, t, ts, label, arena, {mode: exitedMode, environment, ...raised});
|
return fireSecondHalfOfTransition(simtime, t, ts, label, arena, {mode: exitedMode, history, environment, ...raised});
|
||||||
}
|
}
|
||||||
|
|
||||||
// assuming we've already exited the source state of the transition, now enter the target state
|
// assuming we've already exited the source state of the transition, now enter the target state
|
||||||
// IF however, the target is a pseudo-state, DON'T enter it (pseudo-states are NOT states), instead fire the first pseudo-outgoing transition.
|
// IF however, the target is a pseudo-state, DON'T enter it (pseudo-states are NOT states), instead fire the first pseudo-outgoing transition.
|
||||||
export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, history, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||||
// exec transition actions
|
// exec transition actions
|
||||||
for (const action of label.actions) {
|
for (const action of label.actions) {
|
||||||
({environment, ...raised} = execAction(action, {environment, ...raised}));
|
({environment, history, ...raised} = execAction(action, {environment, history, ...raised}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t.tgt.kind === "pseudo") {
|
if (t.tgt.kind === "pseudo") {
|
||||||
|
|
@ -322,7 +323,7 @@ export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: M
|
||||||
if (evalExpr(nextLabel.guard, environment)) {
|
if (evalExpr(nextLabel.guard, environment)) {
|
||||||
console.log('fire', transitionDescription(nextT));
|
console.log('fire', transitionDescription(nextT));
|
||||||
// found ourselves an enabled transition
|
// found ourselves an enabled transition
|
||||||
return fireSecondHalfOfTransition(simtime, nextT, ts, nextLabel, arena, {mode, environment, ...raised});
|
return fireSecondHalfOfTransition(simtime, nextT, ts, nextLabel, arena, {mode, environment, history, ...raised});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,13 +332,25 @@ export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: M
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
||||||
|
const state = tgtPath[0] as ConcreteState;
|
||||||
|
let toEnter;
|
||||||
|
if (t.tgt.kind === "deep" || t.tgt.kind === "shallow") {
|
||||||
|
toEnter = new Set([
|
||||||
|
...tgtPath.slice(0,-1).map(s => s.uid),
|
||||||
|
...history.get(t.tgt.uid)!
|
||||||
|
]) as Set<string>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toEnter = new Set(tgtPath.map(s=>s.uid));
|
||||||
|
}
|
||||||
|
|
||||||
// enter tgt
|
// enter tgt
|
||||||
let enteredStates;
|
let enteredStates;
|
||||||
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath, {environment, ...raised}));
|
({enteredStates, environment, history, ...raised} = enterStates(simtime, state, toEnter, {environment, history, ...raised}));
|
||||||
const enteredMode = mode.union(enteredStates);
|
const enteredMode = mode.union(enteredStates);
|
||||||
|
|
||||||
// console.log({enteredMode});
|
// console.log({enteredMode});
|
||||||
|
|
||||||
return {mode: enteredMode, environment, ...raised};
|
return {mode: enteredMode, environment, history, ...raised};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { AbstractState, ConcreteState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax";
|
import { ConcreteState, HistoryState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax";
|
||||||
import { findNearestArrow, findNearestSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax";
|
import { Rountangle, VisualEditorState } from "./concrete_syntax";
|
||||||
import { isEntirelyWithin } from "../VisualEditor/geometry";
|
import { isEntirelyWithin, Rect2D } from "../VisualEditor/geometry";
|
||||||
import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
|
import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
|
||||||
|
|
||||||
import { parse as parseLabel, SyntaxError } from "./label_parser";
|
import { parse as parseLabel, SyntaxError } from "./label_parser";
|
||||||
|
import { Connections } from "./detect_connections";
|
||||||
|
import { HISTORY_RADIUS } from "../VisualEditor/parameters";
|
||||||
|
|
||||||
export type TraceableError = {
|
export type TraceableError = {
|
||||||
shapeUid: string;
|
shapeUid: string;
|
||||||
|
|
@ -29,7 +30,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[] = [];
|
const errors: TraceableError[] = [];
|
||||||
|
|
||||||
// implicitly, the root is always an Or-state
|
// implicitly, the root is always an Or-state
|
||||||
|
|
@ -37,6 +38,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
kind: "or",
|
kind: "or",
|
||||||
uid: "root",
|
uid: "root",
|
||||||
children: [],
|
children: [],
|
||||||
|
history: [],
|
||||||
initial: [],
|
initial: [],
|
||||||
comments: [],
|
comments: [],
|
||||||
entryActions: [],
|
entryActions: [],
|
||||||
|
|
@ -46,6 +48,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid2State = new Map<string, ConcreteState|PseudoState>([["root", root]]);
|
const uid2State = new Map<string, ConcreteState|PseudoState>([["root", root]]);
|
||||||
|
const historyStates: HistoryState[] = [];
|
||||||
|
|
||||||
// we will always look for the smallest parent rountangle
|
// we will always look for the smallest parent rountangle
|
||||||
const parentCandidates: Rountangle[] = [{
|
const parentCandidates: Rountangle[] = [{
|
||||||
|
|
@ -57,24 +60,39 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
|
|
||||||
const parentLinks = new Map<string, string>();
|
const parentLinks = new Map<string, string>();
|
||||||
|
|
||||||
|
function findParent(geom: Rect2D): ConcreteState {
|
||||||
|
// iterate in reverse:
|
||||||
|
for (let i=parentCandidates.length-1; i>=0; i--) {
|
||||||
|
const candidate = parentCandidates[i];
|
||||||
|
if (candidate.uid === "root" || isEntirelyWithin(geom, candidate)) {
|
||||||
|
// found our parent
|
||||||
|
return uid2State.get(candidate.uid)! as ConcreteState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("impossible: should always find a parent state");
|
||||||
|
}
|
||||||
|
|
||||||
// step 1: figure out state hierarchy
|
// step 1: figure out state hierarchy
|
||||||
|
|
||||||
// IMPORTANT ASSUMPTION: state.rountangles is sorted from big to small surface area:
|
// IMPORTANT ASSUMPTION: state.rountangles is sorted from big to small surface area:
|
||||||
for (const rt of state.rountangles) {
|
for (const rt of state.rountangles) {
|
||||||
|
const parent = findParent(rt);
|
||||||
const common = {
|
const common = {
|
||||||
kind: rt.kind,
|
kind: rt.kind,
|
||||||
uid: rt.uid,
|
uid: rt.uid,
|
||||||
comments: [],
|
comments: [],
|
||||||
entryActions: [],
|
entryActions: [],
|
||||||
exitActions: [],
|
exitActions: [],
|
||||||
|
parent,
|
||||||
|
depth: parent.depth + 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let state;
|
let state;
|
||||||
if (rt.kind === "or") {
|
if (rt.kind === "or") {
|
||||||
state = {
|
state = {
|
||||||
...common,
|
...common,
|
||||||
initial: [],
|
initial: [],
|
||||||
children: [],
|
children: [],
|
||||||
|
history: [],
|
||||||
timers: [],
|
timers: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -82,30 +100,15 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
state = {
|
state = {
|
||||||
...common,
|
...common,
|
||||||
children: [],
|
children: [],
|
||||||
|
history: [],
|
||||||
timers: [],
|
timers: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
parent.children.push(state as ConcreteState);
|
||||||
// iterate in reverse:
|
|
||||||
for (let i=parentCandidates.length-1; i>=0; i--) {
|
|
||||||
const candidate = parentCandidates[i];
|
|
||||||
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
|
|
||||||
// found our parent
|
|
||||||
const parentState = uid2State.get(candidate.uid)! as ConcreteState;
|
|
||||||
parentState.children.push(state as unknown as ConcreteState);
|
|
||||||
parentCandidates.push(rt);
|
parentCandidates.push(rt);
|
||||||
parentLinks.set(rt.uid, candidate.uid);
|
parentLinks.set(rt.uid, parent.uid);
|
||||||
state = {
|
|
||||||
...state,
|
|
||||||
parent: parentState,
|
|
||||||
depth: parentState.depth + 1,
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uid2State.set(rt.uid, state as ConcreteState);
|
uid2State.set(rt.uid, state as ConcreteState);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const d of state.diamonds) {
|
for (const d of state.diamonds) {
|
||||||
uid2State.set(d.uid, {
|
uid2State.set(d.uid, {
|
||||||
kind: "pseudo",
|
kind: "pseudo",
|
||||||
|
|
@ -113,6 +116,17 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
comments: [],
|
comments: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for (const h of state.history) {
|
||||||
|
const parent = findParent({topLeft: h.topLeft, size: {x: HISTORY_RADIUS*2, y: HISTORY_RADIUS*2}});
|
||||||
|
const historyState = {
|
||||||
|
kind: h.kind,
|
||||||
|
uid: h.uid,
|
||||||
|
parent,
|
||||||
|
depth: parent.depth+1,
|
||||||
|
};
|
||||||
|
parent.history.push(historyState);
|
||||||
|
historyStates.push(historyState);
|
||||||
|
}
|
||||||
|
|
||||||
// step 2: figure out transitions
|
// step 2: figure out transitions
|
||||||
|
|
||||||
|
|
@ -120,11 +134,14 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
const uid2Transition = new Map<string, Transition>();
|
const uid2Transition = new Map<string, Transition>();
|
||||||
|
|
||||||
for (const arr of state.arrows) {
|
for (const arr of state.arrows) {
|
||||||
const sides = [...state.rountangles, ...state.diamonds];
|
const srcUID = conns.arrow2SideMap.get(arr.uid)?.[0]?.uid;
|
||||||
const srcUID = findNearestSide(arr, "start", sides)?.uid;
|
const tgtUID = conns.arrow2SideMap.get(arr.uid)?.[1]?.uid;
|
||||||
const tgtUID = findNearestSide(arr, "end", sides)?.uid;
|
const historyTgtUID = conns.arrow2HistoryMap.get(arr.uid);
|
||||||
if (!srcUID) {
|
if (!srcUID) {
|
||||||
if (!tgtUID) {
|
if (historyTgtUID) {
|
||||||
|
errors.push({shapeUid: arr.uid, message: "no source"});
|
||||||
|
}
|
||||||
|
else if (!tgtUID) {
|
||||||
// dangling edge
|
// dangling edge
|
||||||
errors.push({shapeUid: arr.uid, message: "dangling"});
|
errors.push({shapeUid: arr.uid, message: "dangling"});
|
||||||
}
|
}
|
||||||
|
|
@ -154,25 +171,33 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!tgtUID) {
|
if (historyTgtUID || tgtUID) {
|
||||||
errors.push({
|
// add transition
|
||||||
shapeUid: arr.uid,
|
let tgt;
|
||||||
message: "no target",
|
if (historyTgtUID) {
|
||||||
});
|
tgt = historyStates.find(h => h.uid === historyTgtUID)!;
|
||||||
|
console.log(tgt);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// add transition
|
tgt = uid2State.get(tgtUID!)!;
|
||||||
|
}
|
||||||
const transition: Transition = {
|
const transition: Transition = {
|
||||||
uid: arr.uid,
|
uid: arr.uid,
|
||||||
src: uid2State.get(srcUID)!,
|
src: uid2State.get(srcUID)!,
|
||||||
tgt: uid2State.get(tgtUID)!,
|
tgt,
|
||||||
label: [],
|
label: [],
|
||||||
};
|
}
|
||||||
const existingTransitions = transitions.get(srcUID) || [];
|
const existingTransitions = transitions.get(srcUID) || [];
|
||||||
existingTransitions.push(transition);
|
existingTransitions.push(transition);
|
||||||
transitions.set(srcUID, existingTransitions);
|
transitions.set(srcUID, existingTransitions);
|
||||||
uid2Transition.set(arr.uid, transition);
|
uid2Transition.set(arr.uid, transition);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
errors.push({
|
||||||
|
shapeUid: arr.uid,
|
||||||
|
message: "no target",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,9 +247,8 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
|
const belongsToArrowUID = conns.text2ArrowMap.get(text.uid);
|
||||||
if (belongsToArrow) {
|
const belongsToTransition = uid2Transition.get(belongsToArrowUID!);
|
||||||
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
|
||||||
if (belongsToTransition) {
|
if (belongsToTransition) {
|
||||||
const {src} = belongsToTransition;
|
const {src} = belongsToTransition;
|
||||||
belongsToTransition.label.push(parsed);
|
belongsToTransition.label.push(parsed);
|
||||||
|
|
@ -283,12 +307,11 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
// text does not belong to transition...
|
// text does not belong to transition...
|
||||||
// so it belongs to a rountangle (a state)
|
// so it belongs to a rountangle (a state)
|
||||||
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
const rountangleUID = conns.text2RountangleMap.get(text.uid);
|
||||||
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! as ConcreteState : root;
|
const belongsToState = uid2State.get(rountangleUID!) as ConcreteState || root;
|
||||||
if (parsed.kind === "transitionLabel") {
|
if (parsed.kind === "transitionLabel") {
|
||||||
// labels belonging to a rountangle (= a state) must by entry/exit actions
|
// 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
|
// if we cannot find a containing state, then it belong to the root
|
||||||
|
|
@ -336,6 +359,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
internalEvents,
|
internalEvents,
|
||||||
outputEvents,
|
outputEvents,
|
||||||
uid2State,
|
uid2State,
|
||||||
|
historyStates,
|
||||||
}, errors];
|
}, errors];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,12 @@ export class Environment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RT_History = Map<string, Set<string>>;
|
||||||
|
|
||||||
export type RT_Statechart = {
|
export type RT_Statechart = {
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
environment: Environment;
|
environment: Environment;
|
||||||
// history: // TODO
|
history: RT_History; // history-uid -> set of states
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BigStepOutput = RT_Statechart & {
|
export type BigStepOutput = RT_Statechart & {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue