editable connections sc <-> plant
This commit is contained in:
parent
e27d3c4c88
commit
8ac5a730cc
28 changed files with 1191 additions and 1016 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import { Dispatch, memo, ReactElement, SetStateAction } from "react";
|
||||
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect } from "react";
|
||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||
import { InsertMode } from "@/App/VisualEditor/VisualEditor";
|
||||
import { HistoryIcon, PseudoStateIcon, RountangleIcon } from "./Icons";
|
||||
|
||||
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
|
||||
|
||||
export type InsertMode = "and" | "or" | "pseudo" | "shallow" | "deep" | "transition" | "text";
|
||||
|
||||
const insertModes: [InsertMode, string, ReactElement, ReactElement][] = [
|
||||
["and", "AND-states", <RountangleIcon kind="and"/>, <kbd>A</kbd>],
|
||||
["or", "OR-states", <RountangleIcon kind="or"/>, <kbd>O</kbd>],
|
||||
|
|
@ -16,6 +17,47 @@ const insertModes: [InsertMode, string, ReactElement, ReactElement][] = [
|
|||
];
|
||||
|
||||
export const InsertModes = memo(function InsertModes({showKeys, insertMode, setInsertMode}: {showKeys: boolean, insertMode: InsertMode, setInsertMode: Dispatch<SetStateAction<InsertMode>>}) {
|
||||
|
||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
// @ts-ignore
|
||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
if (e.key === "a") {
|
||||
e.preventDefault();
|
||||
setInsertMode("and");
|
||||
}
|
||||
if (e.key === "o") {
|
||||
e.preventDefault();
|
||||
setInsertMode("or");
|
||||
}
|
||||
if (e.key === "p") {
|
||||
e.preventDefault();
|
||||
setInsertMode("pseudo");
|
||||
}
|
||||
if (e.key === "t") {
|
||||
e.preventDefault();
|
||||
setInsertMode("transition");
|
||||
}
|
||||
if (e.key === "x") {
|
||||
e.preventDefault();
|
||||
setInsertMode("text");
|
||||
}
|
||||
if (e.key === "h") {
|
||||
e.preventDefault();
|
||||
setInsertMode(oldMode => {
|
||||
if (oldMode === "shallow") return "deep";
|
||||
return "shallow";
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [setInsertMode]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
() => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [onKeyDown]);
|
||||
|
||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||
return <>{insertModes.map(([m, hint, buttonTxt, keyInfo]) => <KeyInfo key={m} keyInfo={keyInfo}>
|
||||
<button
|
||||
|
|
@ -25,4 +67,4 @@ export const InsertModes = memo(function InsertModes({showKeys, insertMode, setI
|
|||
onClick={() => setInsertMode(m)}
|
||||
>{buttonTxt}</button>
|
||||
</KeyInfo>)}</>;
|
||||
})
|
||||
})
|
||||
|
|
|
|||
61
src/App/TopPanel/SpeedControl.tsx
Normal file
61
src/App/TopPanel/SpeedControl.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { Dispatch, memo, SetStateAction, useCallback, useEffect } from "react";
|
||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||
import { setRealtime, TimeMode } from "@/statecharts/time";
|
||||
|
||||
export const SpeedControl = memo(function SpeedControl({showKeys, timescale, setTimescale, setTime}: {showKeys: boolean, timescale: number, setTimescale: Dispatch<SetStateAction<number>>, setTime: Dispatch<SetStateAction<TimeMode>>}) {
|
||||
|
||||
const onTimeScaleChange = useCallback((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);
|
||||
}
|
||||
});
|
||||
}, [setTime, setTimescale]);
|
||||
|
||||
const onSlower = useCallback(() => {
|
||||
onTimeScaleChange((timescale/2).toString(), Math.round(performance.now()));
|
||||
}, [onTimeScaleChange, timescale]);
|
||||
const onFaster = useCallback(() => {
|
||||
onTimeScaleChange((timescale*2).toString(), Math.round(performance.now()));
|
||||
}, [onTimeScaleChange, timescale]);
|
||||
|
||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (!e.ctrlKey) {
|
||||
if (e.key === "s") {
|
||||
e.preventDefault();
|
||||
onSlower();
|
||||
}
|
||||
if (e.key === "f") {
|
||||
e.preventDefault();
|
||||
onFaster();
|
||||
}
|
||||
}
|
||||
}, [onSlower, onFaster])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [onKeyDown])
|
||||
|
||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||
return <>
|
||||
<label htmlFor="number-timescale">speed</label>
|
||||
<KeyInfo keyInfo={<kbd>S</kbd>}>
|
||||
<button title="slower" onClick={onSlower}>÷2</button>
|
||||
</KeyInfo>
|
||||
<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, Math.round(performance.now()))}/>
|
||||
<KeyInfo keyInfo={<kbd>F</kbd>}>
|
||||
<button title="faster" onClick={onFaster}>×2</button>
|
||||
</KeyInfo>
|
||||
</>
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useState } from "react";
|
||||
import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types";
|
||||
import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time";
|
||||
import { InsertMode } from "../VisualEditor/VisualEditor";
|
||||
import { InsertMode } from "./InsertModes";
|
||||
import { About } from "../Modals/About";
|
||||
import { EditHistory, TraceState } from "../App";
|
||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||
|
|
@ -20,6 +20,7 @@ import StopIcon from '@mui/icons-material/Stop';
|
|||
import { InsertModes } from "./InsertModes";
|
||||
import { usePersistentState } from "@/App/persistent_state";
|
||||
import { RotateButtons } from "./RotateButtons";
|
||||
import { SpeedControl } from "./SpeedControl";
|
||||
|
||||
export type TopPanelProps = {
|
||||
trace: TraceState | null,
|
||||
|
|
@ -79,24 +80,6 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
|||
updateDisplayedTime();
|
||||
}, [setTime, timescale, updateDisplayedTime]);
|
||||
|
||||
const onTimeScaleChange = useCallback((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);
|
||||
}
|
||||
});
|
||||
}, [setTime, setTimescale]);
|
||||
|
||||
// timestamp of next timed transition, in simulated time
|
||||
const timers: Timers = config?.kind === "bigstep" && config.state.sc.environment.get("_timers") || [];
|
||||
const nextTimedTransition: [number, TimerElapseEvent] | undefined = timers[0];
|
||||
|
|
@ -115,16 +98,10 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
|||
}
|
||||
}, [nextTimedTransition, setTime]);
|
||||
|
||||
const onSlower = useCallback(() => {
|
||||
onTimeScaleChange((timescale/2).toString(), Math.round(performance.now()));
|
||||
}, [onTimeScaleChange, timescale]);
|
||||
const onFaster = useCallback(() => {
|
||||
onTimeScaleChange((timescale*2).toString(), Math.round(performance.now()));
|
||||
}, [onTimeScaleChange, timescale]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
// don't capture keyboard events when focused on an input element:
|
||||
// @ts-ignore
|
||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
|
|
@ -143,7 +120,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
|||
onClear();
|
||||
}
|
||||
if (e.key === "Tab") {
|
||||
if (trace === null) {
|
||||
if (config === null) {
|
||||
onInit();
|
||||
}
|
||||
else {
|
||||
|
|
@ -151,14 +128,6 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
|||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.key === "s") {
|
||||
e.preventDefault();
|
||||
onSlower();
|
||||
}
|
||||
if (e.key === "f") {
|
||||
e.preventDefault();
|
||||
onFaster();
|
||||
}
|
||||
if (e.key === "`") {
|
||||
e.preventDefault();
|
||||
setShowKeys(show => !show);
|
||||
|
|
@ -168,23 +137,12 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
|||
onBack();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// ctrl is down
|
||||
if (e.key === "z") {
|
||||
e.preventDefault();
|
||||
onUndo();
|
||||
}
|
||||
if (e.key === "Z") {
|
||||
e.preventDefault();
|
||||
onRedo();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [trace, config, time, onInit, timescale, onChangePaused, setShowKeys, onUndo, onRedo, onSlower, onFaster, onSkip, onBack, onClear]);
|
||||
}, [config, time, onInit, onChangePaused, setShowKeys, onSkip, onBack, onClear]);
|
||||
|
||||
return <div className="toolbar">
|
||||
{/* shortcuts / about */}
|
||||
|
|
@ -241,14 +199,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
|||
|
||||
{/* speed */}
|
||||
<div className="toolbarGroup">
|
||||
<label htmlFor="number-timescale">speed</label>
|
||||
<KeyInfo keyInfo={<kbd>S</kbd>}>
|
||||
<button title="slower" onClick={onSlower}>÷2</button>
|
||||
</KeyInfo>
|
||||
<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, Math.round(performance.now()))}/>
|
||||
<KeyInfo keyInfo={<kbd>F</kbd>}>
|
||||
<button title="faster" onClick={onFaster}>×2</button>
|
||||
</KeyInfo>
|
||||
<SpeedControl setTime={setTime} timescale={timescale} setTimescale={setTimescale} showKeys={showKeys} />
|
||||
 
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,30 @@
|
|||
import { memo } from "react";
|
||||
import { memo, useCallback, useEffect } from "react";
|
||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||
|
||||
import UndoIcon from '@mui/icons-material/Undo';
|
||||
import RedoIcon from '@mui/icons-material/Redo';
|
||||
|
||||
export const UndoRedoButtons = memo(function UndoRedoButtons({showKeys, onUndo, onRedo, historyLength, futureLength}: {showKeys: boolean, onUndo: () => void, onRedo: () => void, historyLength: number, futureLength: number}) {
|
||||
|
||||
const onKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.ctrlKey) {
|
||||
// ctrl is down
|
||||
if (e.key === "z") {
|
||||
e.preventDefault();
|
||||
onUndo();
|
||||
}
|
||||
if (e.key === "Z") {
|
||||
e.preventDefault();
|
||||
onRedo();
|
||||
}
|
||||
}
|
||||
}, [onUndo, onRedo]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [onKeyDown]);
|
||||
|
||||
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||
return <>
|
||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Z</kbd></>}>
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}:
|
|||
if (e.ctrlKey) {
|
||||
if (e.key === "+") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onZoomIn();
|
||||
}
|
||||
if (e.key === "-") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onZoomOut();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue