plant has its own timed/reactive behavior - typically another statechart
This commit is contained in:
parent
b50f52496a
commit
3e5dca437b
19 changed files with 401 additions and 241 deletions
|
|
@ -33,6 +33,9 @@ details:has(+ details) {
|
||||||
background-color: rgba(0,0,255,0.2);
|
background-color: rgba(0,0,255,0.2);
|
||||||
border: solid blue 1px;
|
border: solid blue 1px;
|
||||||
}
|
}
|
||||||
|
.runtimeState.plantStep * {
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
.runtimeState.runtimeError {
|
.runtimeState.runtimeError {
|
||||||
background-color: lightpink;
|
background-color: lightpink;
|
||||||
|
|
|
||||||
159
src/App/App.tsx
159
src/App/App.tsx
|
|
@ -1,54 +1,26 @@
|
||||||
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter";
|
|
||||||
import { BigStepOutput, RT_Event, RT_Statechart } from "../statecharts/runtime_types";
|
|
||||||
import { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
|
|
||||||
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
|
|
||||||
|
|
||||||
import "../index.css";
|
import "../index.css";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
import { TopPanel } from "./TopPanel/TopPanel";
|
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
|
|
||||||
import { parseStatechart } from "../statecharts/parser";
|
|
||||||
import { getKeyHandler } from "./VisualEditor/shortcut_handler";
|
|
||||||
import { BottomPanel } from "./BottomPanel";
|
|
||||||
import { emptyState } from "@/statecharts/concrete_syntax";
|
import { emptyState } from "@/statecharts/concrete_syntax";
|
||||||
import { PersistentDetails } from "./PersistentDetails";
|
|
||||||
import { DigitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch";
|
|
||||||
import { DummyPlant } from "./Plant/Dummy/Dummy";
|
|
||||||
import { Plant } from "./Plant/Plant";
|
|
||||||
import { usePersistentState } from "./persistent_state";
|
|
||||||
import { RTHistory } from "./RTHistory";
|
|
||||||
import { detectConnections } from "@/statecharts/detect_connections";
|
import { detectConnections } from "@/statecharts/detect_connections";
|
||||||
|
import { Conns, coupledExecution, EventDestination, exposeStatechartInputs, statechartExecution } from "@/statecharts/timed_reactive";
|
||||||
|
import { RuntimeError } from "../statecharts/interpreter";
|
||||||
|
import { parseStatechart } from "../statecharts/parser";
|
||||||
|
import { BigStep, RaisedEvent } from "../statecharts/runtime_types";
|
||||||
|
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
|
||||||
|
import { BottomPanel } from "./BottomPanel";
|
||||||
|
import { usePersistentState } from "./persistent_state";
|
||||||
|
import { PersistentDetails } from "./PersistentDetails";
|
||||||
|
import { DummyPlant } from "./Plant/Dummy/Dummy";
|
||||||
import { MicrowavePlant } from "./Plant/Microwave/Microwave";
|
import { MicrowavePlant } from "./Plant/Microwave/Microwave";
|
||||||
import { coupledExecution, dummyExecution, exposeStatechartInputs, statechartExecution, TimedReactive } from "@/statecharts/timed_reactive";
|
import { autoConnect, exposePlantInputs, Plant } from "./Plant/Plant";
|
||||||
|
import { RTHistory } from "./RTHistory";
|
||||||
// const clock1: TimedReactive<{nextTick: number}> = {
|
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
|
||||||
// initial: () => ({nextTick: 1}),
|
import { TopPanel } from "./TopPanel/TopPanel";
|
||||||
// timeAdvance: (c) => c.nextTick,
|
import { getKeyHandler } from "./VisualEditor/shortcut_handler";
|
||||||
// intTransition: (c) => [[{name: "tick"}], {nextTick: c.nextTick+1}],
|
import { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
|
||||||
// extTransition: (simtime, c, e) => [[], (c)],
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const clock2: TimedReactive<{nextTick: number}> = {
|
|
||||||
// initial: () => ({nextTick: 0.5}),
|
|
||||||
// timeAdvance: (c) => c.nextTick,
|
|
||||||
// intTransition: (c) => [[{name: "tick"}], {nextTick: c.nextTick+1}],
|
|
||||||
// extTransition: (simtime, c, e) => [[], (c)],
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const coupled = coupledExecution({clock1, clock2}, {inputEvents: {}, outputEvents: {
|
|
||||||
// clock1: {tick: {kind:"output", eventName: 'tick'}},
|
|
||||||
// clock2: {tick: {kind:"output", eventName: 'tick'}},
|
|
||||||
// }})
|
|
||||||
|
|
||||||
// let state = coupled.initial();
|
|
||||||
// for (let i=0; i<10; i++) {
|
|
||||||
// const nextWakeup = coupled.timeAdvance(state);
|
|
||||||
// console.log({state, nextWakeup});
|
|
||||||
// [[], state] = coupled.intTransition(state);
|
|
||||||
// }
|
|
||||||
|
|
||||||
export type EditHistory = {
|
export type EditHistory = {
|
||||||
current: VisualEditorState,
|
current: VisualEditorState,
|
||||||
|
|
@ -58,8 +30,9 @@ export type EditHistory = {
|
||||||
|
|
||||||
const plants: [string, Plant<any>][] = [
|
const plants: [string, Plant<any>][] = [
|
||||||
["dummy", DummyPlant],
|
["dummy", DummyPlant],
|
||||||
["digital watch", DigitalWatchPlant],
|
|
||||||
["microwave", MicrowavePlant],
|
["microwave", MicrowavePlant],
|
||||||
|
|
||||||
|
// ["digital watch", DigitalWatchPlant],
|
||||||
]
|
]
|
||||||
|
|
||||||
export type TraceItemError = {
|
export type TraceItemError = {
|
||||||
|
|
@ -69,13 +42,13 @@ export type TraceItemError = {
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoupledState = {
|
type CoupledState = {
|
||||||
sc: BigStepOutput,
|
sc: BigStep,
|
||||||
plant: any,
|
plant: BigStep,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TraceItem =
|
export type TraceItem =
|
||||||
{ kind: "error" } & TraceItemError
|
{ kind: "error" } & TraceItemError
|
||||||
| { kind: "bigstep", simtime: number, cause: string, state: CoupledState };
|
| { kind: "bigstep", simtime: number, cause: string, state: CoupledState, outputEvents: RaisedEvent[] };
|
||||||
|
|
||||||
export type TraceState = {
|
export type TraceState = {
|
||||||
// executor: TimedReactive<CoupledState>,
|
// executor: TimedReactive<CoupledState>,
|
||||||
|
|
@ -83,10 +56,6 @@ export type TraceState = {
|
||||||
idx: number,
|
idx: number,
|
||||||
}; // <-- null if there is no trace
|
}; // <-- 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 {
|
// function getPlantState<T>(plant: Plant<T>, trace: TraceItem[], idx: number): T | null {
|
||||||
// if (idx === -1) {
|
// if (idx === -1) {
|
||||||
// return plant.initial;
|
// return plant.initial;
|
||||||
|
|
@ -240,21 +209,27 @@ export function App() {
|
||||||
}
|
}
|
||||||
}, [refRightSideBar.current]);
|
}, [refRightSideBar.current]);
|
||||||
|
|
||||||
const cE = useMemo(() => ast && coupledExecution({sc: statechartExecution(ast), plant: dummyExecution}, exposeStatechartInputs(ast, "sc")), [ast]);
|
const plantConns = ast && ({
|
||||||
|
inputEvents: {
|
||||||
|
...exposeStatechartInputs(ast, "sc", (eventName: string) => "DEBUG_"+eventName),
|
||||||
|
...exposePlantInputs(plant, "plant", (eventName: string) => "PLANT_UI_"+eventName),
|
||||||
|
},
|
||||||
|
outputEvents: autoConnect(ast, "sc", plant, "plant"),
|
||||||
|
}) as Conns;
|
||||||
|
const cE = useMemo(() => ast && coupledExecution({
|
||||||
|
sc: statechartExecution(ast),
|
||||||
|
plant: plant.execution,
|
||||||
|
}, plantConns!), [ast]);
|
||||||
|
|
||||||
const onInit = useCallback(() => {
|
const onInit = useCallback(() => {
|
||||||
if (cE === null) return;
|
if (cE === null) return;
|
||||||
const metadata = {simtime: 0, cause: "<init>"};
|
const metadata = {simtime: 0, cause: "<init>"};
|
||||||
try {
|
try {
|
||||||
const state = cE.initial(); // may throw if initialing the statechart results in a RuntimeError
|
const [outputEvents, state] = cE.initial(); // may throw if initialing the statechart results in a RuntimeError
|
||||||
setTrace({
|
setTrace({
|
||||||
trace: [{kind: "bigstep", ...metadata, state}],
|
trace: [{kind: "bigstep", ...metadata, state, outputEvents}],
|
||||||
idx: 0,
|
idx: 0,
|
||||||
});
|
});
|
||||||
// config = initialize(ast);
|
|
||||||
// const item = {kind: "bigstep", ...timestampedEvent, ...config};
|
|
||||||
// const plantState = getPlantState(plant, [item], 0);
|
|
||||||
// setTrace({trace: [{...item, plantState}], idx: 0});
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
if (error instanceof RuntimeError) {
|
if (error instanceof RuntimeError) {
|
||||||
|
|
@ -262,7 +237,6 @@ export function App() {
|
||||||
trace: [{kind: "error", ...metadata, error}],
|
trace: [{kind: "error", ...metadata, error}],
|
||||||
idx: 0,
|
idx: 0,
|
||||||
});
|
});
|
||||||
// setTrace({trace: [{kind: "error", ...timestampedEvent, error}], idx: 0});
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw error; // probably a bug in the interpreter
|
throw error; // probably a bug in the interpreter
|
||||||
|
|
@ -291,8 +265,7 @@ export function App() {
|
||||||
if (currentTraceItem.kind === "bigstep") {
|
if (currentTraceItem.kind === "bigstep") {
|
||||||
const simtime = getSimTime(time, Math.round(performance.now()));
|
const simtime = getSimTime(time, Math.round(performance.now()));
|
||||||
appendNewConfig(simtime, inputEvent, () => {
|
appendNewConfig(simtime, inputEvent, () => {
|
||||||
const [_, newState] = cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param});
|
return cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param});
|
||||||
return newState;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -307,8 +280,7 @@ export function App() {
|
||||||
|
|
||||||
const raiseTimeEvent = () => {
|
const raiseTimeEvent = () => {
|
||||||
appendNewConfig(nextTimeout, "<timer>", () => {
|
appendNewConfig(nextTimeout, "<timer>", () => {
|
||||||
const [_, newState] = cE.intTransition(currentTraceItem.state);
|
return cE.intTransition(currentTraceItem.state);
|
||||||
return newState;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -330,12 +302,12 @@ export function App() {
|
||||||
}
|
}
|
||||||
}, [time, trace]); // <-- todo: is this really efficient?
|
}, [time, trace]); // <-- todo: is this really efficient?
|
||||||
|
|
||||||
function appendNewConfig(simtime: number, cause: string, computeNewState: () => CoupledState) {
|
function appendNewConfig(simtime: number, cause: string, computeNewState: () => [RaisedEvent[], CoupledState]) {
|
||||||
let newItem: TraceItem;
|
let newItem: TraceItem;
|
||||||
const metadata = {simtime, cause}
|
const metadata = {simtime, cause}
|
||||||
try {
|
try {
|
||||||
const state = computeNewState(); // may throw RuntimeError
|
const [outputEvents, state] = computeNewState(); // may throw RuntimeError
|
||||||
newItem = {kind: "bigstep", ...metadata, state};
|
newItem = {kind: "bigstep", ...metadata, state, outputEvents};
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
if (error instanceof RuntimeError) {
|
if (error instanceof RuntimeError) {
|
||||||
|
|
@ -397,6 +369,9 @@ export function App() {
|
||||||
const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || [];
|
const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || [];
|
||||||
|
|
||||||
const [showExecutionTrace, setShowExecutionTrace] = usePersistentState("showExecutionTrace", true);
|
const [showExecutionTrace, setShowExecutionTrace] = usePersistentState("showExecutionTrace", true);
|
||||||
|
const [showPlantTrace, setShowPlantTrace] = usePersistentState("showPlantTrace", false);
|
||||||
|
|
||||||
|
const speed = time.kind === "paused" ? 0 : time.scale;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
|
||||||
|
|
@ -437,11 +412,10 @@ export function App() {
|
||||||
|
|
||||||
{/* Right: sidebar */}
|
{/* Right: sidebar */}
|
||||||
<div style={{
|
<div style={{
|
||||||
borderLeft: 1,
|
|
||||||
borderColor: "divider",
|
|
||||||
flex: '0 0 content',
|
flex: '0 0 content',
|
||||||
|
borderLeft: '1px solid lightgrey',
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
overflowX: "visible",
|
overflowX: "auto",
|
||||||
maxWidth: 'min(400px, 50vw)',
|
maxWidth: 'min(400px, 50vw)',
|
||||||
}}>
|
}}>
|
||||||
<div className="stackVertical" style={{height:'100%'}}>
|
<div className="stackVertical" style={{height:'100%'}}>
|
||||||
|
|
@ -459,7 +433,7 @@ export function App() {
|
||||||
<summary>input events</summary>
|
<summary>input events</summary>
|
||||||
{ast && <ShowInputEvents
|
{ast && <ShowInputEvents
|
||||||
inputEvents={ast.inputEvents}
|
inputEvents={ast.inputEvents}
|
||||||
onRaise={onRaise}
|
onRaise={(e,p) => onRaise("DEBUG_"+e,p)}
|
||||||
disabled={trace===null || trace.trace[trace.idx].kind === "error"}
|
disabled={trace===null || trace.trace[trace.idx].kind === "error"}
|
||||||
showKeys={showKeys}/>}
|
showKeys={showKeys}/>}
|
||||||
</PersistentDetails>
|
</PersistentDetails>
|
||||||
|
|
@ -481,16 +455,15 @@ export function App() {
|
||||||
<option>{plantName}</option>
|
<option>{plantName}</option>
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
{/* {trace !== null && trace.trace[trace.idx].plantState &&
|
{plantConns && <ShowConns {...plantConns} />}
|
||||||
<div>{
|
{currentBigStep && <plant.render state={currentBigStep.state.plant} speed={speed}
|
||||||
plant.render(
|
raiseInput={e => onRaise("PLANT_UI_"+e.name, e.param)}
|
||||||
trace.trace[trace.idx].plantState,
|
raiseOutput={() => {}}
|
||||||
event => onRaise(event.name, event.param),
|
/>}
|
||||||
time.kind === "paused" ? 0 : time.scale,
|
|
||||||
)
|
|
||||||
}</div>} */}
|
|
||||||
</PersistentDetails>
|
</PersistentDetails>
|
||||||
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary></details>
|
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary>
|
||||||
|
<input id="checkbox-show-plant-items" type="checkbox" checked={showPlantTrace} onChange={e => setShowPlantTrace(e.target.checked)}/><label htmlFor="checkbox-show-plant-items">show plant steps</label>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* We cheat a bit, and render the execution trace depending on whether the <details> above is 'open' or not, rather than putting it as a child of the <details>. We do this because only then can we get the execution trace to scroll without the rest scrolling as well. */}
|
{/* We cheat a bit, and render the execution trace depending on whether the <details> above is 'open' or not, rather than putting it as a child of the <details>. We do this because only then can we get the execution trace to scroll without the rest scrolling as well. */}
|
||||||
|
|
@ -502,10 +475,9 @@ export function App() {
|
||||||
// minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
|
// minHeight: '75%', // <-- allows us to always scroll down the sidebar far enough such that the execution history is enough in view
|
||||||
}}>
|
}}>
|
||||||
<div ref={refRightSideBar}>
|
<div ref={refRightSideBar}>
|
||||||
{ast && <RTHistory {...{ast, trace, setTrace, setTime}}/>}
|
{ast && <RTHistory {...{ast, trace, setTrace, setTime, showPlantTrace}}/>}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
<div style={{flex: '0 0 content'}}>
|
<div style={{flex: '0 0 content'}}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -520,4 +492,25 @@ export function App() {
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ShowEventDestination(dst: EventDestination) {
|
||||||
|
if (dst.kind === "model") {
|
||||||
|
return <>{dst.model}.{dst.eventName}</>;
|
||||||
|
}
|
||||||
|
else if (dst.kind === "output") {
|
||||||
|
return <>{dst.eventName}</>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return <>🗑</>; // <-- garbage can icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ShowConns({inputEvents, outputEvents}: Conns) {
|
||||||
|
return <div>
|
||||||
|
<div style={{color: "grey"}}>
|
||||||
|
{Object.entries(inputEvents).map(([eventName, destination]) => <div>{eventName} → <ShowEventDestination {...destination}/></div>)}
|
||||||
|
</div>
|
||||||
|
{Object.entries(outputEvents).map(([modelName, mapping]) => <>{Object.entries(mapping).map(([eventName, destination]) => <div>{modelName}.{eventName} → <ShowEventDestination {...destination}/></div>)}</>)}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
import { RaisedEvent } from "@/statecharts/runtime_types";
|
|
||||||
import { Plant } from "../Plant";
|
import { Plant } from "../Plant";
|
||||||
|
import { TimedReactive } from "@/statecharts/timed_reactive";
|
||||||
|
|
||||||
export const DummyPlant: Plant<{}> = {
|
export const dummyExecution: TimedReactive<null> = {
|
||||||
|
initial: () => [[], null],
|
||||||
|
timeAdvance: () => Infinity,
|
||||||
|
intTransition: () => { throw new Error("dummy never makes intTransition"); },
|
||||||
|
extTransition: () => [[], null],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DummyPlant: Plant<null> = {
|
||||||
inputEvents: [],
|
inputEvents: [],
|
||||||
outputEvents: [],
|
outputEvents: [],
|
||||||
initial: () => ({}),
|
execution: dummyExecution,
|
||||||
reduce: (_inputEvent: RaisedEvent, _state: {}) => ({}),
|
render: (props) => <></>,
|
||||||
render: (_state: {}, _raise: (event: RaisedEvent) => void) => <></>,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,9 @@ rect.microwaveButtonHelper:active {
|
||||||
}
|
}
|
||||||
|
|
||||||
rect.microwaveDoorHelper {
|
rect.microwaveDoorHelper {
|
||||||
|
fill: rgba(46, 211, 197);
|
||||||
fill-opacity: 0;
|
fill-opacity: 0;
|
||||||
}
|
}
|
||||||
|
rect.microwaveDoorHelper:hover {
|
||||||
|
fill-opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
@ -2,59 +2,31 @@ import { preload } from "react-dom";
|
||||||
import imgSmallClosedOff from "./small_closed_off.png";
|
import imgSmallClosedOff from "./small_closed_off.png";
|
||||||
import imgSmallClosedOn from "./small_closed_on.png";
|
import imgSmallClosedOn from "./small_closed_on.png";
|
||||||
import imgSmallOpenedOff from "./small_opened_off.png";
|
import imgSmallOpenedOff from "./small_opened_off.png";
|
||||||
import imgSmallOpenedOn from "./small_opened_off.png";
|
import imgSmallOpenedOn from "./small_opened_on.png";
|
||||||
|
|
||||||
import fontDigital from "../DigitalWatch/digital-font.ttf";
|
import fontDigital from "../DigitalWatch/digital-font.ttf";
|
||||||
|
|
||||||
import sndBell from "./bell.wav";
|
import sndBell from "./bell.wav";
|
||||||
import sndRunning from "./running.wav";
|
import sndRunning from "./running.wav";
|
||||||
import { Plant } from "../Plant";
|
import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types";
|
||||||
import { RaisedEvent } from "@/statecharts/runtime_types";
|
import { useEffect } from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import "./Microwave.css";
|
import "./Microwave.css";
|
||||||
import { useAudioContext } from "../../useAudioContext";
|
import { useAudioContext } from "../../useAudioContext";
|
||||||
|
import { Plant } from "../Plant";
|
||||||
export type MagnetronState = "on" | "off";
|
import { statechartExecution } from "@/statecharts/timed_reactive";
|
||||||
export type DoorState = "open" | "closed";
|
import { microwaveAbstractSyntax } from "./model";
|
||||||
|
|
||||||
export function toggleDoor(d: DoorState) {
|
|
||||||
if (d === "open") {
|
|
||||||
return "closed";
|
|
||||||
}
|
|
||||||
else return "open";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleMagnetron(m: MagnetronState) {
|
|
||||||
if (m === "on") {
|
|
||||||
return "off";
|
|
||||||
}
|
|
||||||
return "on";
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MicrowaveState = {
|
|
||||||
// Note: the door state is not part of the MicrowaveState because it is not controlled by the statechart, but by the plant.
|
|
||||||
timeDisplay: number,
|
|
||||||
bell: boolean, // whether the bell should ring
|
|
||||||
magnetron: MagnetronState,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MicrowaveProps = {
|
export type MicrowaveProps = {
|
||||||
state: MicrowaveState,
|
state: RT_Statechart,
|
||||||
speed: number,
|
speed: number,
|
||||||
callbacks: {
|
raiseInput: (event: RaisedEvent) => void;
|
||||||
startPressed: () => void;
|
raiseOutput: (event: RaisedEvent) => void;
|
||||||
stopPressed: () => void;
|
|
||||||
incTimePressed: () => void;
|
|
||||||
incTimeReleased: () => void;
|
|
||||||
doorOpened: () => void;
|
|
||||||
doorClosed: () => void;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const imgs = {
|
const imgs = {
|
||||||
closed: { off: imgSmallClosedOff, on: imgSmallClosedOn },
|
"false": { "false": imgSmallClosedOff, "true": imgSmallClosedOn },
|
||||||
open: { off: imgSmallOpenedOff, on: imgSmallOpenedOn },
|
"true": { "false": imgSmallOpenedOff, "true": imgSmallOpenedOn },
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUTTON_HEIGHT = 18;
|
const BUTTON_HEIGHT = 18;
|
||||||
|
|
@ -71,9 +43,7 @@ const DOOR_Y0 = 68;
|
||||||
const DOOR_WIDTH = 353;
|
const DOOR_WIDTH = 353;
|
||||||
const DOOR_HEIGHT = 217;
|
const DOOR_HEIGHT = 217;
|
||||||
|
|
||||||
export function Magnetron({state: {timeDisplay, bell, magnetron}, speed, callbacks}: MicrowaveProps) {
|
export function Magnetron({state, speed, raiseInput, raiseOutput}: MicrowaveProps) {
|
||||||
const [door, setDoor] = useState<DoorState>("closed");
|
|
||||||
|
|
||||||
const [playSound, preloadAudio] = useAudioContext(speed);
|
const [playSound, preloadAudio] = useAudioContext(speed);
|
||||||
|
|
||||||
// preload(imgSmallClosedOff, {as: "image"});
|
// preload(imgSmallClosedOff, {as: "image"});
|
||||||
|
|
@ -84,30 +54,25 @@ export function Magnetron({state: {timeDisplay, bell, magnetron}, speed, callbac
|
||||||
preloadAudio(sndRunning);
|
preloadAudio(sndRunning);
|
||||||
preloadAudio(sndBell);
|
preloadAudio(sndBell);
|
||||||
|
|
||||||
|
const bellRinging = state.mode.has("45");
|
||||||
|
const magnetronRunning = state.mode.has("28");
|
||||||
|
const doorOpen = state.mode.has("13");
|
||||||
|
const timeDisplay = state.environment.get("timeDisplay");
|
||||||
|
|
||||||
// a bit hacky: when the bell-state changes to true, we play the bell sound...
|
// a bit hacky: when the bell-state changes to true, we play the bell sound...
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bell) {
|
if (bellRinging) {
|
||||||
playSound(sndBell, false);
|
playSound(sndBell, false);
|
||||||
}
|
}
|
||||||
}, [bell]);
|
}, [bellRinging]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (magnetron === "on") {
|
if (magnetronRunning) {
|
||||||
const stopSoundRunning = playSound(sndRunning, true);
|
const stopSoundRunning = playSound(sndRunning, true);
|
||||||
return () => stopSoundRunning();
|
return () => stopSoundRunning();
|
||||||
}
|
}
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [magnetron])
|
}, [magnetronRunning])
|
||||||
|
|
||||||
|
|
||||||
const openDoor = () => {
|
|
||||||
setDoor("open");
|
|
||||||
callbacks.doorOpened();
|
|
||||||
}
|
|
||||||
const closeDoor = () => {
|
|
||||||
setDoor("closed");
|
|
||||||
callbacks.doorClosed();
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<style>{`
|
<style>{`
|
||||||
|
|
@ -117,52 +82,57 @@ export function Magnetron({state: {timeDisplay, bell, magnetron}, speed, callbac
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<svg width='400px' height='auto' viewBox="0 0 520 348">
|
<svg width='400px' height='auto' viewBox="0 0 520 348">
|
||||||
<image xlinkHref={imgs[door][magnetron]} width={520} height={348}/>
|
{/* @ts-ignore */}
|
||||||
|
<image xlinkHref={imgs[doorOpen][magnetronRunning]} width={520} height={348}/>
|
||||||
|
|
||||||
<rect className="microwaveButtonHelper" x={START_X0} y={START_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
|
<rect className="microwaveButtonHelper" x={START_X0} y={START_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
|
||||||
onMouseDown={() => callbacks.startPressed()}
|
onMouseDown={() => raiseInput({name: "startPressed"})}
|
||||||
|
onMouseUp={() => raiseInput({name: "startReleased"})}
|
||||||
/>
|
/>
|
||||||
<rect className="microwaveButtonHelper" x={STOP_X0} y={STOP_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
|
<rect className="microwaveButtonHelper" x={STOP_X0} y={STOP_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
|
||||||
onMouseDown={() => callbacks.stopPressed()}
|
onMouseDown={() => raiseInput({name: "stopPressed"})}
|
||||||
|
onMouseUp={() => raiseInput({name: "stopReleased"})}
|
||||||
/>
|
/>
|
||||||
<rect className="microwaveButtonHelper" x={INCTIME_X0} y={INCTIME_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
|
<rect className="microwaveButtonHelper" x={INCTIME_X0} y={INCTIME_Y0} width={BUTTON_WIDTH} height={BUTTON_HEIGHT}
|
||||||
onMouseDown={() => callbacks.incTimePressed()}
|
onMouseDown={() => raiseInput({name: "incTimePressed"})}
|
||||||
onMouseUp={() => callbacks.incTimeReleased()}
|
onMouseUp={() => raiseInput({name: "incTimeReleased"})}
|
||||||
/>
|
/>
|
||||||
<rect className="microwaveDoorHelper" x={DOOR_X0} y={DOOR_Y0} width={DOOR_WIDTH} height={DOOR_HEIGHT} onMouseDown={() => door === "open" ? closeDoor() : openDoor()}
|
<rect className="microwaveDoorHelper"
|
||||||
|
x={DOOR_X0} y={DOOR_Y0} width={DOOR_WIDTH} height={DOOR_HEIGHT}
|
||||||
|
onMouseDown={() => raiseInput({name: "doorMouseDown"})}
|
||||||
|
onMouseUp={() => raiseInput({name: "doorMouseUp"})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<text x={472} y={106} textAnchor="end" fontFamily="digital-font" fontSize={24} fill="lightgreen">{timeDisplay}</text>
|
<text x={472} y={106} textAnchor="end" fontFamily="digital-font" fontSize={24} fill="lightgreen">{timeDisplay}</text>
|
||||||
</svg>
|
</svg>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MicrowavePlant: Plant<MicrowaveState> = {
|
export const MicrowavePlant: Plant<BigStep> = {
|
||||||
inputEvents: [],
|
inputEvents: [
|
||||||
outputEvents: [],
|
// events coming from statechart
|
||||||
initial: {
|
{kind: "event", event: "setTimeDisplay", paramName: "t"},
|
||||||
timeDisplay: 0,
|
{kind: "event", event: "setMagnetron", paramName: "state"},
|
||||||
magnetron: "off",
|
{kind: "event", event: "ringBell"},
|
||||||
bell: false,
|
|
||||||
},
|
// events coming from UI:
|
||||||
reduce: (inputEvent: RaisedEvent, state: MicrowaveState) => {
|
{kind: "event", event: "doorMouseDown"},
|
||||||
if (inputEvent.name === "setMagnetron") {
|
{kind: "event", event: "doorMouseUp"},
|
||||||
return { ...state, magnetron: inputEvent.param, bell: false };
|
{kind: "event", event: "startPressed"},
|
||||||
}
|
{kind: "event", event: "stopPressed"},
|
||||||
if (inputEvent.name === "setTimeDisplay") {
|
{kind: "event", event: "incTimePressed"},
|
||||||
return { ...state, timeDisplay: inputEvent.param, bell: false };
|
{kind: "event", event: "startReleased"},
|
||||||
}
|
{kind: "event", event: "stopReleased"},
|
||||||
if (inputEvent.name === "ringBell") {
|
{kind: "event", event: "incTimeReleased"},
|
||||||
return { ...state, bell: true };
|
],
|
||||||
}
|
outputEvents: [
|
||||||
return state; // unknown event - ignore it
|
{kind: "event", event: "door", paramName: "state"},
|
||||||
},
|
{kind: "event", event: "startPressed"},
|
||||||
render: (state, raiseEvent, speed) => <Magnetron state={state} speed={speed} callbacks={{
|
{kind: "event", event: "stopPressed"},
|
||||||
startPressed: () => raiseEvent({name: "startPressed"}),
|
{kind: "event", event: "incTimePressed"},
|
||||||
stopPressed: () => raiseEvent({name: "stopPressed"}),
|
{kind: "event", event: "startReleased"},
|
||||||
incTimePressed: () => raiseEvent({name: "incTimePressed"}),
|
{kind: "event", event: "stopReleased"},
|
||||||
incTimeReleased: () => raiseEvent({name: "incTimeReleased"}),
|
{kind: "event", event: "incTimeReleased"},
|
||||||
doorOpened: () => raiseEvent({name: "door", param: "open"}),
|
],
|
||||||
doorClosed: () => raiseEvent({name: "door", param: "closed"}),
|
execution: statechartExecution(microwaveAbstractSyntax),
|
||||||
}}/>,
|
render: Magnetron,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/App/Plant/Microwave/model.ts
Normal file
15
src/App/Plant/Microwave/model.ts
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,12 +1,51 @@
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Statechart } from "@/statecharts/abstract_syntax";
|
||||||
import { EventTrigger } from "@/statecharts/label_ast";
|
import { EventTrigger } from "@/statecharts/label_ast";
|
||||||
import { RaisedEvent } from "@/statecharts/runtime_types";
|
import { RaisedEvent } from "@/statecharts/runtime_types";
|
||||||
import { ReactElement } from "react";
|
import { Conns, TimedReactive } from "@/statecharts/timed_reactive";
|
||||||
|
|
||||||
|
export type PlantRenderProps<StateType> = {
|
||||||
|
state: StateType,
|
||||||
|
speed: number,
|
||||||
|
raiseInput: (e: RaisedEvent) => void,
|
||||||
|
raiseOutput: (e: RaisedEvent) => void,
|
||||||
|
};
|
||||||
|
|
||||||
export type Plant<StateType> = {
|
export type Plant<StateType> = {
|
||||||
inputEvents: EventTrigger[];
|
inputEvents: EventTrigger[];
|
||||||
outputEvents: EventTrigger[];
|
outputEvents: EventTrigger[];
|
||||||
|
execution: TimedReactive<StateType>;
|
||||||
initial: StateType;
|
render: (props: PlantRenderProps<StateType>) => ReactElement;
|
||||||
reduce: (inputEvent: RaisedEvent, state: StateType) => StateType;
|
}
|
||||||
render: (state: StateType, raise: (event: RaisedEvent) => void, timescale: number) => ReactElement;
|
|
||||||
|
// Automatically connect Statechart and Plant inputs/outputs if their event names match.
|
||||||
|
export function autoConnect(ast: Statechart, scName: string, plant: Plant<any>, plantName: string) {
|
||||||
|
const outputs = {
|
||||||
|
[scName]: {},
|
||||||
|
[plantName]: {},
|
||||||
|
}
|
||||||
|
for (const o of ast.outputEvents) {
|
||||||
|
const plantInputEvent = plant.inputEvents.find(e => e.event === o)
|
||||||
|
if (plantInputEvent) {
|
||||||
|
// @ts-ignore
|
||||||
|
outputs[scName][o] = {kind: "model", model: plantName, eventName: plantInputEvent.event};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const o of plant.outputEvents) {
|
||||||
|
const scInputEvent = ast.inputEvents.find(e => e.event === o.event);
|
||||||
|
if (scInputEvent) {
|
||||||
|
// @ts-ignore
|
||||||
|
outputs[plantName][o.event] = {kind: "model", model: scName, eventName: scInputEvent.event};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exposePlantInputs(plant: Plant<any>, plantName: string, tfm = (s: string) => s) {
|
||||||
|
const inputs = {};
|
||||||
|
for (const i of plant.inputEvents) {
|
||||||
|
// @ts-ignore
|
||||||
|
inputs[tfm(i.event)] = {kind: "model", model: plantName, eventName: i.event};
|
||||||
|
}
|
||||||
|
return inputs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,77 @@
|
||||||
import { Dispatch, memo, SetStateAction, useCallback } from "react";
|
import { Dispatch, memo, SetStateAction, useCallback } from "react";
|
||||||
import { Statechart, stateDescription } from "../statecharts/abstract_syntax";
|
import { Statechart, stateDescription } from "../statecharts/abstract_syntax";
|
||||||
import { Mode, RaisedEvent } from "../statecharts/runtime_types";
|
import { Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types";
|
||||||
import { formatTime } from "../util/util";
|
import { formatTime } from "../util/util";
|
||||||
import { TimeMode } from "../statecharts/time";
|
import { TimeMode, timeTravel } from "../statecharts/time";
|
||||||
import { TraceItem, TraceState } from "./App";
|
import { TraceItem, TraceState } from "./App";
|
||||||
import { Environment } from "@/statecharts/environment";
|
import { Environment } from "@/statecharts/environment";
|
||||||
|
import { Conns } from "@/statecharts/timed_reactive";
|
||||||
|
|
||||||
type RTHistoryProps = {
|
type RTHistoryProps = {
|
||||||
trace: TraceState|null,
|
trace: TraceState|null,
|
||||||
setTrace: Dispatch<SetStateAction<TraceState|null>>;
|
setTrace: Dispatch<SetStateAction<TraceState|null>>;
|
||||||
ast: Statechart,
|
ast: Statechart,
|
||||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||||
|
showPlantTrace: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RTHistory({trace, setTrace, ast, setTime}: RTHistoryProps) {
|
export function RTHistory({trace, setTrace, ast, setTime, showPlantTrace}: RTHistoryProps) {
|
||||||
const onMouseDown = useCallback((idx: number, timestamp: number) => {
|
const onMouseDown = useCallback((idx: number, timestamp: number) => {
|
||||||
setTrace(trace => trace && {
|
setTrace(trace => trace && {
|
||||||
...trace,
|
...trace,
|
||||||
idx,
|
idx,
|
||||||
});
|
});
|
||||||
setTime({kind: "paused", simtime: timestamp});
|
setTime(time => timeTravel(time, timestamp, performance.now()));
|
||||||
}, [setTrace, setTime]);
|
}, [setTrace, setTime]);
|
||||||
|
|
||||||
if (trace === null) {
|
if (trace === null) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
return <div>
|
return trace.trace.map((item, i) => {
|
||||||
{trace.trace.map((item, i) => <RTHistoryItem ast={ast} idx={i} item={item} prevItem={trace.trace[i-1]} active={i === trace.idx} onMouseDown={onMouseDown}/>)}
|
const prevItem = trace.trace[i-1];
|
||||||
</div>;
|
// @ts-ignore
|
||||||
|
const isPlantStep = item.state?.sc === prevItem?.state?.sc;
|
||||||
|
if (!showPlantTrace && isPlantStep) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
return <RTHistoryItem ast={ast} idx={i} item={item} prevItem={prevItem} isPlantStep={isPlantStep} active={i === trace.idx} onMouseDown={onMouseDown}/>;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevItem, active, onMouseDown}: {idx: number, ast: Statechart, item: TraceItem, prevItem?: TraceItem, active: boolean, onMouseDown: (idx: number, timestamp: number) => void}) {
|
function RTCause(props: {cause?: RT_Event}) {
|
||||||
|
if (props.cause === undefined) {
|
||||||
|
return <>{"<init>"}</>;
|
||||||
|
}
|
||||||
|
if (props.cause.kind === "timer") {
|
||||||
|
return <>{"<timer>"}</>;
|
||||||
|
}
|
||||||
|
else if (props.cause.kind === "input") {
|
||||||
|
return <>{props.cause.name}<RTEventParam param={props.cause.param}/></>
|
||||||
|
}
|
||||||
|
console.log(props.cause);
|
||||||
|
throw new Error("unreachable");
|
||||||
|
}
|
||||||
|
|
||||||
|
function RTEventParam(props: {param?: any}) {
|
||||||
|
return <>{props.param !== undefined && <>({JSON.stringify(props.param)})</>}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevItem, isPlantStep, active, onMouseDown}: {idx: number, ast: Statechart, item: TraceItem, prevItem?: TraceItem, isPlantStep: boolean, active: boolean, onMouseDown: (idx: number, timestamp: number) => void}) {
|
||||||
if (item.kind === "bigstep") {
|
if (item.kind === "bigstep") {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const newStates = item.state.sc.mode.difference(prevItem?.state.sc.mode || new Set());
|
const newStates = item.state.sc.mode.difference(prevItem?.state.sc.mode || new Set());
|
||||||
return <div
|
return <div
|
||||||
className={"runtimeState" + (active ? " active" : "")}
|
className={"runtimeState" + (active ? " active" : "") + (isPlantStep ? " plantStep" : "")}
|
||||||
onMouseDown={useCallback(() => onMouseDown(idx, item.simtime), [idx, item.simtime])}>
|
onMouseDown={useCallback(() => onMouseDown(idx, item.simtime), [idx, item.simtime])}>
|
||||||
<div>
|
<div>
|
||||||
{formatTime(item.simtime)}
|
{formatTime(item.simtime)}
|
||||||
 
|
 
|
||||||
<div className="inputEvent">{item.cause}</div>
|
<div className="inputEvent"><RTCause cause={item.state.sc.inputEvent}/></div>
|
||||||
</div>
|
</div>
|
||||||
<ShowMode mode={newStates} statechart={ast}/>
|
<ShowMode mode={newStates} statechart={ast}/>
|
||||||
<ShowEnvironment environment={item.state.sc.environment}/>
|
<ShowEnvironment environment={item.state.sc.environment}/>
|
||||||
{item.state.sc.outputEvents.length>0 && <>^
|
{item.state.sc.outputEvents.length>0 && <>^
|
||||||
{item.state.sc.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
|
{item.state.sc.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}<RTEventParam param={e.param}/></span>)}
|
||||||
</>}
|
</>}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | Unst
|
||||||
import BoltIcon from '@mui/icons-material/Bolt';
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
import { KeyInfoHidden, KeyInfoVisible } from "./TopPanel/KeyInfo";
|
import { KeyInfoHidden, KeyInfoVisible } from "./TopPanel/KeyInfo";
|
||||||
import { memo, useEffect } from "react";
|
import { memo, useEffect } from "react";
|
||||||
|
import { usePersistentState } from "./persistent_state";
|
||||||
|
|
||||||
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}) => {
|
||||||
|
|
@ -93,6 +94,9 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// don't capture keyboard events when focused on an input element:
|
||||||
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
||||||
|
|
||||||
const n = (parseInt(e.key)+9) % 10;
|
const n = (parseInt(e.key)+9) % 10;
|
||||||
if (raiseHandlers[n] !== undefined) {
|
if (raiseHandlers[n] !== undefined) {
|
||||||
raiseHandlers[n]();
|
raiseHandlers[n]();
|
||||||
|
|
@ -106,10 +110,16 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp
|
||||||
}, [raiseHandlers]);
|
}, [raiseHandlers]);
|
||||||
// const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
// const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
const KeyInfo = KeyInfoVisible; // always show keyboard shortcuts on input events, we can't expect the user to remember them
|
const KeyInfo = KeyInfoVisible; // always show keyboard shortcuts on input events, we can't expect the user to remember them
|
||||||
|
|
||||||
|
const [inputParams, setInputParams] = usePersistentState<{[eventName:string]: string}>("inputParams", {});
|
||||||
|
|
||||||
return inputEvents.map(({event, paramName}, i) => {
|
return inputEvents.map(({event, paramName}, i) => {
|
||||||
|
const key = event+'/'+paramName;
|
||||||
|
const value = inputParams[key] || "";
|
||||||
|
const width = Math.max(value.length, (paramName||"").length)*6;
|
||||||
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={key} className="toolbarGroup">
|
||||||
<KI keyInfo={<kbd>{shortcut}</kbd>} horizontal={true}>
|
<KI keyInfo={<kbd>{shortcut}</kbd>} horizontal={true}>
|
||||||
<button
|
<button
|
||||||
className="inputEvent"
|
className="inputEvent"
|
||||||
|
|
@ -121,7 +131,7 @@ export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inp
|
||||||
</button>
|
</button>
|
||||||
</KI>
|
</KI>
|
||||||
{paramName &&
|
{paramName &&
|
||||||
<><input id={`input-${event}-param`} style={{width: 20}} placeholder={paramName}/></>
|
<><input id={`input-${event}-param`} style={{width, overflow: 'visible'}} placeholder={paramName} value={value} onChange={e => setInputParams(params => ({...params, [key]: e.target.value, }))}/></>
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// don't capture keyboard events when focused on an input element:
|
||||||
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
||||||
|
|
||||||
if (!e.ctrlKey) {
|
if (!e.ctrlKey) {
|
||||||
if (e.key === " ") {
|
if (e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,15 @@ import { TraceState } from "@/App/App";
|
||||||
import { Mode } from "@/statecharts/runtime_types";
|
import { Mode } from "@/statecharts/runtime_types";
|
||||||
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
|
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
|
||||||
|
|
||||||
export type VisualEditorState = {
|
export type ConcreteSyntax = {
|
||||||
rountangles: Rountangle[];
|
rountangles: Rountangle[];
|
||||||
texts: Text[];
|
texts: Text[];
|
||||||
arrows: Arrow[];
|
arrows: Arrow[];
|
||||||
diamonds: Diamond[];
|
diamonds: Diamond[];
|
||||||
history: History[];
|
history: History[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type VisualEditorState = ConcreteSyntax & {
|
||||||
nextID: number;
|
nextID: number;
|
||||||
selection: Selection;
|
selection: Selection;
|
||||||
};
|
};
|
||||||
|
|
@ -373,6 +376,9 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace,
|
||||||
}, [setState]);
|
}, [setState]);
|
||||||
|
|
||||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
|
// don't capture keyboard events when focused on an input element:
|
||||||
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
||||||
|
|
||||||
if (e.key === "Delete") {
|
if (e.key === "Delete") {
|
||||||
// delete selection
|
// delete selection
|
||||||
makeCheckPoint();
|
makeCheckPoint();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import { InsertMode } from "./VisualEditor";
|
||||||
|
|
||||||
export function getKeyHandler(setMode: Dispatch<SetStateAction<InsertMode>>) {
|
export function getKeyHandler(setMode: Dispatch<SetStateAction<InsertMode>>) {
|
||||||
return function onKeyDown(e: KeyboardEvent) {
|
return function onKeyDown(e: KeyboardEvent) {
|
||||||
|
// don't capture keyboard events when focused on an input element:
|
||||||
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
||||||
|
|
||||||
if (!e.ctrlKey) {
|
if (!e.ctrlKey) {
|
||||||
if (e.key === "a") {
|
if (e.key === "a") {
|
||||||
setMode("and");
|
setMode("and");
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { VisualEditorState } from "@/App/VisualEditor/VisualEditor";
|
import { ConcreteSyntax, VisualEditorState } from "@/App/VisualEditor/VisualEditor";
|
||||||
import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RectSide } from "./concrete_syntax";
|
import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RectSide } from "./concrete_syntax";
|
||||||
|
|
||||||
export type Connections = {
|
export type Connections = {
|
||||||
|
|
@ -12,7 +12,7 @@ export type Connections = {
|
||||||
history2ArrowMap: Map<string, string[]>,
|
history2ArrowMap: Map<string, string[]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function detectConnections(state: VisualEditorState): Connections {
|
export function detectConnections(state: ConcreteSyntax): Connections {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
// detect what is 'connected'
|
// detect what is 'connected'
|
||||||
const arrow2SideMap = new Map<string,[{ uid: string; part: RectSide; } | undefined, { uid: string; part: RectSide; } | undefined]>();
|
const arrow2SideMap = new Map<string,[{ uid: string; part: RectSide; } | undefined, { uid: string; part: RectSide; } | undefined]>();
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,20 @@ import { AbstractState, computeArena, computePath, ConcreteState, getDescendants
|
||||||
import { evalExpr } from "./actionlang_interpreter";
|
import { evalExpr } from "./actionlang_interpreter";
|
||||||
import { Environment, FlatEnvironment, ScopedEnvironment } from "./environment";
|
import { Environment, FlatEnvironment, ScopedEnvironment } from "./environment";
|
||||||
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
|
import { Action, EventTrigger, TransitionLabel } from "./label_ast";
|
||||||
import { BigStepOutput, initialRaised, Mode, RaisedEvents, RT_Event, RT_History, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types";
|
import { BigStep, initialRaised, Mode, RaisedEvents, RT_Event, RT_History, RT_Statechart, TimerElapseEvent, Timers } from "./runtime_types";
|
||||||
|
|
||||||
const initialEnv = new Map<string, any>([
|
const initialEnv = new Map<string, any>([
|
||||||
["_timers", []],
|
["_timers", []],
|
||||||
["_log", (str: string) => console.log(str)],
|
["_log", (str: string) => console.log(str)],
|
||||||
]);
|
]);
|
||||||
const initialScopedEnvironment = new ScopedEnvironment({env: initialEnv, children: {}});
|
// const initialScopedEnvironment = new ScopedEnvironment({env: initialEnv, children: {}});
|
||||||
// const intiialFlatEnvironment = new FlatEnvironment(initialEnv);
|
const intiialFlatEnvironment = new FlatEnvironment(initialEnv);
|
||||||
|
|
||||||
export function initialize(ast: Statechart): BigStepOutput {
|
export function initialize(ast: Statechart): BigStep {
|
||||||
let history = new Map();
|
let history = new Map();
|
||||||
let enteredStates, environment, rest;
|
let enteredStates, environment, rest;
|
||||||
({enteredStates, environment, history, ...rest} = enterDefault(0, ast.root, {
|
({enteredStates, environment, history, ...rest} = enterDefault(0, ast.root, {
|
||||||
environment: initialScopedEnvironment,
|
environment: intiialFlatEnvironment,
|
||||||
history,
|
history,
|
||||||
...initialRaised,
|
...initialRaised,
|
||||||
}));
|
}));
|
||||||
|
|
@ -256,9 +256,12 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_
|
||||||
const addEventParam = (event && event.kind === "input" && event.param !== undefined) ?
|
const addEventParam = (event && event.kind === "input" && event.param !== undefined) ?
|
||||||
(environment: Environment, label: TransitionLabel) => {
|
(environment: Environment, label: TransitionLabel) => {
|
||||||
const varName = (label.trigger as EventTrigger).paramName as string;
|
const varName = (label.trigger as EventTrigger).paramName as string;
|
||||||
|
if (varName) {
|
||||||
const result = environment.newVar(varName, event.param);
|
const result = environment.newVar(varName, event.param);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
: (environment: Environment) => environment;
|
: (environment: Environment) => environment;
|
||||||
// console.log('attemptSrcState', stateDescription(sourceState), arenasFired);
|
// console.log('attemptSrcState', stateDescription(sourceState), arenasFired);
|
||||||
const outgoing = statechart.transitions.get(sourceState.uid) || [];
|
const outgoing = statechart.transitions.get(sourceState.uid) || [];
|
||||||
|
|
@ -342,15 +345,15 @@ export function fairStep(simtime: number, event: RT_Event, statechart: Statechar
|
||||||
return {arenasFired, environment, ...config};
|
return {arenasFired, environment, ...config};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStepOutput {
|
export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStep {
|
||||||
let raised = initialRaised;
|
let raised = initialRaised;
|
||||||
|
|
||||||
({mode, environment, ...raised} = fairStep(simtime, event, statechart, statechart.root, {mode, environment, history, arenasFired: [], ...raised}));
|
({mode, environment, ...raised} = fairStep(simtime, event, statechart, statechart.root, {mode, environment, history, arenasFired: [], ...raised}));
|
||||||
|
|
||||||
return handleInternalEvents(simtime, statechart, {mode, environment, history, ...raised});
|
return {inputEvent: event, ...handleInternalEvents(simtime, statechart, {mode, environment, history, ...raised})};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents): BigStepOutput {
|
export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents) {
|
||||||
while (internalEvents.length > 0) {
|
while (internalEvents.length > 0) {
|
||||||
const [nextEvent, ...remainingEvents] = internalEvents;
|
const [nextEvent, ...remainingEvents] = internalEvents;
|
||||||
({internalEvents, ...rest} = fairStep(simtime,
|
({internalEvents, ...rest} = fairStep(simtime,
|
||||||
|
|
@ -389,11 +392,12 @@ export function fire(simtime: number, t: Transition, ts: Map<string, Transition[
|
||||||
// console.log('exitedMode', exitedMode);
|
// console.log('exitedMode', exitedMode);
|
||||||
|
|
||||||
// transition actions
|
// transition actions
|
||||||
for (const action of label.actions) {
|
|
||||||
environment = addEventParam(environment.enterScope("<transition>"), label);
|
environment = addEventParam(environment.enterScope("<transition>"), label);
|
||||||
|
for (const action of label.actions) {
|
||||||
|
console.log('environment after adding event param:', environment);
|
||||||
({environment, history, ...rest} = execAction(action, {environment, history, ...rest}, [t.uid]));
|
({environment, history, ...rest} = execAction(action, {environment, history, ...rest}, [t.uid]));
|
||||||
environment = environment.dropScope();
|
|
||||||
}
|
}
|
||||||
|
environment = environment.dropScope();
|
||||||
|
|
||||||
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
const tgtPath = computePath({ancestor: arena, descendant: t.tgt});
|
||||||
const state = tgtPath[0] as ConcreteState; // first state to enter
|
const state = tgtPath[0] as ConcreteState; // first state to enter
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
|
||||||
import { parse as parseLabel, SyntaxError } from "./label_parser";
|
import { parse as parseLabel, SyntaxError } from "./label_parser";
|
||||||
import { Connections } from "./detect_connections";
|
import { Connections } from "./detect_connections";
|
||||||
import { HISTORY_RADIUS } from "../App/parameters";
|
import { HISTORY_RADIUS } from "../App/parameters";
|
||||||
import { VisualEditorState } from "@/App/VisualEditor/VisualEditor";
|
import { ConcreteSyntax, VisualEditorState } from "@/App/VisualEditor/VisualEditor";
|
||||||
import { memoize } from "@/util/util";
|
import { memoize } from "@/util/util";
|
||||||
|
|
||||||
export type TraceableError = {
|
export type TraceableError = {
|
||||||
|
|
@ -34,7 +34,7 @@ function addEvent(events: EventTrigger[], e: EventTrigger, textUid: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseStatechart(state: VisualEditorState, conns: Connections): [Statechart, TraceableError[]] {
|
export function parseStatechart(state: ConcreteSyntax, conns: Connections): [Statechart, TraceableError[]] {
|
||||||
const errors: TraceableError[] = [];
|
const errors: TraceableError[] = [];
|
||||||
|
|
||||||
// implicitly, the root is always an Or-state
|
// implicitly, the root is always an Or-state
|
||||||
|
|
|
||||||
|
|
@ -27,16 +27,14 @@ export type RT_Statechart = {
|
||||||
history: RT_History; // history-uid -> set of states
|
history: RT_History; // history-uid -> set of states
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BigStepOutput = RT_Statechart & {
|
export type BigStep = RT_Statechart & {
|
||||||
|
inputEvent?: RT_Event,
|
||||||
outputEvents: RaisedEvent[],
|
outputEvents: RaisedEvent[],
|
||||||
|
|
||||||
|
// we also record the transitions that fired, to highlight them in the UI:
|
||||||
firedTransitions: string[],
|
firedTransitions: string[],
|
||||||
};
|
};
|
||||||
|
|
||||||
// export type BigStep = {
|
|
||||||
// inputEvent: string | null, // null if initialization
|
|
||||||
// simtime: number,
|
|
||||||
// } & BigStepOutput;
|
|
||||||
|
|
||||||
// internal or output event
|
// internal or output event
|
||||||
export type RaisedEvent = {
|
export type RaisedEvent = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,12 @@ export function setPaused(currentMode: TimeMode, wallclktime: number): TimePause
|
||||||
simtime: getSimTime(currentMode, wallclktime),
|
simtime: getSimTime(currentMode, wallclktime),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function timeTravel(currentMode: TimeMode, simtime: number, wallclktime: number): TimeMode {
|
||||||
|
if (currentMode.kind === "paused") {
|
||||||
|
return {kind: "paused", simtime};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {kind: "realtime", scale: currentMode.scale, since: {simtime, wallclktime}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,24 @@
|
||||||
import { Statechart } from "./abstract_syntax";
|
import { Statechart } from "./abstract_syntax";
|
||||||
import { handleInputEvent, initialize } from "./interpreter";
|
import { handleInputEvent, initialize, RuntimeError } from "./interpreter";
|
||||||
import { BigStepOutput, InputEvent, RaisedEvent, RT_Statechart, Timers } from "./runtime_types";
|
import { BigStep, InputEvent, RaisedEvent, RT_Statechart, Timers } from "./runtime_types";
|
||||||
|
|
||||||
// an abstract interface for timed reactive discrete event systems somewhat similar but not equal to DEVS
|
// an abstract interface for timed reactive discrete event systems somewhat similar but not equal to DEVS
|
||||||
// differences from DEVS:
|
// differences from DEVS:
|
||||||
// - extTransition can have output events
|
// - extTransition can have output events
|
||||||
// - time is kept as absolute simulated time (since beginning of simulation), not relative to the last transition
|
// - time is kept as absolute simulated time (since beginning of simulation), not relative to the last transition
|
||||||
export type TimedReactive<RT_Config> = {
|
export type TimedReactive<RT_Config> = {
|
||||||
initial: () => RT_Config,
|
initial: () => [RaisedEvent[], RT_Config],
|
||||||
timeAdvance: (c: RT_Config) => number,
|
timeAdvance: (c: RT_Config) => number,
|
||||||
intTransition: (c: RT_Config) => [RaisedEvent[], RT_Config],
|
intTransition: (c: RT_Config) => [RaisedEvent[], RT_Config],
|
||||||
extTransition: (simtime: number, c: RT_Config, e: InputEvent) => [RaisedEvent[], RT_Config],
|
extTransition: (simtime: number, c: RT_Config, e: InputEvent) => [RaisedEvent[], RT_Config],
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statechartExecution(ast: Statechart): TimedReactive<BigStepOutput> {
|
export function statechartExecution(ast: Statechart): TimedReactive<BigStep> {
|
||||||
return {
|
return {
|
||||||
initial: () => initialize(ast),
|
initial: () => {
|
||||||
|
const bigstep = initialize(ast);
|
||||||
|
return [bigstep.outputEvents, bigstep];
|
||||||
|
},
|
||||||
timeAdvance: (c: RT_Statechart) => (c.environment.get("_timers") as Timers)[0]?.[0] || Infinity,
|
timeAdvance: (c: RT_Statechart) => (c.environment.get("_timers") as Timers)[0]?.[0] || Infinity,
|
||||||
intTransition: (c: RT_Statechart) => {
|
intTransition: (c: RT_Statechart) => {
|
||||||
const timers = c.environment.get("_timers") as Timers;
|
const timers = c.environment.get("_timers") as Timers;
|
||||||
|
|
@ -33,13 +36,6 @@ export function statechartExecution(ast: Statechart): TimedReactive<BigStepOutpu
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dummyExecution: TimedReactive<null> = {
|
|
||||||
initial: () => null,
|
|
||||||
timeAdvance: () => Infinity,
|
|
||||||
intTransition: () => { throw new Error("dummy never makes intTransition"); },
|
|
||||||
extTransition: () => [[], null],
|
|
||||||
};
|
|
||||||
|
|
||||||
export type EventDestination = ModelDestination | OutputDestination;
|
export type EventDestination = ModelDestination | OutputDestination;
|
||||||
|
|
||||||
export type ModelDestination = {
|
export type ModelDestination = {
|
||||||
|
|
@ -53,13 +49,35 @@ export type OutputDestination = {
|
||||||
eventName: string,
|
eventName: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function exposeStatechartInputs(ast: Statechart, model: string): Conns {
|
// export type NowhereDestination = {
|
||||||
|
// kind: "nowhere",
|
||||||
|
// };
|
||||||
|
|
||||||
|
export function exposeStatechartInputsOutputs(ast: Statechart, model: string): Conns {
|
||||||
return {
|
return {
|
||||||
inputEvents: Object.fromEntries(ast.inputEvents.map(e => [e.event, {kind: "model", model, eventName: e.event}])),
|
// all the coupled execution's input events become input events for the statechart
|
||||||
outputEvents: {},
|
inputEvents: exposeStatechartInputs(ast, model),
|
||||||
|
outputEvents: exposeStatechartOutputs(ast, model),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exposeStatechartInputs(ast: Statechart, model: string, tfm = (s: string) => s): {[eventName: string]: ModelDestination} {
|
||||||
|
return Object.fromEntries(ast.inputEvents.map(e => [tfm(e.event), {kind: "model", model, eventName: e.event}]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exposeStatechartOutputs(ast: Statechart, model: string): {[modelName: string]: {[eventName: string]: EventDestination}} {
|
||||||
|
return {
|
||||||
|
// all the statechart's output events become output events of our coupled execution
|
||||||
|
[model]: Object.fromEntries([...ast.outputEvents].map(e => [e, {kind: "output", model, eventName: e}])),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function hideStatechartOutputs(ast: Statechart, model: string) {
|
||||||
|
// return {
|
||||||
|
// [model]: Object.fromEntries([...ast.outputEvents].map(e => [e, {kind: "nowhere" as const}])),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
export type Conns = {
|
export type Conns = {
|
||||||
// inputs coming from outside are routed to the right models
|
// inputs coming from outside are routed to the right models
|
||||||
inputEvents: {[eventName: string]: ModelDestination},
|
inputEvents: {[eventName: string]: ModelDestination},
|
||||||
|
|
@ -85,10 +103,12 @@ export function coupledExecution<T extends {[name: string]: any}>(models: {[name
|
||||||
const destination = conns.outputEvents[model]?.[event.name];
|
const destination = conns.outputEvents[model]?.[event.name];
|
||||||
if (destination === undefined) {
|
if (destination === undefined) {
|
||||||
// ignore
|
// ignore
|
||||||
|
console.log(`${model}.${event.name} goes nowhere`);
|
||||||
return processOutputs(simtime, rest, model, c);
|
return processOutputs(simtime, rest, model, c);
|
||||||
}
|
}
|
||||||
if (destination.kind === "model") {
|
if (destination.kind === "model") {
|
||||||
// output event is input for another model
|
// output event is input for another model
|
||||||
|
console.log(`${model}.${event.name} goes to ${destination.model}.${destination.eventName}`);
|
||||||
const inputEvent = {
|
const inputEvent = {
|
||||||
kind: "input" as const,
|
kind: "input" as const,
|
||||||
name: destination.eventName,
|
name: destination.eventName,
|
||||||
|
|
@ -100,11 +120,13 @@ export function coupledExecution<T extends {[name: string]: any}>(models: {[name
|
||||||
const [restOutputEvents, newConfig2] = processOutputs(simtime, rest, model, newConfig);
|
const [restOutputEvents, newConfig2] = processOutputs(simtime, rest, model, newConfig);
|
||||||
return [[...outputEvents, ...restOutputEvents], newConfig2];
|
return [[...outputEvents, ...restOutputEvents], newConfig2];
|
||||||
}
|
}
|
||||||
else {
|
else if (destination.kind === "output") {
|
||||||
// kind === "output"
|
// kind === "output"
|
||||||
|
console.log(`${model}.${event.name} becomes ^${destination.eventName}`);
|
||||||
const [outputEvents, newConfig] = processOutputs(simtime, rest, model, c);
|
const [outputEvents, newConfig] = processOutputs(simtime, rest, model, c);
|
||||||
return [[event, ...outputEvents], newConfig];
|
return [[event, ...outputEvents], newConfig];
|
||||||
}
|
}
|
||||||
|
throw new Error("unreachable");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return [[], c];
|
return [[], c];
|
||||||
|
|
@ -112,13 +134,33 @@ export function coupledExecution<T extends {[name: string]: any}>(models: {[name
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initial: () => Object.fromEntries(Object.entries(models).map(([name, model]) => {
|
initial: () => {
|
||||||
return [name, model.initial()];
|
// 1. initialize every model
|
||||||
})) as T,
|
const allOutputs = [];
|
||||||
|
let state = {} as T;
|
||||||
|
for (const [modelName, model] of Object.entries(models)) {
|
||||||
|
const [outputEvents, modelState] = model.initial();
|
||||||
|
for (const o of outputEvents) {
|
||||||
|
allOutputs.push([modelName, o]);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
state[modelName] = modelState;
|
||||||
|
}
|
||||||
|
console.log({state});
|
||||||
|
// 2. handle all output events (models' outputs may be inputs for each other)
|
||||||
|
let finalOutputs = [];
|
||||||
|
for (const [modelName, outputEvents] of allOutputs) {
|
||||||
|
let newOutputs;
|
||||||
|
[newOutputs, state] = processOutputs(0, outputEvents, modelName, state);
|
||||||
|
finalOutputs.push(...newOutputs);
|
||||||
|
}
|
||||||
|
return [finalOutputs, state];
|
||||||
|
},
|
||||||
timeAdvance: (c) => {
|
timeAdvance: (c) => {
|
||||||
return Object.entries(models).reduce((acc, [name, {timeAdvance}]) => Math.min(timeAdvance(c[name]), acc), Infinity);
|
return Object.entries(models).reduce((acc, [name, {timeAdvance}]) => Math.min(timeAdvance(c[name]), acc), Infinity);
|
||||||
},
|
},
|
||||||
intTransition: (c) => {
|
intTransition: (c) => {
|
||||||
|
// find earliest internal transition among all models:
|
||||||
const [when, name] = Object.entries(models).reduce(([earliestSoFar, earliestModel], [name, {timeAdvance}]) => {
|
const [when, name] = Object.entries(models).reduce(([earliestSoFar, earliestModel], [name, {timeAdvance}]) => {
|
||||||
const when = timeAdvance(c[name]);
|
const when = timeAdvance(c[name]);
|
||||||
if (when < earliestSoFar) {
|
if (when < earliestSoFar) {
|
||||||
|
|
@ -133,8 +175,9 @@ export function coupledExecution<T extends {[name: string]: any}>(models: {[name
|
||||||
throw new Error("cannot make intTransition - timeAdvance is infinity");
|
throw new Error("cannot make intTransition - timeAdvance is infinity");
|
||||||
},
|
},
|
||||||
extTransition: (simtime, c, e) => {
|
extTransition: (simtime, c, e) => {
|
||||||
|
console.log(e);
|
||||||
const {model, eventName} = conns.inputEvents[e.name];
|
const {model, eventName} = conns.inputEvents[e.name];
|
||||||
// console.log('input event', e, 'goes to', model);
|
console.log('input event', e.name, 'goes to', `${model}.${eventName}`);
|
||||||
const inputEvent: InputEvent = {
|
const inputEvent: InputEvent = {
|
||||||
kind: "input",
|
kind: "input",
|
||||||
name: eventName,
|
name: eventName,
|
||||||
|
|
@ -144,3 +187,32 @@ export function coupledExecution<T extends {[name: string]: any}>(models: {[name
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Example of a coupled execution:
|
||||||
|
|
||||||
|
// const clock1: TimedReactive<{nextTick: number}> = {
|
||||||
|
// initial: () => ({nextTick: 1}),
|
||||||
|
// timeAdvance: (c) => c.nextTick,
|
||||||
|
// intTransition: (c) => [[{name: "tick"}], {nextTick: c.nextTick+1}],
|
||||||
|
// extTransition: (simtime, c, e) => [[], (c)],
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const clock2: TimedReactive<{nextTick: number}> = {
|
||||||
|
// initial: () => ({nextTick: 0.5}),
|
||||||
|
// timeAdvance: (c) => c.nextTick,
|
||||||
|
// intTransition: (c) => [[{name: "tick"}], {nextTick: c.nextTick+1}],
|
||||||
|
// extTransition: (simtime, c, e) => [[], (c)],
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const coupled = coupledExecution({clock1, clock2}, {inputEvents: {}, outputEvents: {
|
||||||
|
// clock1: {tick: {kind:"output", eventName: 'tick'}},
|
||||||
|
// clock2: {tick: {kind:"output", eventName: 'tick'}},
|
||||||
|
// }})
|
||||||
|
|
||||||
|
// let state = coupled.initial();
|
||||||
|
// for (let i=0; i<10; i++) {
|
||||||
|
// const nextWakeup = coupled.timeAdvance(state);
|
||||||
|
// console.log({state, nextWakeup});
|
||||||
|
// [[], state] = coupled.intTransition(state);
|
||||||
|
// }
|
||||||
|
|
|
||||||
1
todo.txt
1
todo.txt
|
|
@ -52,7 +52,6 @@ TODO
|
||||||
- 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
|
||||||
|
|
||||||
- highlight fired transitions
|
|
||||||
- highlight about-to-fire transitions
|
- highlight about-to-fire transitions
|
||||||
|
|
||||||
- when there is a runtime error, e.g.,
|
- when there is a runtime error, e.g.,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue