can toggle between paused and realtime

This commit is contained in:
Joeri Exelmans 2025-10-10 14:08:46 +02:00
parent 56c77de5bc
commit 3f095bb419
3 changed files with 130 additions and 25 deletions

View file

@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { ConcreteState, emptyStatechart, isAncestorOf, Statechart, stateDescription, Transition } from "../VisualEditor/ast"; import { ConcreteState, emptyStatechart, isAncestorOf, Statechart, stateDescription, Transition } from "../VisualEditor/ast";
import { VisualEditor } from "../VisualEditor/VisualEditor"; import { VisualEditor } from "../VisualEditor/VisualEditor";
@ -8,6 +8,7 @@ import { Action, Expression } from "../VisualEditor/label_ast";
import "../index.css"; import "../index.css";
import "./App.css"; import "./App.css";
import { getSimTime, setPaused, setRealtime, TimeMode } from "../VisualEditor/time";
export function ShowTransition(props: {transition: Transition}) { export function ShowTransition(props: {transition: Transition}) {
return <>&#10132; {stateDescription(props.transition.tgt)}</>; return <>&#10132; {stateDescription(props.transition.tgt)}</>;
@ -69,18 +70,21 @@ export function AST(props: {root: ConcreteState, transitions: Map<string, Transi
export function App() { export function App() {
const [ast, setAST] = useState<Statechart>(emptyStatechart); const [ast, setAST] = useState<Statechart>(emptyStatechart);
const [errors, setErrors] = useState<[string,string][]>([]); const [errors, setErrors] = useState<[string,string][]>([]);
const [rt, setRT] = useState<RT_Statechart[]>([]); const [rt, setRT] = useState<RT_Statechart[]>([]);
const [rtIdx, setRTIdx] = useState<number|null>(null); const [rtIdx, setRTIdx] = useState<number|null>(null);
const [timeMs, setTimeMs] = useState(0);
const [paused, setPaused] = useState(true); const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
const [timescale, setTimescale] = useState(1); const [timescale, setTimescale] = useState(1);
const [displayTime, setDisplayTime] = useState("0.000");
function restart() { function restart() {
const rt = initialize(ast); const rt = initialize(ast);
console.log('runtime: ', rt); console.log('runtime: ', rt);
setRT([rt]); setRT([rt]);
setRTIdx(0); setRTIdx(0);
setTime({kind: "paused", simtime: 0});
} }
function stop() { function stop() {
@ -97,6 +101,54 @@ export function App() {
} }
} }
function updateDisplayedTime() {
const now = performance.now();
const timeMs = getSimTime(time, now);
const leadingZeros = "00" + timeMs % 1000;
const formatted = `${Math.floor(timeMs / 1000)}.${(leadingZeros).substring(leadingZeros.length-3)}`;
// console.log(now, timeMs, formatted);
setDisplayTime(formatted);
}
useEffect(() => {
const interval = setInterval(() => {
updateDisplayedTime();
}, 20);
return () => {
clearInterval(interval);
}
}, [time]);
function onChangePaused(paused: boolean, wallclktime: number) {
setTime(time => {
if (paused) {
console.log('setPaused...');
return setPaused(time, performance.now());
}
else {
console.log('setRealtime...');
return setRealtime(time, timescale, wallclktime);
}
});
updateDisplayedTime();
}
function onTimeScaleChange(newValue: string, wallclktime: number) {
const asFloat = parseFloat(newValue);
if (Number.isNaN(asFloat)) {
return;
}
setTimescale(asFloat);
setTime(time => {
if (time.kind === "paused") {
return time;
}
else {
return setRealtime(time, asFloat, wallclktime);
}
})
}
return <div className="layoutVertical"> return <div className="layoutVertical">
<div className="panel"> <div className="panel">
@ -105,18 +157,23 @@ export function App() {
<button onClick={restart}>(re)start</button> <button onClick={restart}>(re)start</button>
<button onClick={stop} disabled={rt===null}>stop</button> <button onClick={stop} disabled={rt===null}>stop</button>
&emsp; &emsp;
raise raise&nbsp;
{[...ast.inputEvents].map(event => <button disabled={rtIdx===null} onClick={() => raise(event)}>{event}</button>)} {[...ast.inputEvents].map(event => <button disabled={rtIdx===null} onClick={() => raise(event)}>{event}</button>)}
&emsp; &emsp;
<input type="radio" name="paused" id="radio-paused" checked={paused} disabled={rtIdx===null} onChange={e => setPaused(e.target.checked)}/> <input type="radio" name="paused" id="radio-paused" checked={time.kind==="paused"} disabled={rtIdx===null} onChange={e => onChangePaused(e.target.checked, performance.now())}/>
<label htmlFor="radio-paused">paused</label> <label htmlFor="radio-paused">paused</label>
<input type="radio" name="realtime" id="radio-realtime" checked={!paused} disabled={rtIdx===null} onChange={e => setPaused(!e.target.checked)}/> <input type="radio" name="realtime" id="radio-realtime" checked={time.kind==="realtime"} disabled={rtIdx===null} onChange={e => onChangePaused(!e.target.checked, performance.now())}/>
<label htmlFor="radio-realtime">real-time</label> <label htmlFor="radio-realtime">real time</label>
&emsp; &emsp;
<label htmlFor="number-timescale">timescale</label> <label htmlFor="number-timescale">timescale</label>&nbsp;
<input type="number" id="number-timescale" disabled={rtIdx===null} value={timescale} style={{width:40}}/> <input type="number" min={0} id="number-timescale" disabled={rtIdx===null} value={timescale} style={{width:40}} onChange={e => onTimeScaleChange(e.target.value, performance.now())}/>
&emsp; &emsp;
time is {timeMs} ms {rtIdx !== null &&
<>
<label htmlFor="time">time (s)</label>&nbsp;
<input id="time" value={displayTime} readOnly={true} style={{width:56, backgroundColor:"#eee", textAlign: "right"}}/>
</>
}
</div> </div>
<div className="layout"> <div className="layout">
<main className="content"> <main className="content">

View file

@ -1,11 +1,3 @@
// modal configuration: maps child-uid to modal configuration of the child
// for OR-states, only the modal configuration of the current state is kept
// for AND-states, the modal configuration of every child is kept
// for basic states (= empty AND-states), the modal configuration is just an empty object
// export type Mode = {[uid:string]: Mode};
import { Transition } from "./ast";
export type Timestamp = number; // milliseconds since begin of simulation export type Timestamp = number; // milliseconds since begin of simulation
export type Event = string; export type Event = string;
@ -20,12 +12,7 @@ export type RT_Statechart = {
// history: // TODO // history: // TODO
} }
export type BigStep = { export type BigStep = RT_Statechart & {outputEvents: string[]};
from: RT_Statechart;
to: RT_Statechart;
inputEvent: string;
outputEvents: string[];
}
export type RaisedEvents = { export type RaisedEvents = {
internalEvents: string[]; internalEvents: string[];

61
src/VisualEditor/time.ts Normal file
View file

@ -0,0 +1,61 @@
export type TimeMode = TimePaused | TimeRealTime;
export type TimePaused = {
kind: "paused",
simtime: number, // the current simulated time
}
export type TimeRealTime = {
kind: "realtime",
scale: number, // time scale relative to wall-clock time
since: {
simtime: number, // the simulated time at which the time was set to realtime
wallclktime: number, // the wall-clock time at which the time was set to realtime
}
}
export function getSimTime(currentMode: TimeMode, wallclktime: number): number {
if (currentMode.kind === "paused") {
return currentMode.simtime;
}
else {
const elapsedWallclk = wallclktime - currentMode.since.wallclktime;
return currentMode.since.simtime + currentMode.scale * elapsedWallclk;
}
}
export function setRealtime(currentMode: TimeMode, scale: number, wallclktime: number): TimeRealTime {
if (currentMode.kind === "paused") {
return {
kind: "realtime",
scale,
since: {
simtime: currentMode.simtime,
wallclktime,
},
};
}
else {
return {
kind: "realtime",
scale,
since: {
simtime: getSimTime(currentMode, wallclktime),
wallclktime,
},
};
}
}
export function setPaused(currentMode: TimeMode, wallclktime: number): TimePaused {
if (currentMode.kind === "paused") {
return currentMode; // no change
}
else {
return {
kind: "paused",
simtime: getSimTime(currentMode, wallclktime),
};
}
}