nice feature: the microwave's sounds playback speed corresponds to the simulation speed, hahahaha
This commit is contained in:
parent
dd82b0433c
commit
710f7be68c
5 changed files with 82 additions and 35 deletions
|
|
@ -215,7 +215,14 @@ export function App() {
|
||||||
throw error; // probably a bug in the interpreter
|
throw error; // probably a bug in the interpreter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTime({kind: "paused", simtime: 0});
|
setTime(time => {
|
||||||
|
if (time.kind === "paused") {
|
||||||
|
return {...time, simtime: 0};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {...time, since: {simtime: 0, wallclktime: performance.now()}};
|
||||||
|
}
|
||||||
|
});
|
||||||
scrollDownSidebar();
|
scrollDownSidebar();
|
||||||
}, [ast, scrollDownSidebar, setTime, setTrace]);
|
}, [ast, scrollDownSidebar, setTime, setTrace]);
|
||||||
|
|
||||||
|
|
@ -399,7 +406,7 @@ export function App() {
|
||||||
flex: '0 0 content',
|
flex: '0 0 content',
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
overflowX: "visible",
|
overflowX: "visible",
|
||||||
maxWidth: 'min(400px,50vw)',
|
maxWidth: '50vw',
|
||||||
}}>
|
}}>
|
||||||
<div className="stackVertical" style={{height:'100%'}}>
|
<div className="stackVertical" style={{height:'100%'}}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -439,7 +446,11 @@ export function App() {
|
||||||
</select>
|
</select>
|
||||||
{trace !== null &&
|
{trace !== null &&
|
||||||
<div>{
|
<div>{
|
||||||
plant.render(trace.trace[trace.idx].plantState, event => onRaise(event.name, event.param))
|
plant.render(
|
||||||
|
trace.trace[trace.idx].plantState,
|
||||||
|
event => onRaise(event.name, event.param),
|
||||||
|
time.kind === "paused" ? 0 : time.scale,
|
||||||
|
)
|
||||||
}</div>}
|
}</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></details>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
import SkipNextIcon from '@mui/icons-material/SkipNext';
|
import SkipNextIcon from '@mui/icons-material/SkipNext';
|
||||||
import StopIcon from '@mui/icons-material/Stop';
|
import StopIcon from '@mui/icons-material/Stop';
|
||||||
import { InsertModes } from "./TopPanel/InsertModes";
|
import { InsertModes } from "./TopPanel/InsertModes";
|
||||||
|
import { usePersistentState } from "@/util/persistent_state";
|
||||||
|
|
||||||
export type TopPanelProps = {
|
export type TopPanelProps = {
|
||||||
trace: TraceState | null,
|
trace: TraceState | null,
|
||||||
|
|
@ -42,7 +43,7 @@ const ShortCutShowKeys = <kbd>~</kbd>;
|
||||||
|
|
||||||
export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) {
|
export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) {
|
||||||
const [displayTime, setDisplayTime] = useState("0.000");
|
const [displayTime, setDisplayTime] = useState("0.000");
|
||||||
const [timescale, setTimescale] = useState(1);
|
const [timescale, setTimescale] = usePersistentState("timescale", 1);
|
||||||
|
|
||||||
const config = trace && trace.trace[trace.idx];
|
const config = trace && trace.trace[trace.idx];
|
||||||
|
|
||||||
|
|
@ -74,7 +75,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
updateDisplayedTime();
|
updateDisplayedTime();
|
||||||
}, [setTime, updateDisplayedTime]);
|
}, [setTime, timescale, updateDisplayedTime]);
|
||||||
|
|
||||||
const onTimeScaleChange = useCallback((newValue: string, wallclktime: number) => {
|
const onTimeScaleChange = useCallback((newValue: string, wallclktime: number) => {
|
||||||
const asFloat = parseFloat(newValue);
|
const asFloat = parseFloat(newValue);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import sndBell from "./bell.wav";
|
||||||
import sndRunning from "./running.wav";
|
import sndRunning from "./running.wav";
|
||||||
import { Plant } from "../Plant";
|
import { Plant } from "../Plant";
|
||||||
import { RaisedEvent } from "@/statecharts/runtime_types";
|
import { RaisedEvent } from "@/statecharts/runtime_types";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import "./Microwave.css";
|
import "./Microwave.css";
|
||||||
|
|
||||||
|
|
@ -40,6 +40,7 @@ export type MicrowaveState = {
|
||||||
|
|
||||||
export type MicrowaveProps = {
|
export type MicrowaveProps = {
|
||||||
state: MicrowaveState,
|
state: MicrowaveState,
|
||||||
|
speed: number,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
startPressed: () => void;
|
startPressed: () => void;
|
||||||
stopPressed: () => void;
|
stopPressed: () => void;
|
||||||
|
|
@ -92,41 +93,75 @@ function fetchAudioBuffer(url: string): Promise<AudioBuffer> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function playAudioBufer(buf: AudioBuffer, loop: boolean) {
|
// Using the Web Audio API was the only way I could get the 'microwave running' sound to properly play gapless in Chrome.
|
||||||
|
function playAudioBufer(buf: AudioBuffer, loop: boolean, speed: number): AudioCallbacks {
|
||||||
const src = ctx.createBufferSource();
|
const src = ctx.createBufferSource();
|
||||||
src.buffer = buf;
|
src.buffer = buf;
|
||||||
src.connect(ctx.destination);
|
|
||||||
|
const lowPass = ctx.createBiquadFilter();
|
||||||
|
lowPass.type = 'highpass';
|
||||||
|
lowPass.frequency.value = 20; // let's not blow up anyone's speakers
|
||||||
|
|
||||||
|
src.connect(lowPass);
|
||||||
|
lowPass.connect(ctx.destination);
|
||||||
|
|
||||||
if (loop) src.loop = true;
|
if (loop) src.loop = true;
|
||||||
src.start();
|
src.start();
|
||||||
return () => src.stop();
|
return [
|
||||||
|
() => src.stop(),
|
||||||
|
(speed: number) => {
|
||||||
|
// instead of setting playback rate to 0 (which browsers seem to handle as if playback rate was set to 1, we just set it to a very small value, making it "almost paused")
|
||||||
|
// combined with the lowpass filter above, this should produce any audible results.
|
||||||
|
src.playbackRate.value = (speed===0) ? 0.00001 : speed;
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Magnetron({state: {timeDisplay, bell, magnetron}, callbacks}: MicrowaveProps) {
|
type AudioCallbacks = [
|
||||||
|
() => void,
|
||||||
|
(speed: number) => void,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Magnetron({state: {timeDisplay, bell, magnetron}, speed, callbacks}: MicrowaveProps) {
|
||||||
const [door, setDoor] = useState<DoorState>("closed");
|
const [door, setDoor] = useState<DoorState>("closed");
|
||||||
|
|
||||||
const bufRunningPromise = useRef(fetchAudioBuffer(sndRunning));
|
const [soundsPlaying, setSoundsPlaying] = useState<AudioCallbacks[]>([]);
|
||||||
const bufBellPromise = useRef(fetchAudioBuffer(sndBell));
|
const [bufRunningPromise] = useState(() => fetchAudioBuffer(sndRunning));
|
||||||
|
const [bufBellPromise] = useState(() => fetchAudioBuffer(sndBell));
|
||||||
|
|
||||||
// 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 (bell) {
|
||||||
bufBellPromise.current.then(buf => {
|
bufBellPromise.then(buf => {
|
||||||
playAudioBufer(buf, false);
|
const cbs = playAudioBufer(buf, false, speed);
|
||||||
})
|
setSoundsPlaying(sounds => [...sounds, cbs]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [bell]);
|
}, [bell]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (magnetron === "on") {
|
if (magnetron === "on") {
|
||||||
const stop = bufRunningPromise.current.then(buf => {
|
const stop = bufRunningPromise.then(buf => {
|
||||||
return playAudioBufer(buf, true);
|
const cbs = playAudioBufer(buf, true, speed);
|
||||||
|
setSoundsPlaying(sounds => [...sounds, cbs]);
|
||||||
|
return () => {
|
||||||
|
cbs[0]();
|
||||||
|
setSoundsPlaying(sounds => sounds.filter(cbs_ => cbs_ !== cbs));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => stop.then(stop => {
|
||||||
|
stop();
|
||||||
});
|
});
|
||||||
return () => stop.then(stop => stop());
|
|
||||||
}
|
}
|
||||||
return () => {};
|
return () => {};
|
||||||
}, [magnetron])
|
}, [magnetron])
|
||||||
|
|
||||||
preload(imgSmallClosedOff, {as: "image"});
|
useEffect(() => {
|
||||||
|
soundsPlaying.forEach(([_, setSpeed]) => setSpeed(speed));
|
||||||
|
}, [soundsPlaying, speed])
|
||||||
|
|
||||||
|
// preload(imgSmallClosedOff, {as: "image"});
|
||||||
preload(imgSmallClosedOn, {as: "image"});
|
preload(imgSmallClosedOn, {as: "image"});
|
||||||
preload(imgSmallOpenedOff, {as: "image"});
|
preload(imgSmallOpenedOff, {as: "image"});
|
||||||
preload(imgSmallOpenedOn, {as: "image"});
|
preload(imgSmallOpenedOn, {as: "image"});
|
||||||
|
|
@ -147,7 +182,7 @@ export function Magnetron({state: {timeDisplay, bell, magnetron}, callbacks}: Mi
|
||||||
src: url(${fontDigital});
|
src: url(${fontDigital});
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<svg style={{maxWidth: 520}} width='100%' 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}/>
|
<image xlinkHref={imgs[door][magnetron]} 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}
|
||||||
|
|
@ -188,7 +223,7 @@ export const MicrowavePlant: Plant<MicrowaveState> = {
|
||||||
}
|
}
|
||||||
return state; // unknown event - ignore it
|
return state; // unknown event - ignore it
|
||||||
},
|
},
|
||||||
render: (state, raiseEvent) => <Magnetron state={state} callbacks={{
|
render: (state, raiseEvent, speed) => <Magnetron state={state} speed={speed} callbacks={{
|
||||||
startPressed: () => raiseEvent({name: "startPressed"}),
|
startPressed: () => raiseEvent({name: "startPressed"}),
|
||||||
stopPressed: () => raiseEvent({name: "stopPressed"}),
|
stopPressed: () => raiseEvent({name: "stopPressed"}),
|
||||||
incTimePressed: () => raiseEvent({name: "incTimePressed"}),
|
incTimePressed: () => raiseEvent({name: "incTimePressed"}),
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,5 @@ export type Plant<StateType> = {
|
||||||
|
|
||||||
initial: StateType;
|
initial: StateType;
|
||||||
reduce: (inputEvent: RaisedEvent, state: StateType) => StateType;
|
reduce: (inputEvent: RaisedEvent, state: StateType) => StateType;
|
||||||
render: (state: StateType, raise: (event: RaisedEvent) => void) => ReactElement;
|
render: (state: StateType, raise: (event: RaisedEvent) => void, timescale: number) => ReactElement;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export function execAction(action: Action, rt: ActionScope): ActionScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function entryActions(simtime: number, state: TransitionSrcTgt, actionScope: ActionScope): ActionScope {
|
export function entryActions(simtime: number, state: TransitionSrcTgt, actionScope: ActionScope): ActionScope {
|
||||||
console.log('enter', stateDescription(state), '...');
|
// console.log('enter', stateDescription(state), '...');
|
||||||
|
|
||||||
let {environment, ...rest} = actionScope;
|
let {environment, ...rest} = actionScope;
|
||||||
|
|
||||||
|
|
@ -99,7 +99,7 @@ export function entryActions(simtime: number, state: TransitionSrcTgt, actionSco
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exitActions(simtime: number, state: TransitionSrcTgt, {enteredStates, ...actionScope}: EnteredScope): ActionScope {
|
export function exitActions(simtime: number, state: TransitionSrcTgt, {enteredStates, ...actionScope}: EnteredScope): ActionScope {
|
||||||
console.log('exit', stateDescription(state), '...');
|
// console.log('exit', stateDescription(state), '...');
|
||||||
|
|
||||||
for (const action of state.exitActions) {
|
for (const action of state.exitActions) {
|
||||||
(actionScope = execAction(action, actionScope));
|
(actionScope = execAction(action, actionScope));
|
||||||
|
|
@ -194,7 +194,7 @@ export function enterStates(simtime: number, state: ConcreteState, toEnter: Set<
|
||||||
|
|
||||||
// exit the given state and all its active descendants
|
// exit the given state and all its active descendants
|
||||||
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
|
export function exitCurrent(simtime: number, state: ConcreteState, rt: EnteredScope): ActionScope {
|
||||||
console.log('exitCurrent', stateDescription(state));
|
// console.log('exitCurrent', stateDescription(state));
|
||||||
let {enteredStates, history, ...actionScope} = rt;
|
let {enteredStates, history, ...actionScope} = rt;
|
||||||
|
|
||||||
if (enteredStates.has(state.uid)) {
|
if (enteredStates.has(state.uid)) {
|
||||||
|
|
@ -241,7 +241,7 @@ function allowedToFire(arena: OrState, alreadyFiredArenas: OrState[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_Event|undefined, statechart: Statechart, {environment, mode, arenasFired, ...rest}: RT_Statechart & RaisedEvents): (RT_Statechart & RaisedEvents) | undefined {
|
function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_Event|undefined, statechart: Statechart, {environment, mode, arenasFired, ...rest}: RT_Statechart & RaisedEvents): (RT_Statechart & RaisedEvents) | undefined {
|
||||||
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) || [];
|
||||||
const labels = outgoing.flatMap(t =>
|
const labels = outgoing.flatMap(t =>
|
||||||
t.label
|
t.label
|
||||||
|
|
@ -295,7 +295,7 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_
|
||||||
for (const activeState of mode) {
|
for (const activeState of mode) {
|
||||||
const s = statechart.uid2State.get(activeState);
|
const s = statechart.uid2State.get(activeState);
|
||||||
if (s?.kind === "pseudo") {
|
if (s?.kind === "pseudo") {
|
||||||
console.log('fire pseudo-state...');
|
// console.log('fire pseudo-state...');
|
||||||
const newConfig = attemptSrcState(simtime, s, undefined, statechart, {environment, mode, arenasFired: [], ...rest});
|
const newConfig = attemptSrcState(simtime, s, undefined, statechart, {environment, mode, arenasFired: [], ...rest});
|
||||||
if (newConfig === undefined) {
|
if (newConfig === undefined) {
|
||||||
throw new RuntimeError("Stuck in choice-state.", [activeState]);
|
throw new RuntimeError("Stuck in choice-state.", [activeState]);
|
||||||
|
|
@ -311,7 +311,7 @@ function attemptSrcState(simtime: number, sourceState: AbstractState, event: RT_
|
||||||
|
|
||||||
// A fair step is a response to one (input|internal) event, where possibly multiple transitions are made as long as their arenas do not overlap. A reasonably accurate and more intuitive explanation is that every orthogonal region is allowed to fire at most one transition.
|
// A fair step is a response to one (input|internal) event, where possibly multiple transitions are made as long as their arenas do not overlap. A reasonably accurate and more intuitive explanation is that every orthogonal region is allowed to fire at most one transition.
|
||||||
export function fairStep(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {arenasFired, ...config}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
export function fairStep(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {arenasFired, ...config}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||||
console.log('fairStep', arenasFired);
|
// console.log('fairStep', arenasFired);
|
||||||
for (const state of activeParent.children) {
|
for (const state of activeParent.children) {
|
||||||
if (config.mode.has(state.uid)) {
|
if (config.mode.has(state.uid)) {
|
||||||
const didFire = attemptSrcState(simtime, state, event, statechart, {...config, arenasFired});
|
const didFire = attemptSrcState(simtime, state, event, statechart, {...config, arenasFired});
|
||||||
|
|
@ -320,7 +320,7 @@ export function fairStep(simtime: number, event: RT_Event, statechart: Statechar
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// no enabled outgoing transitions, try the children:
|
// no enabled outgoing transitions, try the children:
|
||||||
console.log('attempt children');
|
// console.log('attempt children');
|
||||||
({arenasFired, ...config} = fairStep(simtime, event, statechart, state, {...config, arenasFired}));
|
({arenasFired, ...config} = fairStep(simtime, event, statechart, state, {...config, arenasFired}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -358,11 +358,11 @@ function resolveHistory(tgt: AbstractState, history: RT_History): Set<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fire(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, history, ...rest}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
export function fire(simtime: number, t: Transition, ts: Map<string, Transition[]>, label: TransitionLabel, arena: OrState, {mode, environment, history, ...rest}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
|
||||||
console.log('will now fire', transitionDescription(t), 'arena', arena);
|
// console.log('will now fire', transitionDescription(t), 'arena', arena);
|
||||||
|
|
||||||
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}) as ConcreteState[];
|
const srcPath = computePath({ancestor: arena, descendant: t.src as ConcreteState}) as ConcreteState[];
|
||||||
|
|
||||||
console.log(srcPath);
|
// console.log(srcPath);
|
||||||
// console.log('arena:', arena, 'srcPath:', srcPath);
|
// console.log('arena:', arena, 'srcPath:', srcPath);
|
||||||
|
|
||||||
// exit src and other states up to arena
|
// exit src and other states up to arena
|
||||||
|
|
@ -371,8 +371,8 @@ export function fire(simtime: number, t: Transition, ts: Map<string, Transition[
|
||||||
toExit.delete(arena.uid); // do not exit the arena itself
|
toExit.delete(arena.uid); // do not exit the arena itself
|
||||||
const exitedMode = mode.difference(toExit); // active states after exiting
|
const exitedMode = mode.difference(toExit); // active states after exiting
|
||||||
|
|
||||||
console.log('toExit', toExit);
|
// console.log('toExit', toExit);
|
||||||
console.log('exitedMode', exitedMode);
|
// console.log('exitedMode', exitedMode);
|
||||||
|
|
||||||
// transition actions
|
// transition actions
|
||||||
for (const action of label.actions) {
|
for (const action of label.actions) {
|
||||||
|
|
@ -388,9 +388,9 @@ export function fire(simtime: number, t: Transition, ts: Map<string, Transition[
|
||||||
({enteredStates, environment, history, ...rest} = enterStates(simtime, state, toEnter, {environment, history, ...rest}));
|
({enteredStates, environment, history, ...rest} = enterStates(simtime, state, toEnter, {environment, history, ...rest}));
|
||||||
const enteredMode = exitedMode.union(enteredStates);
|
const enteredMode = exitedMode.union(enteredStates);
|
||||||
|
|
||||||
console.log('new mode', enteredMode);
|
// console.log('new mode', enteredMode);
|
||||||
|
|
||||||
console.log('done firing', transitionDescription(t));
|
// console.log('done firing', transitionDescription(t));
|
||||||
|
|
||||||
return {mode: enteredMode, environment, history, ...rest};
|
return {mode: enteredMode, environment, history, ...rest};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue