performance and usability improvements
This commit is contained in:
parent
a25396b6f2
commit
ab988898c0
18 changed files with 381 additions and 206 deletions
3
artwork/about-logo.txt
Normal file
3
artwork/about-logo.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Font used in the logo is Twiddlestix:
|
||||||
|
|
||||||
|
https://www.1001fonts.com/twiddlestix-font.html
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"module": "src/index.tsx",
|
"module": "src/index.tsx",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --hot src/index.tsx",
|
"dev": "bun --hot src/index.tsx",
|
||||||
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
"build": "NODE_ENV=production bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='BUN_PUBLIC_*'",
|
||||||
"start": "NODE_ENV=production bun src/index.tsx"
|
"start": "NODE_ENV=production bun src/index.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ details > summary {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* these two rules add a bit of padding to an opened <details> node */
|
/* these two rules add a bit of padding to an opened <details> node */
|
||||||
details:open > summary {
|
/* details:open > summary:has(+ *) {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
details:open {
|
details:open:has(>summary:has(+ *)) {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
} */
|
||||||
|
|
||||||
details > summary:hover {
|
details > summary:hover {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
|
|
@ -46,30 +46,33 @@ details > summary:hover {
|
||||||
.outputEvent {
|
.outputEvent {
|
||||||
border: 1px black solid;
|
border: 1px black solid;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-left: 4px;
|
/* margin-left: 4px; */
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
background-color: rgb(230, 249, 255);
|
background-color: rgb(230, 249, 255);
|
||||||
|
color: black;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.internalEvent {
|
.internalEvent {
|
||||||
border: 1px black solid;
|
border: 1px black solid;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-left: 4px;
|
/* margin-left: 4px; */
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
background-color: rgb(255, 218, 252);
|
background-color: rgb(255, 218, 252);
|
||||||
|
color: black;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputEvent {
|
.inputEvent {
|
||||||
border: 1px black solid;
|
border: 1px black solid;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-left: 4px;
|
/* margin-left: 4px; */
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
background-color: rgb(224, 247, 209);
|
background-color: rgb(224, 247, 209);
|
||||||
|
color: black;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.inputEvent * {
|
.inputEvent * {
|
||||||
|
|
@ -116,4 +119,4 @@ ul {
|
||||||
.shadowBelow {
|
.shadowBelow {
|
||||||
box-shadow: 0 -15px 15px 15px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 -15px 15px 15px rgba(0, 0, 0, 0.4);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@ details:has(+ details) {
|
||||||
/* border: solid black 3px; */
|
/* border: solid black 3px; */
|
||||||
border: solid blue 1px;
|
border: solid blue 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.runtimeState.runtimeError {
|
||||||
|
background-color: lightpink;
|
||||||
|
color: darkred;
|
||||||
|
}
|
||||||
/* details:not(:has(details)) > summary::marker {
|
/* details:not(:has(details)) > summary::marker {
|
||||||
color: white;
|
color: white;
|
||||||
} */
|
} */
|
||||||
|
|
|
||||||
303
src/App/App.tsx
303
src/App/App.tsx
|
|
@ -1,8 +1,8 @@
|
||||||
import { createElement, Dispatch, ReactElement, SetStateAction, useEffect, useRef, useState } from "react";
|
import { ReactElement, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { emptyStatechart, Statechart } from "../statecharts/abstract_syntax";
|
import { emptyStatechart, Statechart, Transition } from "../statecharts/abstract_syntax";
|
||||||
import { handleInputEvent, initialize } from "../statecharts/interpreter";
|
import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter";
|
||||||
import { BigStep, BigStepOutput } from "../statecharts/runtime_types";
|
import { BigStep, BigStepOutput, RT_Event } from "../statecharts/runtime_types";
|
||||||
import { InsertMode, VisualEditor, VisualEditorState } from "../VisualEditor/VisualEditor";
|
import { InsertMode, VisualEditor, VisualEditorState } from "../VisualEditor/VisualEditor";
|
||||||
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
|
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
|
||||||
|
|
||||||
|
|
@ -12,19 +12,19 @@ import "./App.css";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { TopPanel } from "./TopPanel";
|
import { TopPanel } from "./TopPanel";
|
||||||
import { RTHistory } from "./RTHistory";
|
|
||||||
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
|
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } 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";
|
import { BottomPanel } from "./BottomPanel";
|
||||||
import { emptyState } from "@/statecharts/concrete_syntax";
|
import { emptyState } from "@/statecharts/concrete_syntax";
|
||||||
import { PersistentDetails } from "./PersistentDetails";
|
import { PersistentDetails } from "./PersistentDetails";
|
||||||
import { DigitalWatch, DigitalWatchPlant } from "@/Plant/DigitalWatch/DigitalWatch";
|
import { DigitalWatchPlant } from "@/Plant/DigitalWatch/DigitalWatch";
|
||||||
import { DummyPlant } from "@/Plant/Dummy/Dummy";
|
import { DummyPlant } from "@/Plant/Dummy/Dummy";
|
||||||
import { Plant } from "@/Plant/Plant";
|
import { Plant } from "@/Plant/Plant";
|
||||||
import { usePersistentState } from "@/util/persistent_state";
|
import { usePersistentState } from "@/util/persistent_state";
|
||||||
|
import { RTHistory } from "./RTHistory";
|
||||||
|
|
||||||
type EditHistory = {
|
export type EditHistory = {
|
||||||
current: VisualEditorState,
|
current: VisualEditorState,
|
||||||
history: VisualEditorState[],
|
history: VisualEditorState[],
|
||||||
future: VisualEditorState[],
|
future: VisualEditorState[],
|
||||||
|
|
@ -35,13 +35,46 @@ const plants: [string, Plant<any>][] = [
|
||||||
["digital watch", DigitalWatchPlant],
|
["digital watch", DigitalWatchPlant],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export type BigStepError = {
|
||||||
|
inputEvent: string,
|
||||||
|
simtime: number,
|
||||||
|
error: RuntimeError,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TraceItem = { kind: "error" } & BigStepError | { kind: "bigstep", plantState: any } & BigStep;
|
||||||
|
|
||||||
|
export type TraceState = {
|
||||||
|
trace: [TraceItem, ...TraceItem[]], // non-empty
|
||||||
|
idx: number,
|
||||||
|
}; // <-- null if there is no trace
|
||||||
|
|
||||||
|
function current(ts: TraceState) {
|
||||||
|
return ts.trace[ts.idx]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlantState<T>(plant: Plant<T>, trace: TraceItem[], idx: number): T | null {
|
||||||
|
if (idx === -1) {
|
||||||
|
return plant.initial;
|
||||||
|
}
|
||||||
|
let plantState = getPlantState(plant, trace, idx-1);
|
||||||
|
if (plantState !== null) {
|
||||||
|
const currentConfig = trace[idx];
|
||||||
|
if (currentConfig.kind === "bigstep") {
|
||||||
|
for (const o of currentConfig.outputEvents) {
|
||||||
|
plantState = plant.reduce(o, plantState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return plantState;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [mode, setMode] = useState<InsertMode>("and");
|
const [mode, setMode] = useState<InsertMode>("and");
|
||||||
const [historyState, setHistoryState] = useState<EditHistory>({current: emptyState, history: [], future: []});
|
const [historyState, setHistoryState] = useState<EditHistory>({current: emptyState, history: [], future: []});
|
||||||
const [ast, setAST] = useState<Statechart>(emptyStatechart);
|
const [ast, setAST] = useState<Statechart>(emptyStatechart);
|
||||||
const [errors, setErrors] = useState<TraceableError[]>([]);
|
const [errors, setErrors] = useState<TraceableError[]>([]);
|
||||||
const [rt, setRT] = useState<BigStep[]>([]);
|
const [trace, setTrace] = useState<TraceState|null>(null);
|
||||||
const [rtIdx, setRTIdx] = useState<number|undefined>();
|
|
||||||
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
|
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
|
||||||
const [modal, setModal] = useState<ReactElement|null>(null);
|
const [modal, setModal] = useState<ReactElement|null>(null);
|
||||||
|
|
||||||
|
|
@ -58,7 +91,7 @@ export function App() {
|
||||||
|
|
||||||
const refRightSideBar = useRef<HTMLDivElement>(null);
|
const refRightSideBar = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// append editor state to undo history
|
||||||
function makeCheckPoint() {
|
function makeCheckPoint() {
|
||||||
setHistoryState(historyState => ({
|
setHistoryState(historyState => ({
|
||||||
...historyState,
|
...historyState,
|
||||||
|
|
@ -92,54 +125,124 @@ export function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onInit() {
|
function onInit() {
|
||||||
const config = initialize(ast);
|
const timestampedEvent = {simtime: 0, inputEvent: "<init>"};
|
||||||
setRT([{inputEvent: null, simtime: 0, ...config}]);
|
let config;
|
||||||
setRTIdx(0);
|
try {
|
||||||
|
config = initialize(ast);
|
||||||
|
const item = {kind: "bigstep", ...timestampedEvent, ...config};
|
||||||
|
const plantState = getPlantState(plant, [item], 0);
|
||||||
|
setTrace({trace: [{...item, plantState}], idx: 0});
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof RuntimeError) {
|
||||||
|
setTrace({trace: [{kind: "error", ...timestampedEvent, error}], idx: 0});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw error; // probably a bug in the interpreter
|
||||||
|
}
|
||||||
|
}
|
||||||
setTime({kind: "paused", simtime: 0});
|
setTime({kind: "paused", simtime: 0});
|
||||||
scrollDownSidebar();
|
scrollDownSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClear() {
|
function onClear() {
|
||||||
setRT([]);
|
setTrace(null);
|
||||||
setRTIdx(undefined);
|
|
||||||
setTime({kind: "paused", simtime: 0});
|
setTime({kind: "paused", simtime: 0});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// raise input event, producing a new runtime configuration (or a runtime error)
|
||||||
function onRaise(inputEvent: string, param: any) {
|
function onRaise(inputEvent: string, param: any) {
|
||||||
if (rt.length>0 && rtIdx!==undefined && ast.inputEvents.some(e => e.event === inputEvent)) {
|
if (trace !== null && ast.inputEvents.some(e => e.event === inputEvent)) {
|
||||||
const simtime = getSimTime(time, Math.round(performance.now()));
|
const config = current(trace);
|
||||||
const nextConfig = handleInputEvent(simtime, {kind: "input", name: inputEvent, param}, ast, rt[rtIdx]!);
|
if (config.kind === "bigstep") {
|
||||||
appendNewConfig(inputEvent, simtime, nextConfig);
|
const simtime = getSimTime(time, Math.round(performance.now()));
|
||||||
|
produceNextConfig(simtime, {kind: "input", name: inputEvent, param}, config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: NodeJS.Timeout | undefined;
|
||||||
|
if (trace !== null) {
|
||||||
|
const config = current(trace);
|
||||||
|
if (config.kind === "bigstep") {
|
||||||
|
const timers = config.environment.get("_timers") || [];
|
||||||
|
if (timers.length > 0) {
|
||||||
|
const [nextInterrupt, timeElapsedEvent] = timers[0];
|
||||||
|
const raiseTimeEvent = () => {
|
||||||
|
produceNextConfig(nextInterrupt, timeElapsedEvent, config);
|
||||||
|
}
|
||||||
|
// depending on whether paused or realtime, raise immediately or in the future:
|
||||||
|
if (time.kind === "realtime") {
|
||||||
|
const wallclkDelay = getWallClkDelay(time, nextInterrupt, Math.round(performance.now()));
|
||||||
|
timeout = setTimeout(raiseTimeEvent, wallclkDelay);
|
||||||
|
}
|
||||||
|
else if (time.kind === "paused") {
|
||||||
|
if (nextInterrupt <= time.simtime) {
|
||||||
|
raiseTimeEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [time, trace]); // <-- todo: is this really efficient?
|
||||||
|
function produceNextConfig(simtime: number, event: RT_Event, config: TraceItem) {
|
||||||
|
const timedEvent = {
|
||||||
|
simtime,
|
||||||
|
inputEvent: event.kind === "timer" ? "<timer>" : event.name,
|
||||||
|
};
|
||||||
|
|
||||||
function appendNewConfig(inputEvent: string, simtime: number, config: BigStepOutput) {
|
let newItem: TraceItem;
|
||||||
setRT([...rt.slice(0, rtIdx!+1), {inputEvent, simtime, ...config}]);
|
try {
|
||||||
setRTIdx(rtIdx!+1);
|
const nextConfig = handleInputEvent(simtime, event, ast, config as BigStep); // may throw
|
||||||
// console.log('new config:', config);
|
let plantState = config.plantState;
|
||||||
|
for (const o of nextConfig.outputEvents) {
|
||||||
|
console.log(o);
|
||||||
|
plantState = plant.reduce(o, plantState);
|
||||||
|
}
|
||||||
|
console.log({plantState});
|
||||||
|
newItem = {kind: "bigstep", plantState, ...timedEvent, ...nextConfig};
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error instanceof RuntimeError) {
|
||||||
|
newItem = {kind: "error", ...timedEvent, error};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
setTrace(trace => ({
|
||||||
|
trace: [
|
||||||
|
...trace!.trace.slice(0, trace!.idx+1), // remove everything after current item
|
||||||
|
newItem,
|
||||||
|
],
|
||||||
|
idx: trace!.idx+1,
|
||||||
|
}));
|
||||||
scrollDownSidebar();
|
scrollDownSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onBack() {
|
function onBack() {
|
||||||
setTime(() => {
|
if (trace !== null) {
|
||||||
if (rtIdx !== undefined) {
|
setTime(() => {
|
||||||
if (rtIdx > 0)
|
if (trace !== null) {
|
||||||
return {
|
return {
|
||||||
kind: "paused",
|
kind: "paused",
|
||||||
simtime: rt[rtIdx-1].simtime,
|
simtime: trace.trace[trace.idx-1].simtime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { kind: "paused", simtime: 0 };
|
return { kind: "paused", simtime: 0 };
|
||||||
});
|
});
|
||||||
setRTIdx(rtIdx => {
|
setTrace({
|
||||||
if (rtIdx !== undefined) {
|
...trace,
|
||||||
if (rtIdx > 0)
|
idx: trace.idx-1,
|
||||||
return rtIdx - 1;
|
});
|
||||||
else
|
}
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
else return undefined;
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollDownSidebar() {
|
function scrollDownSidebar() {
|
||||||
|
|
@ -159,36 +262,6 @@ export function App() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let timeout: NodeJS.Timeout | undefined;
|
|
||||||
if (rtIdx !== undefined) {
|
|
||||||
const currentRt = rt[rtIdx]!;
|
|
||||||
const timers = currentRt.environment.get("_timers") || [];
|
|
||||||
if (timers.length > 0) {
|
|
||||||
const [nextInterrupt, timeElapsedEvent] = timers[0];
|
|
||||||
const raiseTimeEvent = () => {
|
|
||||||
const nextConfig = handleInputEvent(nextInterrupt, timeElapsedEvent, ast, currentRt);
|
|
||||||
appendNewConfig('<timer>', nextInterrupt, nextConfig);
|
|
||||||
}
|
|
||||||
if (time.kind === "realtime") {
|
|
||||||
const wallclkDelay = getWallClkDelay(time, nextInterrupt, Math.round(performance.now()));
|
|
||||||
// console.log('scheduling timeout after', wallclkDelay);
|
|
||||||
timeout = setTimeout(raiseTimeEvent, wallclkDelay);
|
|
||||||
}
|
|
||||||
else if (time.kind === "paused") {
|
|
||||||
if (nextInterrupt <= time.simtime) {
|
|
||||||
raiseTimeEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (timeout) clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
}, [time, rtIdx]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = getKeyHandler(setMode);
|
const onKeyDown = getKeyHandler(setMode);
|
||||||
window.addEventListener("keydown", onKeyDown);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
@ -197,27 +270,28 @@ export function App() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// const highlightActive = (rtIdx !== undefined) && new Set([...rt[rtIdx].mode].filter(uid => {
|
let highlightActive: Set<string>;
|
||||||
// const state = ast.uid2State.get(uid);
|
let highlightTransitions: string[];
|
||||||
// return state && state.parent?.kind !== "and";
|
if (trace === null) {
|
||||||
// })) || new Set();
|
highlightActive = new Set();
|
||||||
|
highlightTransitions = [];
|
||||||
const highlightActive: Set<string> = (rtIdx === undefined) ? new Set() : rt[rtIdx].mode;
|
|
||||||
|
|
||||||
const highlightTransitions = (rtIdx === undefined) ? [] : rt[rtIdx].firedTransitions;
|
|
||||||
|
|
||||||
|
|
||||||
const plantStates = [];
|
|
||||||
let ps = plant.initial(e => {
|
|
||||||
onRaise(e.name, e.param);
|
|
||||||
});
|
|
||||||
for (let i=0; i<rt.length; i++) {
|
|
||||||
const r = rt[i];
|
|
||||||
for (const o of r.outputEvents) {
|
|
||||||
ps = plant.reducer(o, ps);
|
|
||||||
}
|
|
||||||
plantStates.push(ps);
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
const item = current(trace);
|
||||||
|
console.log(trace);
|
||||||
|
if (item.kind === "bigstep") {
|
||||||
|
highlightActive = item.mode;
|
||||||
|
highlightTransitions = item.firedTransitions;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
highlightActive = new Set();
|
||||||
|
highlightTransitions = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const plantState = trace && getPlantState(plant, trace.trace, trace.idx);
|
||||||
|
|
||||||
|
const [showExecutionTrace, setShowExecutionTrace] = usePersistentState("showExecutionTrace", true);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
|
||||||
|
|
@ -250,13 +324,12 @@ export function App() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TopPanel
|
<TopPanel
|
||||||
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
|
{...{trace, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history: historyState}}
|
||||||
{...{rtIdx, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{/* Below the top bar: Editor */}
|
{/* Below the top bar: Editor */}
|
||||||
<Box sx={{flexGrow:1, overflow: "auto"}}>
|
<Box sx={{flexGrow:1, overflow: "auto"}}>
|
||||||
<VisualEditor {...{state: editorState, setState: setEditorState, ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}}/>
|
<VisualEditor {...{state: editorState, setState: setEditorState, ast, setAST, trace, setTrace, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -272,18 +345,22 @@ export function App() {
|
||||||
}}>
|
}}>
|
||||||
<Stack sx={{height:'100%'}}>
|
<Stack sx={{height:'100%'}}>
|
||||||
<Box
|
<Box
|
||||||
className="shadowBelow"
|
className={showExecutionTrace ? "shadowBelow" : ""}
|
||||||
sx={{flex: '0 0 content', backgroundColor: ''}}
|
sx={{flex: '0 0 content', backgroundColor: ''}}
|
||||||
>
|
>
|
||||||
<PersistentDetails localStorageKey="showStateTree" initiallyOpen={true}>
|
<PersistentDetails localStorageKey="showStateTree" initiallyOpen={true}>
|
||||||
<summary>state tree</summary>
|
<summary>state tree</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
|
<ShowAST {...{...ast, trace, highlightActive}}/>
|
||||||
</ul>
|
</ul>
|
||||||
</PersistentDetails>
|
</PersistentDetails>
|
||||||
<PersistentDetails localStorageKey="showInputEvents" initiallyOpen={true}>
|
<PersistentDetails localStorageKey="showInputEvents" initiallyOpen={true}>
|
||||||
<summary>input events</summary>
|
<summary>input events</summary>
|
||||||
<ShowInputEvents inputEvents={ast.inputEvents} onRaise={onRaise} disabled={rtIdx===undefined} showKeys={showKeys}/>
|
<ShowInputEvents
|
||||||
|
inputEvents={ast.inputEvents}
|
||||||
|
onRaise={onRaise}
|
||||||
|
disabled={trace===null || trace.trace[trace.idx].kind === "error"}
|
||||||
|
showKeys={showKeys}/>
|
||||||
</PersistentDetails>
|
</PersistentDetails>
|
||||||
<PersistentDetails localStorageKey="showInternalEvents" initiallyOpen={true}>
|
<PersistentDetails localStorageKey="showInternalEvents" initiallyOpen={true}>
|
||||||
<summary>internal events</summary>
|
<summary>internal events</summary>
|
||||||
|
|
@ -293,20 +370,6 @@ export function App() {
|
||||||
<summary>output events</summary>
|
<summary>output events</summary>
|
||||||
<ShowOutputEvents outputEvents={ast.outputEvents}/>
|
<ShowOutputEvents outputEvents={ast.outputEvents}/>
|
||||||
</PersistentDetails>
|
</PersistentDetails>
|
||||||
</Box>
|
|
||||||
<Box sx={{
|
|
||||||
flexGrow:1,
|
|
||||||
overflow:'auto',
|
|
||||||
minHeight: 400,
|
|
||||||
// minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
|
|
||||||
}}>
|
|
||||||
<Box sx={{ height: '100%'}}>
|
|
||||||
<div ref={refRightSideBar}>
|
|
||||||
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx, refRightSideBar}}/>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{flex: '0 0 content'}}>
|
|
||||||
<PersistentDetails localStorageKey="showPlant" initiallyOpen={true}>
|
<PersistentDetails localStorageKey="showPlant" initiallyOpen={true}>
|
||||||
<summary>plant</summary>
|
<summary>plant</summary>
|
||||||
<select
|
<select
|
||||||
|
|
@ -316,8 +379,28 @@ export function App() {
|
||||||
<option>{plantName}</option>
|
<option>{plantName}</option>
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
{rtIdx!==undefined && <plant.render {...plantStates[rtIdx]}/>}
|
{trace !== null &&
|
||||||
|
plant.render(trace.trace[trace.idx].plantState, event => onRaise(event.name, event.param))}
|
||||||
</PersistentDetails>
|
</PersistentDetails>
|
||||||
|
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary></details>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{showExecutionTrace &&
|
||||||
|
<Box sx={{
|
||||||
|
flexGrow:1,
|
||||||
|
overflow:'auto',
|
||||||
|
minHeight: '50vh',
|
||||||
|
// minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
|
||||||
|
}}>
|
||||||
|
{/* <PersistentDetails localStorageKey="showExecutionTrace" initiallyOpen={true}> */}
|
||||||
|
{/* <summary>execution trace</summary> */}
|
||||||
|
<div ref={refRightSideBar}>
|
||||||
|
<RTHistory {...{ast, trace, setTrace, setTime}}/>
|
||||||
|
</div>
|
||||||
|
{/* </PersistentDetails> */}
|
||||||
|
</Box>}
|
||||||
|
|
||||||
|
<Box sx={{flex: '0 0 content'}}>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
import { Stack } from "@mui/material";
|
import { Box, Stack } from "@mui/material";
|
||||||
|
|
||||||
export function KeyInfoVisible(props: {keyInfo, children}) {
|
export function KeyInfoVisible(props: {keyInfo, children, horizontal?: boolean}) {
|
||||||
return <Stack style={{display: "inline-block"}}>
|
return <div style={{display: 'inline-block'}}>
|
||||||
<div style={{fontSize:11, height: 18, textAlign:"center", paddingLeft: 3, paddingRight: 3}}>
|
{/* <Stack direction={props.horizontal ? "row" : "column"}> */}
|
||||||
{props.keyInfo}
|
<div style={{display: props.horizontal ? 'inline-block' : '', fontSize:11, height: 18, textAlign:"center", paddingLeft: 3, paddingRight: 3}}>
|
||||||
</div>
|
{props.keyInfo}
|
||||||
<div style={{textAlign:"center"}}>
|
</div>
|
||||||
{props.children}
|
<div style={{display: props.horizontal ? 'inline-block' : '', textAlign:"center"}}>
|
||||||
</div>
|
{props.children}
|
||||||
</Stack>
|
</div>
|
||||||
|
{/* </Stack> */}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KeyInfoHidden(props: {children}) {
|
export function KeyInfoHidden(props: {children}) {
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,61 @@
|
||||||
import { Dispatch, Ref, SetStateAction } from "react";
|
import { Dispatch, Ref, SetStateAction } from "react";
|
||||||
import { Statechart, stateDescription } from "../statecharts/abstract_syntax";
|
import { Statechart, stateDescription } from "../statecharts/abstract_syntax";
|
||||||
import { BigStep, Environment, Mode, RaisedEvent } from "../statecharts/runtime_types";
|
import { BigStep, Environment, Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types";
|
||||||
import { formatTime } from "./util";
|
import { formatTime } from "./util";
|
||||||
import { TimeMode } from "../statecharts/time";
|
import { TimeMode } from "../statecharts/time";
|
||||||
|
import { TraceState } from "./App";
|
||||||
|
|
||||||
type RTHistoryProps = {
|
type RTHistoryProps = {
|
||||||
rt: BigStep[],
|
trace: TraceState|null,
|
||||||
rtIdx: number | undefined,
|
setTrace: Dispatch<SetStateAction<TraceState|null>>;
|
||||||
ast: Statechart,
|
ast: Statechart,
|
||||||
setRTIdx: Dispatch<SetStateAction<number|undefined>>,
|
|
||||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||||
refRightSideBar: Ref<HTMLDivElement>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RTHistory({rt, rtIdx, ast, setRTIdx, setTime, refRightSideBar}: RTHistoryProps) {
|
export function RTHistory({trace, setTrace, ast, setTime}: RTHistoryProps) {
|
||||||
function gotoRt(idx: number, timestamp: number) {
|
function gotoRt(idx: number, timestamp: number) {
|
||||||
setRTIdx(idx);
|
setTrace(trace => trace && {
|
||||||
|
...trace,
|
||||||
|
idx,
|
||||||
|
});
|
||||||
setTime({kind: "paused", simtime: timestamp});
|
setTime({kind: "paused", simtime: timestamp});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trace === null) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
return <div>
|
return <div>
|
||||||
{rt.map((r, idx) => <>
|
{trace.trace.map((item, i) => {
|
||||||
<div
|
if (item.kind === "bigstep") {
|
||||||
className={"runtimeState"+(idx===rtIdx?" active":"")}
|
const newStates = item.mode.difference(trace.trace[i-1]?.mode || new Set());
|
||||||
onClick={() => gotoRt(idx, r.simtime)}>
|
return <div
|
||||||
<div>
|
className={"runtimeState" + (i === trace.idx ? " active" : "")}
|
||||||
{formatTime(r.simtime)}
|
onClick={() => gotoRt(i, item.simtime)}>
|
||||||
 
|
<div>
|
||||||
<div className="inputEvent">{r.inputEvent || "<init>"}</div>
|
{formatTime(item.simtime)}
|
||||||
</div>
|
 
|
||||||
<ShowMode mode={r.mode.difference(rt[idx-1]?.mode || new Set())} statechart={ast}/>
|
<div className="inputEvent">{item.inputEvent || "<init>"}</div>
|
||||||
<ShowEnvironment environment={r.environment}/>
|
</div>
|
||||||
{r.outputEvents.length>0 && <>^
|
<ShowMode mode={newStates} statechart={ast}/>
|
||||||
{r.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
|
<ShowEnvironment environment={item.environment}/>
|
||||||
</>}
|
{item.outputEvents.length>0 && <>^
|
||||||
{/* <hr/> */}
|
{item.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
|
||||||
</div></>)}
|
</>}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return <div className="runtimeState runtimeError">
|
||||||
|
<div>
|
||||||
|
{formatTime(item.simtime)}
|
||||||
|
 
|
||||||
|
<div className="inputEvent">{item.inputEvent}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{item.error.message}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
})}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function ShowAction(props: {action: Action}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map<string, Transition[]>, rt: RT_Statechart | undefined, highlightActive: Set<string>}) {
|
export function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map<string, Transition[]>, trace: TraceState | null, highlightActive: Set<string>}) {
|
||||||
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) || [];
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function ShowAST(props: {root: ConcreteState | PseudoState, transitions:
|
||||||
{props.root.kind !== "pseudo" && props.root.children.length>0 &&
|
{props.root.kind !== "pseudo" && props.root.children.length>0 &&
|
||||||
<ul>
|
<ul>
|
||||||
{props.root.children.map(child =>
|
{props.root.children.map(child =>
|
||||||
<ShowAST key={child.uid} root={child} transitions={props.transitions} rt={props.rt} highlightActive={props.highlightActive} />
|
<ShowAST key={child.uid} root={child} transitions={props.transitions} trace={props.trace} highlightActive={props.highlightActive} />
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
|
|
@ -74,6 +74,7 @@ export function ShowAST(props: {root: ConcreteState | PseudoState, transitions:
|
||||||
import BoltIcon from '@mui/icons-material/Bolt';
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { TraceState } from "./App";
|
||||||
|
|
||||||
export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean, showKeys: boolean}) {
|
export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean, showKeys: boolean}) {
|
||||||
const raiseHandlers = inputEvents.map(({event}) => {
|
const raiseHandlers = inputEvents.map(({event}) => {
|
||||||
|
|
@ -110,7 +111,7 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp
|
||||||
const shortcut = (i+1)%10;
|
const shortcut = (i+1)%10;
|
||||||
const KI = (i <= 10) ? KeyInfo : KeyInfoHidden;
|
const KI = (i <= 10) ? KeyInfo : KeyInfoHidden;
|
||||||
return <div key={event+'/'+paramName} className="toolbarGroup">
|
return <div key={event+'/'+paramName} className="toolbarGroup">
|
||||||
<KI keyInfo={<kbd>{shortcut}</kbd>}>
|
<KI keyInfo={<kbd>{shortcut}</kbd>} horizontal={true}>
|
||||||
<button
|
<button
|
||||||
className="inputEvent"
|
className="inputEvent"
|
||||||
title={`raise this input event`}
|
title={`raise this input event`}
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,12 @@ import { About } from "./About";
|
||||||
import { usePersistentState } from "@/util/persistent_state";
|
import { usePersistentState } from "@/util/persistent_state";
|
||||||
import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons";
|
import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons";
|
||||||
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters";
|
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters";
|
||||||
|
import { EditHistory, TraceState } from "./App";
|
||||||
|
|
||||||
export type TopPanelProps = {
|
export type TopPanelProps = {
|
||||||
rt?: BigStep,
|
trace: TraceState | null,
|
||||||
rtIdx?: number,
|
// rt?: BigStep,
|
||||||
|
// rtIdx?: number,
|
||||||
time: TimeMode,
|
time: TimeMode,
|
||||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||||
onUndo: () => void,
|
onUndo: () => void,
|
||||||
|
|
@ -45,12 +47,15 @@ export type TopPanelProps = {
|
||||||
setZoom: Dispatch<SetStateAction<number>>,
|
setZoom: Dispatch<SetStateAction<number>>,
|
||||||
showKeys: boolean,
|
showKeys: boolean,
|
||||||
setShowKeys: Dispatch<SetStateAction<boolean>>,
|
setShowKeys: Dispatch<SetStateAction<boolean>>,
|
||||||
|
history: EditHistory,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, ast, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys}: TopPanelProps) {
|
export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, ast, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history}: TopPanelProps) {
|
||||||
const [displayTime, setDisplayTime] = useState("0.000");
|
const [displayTime, setDisplayTime] = useState("0.000");
|
||||||
const [timescale, setTimescale] = useState(1);
|
const [timescale, setTimescale] = useState(1);
|
||||||
|
|
||||||
|
const config = trace && trace.trace[trace.idx];
|
||||||
|
|
||||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -58,8 +63,9 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
if (!e.ctrlKey) {
|
if (!e.ctrlKey) {
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (rt)
|
if (config) {
|
||||||
onChangePaused(time.kind !== "paused", Math.round(performance.now()));
|
onChangePaused(time.kind !== "paused", Math.round(performance.now()));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (e.key === "i") {
|
if (e.key === "i") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -70,7 +76,7 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
onClear();
|
onClear();
|
||||||
}
|
}
|
||||||
if (e.key === "Tab") {
|
if (e.key === "Tab") {
|
||||||
if (rtIdx === undefined) {
|
if (trace === null) {
|
||||||
onInit();
|
onInit();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
@ -175,7 +181,7 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
}
|
}
|
||||||
|
|
||||||
// timestamp of next timed transition, in simulated time
|
// timestamp of next timed transition, in simulated time
|
||||||
const timers: Timers = (rt?.environment.get("_timers") || []);
|
const timers: Timers = config?.kind === "bigstep" && config.environment.get("_timers") || [];
|
||||||
const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0];
|
const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0];
|
||||||
|
|
||||||
function onSkip() {
|
function onSkip() {
|
||||||
|
|
@ -225,10 +231,10 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
{/* undo / redo */}
|
{/* undo / redo */}
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Z</kbd></>}>
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Z</kbd></>}>
|
||||||
<button title="undo" onClick={onUndo}><UndoIcon fontSize="small"/></button>
|
<button title="undo" onClick={onUndo} disabled={history.history.length === 0}><UndoIcon fontSize="small"/> ({history.history.length})</button>
|
||||||
</KeyInfo>
|
</KeyInfo>
|
||||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></>}>
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></>}>
|
||||||
<button title="redo" onClick={onRedo}><RedoIcon fontSize="small"/></button>
|
<button title="redo" onClick={onRedo} disabled={history.future.length === 0}><RedoIcon fontSize="small"/> ({history.future.length})</button>
|
||||||
</KeyInfo>
|
</KeyInfo>
|
||||||
 
|
 
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -263,12 +269,12 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
<button title="(re)initialize simulation" onClick={onInit} ><PlayArrowIcon fontSize="small"/><CachedIcon fontSize="small"/></button>
|
<button title="(re)initialize simulation" onClick={onInit} ><PlayArrowIcon fontSize="small"/><CachedIcon fontSize="small"/></button>
|
||||||
</KeyInfo>
|
</KeyInfo>
|
||||||
<KeyInfo keyInfo={<kbd>C</kbd>}>
|
<KeyInfo keyInfo={<kbd>C</kbd>}>
|
||||||
<button title="clear the simulation" onClick={onClear} disabled={!rt}><StopIcon fontSize="small"/></button>
|
<button title="clear the simulation" onClick={onClear} disabled={!config}><StopIcon fontSize="small"/></button>
|
||||||
</KeyInfo>
|
</KeyInfo>
|
||||||
 
|
 
|
||||||
<KeyInfo keyInfo={<><kbd>Space</kbd> toggles</>}>
|
<KeyInfo keyInfo={<><kbd>Space</kbd> toggles</>}>
|
||||||
<button title="pause the simulation" disabled={!rt || time.kind==="paused"} className={(rt && time.kind==="paused") ? "active":""} onClick={() => onChangePaused(true, Math.round(performance.now()))}><PauseIcon fontSize="small"/></button>
|
<button title="pause the simulation" disabled={!config || time.kind==="paused"} className={(config && time.kind==="paused") ? "active":""} onClick={() => onChangePaused(true, Math.round(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, Math.round(performance.now()))}><PlayArrowIcon fontSize="small"/></button>
|
<button title="run the simulation in real time" disabled={!config || time.kind==="realtime"} className={(config && time.kind==="realtime") ? "active":""} onClick={() => onChangePaused(false, Math.round(performance.now()))}><PlayArrowIcon fontSize="small"/></button>
|
||||||
</KeyInfo>
|
</KeyInfo>
|
||||||
 
|
 
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -290,12 +296,12 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
<label htmlFor="time">time (s)</label>
|
<label htmlFor="time">time (s)</label>
|
||||||
<input title="the current simulated time" id="time" disabled={!rt} value={displayTime} readOnly={true} className="readonlyTextBox" />
|
<input title="the current simulated time" id="time" disabled={!config} value={displayTime} readOnly={true} className="readonlyTextBox" />
|
||||||
</div>
|
</div>
|
||||||
 
|
 
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
<label htmlFor="next-timeout">next (s)</label>
|
<label htmlFor="next-timeout">next (s)</label>
|
||||||
<input title="next point in simulated time where a timed transition may fire" id="next-timeout" disabled={!rt} value={nextTimedTransition ? formatTime(nextTimedTransition[0]) : '+inf'} readOnly={true} className="readonlyTextBox"/>
|
<input title="next point in simulated time where a timed transition may fire" id="next-timeout" disabled={!config} value={nextTimedTransition ? formatTime(nextTimedTransition[0]) : '+inf'} readOnly={true} className="readonlyTextBox"/>
|
||||||
<KeyInfo keyInfo={<kbd>Tab</kbd>}>
|
<KeyInfo keyInfo={<kbd>Tab</kbd>}>
|
||||||
<button title="advance time just enough for the next timer to elapse" disabled={nextTimedTransition===undefined} onClick={onSkip}><SkipNextIcon fontSize="small"/><AccessAlarmIcon fontSize="small"/></button>
|
<button title="advance time just enough for the next timer to elapse" disabled={nextTimedTransition===undefined} onClick={onSkip}><SkipNextIcon fontSize="small"/><AccessAlarmIcon fontSize="small"/></button>
|
||||||
</KeyInfo>
|
</KeyInfo>
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,15 @@ export function compactTime(timeMs: number) {
|
||||||
return `${timeMs} ms`;
|
return `${timeMs} ms`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function memoize<InType,OutType>(fn: (i: InType) => OutType) {
|
||||||
|
const cache = new Map();
|
||||||
|
return (i: InType) => {
|
||||||
|
const found = cache.get(i);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
const result = fn(i);
|
||||||
|
cache.set(i, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
14
src/Plant/DigitalWatch/DigitalWatch.css
Normal file
14
src/Plant/DigitalWatch/DigitalWatch.css
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
.watchButtonHelper {
|
||||||
|
fill-opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchButtonHelper:hover {
|
||||||
|
fill: beige;
|
||||||
|
fill-opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchButtonHelper:active {
|
||||||
|
fill: red;
|
||||||
|
fill-opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -4,6 +4,8 @@ import digitalFont from "./digital-font.ttf";
|
||||||
import { Plant } from "../Plant";
|
import { Plant } from "../Plant";
|
||||||
import { RaisedEvent } from "@/statecharts/runtime_types";
|
import { RaisedEvent } from "@/statecharts/runtime_types";
|
||||||
|
|
||||||
|
import "./DigitalWatch.css";
|
||||||
|
|
||||||
type DigitalWatchState = {
|
type DigitalWatchState = {
|
||||||
light: boolean;
|
light: boolean;
|
||||||
h: number;
|
h: number;
|
||||||
|
|
@ -12,7 +14,8 @@ type DigitalWatchState = {
|
||||||
alarm: boolean;
|
alarm: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DigitalWatchProps = DigitalWatchState & {
|
type DigitalWatchProps = {
|
||||||
|
state: DigitalWatchState,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
onTopLeftPressed: () => void;
|
onTopLeftPressed: () => void;
|
||||||
onTopRightPressed: () => void;
|
onTopRightPressed: () => void;
|
||||||
|
|
@ -25,7 +28,7 @@ type DigitalWatchProps = DigitalWatchState & {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DigitalWatch({light, h, m, s, alarm, callbacks}: DigitalWatchProps) {
|
export function DigitalWatch({state: {light, h, m, s, alarm}, callbacks}: DigitalWatchProps) {
|
||||||
const twoDigits = (n: number) => n < 0 ? " " : ("0"+n.toString()).slice(-2);
|
const twoDigits = (n: number) => n < 0 ? " " : ("0"+n.toString()).slice(-2);
|
||||||
const hhmmss = `${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}`;
|
const hhmmss = `${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}`;
|
||||||
|
|
||||||
|
|
@ -44,19 +47,19 @@ export function DigitalWatch({light, h, m, s, alarm, callbacks}: DigitalWatchPro
|
||||||
|
|
||||||
<text x="111" y="126" dominantBaseline="middle" textAnchor="middle" fontFamily="digital-font" fontSize={28} style={{whiteSpace:'preserve'}}>{hhmmss}</text>
|
<text x="111" y="126" dominantBaseline="middle" textAnchor="middle" fontFamily="digital-font" fontSize={28} style={{whiteSpace:'preserve'}}>{hhmmss}</text>
|
||||||
|
|
||||||
<rect x="0" y="59" width="16" height="16" fill="#fff" fillOpacity="0"
|
<rect className="watchButtonHelper" x={0} y={54} width={24} height={24}
|
||||||
onMouseDown={() => callbacks.onTopLeftPressed()}
|
onMouseDown={() => callbacks.onTopLeftPressed()}
|
||||||
onMouseUp={() => callbacks.onTopLeftReleased()}
|
onMouseUp={() => callbacks.onTopLeftReleased()}
|
||||||
/>
|
/>
|
||||||
<rect x="206" y="57" width="16" height="16" fill="#fff" fillOpacity="0"
|
<rect className="watchButtonHelper" x={198} y={54} width={24} height={24}
|
||||||
onMouseDown={() => callbacks.onTopRightPressed()}
|
onMouseDown={() => callbacks.onTopRightPressed()}
|
||||||
onMouseUp={() => callbacks.onTopRightReleased()}
|
onMouseUp={() => callbacks.onTopRightReleased()}
|
||||||
/>
|
/>
|
||||||
<rect x="0" y="158" width="16" height="16" fill="#fff" fillOpacity="0"
|
<rect className="watchButtonHelper" x={0} y={154} width={24} height={24}
|
||||||
onMouseDown={() => callbacks.onBottomLeftPressed()}
|
onMouseDown={() => callbacks.onBottomLeftPressed()}
|
||||||
onMouseUp={() => callbacks.onBottomLeftReleased()}
|
onMouseUp={() => callbacks.onBottomLeftReleased()}
|
||||||
/>
|
/>
|
||||||
<rect x="208" y="158" width="16" height="16" fill="#fff" fillOpacity="0"
|
<rect className="watchButtonHelper" x={198} y={154} width={24} height={24}
|
||||||
onMouseDown={() => callbacks.onBottomRightPressed()}
|
onMouseDown={() => callbacks.onBottomRightPressed()}
|
||||||
onMouseUp={() => callbacks.onBottomRightReleased()}
|
onMouseUp={() => callbacks.onBottomRightReleased()}
|
||||||
/>
|
/>
|
||||||
|
|
@ -68,7 +71,7 @@ export function DigitalWatch({light, h, m, s, alarm, callbacks}: DigitalWatchPro
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DigitalWatchPlant: Plant<DigitalWatchProps> = {
|
export const DigitalWatchPlant: Plant<DigitalWatchState> = {
|
||||||
inputEvents: [
|
inputEvents: [
|
||||||
{ kind: "event", event: "setH", paramName: 'h' },
|
{ kind: "event", event: "setH", paramName: 'h' },
|
||||||
{ kind: "event", event: "setM", paramName: 'm' },
|
{ kind: "event", event: "setM", paramName: 'm' },
|
||||||
|
|
@ -86,24 +89,14 @@ export const DigitalWatchPlant: Plant<DigitalWatchProps> = {
|
||||||
{ kind: "event", event: "bottomRightReleased" },
|
{ kind: "event", event: "bottomRightReleased" },
|
||||||
{ kind: "event", event: "bottomLeftReleased" },
|
{ kind: "event", event: "bottomLeftReleased" },
|
||||||
],
|
],
|
||||||
initial: (raise: (event: RaisedEvent) => void) => ({
|
initial: {
|
||||||
light: false,
|
light: false,
|
||||||
alarm: false,
|
alarm: false,
|
||||||
h: 12,
|
h: 12,
|
||||||
m: 0,
|
m: 0,
|
||||||
s: 0,
|
s: 0,
|
||||||
callbacks: {
|
},
|
||||||
onTopLeftPressed: () => raise({ name: "topLeftPressed" }),
|
reduce: (inputEvent: RaisedEvent, state: DigitalWatchState) => {
|
||||||
onTopRightPressed: () => raise({ name: "topRightPressed" }),
|
|
||||||
onBottomRightPressed: () => raise({ name: "bottomRightPressed" }),
|
|
||||||
onBottomLeftPressed: () => raise({ name: "bottomLeftPressed" }),
|
|
||||||
onTopLeftReleased: () => raise({ name: "topLeftReleased" }),
|
|
||||||
onTopRightReleased: () => raise({ name: "topRightReleased" }),
|
|
||||||
onBottomRightReleased: () => raise({ name: "bottomRightReleased" }),
|
|
||||||
onBottomLeftReleased: () => raise({ name: "bottomLeftReleased" }),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
reducer: (inputEvent: RaisedEvent, state: DigitalWatchProps) => {
|
|
||||||
if (inputEvent.name === "setH") {
|
if (inputEvent.name === "setH") {
|
||||||
return { ...state, h: inputEvent.param };
|
return { ...state, h: inputEvent.param };
|
||||||
}
|
}
|
||||||
|
|
@ -127,5 +120,14 @@ export const DigitalWatchPlant: Plant<DigitalWatchProps> = {
|
||||||
}
|
}
|
||||||
return state; // unknown event - ignore it
|
return state; // unknown event - ignore it
|
||||||
},
|
},
|
||||||
render: DigitalWatch,
|
render: (state, raiseEvent) => <DigitalWatch state={state} callbacks={{
|
||||||
|
onTopLeftPressed: () => raiseEvent({name: "topLeftPressed"}),
|
||||||
|
onTopRightPressed: () => raiseEvent({name: "topRightPressed"}),
|
||||||
|
onBottomRightPressed: () => raiseEvent({name: "bottomRightPressed"}),
|
||||||
|
onBottomLeftPressed: () => raiseEvent({name: "bottomLeftPressed"}),
|
||||||
|
onTopLeftReleased: () => raiseEvent({name: "topLeftReleased"}),
|
||||||
|
onTopRightReleased: () => raiseEvent({name: "topRightReleased"}),
|
||||||
|
onBottomRightReleased: () => raiseEvent({name: "bottomRightReleased"}),
|
||||||
|
onBottomLeftReleased: () => raiseEvent({name: "bottomLeftReleased"}),
|
||||||
|
}}/>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,6 @@ export const DummyPlant: Plant<{}> = {
|
||||||
inputEvents: [],
|
inputEvents: [],
|
||||||
outputEvents: [],
|
outputEvents: [],
|
||||||
initial: () => ({}),
|
initial: () => ({}),
|
||||||
reducer: (_inputEvent: RaisedEvent, _state: {}) => ({}),
|
reduce: (_inputEvent: RaisedEvent, _state: {}) => ({}),
|
||||||
render: (_state: {}) => <></>,
|
render: (_state: {}, _raise: (event: RaisedEvent) => void) => <></>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export type Plant<StateType> = {
|
||||||
inputEvents: EventTrigger[];
|
inputEvents: EventTrigger[];
|
||||||
outputEvents: EventTrigger[];
|
outputEvents: EventTrigger[];
|
||||||
|
|
||||||
initial: (raise: (event: RaisedEvent) => void) => StateType;
|
initial: StateType;
|
||||||
reducer: (inputEvent: RaisedEvent, state: StateType) => StateType;
|
reduce: (inputEvent: RaisedEvent, state: StateType) => StateType;
|
||||||
render: (state: StateType) => ReactElement;
|
render: (state: StateType, raise: (event: RaisedEvent) => void) => ReactElement;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ text.helper:hover {
|
||||||
stroke: var(--error-color);
|
stroke: var(--error-color);
|
||||||
}
|
}
|
||||||
.arrow.fired {
|
.arrow.fired {
|
||||||
stroke: rgb(160, 0, 168);
|
stroke: rgb(160 0 168);
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
animation: blinkTransition 1s;
|
animation: blinkTransition 1s;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { HistorySVG } from "./HistorySVG";
|
||||||
import { detectConnections } from "../statecharts/detect_connections";
|
import { detectConnections } from "../statecharts/detect_connections";
|
||||||
|
|
||||||
import "./VisualEditor.css";
|
import "./VisualEditor.css";
|
||||||
|
import { TraceState } from "@/App/App";
|
||||||
|
|
||||||
export type VisualEditorState = {
|
export type VisualEditorState = {
|
||||||
rountangles: Rountangle[];
|
rountangles: Rountangle[];
|
||||||
|
|
@ -65,7 +66,7 @@ type VisualEditorProps = {
|
||||||
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
||||||
ast: Statechart,
|
ast: Statechart,
|
||||||
setAST: Dispatch<SetStateAction<Statechart>>,
|
setAST: Dispatch<SetStateAction<Statechart>>,
|
||||||
rt: BigStep|undefined,
|
trace: TraceState | null,
|
||||||
errors: TraceableError[],
|
errors: TraceableError[],
|
||||||
setErrors: Dispatch<SetStateAction<TraceableError[]>>,
|
setErrors: Dispatch<SetStateAction<TraceableError[]>>,
|
||||||
mode: InsertMode,
|
mode: InsertMode,
|
||||||
|
|
@ -76,7 +77,7 @@ type VisualEditorProps = {
|
||||||
zoom: number;
|
zoom: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VisualEditor({state, setState, ast, setAST, rt, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
|
export function VisualEditor({state, setState, ast, setAST, trace, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
|
||||||
|
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
|
|
@ -132,7 +133,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}, [rt]);
|
}, [trace && trace.idx]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
|
@ -667,7 +668,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = rt?.mode || new Set();
|
const active = trace && trace.trace[trace.idx].mode || new Set();
|
||||||
|
|
||||||
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,16 @@ type ActionScope = {
|
||||||
|
|
||||||
type EnteredScope = { enteredStates: Mode } & ActionScope;
|
type EnteredScope = { enteredStates: Mode } & ActionScope;
|
||||||
|
|
||||||
|
export class RuntimeError extends Error {
|
||||||
|
highlight: string[];
|
||||||
|
constructor(message: string, highlight: string[]) {
|
||||||
|
super(message);
|
||||||
|
this.highlight = highlight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NonDeterminismError extends RuntimeError {}
|
||||||
|
|
||||||
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
if (action.kind === "assignment") {
|
if (action.kind === "assignment") {
|
||||||
const rhs = evalExpr(action.rhs, rt.environment);
|
const rhs = evalExpr(action.rhs, rt.environment);
|
||||||
|
|
@ -116,7 +126,7 @@ export function enterDefault(simtime: number, state: ConcreteState, rt: ActionSc
|
||||||
// same as AND-state, but we only enter the initial state(s)
|
// same as AND-state, but we only enter the initial state(s)
|
||||||
if (state.initial.length > 0) {
|
if (state.initial.length > 0) {
|
||||||
if (state.initial.length > 1) {
|
if (state.initial.length > 1) {
|
||||||
console.warn(state.uid + ': multiple initial states, only entering one of them');
|
throw new NonDeterminismError(`Non-determinism: state '${stateDescription(state)} has multiple (${state.initial.length}) initial states.`, [...state.initial.map(i => i[0]), state.uid]);
|
||||||
}
|
}
|
||||||
const [arrowUid, toEnter] = state.initial[0];
|
const [arrowUid, toEnter] = state.initial[0];
|
||||||
firedTransitions = [...firedTransitions, arrowUid];
|
firedTransitions = [...firedTransitions, arrowUid];
|
||||||
|
|
@ -237,7 +247,7 @@ export function handleEvent(simtime: number, event: RT_Event, statechart: Statec
|
||||||
evalExpr(l.guard, guardEnvironment));
|
evalExpr(l.guard, guardEnvironment));
|
||||||
if (enabled.length > 0) {
|
if (enabled.length > 0) {
|
||||||
if (enabled.length > 1) {
|
if (enabled.length > 1) {
|
||||||
console.warn('nondeterminism!!!!');
|
throw new NonDeterminismError(`Non-determinism: state '${stateDescription(state)}' has multiple (${enabled.length}) enabled outgoing transitions: ${enabled.map(([t]) => transitionDescription(t)).join(', ')}`, [...enabled.map(([t]) => t.uid), state.uid]);
|
||||||
}
|
}
|
||||||
const [t,l] = enabled[0]; // just pick one transition
|
const [t,l] = enabled[0]; // just pick one transition
|
||||||
const arena = computeArena2(t, statechart.transitions);
|
const arena = computeArena2(t, statechart.transitions);
|
||||||
|
|
|
||||||
23
todo.txt
23
todo.txt
|
|
@ -27,19 +27,23 @@
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
|
|
||||||
- must have:
|
- digital watch:
|
||||||
- event parameters on output / internal events
|
highlight when watch button pressed/released
|
||||||
|
|
||||||
|
- maybe support:
|
||||||
- explicit order of:
|
- explicit order of:
|
||||||
- outgoing transitions
|
- outgoing transitions?
|
||||||
- regions in AND-state
|
|
||||||
|
|
||||||
- usability stuff:
|
- usability stuff:
|
||||||
- show internal events
|
- ability to hide statechart and only show the plant?
|
||||||
- hovering over event in side panel should highlight all occurrences of the event in the SC
|
- hovering over event in side panel should highlight all occurrences of the event in the SC
|
||||||
- hovering over error in bottom panel should highlight that rror in the SC
|
- hovering over error in bottom panel should highlight that rror in the SC
|
||||||
- highlight selected shapes while making a selection
|
- highlight selected shapes while making a selection
|
||||||
- comments sometimes snap to transitions even if they belong to a state
|
- comments sometimes snap to transitions even if they belong to a state
|
||||||
|
|
||||||
- highlight fired transitions
|
- highlight fired transitions
|
||||||
|
- highlight about-to-fire transitions
|
||||||
|
|
||||||
- when there is a runtime error, e.g.,
|
- when there is a runtime error, e.g.,
|
||||||
- variable not found
|
- variable not found
|
||||||
- stuck in pseudo-state
|
- stuck in pseudo-state
|
||||||
|
|
@ -50,3 +54,12 @@ TODO
|
||||||
- experimental features:
|
- experimental features:
|
||||||
- multiverse execution history
|
- multiverse execution history
|
||||||
- local scopes
|
- local scopes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for the assignment:
|
||||||
|
*ALL* features
|
||||||
|
add history (look at original Harel paper)
|
||||||
|
|
||||||
|
Publish StateBuddy paper(s):
|
||||||
|
compare CS approach to other tools, not only YAKINDU
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue