re-organize project structure a bit + add icons
This commit is contained in:
parent
3cb3ef91d2
commit
5e7b944978
24 changed files with 514 additions and 249 deletions
58
src/App/AST.tsx
Normal file
58
src/App/AST.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { ConcreteState, stateDescription, Transition } from "../statecharts/abstract_syntax";
|
||||
import { Action, Expression } from "../statecharts/label_ast";
|
||||
|
||||
export function ShowTransition(props: {transition: Transition}) {
|
||||
return <>➔ {stateDescription(props.transition.tgt)}</>;
|
||||
}
|
||||
|
||||
export function ShowExpr(props: {expr: Expression}) {
|
||||
if (props.expr.kind === "literal") {
|
||||
return <>{props.expr.value}</>;
|
||||
}
|
||||
else if (props.expr.kind === "ref") {
|
||||
return <>{props.expr.variable}</>;
|
||||
}
|
||||
else if (props.expr.kind === "unaryExpr") {
|
||||
return <>{props.expr.operator}<ShowExpr expr={props.expr.expr}/></>;
|
||||
}
|
||||
else if (props.expr.kind === "binaryExpr") {
|
||||
return <><ShowExpr expr={props.expr.lhs}/>{props.expr.operator}<ShowExpr expr={props.expr.rhs}/></>;
|
||||
}
|
||||
}
|
||||
|
||||
export function ShowAction(props: {action: Action}) {
|
||||
if (props.action.kind === "raise") {
|
||||
return <>^{props.action.event}</>;
|
||||
}
|
||||
else if (props.action.kind === "assignment") {
|
||||
return <>{props.action.lhs} = <ShowExpr expr={props.action.rhs}/>;</>;
|
||||
}
|
||||
}
|
||||
|
||||
export function AST(props: {root: ConcreteState, transitions: Map<string, Transition[]>}) {
|
||||
const description = stateDescription(props.root);
|
||||
const outgoing = props.transitions.get(props.root.uid) || [];
|
||||
|
||||
return <details open={true}>
|
||||
<summary>{props.root.kind}: {description}</summary>
|
||||
|
||||
{props.root.entryActions.length>0 &&
|
||||
props.root.entryActions.map(action =>
|
||||
<div> entry / <ShowAction action={action}/></div>
|
||||
)
|
||||
}
|
||||
{props.root.exitActions.length>0 &&
|
||||
props.root.exitActions.map(action =>
|
||||
<div> exit / <ShowAction action={action}/></div>
|
||||
)
|
||||
}
|
||||
{props.root.children.length>0 &&
|
||||
props.root.children.map(child =>
|
||||
<AST root={child} transitions={props.transitions} />
|
||||
)
|
||||
}
|
||||
{outgoing.length>0 &&
|
||||
outgoing.map(transition => <> <ShowTransition transition={transition}/><br/></>)
|
||||
}
|
||||
</details>
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
.layoutVertical {
|
||||
/* .layoutVertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
|
@ -21,10 +21,22 @@
|
|||
.content {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
} */
|
||||
|
||||
details {
|
||||
padding-left :10;
|
||||
padding-left: 20;
|
||||
/* margin-left: 30; */
|
||||
}
|
||||
summary {
|
||||
margin-left: -20;
|
||||
}
|
||||
|
||||
.runtimeState {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.runtimeState:hover {
|
||||
|
|
@ -47,3 +59,13 @@ details {
|
|||
background-color:"#eee";
|
||||
text-align: "right";
|
||||
}
|
||||
|
||||
|
||||
|
||||
.toolbar > * {
|
||||
vertical-align: middle;
|
||||
height: 26px;
|
||||
}
|
||||
.toolbar > input {
|
||||
height: 20px;
|
||||
}
|
||||
267
src/App/App.tsx
267
src/App/App.tsx
|
|
@ -1,100 +1,29 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { ConcreteState, emptyStatechart, Statechart, stateDescription, Transition } from "../VisualEditor/ast";
|
||||
import { handleInputEvent, initialize } from "../VisualEditor/interpreter";
|
||||
import { TimerElapseEvent, Timers } from "@/VisualEditor/runtime_types";
|
||||
import { Action, Expression } from "../VisualEditor/label_ast";
|
||||
import { BigStep, BigStepOutput, Environment, Mode } from "../VisualEditor/runtime_types";
|
||||
import { emptyStatechart, Statechart } from "../statecharts/abstract_syntax";
|
||||
import { handleInputEvent, initialize } from "../statecharts/interpreter";
|
||||
import { BigStep, BigStepOutput } from "../statecharts/runtime_types";
|
||||
import { VisualEditor } from "../VisualEditor/VisualEditor";
|
||||
import { getSimTime, getWallClkDelay, setPaused, setRealtime, TimeMode } from "../VisualEditor/time";
|
||||
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
|
||||
|
||||
import "../index.css";
|
||||
import "./App.css";
|
||||
|
||||
export function ShowTransition(props: {transition: Transition}) {
|
||||
return <>➔ {stateDescription(props.transition.tgt)}</>;
|
||||
}
|
||||
|
||||
export function ShowExpr(props: {expr: Expression}) {
|
||||
if (props.expr.kind === "literal") {
|
||||
return <>{props.expr.value}</>;
|
||||
}
|
||||
else if (props.expr.kind === "ref") {
|
||||
return <>{props.expr.variable}</>;
|
||||
}
|
||||
else if (props.expr.kind === "unaryExpr") {
|
||||
return <>{props.expr.operator}<ShowExpr expr={props.expr.expr}/></>;
|
||||
}
|
||||
else if (props.expr.kind === "binaryExpr") {
|
||||
return <><ShowExpr expr={props.expr.lhs}/>{props.expr.operator}<ShowExpr expr={props.expr.rhs}/></>;
|
||||
}
|
||||
}
|
||||
|
||||
export function ShowAction(props: {action: Action}) {
|
||||
if (props.action.kind === "raise") {
|
||||
return <>^{props.action.event}</>;
|
||||
}
|
||||
else if (props.action.kind === "assignment") {
|
||||
return <>{props.action.lhs} = <ShowExpr expr={props.action.rhs}/>;</>;
|
||||
}
|
||||
}
|
||||
|
||||
export function AST(props: {root: ConcreteState, transitions: Map<string, Transition[]>}) {
|
||||
const description = stateDescription(props.root);
|
||||
const outgoing = props.transitions.get(props.root.uid) || [];
|
||||
|
||||
return <details open={true}>
|
||||
<summary>{props.root.kind}: {description}</summary>
|
||||
|
||||
{props.root.entryActions.length>0 &&
|
||||
props.root.entryActions.map(action =>
|
||||
<div> entry / <ShowAction action={action}/></div>
|
||||
)
|
||||
}
|
||||
{props.root.exitActions.length>0 &&
|
||||
props.root.exitActions.map(action =>
|
||||
<div> exit / <ShowAction action={action}/></div>
|
||||
)
|
||||
}
|
||||
{props.root.children.length>0 &&
|
||||
props.root.children.map(child =>
|
||||
<AST root={child} transitions={props.transitions} />
|
||||
)
|
||||
}
|
||||
{outgoing.length>0 &&
|
||||
outgoing.map(transition => <> <ShowTransition transition={transition}/><br/></>)
|
||||
}
|
||||
</details>
|
||||
}
|
||||
|
||||
|
||||
function formatTime(timeMs: number) {
|
||||
const leadingZeros = "00" + Math.floor(timeMs) % 1000;
|
||||
const formatted = `${Math.floor(timeMs / 1000)}.${(leadingZeros).substring(leadingZeros.length-3)}`;
|
||||
return formatted;
|
||||
}
|
||||
|
||||
function compactTime(timeMs: number) {
|
||||
if (timeMs % 1000 === 0) {
|
||||
return `${timeMs / 1000}s`;
|
||||
}
|
||||
return `${timeMs} ms`;
|
||||
}
|
||||
|
||||
import { Box, Stack } from "@mui/material";
|
||||
import { TopPanel } from "./TopPanel";
|
||||
import { RTHistory } from "./RTHistory";
|
||||
import { AST } from "./AST";
|
||||
|
||||
export function App() {
|
||||
const [ast, setAST] = useState<Statechart>(emptyStatechart);
|
||||
const [errors, setErrors] = useState<[string,string][]>([]);
|
||||
|
||||
const [rt, setRT] = useState<BigStep[]>([]);
|
||||
const [rtIdx, setRTIdx] = useState<number|null>(null);
|
||||
const [rtIdx, setRTIdx] = useState<number|undefined>();
|
||||
|
||||
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
|
||||
const [timescale, setTimescale] = useState(1);
|
||||
const [displayTime, setDisplayTime] = useState("0.000");
|
||||
|
||||
|
||||
function restart() {
|
||||
function onInit() {
|
||||
const config = initialize(ast);
|
||||
console.log('runtime: ', rt);
|
||||
setRT([{inputEvent: null, simtime: 0, ...config}]);
|
||||
|
|
@ -102,14 +31,14 @@ export function App() {
|
|||
setTime({kind: "paused", simtime: 0});
|
||||
}
|
||||
|
||||
function clear() {
|
||||
function onClear() {
|
||||
setRT([]);
|
||||
setRTIdx(null);
|
||||
setRTIdx(undefined);
|
||||
setTime({kind: "paused", simtime: 0});
|
||||
}
|
||||
|
||||
function raise(inputEvent: string) {
|
||||
if (rt.length>0 && rtIdx!==null && ast.inputEvents.has(inputEvent)) {
|
||||
function onRaise(inputEvent: string) {
|
||||
if (rt.length>0 && rtIdx!==undefined && ast.inputEvents.has(inputEvent)) {
|
||||
const simtime = getSimTime(time, performance.now());
|
||||
const nextConfig = handleInputEvent(simtime, inputEvent, ast, rt[rtIdx]!);
|
||||
appendNewConfig(inputEvent, simtime, nextConfig);
|
||||
|
|
@ -121,19 +50,9 @@ export function App() {
|
|||
setRTIdx(rtIdx!+1);
|
||||
}
|
||||
|
||||
function updateDisplayedTime() {
|
||||
const now = performance.now();
|
||||
const timeMs = getSimTime(time, now);
|
||||
setDisplayTime(formatTime(timeMs));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
updateDisplayedTime();
|
||||
}, 20);
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
if (rtIdx !== null) {
|
||||
if (rtIdx !== undefined) {
|
||||
const currentRt = rt[rtIdx]!;
|
||||
const timers = currentRt.environment.get("_timers") || [];
|
||||
if (timers.length > 0) {
|
||||
|
|
@ -156,135 +75,45 @@ export function App() {
|
|||
}
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
if (timeout) clearTimeout(timeout);
|
||||
}
|
||||
|
||||
}, [time, rtIdx]);
|
||||
|
||||
function onChangePaused(paused: boolean, wallclktime: number) {
|
||||
setTime(time => {
|
||||
if (paused) {
|
||||
return setPaused(time, performance.now());
|
||||
}
|
||||
else {
|
||||
return setRealtime(time, timescale, wallclktime);
|
||||
}
|
||||
});
|
||||
updateDisplayedTime();
|
||||
}
|
||||
|
||||
function onTimeScaleChange(newValue: string, wallclktime: number) {
|
||||
const asFloat = parseFloat(newValue);
|
||||
if (Number.isNaN(asFloat)) {
|
||||
return;
|
||||
}
|
||||
setTimescale(asFloat);
|
||||
setTime(time => {
|
||||
if (time.kind === "paused") {
|
||||
return time;
|
||||
}
|
||||
else {
|
||||
return setRealtime(time, asFloat, wallclktime);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function gotoRt(idx: number, timestamp: number) {
|
||||
setRTIdx(idx);
|
||||
setTime({kind: "paused", simtime: timestamp});
|
||||
}
|
||||
|
||||
// timestamp of next timed transition, in simulated time
|
||||
const timers: Timers = (rt[rtIdx!]?.environment.get("_timers") || []);
|
||||
const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0];
|
||||
|
||||
return <div className="layoutVertical">
|
||||
<div className="panel">
|
||||
|
||||
</div>
|
||||
<div className="panel">
|
||||
<button onClick={restart}>(re)start</button>
|
||||
<button onClick={clear} disabled={rtIdx===null}>clear</button>
|
||||
 
|
||||
{ast.inputEvents &&
|
||||
<>raise
|
||||
{[...ast.inputEvents].map(event => <button title="raise input event" disabled={rtIdx===null} onClick={() => raise(event)}>{event}</button>)}
|
||||
 </>
|
||||
}
|
||||
<input type="radio" name="paused" id="radio-paused" checked={time.kind==="paused"} disabled={rtIdx===null} onChange={e => onChangePaused(e.target.checked, performance.now())}/>
|
||||
<label htmlFor="radio-paused">paused</label>
|
||||
<input type="radio" name="realtime" id="radio-realtime" checked={time.kind==="realtime"} disabled={rtIdx===null} onChange={e => onChangePaused(!e.target.checked, performance.now())}/>
|
||||
<label htmlFor="radio-realtime">real time</label>
|
||||
 
|
||||
<label htmlFor="number-timescale">timescale</label>
|
||||
<input title="controls how fast the simulation should run in real time mode - larger than 1 means: faster than wall-clock time" type="number" min={0} id="number-timescale" disabled={rtIdx===null} value={timescale} style={{width:40}} onChange={e => onTimeScaleChange(e.target.value, performance.now())}/>
|
||||
 
|
||||
<label htmlFor="time">time (s)</label>
|
||||
<input title="the current simulated time" id="time" disabled={rtIdx===null} value={displayTime} readOnly={true} className="readonlyTextBox" />
|
||||
{nextTimedTransition &&
|
||||
<>
|
||||
 
|
||||
<label htmlFor="next-timeout">next timeout (s)</label>
|
||||
<input id="next-timeout" disabled={rtIdx===null} value={formatTime(nextTimedTransition[0])} readOnly={true} className="readonlyTextBox"/>
|
||||
<button title="advance time to the next timer elapse" onClick={() => {
|
||||
const now = performance.now();
|
||||
setTime(time => {
|
||||
if (time.kind === "paused") {
|
||||
return {kind: "paused", simtime: nextTimedTransition[0]};
|
||||
}
|
||||
else {
|
||||
return {kind: "realtime", scale: time.scale, since: {simtime: nextTimedTransition[0], wallclktime: now}};
|
||||
}
|
||||
});
|
||||
}}>advance</button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="layout">
|
||||
<main className="content">
|
||||
return <Stack sx={{height:'100vh'}}>
|
||||
{/* Top bar */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<TopPanel
|
||||
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
|
||||
{...{ast, time, setTime, onInit, onClear, onRaise}}
|
||||
/>
|
||||
</Box>
|
||||
<Stack direction="row" sx={{height:'calc(100vh - 32px)'}}>
|
||||
{/* main */}
|
||||
<Box sx={{flexGrow:1, overflow:'auto'}}>
|
||||
<VisualEditor {...{ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors}}/>
|
||||
</main>
|
||||
<aside className="sidebar">
|
||||
<AST {...ast}/>
|
||||
{rt.map((rt, idx) => <><hr/><div className={"runtimeState"+(idx===rtIdx?" active":"")} onClick={() => gotoRt(idx, rt.simtime)}>
|
||||
<div>({formatTime(rt.simtime)}, {rt.inputEvent || "<init>"})</div>
|
||||
<ShowMode mode={rt.mode} statechart={ast}/>
|
||||
<ShowEnvironment environment={rt.environment}/>
|
||||
{rt.outputEvents.length>0 && <div>
|
||||
{rt.outputEvents.map((e:string) => '^'+e).join(', ')}
|
||||
</div>}
|
||||
</div></>)}
|
||||
</aside>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ShowEnvironment(props: {environment: Environment}) {
|
||||
return <div>{[...props.environment.entries()]
|
||||
.filter(([variable]) => !variable.startsWith('_'))
|
||||
.map(([variable,value]) =>
|
||||
`${variable}: ${value}`
|
||||
).join(', ')}</div>;
|
||||
}
|
||||
|
||||
function ShowMode(props: {mode: Mode, statechart: Statechart}) {
|
||||
const activeLeafs = getActiveLeafs(props.mode, props.statechart);
|
||||
return <div>mode: {[...activeLeafs].map(uid =>
|
||||
stateDescription(props.statechart.uid2State.get(uid)!)).join(", ")}</div>;
|
||||
}
|
||||
|
||||
function getActiveLeafs(mode: Mode, sc: Statechart) {
|
||||
const toDelete = [];
|
||||
for (const stateA of mode) {
|
||||
for (const stateB of mode) {
|
||||
if (sc.uid2State.get(stateA)!.parent === sc.uid2State.get(stateB)) {
|
||||
toDelete.push(stateB);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mode.difference(new Set(toDelete));
|
||||
</Box>
|
||||
{/* right sidebar */}
|
||||
<Box
|
||||
sx={{
|
||||
borderLeft: 1,
|
||||
borderColor: "divider",
|
||||
flex: '0 0 content',
|
||||
paddingRight: 1,
|
||||
paddingLeft: 1,
|
||||
}}>
|
||||
<AST {...ast}/>
|
||||
<hr/>
|
||||
<RTHistory {...{ast, rt, rtIdx, setTime, setRTIdx}}/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
57
src/App/RTHistory.tsx
Normal file
57
src/App/RTHistory.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Dispatch, SetStateAction } from "react";
|
||||
import { Statechart, stateDescription } from "../statecharts/abstract_syntax";
|
||||
import { BigStep, Environment, Mode } from "../statecharts/runtime_types";
|
||||
import { formatTime } from "./util";
|
||||
import { TimeMode } from "../statecharts/time";
|
||||
|
||||
type RTHistoryProps = {
|
||||
rt: BigStep[],
|
||||
rtIdx: number | undefined,
|
||||
ast: Statechart,
|
||||
setRTIdx: Dispatch<SetStateAction<number|undefined>>,
|
||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||
}
|
||||
|
||||
export function RTHistory({rt, rtIdx, ast, setRTIdx, setTime}: RTHistoryProps) {
|
||||
function gotoRt(idx: number, timestamp: number) {
|
||||
setRTIdx(idx);
|
||||
setTime({kind: "paused", simtime: timestamp});
|
||||
}
|
||||
|
||||
return rt.map((rt, idx) => <>
|
||||
<div className={"runtimeState"+(idx===rtIdx?" active":"")} onClick={() => gotoRt(idx, rt.simtime)}>
|
||||
<div>({formatTime(rt.simtime)}, {rt.inputEvent || "<init>"})</div>
|
||||
<ShowMode mode={rt.mode} statechart={ast}/>
|
||||
<ShowEnvironment environment={rt.environment}/>
|
||||
{rt.outputEvents.length>0 && <div>
|
||||
{rt.outputEvents.map((e:string) => '^'+e).join(', ')}
|
||||
</div>}
|
||||
</div></>);
|
||||
}
|
||||
|
||||
|
||||
function ShowEnvironment(props: {environment: Environment}) {
|
||||
return <div>{[...props.environment.entries()]
|
||||
.filter(([variable]) => !variable.startsWith('_'))
|
||||
.map(([variable,value]) =>
|
||||
`${variable}: ${value}`
|
||||
).join(', ')}</div>;
|
||||
}
|
||||
|
||||
function ShowMode(props: {mode: Mode, statechart: Statechart}) {
|
||||
const activeLeafs = getActiveLeafs(props.mode, props.statechart);
|
||||
return <div>mode: {[...activeLeafs].map(uid =>
|
||||
stateDescription(props.statechart.uid2State.get(uid)!)).join(", ")}</div>;
|
||||
}
|
||||
|
||||
function getActiveLeafs(mode: Mode, sc: Statechart) {
|
||||
const toDelete = [];
|
||||
for (const stateA of mode) {
|
||||
for (const stateB of mode) {
|
||||
if (sc.uid2State.get(stateA)!.parent === sc.uid2State.get(stateB)) {
|
||||
toDelete.push(stateB);
|
||||
}
|
||||
}
|
||||
}
|
||||
return mode.difference(new Set(toDelete));
|
||||
}
|
||||
127
src/App/TopPanel.tsx
Normal file
127
src/App/TopPanel.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { BigStep, TimerElapseEvent, Timers } from "../statecharts/runtime_types";
|
||||
import { getSimTime, setPaused, setRealtime, TimeMode } from "../statecharts/time";
|
||||
import { Statechart } from "../statecharts/abstract_syntax";
|
||||
|
||||
import CachedIcon from '@mui/icons-material/Cached';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import PauseIcon from '@mui/icons-material/Pause';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import BoltIcon from '@mui/icons-material/Bolt';
|
||||
import SkipNextIcon from '@mui/icons-material/SkipNext';
|
||||
import { formatTime } from "./util";
|
||||
|
||||
export type TopPanelProps = {
|
||||
rt?: BigStep,
|
||||
time: TimeMode,
|
||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||
onInit: () => void,
|
||||
onClear: () => void,
|
||||
onRaise: (e: string) => void,
|
||||
ast: Statechart,
|
||||
}
|
||||
|
||||
export function TopPanel({rt, time, setTime, onInit, onClear, onRaise, ast}: TopPanelProps) {
|
||||
const [displayTime, setDisplayTime] = useState("0.000");
|
||||
const [timescale, setTimescale] = useState(1);
|
||||
|
||||
function updateDisplayedTime() {
|
||||
const now = performance.now();
|
||||
const timeMs = getSimTime(time, now);
|
||||
setDisplayTime(formatTime(timeMs));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
updateDisplayedTime();
|
||||
}, 20);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, [time]);
|
||||
|
||||
function onChangePaused(paused: boolean, wallclktime: number) {
|
||||
setTime(time => {
|
||||
if (paused) {
|
||||
return setPaused(time, performance.now());
|
||||
}
|
||||
else {
|
||||
return setRealtime(time, timescale, wallclktime);
|
||||
}
|
||||
});
|
||||
updateDisplayedTime();
|
||||
}
|
||||
|
||||
function onTimeScaleChange(newValue: string, wallclktime: number) {
|
||||
const asFloat = parseFloat(newValue);
|
||||
if (Number.isNaN(asFloat)) {
|
||||
return;
|
||||
}
|
||||
const maxed = Math.min(asFloat, 64);
|
||||
const mined = Math.max(maxed, 1/64);
|
||||
setTimescale(mined);
|
||||
setTime(time => {
|
||||
if (time.kind === "paused") {
|
||||
return time;
|
||||
}
|
||||
else {
|
||||
return setRealtime(time, mined, wallclktime);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// timestamp of next timed transition, in simulated time
|
||||
const timers: Timers = (rt?.environment.get("_timers") || []);
|
||||
const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0];
|
||||
|
||||
return <div className="toolbar">
|
||||
<button title="(re)initialize simulation" onClick={onInit} ><CachedIcon fontSize="small"/></button>
|
||||
<button title="clear the simulation" onClick={onClear} disabled={!rt}><ClearIcon fontSize="small"/></button>
|
||||
|
||||
 
|
||||
|
||||
<button title="pause the simulation" disabled={!rt || time.kind==="paused"} onClick={() => onChangePaused(true, performance.now())}><PauseIcon fontSize="small"/></button>
|
||||
<button title="run the simulation in real time" disabled={!rt || time.kind==="realtime"} onClick={() => onChangePaused(false, performance.now())}><PlayArrowIcon fontSize="small"/></button>
|
||||
|
||||
 
|
||||
|
||||
<label htmlFor="number-timescale">timescale</label>
|
||||
<button title="slower" onClick={() => onTimeScaleChange((timescale/2).toString(), performance.now())}>÷2</button>
|
||||
<input title="controls how fast the simulation should run in real time mode - larger than 1 means: faster than wall-clock time" id="number-timescale" value={timescale.toFixed(3)} style={{width:40}} readOnly onChange={e => onTimeScaleChange(e.target.value, performance.now())}/>
|
||||
<button title="faster" onClick={() => onTimeScaleChange((timescale*2).toString(), performance.now())}>×2</button>
|
||||
|
||||
 
|
||||
|
||||
{ast.inputEvents &&
|
||||
<>
|
||||
{[...ast.inputEvents].map(event => <button title={`raise input event '${event}'`} disabled={!rt} onClick={() => onRaise(event)}><BoltIcon fontSize="small"/> {event}</button>)}
|
||||
 </>
|
||||
}
|
||||
|
||||
{/* <ToggleButtonGroup value={time.kind} exclusive onChange={(_,newValue) => onChangePaused(newValue==="paused", performance.now())} size="small">
|
||||
<ToggleButton disableRipple value="paused" disabled={!rt}><PauseIcon/></ToggleButton>
|
||||
<ToggleButton disableRipple value="realtime" disabled={!rt}><PlayArrowIcon/></ToggleButton>
|
||||
</ToggleButtonGroup> */}
|
||||
|
||||
 
|
||||
|
||||
<label htmlFor="time">time (s)</label>
|
||||
<input title="the current simulated time" id="time" disabled={!rt} value={displayTime} readOnly={true} className="readonlyTextBox" />
|
||||
|
||||
 
|
||||
|
||||
<label htmlFor="next-timeout">next (s)</label>
|
||||
<input title="next point in simulated time where a timed transition may fire" id="next-timeout" disabled={!rt} value={nextTimedTransition ? formatTime(nextTimedTransition[0]) : '+inf'} readOnly={true} className="readonlyTextBox"/>
|
||||
<button title="advance time just enough for the next timer to elapse" disabled={nextTimedTransition===undefined} onClick={() => {
|
||||
const now = performance.now();
|
||||
setTime(time => {
|
||||
if (time.kind === "paused") {
|
||||
return {kind: "paused", simtime: nextTimedTransition[0]};
|
||||
}
|
||||
else {
|
||||
return {kind: "realtime", scale: time.scale, since: {simtime: nextTimedTransition[0], wallclktime: now}};
|
||||
}
|
||||
});
|
||||
}}><SkipNextIcon fontSize="small"/></button>
|
||||
</div>;
|
||||
}
|
||||
13
src/App/util.ts
Normal file
13
src/App/util.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export function formatTime(timeMs: number) {
|
||||
const leadingZeros = "00" + Math.floor(timeMs) % 1000;
|
||||
const formatted = `${Math.floor(timeMs / 1000)}.${(leadingZeros).substring(leadingZeros.length-3)}`;
|
||||
return formatted;
|
||||
}
|
||||
|
||||
export function compactTime(timeMs: number) {
|
||||
if (timeMs % 1000 === 0) {
|
||||
return `${timeMs / 1000}s`;
|
||||
}
|
||||
return `${timeMs} ms`;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue