performance and usability improvements

This commit is contained in:
Joeri Exelmans 2025-10-23 19:16:46 +02:00
parent a25396b6f2
commit ab988898c0
18 changed files with 381 additions and 206 deletions

View file

@ -9,12 +9,12 @@ details > summary {
}
/* these two rules add a bit of padding to an opened <details> node */
details:open > summary {
/* details:open > summary:has(+ *) {
margin-bottom: 4px;
}
details:open {
details:open:has(>summary:has(+ *)) {
padding-bottom: 8px;
}
} */
details > summary:hover {
background-color: #eee;
@ -46,30 +46,33 @@ details > summary:hover {
.outputEvent {
border: 1px black solid;
border-radius: 6px;
margin-left: 4px;
/* margin-left: 4px; */
padding-left: 2px;
padding-right: 2px;
background-color: rgb(230, 249, 255);
color: black;
display: inline-block;
}
.internalEvent {
border: 1px black solid;
border-radius: 6px;
margin-left: 4px;
/* margin-left: 4px; */
padding-left: 2px;
padding-right: 2px;
background-color: rgb(255, 218, 252);
color: black;
display: inline-block;
}
.inputEvent {
border: 1px black solid;
border-radius: 6px;
margin-left: 4px;
/* margin-left: 4px; */
padding-left: 2px;
padding-right: 2px;
background-color: rgb(224, 247, 209);
color: black;
display: inline-block;
}
.inputEvent * {
@ -116,4 +119,4 @@ ul {
.shadowBelow {
box-shadow: 0 -15px 15px 15px rgba(0, 0, 0, 0.4);
z-index: 1;
}
}

View file

@ -35,6 +35,11 @@ details:has(+ details) {
/* border: solid black 3px; */
border: solid blue 1px;
}
.runtimeState.runtimeError {
background-color: lightpink;
color: darkred;
}
/* details:not(:has(details)) > summary::marker {
color: white;
} */

View file

@ -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 { handleInputEvent, initialize } from "../statecharts/interpreter";
import { BigStep, BigStepOutput } from "../statecharts/runtime_types";
import { emptyStatechart, Statechart, Transition } from "../statecharts/abstract_syntax";
import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter";
import { BigStep, BigStepOutput, RT_Event } from "../statecharts/runtime_types";
import { InsertMode, VisualEditor, VisualEditorState } from "../VisualEditor/VisualEditor";
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
@ -12,19 +12,19 @@ import "./App.css";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { TopPanel } from "./TopPanel";
import { RTHistory } from "./RTHistory";
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
import { TraceableError } from "../statecharts/parser";
import { getKeyHandler } from "./shortcut_handler";
import { BottomPanel } from "./BottomPanel";
import { emptyState } from "@/statecharts/concrete_syntax";
import { PersistentDetails } from "./PersistentDetails";
import { DigitalWatch, DigitalWatchPlant } from "@/Plant/DigitalWatch/DigitalWatch";
import { DigitalWatchPlant } from "@/Plant/DigitalWatch/DigitalWatch";
import { DummyPlant } from "@/Plant/Dummy/Dummy";
import { Plant } from "@/Plant/Plant";
import { usePersistentState } from "@/util/persistent_state";
import { RTHistory } from "./RTHistory";
type EditHistory = {
export type EditHistory = {
current: VisualEditorState,
history: VisualEditorState[],
future: VisualEditorState[],
@ -35,13 +35,46 @@ const plants: [string, Plant<any>][] = [
["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() {
const [mode, setMode] = useState<InsertMode>("and");
const [historyState, setHistoryState] = useState<EditHistory>({current: emptyState, history: [], future: []});
const [ast, setAST] = useState<Statechart>(emptyStatechart);
const [errors, setErrors] = useState<TraceableError[]>([]);
const [rt, setRT] = useState<BigStep[]>([]);
const [rtIdx, setRTIdx] = useState<number|undefined>();
const [trace, setTrace] = useState<TraceState|null>(null);
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
const [modal, setModal] = useState<ReactElement|null>(null);
@ -58,7 +91,7 @@ export function App() {
const refRightSideBar = useRef<HTMLDivElement>(null);
// append editor state to undo history
function makeCheckPoint() {
setHistoryState(historyState => ({
...historyState,
@ -92,54 +125,124 @@ export function App() {
}
function onInit() {
const config = initialize(ast);
setRT([{inputEvent: null, simtime: 0, ...config}]);
setRTIdx(0);
const timestampedEvent = {simtime: 0, inputEvent: "<init>"};
let config;
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});
scrollDownSidebar();
}
function onClear() {
setRT([]);
setRTIdx(undefined);
setTrace(null);
setTime({kind: "paused", simtime: 0});
}
// raise input event, producing a new runtime configuration (or a runtime error)
function onRaise(inputEvent: string, param: any) {
if (rt.length>0 && rtIdx!==undefined && ast.inputEvents.some(e => e.event === inputEvent)) {
const simtime = getSimTime(time, Math.round(performance.now()));
const nextConfig = handleInputEvent(simtime, {kind: "input", name: inputEvent, param}, ast, rt[rtIdx]!);
appendNewConfig(inputEvent, simtime, nextConfig);
if (trace !== null && ast.inputEvents.some(e => e.event === inputEvent)) {
const config = current(trace);
if (config.kind === "bigstep") {
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) {
setRT([...rt.slice(0, rtIdx!+1), {inputEvent, simtime, ...config}]);
setRTIdx(rtIdx!+1);
// console.log('new config:', config);
let newItem: TraceItem;
try {
const nextConfig = handleInputEvent(simtime, event, ast, config as BigStep); // may throw
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();
}
function onBack() {
setTime(() => {
if (rtIdx !== undefined) {
if (rtIdx > 0)
if (trace !== null) {
setTime(() => {
if (trace !== null) {
return {
kind: "paused",
simtime: rt[rtIdx-1].simtime,
simtime: trace.trace[trace.idx-1].simtime,
}
}
return { kind: "paused", simtime: 0 };
});
setRTIdx(rtIdx => {
if (rtIdx !== undefined) {
if (rtIdx > 0)
return rtIdx - 1;
else
return 0;
}
else return undefined;
})
}
return { kind: "paused", simtime: 0 };
});
setTrace({
...trace,
idx: trace.idx-1,
});
}
}
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(() => {
const onKeyDown = getKeyHandler(setMode);
window.addEventListener("keydown", onKeyDown);
@ -197,27 +270,28 @@ export function App() {
};
}, []);
// const highlightActive = (rtIdx !== undefined) && new Set([...rt[rtIdx].mode].filter(uid => {
// const state = ast.uid2State.get(uid);
// return state && state.parent?.kind !== "and";
// })) || new Set();
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);
let highlightActive: Set<string>;
let highlightTransitions: string[];
if (trace === null) {
highlightActive = new Set();
highlightTransitions = [];
}
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 <>
@ -250,13 +324,12 @@ export function App() {
}}
>
<TopPanel
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
{...{rtIdx, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys}}
{...{trace, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history: historyState}}
/>
</Box>
{/* Below the top bar: Editor */}
<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>
</Stack>
</Box>
@ -272,18 +345,22 @@ export function App() {
}}>
<Stack sx={{height:'100%'}}>
<Box
className="shadowBelow"
className={showExecutionTrace ? "shadowBelow" : ""}
sx={{flex: '0 0 content', backgroundColor: ''}}
>
<PersistentDetails localStorageKey="showStateTree" initiallyOpen={true}>
<summary>state tree</summary>
<ul>
<ShowAST {...{...ast, rt: rt.at(rtIdx!), highlightActive}}/>
<ShowAST {...{...ast, trace, highlightActive}}/>
</ul>
</PersistentDetails>
<PersistentDetails localStorageKey="showInputEvents" initiallyOpen={true}>
<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 localStorageKey="showInternalEvents" initiallyOpen={true}>
<summary>internal events</summary>
@ -293,20 +370,6 @@ export function App() {
<summary>output events</summary>
<ShowOutputEvents outputEvents={ast.outputEvents}/>
</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}>
<summary>plant</summary>
<select
@ -316,8 +379,28 @@ export function App() {
<option>{plantName}</option>
)}
</select>
{rtIdx!==undefined && <plant.render {...plantStates[rtIdx]}/>}
{trace !== null &&
plant.render(trace.trace[trace.idx].plantState, event => onRaise(event.name, event.param))}
</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>
</Stack>
</Box>

View file

@ -1,14 +1,16 @@
import { Stack } from "@mui/material";
import { Box, Stack } from "@mui/material";
export function KeyInfoVisible(props: {keyInfo, children}) {
return <Stack style={{display: "inline-block"}}>
<div style={{fontSize:11, height: 18, textAlign:"center", paddingLeft: 3, paddingRight: 3}}>
{props.keyInfo}
</div>
<div style={{textAlign:"center"}}>
{props.children}
</div>
</Stack>
export function KeyInfoVisible(props: {keyInfo, children, horizontal?: boolean}) {
return <div style={{display: 'inline-block'}}>
{/* <Stack direction={props.horizontal ? "row" : "column"}> */}
<div style={{display: props.horizontal ? 'inline-block' : '', fontSize:11, height: 18, textAlign:"center", paddingLeft: 3, paddingRight: 3}}>
{props.keyInfo}
</div>
<div style={{display: props.horizontal ? 'inline-block' : '', textAlign:"center"}}>
{props.children}
</div>
{/* </Stack> */}
</div>
}
export function KeyInfoHidden(props: {children}) {

View file

@ -1,41 +1,61 @@
import { Dispatch, Ref, SetStateAction } from "react";
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 { TimeMode } from "../statecharts/time";
import { TraceState } from "./App";
type RTHistoryProps = {
rt: BigStep[],
rtIdx: number | undefined,
trace: TraceState|null,
setTrace: Dispatch<SetStateAction<TraceState|null>>;
ast: Statechart,
setRTIdx: Dispatch<SetStateAction<number|undefined>>,
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) {
setRTIdx(idx);
setTrace(trace => trace && {
...trace,
idx,
});
setTime({kind: "paused", simtime: timestamp});
}
if (trace === null) {
return <></>;
}
return <div>
{rt.map((r, idx) => <>
<div
className={"runtimeState"+(idx===rtIdx?" active":"")}
onClick={() => gotoRt(idx, r.simtime)}>
<div>
{formatTime(r.simtime)}
&emsp;
<div className="inputEvent">{r.inputEvent || "<init>"}</div>
</div>
<ShowMode mode={r.mode.difference(rt[idx-1]?.mode || new Set())} statechart={ast}/>
<ShowEnvironment environment={r.environment}/>
{r.outputEvents.length>0 && <>^
{r.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
</>}
{/* <hr/> */}
</div></>)}
{trace.trace.map((item, i) => {
if (item.kind === "bigstep") {
const newStates = item.mode.difference(trace.trace[i-1]?.mode || new Set());
return <div
className={"runtimeState" + (i === trace.idx ? " active" : "")}
onClick={() => gotoRt(i, item.simtime)}>
<div>
{formatTime(item.simtime)}
&emsp;
<div className="inputEvent">{item.inputEvent || "<init>"}</div>
</div>
<ShowMode mode={newStates} statechart={ast}/>
<ShowEnvironment environment={item.environment}/>
{item.outputEvents.length>0 && <>^
{item.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
</>}
</div>;
}
else {
return <div className="runtimeState runtimeError">
<div>
{formatTime(item.simtime)}
&emsp;
<div className="inputEvent">{item.inputEvent}</div>
</div>
<div>
{item.error.message}
</div>
</div>;
}
})}
</div>;
}

View file

@ -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 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 &&
<ul>
{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>
}
@ -74,6 +74,7 @@ export function ShowAST(props: {root: ConcreteState | PseudoState, transitions:
import BoltIcon from '@mui/icons-material/Bolt';
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
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}) {
const raiseHandlers = inputEvents.map(({event}) => {
@ -110,7 +111,7 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp
const shortcut = (i+1)%10;
const KI = (i <= 10) ? KeyInfo : KeyInfoHidden;
return <div key={event+'/'+paramName} className="toolbarGroup">
<KI keyInfo={<kbd>{shortcut}</kbd>}>
<KI keyInfo={<kbd>{shortcut}</kbd>} horizontal={true}>
<button
className="inputEvent"
title={`raise this input event`}

View file

@ -25,10 +25,12 @@ import { About } from "./About";
import { usePersistentState } from "@/util/persistent_state";
import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons";
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters";
import { EditHistory, TraceState } from "./App";
export type TopPanelProps = {
rt?: BigStep,
rtIdx?: number,
trace: TraceState | null,
// rt?: BigStep,
// rtIdx?: number,
time: TimeMode,
setTime: Dispatch<SetStateAction<TimeMode>>,
onUndo: () => void,
@ -45,12 +47,15 @@ export type TopPanelProps = {
setZoom: Dispatch<SetStateAction<number>>,
showKeys: 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 [timescale, setTimescale] = useState(1);
const config = trace && trace.trace[trace.idx];
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
useEffect(() => {
@ -58,8 +63,9 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
if (!e.ctrlKey) {
if (e.key === " ") {
e.preventDefault();
if (rt)
onChangePaused(time.kind !== "paused", Math.round(performance.now()));
if (config) {
onChangePaused(time.kind !== "paused", Math.round(performance.now()));
}
};
if (e.key === "i") {
e.preventDefault();
@ -70,7 +76,7 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
onClear();
}
if (e.key === "Tab") {
if (rtIdx === undefined) {
if (trace === null) {
onInit();
}
else {
@ -175,7 +181,7 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
}
// 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];
function onSkip() {
@ -225,10 +231,10 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
{/* undo / redo */}
<div className="toolbarGroup">
<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"/>&nbsp;({history.history.length})</button>
</KeyInfo>
<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"/>&nbsp;({history.future.length})</button>
</KeyInfo>
&emsp;
</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>
</KeyInfo>
<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>
&emsp;
<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="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="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={!config || time.kind==="realtime"} className={(config && time.kind==="realtime") ? "active":""} onClick={() => onChangePaused(false, Math.round(performance.now()))}><PlayArrowIcon fontSize="small"/></button>
</KeyInfo>
&emsp;
</div>
@ -290,12 +296,12 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
<div className="toolbarGroup">
<div className="toolbarGroup">
<label htmlFor="time">time (s)</label>&nbsp;
<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>
&emsp;
<div className="toolbarGroup">
<label htmlFor="next-timeout">next (s)</label>&nbsp;
<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>}>
<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>

View file

@ -11,3 +11,15 @@ export function compactTime(timeMs: number) {
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;
}
}