timed transition appear to be working

This commit is contained in:
Joeri Exelmans 2025-10-10 17:38:34 +02:00
parent 166fd6d4bc
commit 966b168a74
3 changed files with 64 additions and 13 deletions

View file

@ -3,9 +3,9 @@ import { useEffect, useState } from "react";
import { ConcreteState, emptyStatechart, Statechart, stateDescription, Transition } from "../VisualEditor/ast"; import { ConcreteState, emptyStatechart, Statechart, stateDescription, Transition } from "../VisualEditor/ast";
import { handleInputEvent, initialize } from "../VisualEditor/interpreter"; import { handleInputEvent, initialize } from "../VisualEditor/interpreter";
import { Action, Expression } from "../VisualEditor/label_ast"; import { Action, Expression } from "../VisualEditor/label_ast";
import { BigStep, Environment, Mode } from "../VisualEditor/runtime_types"; import { BigStep, BigStepOutput, Environment, Mode } from "../VisualEditor/runtime_types";
import { VisualEditor } from "../VisualEditor/VisualEditor"; import { VisualEditor } from "../VisualEditor/VisualEditor";
import { getSimTime, setPaused, setRealtime, TimeMode } from "../VisualEditor/time"; import { getSimTime, getWallClkDelay, setPaused, setRealtime, TimeMode } from "../VisualEditor/time";
import "../index.css"; import "../index.css";
import "./App.css"; import "./App.css";
@ -101,15 +101,18 @@ export function App() {
} }
function raise(inputEvent: string) { function raise(inputEvent: string) {
console.log(rtIdx);
if (rt.length>0 && rtIdx!==null && ast.inputEvents.has(inputEvent)) { if (rt.length>0 && rtIdx!==null && ast.inputEvents.has(inputEvent)) {
const simtime = getSimTime(time, performance.now()); const simtime = getSimTime(time, performance.now());
const nextConfig = handleInputEvent(simtime, inputEvent, ast, rt[rtIdx]!); const nextConfig = handleInputEvent(simtime, inputEvent, ast, rt[rtIdx]!);
setRT([...rt.slice(0, rtIdx+1), {inputEvent, simtime, ...nextConfig}]); appendNewConfig(inputEvent, simtime, nextConfig);
setRTIdx(rtIdx+1);
} }
} }
function appendNewConfig(inputEvent: string, simtime: number, config: BigStepOutput) {
setRT([...rt.slice(0, rtIdx!+1), {inputEvent, simtime, ...config}]);
setRTIdx(rtIdx!+1);
}
function updateDisplayedTime() { function updateDisplayedTime() {
const now = performance.now(); const now = performance.now();
const timeMs = getSimTime(time, now); const timeMs = getSimTime(time, now);
@ -120,19 +123,36 @@ export function App() {
const interval = setInterval(() => { const interval = setInterval(() => {
updateDisplayedTime(); updateDisplayedTime();
}, 20); }, 20);
let timeout: NodeJS.Timeout | undefined;
if (time.kind === "realtime" && rtIdx !== null) {
console.log('checking timers...');
const currentRt = rt[rtIdx]!;
const timers = currentRt.environment.get("_timers") || [];
if (timers.length > 0) {
const [nextInterrupt, timeElapsedEvent] = timers[0];
const wallclkDelay = getWallClkDelay(time, nextInterrupt, performance.now());
console.log('scheduling timeout after', wallclkDelay);
timeout = setTimeout(() => {
const nextConfig = handleInputEvent(nextInterrupt, timeElapsedEvent, ast, currentRt);
appendNewConfig('<timer>', nextInterrupt, nextConfig);
}, wallclkDelay);
}
}
return () => { return () => {
clearInterval(interval); clearInterval(interval);
if (timeout) clearTimeout(timeout);
} }
}, [time]); }, [time]);
function onChangePaused(paused: boolean, wallclktime: number) { function onChangePaused(paused: boolean, wallclktime: number) {
setTime(time => { setTime(time => {
if (paused) { if (paused) {
console.log('setPaused...');
return setPaused(time, performance.now()); return setPaused(time, performance.now());
} }
else { else {
console.log('setRealtime...');
return setRealtime(time, timescale, wallclktime); return setRealtime(time, timescale, wallclktime);
} }
}); });

View file

@ -17,6 +17,9 @@ type ActionScope = {
type EnteredScope = { enteredStates: Mode } & ActionScope; type EnteredScope = { enteredStates: Mode } & ActionScope;
type TimerElapseEvent = {state: string, timeDurMs: number};
type Timers = [number, TimerElapseEvent][];
export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope { export function entryActions(simtime: number, state: ConcreteState, actionScope: ActionScope): ActionScope {
for (const action of state.entryActions) { for (const action of state.entryActions) {
(actionScope = execAction(action, actionScope)); (actionScope = execAction(action, actionScope));
@ -24,10 +27,10 @@ export function entryActions(simtime: number, state: ConcreteState, actionScope:
// schedule timers // schedule timers
// we store timers in the environment (dirty!) // we store timers in the environment (dirty!)
const environment = new Map(actionScope.environment); const environment = new Map(actionScope.environment);
const timers: [number,string][] = [...(environment.get("_timers") || [])]; const timers: Timers = [...(environment.get("_timers") || [])];
for (const timeOffset of state.timers) { for (const timeOffset of state.timers) {
const futureSimTime = simtime + timeOffset; // point in simtime when after-trigger becomes enabled const futureSimTime = simtime + timeOffset; // point in simtime when after-trigger becomes enabled
timers.push([futureSimTime, state.uid+'/'+timeOffset]); timers.push([futureSimTime, {state: state.uid, timeDurMs: timeOffset}]);
} }
timers.sort((a,b) => a[0] - b[0]); // smallest futureSimTime comes first timers.sort((a,b) => a[0] - b[0]); // smallest futureSimTime comes first
environment.set("_timers", timers); environment.set("_timers", timers);
@ -40,8 +43,8 @@ export function exitActions(simtime: number, state: ConcreteState, actionScope:
} }
// cancel timers // cancel timers
const environment = new Map(actionScope.environment); const environment = new Map(actionScope.environment);
const timers: [number,string][] = environment.get("_timers") || []; const timers: Timers = environment.get("_timers") || [];
const filtered = timers.filter(([_, timerId]) => !timerId.startsWith(state.uid+'/')); const filtered = timers.filter(([_, {state: s}]) => s !== state.uid);
environment.set("_timers", filtered); environment.set("_timers", filtered);
return {...actionScope, environment}; return {...actionScope, environment};
} }
@ -185,12 +188,30 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
throw new Error("should never reach here"); throw new Error("should never reach here");
} }
export function handleEvent(simtime: number, event: string, statechart: Statechart, activeParent: ConcreteState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents { export function handleEvent(simtime: number, event: string | TimerElapseEvent, statechart: Statechart, activeParent: ConcreteState, {environment, mode, ...raised}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
const arenasFired = new Set<OrState>(); const arenasFired = new Set<OrState>();
for (const state of activeParent.children) { for (const state of activeParent.children) {
if (mode.has(state.uid)) { if (mode.has(state.uid)) {
const outgoing = statechart.transitions.get(state.uid) || []; const outgoing = statechart.transitions.get(state.uid) || [];
const triggered = outgoing.filter(transition => transition.label[0].trigger.kind === "event" && transition.label[0].trigger.event === event); let triggered;
if (typeof event === 'string') {
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "event") {
return trigger.event === event;
}
return false;
});
}
else {
triggered = outgoing.filter(transition => {
const trigger = transition.label[0].trigger;
if (trigger.kind === "after") {
return trigger.durationMs === event.timeDurMs;
}
return false;
});
}
const enabled = triggered.filter(transition => const enabled = triggered.filter(transition =>
evalExpr(transition.label[0].guard, environment) evalExpr(transition.label[0].guard, environment)
); );

View file

@ -21,6 +21,7 @@ export type TimeRealTime = {
scale: number, // time scale relative to wall-clock time scale: number, // time scale relative to wall-clock time
} }
// given a wall-clock time, how does it translate to simtime?
export function getSimTime(currentMode: TimeMode, wallclktime: number): number { export function getSimTime(currentMode: TimeMode, wallclktime: number): number {
if (currentMode.kind === "paused") { if (currentMode.kind === "paused") {
return currentMode.simtime; return currentMode.simtime;
@ -31,6 +32,14 @@ export function getSimTime(currentMode: TimeMode, wallclktime: number): number {
} }
} }
// given a simulated real time clock, how long will it take in wall-clock time duration until 'simtime' is the current time?
export function getWallClkDelay(realtime: TimeRealTime, simtime: number, wallclktime: number): number {
const currentSimTime = getSimTime(realtime, wallclktime);
const simtimeDelay = simtime - currentSimTime;
return Math.max(0, simtimeDelay / realtime.scale);
}
// given a current simulated clock (paused or real time), switch to real time with given time scale
export function setRealtime(currentMode: TimeMode, scale: number, wallclktime: number): TimeRealTime { export function setRealtime(currentMode: TimeMode, scale: number, wallclktime: number): TimeRealTime {
if (currentMode.kind === "paused") { if (currentMode.kind === "paused") {
return { return {
@ -54,6 +63,7 @@ export function setRealtime(currentMode: TimeMode, scale: number, wallclktime: n
} }
} }
// given a current simulated clock (paused or real time), switch to paused
export function setPaused(currentMode: TimeMode, wallclktime: number): TimePaused { export function setPaused(currentMode: TimeMode, wallclktime: number): TimePaused {
if (currentMode.kind === "paused") { if (currentMode.kind === "paused") {
return currentMode; // no change return currentMode; // no change