Compare commits
2 commits
ec49c47b39
...
d4930eb13d
| Author | SHA1 | Date | |
|---|---|---|---|
| d4930eb13d | |||
| db1479bfc4 |
22 changed files with 781 additions and 608 deletions
|
|
@ -1,28 +1,3 @@
|
||||||
/* .layoutVertical {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
.panel {
|
|
||||||
height: 1.5rem;
|
|
||||||
background-color: lightgrey;
|
|
||||||
}
|
|
||||||
.layout {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100vh - 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
flex: 0 0 content;
|
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: auto;
|
|
||||||
} */
|
|
||||||
|
|
||||||
details {
|
details {
|
||||||
padding-left: 20;
|
padding-left: 20;
|
||||||
/* margin-left: 30; */
|
/* margin-left: 30; */
|
||||||
|
|
@ -69,3 +44,9 @@ summary {
|
||||||
.toolbar > input {
|
.toolbar > input {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.active {
|
||||||
|
border: solid blue 2px;
|
||||||
|
background-color: rgba(0,0,255,0.2);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
@ -12,9 +12,10 @@ import "./App.css";
|
||||||
import { Box, Stack } from "@mui/material";
|
import { Box, Stack } from "@mui/material";
|
||||||
import { TopPanel } from "./TopPanel";
|
import { TopPanel } from "./TopPanel";
|
||||||
import { RTHistory } from "./RTHistory";
|
import { RTHistory } from "./RTHistory";
|
||||||
import { AST } from "./AST";
|
import { ShowAST } from "./ShowAST";
|
||||||
import { TraceableError } from "../statecharts/parser";
|
import { TraceableError } from "../statecharts/parser";
|
||||||
import { getKeyHandler } from "./shortcut_handler";
|
import { getKeyHandler } from "./shortcut_handler";
|
||||||
|
import { BottomPanel } from "./BottomPanel";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [mode, setMode] = useState<InsertMode>("and");
|
const [mode, setMode] = useState<InsertMode>("and");
|
||||||
|
|
@ -106,7 +107,7 @@ export function App() {
|
||||||
{...{ast, time, setTime, onInit, onClear, onRaise, mode, setMode}}
|
{...{ast, time, setTime, onInit, onClear, onRaise, mode, setMode}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack direction="row" sx={{height:'calc(100vh - 32px)'}}>
|
<Stack direction="row" sx={{height:'calc(100vh - 64px)'}}>
|
||||||
{/* main */}
|
{/* main */}
|
||||||
<Box sx={{flexGrow:1, overflow:'auto'}}>
|
<Box sx={{flexGrow:1, overflow:'auto'}}>
|
||||||
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode}}/>
|
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode}}/>
|
||||||
|
|
@ -117,14 +118,17 @@ export function App() {
|
||||||
borderLeft: 1,
|
borderLeft: 1,
|
||||||
borderColor: "divider",
|
borderColor: "divider",
|
||||||
flex: '0 0 content',
|
flex: '0 0 content',
|
||||||
paddingRight: 1,
|
// paddingRight: 1,
|
||||||
paddingLeft: 1,
|
// paddingLeft: 1,
|
||||||
}}>
|
}}>
|
||||||
<AST {...{...ast, rt: rt.at(rtIdx!)}}/>
|
<ShowAST {...{...ast, rt: rt.at(rtIdx!)}}/>
|
||||||
<br/>
|
<br/>
|
||||||
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx}}/>
|
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx}}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Box>
|
||||||
|
<BottomPanel {...{errors}}/>
|
||||||
|
</Box>
|
||||||
</Stack>;
|
</Stack>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
3
src/App/BottomPanel.css
Normal file
3
src/App/BottomPanel.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.errorStatus {
|
||||||
|
color: rgb(230,0,0);
|
||||||
|
}
|
||||||
10
src/App/BottomPanel.tsx
Normal file
10
src/App/BottomPanel.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { TraceableError } from "../statecharts/parser";
|
||||||
|
|
||||||
|
import "./BottomPanel.css";
|
||||||
|
|
||||||
|
export function BottomPanel(props: {errors: TraceableError[]}) {
|
||||||
|
return <div className="toolbar">
|
||||||
|
<div className="errorStatus">{
|
||||||
|
props.errors.length>0 && <>{props.errors.length} errors {props.errors.map(({message})=>message).join(',')}</>}</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ConcreteState, stateDescription, Transition } from "../statecharts/abstract_syntax";
|
import { ConcreteState, PseudoState, stateDescription, Transition } from "../statecharts/abstract_syntax";
|
||||||
import { Action, Expression } from "../statecharts/label_ast";
|
import { Action, Expression } from "../statecharts/label_ast";
|
||||||
import { RT_Statechart } from "../statecharts/runtime_types";
|
import { RT_Statechart } from "../statecharts/runtime_types";
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function ShowAction(props: {action: Action}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AST(props: {root: ConcreteState, transitions: Map<string, Transition[]>, rt: RT_Statechart | undefined}) {
|
export function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map<string, Transition[]>, rt: RT_Statechart | undefined}) {
|
||||||
const description = stateDescription(props.root);
|
const description = stateDescription(props.root);
|
||||||
const outgoing = props.transitions.get(props.root.uid) || [];
|
const outgoing = props.transitions.get(props.root.uid) || [];
|
||||||
|
|
||||||
|
|
@ -49,9 +49,9 @@ export function AST(props: {root: ConcreteState, transitions: Map<string, Transi
|
||||||
<div> exit / <ShowAction action={action}/></div>
|
<div> exit / <ShowAction action={action}/></div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{props.root.children.length>0 &&
|
{props.root.kind !== "pseudo" && props.root.children.length>0 &&
|
||||||
props.root.children.map(child =>
|
props.root.children.map(child =>
|
||||||
<AST root={child} transitions={props.transitions} rt={props.rt} />
|
<ShowAST root={child} transitions={props.transitions} rt={props.rt} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{outgoing.length>0 &&
|
{outgoing.length>0 &&
|
||||||
|
|
@ -10,10 +10,11 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
import BoltIcon from '@mui/icons-material/Bolt';
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
import SkipNextIcon from '@mui/icons-material/SkipNext';
|
import SkipNextIcon from '@mui/icons-material/SkipNext';
|
||||||
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
|
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
|
||||||
|
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
|
||||||
|
import StopIcon from '@mui/icons-material/Stop';
|
||||||
|
|
||||||
import { formatTime } from "./util";
|
import { formatTime } from "./util";
|
||||||
import { InsertMode } from "../VisualEditor/VisualEditor";
|
import { InsertMode } from "../VisualEditor/VisualEditor";
|
||||||
import { DiamondShape } from "../VisualEditor/RountangleSVG";
|
|
||||||
|
|
||||||
export type TopPanelProps = {
|
export type TopPanelProps = {
|
||||||
rt?: BigStep,
|
rt?: BigStep,
|
||||||
|
|
@ -33,16 +34,21 @@ function RountangleIcon(props: {kind: string}) {
|
||||||
x={1} y={1}
|
x={1} y={1}
|
||||||
width={18} height={18}
|
width={18} height={18}
|
||||||
className={`rountangle ${props.kind}`}
|
className={`rountangle ${props.kind}`}
|
||||||
style={props.kind === "or" ? {strokeDasharray: '3 2'}: {}}
|
style={{...(props.kind === "or" ? {strokeDasharray: '3 2'}: {}), strokeWidth: 1.2}}
|
||||||
/>
|
/>
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PseudoStateIcon(props: {}) {
|
function PseudoStateIcon(props: {}) {
|
||||||
return <svg width={20} height={20}>
|
const w=20, h=20;
|
||||||
<g transform="translate(2,1)">
|
return <svg width={w} height={h}>
|
||||||
<DiamondShape geometry={{topLeft:{x:0,y:0}, size:{x:16,y:18}}} extraAttrs={{className: 'rountangle pseudo'}}/>
|
<polygon
|
||||||
</g>
|
points={`
|
||||||
|
${w/2} ${1},
|
||||||
|
${w-1} ${h/2},
|
||||||
|
${w/2} ${h-1},
|
||||||
|
${1} ${h/2},
|
||||||
|
`} fill="white" stroke="black" strokeWidth={1.2}/>
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,28 +107,29 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
{([
|
{([
|
||||||
["and", "AND-states", <RountangleIcon kind="and"/>],
|
["and", "AND-states", <RountangleIcon kind="and"/>],
|
||||||
["or", "OR-states", <RountangleIcon kind="or"/>],
|
["or", "OR-states", <RountangleIcon kind="or"/>],
|
||||||
["pseudo", "pseudo-states", <PseudoStateIcon/>],
|
["pseudo", "pseudo-states", <PseudoStateIcon/>],
|
||||||
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>],
|
["transition", "transitions", <TrendingFlatIcon fontSize="small"/>],
|
||||||
["text", "text", <>T</>],
|
["text", "text", <> T </>],
|
||||||
] as [InsertMode, string, ReactElement][]).map(([m, hint, buttonTxt]) =>
|
] as [InsertMode, string, ReactElement][]).map(([m, hint, buttonTxt]) =>
|
||||||
<button
|
<button
|
||||||
title={"insert "+hint}
|
title={"insert "+hint}
|
||||||
disabled={mode===m}
|
disabled={mode===m}
|
||||||
onClick={() => setMode(m)}
|
className={mode===m ? "active":""}
|
||||||
>{buttonTxt}</button>)}
|
onClick={() => setMode(m)}
|
||||||
</div>
|
>{buttonTxt}</button>)}
|
||||||
 
|
|
||||||
<div className="toolbar">
|
|
||||||
<button title="(re)initialize simulation" onClick={onInit} ><CachedIcon fontSize="small"/><PlayArrowIcon fontSize="small"/></button>
|
|
||||||
<button title="clear the simulation" onClick={onClear} disabled={!rt}><ClearIcon fontSize="small"/></button>
|
|
||||||
|
|
||||||
 
|
 
|
||||||
|
|
||||||
<button title="pause the simulation" disabled={!rt || time.kind==="paused"} onClick={() => onChangePaused(true, performance.now())}><PauseIcon fontSize="small"/></button>
|
<button title="(re)initialize simulation" onClick={onInit} ><PlayArrowIcon fontSize="small"/><CachedIcon fontSize="small"/></button>
|
||||||
<button title="run the simulation in real time" disabled={!rt || time.kind==="realtime"} onClick={() => onChangePaused(false, performance.now())}><PlayArrowIcon fontSize="small"/></button>
|
<button title="clear the simulation" onClick={onClear} disabled={!rt}><StopIcon fontSize="small"/></button>
|
||||||
|
|
||||||
|
 
|
||||||
|
|
||||||
|
<button title="pause the simulation" disabled={!rt || time.kind==="paused"} className={(rt && time.kind==="paused") ? "active":""} onClick={() => onChangePaused(true, performance.now())}><PauseIcon fontSize="small"/></button>
|
||||||
|
<button title="run the simulation in real time" disabled={!rt || time.kind==="realtime"} className={(rt && time.kind==="realtime") ? "active":""} onClick={() => onChangePaused(false, performance.now())}><PlayArrowIcon fontSize="small"/></button>
|
||||||
|
|
||||||
{/* <ToggleButtonGroup value={time.kind} exclusive onChange={(_,newValue) => onChangePaused(newValue==="paused", performance.now())} size="small">
|
{/* <ToggleButtonGroup value={time.kind} exclusive onChange={(_,newValue) => onChangePaused(newValue==="paused", performance.now())} size="small">
|
||||||
<ToggleButton disableRipple value="paused" disabled={!rt}><PauseIcon/></ToggleButton>
|
<ToggleButton disableRipple value="paused" disabled={!rt}><PauseIcon/></ToggleButton>
|
||||||
|
|
@ -155,12 +162,15 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
|
||||||
return {kind: "realtime", scale: time.scale, since: {simtime: nextTimedTransition[0], wallclktime: now}};
|
return {kind: "realtime", scale: time.scale, since: {simtime: nextTimedTransition[0], wallclktime: now}};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}><SkipNextIcon fontSize="small"/></button>
|
}}><SkipNextIcon fontSize="small"/><AccessAlarmIcon fontSize="small"/></button>
|
||||||
|
|
||||||
|
 
|
||||||
|
|
||||||
{ast.inputEvents &&
|
{ast.inputEvents &&
|
||||||
<>
|
<>
|
||||||
{ast.inputEvents.map(({event, paramName}) =>
|
{ast.inputEvents.map(({event, paramName}) =>
|
||||||
<> <button title={`raise input event '${event}'`} disabled={!rt} onClick={() => {
|
<><button title={`raise input event '${event}'`} disabled={!rt} onClick={() => {
|
||||||
|
// @ts-ignore
|
||||||
const param = document.getElementById(`input-${event}-param`)?.value;
|
const param = document.getElementById(`input-${event}-param`)?.value;
|
||||||
let paramParsed;
|
let paramParsed;
|
||||||
try {
|
try {
|
||||||
|
|
@ -176,7 +186,7 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode
|
||||||
}}>
|
}}>
|
||||||
<BoltIcon fontSize="small"/>
|
<BoltIcon fontSize="small"/>
|
||||||
{event}
|
{event}
|
||||||
</button>{paramName && <><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>}</>)}
|
</button>{paramName && <><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>} </>)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,16 @@ export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: stri
|
||||||
data-uid={uid}
|
data-uid={uid}
|
||||||
data-parts="start end" />
|
data-parts="start end" />
|
||||||
|
|
||||||
{props.errors.length > 0 && <text className="error" x={(start.x + end.x) / 2 + 5} y={(start.y + end.y) / 2} data-uid={uid} data-parts="start end">{props.errors.join(' ')}</text>}
|
{props.errors.length > 0 && <text
|
||||||
|
className="error"
|
||||||
|
x={(start.x + end.x) / 2 + 5}
|
||||||
|
y={(start.y + end.y) / 2}
|
||||||
|
textAnchor="middle"
|
||||||
|
data-uid={uid}
|
||||||
|
data-parts="start end">{props.errors.join(' ')}</text>}
|
||||||
|
|
||||||
<path
|
<path
|
||||||
className="pathHelper"
|
className="pathHelper"
|
||||||
// markerEnd='url(#arrowEnd)'
|
|
||||||
d={`M ${start.x} ${start.y}
|
d={`M ${start.x} ${start.y}
|
||||||
${arcOrLine}
|
${arcOrLine}
|
||||||
${end.x} ${end.y}`}
|
${end.x} ${end.y}`}
|
||||||
|
|
|
||||||
37
src/VisualEditor/DiamondSVG.tsx
Normal file
37
src/VisualEditor/DiamondSVG.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Diamond, RountanglePart } from "@/statecharts/concrete_syntax";
|
||||||
|
import { rountangleMinSize } from "./VisualEditor";
|
||||||
|
import { Rect2D, Vec2D } from "./geometry";
|
||||||
|
import { RectHelper } from "./RectHelpers";
|
||||||
|
|
||||||
|
export function DiamondShape(props: {size: Vec2D, extraAttrs: object}) {
|
||||||
|
const minSize = rountangleMinSize(props.size);
|
||||||
|
return <polygon
|
||||||
|
points={`
|
||||||
|
${minSize.x/2} ${0},
|
||||||
|
${minSize.x} ${minSize.y/2},
|
||||||
|
${minSize.x/2} ${minSize.y},
|
||||||
|
${0} ${minSize.y/2}
|
||||||
|
`}
|
||||||
|
fill="white"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth={2}
|
||||||
|
{...props.extraAttrs}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DiamondSVG(props: { diamond: Diamond; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
|
||||||
|
const minSize = rountangleMinSize(props.diamond.size);
|
||||||
|
const extraAttrs = {
|
||||||
|
className: ''
|
||||||
|
+ (props.selected.length === 4 ? " selected" : "")
|
||||||
|
+ (props.errors.length > 0 ? " error" : "")
|
||||||
|
+ (props.active ? " active" : ""),
|
||||||
|
"data-uid": props.diamond.uid,
|
||||||
|
"data-parts": "left top right bottom",
|
||||||
|
};
|
||||||
|
return <g transform={`translate(${props.diamond.topLeft.x} ${props.diamond.topLeft.y})`}>
|
||||||
|
<DiamondShape size={minSize} extraAttrs={extraAttrs}/>
|
||||||
|
|
||||||
|
<RectHelper uid={props.diamond.uid} size={minSize} highlight={props.highlight} selected={props.selected} />
|
||||||
|
</g>;
|
||||||
|
}
|
||||||
3
src/VisualEditor/HistorySVG.tsx
Normal file
3
src/VisualEditor/HistorySVG.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function ShallowHistorySVG() {
|
||||||
|
|
||||||
|
}
|
||||||
80
src/VisualEditor/RectHelpers.tsx
Normal file
80
src/VisualEditor/RectHelpers.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { RountanglePart } from "../statecharts/concrete_syntax";
|
||||||
|
import { Vec2D } from "./geometry";
|
||||||
|
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters";
|
||||||
|
|
||||||
|
export function RectHelper(props: { uid: string, size: Vec2D, selected: string[], highlight: RountanglePart[] }) {
|
||||||
|
return <>
|
||||||
|
<line
|
||||||
|
className={"lineHelper"
|
||||||
|
+ (props.selected.includes("top") ? " selected" : "")
|
||||||
|
+ (props.highlight.includes("top") ? " highlight" : "")}
|
||||||
|
x1={0}
|
||||||
|
y1={0}
|
||||||
|
x2={props.size.x}
|
||||||
|
y2={0}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="top" />
|
||||||
|
<line
|
||||||
|
className={"lineHelper"
|
||||||
|
+ (props.selected.includes("right") ? " selected" : "")
|
||||||
|
+ (props.highlight.includes("right") ? " highlight" : "")}
|
||||||
|
x1={props.size.x}
|
||||||
|
y1={0}
|
||||||
|
x2={props.size.x}
|
||||||
|
y2={props.size.y}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="right" />
|
||||||
|
<line
|
||||||
|
className={"lineHelper"
|
||||||
|
+ (props.selected.includes("bottom") ? " selected" : "")
|
||||||
|
+ (props.highlight.includes("bottom") ? " highlight" : "")}
|
||||||
|
x1={0}
|
||||||
|
y1={props.size.y}
|
||||||
|
x2={props.size.x}
|
||||||
|
y2={props.size.y}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="bottom" />
|
||||||
|
<line
|
||||||
|
className={"lineHelper"
|
||||||
|
+ (props.selected.includes("left") ? " selected" : "")
|
||||||
|
+ (props.highlight.includes("left") ? " highlight" : "")}
|
||||||
|
x1={0}
|
||||||
|
y1={0}
|
||||||
|
x2={0}
|
||||||
|
y2={props.size.y}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="left" />
|
||||||
|
|
||||||
|
<circle
|
||||||
|
className="circleHelper corner"
|
||||||
|
cx={CORNER_HELPER_OFFSET}
|
||||||
|
cy={CORNER_HELPER_OFFSET}
|
||||||
|
r={CORNER_HELPER_RADIUS}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="top left" />
|
||||||
|
<circle
|
||||||
|
className="circleHelper corner"
|
||||||
|
cx={props.size.x - CORNER_HELPER_OFFSET}
|
||||||
|
cy={CORNER_HELPER_OFFSET}
|
||||||
|
r={CORNER_HELPER_RADIUS}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="top right" />
|
||||||
|
<circle
|
||||||
|
className="circleHelper corner"
|
||||||
|
cx={props.size.x - CORNER_HELPER_OFFSET}
|
||||||
|
cy={props.size.y - CORNER_HELPER_OFFSET}
|
||||||
|
r={CORNER_HELPER_RADIUS}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="bottom right" />
|
||||||
|
<circle
|
||||||
|
className="circleHelper corner"
|
||||||
|
cx={CORNER_HELPER_OFFSET}
|
||||||
|
cy={props.size.y - CORNER_HELPER_OFFSET}
|
||||||
|
r={CORNER_HELPER_RADIUS}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="bottom left" />
|
||||||
|
<text x={10} y={20}
|
||||||
|
className="uid"
|
||||||
|
data-uid={props.uid}>{props.uid}</text>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,8 @@
|
||||||
import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax";
|
import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax";
|
||||||
import { Rect2D } from "./geometry";
|
import { ROUNTANGLE_RADIUS } from "./parameters";
|
||||||
import { ROUNTANGLE_RADIUS, CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters";
|
import { RectHelper } from "./RectHelpers";
|
||||||
import { rountangleMinSize } from "./VisualEditor";
|
import { rountangleMinSize } from "./VisualEditor";
|
||||||
|
|
||||||
export function DiamondShape(props: {geometry: Rect2D, extraAttrs: object}) {
|
|
||||||
const {geometry} = props;
|
|
||||||
return <polygon
|
|
||||||
points={`
|
|
||||||
${geometry.size.x/2} ${0},
|
|
||||||
${geometry.size.x} ${geometry.size.y/2},
|
|
||||||
${geometry.size.x/2} ${geometry.size.y},
|
|
||||||
${0} ${geometry.size.y/2}
|
|
||||||
`}
|
|
||||||
{...props.extraAttrs}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function RountangleSVG(props: { rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
|
export function RountangleSVG(props: { rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
|
||||||
const { topLeft, size, uid } = props.rountangle;
|
const { topLeft, size, uid } = props.rountangle;
|
||||||
|
|
@ -31,94 +19,20 @@ export function RountangleSVG(props: { rountangle: Rountangle; selected: string[
|
||||||
"data-parts": "left top right bottom",
|
"data-parts": "left top right bottom",
|
||||||
};
|
};
|
||||||
return <g transform={`translate(${topLeft.x} ${topLeft.y})`}>
|
return <g transform={`translate(${topLeft.x} ${topLeft.y})`}>
|
||||||
{props.rountangle.kind === "pseudo" ?
|
<rect
|
||||||
<DiamondShape geometry={props.rountangle} extraAttrs={extraAttrs}/>
|
rx={ROUNTANGLE_RADIUS} ry={ROUNTANGLE_RADIUS}
|
||||||
: <rect
|
x={0}
|
||||||
rx={ROUNTANGLE_RADIUS} ry={ROUNTANGLE_RADIUS}
|
y={0}
|
||||||
x={0}
|
width={minSize.x}
|
||||||
y={0}
|
height={minSize.y}
|
||||||
width={minSize.x}
|
{...extraAttrs}
|
||||||
height={minSize.y}
|
/>
|
||||||
{...extraAttrs}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{(props.errors.length > 0) &&
|
{(props.errors.length > 0) &&
|
||||||
<text className="error" x={10} y={40} data-uid={uid} data-parts="left top right bottom">{props.errors.join(' ')}</text>}
|
<text className="error" x={10} y={40} data-uid={uid} data-parts="left top right bottom">{props.errors.join(' ')}</text>}
|
||||||
|
|
||||||
|
<RectHelper uid={uid} size={minSize}
|
||||||
<line
|
selected={props.selected}
|
||||||
className={"lineHelper"
|
highlight={props.highlight} />
|
||||||
+ (props.selected.includes("top") ? " selected" : "")
|
|
||||||
+ (props.highlight.includes("top") ? " highlight" : "")}
|
|
||||||
x1={0}
|
|
||||||
y1={0}
|
|
||||||
x2={minSize.x}
|
|
||||||
y2={0}
|
|
||||||
data-uid={uid}
|
|
||||||
data-parts="top" />
|
|
||||||
<line
|
|
||||||
className={"lineHelper"
|
|
||||||
+ (props.selected.includes("right") ? " selected" : "")
|
|
||||||
+ (props.highlight.includes("right") ? " highlight" : "")}
|
|
||||||
x1={minSize.x}
|
|
||||||
y1={0}
|
|
||||||
x2={minSize.x}
|
|
||||||
y2={minSize.y}
|
|
||||||
data-uid={uid}
|
|
||||||
data-parts="right" />
|
|
||||||
<line
|
|
||||||
className={"lineHelper"
|
|
||||||
+ (props.selected.includes("bottom") ? " selected" : "")
|
|
||||||
+ (props.highlight.includes("bottom") ? " highlight" : "")}
|
|
||||||
x1={0}
|
|
||||||
y1={minSize.y}
|
|
||||||
x2={minSize.x}
|
|
||||||
y2={minSize.y}
|
|
||||||
data-uid={uid}
|
|
||||||
data-parts="bottom" />
|
|
||||||
<line
|
|
||||||
className={"lineHelper"
|
|
||||||
+ (props.selected.includes("left") ? " selected" : "")
|
|
||||||
+ (props.highlight.includes("left") ? " highlight" : "")}
|
|
||||||
x1={0}
|
|
||||||
y1={0}
|
|
||||||
x2={0}
|
|
||||||
y2={minSize.y}
|
|
||||||
data-uid={uid}
|
|
||||||
data-parts="left" />
|
|
||||||
|
|
||||||
<circle
|
|
||||||
className="circleHelper corner"
|
|
||||||
cx={CORNER_HELPER_OFFSET}
|
|
||||||
cy={CORNER_HELPER_OFFSET}
|
|
||||||
r={CORNER_HELPER_RADIUS}
|
|
||||||
data-uid={uid}
|
|
||||||
data-parts="top left" />
|
|
||||||
<circle
|
|
||||||
className="circleHelper corner"
|
|
||||||
cx={minSize.x - CORNER_HELPER_OFFSET}
|
|
||||||
cy={CORNER_HELPER_OFFSET}
|
|
||||||
r={CORNER_HELPER_RADIUS}
|
|
||||||
data-uid={uid}
|
|
||||||
data-parts="top right" />
|
|
||||||
<circle
|
|
||||||
className="circleHelper corner"
|
|
||||||
cx={minSize.x - CORNER_HELPER_OFFSET}
|
|
||||||
cy={minSize.y - CORNER_HELPER_OFFSET}
|
|
||||||
r={CORNER_HELPER_RADIUS}
|
|
||||||
data-uid={uid}
|
|
||||||
data-parts="bottom right" />
|
|
||||||
<circle
|
|
||||||
className="circleHelper corner"
|
|
||||||
cx={CORNER_HELPER_OFFSET}
|
|
||||||
cy={minSize.y - CORNER_HELPER_OFFSET}
|
|
||||||
r={CORNER_HELPER_RADIUS}
|
|
||||||
data-uid={uid}
|
|
||||||
data-parts="bottom left" />
|
|
||||||
<text x={10} y={20}
|
|
||||||
className="uid"
|
|
||||||
data-uid={uid}>{uid}</text>
|
|
||||||
</g>;
|
</g>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
src/VisualEditor/TextSVG.tsx
Normal file
41
src/VisualEditor/TextSVG.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { TraceableError } from "..//statecharts/parser";
|
||||||
|
import {Text} from "../statecharts/concrete_syntax";
|
||||||
|
|
||||||
|
export function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (newText: string) => void}) {
|
||||||
|
const commonProps = {
|
||||||
|
"data-uid": props.text.uid,
|
||||||
|
"data-parts": "text",
|
||||||
|
textAnchor: "middle" as "middle",
|
||||||
|
className:
|
||||||
|
(props.selected ? "selected":"")
|
||||||
|
+(props.highlight ? " highlight":""),
|
||||||
|
}
|
||||||
|
|
||||||
|
let textNode;
|
||||||
|
if (props.error?.data?.location) {
|
||||||
|
const {start,end} = props.error.data.location;
|
||||||
|
textNode = <><text {...commonProps}>
|
||||||
|
{props.text.text.slice(0, start.offset)}
|
||||||
|
<tspan className="error" data-uid={props.text.uid} data-parts="text">
|
||||||
|
{props.text.text.slice(start.offset, end.offset)}
|
||||||
|
{start.offset === end.offset && <>_</>}
|
||||||
|
</tspan>
|
||||||
|
{props.text.text.slice(end.offset)}
|
||||||
|
</text>
|
||||||
|
<text className="error errorHover" y={20} textAnchor="middle">{props.error.message}</text></>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
textNode = <text {...commonProps}>{props.text.text}</text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <g
|
||||||
|
key={props.text.uid}
|
||||||
|
transform={`translate(${props.text.topLeft.x} ${props.text.topLeft.y})`}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
const newText = prompt("", props.text.text);
|
||||||
|
if (newText) {
|
||||||
|
props.onEdit(newText);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>{textNode}</g>;
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.svgCanvas.dragging {
|
||||||
|
cursor: grabbing !important;
|
||||||
|
}
|
||||||
|
|
||||||
.svgCanvas.active {
|
.svgCanvas.active {
|
||||||
background-color: rgb(255, 140, 0, 0.2);
|
background-color: rgb(255, 140, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
@ -49,12 +53,6 @@ text.highlight {
|
||||||
/* cursor: grab; */
|
/* cursor: grab; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.rountangle.dragging {
|
|
||||||
/* fill: lightgrey; */
|
|
||||||
/* stroke-width: 4px; */
|
|
||||||
cursor: grabbing;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rountangle.selected {
|
.rountangle.selected {
|
||||||
fill: rgba(0, 0, 255, 0.2);
|
fill: rgba(0, 0, 255, 0.2);
|
||||||
/* stroke: blue;
|
/* stroke: blue;
|
||||||
|
|
@ -70,7 +68,7 @@ text.highlight {
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected:hover {
|
.selected:hover:not(:active) {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,10 +76,10 @@ text.highlight {
|
||||||
stroke: rgba(0, 0, 0, 0);
|
stroke: rgba(0, 0, 0, 0);
|
||||||
stroke-width: 16px;
|
stroke-width: 16px;
|
||||||
}
|
}
|
||||||
.lineHelper:hover {
|
.lineHelper:hover:not(:active) {
|
||||||
stroke: blue;
|
stroke: blue;
|
||||||
stroke-opacity: 0.2;
|
stroke-opacity: 0.2;
|
||||||
/* cursor: grab; */
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pathHelper {
|
.pathHelper {
|
||||||
|
|
@ -89,7 +87,7 @@ text.highlight {
|
||||||
stroke: rgba(0, 0, 0, 0);
|
stroke: rgba(0, 0, 0, 0);
|
||||||
stroke-width: 16px;
|
stroke-width: 16px;
|
||||||
}
|
}
|
||||||
.pathHelper:hover {
|
.pathHelper:hover:not(:active) {
|
||||||
stroke: blue;
|
stroke: blue;
|
||||||
stroke-opacity: 0.2;
|
stroke-opacity: 0.2;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
|
@ -99,10 +97,10 @@ text.highlight {
|
||||||
.circleHelper {
|
.circleHelper {
|
||||||
fill: rgba(0, 0, 0, 0);
|
fill: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
.circleHelper:hover {
|
.circleHelper:hover:not(:active) {
|
||||||
fill: blue;
|
fill: blue;
|
||||||
fill-opacity: 0.2;
|
fill-opacity: 0.2;
|
||||||
/* cursor: grab; */
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rountangle.or {
|
.rountangle.or {
|
||||||
|
|
@ -134,9 +132,6 @@ text.highlight {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
line.selected, circle.selected {
|
line.selected, circle.selected {
|
||||||
fill: rgba(0, 0, 255, 0.2);
|
fill: rgba(0, 0, 255, 0.2);
|
||||||
/* stroke-dasharray: 7 6; */
|
/* stroke-dasharray: 7 6; */
|
||||||
|
|
@ -148,9 +143,9 @@ text.selected, text.selected:hover {
|
||||||
fill: blue;
|
fill: blue;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
text:hover {
|
text:hover:not(:active) {
|
||||||
fill: blue;
|
fill: blue;
|
||||||
/* cursor: grab; */
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import * as lz4 from "@nick/lz4";
|
||||||
import { Dispatch, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react";
|
import { Dispatch, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react";
|
||||||
|
|
||||||
import { Statechart } from "../statecharts/abstract_syntax";
|
import { Statechart } from "../statecharts/abstract_syntax";
|
||||||
import { Arrow, ArrowPart, Rountangle, RountanglePart, Text, VisualEditorState, emptyState, findNearestArrow, findNearestRountangleSide, findRountangle } from "../statecharts/concrete_syntax";
|
import { Arrow, ArrowPart, Diamond, Rountangle, RountanglePart, Text, VisualEditorState, emptyState, findNearestArrow, findNearestSide, findRountangle } 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";
|
||||||
|
|
@ -12,6 +12,8 @@ import { getBBoxInSvgCoords } from "./svg_helper";
|
||||||
import "./VisualEditor.css";
|
import "./VisualEditor.css";
|
||||||
import { ArrowSVG } from "./ArrowSVG";
|
import { ArrowSVG } from "./ArrowSVG";
|
||||||
import { RountangleSVG } from "./RountangleSVG";
|
import { RountangleSVG } from "./RountangleSVG";
|
||||||
|
import { TextSVG } from "./TextSVG";
|
||||||
|
import { DiamondSVG } from "./DiamondSVG";
|
||||||
|
|
||||||
|
|
||||||
type DraggingState = {
|
type DraggingState = {
|
||||||
|
|
@ -115,7 +117,6 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
const [dragging, setDragging] = useState<DraggingState>(null);
|
const [dragging, setDragging] = useState<DraggingState>(null);
|
||||||
const [showHelp, setShowHelp] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// uid's of selected rountangles
|
// uid's of selected rountangles
|
||||||
const [selection, setSelection] = useState<Selection>([]);
|
const [selection, setSelection] = useState<Selection>([]);
|
||||||
|
|
@ -147,14 +148,20 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
const compressedStateString = compressedStateBuffer.toBase64();
|
const compressedStateString = compressedStateBuffer.toBase64();
|
||||||
window.location.hash = "#"+compressedStateString;
|
window.location.hash = "#"+compressedStateString;
|
||||||
|
|
||||||
const [statechart, errors] = parseStatechart(state);
|
// const [statechart, errors] = parseStatechart(state);
|
||||||
// console.log('statechart: ', statechart, 'errors:', errors);
|
// setErrors(errors);
|
||||||
setErrors(errors);
|
// setAST(statechart);
|
||||||
setAST(statechart);
|
}, 200);
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const [statechart, errors] = parseStatechart(state);
|
||||||
|
setErrors(errors);
|
||||||
|
setAST(statechart);
|
||||||
|
}, [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 {
|
||||||
|
|
@ -171,7 +178,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
// ignore selection, middle mouse button always inserts
|
// ignore selection, middle mouse button always inserts
|
||||||
setState(state => {
|
setState(state => {
|
||||||
const newID = state.nextID.toString();
|
const newID = state.nextID.toString();
|
||||||
if (mode === "and" || mode === "or" || mode === "pseudo") {
|
if (mode === "and" || mode === "or") {
|
||||||
// insert rountangle
|
// insert rountangle
|
||||||
setSelection([{uid: newID, parts: ["bottom", "right"]}]);
|
setSelection([{uid: newID, parts: ["bottom", "right"]}]);
|
||||||
return {
|
return {
|
||||||
|
|
@ -185,6 +192,18 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
nextID: state.nextID+1,
|
nextID: state.nextID+1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
else if (mode === "pseudo") {
|
||||||
|
setSelection([{uid: newID, parts: ["bottom", "right"]}]);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
diamonds: [...state.diamonds, {
|
||||||
|
uid: newID,
|
||||||
|
topLeft: currentPointer,
|
||||||
|
size: MIN_ROUNTANGLE_SIZE,
|
||||||
|
}],
|
||||||
|
nextID: state.nextID+1,
|
||||||
|
};
|
||||||
|
}
|
||||||
else if (mode === "transition") {
|
else if (mode === "transition") {
|
||||||
setSelection([{uid: newID, parts: ["end"]}]);
|
setSelection([{uid: newID, parts: ["end"]}]);
|
||||||
return {
|
return {
|
||||||
|
|
@ -219,7 +238,9 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
|
|
||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
// left mouse button on a shape will drag that shape (and everything else that's selected). if the shape under the pointer was not in the selection then the selection is reset to contain only that shape.
|
// left mouse button on a shape will drag that shape (and everything else that's selected). if the shape under the pointer was not in the selection then the selection is reset to contain only that shape.
|
||||||
|
// @ts-ignore
|
||||||
const uid = e.target?.dataset.uid;
|
const uid = e.target?.dataset.uid;
|
||||||
|
// @ts-ignore
|
||||||
const parts: string[] = e.target?.dataset.parts?.split(' ') || [];
|
const parts: string[] = e.target?.dataset.parts?.split(' ') || [];
|
||||||
if (uid) {
|
if (uid) {
|
||||||
checkPoint();
|
checkPoint();
|
||||||
|
|
@ -232,26 +253,25 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if (!allPartsInSelection) {
|
if (!allPartsInSelection) {
|
||||||
// setSelection([{uid, parts}] as Selection);
|
setSelection([{uid, parts}] as Selection);
|
||||||
// }
|
|
||||||
|
|
||||||
if (allPartsInSelection) {
|
|
||||||
// start dragging
|
|
||||||
setDragging({
|
|
||||||
lastMousePos: currentPointer,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start dragging
|
||||||
|
setDragging({
|
||||||
|
lastMousePos: currentPointer,
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// otherwise, just start making a selection
|
|
||||||
setDragging(null);
|
|
||||||
setSelectingState({
|
|
||||||
topLeft: currentPointer,
|
|
||||||
size: {x: 0, y: 0},
|
|
||||||
});
|
|
||||||
setSelection([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// otherwise, just start making a selection
|
||||||
|
setDragging(null);
|
||||||
|
setSelectingState({
|
||||||
|
topLeft: currentPointer,
|
||||||
|
size: {x: 0, y: 0},
|
||||||
|
});
|
||||||
|
setSelection([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseMove = (e: {pageX: number, pageY: number}) => {
|
const onMouseMove = (e: {pageX: number, pageY: number}) => {
|
||||||
|
|
@ -266,8 +286,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
uid: r.uid,
|
...r,
|
||||||
kind: r.kind,
|
|
||||||
...transformRect(r, parts, pointerDelta),
|
...transformRect(r, parts, pointerDelta),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
@ -278,7 +297,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
uid: a.uid,
|
...a,
|
||||||
...transformLine(a, parts, pointerDelta),
|
...transformLine(a, parts, pointerDelta),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
@ -288,11 +307,20 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
uid: t.uid,
|
...t,
|
||||||
text: t.text,
|
|
||||||
topLeft: addV2D(t.topLeft, pointerDelta),
|
topLeft: addV2D(t.topLeft, pointerDelta),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
diamonds: state.diamonds.map(d => {
|
||||||
|
const parts = selection.find(selected => selected.uid === d.uid)?.parts || [];
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
...transformRect(d, parts, pointerDelta),
|
||||||
|
}
|
||||||
|
})
|
||||||
}));
|
}));
|
||||||
setDragging({lastMousePos: currentPointer});
|
setDragging({lastMousePos: currentPointer});
|
||||||
}
|
}
|
||||||
|
|
@ -307,7 +335,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseUp = (e) => {
|
const onMouseUp = (e: {pageX: number, pageY: number}) => {
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
setDragging(null);
|
setDragging(null);
|
||||||
// do not persist sizes smaller than 40x40
|
// do not persist sizes smaller than 40x40
|
||||||
|
|
@ -318,53 +346,37 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
...r,
|
...r,
|
||||||
size: rountangleMinSize(r.size),
|
size: rountangleMinSize(r.size),
|
||||||
})),
|
})),
|
||||||
|
diamonds: state.diamonds.map(d => ({
|
||||||
|
...d,
|
||||||
|
size: rountangleMinSize(d.size),
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (selectingState) {
|
if (selectingState) {
|
||||||
// we were making a selection
|
// we were making a selection
|
||||||
if (selectingState.size.x === 0 && selectingState.size.y === 0) {
|
const normalizedSS = normalizeRect(selectingState);
|
||||||
const uid = e.target?.dataset.uid;
|
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
|
||||||
const parts: string[] = e.target?.dataset.parts?.split(' ') || [];
|
const shapesInSelection = shapes.filter(el => {
|
||||||
|
const bbox = getBBoxInSvgCoords(el, refSVG.current!);
|
||||||
|
return isEntirelyWithin(bbox, normalizedSS);
|
||||||
|
}).filter(el => !el.classList.contains("corner"));
|
||||||
|
|
||||||
|
const uidToParts = new Map();
|
||||||
|
for (const shape of shapesInSelection) {
|
||||||
|
const uid = shape.dataset.uid;
|
||||||
if (uid) {
|
if (uid) {
|
||||||
checkPoint();
|
const parts: Set<string> = uidToParts.get(uid) || new Set();
|
||||||
// @ts-ignore
|
for (const part of shape.dataset.parts?.split(' ') || []) {
|
||||||
setSelection(() => ([{uid, parts}]));
|
parts.add(part);
|
||||||
|
|
||||||
// if the mouse button is pressed outside of the current selection, we reset the selection to whatever shape the mouse is on
|
|
||||||
let allPartsInSelection = true;
|
|
||||||
for (const part of parts) {
|
|
||||||
if (!(selection.find(s => s.uid === uid)?.parts || [] as string[]).includes(part)) {
|
|
||||||
allPartsInSelection = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
uidToParts.set(uid, parts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({
|
||||||
const normalizedSS = normalizeRect(selectingState);
|
uid,
|
||||||
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
|
parts: [...parts],
|
||||||
const shapesInSelection = shapes.filter(el => {
|
})));
|
||||||
const bbox = getBBoxInSvgCoords(el, refSVG.current!);
|
|
||||||
return isEntirelyWithin(bbox, normalizedSS);
|
|
||||||
}).filter(el => !el.classList.contains("corner"));
|
|
||||||
|
|
||||||
const uidToParts = new Map();
|
|
||||||
for (const shape of shapesInSelection) {
|
|
||||||
const uid = shape.dataset.uid;
|
|
||||||
if (uid) {
|
|
||||||
const parts: Set<string> = uidToParts.get(uid) || new Set();
|
|
||||||
for (const part of shape.dataset.parts?.split(' ') || []) {
|
|
||||||
parts.add(part);
|
|
||||||
}
|
|
||||||
uidToParts.set(uid, parts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({
|
|
||||||
uid,
|
|
||||||
parts: [...parts],
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setSelectingState(null); // no longer making a selection
|
setSelectingState(null); // no longer making a selection
|
||||||
};
|
};
|
||||||
|
|
@ -375,6 +387,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)),
|
rountangles: state.rountangles.filter(r => !selection.some(rs => rs.uid === r.uid)),
|
||||||
arrows: state.arrows.filter(a => !selection.some(as => as.uid === a.uid)),
|
arrows: state.arrows.filter(a => !selection.some(as => as.uid === a.uid)),
|
||||||
texts: state.texts.filter(t => !selection.some(ts => ts.uid === t.uid)),
|
texts: state.texts.filter(t => !selection.some(ts => ts.uid === t.uid)),
|
||||||
|
diamonds: state.diamonds.filter(d => !selection.some(ds => ds.uid === d.uid)),
|
||||||
}));
|
}));
|
||||||
setSelection([]);
|
setSelection([]);
|
||||||
}
|
}
|
||||||
|
|
@ -407,74 +420,17 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
return selection;
|
return selection;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (e.key === "p") {
|
// if (e.key === "p") {
|
||||||
// selected states become pseudo-states
|
// // selected states become pseudo-states
|
||||||
setSelection(selection => {
|
// setSelection(selection => {
|
||||||
setState(state => ({
|
// setState(state => ({
|
||||||
...state,
|
// ...state,
|
||||||
rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "pseudo"}) : r),
|
// rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "pseudo"}) : r),
|
||||||
}));
|
// }));
|
||||||
return selection;
|
// return selection;
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
if (e.key === "h") {
|
|
||||||
setShowHelp(showHelp => !showHelp);
|
|
||||||
}
|
|
||||||
if (e.ctrlKey) {
|
if (e.ctrlKey) {
|
||||||
// if (e.key === "c") {
|
|
||||||
// if (selection.length > 0) {
|
|
||||||
// e.preventDefault();
|
|
||||||
// setClipboard(new Set(selection.map(shape => shape.uid)));
|
|
||||||
// console.log('set clipboard', new Set(selection.map(shape => shape.uid)));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (e.key === "v") {
|
|
||||||
// console.log('paste shortcut..', clipboard);
|
|
||||||
// if (clipboard.size > 0) {
|
|
||||||
// console.log('pasting...a');
|
|
||||||
// e.preventDefault();
|
|
||||||
// checkPoint();
|
|
||||||
// const offset = {x: 40, y: 40};
|
|
||||||
// const rountanglesToCopy = state.rountangles.filter(r => clipboard.has(r.uid));
|
|
||||||
// const arrowsToCopy = state.arrows.filter(a => clipboard.has(a.uid));
|
|
||||||
// const textsToCopy = state.texts.filter(t => clipboard.has(t.uid));
|
|
||||||
// let nextUid = state.nextID;
|
|
||||||
// const rountanglesCopied: Rountangle[] = rountanglesToCopy.map(r => ({
|
|
||||||
// ...r,
|
|
||||||
// uid: (nextUid++).toString(),
|
|
||||||
// topLeft: addV2D(r.topLeft, offset),
|
|
||||||
// }));
|
|
||||||
// const arrowsCopied: Arrow[] = arrowsToCopy.map(a => ({
|
|
||||||
// ...a,
|
|
||||||
// uid: (nextUid++).toString(),
|
|
||||||
// start: addV2D(a.start, offset),
|
|
||||||
// end: addV2D(a.end, offset),
|
|
||||||
// }));
|
|
||||||
// const textsCopied: Text[] = textsToCopy.map(t => ({
|
|
||||||
// ...t,
|
|
||||||
// uid: (nextUid++).toString(),
|
|
||||||
// topLeft: addV2D(t.topLeft, offset),
|
|
||||||
// }));
|
|
||||||
// setState(state => ({
|
|
||||||
// ...state,
|
|
||||||
// rountangles: [...state.rountangles, ...rountanglesCopied],
|
|
||||||
// arrows: [...state.arrows, ...arrowsCopied],
|
|
||||||
// texts: [...state.texts, ...textsCopied],
|
|
||||||
// nextID: nextUid,
|
|
||||||
// }));
|
|
||||||
// setClipboard(new Set([
|
|
||||||
// ...rountanglesCopied.map(r => r.uid),
|
|
||||||
// ...arrowsCopied.map(a => a.uid),
|
|
||||||
// ...textsCopied.map(t => t.uid),
|
|
||||||
// ]));
|
|
||||||
// // @ts-ignore
|
|
||||||
// setSelection([
|
|
||||||
// ...rountanglesCopied.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
|
|
||||||
// ...arrowsCopied.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
|
||||||
// ...textsCopied.map(t => ({uid: t.uid, parts: ["text"]})),
|
|
||||||
// ]);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
if (e.key === "z") {
|
if (e.key === "z") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
undo();
|
undo();
|
||||||
|
|
@ -493,11 +449,6 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
...state.texts.map(t => ({uid: t.uid, parts: ["text"]})),
|
...state.texts.map(t => ({uid: t.uid, parts: ["text"]})),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "c") {
|
|
||||||
// e.preventDefault();
|
|
||||||
// setClipboard()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -520,9 +471,12 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
const arrow2TextMap = new Map<string,string[]>();
|
const arrow2TextMap = new Map<string,string[]>();
|
||||||
const text2RountangleMap = new Map<string, string>();
|
const text2RountangleMap = new Map<string, string>();
|
||||||
const rountangle2TextMap = new Map<string, string[]>();
|
const rountangle2TextMap = new Map<string, string[]>();
|
||||||
|
|
||||||
|
// arrow <-> (rountangle | diamond)
|
||||||
for (const arrow of state.arrows) {
|
for (const arrow of state.arrows) {
|
||||||
const startSide = findNearestRountangleSide(arrow, "start", state.rountangles);
|
const sides = [...state.rountangles, ...state.diamonds];
|
||||||
const endSide = findNearestRountangleSide(arrow, "end", state.rountangles);
|
const startSide = findNearestSide(arrow, "start", sides);
|
||||||
|
const endSide = findNearestSide(arrow, "end", sides);
|
||||||
if (startSide || endSide) {
|
if (startSide || endSide) {
|
||||||
arrow2SideMap.set(arrow.uid, [startSide, endSide]);
|
arrow2SideMap.set(arrow.uid, [startSide, endSide]);
|
||||||
}
|
}
|
||||||
|
|
@ -537,6 +491,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
side2ArrowMap.set(endSide.uid + '/' + endSide.part, arrowConns);
|
side2ArrowMap.set(endSide.uid + '/' + endSide.part, arrowConns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// text <-> arrow
|
||||||
for (const text of state.texts) {
|
for (const text of state.texts) {
|
||||||
const nearestArrow = findNearestArrow(text.topLeft, state.arrows);
|
const nearestArrow = findNearestArrow(text.topLeft, state.arrows);
|
||||||
if (nearestArrow) {
|
if (nearestArrow) {
|
||||||
|
|
@ -547,7 +502,7 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
arrow2TextMap.set(nearestArrow.uid, textsOfArrow);
|
arrow2TextMap.set(nearestArrow.uid, textsOfArrow);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// no arrow, then the text belongs to the rountangle it is in
|
// text <-> rountangle
|
||||||
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
||||||
if (rountangle) {
|
if (rountangle) {
|
||||||
text2RountangleMap.set(text.uid, rountangle.uid);
|
text2RountangleMap.set(text.uid, rountangle.uid);
|
||||||
|
|
@ -672,20 +627,50 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
deleteShapes(selection);
|
deleteShapes(selection);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEditText(text: Text, newText: string) {
|
||||||
|
if (newText === "") {
|
||||||
|
// delete text node
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
texts: state.texts.filter(t => t.uid !== text.uid),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setState(state => ({
|
||||||
|
...state,
|
||||||
|
texts: state.texts.map(t => {
|
||||||
|
if (t.uid === text.uid) {
|
||||||
|
return {
|
||||||
|
...text,
|
||||||
|
text: newText,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const active = rt?.mode || new Set();
|
const active = rt?.mode || new Set();
|
||||||
|
|
||||||
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
||||||
|
|
||||||
return <svg width="4000px" height="4000px"
|
return <svg width="4000px" height="4000px"
|
||||||
className={"svgCanvas"+(active.has("root")?" active":"")}
|
className={"svgCanvas"+(active.has("root")?" active":"")+(dragging!==null?" dragging":"")}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
ref={refSVG}
|
ref={refSVG}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
|
// @ts-ignore
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
|
// @ts-ignore
|
||||||
onCut={onCut}
|
onCut={onCut}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
|
|
@ -694,14 +679,15 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
viewBox="0 0 10 10"
|
viewBox="0 0 10 10"
|
||||||
refX="5"
|
refX="5"
|
||||||
refY="5"
|
refY="5"
|
||||||
markerWidth="6"
|
markerWidth="12"
|
||||||
markerHeight="6"
|
markerHeight="12"
|
||||||
orient="auto-start-reverse">
|
orient="auto-start-reverse"
|
||||||
|
markerUnits="userSpaceOnUse">
|
||||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||||
</marker>
|
</marker>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
{(rootErrors.length>0) && <text className="error" x={5} y={50}>{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}
|
||||||
|
|
@ -714,6 +700,18 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
active={active.has(rountangle.uid)}
|
active={active.has(rountangle.uid)}
|
||||||
/>)}
|
/>)}
|
||||||
|
|
||||||
|
{state.diamonds.map(diamond => <>
|
||||||
|
<DiamondSVG
|
||||||
|
key={diamond.uid}
|
||||||
|
diamond={diamond}
|
||||||
|
selected={selection.find(r => r.uid === diamond.uid)?.parts || []}
|
||||||
|
highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]}
|
||||||
|
errors={errors
|
||||||
|
.filter(({shapeUid}) => shapeUid === diamond.uid)
|
||||||
|
.map(({message}) => message)}
|
||||||
|
active={active.has(diamond.uid)}/>
|
||||||
|
</>)}
|
||||||
|
|
||||||
{state.arrows.map(arrow => {
|
{state.arrows.map(arrow => {
|
||||||
const sides = arrow2SideMap.get(arrow.uid);
|
const sides = arrow2SideMap.get(arrow.uid);
|
||||||
let arc = "no" as ArcDirection;
|
let arc = "no" as ArcDirection;
|
||||||
|
|
@ -734,60 +732,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{state.texts.map(txt => {
|
{state.texts.map(txt => {
|
||||||
const err = errors.find(({shapeUid}) => txt.uid === shapeUid);
|
return <TextSVG
|
||||||
const commonProps = {
|
error={errors.find(({shapeUid}) => txt.uid === shapeUid)}
|
||||||
"data-uid": txt.uid,
|
text={txt}
|
||||||
"data-parts": "text",
|
selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)}
|
||||||
textAnchor: "middle" as "middle",
|
highlight={textsToHighlight.hasOwnProperty(txt.uid)}
|
||||||
className:
|
onEdit={newText => onEditText(txt, newText)}
|
||||||
(selection.find(s => s.uid === txt.uid)?.parts?.length ? "selected":"")
|
/>
|
||||||
+(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":""),
|
})}
|
||||||
}
|
|
||||||
let textNode;
|
|
||||||
if (err?.data?.location) {
|
|
||||||
const {start,end} = err.data.location;
|
|
||||||
textNode = <><text {...commonProps}>
|
|
||||||
{txt.text.slice(0, start.offset)}
|
|
||||||
<tspan className="error" data-uid={txt.uid} data-parts="text">
|
|
||||||
{txt.text.slice(start.offset, end.offset)}
|
|
||||||
{start.offset === end.offset && <>_</>}
|
|
||||||
</tspan>
|
|
||||||
{txt.text.slice(end.offset)}
|
|
||||||
</text>
|
|
||||||
<text className="error errorHover" y={20} textAnchor="middle">{err.message}</text></>;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
textNode = <text {...commonProps}>{txt.text}</text>;
|
|
||||||
}
|
|
||||||
return <g
|
|
||||||
key={txt.uid}
|
|
||||||
transform={`translate(${txt.topLeft.x} ${txt.topLeft.y})`}
|
|
||||||
onDoubleClick={() => {
|
|
||||||
const newText = prompt("", txt.text);
|
|
||||||
if (newText) {
|
|
||||||
setState(state => ({
|
|
||||||
...state,
|
|
||||||
texts: state.texts.map(t => {
|
|
||||||
if (t.uid === txt.uid) {
|
|
||||||
return {
|
|
||||||
...txt,
|
|
||||||
text: newText,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
else if (newText === "") {
|
|
||||||
setState(state => ({
|
|
||||||
...state,
|
|
||||||
texts: state.texts.filter(t => t.uid !== txt.uid),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>{textNode}</g>;})}
|
|
||||||
|
|
||||||
{selectingState && <Selecting {...selectingState} />}
|
{selectingState && <Selecting {...selectingState} />}
|
||||||
</svg>;
|
</svg>;
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,43 @@
|
||||||
import { Action, EventTrigger, ParsedText, TransitionLabel } from "./label_ast";
|
import { Action, EventTrigger, ParsedText } from "./label_ast";
|
||||||
|
|
||||||
export type AbstractState = {
|
export type AbstractState = {
|
||||||
uid: string;
|
uid: string;
|
||||||
parent?: ConcreteState;
|
parent?: ConcreteState;
|
||||||
children: ConcreteState[];
|
|
||||||
comments: [string, string][]; // array of tuple (text-uid, text-text)
|
comments: [string, string][]; // array of tuple (text-uid, text-text)
|
||||||
entryActions: Action[];
|
entryActions: Action[];
|
||||||
exitActions: Action[];
|
exitActions: Action[];
|
||||||
depth: number;
|
depth: number;
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StableState = {
|
||||||
|
kind: "and" | "or";
|
||||||
|
children: ConcreteState[];
|
||||||
|
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;
|
||||||
|
|
||||||
export type AndState = {
|
export type AndState = {
|
||||||
kind: "and";
|
kind: "and";
|
||||||
} & AbstractState;
|
} & StableState;
|
||||||
|
|
||||||
export type OrState = {
|
export type OrState = {
|
||||||
kind: "or";
|
kind: "or";
|
||||||
// array of tuples: (uid of Arrow indicating initial state, initial state)
|
// array of tuples: (uid of Arrow indicating initial state, initial state)
|
||||||
// in a valid AST, there must be one initial state, but we allow the user to draw crazy shit
|
// in a valid AST, there must be one initial state, but we allow the user to draw crazy shit
|
||||||
initial: [string, ConcreteState][];
|
initial: [string, ConcreteState][];
|
||||||
} & AbstractState;
|
} & StableState;
|
||||||
|
|
||||||
|
export type PseudoState = {
|
||||||
|
kind: "pseudo";
|
||||||
|
uid: string;
|
||||||
|
comments: [string, string][];
|
||||||
|
};
|
||||||
|
|
||||||
export type ConcreteState = AndState | OrState;
|
export type ConcreteState = AndState | OrState;
|
||||||
|
|
||||||
export type Transition = {
|
export type Transition = {
|
||||||
uid: string;
|
uid: string; // uid of arrow in concrete syntax
|
||||||
src: ConcreteState;
|
src: ConcreteState | PseudoState;
|
||||||
tgt: ConcreteState;
|
tgt: ConcreteState | PseudoState;
|
||||||
label: ParsedText[];
|
label: ParsedText[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +51,7 @@ export type Statechart = {
|
||||||
internalEvents: EventTrigger[];
|
internalEvents: EventTrigger[];
|
||||||
outputEvents: Set<string>;
|
outputEvents: Set<string>;
|
||||||
|
|
||||||
uid2State: Map<string, ConcreteState>;
|
uid2State: Map<string, ConcreteState|PseudoState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyRoot: OrState = {
|
const emptyRoot: OrState = {
|
||||||
|
|
@ -87,6 +97,57 @@ export function isOverlapping(a: ConcreteState, b: ConcreteState): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function computeLCA(a: ConcreteState, b: ConcreteState): ConcreteState {
|
||||||
|
if (a === b) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
if (a.depth > b.depth) {
|
||||||
|
return computeLCA(a.parent!, b);
|
||||||
|
}
|
||||||
|
return computeLCA(a, b.parent!);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeLCA2(states: ConcreteState[]): ConcreteState {
|
||||||
|
if (states.length === 0) {
|
||||||
|
throw new Error("cannot compute LCA of empty set of states");
|
||||||
|
}
|
||||||
|
if (states.length === 1) {
|
||||||
|
return states[0];
|
||||||
|
}
|
||||||
|
// 2 states or more
|
||||||
|
return states.reduce((acc, cur) => computeLCA(acc, cur));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPossibleTargets(t: Transition, ts: Map<string, Transition[]>): ConcreteState[] {
|
||||||
|
if (t.tgt.kind !== "pseudo") {
|
||||||
|
return [t.tgt];
|
||||||
|
}
|
||||||
|
const pseudoOutgoing = ts.get(t.tgt.uid) || [];
|
||||||
|
return pseudoOutgoing.flatMap(t => getPossibleTargets(t, ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeArena2(t: Transition, ts: Map<string, Transition[]>): OrState {
|
||||||
|
const tgts = getPossibleTargets(t, ts);
|
||||||
|
let lca = computeLCA2([t.src as ConcreteState, ...tgts]);
|
||||||
|
while (lca.kind !== "or") {
|
||||||
|
lca = lca.parent!;
|
||||||
|
}
|
||||||
|
return lca as OrState;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming ancestor is already entered, what states to enter in order to enter descendants?
|
||||||
|
// E.g.
|
||||||
|
// root > A > B > C > D
|
||||||
|
// computePath({ancestor: A, descendant: A}) = []
|
||||||
|
// computePath({ancestor: A, descendant: C}) = [B, C]
|
||||||
|
export function computePath({ancestor, descendant}: {ancestor: ConcreteState, descendant: ConcreteState}): ConcreteState[] {
|
||||||
|
if (ancestor === descendant) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [...computePath({ancestor, descendant: descendant.parent!}), descendant];
|
||||||
|
}
|
||||||
|
|
||||||
// the arena of a transition is the lowest common ancestor state that is an OR-state
|
// the arena of a transition is the lowest common ancestor state that is an OR-state
|
||||||
// see "Deconstructing the Semantics of Big-Step Modelling Languages" by Shahram Esmaeilsabzali, 2009
|
// see "Deconstructing the Semantics of Big-Step Modelling Languages" by Shahram Esmaeilsabzali, 2009
|
||||||
export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState}): {
|
export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState}): {
|
||||||
|
|
@ -98,7 +159,7 @@ export function computeArena({src, tgt}: {src: ConcreteState, tgt: ConcreteState
|
||||||
const path = isAncestorOf({descendant: src, ancestor: tgt});
|
const path = isAncestorOf({descendant: src, ancestor: tgt});
|
||||||
if (path) {
|
if (path) {
|
||||||
if (tgt.kind === "or") {
|
if (tgt.kind === "or") {
|
||||||
return {arena: tgt, srcPath: path, tgtPath: [tgt]};
|
return {arena: tgt as OrState, srcPath: path, tgtPath: [tgt]};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// keep looking
|
// keep looking
|
||||||
|
|
@ -126,8 +187,7 @@ 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) {
|
export function stateDescription(state: ConcreteState | PseudoState) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,5 +42,10 @@ export function evalExpr(expr: Expression, environment: Environment): any {
|
||||||
const rhs = evalExpr(expr.rhs, environment);
|
const rhs = evalExpr(expr.rhs, environment);
|
||||||
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs);
|
return BINARY_OPERATOR_MAP.get(expr.operator)!(lhs, rhs);
|
||||||
}
|
}
|
||||||
|
else if (expr.kind === "call") {
|
||||||
|
const fn = evalExpr(expr.fn, environment);
|
||||||
|
const param = evalExpr(expr.param, environment);
|
||||||
|
return fn(param);
|
||||||
|
}
|
||||||
throw new Error("should never reach here");
|
throw new Error("should never reach here");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import { sides } from "../VisualEditor/VisualEditor";
|
||||||
|
|
||||||
export type Rountangle = {
|
export type Rountangle = {
|
||||||
uid: string;
|
uid: string;
|
||||||
kind: "and" | "or" | "pseudo";
|
kind: "and" | "or";
|
||||||
|
} & Rect2D;
|
||||||
|
|
||||||
|
export type Diamond = {
|
||||||
|
uid: string;
|
||||||
} & Rect2D;
|
} & Rect2D;
|
||||||
|
|
||||||
export type Text = {
|
export type Text = {
|
||||||
|
|
@ -21,6 +25,7 @@ export type VisualEditorState = {
|
||||||
rountangles: Rountangle[];
|
rountangles: Rountangle[];
|
||||||
texts: Text[];
|
texts: Text[];
|
||||||
arrows: Arrow[];
|
arrows: Arrow[];
|
||||||
|
diamonds: Diamond[];
|
||||||
nextID: number;
|
nextID: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -28,8 +33,8 @@ export type VisualEditorState = {
|
||||||
export type RountanglePart = "left" | "top" | "right" | "bottom";
|
export type RountanglePart = "left" | "top" | "right" | "bottom";
|
||||||
export type ArrowPart = "start" | "end";
|
export type ArrowPart = "start" | "end";
|
||||||
|
|
||||||
export const emptyState = {
|
export const emptyState: VisualEditorState = {
|
||||||
rountangles: [], texts: [], arrows: [], nextID: 0,
|
rountangles: [], texts: [], arrows: [], diamonds: [], nextID: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onOffStateMachine = {
|
export const onOffStateMachine = {
|
||||||
|
|
@ -45,7 +50,7 @@ export const onOffStateMachine = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// used to find which rountangle an arrow connects to (src/tgt)
|
// used to find which rountangle an arrow connects to (src/tgt)
|
||||||
export function findNearestRountangleSide(arrow: Line2D, arrowPart: "start" | "end", candidates: Rountangle[]): {uid: string, part: RountanglePart} | undefined {
|
export function findNearestSide(arrow: Line2D, arrowPart: "start" | "end", candidates: (Rountangle|Diamond)[]): {uid: string, part: RountanglePart} | undefined {
|
||||||
let best = Infinity;
|
let best = Infinity;
|
||||||
let bestSide: undefined | {uid: string, part: RountanglePart};
|
let bestSide: undefined | {uid: string, part: RountanglePart};
|
||||||
for (const rountangle of candidates) {
|
for (const rountangle of candidates) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
import { computeArena2, computePath, ConcreteState, getDescendants, isOverlapping, OrState, StableState, Statechart, stateDescription, Transition } from "./abstract_syntax";
|
||||||
import { evalExpr } from "./actionlang_interpreter";
|
import { evalExpr } from "./actionlang_interpreter";
|
||||||
import { computeArena, ConcreteState, getDescendants, isOverlapping, OrState, Statechart, stateDescription, Transition } from "./abstract_syntax";
|
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
|
||||||
import { Action, AfterTrigger, EventTrigger } from "./label_ast";
|
import { BigStepOutput, Environment, initialRaised, Mode, RaisedEvents, RT_Event, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types";
|
||||||
import { Environment, RaisedEvents, Mode, RT_Statechart, initialRaised, BigStepOutput, Timers, RT_Event, TimerElapseEvent } from "./runtime_types";
|
|
||||||
|
|
||||||
export function initialize(ast: Statechart): BigStepOutput {
|
export function initialize(ast: Statechart): BigStepOutput {
|
||||||
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
|
let {enteredStates, environment, ...raised} = enterDefault(0, ast.root, {
|
||||||
environment: new Environment(),
|
environment: new Environment([new Map([["_timers", []]])]),
|
||||||
...initialRaised,
|
...initialRaised,
|
||||||
});
|
});
|
||||||
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
|
return handleInternalEvents(0, ast, {mode: enteredStates, environment, ...raised});
|
||||||
|
|
@ -18,12 +18,15 @@ type ActionScope = {
|
||||||
type EnteredScope = { enteredStates: Mode } & ActionScope;
|
type EnteredScope = { enteredStates: Mode } & ActionScope;
|
||||||
|
|
||||||
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): 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) {
|
for (const action of state.entryActions) {
|
||||||
(actionScope = execAction(action, actionScope));
|
({environment, ...rest} = execAction(action, {environment, ...rest}));
|
||||||
}
|
}
|
||||||
// schedule timers
|
// schedule timers
|
||||||
// we store timers in the environment (dirty!)
|
// we store timers in the environment (dirty!)
|
||||||
let environment = actionScope.environment.transform<Timers>("_timers", oldTimers => {
|
environment = environment.transform<Timers>("_timers", oldTimers => {
|
||||||
const newTimers = [
|
const newTimers = [
|
||||||
...oldTimers,
|
...oldTimers,
|
||||||
...state.timers.map(timeOffset => {
|
...state.timers.map(timeOffset => {
|
||||||
|
|
@ -35,20 +38,21 @@ export function entryActions(simtime: number, state: ConcreteState, actionScope:
|
||||||
return newTimers;
|
return newTimers;
|
||||||
}, []);
|
}, []);
|
||||||
// new nested scope
|
// new nested scope
|
||||||
environment = environment.pushScope();
|
return {environment, ...rest};
|
||||||
return {...actionScope, environment};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exitActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
|
export function exitActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
|
||||||
|
// console.log('exit', stateDescription(state), '...');
|
||||||
for (const action of state.exitActions) {
|
for (const action of state.exitActions) {
|
||||||
(actionScope = execAction(action, actionScope));
|
(actionScope = execAction(action, actionScope));
|
||||||
}
|
}
|
||||||
let environment = actionScope.environment.popScope();
|
let environment = actionScope.environment;
|
||||||
// cancel timers
|
// cancel timers
|
||||||
environment = environment.transform<Timers>("_timers", oldTimers => {
|
environment = environment.transform<Timers>("_timers", oldTimers => {
|
||||||
// remove all timers of 'state':
|
// remove all timers of 'state':
|
||||||
return oldTimers.filter(([_, {state: s}]) => s !== state.uid);
|
return oldTimers.filter(([_, {state: s}]) => s !== state.uid);
|
||||||
}, []);
|
}, []);
|
||||||
|
environment = environment.popScope();
|
||||||
return {...actionScope, environment};
|
return {...actionScope, environment};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,42 +197,42 @@ 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: ConcreteState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
export function handleEvent(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {environment, mode, ...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)) {
|
||||||
const outgoing = statechart.transitions.get(state.uid) || [];
|
const outgoing = statechart.transitions.get(state.uid) || [];
|
||||||
|
const labels = outgoing.flatMap(t =>
|
||||||
|
t.label
|
||||||
|
.filter(l => l.kind === "transitionLabel")
|
||||||
|
.map(l => [t,l] as [Transition, TransitionLabel]));
|
||||||
let triggered;
|
let triggered;
|
||||||
if (event.kind === "input") {
|
if (event.kind === "input") {
|
||||||
// get transitions triggered by event
|
// get transitions triggered by event
|
||||||
triggered = outgoing.filter(transition => {
|
triggered = labels.filter(([_t,l]) =>
|
||||||
const trigger = transition.label[0].trigger;
|
l.trigger.kind === "event" && l.trigger.event === event.name);
|
||||||
if (trigger.kind === "event") {
|
|
||||||
return trigger.event === event.name;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// get transitions triggered by timeout
|
// get transitions triggered by timeout
|
||||||
triggered = outgoing.filter(transition => {
|
triggered = labels.filter(([_t,l]) =>
|
||||||
const trigger = transition.label[0].trigger;
|
l.trigger.kind === "after" && l.trigger.durationMs === event.timeDurMs);
|
||||||
if (trigger.kind === "after") {
|
|
||||||
return trigger.durationMs === event.timeDurMs;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// eval guard
|
// eval guard
|
||||||
const enabled = triggered.filter(transition =>
|
const guardEnvironment = environment.set("inState", (stateLabel: string) => {
|
||||||
evalExpr(transition.label[0].guard, environment)
|
for (const [uid, state] of statechart.uid2State.entries()) {
|
||||||
);
|
if (stateDescription(state) === stateLabel) {
|
||||||
|
return (mode.has(uid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const enabled = triggered.filter(([t,l]) =>
|
||||||
|
evalExpr(l.guard, guardEnvironment));
|
||||||
if (enabled.length > 0) {
|
if (enabled.length > 0) {
|
||||||
if (enabled.length > 1) {
|
if (enabled.length > 1) {
|
||||||
console.warn('nondeterminism!!!!');
|
console.warn('nondeterminism!!!!');
|
||||||
}
|
}
|
||||||
const t = enabled[0];
|
const [t,l] = enabled[0]; // just pick one transition
|
||||||
const {arena, srcPath, tgtPath} = computeArena(t);
|
const arena = computeArena2(t, statechart.transitions);
|
||||||
let overlapping = false;
|
let overlapping = false;
|
||||||
for (const alreadyFired of arenasFired) {
|
for (const alreadyFired of arenasFired) {
|
||||||
if (isOverlapping(arena, alreadyFired)) {
|
if (isOverlapping(arena, alreadyFired)) {
|
||||||
|
|
@ -236,20 +240,18 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!overlapping) {
|
if (!overlapping) {
|
||||||
let oldValue;
|
|
||||||
if (event.kind === "input" && event.param !== undefined) {
|
if (event.kind === "input" && event.param !== undefined) {
|
||||||
// input events may have a parameter
|
// input events may have a parameter
|
||||||
// add event parameter to environment in new scope
|
// add event parameter to environment in new scope
|
||||||
environment = environment.pushScope();
|
environment = environment.pushScope();
|
||||||
environment = environment.newVar(
|
environment = environment.newVar(
|
||||||
(t.label[0].trigger as EventTrigger).paramName as string,
|
(l.trigger as EventTrigger).paramName as string,
|
||||||
event.param,
|
event.param,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised}));
|
({mode, environment, ...raised} = fireTransition2(simtime, t, statechart.transitions, l, arena, {mode, environment, ...raised}));
|
||||||
if (event.kind === "input" && event.param) {
|
if (event.kind === "input" && event.param !== undefined) {
|
||||||
environment = environment.popScope();
|
environment = environment.popScope();
|
||||||
// console.log('restored environment:', environment);
|
|
||||||
}
|
}
|
||||||
arenasFired.add(arena);
|
arenasFired.add(arena);
|
||||||
}
|
}
|
||||||
|
|
@ -288,27 +290,54 @@ function transitionDescription(t: Transition) {
|
||||||
return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt);
|
return stateDescription(t.src) + ' ➔ ' + stateDescription(t.tgt);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fireTransition(simtime: number, t: Transition, arena: OrState, srcPath: ConcreteState[], tgtPath: ConcreteState[], {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
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), {arena, srcPath, tgtPath});
|
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}).reverse();
|
||||||
|
|
||||||
// exit src
|
// exit src and other states up to arena
|
||||||
console.log('exit src...');
|
({environment, ...raised} = exitPath(simtime, srcPath, {environment, enteredStates: mode, ...raised}));
|
||||||
({environment, ...raised} = exitPath(simtime, srcPath.slice(1), {environment, enteredStates: mode, ...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);
|
const exitedMode = mode.difference(toExit); // active states after exiting the states we need to exit
|
||||||
|
|
||||||
|
// console.log({exitedMode});
|
||||||
|
|
||||||
|
return fireSecondHalfOfTransition(simtime, t, ts, label, arena, {mode: exitedMode, environment, ...raised});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
export function fireSecondHalfOfTransition(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||||
// exec transition actions
|
// exec transition actions
|
||||||
for (const action of t.label[0].actions) {
|
for (const action of label.actions) {
|
||||||
({environment, ...raised} = execAction(action, {environment, ...raised}));
|
({environment, ...raised} = execAction(action, {environment, ...raised}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// enter tgt
|
if (t.tgt.kind === "pseudo") {
|
||||||
console.log('enter tgt...');
|
const outgoing = ts.get(t.tgt.uid) || [];
|
||||||
let enteredStates;
|
for (const nextT of outgoing) {
|
||||||
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath.slice(1), {environment, ...raised}));
|
for (const nextLabel of nextT.label) {
|
||||||
const enteredMode = exitedMode.union(enteredStates);
|
if (nextLabel.kind === "transitionLabel") {
|
||||||
|
if (evalExpr(nextLabel.guard, environment)) {
|
||||||
|
console.log('fire', transitionDescription(nextT));
|
||||||
|
// found ourselves an enabled transition
|
||||||
|
return fireSecondHalfOfTransition(simtime, nextT, ts, nextLabel, arena, {mode, environment, ...raised});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("stuck in pseudo-state!!")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
||||||
|
// enter tgt
|
||||||
|
let enteredStates;
|
||||||
|
({enteredStates, environment, ...raised} = enterPath(simtime, tgtPath, {environment, ...raised}));
|
||||||
|
const enteredMode = mode.union(enteredStates);
|
||||||
|
|
||||||
return {mode: enteredMode, environment, ...raised};
|
// console.log({enteredMode});
|
||||||
|
|
||||||
|
return {mode: enteredMode, environment, ...raised};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,11 @@ export type ParserError = {
|
||||||
uid: string; // uid of the text node
|
uid: string; // uid of the text node
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Trigger = EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
|
export type Trigger = TriggerLess | EventTrigger | AfterTrigger | EntryTrigger | ExitTrigger;
|
||||||
|
|
||||||
|
export type TriggerLess = {
|
||||||
|
kind: "triggerless";
|
||||||
|
}
|
||||||
|
|
||||||
export type EventTrigger = {
|
export type EventTrigger = {
|
||||||
kind: "event";
|
kind: "event";
|
||||||
|
|
@ -55,7 +59,7 @@ export type RaiseEvent = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type Expression = BinaryExpression | UnaryExpression | VarRef | Literal;
|
export type Expression = BinaryExpression | UnaryExpression | VarRef | Literal | FunctionCall;
|
||||||
|
|
||||||
export type BinaryExpression = {
|
export type BinaryExpression = {
|
||||||
kind: "binaryExpr";
|
kind: "binaryExpr";
|
||||||
|
|
|
||||||
|
|
@ -211,9 +211,9 @@ function peg$parse(input, options) {
|
||||||
const peg$e13 = peg$classExpectation([["0", "9"]], false, false, false);
|
const peg$e13 = peg$classExpectation([["0", "9"]], false, false, false);
|
||||||
const peg$e14 = peg$literalExpectation("==", false);
|
const peg$e14 = peg$literalExpectation("==", false);
|
||||||
const peg$e15 = peg$literalExpectation("!=", false);
|
const peg$e15 = peg$literalExpectation("!=", false);
|
||||||
const peg$e16 = peg$classExpectation(["<", ">"], false, false, false);
|
const peg$e16 = peg$literalExpectation("<=", false);
|
||||||
const peg$e17 = peg$literalExpectation("<=", false);
|
const peg$e17 = peg$literalExpectation(">=", false);
|
||||||
const peg$e18 = peg$literalExpectation(">=", false);
|
const peg$e18 = peg$classExpectation(["<", ">"], false, false, false);
|
||||||
const peg$e19 = peg$classExpectation(["+", "-"], false, false, false);
|
const peg$e19 = peg$classExpectation(["+", "-"], false, false, false);
|
||||||
const peg$e20 = peg$classExpectation(["*", "/"], false, false, false);
|
const peg$e20 = peg$classExpectation(["*", "/"], false, false, false);
|
||||||
const peg$e21 = peg$literalExpectation("true", false);
|
const peg$e21 = peg$literalExpectation("true", false);
|
||||||
|
|
@ -229,7 +229,7 @@ function peg$parse(input, options) {
|
||||||
function peg$f0(trigger, guard, actions) {
|
function peg$f0(trigger, guard, actions) {
|
||||||
return {
|
return {
|
||||||
kind: "transitionLabel",
|
kind: "transitionLabel",
|
||||||
trigger,
|
trigger: trigger ? trigger : {kind: "triggerless"},
|
||||||
guard: guard ? guard[2] : {kind: "literal", value: true},
|
guard: guard ? guard[2] : {kind: "literal", value: true},
|
||||||
actions: actions ? actions[2] : [],
|
actions: actions ? actions[2] : [],
|
||||||
};
|
};
|
||||||
|
|
@ -502,9 +502,9 @@ function peg$parse(input, options) {
|
||||||
function peg$parsestart() {
|
function peg$parsestart() {
|
||||||
let s0;
|
let s0;
|
||||||
|
|
||||||
s0 = peg$parsetlabel();
|
s0 = peg$parsecomment();
|
||||||
if (s0 === peg$FAILED) {
|
if (s0 === peg$FAILED) {
|
||||||
s0 = peg$parsecomment();
|
s0 = peg$parsetlabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
return s0;
|
return s0;
|
||||||
|
|
@ -516,35 +516,33 @@ function peg$parse(input, options) {
|
||||||
s0 = peg$currPos;
|
s0 = peg$currPos;
|
||||||
s1 = peg$parse_();
|
s1 = peg$parse_();
|
||||||
s2 = peg$parsetrigger();
|
s2 = peg$parsetrigger();
|
||||||
if (s2 !== peg$FAILED) {
|
if (s2 === peg$FAILED) {
|
||||||
s3 = peg$parse_();
|
s2 = null;
|
||||||
s4 = peg$currPos;
|
}
|
||||||
if (input.charCodeAt(peg$currPos) === 91) {
|
s3 = peg$parse_();
|
||||||
s5 = peg$c0;
|
s4 = peg$currPos;
|
||||||
peg$currPos++;
|
if (input.charCodeAt(peg$currPos) === 91) {
|
||||||
} else {
|
s5 = peg$c0;
|
||||||
s5 = peg$FAILED;
|
peg$currPos++;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
} else {
|
||||||
}
|
s5 = peg$FAILED;
|
||||||
if (s5 !== peg$FAILED) {
|
if (peg$silentFails === 0) { peg$fail(peg$e0); }
|
||||||
s6 = peg$parse_();
|
}
|
||||||
s7 = peg$parsecompare();
|
if (s5 !== peg$FAILED) {
|
||||||
if (s7 !== peg$FAILED) {
|
s6 = peg$parse_();
|
||||||
s8 = peg$parse_();
|
s7 = peg$parsecompare();
|
||||||
if (input.charCodeAt(peg$currPos) === 93) {
|
if (s7 !== peg$FAILED) {
|
||||||
s9 = peg$c1;
|
s8 = peg$parse_();
|
||||||
peg$currPos++;
|
if (input.charCodeAt(peg$currPos) === 93) {
|
||||||
} else {
|
s9 = peg$c1;
|
||||||
s9 = peg$FAILED;
|
peg$currPos++;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
} else {
|
||||||
}
|
s9 = peg$FAILED;
|
||||||
if (s9 !== peg$FAILED) {
|
if (peg$silentFails === 0) { peg$fail(peg$e1); }
|
||||||
s5 = [s5, s6, s7, s8, s9];
|
}
|
||||||
s4 = s5;
|
if (s9 !== peg$FAILED) {
|
||||||
} else {
|
s5 = [s5, s6, s7, s8, s9];
|
||||||
peg$currPos = s4;
|
s4 = s5;
|
||||||
s4 = peg$FAILED;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s4;
|
peg$currPos = s4;
|
||||||
s4 = peg$FAILED;
|
s4 = peg$FAILED;
|
||||||
|
|
@ -553,42 +551,42 @@ function peg$parse(input, options) {
|
||||||
peg$currPos = s4;
|
peg$currPos = s4;
|
||||||
s4 = peg$FAILED;
|
s4 = peg$FAILED;
|
||||||
}
|
}
|
||||||
if (s4 === peg$FAILED) {
|
} else {
|
||||||
s4 = null;
|
peg$currPos = s4;
|
||||||
}
|
s4 = peg$FAILED;
|
||||||
s5 = peg$parse_();
|
}
|
||||||
s6 = peg$currPos;
|
if (s4 === peg$FAILED) {
|
||||||
if (input.charCodeAt(peg$currPos) === 47) {
|
s4 = null;
|
||||||
s7 = peg$c2;
|
}
|
||||||
peg$currPos++;
|
s5 = peg$parse_();
|
||||||
} else {
|
s6 = peg$currPos;
|
||||||
s7 = peg$FAILED;
|
if (input.charCodeAt(peg$currPos) === 47) {
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e2); }
|
s7 = peg$c2;
|
||||||
}
|
peg$currPos++;
|
||||||
if (s7 !== peg$FAILED) {
|
} else {
|
||||||
s8 = peg$parse_();
|
s7 = peg$FAILED;
|
||||||
s9 = peg$parseactions();
|
if (peg$silentFails === 0) { peg$fail(peg$e2); }
|
||||||
if (s9 !== peg$FAILED) {
|
}
|
||||||
s7 = [s7, s8, s9];
|
if (s7 !== peg$FAILED) {
|
||||||
s6 = s7;
|
s8 = peg$parse_();
|
||||||
} else {
|
s9 = peg$parseactions();
|
||||||
peg$currPos = s6;
|
if (s9 !== peg$FAILED) {
|
||||||
s6 = peg$FAILED;
|
s7 = [s7, s8, s9];
|
||||||
}
|
s6 = s7;
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s6;
|
peg$currPos = s6;
|
||||||
s6 = peg$FAILED;
|
s6 = peg$FAILED;
|
||||||
}
|
}
|
||||||
if (s6 === peg$FAILED) {
|
|
||||||
s6 = null;
|
|
||||||
}
|
|
||||||
s7 = peg$parse_();
|
|
||||||
peg$savedPos = s0;
|
|
||||||
s0 = peg$f0(s2, s4, s6);
|
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s6;
|
||||||
s0 = peg$FAILED;
|
s6 = peg$FAILED;
|
||||||
}
|
}
|
||||||
|
if (s6 === peg$FAILED) {
|
||||||
|
s6 = null;
|
||||||
|
}
|
||||||
|
s7 = peg$parse_();
|
||||||
|
peg$savedPos = s0;
|
||||||
|
s0 = peg$f0(s2, s4, s6);
|
||||||
|
|
||||||
return s0;
|
return s0;
|
||||||
}
|
}
|
||||||
|
|
@ -1002,25 +1000,25 @@ function peg$parse(input, options) {
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e15); }
|
if (peg$silentFails === 0) { peg$fail(peg$e15); }
|
||||||
}
|
}
|
||||||
if (s5 === peg$FAILED) {
|
if (s5 === peg$FAILED) {
|
||||||
s5 = input.charAt(peg$currPos);
|
if (input.substr(peg$currPos, 2) === peg$c14) {
|
||||||
if (peg$r2.test(s5)) {
|
s5 = peg$c14;
|
||||||
peg$currPos++;
|
peg$currPos += 2;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e16); }
|
if (peg$silentFails === 0) { peg$fail(peg$e16); }
|
||||||
}
|
}
|
||||||
if (s5 === peg$FAILED) {
|
if (s5 === peg$FAILED) {
|
||||||
if (input.substr(peg$currPos, 2) === peg$c14) {
|
if (input.substr(peg$currPos, 2) === peg$c15) {
|
||||||
s5 = peg$c14;
|
s5 = peg$c15;
|
||||||
peg$currPos += 2;
|
peg$currPos += 2;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
if (peg$silentFails === 0) { peg$fail(peg$e17); }
|
||||||
}
|
}
|
||||||
if (s5 === peg$FAILED) {
|
if (s5 === peg$FAILED) {
|
||||||
if (input.substr(peg$currPos, 2) === peg$c15) {
|
s5 = input.charAt(peg$currPos);
|
||||||
s5 = peg$c15;
|
if (peg$r2.test(s5)) {
|
||||||
peg$currPos += 2;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s5 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
if (peg$silentFails === 0) { peg$fail(peg$e18); }
|
||||||
|
|
@ -1474,8 +1472,16 @@ function peg$parse(input, options) {
|
||||||
}
|
}
|
||||||
if (s1 !== peg$FAILED) {
|
if (s1 !== peg$FAILED) {
|
||||||
s2 = peg$parse_();
|
s2 = peg$parse_();
|
||||||
if (s2 !== peg$FAILED) {
|
s3 = [];
|
||||||
s3 = [];
|
if (input.length > peg$currPos) {
|
||||||
|
s4 = input.charAt(peg$currPos);
|
||||||
|
peg$currPos++;
|
||||||
|
} else {
|
||||||
|
s4 = peg$FAILED;
|
||||||
|
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||||
|
}
|
||||||
|
while (s4 !== peg$FAILED) {
|
||||||
|
s3.push(s4);
|
||||||
if (input.length > peg$currPos) {
|
if (input.length > peg$currPos) {
|
||||||
s4 = input.charAt(peg$currPos);
|
s4 = input.charAt(peg$currPos);
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
|
|
@ -1483,54 +1489,36 @@ function peg$parse(input, options) {
|
||||||
s4 = peg$FAILED;
|
s4 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||||
}
|
}
|
||||||
while (s4 !== peg$FAILED) {
|
}
|
||||||
s3.push(s4);
|
s4 = peg$parse_();
|
||||||
if (input.length > peg$currPos) {
|
if (input.charCodeAt(peg$currPos) === 10) {
|
||||||
s4 = input.charAt(peg$currPos);
|
s5 = peg$c21;
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
} else {
|
||||||
s4 = peg$FAILED;
|
s5 = peg$FAILED;
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
||||||
}
|
}
|
||||||
}
|
if (s5 === peg$FAILED) {
|
||||||
s4 = peg$parse_();
|
s5 = peg$currPos;
|
||||||
if (s4 !== peg$FAILED) {
|
peg$silentFails++;
|
||||||
if (input.charCodeAt(peg$currPos) === 10) {
|
if (input.length > peg$currPos) {
|
||||||
s5 = peg$c21;
|
s6 = input.charAt(peg$currPos);
|
||||||
peg$currPos++;
|
peg$currPos++;
|
||||||
} else {
|
|
||||||
s5 = peg$FAILED;
|
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e29); }
|
|
||||||
}
|
|
||||||
if (s5 === peg$FAILED) {
|
|
||||||
s5 = peg$currPos;
|
|
||||||
peg$silentFails++;
|
|
||||||
if (input.length > peg$currPos) {
|
|
||||||
s6 = input.charAt(peg$currPos);
|
|
||||||
peg$currPos++;
|
|
||||||
} else {
|
|
||||||
s6 = peg$FAILED;
|
|
||||||
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
|
||||||
}
|
|
||||||
peg$silentFails--;
|
|
||||||
if (s6 === peg$FAILED) {
|
|
||||||
s5 = undefined;
|
|
||||||
} else {
|
|
||||||
peg$currPos = s5;
|
|
||||||
s5 = peg$FAILED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (s5 !== peg$FAILED) {
|
|
||||||
peg$savedPos = s0;
|
|
||||||
s0 = peg$f22(s3);
|
|
||||||
} else {
|
|
||||||
peg$currPos = s0;
|
|
||||||
s0 = peg$FAILED;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
s6 = peg$FAILED;
|
||||||
s0 = peg$FAILED;
|
if (peg$silentFails === 0) { peg$fail(peg$e28); }
|
||||||
}
|
}
|
||||||
|
peg$silentFails--;
|
||||||
|
if (s6 === peg$FAILED) {
|
||||||
|
s5 = undefined;
|
||||||
|
} else {
|
||||||
|
peg$currPos = s5;
|
||||||
|
s5 = peg$FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s5 !== peg$FAILED) {
|
||||||
|
peg$savedPos = s0;
|
||||||
|
s0 = peg$f22(s3);
|
||||||
} else {
|
} else {
|
||||||
peg$currPos = s0;
|
peg$currPos = s0;
|
||||||
s0 = peg$FAILED;
|
s0 = peg$FAILED;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ConcreteState, OrState, Statechart, Transition } from "./abstract_syntax";
|
import { AbstractState, ConcreteState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax";
|
||||||
import { findNearestArrow, findNearestRountangleSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax";
|
import { findNearestArrow, findNearestSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax";
|
||||||
import { isEntirelyWithin } from "../VisualEditor/geometry";
|
import { isEntirelyWithin } from "../VisualEditor/geometry";
|
||||||
import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
|
import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
timers: [],
|
timers: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid2State = new Map<string, ConcreteState>([["root", root]]);
|
const uid2State = new Map<string, ConcreteState|PseudoState>([["root", root]]);
|
||||||
|
|
||||||
// we will always look for the smallest parent rountangle
|
// we will always look for the smallest parent rountangle
|
||||||
const parentCandidates: Rountangle[] = [{
|
const parentCandidates: Rountangle[] = [{
|
||||||
|
|
@ -59,37 +59,59 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
|
|
||||||
// step 1: figure out state hierarchy
|
// step 1: figure out state hierarchy
|
||||||
|
|
||||||
// we assume that the rountangles are sorted from big to small:
|
// IMPORTANT ASSUMPTION: state.rountangles is sorted from big to small surface area:
|
||||||
for (const rt of state.rountangles) {
|
for (const rt of state.rountangles) {
|
||||||
// @ts-ignore
|
const common = {
|
||||||
const state: ConcreteState = {
|
|
||||||
kind: rt.kind,
|
kind: rt.kind,
|
||||||
uid: rt.uid,
|
uid: rt.uid,
|
||||||
children: [],
|
|
||||||
comments: [],
|
comments: [],
|
||||||
entryActions: [],
|
entryActions: [],
|
||||||
exitActions: [],
|
exitActions: [],
|
||||||
timers: [],
|
|
||||||
};
|
};
|
||||||
if (state.kind === "or") {
|
|
||||||
(state as unknown as OrState).initial = [];
|
let state;
|
||||||
|
if (rt.kind === "or") {
|
||||||
|
state = {
|
||||||
|
...common,
|
||||||
|
initial: [],
|
||||||
|
children: [],
|
||||||
|
timers: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (rt.kind === "and") {
|
||||||
|
state = {
|
||||||
|
...common,
|
||||||
|
children: [],
|
||||||
|
timers: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
uid2State.set(rt.uid, (state));
|
|
||||||
|
|
||||||
// iterate in reverse:
|
// iterate in reverse:
|
||||||
for (let i=parentCandidates.length-1; i>=0; i--) {
|
for (let i=parentCandidates.length-1; i>=0; i--) {
|
||||||
const candidate = parentCandidates[i];
|
const candidate = parentCandidates[i];
|
||||||
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
|
if (candidate.uid === "root" || isEntirelyWithin(rt, candidate)) {
|
||||||
// found our parent :)
|
// found our parent
|
||||||
const parentState = uid2State.get(candidate.uid)!;
|
const parentState = uid2State.get(candidate.uid)! as ConcreteState;
|
||||||
parentState.children.push(state as unknown 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, candidate.uid);
|
||||||
state.parent = parentState;
|
state = {
|
||||||
state.depth = parentState.depth+1;
|
...state,
|
||||||
|
parent: parentState,
|
||||||
|
depth: parentState.depth + 1,
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
uid2State.set(rt.uid, state as ConcreteState);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of state.diamonds) {
|
||||||
|
uid2State.set(d.uid, {
|
||||||
|
kind: "pseudo",
|
||||||
|
uid: d.uid,
|
||||||
|
comments: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// step 2: figure out transitions
|
// step 2: figure out transitions
|
||||||
|
|
@ -98,27 +120,37 @@ 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 srcUID = findNearestRountangleSide(arr, "start", state.rountangles)?.uid;
|
const sides = [...state.rountangles, ...state.diamonds];
|
||||||
const tgtUID = findNearestRountangleSide(arr, "end", state.rountangles)?.uid;
|
const srcUID = findNearestSide(arr, "start", sides)?.uid;
|
||||||
|
const tgtUID = findNearestSide(arr, "end", sides)?.uid;
|
||||||
if (!srcUID) {
|
if (!srcUID) {
|
||||||
if (!tgtUID) {
|
if (!tgtUID) {
|
||||||
// dangling edge - todo: display error...
|
// dangling edge
|
||||||
errors.push({shapeUid: arr.uid, message: "dangling"});
|
errors.push({shapeUid: arr.uid, message: "dangling"});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// target but no source, so we treat is as an 'initial' marking
|
// target but no source, so we treat is as an 'initial' marking
|
||||||
const initialState = uid2State.get(tgtUID)!;
|
const tgtState = uid2State.get(tgtUID)!;
|
||||||
const ofState = uid2State.get(parentLinks.get(tgtUID)!)!;
|
if (tgtState.kind === "pseudo") {
|
||||||
if (ofState.kind === "or") {
|
// maybe allow this in the future?
|
||||||
ofState.initial.push([arr.uid, initialState]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// and states do not have an 'initial' state - todo: display error...
|
|
||||||
errors.push({
|
errors.push({
|
||||||
shapeUid: arr.uid,
|
shapeUid: arr.uid,
|
||||||
message: "AND-state cannot have an initial state",
|
message: "pseudo-state cannot be initial state",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
const ofState = uid2State.get(parentLinks.get(tgtUID)!)!;
|
||||||
|
if (ofState.kind === "or") {
|
||||||
|
ofState.initial.push([arr.uid, tgtState]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// and states do not have an 'initial' state
|
||||||
|
errors.push({
|
||||||
|
shapeUid: arr.uid,
|
||||||
|
message: "AND-state cannot have an initial state",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -194,26 +226,42 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
if (belongsToArrow) {
|
if (belongsToArrow) {
|
||||||
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
||||||
if (belongsToTransition) {
|
if (belongsToTransition) {
|
||||||
|
const {src} = belongsToTransition;
|
||||||
belongsToTransition.label.push(parsed);
|
belongsToTransition.label.push(parsed);
|
||||||
if (parsed.kind === "transitionLabel") {
|
if (parsed.kind === "transitionLabel") {
|
||||||
// collect events
|
// collect events
|
||||||
// triggers
|
// triggers
|
||||||
if (parsed.trigger.kind === "event") {
|
if (parsed.trigger.kind === "event") {
|
||||||
const {event} = parsed.trigger;
|
if (src.kind === "pseudo") {
|
||||||
if (event.startsWith("_")) {
|
errors.push({shapeUid: text.uid, message: "pseudo state outgoing transition must not have event trigger"});
|
||||||
errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid));
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid));
|
const {event} = parsed.trigger;
|
||||||
|
if (event.startsWith("_")) {
|
||||||
|
errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (parsed.trigger.kind === "after") {
|
else if (parsed.trigger.kind === "after") {
|
||||||
belongsToTransition.src.timers.push(parsed.trigger.durationMs);
|
if (src.kind === "pseudo") {
|
||||||
belongsToTransition.src.timers.sort();
|
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 (["entry", "exit"].includes(parsed.trigger.kind)) {
|
else if (["entry", "exit"].includes(parsed.trigger.kind)) {
|
||||||
errors.push({shapeUid: text.uid, message: "entry/exit trigger not allowed on transitions"});
|
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
|
// // raise-actions
|
||||||
// for (const action of parsed.actions) {
|
// for (const action of parsed.actions) {
|
||||||
|
|
@ -240,7 +288,7 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
// 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 rountangle = findRountangle(text.topLeft, state.rountangles);
|
||||||
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! : root;
|
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! 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
|
||||||
|
|
@ -257,7 +305,6 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
data: {start: {offset: 0}, end: {offset: text.text.length}},
|
data: {start: {offset: 0}, end: {offset: text.text.length}},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (parsed.kind === "comment") {
|
else if (parsed.kind === "comment") {
|
||||||
// just append comments to their respective states
|
// just append comments to their respective states
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
start = tlabel / comment
|
start = comment / tlabel
|
||||||
|
|
||||||
tlabel = _ trigger:trigger _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
|
tlabel = _ trigger:trigger? _ guard:("[" _ guard _ "]")? _ actions:("/" _ actions )? _ {
|
||||||
return {
|
return {
|
||||||
kind: "transitionLabel",
|
kind: "transitionLabel",
|
||||||
trigger,
|
trigger: trigger ? trigger : {kind: "triggerless"},
|
||||||
guard: guard ? guard[2] : {kind: "literal", value: true},
|
guard: guard ? guard[2] : {kind: "literal", value: true},
|
||||||
actions: actions ? actions[2] : [],
|
actions: actions ? actions[2] : [],
|
||||||
};
|
};
|
||||||
|
|
@ -57,7 +57,7 @@ number = [0-9]+ {
|
||||||
|
|
||||||
expr = compare
|
expr = compare
|
||||||
|
|
||||||
compare = sum:sum rest:((_ ("==" / "!=" / "<" / ">" / "<=" / ">=") _) compare)? {
|
compare = sum:sum rest:((_ ("==" / "!=" / "<=" / ">=" / "<" / ">") _) compare)? {
|
||||||
if (rest === null) {
|
if (rest === null) {
|
||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue