diff --git a/src/App/App.tsx b/src/App/App.tsx index 692feb2..7450429 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import { emptyStatechart, Statechart } from "../statecharts/abstract_syntax"; import { handleInputEvent, initialize } from "../statecharts/interpreter"; import { BigStep, BigStepOutput } from "../statecharts/runtime_types"; -import { VisualEditor } from "../VisualEditor/VisualEditor"; +import { InsertMode, VisualEditor } from "../VisualEditor/VisualEditor"; import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time"; import "../index.css"; @@ -16,6 +16,8 @@ import { AST } from "./AST"; import { TraceableError } from "../statecharts/parser"; export function App() { + const [mode, setMode] = useState("and"); + const [ast, setAST] = useState(emptyStatechart); const [errors, setErrors] = useState([]); @@ -38,10 +40,10 @@ export function App() { setTime({kind: "paused", simtime: 0}); } - function onRaise(inputEvent: string) { - if (rt.length>0 && rtIdx!==undefined && ast.inputEvents.has(inputEvent)) { + function onRaise(inputEvent: string, param: any) { + if (rt.length>0 && rtIdx!==undefined && ast.inputEvents.some(e => e.event === inputEvent)) { const simtime = getSimTime(time, performance.now()); - const nextConfig = handleInputEvent(simtime, {kind: "input", name: inputEvent}, ast, rt[rtIdx]!); + const nextConfig = handleInputEvent(simtime, {kind: "input", name: inputEvent, param}, ast, rt[rtIdx]!); appendNewConfig(inputEvent, simtime, nextConfig); } } @@ -92,13 +94,13 @@ export function App() { }}> {/* main */} - + {/* right sidebar */} >, onInit: () => void, onClear: () => void, - onRaise: (e: string) => void, + onRaise: (e: string, p: any) => void, ast: Statechart, + mode: InsertMode, + setMode: Dispatch>, } -export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast}: TopPanelProps) { +function RountangleIcon(props: {kind: string}) { + return + + ; +} + +function PseudoStateIcon(props: {}) { + return + + + + ; +} + +export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast, mode, setMode}: TopPanelProps) { const [displayTime, setDisplayTime] = useState("0.000"); const [timescale, setTimescale] = useState(1); @@ -74,7 +99,22 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast}: Top const timers: Timers = (rt?.environment.get("_timers") || []); const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0]; - return
+ return <> +
+ {([ + ["and", ], + ["or", ], + ["pseudo", ], + ["transition", ], + ["text", <>T], + ] as [InsertMode, ReactElement][]).map(([m, buttonTxt]) => + )} +
+   +
@@ -83,6 +123,11 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast}: Top + {/* onChangePaused(newValue==="paused", performance.now())} size="small"> + + + */} +     @@ -92,19 +137,6 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast}: Top   - {ast.inputEvents && - <> - {[...ast.inputEvents].map(event => )} -   - } - - {/* onChangePaused(newValue==="paused", performance.now())} size="small"> - - - */} - -   -   @@ -123,5 +155,29 @@ export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast}: Top } }); }}> -
; + + {ast.inputEvents && + <> + {ast.inputEvents.map(({event, paramName}) => + <> {paramName && <>})} + + } + +
; } diff --git a/src/VisualEditor/ArrowSVG.tsx b/src/VisualEditor/ArrowSVG.tsx new file mode 100644 index 0000000..44496c1 --- /dev/null +++ b/src/VisualEditor/ArrowSVG.tsx @@ -0,0 +1,53 @@ +import { Arrow } from "../statecharts/concrete_syntax"; +import { ArcDirection, euclideanDistance } from "./geometry"; +import { CORNER_HELPER_RADIUS } from "./parameters"; + + +export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: string[]; highlight: boolean; arc: ArcDirection; }) { + const { start, end, uid } = props.arrow; + const radius = euclideanDistance(start, end) / 1.6; + const largeArc = "1"; + const arcOrLine = props.arc === "no" ? "L" : + `A ${radius} ${radius} 0 ${largeArc} ${props.arc === "ccw" ? "0" : "1"}`; + return + 0 ? " error" : "") + + (props.highlight ? " highlight" : "")} + markerEnd='url(#arrowEnd)' + d={`M ${start.x} ${start.y} + ${arcOrLine} + ${end.x} ${end.y}`} + data-uid={uid} + data-parts="start end" /> + + {props.errors.length > 0 && {props.errors.join(' ')}} + + + + + + ; +} diff --git a/src/VisualEditor/RountangleSVG.tsx b/src/VisualEditor/RountangleSVG.tsx new file mode 100644 index 0000000..f19657d --- /dev/null +++ b/src/VisualEditor/RountangleSVG.tsx @@ -0,0 +1,124 @@ +import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax"; +import { Rect2D } from "./geometry"; +import { ROUNTANGLE_RADIUS, CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters"; +import { rountangleMinSize } from "./VisualEditor"; + +export function DiamondShape(props: {geometry: Rect2D, extraAttrs: object}) { + const {geometry} = props; + return ; +} + +export function RountangleSVG(props: { rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) { + const { topLeft, size, uid } = props.rountangle; + // always draw a rountangle with a minimum size + // during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it + const minSize = rountangleMinSize(size); + const extraAttrs = { + className: 'rountangle' + + (props.selected.length === 4 ? " selected" : "") + + (' ' + props.rountangle.kind) + + (props.errors.length > 0 ? " error" : "") + + (props.active ? " active" : ""), + "data-uid": uid, + "data-parts": "left top right bottom", + }; + return + {props.rountangle.kind === "pseudo" ? + + : + } + + + {(props.errors.length > 0) && + {props.errors.join(' ')}} + + + + + + + + + + + + {uid} + ; +} diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index 7b2e82c..6d99536 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -1,15 +1,17 @@ import * as lz4 from "@nick/lz4"; -import { Dispatch, MouseEventHandler, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react"; +import { Dispatch, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react"; import { Statechart } from "../statecharts/abstract_syntax"; -import { Arrow, ArrowPart, Rountangle, RountanglePart, VisualEditorState, emptyState, findNearestArrow, findNearestRountangleSide, findRountangle } from "../statecharts/concrete_syntax"; +import { ArrowPart, RountanglePart, VisualEditorState, emptyState, findNearestArrow, findNearestRountangleSide, findRountangle } from "../statecharts/concrete_syntax"; import { parseStatechart, TraceableError } from "../statecharts/parser"; import { BigStep } from "../statecharts/runtime_types"; -import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, euclideanDistance, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry"; -import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS, MIN_ROUNTANGLE_SIZE, ROUNTANGLE_RADIUS } from "./parameters"; +import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry"; +import { MIN_ROUNTANGLE_SIZE } from "./parameters"; import { getBBoxInSvgCoords } from "./svg_helper"; import "./VisualEditor.css"; +import { ArrowSVG } from "./ArrowSVG"; +import { RountangleSVG } from "./RountangleSVG"; type DraggingState = { @@ -48,14 +50,17 @@ export const sides: [RountanglePart, (r:Rect2D)=>Line2D][] = [ ["bottom", getBottomSide], ]; +export type InsertMode = "and"|"or"|"pseudo"|"transition"|"text"; + type VisualEditorProps = { setAST: Dispatch>, rt: BigStep|undefined, errors: TraceableError[], setErrors: Dispatch>, + mode: InsertMode, }; -export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) { +export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditorProps) { const [historyState, setHistoryState] = useState({current: emptyState, history: [], future: []}); const state = historyState.current; @@ -108,7 +113,6 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) } const [dragging, setDragging] = useState(null); - const [mode, setMode] = useState<"state"|"transition"|"text">("state"); const [showHelp, setShowHelp] = useState(false); // uid's of selected rountangles @@ -165,7 +169,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) // ignore selection, middle mouse button always inserts setState(state => { const newID = state.nextID.toString(); - if (mode === "state") { + if (mode === "and" || mode === "or" || mode === "pseudo") { // insert rountangle setSelection([{uid: newID, parts: ["bottom", "right"]}]); return { @@ -174,7 +178,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) uid: newID, topLeft: currentPointer, size: MIN_ROUNTANGLE_SIZE, - kind: "and", + kind: mode, }], nextID: state.nextID+1, }; @@ -203,7 +207,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) nextID: state.nextID+1, } } - throw new Error("unreachable"); // shut up typescript + throw new Error("unreachable, mode=" + mode); // shut up typescript }); setDragging({ lastMousePos: currentPointer, @@ -377,18 +381,28 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) return selection; }); } + if (e.key === "p") { + // selected states become pseudo-states + setSelection(selection => { + setState(state => ({ + ...state, + rountangles: state.rountangles.map(r => selection.some(rs => rs.uid === r.uid) ? ({...r, kind: "pseudo"}) : r), + })); + return selection; + }); + } if (e.key === "h") { setShowHelp(showHelp => !showHelp); } - if (e.key === "s") { - setMode("state"); - } - if (e.key === "t") { - setMode("transition"); - } - if (e.key === "x") { - setMode("text"); - } + // if (e.key === "s") { + // setMode("state"); + // } + // if (e.key === "t") { + // setMode("transition"); + // } + // if (e.key === "x") { + // setMode("text"); + // } if (e.ctrlKey) { if (e.key === "z") { @@ -577,8 +591,8 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) +(textsToHighlight.hasOwnProperty(txt.uid)?" highlight":""), } let textNode; - if (err) { - const {start,end} = err.data; + if (err?.data?.location) { + const {start,end} = err.data.location; textNode = <> {txt.text.slice(0, start.offset)} @@ -650,7 +664,7 @@ export function VisualEditor({setAST, rt, errors, setErrors}: VisualEditorProps) ; } -function rountangleMinSize(size: Vec2D): Vec2D { +export function rountangleMinSize(size: Vec2D): Vec2D { if (size.x >= 40 && size.y >= 40) { return size; } @@ -660,173 +674,6 @@ function rountangleMinSize(size: Vec2D): Vec2D { }; } -export function RountangleSVG(props: {rountangle: Rountangle, selected: string[], highlight: RountanglePart[], errors: string[], active: boolean}) { - const {topLeft, size, uid} = props.rountangle; - // always draw a rountangle with a minimum size - // during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it - const minSize = rountangleMinSize(size); - return - 0?" error":"") - +(props.active?" active":"") - } - rx={ROUNTANGLE_RADIUS} ry={ROUNTANGLE_RADIUS} - x={0} - y={0} - width={minSize.x} - height={minSize.y} - data-uid={uid} - data-parts="left top right bottom" - /> - - {(props.errors.length>0) && - {props.errors.join(' ')}} - - - - - - - - - - - - {uid} - ; -} - -export function ArrowSVG(props: {arrow: Arrow, selected: string[], errors: string[], highlight: boolean, arc: ArcDirection}) { - const {start, end, uid} = props.arrow; - const radius = euclideanDistance(start, end)/1.6; - const largeArc = "1"; - const arcOrLine = props.arc === "no" ? "L" : - `A ${radius} ${radius} 0 ${largeArc} ${props.arc === "ccw" ? "0" : "1"}`; - return - 0?" error":"") - +(props.highlight?" highlight":"") - } - markerEnd='url(#arrowEnd)' - d={`M ${start.x} ${start.y} - ${arcOrLine} - ${end.x} ${end.y}`} - data-uid={uid} - data-parts="start end" - /> - - {props.errors.length>0 && {props.errors.join(' ')}} - - - - - - ; -} - export function Selecting(props: SelectingState) { const normalizedRect = normalizeRect(props!); return ; - inputEvents: Set; - internalEvents: Set; + inputEvents: EventTrigger[]; + internalEvents: EventTrigger[]; outputEvents: Set; uid2State: Map; @@ -60,8 +60,8 @@ export const emptyStatechart: Statechart = { root: emptyRoot, transitions: new Map(), variables: new Set(), - inputEvents: new Set(), - internalEvents: new Set(), + inputEvents: [], + internalEvents: [], outputEvents: new Set(), uid2State: new Map([["root", emptyRoot]]), }; diff --git a/src/statecharts/actionlang_interpreter.ts b/src/statecharts/actionlang_interpreter.ts index cca5b31..13d3f97 100644 --- a/src/statecharts/actionlang_interpreter.ts +++ b/src/statecharts/actionlang_interpreter.ts @@ -29,6 +29,7 @@ export function evalExpr(expr: Expression, environment: Environment): any { else if (expr.kind === "ref") { const found = environment.get(expr.variable); if (found === undefined) { + console.log({environment}); throw new Error(`variable '${expr.variable}' does not exist in environment`); } return found; diff --git a/src/statecharts/concrete_syntax.ts b/src/statecharts/concrete_syntax.ts index d550465..62f7bc3 100644 --- a/src/statecharts/concrete_syntax.ts +++ b/src/statecharts/concrete_syntax.ts @@ -1,10 +1,10 @@ -import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox, isEntirelyWithin } from "../VisualEditor/geometry"; +import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox } from "../VisualEditor/geometry"; import { ARROW_SNAP_THRESHOLD, TEXT_SNAP_THRESHOLD } from "../VisualEditor/parameters"; import { sides } from "../VisualEditor/VisualEditor"; export type Rountangle = { uid: string; - kind: "and" | "or"; + kind: "and" | "or" | "pseudo"; } & Rect2D; export type Text = { diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts index 5c5a7db..e9e81ce 100644 --- a/src/statecharts/interpreter.ts +++ b/src/statecharts/interpreter.ts @@ -238,10 +238,10 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec if (event.kind === "input" && event.param !== undefined) { // input events may have a parameter // *temporarily* add event to environment (dirty!) - oldValue = environment.get(event.param.name); + oldValue = environment.get(event.param); environment = new Map([ ...environment, - [(t.label[0].trigger as EventTrigger).paramName as string, event.param.value], + [(t.label[0].trigger as EventTrigger).paramName as string, event.param], ]); } ({mode, environment, ...raised} = fireTransition(simtime, t, arena, srcPath, tgtPath, {mode, environment, ...raised})); @@ -251,6 +251,7 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec ...environment, [(t.label[0].trigger as EventTrigger).paramName as string, oldValue], ]); + console.log('restored environment:', environment); } arenasFired.add(arena); } diff --git a/src/statecharts/label_parser.js b/src/statecharts/label_parser.js index b92129b..03b31d7 100644 --- a/src/statecharts/label_parser.js +++ b/src/statecharts/label_parser.js @@ -231,7 +231,7 @@ function peg$parse(input, options) { }; } function peg$f1(event, param) { - return {kind: "event", event, param: param ? param[1] : undefined}; + return {kind: "event", event, paramName: param ? param[1] : undefined}; } function peg$f2(dur) { return {kind: "after", durationMs: dur}; diff --git a/src/statecharts/parser.ts b/src/statecharts/parser.ts index 67d3a32..dd64420 100644 --- a/src/statecharts/parser.ts +++ b/src/statecharts/parser.ts @@ -1,7 +1,7 @@ import { ConcreteState, OrState, Statechart, Transition } from "./abstract_syntax"; import { findNearestArrow, findNearestRountangleSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax"; import { isEntirelyWithin } from "../VisualEditor/geometry"; -import { Action, Expression, ParsedText } from "./label_ast"; +import { Action, EventTrigger, Expression, ParsedText } from "./label_ast"; import { parse as parseLabel, SyntaxError } from "./label_parser"; @@ -11,6 +11,24 @@ export type TraceableError = { data?: any; } +function addEvent(events: EventTrigger[], e: EventTrigger, textUid: string) { + const haveEvent = events.find(({event}) => event === e.event); + if (haveEvent) { + if (haveEvent.paramName !== e.paramName === undefined) { + return [{ + shapeUid: textUid, + message: "inconsistent event parameter", + }]; + } + return []; + } + else { + events.push(e); + events.sort((a,b) => a.event.localeCompare(b.event)); + return []; + } +} + export function parseStatechart(state: VisualEditorState): [Statechart, TraceableError[]] { const errors: TraceableError[] = []; @@ -144,9 +162,9 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl } let variables = new Set(); - const inputEvents = new Set(); + const inputEvents: EventTrigger[] = []; + const internalEvents: EventTrigger[] = []; const outputEvents = new Set(); - const internalEvents = new Set(); // step 3: figure out labels @@ -176,31 +194,35 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl if (belongsToTransition) { // parse as transition label belongsToTransition.label.push(parsed); + // collect events + // triggers if (parsed.trigger.kind === "event") { const {event} = parsed.trigger; if (event.startsWith("_")) { - internalEvents.add(event); + errors.push(...addEvent(internalEvents, parsed.trigger, parsed.uid)); } else { - inputEvents.add(event); + errors.push(...addEvent(inputEvents, parsed.trigger, parsed.uid)); } } else if (parsed.trigger.kind === "after") { belongsToTransition.src.timers.push(parsed.trigger.durationMs); belongsToTransition.src.timers.sort(); } - for (const action of parsed.actions) { - if (action.kind === "raise") { - const {event} = action; - if (event.startsWith("_")) { - internalEvents.add(event); - } - else { - outputEvents.add(event); - } - } - } + // // raise-actions + // for (const action of parsed.actions) { + // if (action.kind === "raise") { + // const {event} = action; + // if (event.startsWith("_")) { + // internalEvents.add(event); + // } + // else { + // outputEvents.add(event); + // } + // } + // } + // collect variables variables = variables .union(findVariables(parsed.guard)); diff --git a/src/statecharts/runtime_types.ts b/src/statecharts/runtime_types.ts index 31fca1a..4d99b25 100644 --- a/src/statecharts/runtime_types.ts +++ b/src/statecharts/runtime_types.ts @@ -19,6 +19,28 @@ export type Mode = Set; // set of active states export type Environment = ReadonlyMap; // variable name -> value +// export class Environment { +// env: Map[]; +// constructor(env = [new Map()]) { +// this.env = env; +// } + +// with(key: string, value: any): Environment { +// for (let i=0; i