editable connections sc <-> plant

This commit is contained in:
Joeri Exelmans 2025-10-30 17:14:57 +01:00
parent e27d3c4c88
commit 8ac5a730cc
28 changed files with 1191 additions and 1016 deletions

View file

@ -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>)}</>;
})
})

View 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>&nbsp;
<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>
</>
});

View file

@ -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>&nbsp;
<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} />
&emsp;
</div>

View file

@ -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></>}>

View file

@ -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();
}
}