From 3f6b2ba9500dec77ef00546f2713481d1edcd1fd Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Fri, 7 Nov 2025 14:09:04 +0100
Subject: [PATCH 01/30] fix interpreter bug
---
src/statecharts/interpreter.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts
index 2520187..f7db64c 100644
--- a/src/statecharts/interpreter.ts
+++ b/src/statecharts/interpreter.ts
@@ -326,6 +326,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.
export function fairStep(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {arenasFired, environment, ...config}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
+ console.log('fair step', event, activeParent);
environment = environment.enterScope(activeParent.uid);
// console.log('fairStep', arenasFired);
for (const state of activeParent.children) {
@@ -346,6 +347,7 @@ export function fairStep(simtime: number, event: RT_Event, statechart: Statechar
}
export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStep {
+ console.log('handleInputEvent', event);
let raised = initialRaised;
({mode, environment, ...raised} = fairStep(simtime, event, statechart, statechart.root, {mode, environment, history, arenasFired: [], ...raised}));
@@ -354,11 +356,12 @@ export function handleInputEvent(simtime: number, event: RT_Event, statechart: S
}
export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents) {
+ console.log('handleInternalEvents');
while (internalEvents.length > 0) {
const [nextEvent, ...remainingEvents] = internalEvents;
({internalEvents, ...rest} = fairStep(simtime,
{kind: "input", ...nextEvent}, // internal event becomes input event
- statechart, statechart.root, { arenasFired: [], internalEvents: remainingEvents, ...rest}));
+ statechart, statechart.root, { ...rest, arenasFired: [], internalEvents: remainingEvents, }));
}
return rest;
}
From 5713c3a59f787e9f81ea23faf26366f56f1301fb Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Fri, 7 Nov 2025 14:30:37 +0100
Subject: [PATCH 02/30] edit assignment.html
---
assignment.html | 26 +++++++++++++++-----------
1 file changed, 15 insertions(+), 11 deletions(-)
diff --git a/assignment.html b/assignment.html
index b8252bb..3e807cd 100644
--- a/assignment.html
+++ b/assignment.html
@@ -93,7 +93,10 @@
Note: StateBuddy will be updated from time to time, to fix bugs or add new features.
To make sure you have the latest version, in StateBuddy, use the shortcut
Ctrl +
Shift +
R to refresh the page while clearing your browser cache.
+
StateBuddy was tested in Firefox 144.0 and Chromium 141.0.7390.107.
+
+Problems? Please use the GitHub
issue tracker .
Exercises
@@ -103,15 +106,15 @@ If you can solve the exercises, you will have a good (enough) understanding of t
The exercises can be opened by clicking on their respective links:
-
+
nested timed transitions
-
+
parent-first
-
+
order of orthogonal regions
-
+
crossing orthogonal regions
-
+
internal events (yes, there's a bug here that i should fix)
@@ -137,7 +140,7 @@ If you can solve the exercises, you will have a good (enough) understanding of t
When a transition fires: first, all the exit actions of all the exited states are executed (in order: child to parent), then the action of the transition itself, followed by the enter actions of the entered states (in order: parent to child)
- In this example , when firing the transition from A to F, first the exit actions of A, B, and C are executed, then the actions of the transition itself, and finally the enter actions of D, E and F (in that order).
+ In this example , when firing the transition from A to F, first the exit actions of A, B, and C are executed, then the actions of the transition itself, and finally the enter actions of D, E and F (in that order).
Any internal events that are raised (as a result of firing transitions), are added to the internal event (FIFO) queue.
@@ -151,9 +154,10 @@ If you can solve the exercises, you will have a good (enough) understanding of t
+ Non-determinism (e.g., multiple enabled outgoing transitions of the same state) results in a run-time error!
Example:
- Consider the linked Statechart .
+ Consider the linked Statechart .
After initialization, the current states are: OrthogonalState, A, C, E.
Then, the Statechart remains idle until it receives an input event. Suppose at time T=5s, the input event e is received. This triggers the execution of an RTC step.
The RTC step starts with a fair-step, where regions r1 , r2 and r3 (in that order) are allowed to fire at most one transition each.
@@ -199,7 +203,7 @@ If you can solve the exercises, you will have a good (enough) understanding of t
Interfaces
You will implement the plant (= digital watch) controller as a Statechart. The controller only talks to the plant via input-events and output-events. In StateBuddy, you can also interactively raise input events directly into the controller statechart (Debugger UI). Finally, the plant also has its own UI, which sends input events to the plant.
-For the curious student: Yes, the (simulated) plant is also implemented as a (rather big) Statechart .
+For the curious student: Yes, the (simulated) plant is also implemented as a (rather big) Statechart .
Overview of our simulated system-under-study.
@@ -313,7 +317,7 @@ The controller can send the following events to the plant:
Starting point
-Use this link to the starting point for this assignment.
+Use this link to the starting point for this assignment.
Testing your solution
To test your solution, initialize the execution, and interact with the plant UI. The execution can run in (scaled) real-time, with the ability to pause/resume.
@@ -359,8 +363,8 @@ meaning: "as long as the top-right button is pressed, the light should be on, an
Additional resources
From 2d6b8a764b1c13055b3aa36ff53f4d90d0ca28c1 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Fri, 7 Nov 2025 14:32:43 +0100
Subject: [PATCH 03/30] update assignment.html (the bug mentioned has been
fixed)
---
assignment.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assignment.html b/assignment.html
index 3e807cd..5c829af 100644
--- a/assignment.html
+++ b/assignment.html
@@ -115,7 +115,7 @@ If you can solve the exercises, you will have a good (enough) understanding of t
crossing orthogonal regions
- internal events (yes, there's a bug here that i should fix)
+ internal events
From fcf9448441d8609300405d0da47a5d8406f7be0f Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Fri, 7 Nov 2025 14:35:53 +0100
Subject: [PATCH 04/30] assignment: fix example
---
assignment.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assignment.html b/assignment.html
index 5c829af..337eefc 100644
--- a/assignment.html
+++ b/assignment.html
@@ -112,7 +112,7 @@ If you can solve the exercises, you will have a good (enough) understanding of t
parent-first
order of orthogonal regions
-
+
crossing orthogonal regions
internal events
From 2dd35ab079121c4a10a26bfb63b3d754877934c3 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Fri, 7 Nov 2025 15:09:21 +0100
Subject: [PATCH 05/30] assignment: fix property (it was wrong) + always
include traces for 'true' and 'false'
---
assignment.html | 4 ++--
src/App/check_property.ts | 5 ++++-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/assignment.html b/assignment.html
index 337eefc..827ce77 100644
--- a/assignment.html
+++ b/assignment.html
@@ -317,12 +317,12 @@ The controller can send the following events to the plant:
Starting point
-Use this link to the starting point for this assignment.
+Use this link to the starting point for this assignment.
Testing your solution
To test your solution, initialize the execution, and interact with the plant UI. The execution can run in (scaled) real-time, with the ability to pause/resume.
To gain more confidence that you correctly implemented the requirements, you can write Metric Temporal Logic (MTL) properties. An example of such a property is:
-
G (topRightPressed -> (lightOn U (~topRightPressed & F[0,2] lightOn)))
+ G ((topRightPressed & (X ~topRightPressed)) -> (G[0,2000] (lightOn)))
meaning: "as long as the top-right button is pressed, the light should be on, and after the top-right button is released, the light should remain on for 2 seconds" AKA Requirement 1.
diff --git a/src/App/check_property.ts b/src/App/check_property.ts
index ff6e25a..85e1cce 100644
--- a/src/App/check_property.ts
+++ b/src/App/check_property.ts
@@ -36,7 +36,10 @@ export async function checkProperty(plant: Plant, property:
return [entry];
}, [] as {simtime: number, state: any}[]);
- let traces = {} as {[key: string]: [number, any][]};
+ let traces = {
+ 'true': [0, true] as [number, any],
+ 'false': [0, false] as [number, any],
+ } as {[key: string]: [number, any][]};
for (const {simtime, state} of cleanPlantStates) {
for (const [key, value] of Object.entries(state)) {
// just append
From 9922f8588daebd04e28d2e1132533d3879ccf248 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Sat, 8 Nov 2025 10:32:28 +0100
Subject: [PATCH 06/30] clean up code a bit (split of SideBar component and
simulator callbacks from App) + fix bug in property checker
---
src/App/App.tsx | 601 +++-----------------------
src/App/SideBar.tsx | 331 ++++++++++++++
src/App/TopPanel/TopPanel.tsx | 3 +-
src/App/VisualEditor/VisualEditor.tsx | 14 +-
src/App/check_property.ts | 6 +-
src/App/makePartialSetter.ts | 36 ++
src/App/plants.ts | 14 +
src/App/useSimulator.ts | 226 +++++++++-
src/App/useUrlHashState.ts | 191 +-------
src/statecharts/interpreter.ts | 3 -
10 files changed, 707 insertions(+), 718 deletions(-)
create mode 100644 src/App/SideBar.tsx
create mode 100644 src/App/makePartialSetter.ts
create mode 100644 src/App/plants.ts
diff --git a/src/App/App.tsx b/src/App/App.tsx
index b18d96a..88450c4 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -1,36 +1,20 @@
import "../index.css";
import "./App.css";
-import { Dispatch, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
-import AddIcon from '@mui/icons-material/Add';
-import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
-import CachedOutlinedIcon from '@mui/icons-material/CachedOutlined';
-import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
-import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
-import VisibilityIcon from '@mui/icons-material/Visibility';
-
-import { Statechart } from "@/statecharts/abstract_syntax";
import { detectConnections } from "@/statecharts/detect_connections";
-import { Conns, coupledExecution, 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 { PersistentDetails, PersistentDetailsLocalStorage } from "./PersistentDetails";
-import { digitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch";
-import { dummyPlant } from "./Plant/Dummy/Dummy";
-import { microwavePlant } from "./Plant/Microwave/Microwave";
-import { Plant } from "./Plant/Plant";
-import { trafficLightPlant } from "./Plant/TrafficLight/TrafficLight";
-import { RTHistory } from "./RTHistory";
-import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
+import { defaultSideBarState, SideBar, SideBarState } from "./SideBar";
+import { InsertMode } from "./TopPanel/InsertModes";
import { TopPanel } from "./TopPanel/TopPanel";
import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
-import { checkProperty, PropertyCheckResult } from "./check_property";
+import { makeIndividualSetters } from "./makePartialSetter";
import { useEditor } from "./useEditor";
+import { useSimulator } from "./useSimulator";
import { useUrlHashState } from "./useUrlHashState";
+import { plants } from "./plants";
export type EditHistory = {
current: VisualEditorState,
@@ -38,53 +22,22 @@ export type EditHistory = {
future: VisualEditorState[],
}
-type UniversalPlantState = {[property: string]: boolean|number};
+export type AppState = {
+ showKeys: boolean,
+ zoom: number,
+ insertMode: InsertMode,
+} & SideBarState;
-const plants: [string, Plant][] = [
- ["dummy", dummyPlant],
- ["microwave", microwavePlant as unknown as Plant],
- ["digital watch", digitalWatchPlant as unknown as Plant],
- ["traffic light", trafficLightPlant as unknown as Plant],
-]
+const defaultAppState: AppState = {
+ showKeys: true,
+ zoom: 1,
+ insertMode: 'and',
-export type TraceItemError = {
- cause: BigStepCause, // event name, or
- simtime: number,
- error: RuntimeError,
+ ...defaultSideBarState,
}
-type CoupledState = {
- sc: BigStep,
- plant: BigStep,
- // plantCleanState: {[prop: string]: boolean|number},
-};
-
-export type BigStepCause = {
- kind: "init",
- simtime: 0,
-} | {
- kind: "input",
- simtime: number,
- eventName: string,
- param?: any,
-} | {
- kind: "timer",
- simtime: number,
-};
-
-export type TraceItem =
- { kind: "error" } & TraceItemError
-| { kind: "bigstep", simtime: number, cause: BigStepCause, state: CoupledState, outputEvents: RaisedEvent[] };
-
-export type TraceState = {
- trace: [TraceItem, ...TraceItem[]], // non-empty
- idx: number,
-};
-
export function App() {
const [editHistory, setEditHistory] = useState(null);
- const [trace, setTrace] = useState(null);
- const [time, setTime] = useState({kind: "paused", simtime: 0});
const [modal, setModal] = useState(null);
const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
@@ -94,54 +47,51 @@ export function App() {
setEditHistory(historyState => historyState && ({...historyState, current: cb(historyState.current)}));
}, [setEditHistory]);
- const {
- autoConnect,
- setAutoConnect,
- autoScroll,
- setAutoScroll,
- plantConns,
- setPlantConns,
- showKeys,
- setShowKeys,
- zoom,
- setZoom,
- insertMode,
- setInsertMode,
- plantName,
- setPlantName,
- showConnections,
- setShowConnections,
- showProperties,
- setShowProperties,
- showExecutionTrace,
- setShowExecutionTrace,
- showPlantTrace,
- setShowPlantTrace,
- properties,
- setProperties,
- savedTraces,
- setSavedTraces,
- activeProperty,
- setActiveProperty,
- } = useUrlHashState(editorState, setEditHistory);
- const plant = plants.find(([pn, p]) => pn === plantName)![1];
-
- const refRightSideBar = useRef(null);
-
// parse concrete syntax always:
const conns = useMemo(() => editorState && detectConnections(editorState), [editorState]);
const parsed = useMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns]);
const ast = parsed && parsed[0];
- const syntaxErrors = parsed && parsed[1] || [];
- const currentTraceItem = trace && trace.trace[trace.idx];
- const allErrors = [
- ...syntaxErrors,
- ...(currentTraceItem && currentTraceItem.kind === "error") ? [{
- message: currentTraceItem.error.message,
- shapeUid: currentTraceItem.error.highlight[0],
- }] : [],
- ];
+ const [appState, setAppState] = useState(defaultAppState);
+
+ const persist = useUrlHashState(
+ recoveredState => {
+ // we support two formats
+ // @ts-ignore
+ if (recoveredState.nextID) {
+ // old format
+ setEditHistory(() => ({current: recoveredState as VisualEditorState, history: [], future: []}));
+ }
+ else {
+ // new format
+ // @ts-ignore
+ if (recoveredState.editorState !== undefined) {
+ const {editorState, ...appState} = recoveredState as AppState & {editorState: VisualEditorState};
+ setEditHistory(() => ({current: editorState, history: [], future: []}));
+ setAppState(() => appState);
+ }
+ }
+ },
+ );
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ if (editorState !== null) {
+ persist({editorState, ...appState});
+ }
+ }, 100);
+ return () => clearTimeout(timeout);
+ }, [editorState, appState]);
+
+ const {
+ autoScroll,
+ plantConns,
+ plantName,
+ } = appState;
+
+ const plant = plants.find(([pn, p]) => pn === plantName)![1];
+
+ const refRightSideBar = useRef(null);
const scrollDownSidebar = useCallback(() => {
if (autoScroll && refRightSideBar.current) {
const el = refRightSideBar.current;
@@ -152,221 +102,25 @@ export function App() {
}
}, [refRightSideBar.current, autoScroll]);
- // coupled execution
- const cE = useMemo(() => ast && coupledExecution({
- sc: statechartExecution(ast),
- plant: plant.execution,
- }, {
- ...plantConns,
- ...Object.fromEntries(ast.inputEvents.map(({event}) => ["debug."+event, ['sc',event] as [string,string]])),
- }), [ast]);
-
- const onInit = useCallback(() => {
- if (cE === null) return;
- const metadata = {simtime: 0, cause: {kind: "init" as const, simtime: 0 as const}};
- try {
- const [outputEvents, state] = cE.initial(); // may throw if initialing the statechart results in a RuntimeError
- setTrace({
- trace: [{kind: "bigstep", ...metadata, state, outputEvents}],
- idx: 0,
- });
- }
- catch (error) {
- if (error instanceof RuntimeError) {
- setTrace({
- trace: [{kind: "error", ...metadata, error}],
- idx: 0,
- });
- }
- else {
- throw error; // probably a bug in the interpreter
- }
- }
- setTime(time => {
- if (time.kind === "paused") {
- return {...time, simtime: 0};
- }
- else {
- return {...time, since: {simtime: 0, wallclktime: performance.now()}};
- }
- });
- scrollDownSidebar();
- }, [cE, scrollDownSidebar]);
-
- const onClear = useCallback(() => {
- setTrace(null);
- setTime({kind: "paused", simtime: 0});
- }, [setTrace, setTime]);
-
- // raise input event, producing a new runtime configuration (or a runtime error)
- const onRaise = (inputEvent: string, param: any) => {
- if (cE === null) return;
- if (currentTraceItem !== null /*&& ast.inputEvents.some(e => e.event === inputEvent)*/) {
- if (currentTraceItem.kind === "bigstep") {
- const simtime = getSimTime(time, Math.round(performance.now()));
- appendNewConfig(simtime, {kind: "input", simtime, eventName: inputEvent, param}, () => {
- return cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param});
- });
- }
- }
- };
-
- // timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
- useEffect(() => {
- let timeout: NodeJS.Timeout | undefined;
- if (currentTraceItem !== null && cE !== null) {
- if (currentTraceItem.kind === "bigstep") {
- const nextTimeout = cE?.timeAdvance(currentTraceItem.state);
-
- const raiseTimeEvent = () => {
- appendNewConfig(nextTimeout, {kind: "timer", simtime: nextTimeout}, () => {
- return cE.intTransition(currentTraceItem.state);
- });
- }
-
- if (time.kind === "realtime") {
- const wallclkDelay = getWallClkDelay(time, nextTimeout, Math.round(performance.now()));
- if (wallclkDelay !== Infinity) {
- timeout = setTimeout(raiseTimeEvent, wallclkDelay);
- }
- }
- else if (time.kind === "paused") {
- if (nextTimeout <= time.simtime) {
- raiseTimeEvent();
- }
- }
- }
- }
- return () => {
- if (timeout) clearTimeout(timeout);
- }
- }, [time, currentTraceItem]); // <-- todo: is this really efficient?
-
- function appendNewConfig(simtime: number, cause: BigStepCause, computeNewState: () => [RaisedEvent[], CoupledState]) {
- let newItem: TraceItem;
- const metadata = {simtime, cause}
- try {
- const [outputEvents, state] = computeNewState(); // may throw RuntimeError
- newItem = {kind: "bigstep", ...metadata, state, outputEvents};
- }
- catch (error) {
- if (error instanceof RuntimeError) {
- newItem = {kind: "error", ...metadata, error};
- // also pause the simulation, for dramatic effect:
- setTime({kind: "paused", simtime});
- }
- else {
- throw error;
- }
- }
- // @ts-ignore
- setTrace(trace => ({
- trace: [
- ...trace!.trace.slice(0, trace!.idx+1), // remove everything after current item
- newItem,
- ],
- // idx: 0,
- idx: trace!.idx+1,
- }));
- scrollDownSidebar();
- }
-
- const onBack = useCallback(() => {
- if (trace !== null) {
- setTime(() => {
- if (trace !== null) {
- return {
- kind: "paused",
- simtime: trace.trace[trace.idx-1].simtime,
- }
- }
- return { kind: "paused", simtime: 0 };
- });
- setTrace({
- ...trace,
- idx: trace.idx-1,
- });
- }
- }, [trace]);
+ const simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar);
+
+ const setters = makeIndividualSetters(setAppState, Object.keys(appState) as (keyof AppState)[]);
+ const syntaxErrors = parsed && parsed[1] || [];
+ const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx];
const currentBigStep = currentTraceItem && currentTraceItem.kind === "bigstep" && currentTraceItem;
+ const allErrors = [
+ ...syntaxErrors,
+ ...(currentTraceItem && currentTraceItem.kind === "error") ? [{
+ message: currentTraceItem.error.message,
+ shapeUid: currentTraceItem.error.highlight[0],
+ }] : [],
+ ];
const highlightActive = (currentBigStep && currentBigStep.state.sc.mode) || new Set();
const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || [];
-
- const speed = time.kind === "paused" ? 0 : time.scale;
-
const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1];
- useEffect(() => {
- ast && autoConnect && autoDetectConns(ast, plant, setPlantConns);
- }, [ast, plant, autoConnect]);
-
- const [propertyResults, setPropertyResults] = useState(null);
-
-
- const onSaveTrace = () => {
- if (trace) {
- setSavedTraces(savedTraces => [
- ...savedTraces,
- ["untitled", trace.trace.map((item) => item.cause)] as [string, BigStepCause[]],
- ]);
- }
- }
-
- const onReplayTrace = (causes: BigStepCause[]) => {
- if (cE) {
- function run_until(simtime: number) {
- while (true) {
- const nextTimeout = cE!.timeAdvance(lastState);
- if (nextTimeout > simtime) {
- break;
- }
- const [outputEvents, coupledState] = cE!.intTransition(lastState);
- lastState = coupledState;
- lastSimtime = nextTimeout;
- newTrace.push({kind: "bigstep", simtime: nextTimeout, state: coupledState, outputEvents, cause: {kind: "timer", simtime: nextTimeout}});
- }
- }
- const [outputEvents, coupledState] = cE.initial();
- const newTrace = [{kind: "bigstep", simtime: 0, state: coupledState, outputEvents, cause: {kind: "init"} as BigStepCause} as TraceItem] as [TraceItem, ...TraceItem[]];
- let lastState = coupledState;
- let lastSimtime = 0;
- for (const cause of causes) {
- if (cause.kind === "input") {
- run_until(cause.simtime); // <-- just make sure we haven't missed any timers elapsing
- // @ts-ignore
- const [outputEvents, coupledState] = cE.extTransition(cause.simtime, newTrace.at(-1)!.state, {kind: "input", name: cause.eventName, param: cause.param});
- lastState = coupledState;
- lastSimtime = cause.simtime;
- newTrace.push({kind: "bigstep", simtime: cause.simtime, state: coupledState, outputEvents, cause});
- }
- else if (cause.kind === "timer") {
- run_until(cause.simtime);
- }
- }
- setTrace({trace: newTrace, idx: newTrace.length-1});
- setTime({kind: "paused", simtime: lastSimtime});
- }
- }
-
- // if some properties change, re-evaluate them:
- useEffect(() => {
- let timeout: NodeJS.Timeout;
- if (trace) {
- setPropertyResults(null);
- timeout = setTimeout(() => {
- Promise.all(properties.map((property, i) => {
- return checkProperty(plant, property, trace.trace);
- }))
- .then(results => {
- setPropertyResults(results);
- })
- })
- }
- return () => clearTimeout(timeout);
- }, [properties, trace, plant]);
-
return <>
{/* Modal dialog */}
@@ -394,13 +148,13 @@ export function App() {
style={{flex: '0 0 content'}}
>
{editHistory && }
{/* Editor */}
{editorState && conns && syntaxErrors &&
- }
+ }
@@ -413,142 +167,7 @@ export function App() {
maxWidth: 'min(400px, 50vw)',
}}>
-
- {/* State tree */}
-
- state tree
-
-
- {/* Input events */}
-
- input events
- {ast && onRaise("debug."+e,p)}
- disabled={trace===null || trace.trace[trace.idx].kind === "error"}
- showKeys={showKeys}/>}
-
- {/* Internal events */}
-
- internal events
- {ast && }
-
- {/* Output events */}
-
- output events
- {ast && }
-
- {/* Plant */}
-
- plant
- setPlantName(() => e.target.value)}>
- {plants.map(([plantName, p]) =>
- {plantName}
- )}
-
-
- {/* Render plant */}
- { onRaise("plant.ui."+e.name, e.param)}
- />}
-
- {/* Connections */}
-
- connections
- setAutoConnect(c => !c)}>
-
-
- {ast && ConnEditor(ast, plant, plantConns, setPlantConns)}
-
- {/* Properties */}
-
setShowProperties(e.newState === "open")}>
- properties
- {plant &&
- available signals:
-
- {plant.signals.join(', ')}
-
}
- {properties.map((property, i) => {
- const result = propertyResults && propertyResults[i];
- let violated = null, propertyError = null;
- if (result) {
- violated = result[0] && result[0].length > 0 && !result[0][0].satisfied;
- propertyError = result[1];
- }
- return
-
-
setActiveProperty(i)}>
-
-
-
setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/>
-
setProperties(properties => properties.toSpliced(i, 1))}>
-
-
- {propertyError &&
{propertyError}
}
-
;
- })}
-
-
setProperties(properties => [...properties, ""])}>
- add property
-
-
-
- {/* Traces */}
-
setShowExecutionTrace(e.newState === "open")}>execution trace
-
- {savedTraces.map((savedTrace, i) =>
-
- onReplayTrace(savedTrace[1])}>
-
-
-
- {(Math.floor(savedTrace[1].at(-1)!.simtime/1000))}s
- ({savedTrace[1].length})
-
- setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1, [e.target.value, savedTraces[i][1]]))}/>
- setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1))}>
-
-
-
- )}
-
-
- setShowPlantTrace(e.target.checked)}/>
- show plant steps
- setAutoScroll(e.target.checked)}/>
- auto-scroll
-
- onSaveTrace()}>
- save trace
-
-
-
-
-
- {/* We cheat a bit, and render the execution trace depending on whether the
above is 'open' or not, rather than putting it as a child of the . We do this because only then can we get the execution trace to scroll without the rest scrolling as well. */}
- {showExecutionTrace &&
- }
-
-
+
@@ -561,83 +180,5 @@ export function App() {
>;
}
-function autoDetectConns(ast: Statechart, plant: Plant, setPlantConns: Dispatch>) {
- for (const {event: a} of plant.uiEvents) {
- for (const {event: b} of plant.inputEvents) {
- if (a === b) {
- setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['plant', b]}));
- break;
- }
- }
- for (const {event: b} of ast.inputEvents) {
- if (a === b) {
- setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['sc', b]}));
- }
- }
- }
- for (const a of ast.outputEvents) {
- for (const {event: b} of plant.inputEvents) {
- if (a === b) {
- setPlantConns(conns => ({...conns, ['sc.'+a]: ['plant', b]}));
- }
- }
- }
- for (const {event: a} of plant.outputEvents) {
- for (const {event: b} of ast.inputEvents) {
- if (a === b) {
- setPlantConns(conns => ({...conns, ['plant.'+a]: ['sc', b]}));
- }
- }
- }
-}
-
-
-function ConnEditor(ast: Statechart, plant: Plant, plantConns: Conns, setPlantConns: Dispatch>) {
- const plantInputs = <>{plant.inputEvents.map(e => plant.{e.event} )}>
- const scInputs = <>{ast.inputEvents.map(e => sc.{e.event} )}>;
- return <>
-
- {/* SC output events can go to Plant */}
- {[...ast.outputEvents].map(e =>
- sc.{e} →
- setPlantConns(conns => ({...conns, [`sc.${e}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
-
- {plantInputs}
-
-
)}
-
- {/* Plant output events can go to Statechart */}
- {[...plant.outputEvents.map(e =>
- plant.{e.event} →
- setPlantConns(conns => ({...conns, [`plant.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))})))}>
-
- {scInputs}
-
-
)]}
-
- {/* Plant UI events typically go to the Plant */}
- {plant.uiEvents.map(e =>
- ui.{e.event} →
- setPlantConns(conns => ({...conns, [`plant.ui.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
-
- {scInputs}
- {plantInputs}
-
-
)}
- >;
-}
-
export default App;
diff --git a/src/App/SideBar.tsx b/src/App/SideBar.tsx
new file mode 100644
index 0000000..0fc79df
--- /dev/null
+++ b/src/App/SideBar.tsx
@@ -0,0 +1,331 @@
+import AddIcon from '@mui/icons-material/Add';
+import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
+import CachedOutlinedIcon from '@mui/icons-material/CachedOutlined';
+import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
+import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
+import VisibilityIcon from '@mui/icons-material/Visibility';
+import { Conns } from '@/statecharts/timed_reactive';
+import { Dispatch, Ref, SetStateAction, useEffect, useRef, useState } from 'react';
+import { Statechart } from '@/statecharts/abstract_syntax';
+import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from './ShowAST';
+import { Plant } from './Plant/Plant';
+import { checkProperty, PropertyCheckResult } from './check_property';
+import { Setters } from './makePartialSetter';
+import { RTHistory } from './RTHistory';
+import { BigStepCause, TraceState } from './useSimulator';
+import { plants, UniversalPlantState } from './plants';
+import { TimeMode } from '@/statecharts/time';
+import { PersistentDetails } from './PersistentDetails';
+
+type SavedTraces = [string, BigStepCause[]][];
+
+export type SideBarState = {
+ showStateTree: boolean,
+ showInputEvents: boolean,
+ showInternalEvents: boolean,
+ showOutputEvents: boolean,
+ showPlant: boolean,
+ showConnections: boolean,
+ showProperties: boolean,
+ showExecutionTrace: boolean,
+
+ plantName: string,
+ plantConns: Conns,
+ autoConnect: boolean,
+
+ properties: string[],
+ activeProperty: number,
+ savedTraces: SavedTraces,
+ autoScroll: boolean,
+ showPlantTrace: boolean,
+};
+
+export const defaultSideBarState = {
+ showStateTree: false,
+ showInputEvents: true,
+ showInternalEvents: true,
+ showOutputEvents: true,
+ showPlant: false,
+ showConnections: false,
+ showProperties: false,
+ showExecutionTrace: true,
+
+ plantName: 'dummy',
+ plantConns: {},
+ autoConnect: true,
+
+ properties: [],
+ activeProperty: 0,
+ savedTraces: [],
+ autoScroll: false,
+ showPlantTrace: false,
+};
+
+type SideBarProps = SideBarState & {
+ refRightSideBar: Ref,
+ ast: Statechart | null,
+ plant: Plant,
+ // setSavedTraces: Dispatch>,
+ trace: TraceState|null,
+ setTrace: Dispatch>,
+ plantState: UniversalPlantState,
+ onRaise: (inputEvent: string, param: any) => void,
+ onReplayTrace: (causes: BigStepCause[]) => void,
+ setTime: Dispatch>,
+ time: TimeMode,
+} & Setters;
+
+export function SideBar({showExecutionTrace, showConnections, plantName, showPlantTrace, showProperties, activeProperty, autoConnect, autoScroll, plantConns, properties, savedTraces, refRightSideBar, ast, plant, setSavedTraces, trace, setTrace, setProperties, setShowPlantTrace, setActiveProperty, setPlantConns, setPlantName, setAutoConnect, setShowProperties, setAutoScroll, time, plantState, onReplayTrace, onRaise, setTime, setShowConnections, setShowExecutionTrace, showPlant, setShowPlant, showOutputEvents, setShowOutputEvents, setShowInternalEvents, showInternalEvents, setShowInputEvents, setShowStateTree, showInputEvents, showStateTree}: SideBarProps) {
+
+ const [propertyResults, setPropertyResults] = useState(null);
+
+ const speed = time.kind === "paused" ? 0 : time.scale;
+
+ const onSaveTrace = () => {
+ if (trace) {
+ setSavedTraces(savedTraces => [
+ ...savedTraces,
+ ["untitled", trace.trace.map((item) => item.cause)] as [string, BigStepCause[]],
+ ]);
+ }
+ }
+
+ // if some properties change, re-evaluate them:
+ useEffect(() => {
+ let timeout: NodeJS.Timeout;
+ if (trace) {
+ setPropertyResults(null);
+ timeout = setTimeout(() => {
+ Promise.all(properties.map((property, i) => {
+ return checkProperty(plant, property, trace.trace);
+ }))
+ .then(results => {
+ setPropertyResults(results);
+ })
+ })
+ }
+ return () => clearTimeout(timeout);
+ }, [properties, trace, plant]);
+
+ // whenever the ast, the plant or 'autoconnect' option changes, detect connections:
+ useEffect(() => {
+ if (ast && autoConnect) {
+ autoDetectConns(ast, plant, setPlantConns);
+ }
+ }, [ast, plant, autoConnect]);
+
+ return <>
+
+ {/* State tree */}
+
+ state tree
+
+
+ {/* Input events */}
+
+ input events
+ {ast && onRaise("debug."+e,p)}
+ disabled={trace===null || trace.trace[trace.idx].kind === "error"}
+ showKeys={true}/>}
+
+ {/* Internal events */}
+
+ internal events
+ {ast && }
+
+ {/* Output events */}
+
+ output events
+ {ast && }
+
+ {/* Plant */}
+
+ plant
+ setPlantName(() => e.target.value)}>
+ {plants.map(([plantName, p]) =>
+ {plantName}
+ )}
+
+
+ {/* Render plant */}
+ { onRaise("plant.ui."+e.name, e.param)}
+ />}
+
+ {/* Connections */}
+
+ connections
+ setAutoConnect(c => !c)}>
+
+
+ {ast && ConnEditor(ast, plant, plantConns, setPlantConns)}
+
+ {/* Properties */}
+
setShowProperties(e.newState === "open")}>
+ properties
+ {plant &&
+ available signals:
+
+ {plant.signals.join(', ')}
+
}
+ {properties.map((property, i) => {
+ const result = propertyResults && propertyResults[i];
+ let violated = null, propertyError = null;
+ if (result) {
+ violated = result[0] && result[0].length > 0 && !result[0][0].satisfied;
+ propertyError = result[1];
+ }
+ return
+
+
setActiveProperty(i)}>
+
+
+
setProperties(properties => properties.toSpliced(i, 1, e.target.value))}/>
+
setProperties(properties => properties.toSpliced(i, 1))}>
+
+
+ {propertyError &&
{propertyError}
}
+
;
+ })}
+
+
setProperties(properties => [...properties, ""])}>
+ add property
+
+
+
+ {/* Traces */}
+
setShowExecutionTrace(e.newState === "open")}>execution trace
+
+ {savedTraces.map((savedTrace, i) =>
+
+ onReplayTrace(savedTrace[1])}>
+
+
+
+ {(Math.floor(savedTrace[1].at(-1)!.simtime/1000))}s
+ ({savedTrace[1].length})
+
+ setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1, [e.target.value, savedTraces[i][1]]))}/>
+ setSavedTraces(savedTraces => savedTraces.toSpliced(i, 1))}>
+
+
+
+ )}
+
+
+ setShowPlantTrace(e.target.checked)}/>
+ show plant steps
+ setAutoScroll(e.target.checked)}/>
+ auto-scroll
+
+ onSaveTrace()}>
+ save trace
+
+
+
+
+
+ {/* We cheat a bit, and render the execution trace depending on whether the above is 'open' or not, rather than putting it as a child of the . We do this because only then can we get the execution trace to scroll without the rest scrolling as well. */}
+ {showExecutionTrace &&
+ }
+ >;
+}
+
+function autoDetectConns(ast: Statechart, plant: Plant, setPlantConns: Dispatch>) {
+ for (const {event: a} of plant.uiEvents) {
+ for (const {event: b} of plant.inputEvents) {
+ if (a === b) {
+ setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['plant', b]}));
+ break;
+ }
+ }
+ for (const {event: b} of ast.inputEvents) {
+ if (a === b) {
+ setPlantConns(conns => ({...conns, ['plant.ui.'+a]: ['sc', b]}));
+ }
+ }
+ }
+ for (const a of ast.outputEvents) {
+ for (const {event: b} of plant.inputEvents) {
+ if (a === b) {
+ setPlantConns(conns => ({...conns, ['sc.'+a]: ['plant', b]}));
+ }
+ }
+ }
+ for (const {event: a} of plant.outputEvents) {
+ for (const {event: b} of ast.inputEvents) {
+ if (a === b) {
+ setPlantConns(conns => ({...conns, ['plant.'+a]: ['sc', b]}));
+ }
+ }
+ }
+}
+
+function ConnEditor(ast: Statechart, plant: Plant, plantConns: Conns, setPlantConns: Dispatch>) {
+ const plantInputs = <>{plant.inputEvents.map(e => plant.{e.event} )}>
+ const scInputs = <>{ast.inputEvents.map(e => sc.{e.event} )}>;
+ return <>
+
+ {/* SC output events can go to Plant */}
+ {[...ast.outputEvents].map(e =>
+ sc.{e} →
+ setPlantConns(conns => ({...conns, [`sc.${e}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
+
+ {plantInputs}
+
+
)}
+
+ {/* Plant output events can go to Statechart */}
+ {[...plant.outputEvents.map(e =>
+ plant.{e.event} →
+ setPlantConns(conns => ({...conns, [`plant.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))})))}>
+
+ {scInputs}
+
+
)]}
+
+ {/* Plant UI events typically go to the Plant */}
+ {plant.uiEvents.map(e =>
+ ui.{e.event} →
+ setPlantConns(conns => ({...conns, [`plant.ui.${e.event}`]: (domEvent.target.value === "" ? undefined : (domEvent.target.value.split('.') as [string,string]))}))}>
+
+ {scInputs}
+ {plantInputs}
+
+
)}
+ >;
+}
+
diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx
index 73260c9..ed59b0a 100644
--- a/src/App/TopPanel/TopPanel.tsx
+++ b/src/App/TopPanel/TopPanel.tsx
@@ -3,7 +3,7 @@ import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types";
import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time";
import { InsertMode } from "./InsertModes";
import { About } from "../Modals/About";
-import { EditHistory, TraceState } from "../App";
+import { EditHistory } from "../App";
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
import { UndoRedoButtons } from "./UndoRedoButtons";
import { ZoomButtons } from "./ZoomButtons";
@@ -21,6 +21,7 @@ import { InsertModes } from "./InsertModes";
import { usePersistentState } from "@/App/persistent_state";
import { RotateButtons } from "./RotateButtons";
import { SpeedControl } from "./SpeedControl";
+import { TraceState } from "../useSimulator";
export type TopPanelProps = {
trace: TraceState | null,
diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx
index 4505dc6..9741520 100644
--- a/src/App/VisualEditor/VisualEditor.tsx
+++ b/src/App/VisualEditor/VisualEditor.tsx
@@ -1,6 +1,5 @@
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
-import { TraceState } from "@/App/App";
import { InsertMode } from "../TopPanel/InsertModes";
import { Mode } from "@/statecharts/runtime_types";
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
@@ -17,7 +16,6 @@ import { useCopyPaste } from "./useCopyPaste";
import "./VisualEditor.css";
import { useMouse } from "./useMouse";
-import { Selecting } from "./Selection";
export type ConcreteSyntax = {
rountangles: Rountangle[];
@@ -59,7 +57,8 @@ type VisualEditorProps = {
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
conns: Connections,
syntaxErrors: TraceableError[],
- trace: TraceState | null,
+ // trace: TraceState | null,
+ // activeStates: Set,
insertMode: InsertMode,
highlightActive: Set,
highlightTransitions: string[],
@@ -68,7 +67,7 @@ type VisualEditorProps = {
zoom: number;
};
-export const VisualEditor = memo(function VisualEditor({state, setState, trace, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
+export const VisualEditor = memo(function VisualEditor({state, setState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
// uid's of selected rountangles
const selection = state.selection || [];
@@ -87,7 +86,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace,
})
});
})
- }, [trace && trace.idx]);
+ }, [highlightTransitions]);
const {onCopy, onPaste, onCut, deleteSelection} = useCopyPaste(makeCheckPoint, state, setState, selection);
@@ -167,15 +166,12 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace,
}
}, [setState]);
- // @ts-ignore
- const active = trace && trace.trace[trace.idx].mode || new Set();
-
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
const size = 4000*zoom;
return e.preventDefault()}
ref={refSVG}
diff --git a/src/App/check_property.ts b/src/App/check_property.ts
index 85e1cce..110112b 100644
--- a/src/App/check_property.ts
+++ b/src/App/check_property.ts
@@ -1,6 +1,6 @@
import { RT_Statechart } from "@/statecharts/runtime_types";
-import { TraceItem } from "./App";
import { Plant } from "./Plant/Plant";
+import { TraceItem } from "./useSimulator";
// const endpoint = "http://localhost:15478/check_property";
const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
@@ -37,8 +37,8 @@ export async function checkProperty(plant: Plant, property:
}, [] as {simtime: number, state: any}[]);
let traces = {
- 'true': [0, true] as [number, any],
- 'false': [0, false] as [number, any],
+ 'true': [[0, true] as [number, any]],
+ 'false': [[0, false] as [number, any]],
} as {[key: string]: [number, any][]};
for (const {simtime, state} of cleanPlantStates) {
for (const [key, value] of Object.entries(state)) {
diff --git a/src/App/makePartialSetter.ts b/src/App/makePartialSetter.ts
new file mode 100644
index 0000000..a9ef4a7
--- /dev/null
+++ b/src/App/makePartialSetter.ts
@@ -0,0 +1,36 @@
+import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
+
+export function makePartialSetter(fullSetter: Dispatch>, key: K): Dispatch> {
+ return (newValueOrCallback: T[K] | ((newValue: T[K]) => T[K])) => {
+ fullSetter(oldFullValue => {
+ if (typeof newValueOrCallback === 'function') {
+ return {
+ ...oldFullValue,
+ [key]: (newValueOrCallback as (newValue: T[K]) => T[K])(oldFullValue[key] as T[K]),
+ }
+ }
+ return {
+ ...oldFullValue,
+ [key]: newValueOrCallback as T[K],
+ }
+ })
+ };
+}
+
+export type Setters = {
+ [K in keyof T as `set${Capitalize>}`]: Dispatch>;
+}
+
+export function makeIndividualSetters(
+ fullSetter: Dispatch>,
+ keys: (keyof T)[],
+): Setters {
+ // @ts-ignore
+ return useMemo(() =>
+ // @ts-ignore
+ Object.fromEntries(keys.map((key: string) => {
+ return [`set${key.charAt(0).toUpperCase()}${key.slice(1)}`, makePartialSetter(fullSetter, key)];
+ })),
+ [fullSetter]
+ );
+}
diff --git a/src/App/plants.ts b/src/App/plants.ts
new file mode 100644
index 0000000..84157a9
--- /dev/null
+++ b/src/App/plants.ts
@@ -0,0 +1,14 @@
+import { digitalWatchPlant } from "./Plant/DigitalWatch/DigitalWatch";
+import { dummyPlant } from "./Plant/Dummy/Dummy";
+import { microwavePlant } from "./Plant/Microwave/Microwave";
+import { Plant } from "./Plant/Plant";
+import { trafficLightPlant } from "./Plant/TrafficLight/TrafficLight";
+
+export type UniversalPlantState = {[property: string]: boolean|number};
+
+export const plants: [string, Plant][] = [
+ ["dummy", dummyPlant],
+ ["microwave", microwavePlant as unknown as Plant],
+ ["digital watch", digitalWatchPlant as unknown as Plant],
+ ["traffic light", trafficLightPlant as unknown as Plant],
+];
diff --git a/src/App/useSimulator.ts b/src/App/useSimulator.ts
index 1f75028..9844455 100644
--- a/src/App/useSimulator.ts
+++ b/src/App/useSimulator.ts
@@ -1,3 +1,223 @@
-export function useSimulator() {
-
-}
\ No newline at end of file
+import { Statechart } from "@/statecharts/abstract_syntax";
+import { RuntimeError } from "@/statecharts/interpreter";
+import { BigStep, RaisedEvent } from "@/statecharts/runtime_types";
+import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { Plant } from "./Plant/Plant";
+import { getSimTime, getWallClkDelay, TimeMode } from "@/statecharts/time";
+import { UniversalPlantState } from "./plants";
+
+type CoupledState = {
+ sc: BigStep,
+ plant: BigStep,
+};
+
+export type TraceItemError = {
+ cause: BigStepCause, // event name, or
+ simtime: number,
+ error: RuntimeError,
+}
+
+export type BigStepCause = {
+ kind: "init",
+ simtime: 0,
+} | {
+ kind: "input",
+ simtime: number,
+ eventName: string,
+ param?: any,
+} | {
+ kind: "timer",
+ simtime: number,
+};
+
+export type TraceItem =
+ { kind: "error" } & TraceItemError
+| { kind: "bigstep", simtime: number, cause: BigStepCause, state: CoupledState, outputEvents: RaisedEvent[] };
+
+export type TraceState = {
+ trace: [TraceItem, ...TraceItem[]], // non-empty
+ idx: number,
+};
+
+
+export function useSimulator(ast: Statechart|null, plant: Plant, plantConns: Conns, onStep: () => void) {
+ const [time, setTime] = useState({kind: "paused", simtime: 0});
+ const [trace, setTrace] = useState(null);
+ const currentTraceItem = trace && trace.trace[trace.idx];
+
+ // coupled execution
+ const cE = useMemo(() => ast && coupledExecution({
+ sc: statechartExecution(ast),
+ plant: plant.execution,
+ }, {
+ ...plantConns,
+ ...Object.fromEntries(ast.inputEvents.map(({event}) => ["debug."+event, ['sc',event] as [string,string]])),
+ }), [ast]);
+
+ const onInit = useCallback(() => {
+ if (cE === null) return;
+ const metadata = {simtime: 0, cause: {kind: "init" as const, simtime: 0 as const}};
+ try {
+ const [outputEvents, state] = cE.initial(); // may throw if initialing the statechart results in a RuntimeError
+ setTrace({
+ trace: [{kind: "bigstep", ...metadata, state, outputEvents}],
+ idx: 0,
+ });
+ }
+ catch (error) {
+ if (error instanceof RuntimeError) {
+ setTrace({
+ trace: [{kind: "error", ...metadata, error}],
+ idx: 0,
+ });
+ }
+ else {
+ throw error; // probably a bug in the interpreter
+ }
+ }
+ setTime(time => {
+ if (time.kind === "paused") {
+ return {...time, simtime: 0};
+ }
+ else {
+ return {...time, since: {simtime: 0, wallclktime: performance.now()}};
+ }
+ });
+ onStep();
+ }, [cE, onStep]);
+
+ const onClear = useCallback(() => {
+ setTrace(null);
+ setTime({kind: "paused", simtime: 0});
+ }, [setTrace, setTime]);
+
+ // raise input event, producing a new runtime configuration (or a runtime error)
+ const onRaise = (inputEvent: string, param: any) => {
+ if (cE === null) return;
+ if (currentTraceItem !== null /*&& ast.inputEvents.some(e => e.event === inputEvent)*/) {
+ if (currentTraceItem.kind === "bigstep") {
+ const simtime = getSimTime(time, Math.round(performance.now()));
+ appendNewConfig(simtime, {kind: "input", simtime, eventName: inputEvent, param}, () => {
+ return cE.extTransition(simtime, currentTraceItem.state, {kind: "input", name: inputEvent, param});
+ });
+ }
+ }
+ };
+
+ // timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
+ useEffect(() => {
+ let timeout: NodeJS.Timeout | undefined;
+ if (currentTraceItem !== null && cE !== null) {
+ if (currentTraceItem.kind === "bigstep") {
+ const nextTimeout = cE?.timeAdvance(currentTraceItem.state);
+
+ const raiseTimeEvent = () => {
+ appendNewConfig(nextTimeout, {kind: "timer", simtime: nextTimeout}, () => {
+ return cE.intTransition(currentTraceItem.state);
+ });
+ }
+
+ if (time.kind === "realtime") {
+ const wallclkDelay = getWallClkDelay(time, nextTimeout, Math.round(performance.now()));
+ if (wallclkDelay !== Infinity) {
+ timeout = setTimeout(raiseTimeEvent, wallclkDelay);
+ }
+ }
+ else if (time.kind === "paused") {
+ if (nextTimeout <= time.simtime) {
+ raiseTimeEvent();
+ }
+ }
+ }
+ }
+ return () => {
+ if (timeout) clearTimeout(timeout);
+ }
+ }, [time, currentTraceItem]); // <-- todo: is this really efficient?
+
+ function appendNewConfig(simtime: number, cause: BigStepCause, computeNewState: () => [RaisedEvent[], CoupledState]) {
+ let newItem: TraceItem;
+ const metadata = {simtime, cause}
+ try {
+ const [outputEvents, state] = computeNewState(); // may throw RuntimeError
+ newItem = {kind: "bigstep", ...metadata, state, outputEvents};
+ }
+ catch (error) {
+ if (error instanceof RuntimeError) {
+ newItem = {kind: "error", ...metadata, error};
+ // also pause the simulation, for dramatic effect:
+ setTime({kind: "paused", simtime});
+ }
+ else {
+ throw error;
+ }
+ }
+ // @ts-ignore
+ setTrace(trace => ({
+ trace: [
+ ...trace!.trace.slice(0, trace!.idx+1), // remove everything after current item
+ newItem,
+ ],
+ // idx: 0,
+ idx: trace!.idx+1,
+ }));
+ onStep();
+ }
+
+ const onBack = useCallback(() => {
+ if (trace !== null) {
+ setTime(() => {
+ if (trace !== null) {
+ return {
+ kind: "paused",
+ simtime: trace.trace[trace.idx-1].simtime,
+ }
+ }
+ return { kind: "paused", simtime: 0 };
+ });
+ setTrace({
+ ...trace,
+ idx: trace.idx-1,
+ });
+ }
+ }, [trace]);
+
+ const onReplayTrace = (causes: BigStepCause[]) => {
+ if (cE) {
+ function run_until(simtime: number) {
+ while (true) {
+ const nextTimeout = cE!.timeAdvance(lastState);
+ if (nextTimeout > simtime) {
+ break;
+ }
+ const [outputEvents, coupledState] = cE!.intTransition(lastState);
+ lastState = coupledState;
+ lastSimtime = nextTimeout;
+ newTrace.push({kind: "bigstep", simtime: nextTimeout, state: coupledState, outputEvents, cause: {kind: "timer", simtime: nextTimeout}});
+ }
+ }
+ const [outputEvents, coupledState] = cE.initial();
+ const newTrace = [{kind: "bigstep", simtime: 0, state: coupledState, outputEvents, cause: {kind: "init"} as BigStepCause} as TraceItem] as [TraceItem, ...TraceItem[]];
+ let lastState = coupledState;
+ let lastSimtime = 0;
+ for (const cause of causes) {
+ if (cause.kind === "input") {
+ run_until(cause.simtime); // <-- just make sure we haven't missed any timers elapsing
+ // @ts-ignore
+ const [outputEvents, coupledState] = cE.extTransition(cause.simtime, newTrace.at(-1)!.state, {kind: "input", name: cause.eventName, param: cause.param});
+ lastState = coupledState;
+ lastSimtime = cause.simtime;
+ newTrace.push({kind: "bigstep", simtime: cause.simtime, state: coupledState, outputEvents, cause});
+ }
+ else if (cause.kind === "timer") {
+ run_until(cause.simtime);
+ }
+ }
+ setTrace({trace: newTrace, idx: newTrace.length-1});
+ setTime({kind: "paused", simtime: lastSimtime});
+ }
+ }
+
+ return {trace, setTrace, plant, onInit, onClear, onBack, onRaise, onReplayTrace, time, setTime};
+}
diff --git a/src/App/useUrlHashState.ts b/src/App/useUrlHashState.ts
index 3c9dd16..3859d54 100644
--- a/src/App/useUrlHashState.ts
+++ b/src/App/useUrlHashState.ts
@@ -1,29 +1,7 @@
-import { Dispatch, SetStateAction, useEffect, useState } from "react";
-import { BigStepCause, EditHistory } from "./App";
-import { VisualEditorState } from "./VisualEditor/VisualEditor";
-import { emptyState } from "@/statecharts/concrete_syntax";
-import { InsertMode } from "./TopPanel/InsertModes";
-import { Conns } from "@/statecharts/timed_reactive";
-
-export function useUrlHashState(editorState: VisualEditorState | null, setEditHistory: Dispatch>) {
-
- // i should probably put all these things into a single object, the 'app state'...
- const [autoScroll, setAutoScroll] = useState(false);
- const [autoConnect, setAutoConnect] = useState(true);
- const [plantConns, setPlantConns] = useState({});
- const [showKeys, setShowKeys] = useState(true);
- const [zoom, setZoom] = useState(1);
- const [insertMode, setInsertMode] = useState("and");
- const [plantName, setPlantName] = useState("dummy");
-
- const [showConnections, setShowConnections] = useState(false);
- const [showProperties, setShowProperties] = useState(false);
- const [showExecutionTrace, setShowExecutionTrace] = useState(true);
- const [showPlantTrace, setShowPlantTrace] = useState(false);
- const [properties, setProperties] = useState([]);
- const [savedTraces, setSavedTraces] = useState<[string, BigStepCause[]][]>([]);
- const [activeProperty, setActiveProperty] = useState(0);
+import { useEffect } from "react";
+// persist state in URL hash
+export function useUrlHashState(recoverCallback: (recoveredState: T) => void): (toPersist: T) => void {
// recover editor state from URL - we need an effect here because decompression is asynchronous
useEffect(() => {
@@ -32,7 +10,7 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi
if (compressedState.length === 0) {
// empty URL hash
console.log("no state to recover");
- setEditHistory(() => ({current: emptyState, history: [], future: []}));
+ // setEditHistory(() => ({current: emptyState, history: [], future: []}));
return;
}
let compressedBuffer;
@@ -41,7 +19,7 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi
} catch (e) {
// probably invalid base64
console.error("failed to recover state:", e);
- setEditHistory(() => ({current: emptyState, history: [], future: []}));
+ // setEditHistory(() => ({current: emptyState, history: [], future: []}));
return;
}
const ds = new DecompressionStream("deflate");
@@ -51,153 +29,28 @@ export function useUrlHashState(editorState: VisualEditorState | null, setEditHi
new Response(ds.readable).arrayBuffer()
.then(decompressedBuffer => {
const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer));
- // we support two formats
- if (recoveredState.nextID) {
- // old format
- setEditHistory(() => ({current: recoveredState, history: [], future: []}));
- }
- else {
- console.log(recoveredState);
- // new format
- if (recoveredState.editorState !== undefined) {
- setEditHistory(() => ({current: recoveredState.editorState, history: [], future: []}));
- }
- if (recoveredState.plantName !== undefined) {
- setPlantName(recoveredState.plantName);
- }
- if (recoveredState.autoScroll !== undefined) {
- setAutoScroll(recoveredState.autoScroll);
- }
- if (recoveredState.autoConnect !== undefined) {
- setAutoConnect(recoveredState.autoConnect);
- }
- if (recoveredState.plantConns !== undefined) {
- setPlantConns(recoveredState.plantConns);
- }
-
- if (recoveredState.showKeys !== undefined) {
- setShowKeys(recoveredState.showKeys);
- }
- if (recoveredState.zoom !== undefined) {
- setZoom(recoveredState.zoom);
- }
- if (recoveredState.insertMode !== undefined) {
- setInsertMode(recoveredState.insertMode);
- }
- if (recoveredState.showConnections !== undefined) {
- setShowConnections(recoveredState.showConnections);
- }
- if (recoveredState.showProperties !== undefined) {
- setShowProperties(recoveredState.showProperties);
- }
- if (recoveredState.showExecutionTrace !== undefined) {
- setShowExecutionTrace(recoveredState.showExecutionTrace);
- }
- if (recoveredState.showPlantTrace !== undefined) {
- setShowPlantTrace(recoveredState.showPlantTrace);
- }
- if (recoveredState.properties !== undefined) {
- setProperties(recoveredState.properties);
- }
- if (recoveredState.savedTraces !== undefined) {
- setSavedTraces(recoveredState.savedTraces);
- }
- if (recoveredState.activeProperty !== undefined) {
- setActiveProperty(recoveredState.activeProperty);
- }
-
- }
+ recoverCallback(recoveredState);
})
.catch(e => {
// any other error: invalid JSON, or decompression failed.
console.error("failed to recover state:", e);
- setEditHistory({current: emptyState, history: [], future: []});
+ // setEditHistory({current: emptyState, history: [], future: []});
});
}, []);
- // save editor state in URL
- useEffect(() => {
- const timeout = setTimeout(() => {
- if (editorState === null) {
- window.location.hash = "#";
- return;
- }
- const serializedState = JSON.stringify({
- autoConnect,
- autoScroll,
- plantConns,
- showKeys,
- zoom,
- insertMode,
- plantName,
- editorState,
- showConnections,
- showProperties,
- showExecutionTrace,
- showPlantTrace,
- properties,
- savedTraces,
- activeProperty,
- });
- const stateBuffer = new TextEncoder().encode(serializedState);
- const cs = new CompressionStream("deflate");
- const writer = cs.writable.getWriter();
- writer.write(stateBuffer);
- writer.close();
- // todo: cancel this promise handler when concurrently starting another compression job
- new Response(cs.readable).arrayBuffer().then(compressedStateBuffer => {
- const compressedStateString = new Uint8Array(compressedStateBuffer).toBase64();
- window.location.hash = "#"+compressedStateString;
- });
- }, 100);
- return () => clearTimeout(timeout);
- }, [
- editorState,
-
- autoConnect,
- autoScroll,
- plantConns,
- showKeys,
- zoom,
- insertMode,
- plantName,
- showConnections,
- showProperties,
- showExecutionTrace,
- showPlantTrace,
- properties,
- savedTraces,
- activeProperty,
- ]);
-
- return {
- autoConnect,
- setAutoConnect,
- autoScroll,
- setAutoScroll,
- plantConns,
- setPlantConns,
- showKeys,
- setShowKeys,
- zoom,
- setZoom,
- insertMode,
- setInsertMode,
- plantName,
- setPlantName,
- showConnections,
- setShowConnections,
- showProperties,
- setShowProperties,
- showExecutionTrace,
- setShowExecutionTrace,
- showPlantTrace,
- setShowPlantTrace,
- properties,
- setProperties,
- savedTraces,
- setSavedTraces,
- activeProperty,
- setActiveProperty,
+ function persist(state: T) {
+ const serializedState = JSON.stringify(state);
+ const stateBuffer = new TextEncoder().encode(serializedState);
+ const cs = new CompressionStream("deflate");
+ const writer = cs.writable.getWriter();
+ writer.write(stateBuffer);
+ writer.close();
+ // todo: cancel this promise handler when concurrently starting another compression job
+ new Response(cs.readable).arrayBuffer().then(compressedStateBuffer => {
+ const compressedStateString = new Uint8Array(compressedStateBuffer).toBase64();
+ window.location.hash = "#"+compressedStateString;
+ });
}
-}
\ No newline at end of file
+
+ return persist;
+}
diff --git a/src/statecharts/interpreter.ts b/src/statecharts/interpreter.ts
index f7db64c..deabf61 100644
--- a/src/statecharts/interpreter.ts
+++ b/src/statecharts/interpreter.ts
@@ -326,7 +326,6 @@ 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.
export function fairStep(simtime: number, event: RT_Event, statechart: Statechart, activeParent: StableState, {arenasFired, environment, ...config}: RT_Statechart & RaisedEvents): RT_Statechart & RaisedEvents {
- console.log('fair step', event, activeParent);
environment = environment.enterScope(activeParent.uid);
// console.log('fairStep', arenasFired);
for (const state of activeParent.children) {
@@ -347,7 +346,6 @@ export function fairStep(simtime: number, event: RT_Event, statechart: Statechar
}
export function handleInputEvent(simtime: number, event: RT_Event, statechart: Statechart, {mode, environment, history}: {mode: Mode, environment: Environment, history: RT_History}): BigStep {
- console.log('handleInputEvent', event);
let raised = initialRaised;
({mode, environment, ...raised} = fairStep(simtime, event, statechart, statechart.root, {mode, environment, history, arenasFired: [], ...raised}));
@@ -356,7 +354,6 @@ export function handleInputEvent(simtime: number, event: RT_Event, statechart: S
}
export function handleInternalEvents(simtime: number, statechart: Statechart, {internalEvents, ...rest}: RT_Statechart & RaisedEvents) {
- console.log('handleInternalEvents');
while (internalEvents.length > 0) {
const [nextEvent, ...remainingEvents] = internalEvents;
({internalEvents, ...rest} = fairStep(simtime,
From e0863c9443143ee90a60039b4bfd7398b9cb012c Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Sat, 8 Nov 2025 10:36:42 +0100
Subject: [PATCH 07/30] merge recovered app state with default app state (makes
a difference if the recovered app state is only partially defined)
---
src/App/App.tsx | 2 +-
src/App/SideBar.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/App/App.tsx b/src/App/App.tsx
index 88450c4..ea8e514 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -68,7 +68,7 @@ export function App() {
if (recoveredState.editorState !== undefined) {
const {editorState, ...appState} = recoveredState as AppState & {editorState: VisualEditorState};
setEditHistory(() => ({current: editorState, history: [], future: []}));
- setAppState(() => appState);
+ setAppState(defaultAppState => Object.assign({}, defaultAppState, appState));
}
}
},
diff --git a/src/App/SideBar.tsx b/src/App/SideBar.tsx
index 0fc79df..ee2714b 100644
--- a/src/App/SideBar.tsx
+++ b/src/App/SideBar.tsx
@@ -45,7 +45,7 @@ export const defaultSideBarState = {
showInputEvents: true,
showInternalEvents: true,
showOutputEvents: true,
- showPlant: false,
+ showPlant: true,
showConnections: false,
showProperties: false,
showExecutionTrace: true,
From a013fca768f0a079e7f07d2f7c8a09aa743f06ac Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Sat, 8 Nov 2025 10:41:41 +0100
Subject: [PATCH 08/30] update assignment (MTL property was wrong! i blame
chatgpt)
---
assignment.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/assignment.html b/assignment.html
index 827ce77..13a80a2 100644
--- a/assignment.html
+++ b/assignment.html
@@ -317,12 +317,12 @@ The controller can send the following events to the plant:
Starting point
-Use this link to the starting point for this assignment.
+Use this link to the starting point for this assignment.
Testing your solution
To test your solution, initialize the execution, and interact with the plant UI. The execution can run in (scaled) real-time, with the ability to pause/resume.
To gain more confidence that you correctly implemented the requirements, you can write Metric Temporal Logic (MTL) properties. An example of such a property is:
-
G ((topRightPressed & (X ~topRightPressed)) -> (G[0,2000] (lightOn)))
+ G ((topRightPressed -> lightOn) & ((topRightPressed & (X ~topRightPressed)) -> (G[0,2000] (lightOn))))
meaning: "as long as the top-right button is pressed, the light should be on, and after the top-right button is released, the light should remain on for 2 seconds" AKA Requirement 1.
From 5674416623786e24f0d07263e0472577428cc403 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Sat, 8 Nov 2025 10:49:51 +0100
Subject: [PATCH 09/30] assignment: get rid of the water levels
---
assignment.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assignment.html b/assignment.html
index 13a80a2..1b41ba9 100644
--- a/assignment.html
+++ b/assignment.html
@@ -344,7 +344,7 @@ meaning: "as long as the top-right button is pressed, the light should be on, an
What is expected
-Your solution needs to be precisely correct: superficially correct behavior when running the generated code with the GUI (e.g., seeing the water levels change) is not enough : the timing-related requirements are exact .
+Your solution needs to be precisely correct: superficially correct behavior when interacting with the Plant UI is not enough : the timing-related requirements are exact .
The assignment has been designed specifically to encourage use of as many Statechart features as possible:
composite states
From 07b51dd2f2a1a5017128ea941c579defb4ef8687 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Sat, 8 Nov 2025 10:50:41 +0100
Subject: [PATCH 10/30] move file
---
assignment.html => teaching/mosis25/assignment.html | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename assignment.html => teaching/mosis25/assignment.html (100%)
diff --git a/assignment.html b/teaching/mosis25/assignment.html
similarity index 100%
rename from assignment.html
rename to teaching/mosis25/assignment.html
From 1f72542234056a17af81bce0a51a64bb32ea8f49 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Wed, 12 Nov 2025 14:34:46 +0100
Subject: [PATCH 11/30] cleanup code a bit more
---
src/App/App.tsx | 116 +++++++++---------
src/App/{ => BottomPanel}/BottomPanel.css | 0
src/App/{ => BottomPanel}/BottomPanel.tsx | 6 +-
src/App/Modals/ModalOverlay.tsx | 17 +++
src/App/PersistentDetails.tsx | 2 +-
src/App/Plant/DigitalWatch/DigitalWatch.tsx | 2 +-
src/App/Plant/Microwave/Microwave.tsx | 2 +-
src/App/Plant/Plant.ts | 1 -
src/App/Plant/TrafficLight/TrafficLight.tsx | 2 +-
src/App/{ => SideBar}/RTHistory.tsx | 10 +-
src/App/{ => SideBar}/ShowAST.tsx | 14 +--
src/App/{AST.css => SideBar/SideBar.css} | 0
src/App/{ => SideBar}/SideBar.tsx | 11 +-
src/App/{ => SideBar}/check_property.ts | 4 +-
src/App/TopPanel/TopPanel.tsx | 4 +-
src/App/VisualEditor/VisualEditor.tsx | 9 +-
.../VisualEditor/{ => hooks}/useCopyPaste.ts | 2 +-
src/App/VisualEditor/{ => hooks}/useMouse.tsx | 8 +-
src/App/{ => hooks}/useEditor.ts | 5 +-
src/App/{ => hooks}/useSimulator.ts | 5 +-
src/App/makePartialSetter.ts | 24 ++--
src/{App => hooks}/useAudioContext.ts | 0
src/hooks/useDetectChange.ts | 8 ++
.../usePersistentState.ts} | 0
src/{App => hooks}/useUrlHashState.ts | 16 +--
25 files changed, 146 insertions(+), 122 deletions(-)
rename src/App/{ => BottomPanel}/BottomPanel.css (100%)
rename src/App/{ => BottomPanel}/BottomPanel.tsx (84%)
create mode 100644 src/App/Modals/ModalOverlay.tsx
rename src/App/{ => SideBar}/RTHistory.tsx (94%)
rename src/App/{ => SideBar}/ShowAST.tsx (92%)
rename src/App/{AST.css => SideBar/SideBar.css} (100%)
rename src/App/{ => SideBar}/SideBar.tsx (98%)
rename src/App/{ => SideBar}/check_property.ts (96%)
rename src/App/VisualEditor/{ => hooks}/useCopyPaste.ts (98%)
rename src/App/VisualEditor/{ => hooks}/useMouse.tsx (98%)
rename src/App/{ => hooks}/useEditor.ts (97%)
rename src/App/{ => hooks}/useSimulator.ts (98%)
rename src/{App => hooks}/useAudioContext.ts (100%)
create mode 100644 src/hooks/useDetectChange.ts
rename src/{App/persistent_state.ts => hooks/usePersistentState.ts} (100%)
rename src/{App => hooks}/useUrlHashState.ts (83%)
diff --git a/src/App/App.tsx b/src/App/App.tsx
index ea8e514..9b77e2e 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -5,16 +5,18 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from
import { detectConnections } from "@/statecharts/detect_connections";
import { parseStatechart } from "../statecharts/parser";
-import { BottomPanel } from "./BottomPanel";
-import { defaultSideBarState, SideBar, SideBarState } from "./SideBar";
+import { BottomPanel } from "./BottomPanel/BottomPanel";
+import { defaultSideBarState, SideBar, SideBarState } from "./SideBar/SideBar";
import { InsertMode } from "./TopPanel/InsertModes";
import { TopPanel } from "./TopPanel/TopPanel";
import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
-import { makeIndividualSetters } from "./makePartialSetter";
-import { useEditor } from "./useEditor";
-import { useSimulator } from "./useSimulator";
-import { useUrlHashState } from "./useUrlHashState";
+import { makeAllSetters } from "./makePartialSetter";
+import { useEditor } from "./hooks/useEditor";
+import { useSimulator } from "./hooks/useSimulator";
+import { useUrlHashState } from "../hooks/useUrlHashState";
import { plants } from "./plants";
+import { emptyState } from "@/statecharts/concrete_syntax";
+import { ModalOverlay } from "./Modals/ModalOverlay";
export type EditHistory = {
current: VisualEditorState,
@@ -56,9 +58,12 @@ export function App() {
const persist = useUrlHashState(
recoveredState => {
+ if (recoveredState === null) {
+ setEditHistory(() => ({current: emptyState, history: [], future: []}));
+ }
// we support two formats
// @ts-ignore
- if (recoveredState.nextID) {
+ else if (recoveredState.nextID) {
// old format
setEditHistory(() => ({current: recoveredState as VisualEditorState, history: [], future: []}));
}
@@ -77,6 +82,7 @@ export function App() {
useEffect(() => {
const timeout = setTimeout(() => {
if (editorState !== null) {
+ console.log('persisting state to url');
persist({editorState, ...appState});
}
}, 100);
@@ -104,7 +110,15 @@ export function App() {
const simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar);
- const setters = makeIndividualSetters(setAppState, Object.keys(appState) as (keyof AppState)[]);
+ // console.log('render app', {ast, plant, appState});
+ // useDetectChange(ast, 'ast');
+ // useDetectChange(plant, 'plant');
+ // useDetectChange(scrollDownSidebar, 'scrollDownSidebar');
+ // useDetectChange(appState, 'appState');
+ // useDetectChange(simulator.time, 'simulator.time');
+ // useDetectChange(simulator.trace, 'simulator.trace');
+
+ const setters = makeAllSetters(setAppState, Object.keys(appState) as (keyof AppState)[]);
const syntaxErrors = parsed && parsed[1] || [];
const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx];
@@ -121,63 +135,51 @@ export function App() {
const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1];
- return <>
+ return
+ {/* top-to-bottom: everything -> bottom panel */}
+
- {/* Modal dialog */}
- {modal &&
setModal(null)}>
-
- e.stopPropagation()}>
- {modal}
-
-
-
}
+ {/* left-to-right: main -> sidebar */}
+
- {/* top-to-bottom: everything -> bottom panel */}
-
-
- {/* left-to-right: main -> sidebar */}
-
-
- {/* top-to-bottom: top bar, editor */}
-
- {/* Top bar */}
-
- {editHistory &&
}
+ {/* top-to-bottom: top bar, editor */}
+
+ {/* Top bar */}
+
+ {editHistory && }
+
+ {/* Editor */}
+
+ {editorState && conns && syntaxErrors &&
+ }
+
- {/* Editor */}
-
- {editorState && conns && syntaxErrors &&
-
}
+
+ {/* Right: sidebar */}
+
- {/* Right: sidebar */}
-
-
-
-
+ {/* Bottom panel */}
+
+ {syntaxErrors && }
-
- {/* Bottom panel */}
-
- {syntaxErrors && }
-
-
- >;
+ ;
}
export default App;
diff --git a/src/App/BottomPanel.css b/src/App/BottomPanel/BottomPanel.css
similarity index 100%
rename from src/App/BottomPanel.css
rename to src/App/BottomPanel/BottomPanel.css
diff --git a/src/App/BottomPanel.tsx b/src/App/BottomPanel/BottomPanel.tsx
similarity index 84%
rename from src/App/BottomPanel.tsx
rename to src/App/BottomPanel/BottomPanel.tsx
index 2f84992..c0d02ae 100644
--- a/src/App/BottomPanel.tsx
+++ b/src/App/BottomPanel/BottomPanel.tsx
@@ -1,10 +1,10 @@
import { useEffect, useState } from "react";
-import { TraceableError } from "../statecharts/parser";
+import { TraceableError } from "../../statecharts/parser";
import "./BottomPanel.css";
-import logo from "../../artwork/logo-playful.svg";
-import { PersistentDetailsLocalStorage } from "./PersistentDetails";
+import logo from "../../../artwork/logo-playful.svg";
+import { PersistentDetailsLocalStorage } from "../PersistentDetails";
export function BottomPanel(props: {errors: TraceableError[]}) {
const [greeting, setGreeting] = useState(
diff --git a/src/App/Modals/ModalOverlay.tsx b/src/App/Modals/ModalOverlay.tsx
new file mode 100644
index 0000000..965879d
--- /dev/null
+++ b/src/App/Modals/ModalOverlay.tsx
@@ -0,0 +1,17 @@
+import { Dispatch, PropsWithChildren, ReactElement, SetStateAction } from "react";
+
+export function ModalOverlay(props: PropsWithChildren<{modal: ReactElement|null, setModal: Dispatch
>}>) {
+ return <>
+ {props.modal && props.setModal(null)}>
+
+ e.stopPropagation()}>
+ {props.modal}
+
+
+
}
+
+ {props.children}
+ >;
+}
diff --git a/src/App/PersistentDetails.tsx b/src/App/PersistentDetails.tsx
index 4d24bb0..c2f553b 100644
--- a/src/App/PersistentDetails.tsx
+++ b/src/App/PersistentDetails.tsx
@@ -1,4 +1,4 @@
-import { usePersistentState } from "@/App/persistent_state"
+import { usePersistentState } from "@/hooks/usePersistentState"
import { DetailsHTMLAttributes, Dispatch, PropsWithChildren, SetStateAction } from "react";
type Props = {
diff --git a/src/App/Plant/DigitalWatch/DigitalWatch.tsx b/src/App/Plant/DigitalWatch/DigitalWatch.tsx
index 9b41f94..3cb5f55 100644
--- a/src/App/Plant/DigitalWatch/DigitalWatch.tsx
+++ b/src/App/Plant/DigitalWatch/DigitalWatch.tsx
@@ -1,4 +1,4 @@
-import { useAudioContext } from "@/App/useAudioContext";
+import { useAudioContext } from "@/hooks/useAudioContext";
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
import { detectConnections } from "@/statecharts/detect_connections";
import { parseStatechart } from "@/statecharts/parser";
diff --git a/src/App/Plant/Microwave/Microwave.tsx b/src/App/Plant/Microwave/Microwave.tsx
index 516b444..bc50993 100644
--- a/src/App/Plant/Microwave/Microwave.tsx
+++ b/src/App/Plant/Microwave/Microwave.tsx
@@ -12,7 +12,7 @@ import { RT_Statechart } from "@/statecharts/runtime_types";
import { memo, useEffect } from "react";
import "./Microwave.css";
-import { useAudioContext } from "../../useAudioContext";
+import { useAudioContext } from "../../../hooks/useAudioContext";
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
import { detectConnections } from "@/statecharts/detect_connections";
import { parseStatechart } from "@/statecharts/parser";
diff --git a/src/App/Plant/Plant.ts b/src/App/Plant/Plant.ts
index 444c848..afa26ef 100644
--- a/src/App/Plant/Plant.ts
+++ b/src/App/Plant/Plant.ts
@@ -3,7 +3,6 @@ import { Statechart } from "@/statecharts/abstract_syntax";
import { EventTrigger } from "@/statecharts/label_ast";
import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types";
import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive";
-import { setsEqual } from "@/util/util";
export type PlantRenderProps = {
state: StateType,
diff --git a/src/App/Plant/TrafficLight/TrafficLight.tsx b/src/App/Plant/TrafficLight/TrafficLight.tsx
index fb47f48..5c5c833 100644
--- a/src/App/Plant/TrafficLight/TrafficLight.tsx
+++ b/src/App/Plant/TrafficLight/TrafficLight.tsx
@@ -13,7 +13,7 @@ import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
import { detectConnections } from "@/statecharts/detect_connections";
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
import { RT_Statechart } from "@/statecharts/runtime_types";
-import { useAudioContext } from "@/App/useAudioContext";
+import { useAudioContext } from "@/hooks/useAudioContext";
import { memo, useEffect } from "react";
import { objectsEqual } from "@/util/util";
diff --git a/src/App/RTHistory.tsx b/src/App/SideBar/RTHistory.tsx
similarity index 94%
rename from src/App/RTHistory.tsx
rename to src/App/SideBar/RTHistory.tsx
index 938867d..7e69844 100644
--- a/src/App/RTHistory.tsx
+++ b/src/App/SideBar/RTHistory.tsx
@@ -1,10 +1,10 @@
import { Dispatch, memo, SetStateAction, useCallback } from "react";
-import { Statechart, stateDescription } from "../statecharts/abstract_syntax";
-import { Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types";
-import { formatTime } from "../util/util";
-import { TimeMode, timeTravel } from "../statecharts/time";
-import { BigStepCause, TraceItem, TraceState } from "./App";
+import { Statechart, stateDescription } from "../../statecharts/abstract_syntax";
+import { Mode, RaisedEvent, RT_Event } from "../../statecharts/runtime_types";
+import { formatTime } from "../../util/util";
+import { TimeMode, timeTravel } from "../../statecharts/time";
import { Environment } from "@/statecharts/environment";
+import { BigStepCause, TraceItem, TraceState } from "../hooks/useSimulator";
type RTHistoryProps = {
trace: TraceState|null,
diff --git a/src/App/ShowAST.tsx b/src/App/SideBar/ShowAST.tsx
similarity index 92%
rename from src/App/ShowAST.tsx
rename to src/App/SideBar/ShowAST.tsx
index 8657331..6c240ad 100644
--- a/src/App/ShowAST.tsx
+++ b/src/App/SideBar/ShowAST.tsx
@@ -1,7 +1,9 @@
-import { ConcreteState, UnstableState, stateDescription, Transition } from "../statecharts/abstract_syntax";
-import { Action, EventTrigger, Expression } from "../statecharts/label_ast";
-
-import "./AST.css";
+import BoltIcon from '@mui/icons-material/Bolt';
+import { memo, useEffect } from "react";
+import { usePersistentState } from "../../hooks/usePersistentState";
+import { ConcreteState, stateDescription, Transition, UnstableState } from "../../statecharts/abstract_syntax";
+import { Action, EventTrigger, Expression } from "../../statecharts/label_ast";
+import { KeyInfoHidden, KeyInfoVisible } from "../TopPanel/KeyInfo";
export function ShowTransition(props: {transition: Transition}) {
return <>➝ {stateDescription(props.transition.tgt)}>;
@@ -46,10 +48,6 @@ export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | Unst
;
});
-import BoltIcon from '@mui/icons-material/Bolt';
-import { KeyInfoHidden, KeyInfoVisible } from "./TopPanel/KeyInfo";
-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}) {
const raiseHandlers = inputEvents.map(({event}) => {
diff --git a/src/App/AST.css b/src/App/SideBar/SideBar.css
similarity index 100%
rename from src/App/AST.css
rename to src/App/SideBar/SideBar.css
diff --git a/src/App/SideBar.tsx b/src/App/SideBar/SideBar.tsx
similarity index 98%
rename from src/App/SideBar.tsx
rename to src/App/SideBar/SideBar.tsx
index ee2714b..27d42af 100644
--- a/src/App/SideBar.tsx
+++ b/src/App/SideBar/SideBar.tsx
@@ -8,14 +8,15 @@ import { Conns } from '@/statecharts/timed_reactive';
import { Dispatch, Ref, SetStateAction, useEffect, useRef, useState } from 'react';
import { Statechart } from '@/statecharts/abstract_syntax';
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from './ShowAST';
-import { Plant } from './Plant/Plant';
+import { Plant } from '../Plant/Plant';
import { checkProperty, PropertyCheckResult } from './check_property';
-import { Setters } from './makePartialSetter';
+import { Setters } from '../makePartialSetter';
import { RTHistory } from './RTHistory';
-import { BigStepCause, TraceState } from './useSimulator';
-import { plants, UniversalPlantState } from './plants';
+import { BigStepCause, TraceState } from '../hooks/useSimulator';
+import { plants, UniversalPlantState } from '../plants';
import { TimeMode } from '@/statecharts/time';
-import { PersistentDetails } from './PersistentDetails';
+import { PersistentDetails } from '../PersistentDetails';
+import "./SideBar.css";
type SavedTraces = [string, BigStepCause[]][];
diff --git a/src/App/check_property.ts b/src/App/SideBar/check_property.ts
similarity index 96%
rename from src/App/check_property.ts
rename to src/App/SideBar/check_property.ts
index 110112b..24a41cb 100644
--- a/src/App/check_property.ts
+++ b/src/App/SideBar/check_property.ts
@@ -1,6 +1,6 @@
import { RT_Statechart } from "@/statecharts/runtime_types";
-import { Plant } from "./Plant/Plant";
-import { TraceItem } from "./useSimulator";
+import { Plant } from "../Plant/Plant";
+import { TraceItem } from "../hooks/useSimulator";
// const endpoint = "http://localhost:15478/check_property";
const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx
index ed59b0a..a8272b2 100644
--- a/src/App/TopPanel/TopPanel.tsx
+++ b/src/App/TopPanel/TopPanel.tsx
@@ -18,10 +18,10 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import SkipNextIcon from '@mui/icons-material/SkipNext';
import StopIcon from '@mui/icons-material/Stop';
import { InsertModes } from "./InsertModes";
-import { usePersistentState } from "@/App/persistent_state";
+import { usePersistentState } from "@/hooks/usePersistentState";
import { RotateButtons } from "./RotateButtons";
import { SpeedControl } from "./SpeedControl";
-import { TraceState } from "../useSimulator";
+import { TraceState } from "../hooks/useSimulator";
export type TopPanelProps = {
trace: TraceState | null,
diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx
index 9741520..8f2a191 100644
--- a/src/App/VisualEditor/VisualEditor.tsx
+++ b/src/App/VisualEditor/VisualEditor.tsx
@@ -1,21 +1,20 @@
-import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
+import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef } from "react";
-import { InsertMode } from "../TopPanel/InsertModes";
import { Mode } from "@/statecharts/runtime_types";
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
import { Arrow, ArrowPart, Diamond, History, RectSide, Rountangle, Text } from "../../statecharts/concrete_syntax";
import { Connections } from "../../statecharts/detect_connections";
import { TraceableError } from "../../statecharts/parser";
import { ArcDirection, arcDirection } from "../../util/geometry";
+import { InsertMode } from "../TopPanel/InsertModes";
import { ArrowSVG } from "./ArrowSVG";
import { DiamondSVG } from "./DiamondSVG";
import { HistorySVG } from "./HistorySVG";
import { RountangleSVG } from "./RountangleSVG";
import { TextSVG } from "./TextSVG";
-import { useCopyPaste } from "./useCopyPaste";
-
import "./VisualEditor.css";
-import { useMouse } from "./useMouse";
+import { useCopyPaste } from "./hooks/useCopyPaste";
+import { useMouse } from "./hooks/useMouse";
export type ConcreteSyntax = {
rountangles: Rountangle[];
diff --git a/src/App/VisualEditor/useCopyPaste.ts b/src/App/VisualEditor/hooks/useCopyPaste.ts
similarity index 98%
rename from src/App/VisualEditor/useCopyPaste.ts
rename to src/App/VisualEditor/hooks/useCopyPaste.ts
index e560cc0..21e8f9b 100644
--- a/src/App/VisualEditor/useCopyPaste.ts
+++ b/src/App/VisualEditor/hooks/useCopyPaste.ts
@@ -1,6 +1,6 @@
import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concrete_syntax";
import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react";
-import { Selection, VisualEditorState } from "./VisualEditor";
+import { Selection, VisualEditorState } from "../VisualEditor";
import { addV2D } from "@/util/geometry";
// const offset = {x: 40, y: 40};
diff --git a/src/App/VisualEditor/useMouse.tsx b/src/App/VisualEditor/hooks/useMouse.tsx
similarity index 98%
rename from src/App/VisualEditor/useMouse.tsx
rename to src/App/VisualEditor/hooks/useMouse.tsx
index 9ef94e5..1e917f7 100644
--- a/src/App/VisualEditor/useMouse.tsx
+++ b/src/App/VisualEditor/hooks/useMouse.tsx
@@ -2,10 +2,10 @@ import { rountangleMinSize } from "@/statecharts/concrete_syntax";
import { addV2D, area, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "@/util/geometry";
import { getBBoxInSvgCoords } from "@/util/svg_helper";
import { Dispatch, useCallback, useEffect, useState } from "react";
-import { MIN_ROUNTANGLE_SIZE } from "../parameters";
-import { InsertMode } from "../TopPanel/InsertModes";
-import { Selecting, SelectingState } from "./Selection";
-import { Selection, VisualEditorState } from "./VisualEditor";
+import { MIN_ROUNTANGLE_SIZE } from "../../parameters";
+import { InsertMode } from "../../TopPanel/InsertModes";
+import { Selecting, SelectingState } from "../Selection";
+import { Selection, VisualEditorState } from "../VisualEditor";
export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoom: number, refSVG: {current: SVGSVGElement|null}, state: VisualEditorState, setState: Dispatch<(v: VisualEditorState) => VisualEditorState>, deleteSelection: () => void) {
const [dragging, setDragging] = useState(false);
diff --git a/src/App/useEditor.ts b/src/App/hooks/useEditor.ts
similarity index 97%
rename from src/App/useEditor.ts
rename to src/App/hooks/useEditor.ts
index e1ca293..058a5d5 100644
--- a/src/App/useEditor.ts
+++ b/src/App/hooks/useEditor.ts
@@ -1,8 +1,7 @@
import { addV2D, rotateLine90CCW, rotateLine90CW, rotatePoint90CCW, rotatePoint90CW, rotateRect90CCW, rotateRect90CW, scaleV2D, subtractV2D, Vec2D } from "@/util/geometry";
-import { HISTORY_RADIUS } from "./parameters";
+import { HISTORY_RADIUS } from "../parameters";
import { Dispatch, SetStateAction, useCallback, useEffect } from "react";
-import { EditHistory } from "./App";
-import { VisualEditorState } from "./VisualEditor/VisualEditor";
+import { EditHistory } from "../App";
export function useEditor(setEditHistory: Dispatch>) {
useEffect(() => {
diff --git a/src/App/useSimulator.ts b/src/App/hooks/useSimulator.ts
similarity index 98%
rename from src/App/useSimulator.ts
rename to src/App/hooks/useSimulator.ts
index 9844455..fa07dd0 100644
--- a/src/App/useSimulator.ts
+++ b/src/App/hooks/useSimulator.ts
@@ -3,9 +3,9 @@ import { RuntimeError } from "@/statecharts/interpreter";
import { BigStep, RaisedEvent } from "@/statecharts/runtime_types";
import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive";
import { useCallback, useEffect, useMemo, useState } from "react";
-import { Plant } from "./Plant/Plant";
+import { Plant } from "../Plant/Plant";
import { getSimTime, getWallClkDelay, TimeMode } from "@/statecharts/time";
-import { UniversalPlantState } from "./plants";
+import { UniversalPlantState } from "../plants";
type CoupledState = {
sc: BigStep,
@@ -107,6 +107,7 @@ export function useSimulator(ast: Statechart|null, plant: Plant {
+ // console.log('time effect:', time, currentTraceItem);
let timeout: NodeJS.Timeout | undefined;
if (currentTraceItem !== null && cE !== null) {
if (currentTraceItem.kind === "bigstep") {
diff --git a/src/App/makePartialSetter.ts b/src/App/makePartialSetter.ts
index a9ef4a7..d3d5109 100644
--- a/src/App/makePartialSetter.ts
+++ b/src/App/makePartialSetter.ts
@@ -3,16 +3,16 @@ import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
export function makePartialSetter(fullSetter: Dispatch>, key: K): Dispatch> {
return (newValueOrCallback: T[K] | ((newValue: T[K]) => T[K])) => {
fullSetter(oldFullValue => {
- if (typeof newValueOrCallback === 'function') {
+ const newValue = (typeof newValueOrCallback === 'function') ? (newValueOrCallback as (newValue: T[K]) => T[K])(oldFullValue[key] as T[K]) : newValueOrCallback as T[K];
+ if (newValue === oldFullValue[key]) {
+ return oldFullValue;
+ }
+ else {
return {
...oldFullValue,
- [key]: (newValueOrCallback as (newValue: T[K]) => T[K])(oldFullValue[key] as T[K]),
+ [key]: newValue,
}
}
- return {
- ...oldFullValue,
- [key]: newValueOrCallback as T[K],
- }
})
};
}
@@ -21,16 +21,16 @@ export type Setters = {
[K in keyof T as `set${Capitalize>}`]: Dispatch>;
}
-export function makeIndividualSetters(
+export function makeAllSetters(
fullSetter: Dispatch>,
keys: (keyof T)[],
): Setters {
// @ts-ignore
- return useMemo(() =>
+ return useMemo(() => {
+ console.log('creating setters for App');
// @ts-ignore
- Object.fromEntries(keys.map((key: string) => {
+ return Object.fromEntries(keys.map((key: string) => {
return [`set${key.charAt(0).toUpperCase()}${key.slice(1)}`, makePartialSetter(fullSetter, key)];
- })),
- [fullSetter]
- );
+ }));
+ }, [fullSetter]);
}
diff --git a/src/App/useAudioContext.ts b/src/hooks/useAudioContext.ts
similarity index 100%
rename from src/App/useAudioContext.ts
rename to src/hooks/useAudioContext.ts
diff --git a/src/hooks/useDetectChange.ts b/src/hooks/useDetectChange.ts
new file mode 100644
index 0000000..3ef07c4
--- /dev/null
+++ b/src/hooks/useDetectChange.ts
@@ -0,0 +1,8 @@
+import { useEffect } from "react";
+
+// useful for debugging
+export function useDetectChange(expr: any, name: string) {
+ useEffect(() => {
+ console.log(name, 'changed to:', expr);
+ }, [expr]);
+}
diff --git a/src/App/persistent_state.ts b/src/hooks/usePersistentState.ts
similarity index 100%
rename from src/App/persistent_state.ts
rename to src/hooks/usePersistentState.ts
diff --git a/src/App/useUrlHashState.ts b/src/hooks/useUrlHashState.ts
similarity index 83%
rename from src/App/useUrlHashState.ts
rename to src/hooks/useUrlHashState.ts
index 3859d54..d442552 100644
--- a/src/App/useUrlHashState.ts
+++ b/src/hooks/useUrlHashState.ts
@@ -1,17 +1,17 @@
-import { useEffect } from "react";
+import { useEffect, useLayoutEffect } from "react";
// persist state in URL hash
-export function useUrlHashState(recoverCallback: (recoveredState: T) => void): (toPersist: T) => void {
+export function useUrlHashState(recoverCallback: (recoveredState: (T|null)) => void): (toPersist: T) => void {
// recover editor state from URL - we need an effect here because decompression is asynchronous
- useEffect(() => {
+ // layout effect because we want to run it before rendering the first frame
+ useLayoutEffect(() => {
console.log('recovering state...');
const compressedState = window.location.hash.slice(1);
if (compressedState.length === 0) {
// empty URL hash
console.log("no state to recover");
- // setEditHistory(() => ({current: emptyState, history: [], future: []}));
- return;
+ return recoverCallback(null);
}
let compressedBuffer;
try {
@@ -19,8 +19,7 @@ export function useUrlHashState(recoverCallback: (recoveredState: T) => void)
} catch (e) {
// probably invalid base64
console.error("failed to recover state:", e);
- // setEditHistory(() => ({current: emptyState, history: [], future: []}));
- return;
+ return recoverCallback(null);
}
const ds = new DecompressionStream("deflate");
const writer = ds.writable.getWriter();
@@ -29,12 +28,13 @@ export function useUrlHashState(recoverCallback: (recoveredState: T) => void)
new Response(ds.readable).arrayBuffer()
.then(decompressedBuffer => {
const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer));
+ console.log('successfully recovered state');
recoverCallback(recoveredState);
})
.catch(e => {
// any other error: invalid JSON, or decompression failed.
console.error("failed to recover state:", e);
- // setEditHistory({current: emptyState, history: [], future: []});
+ recoverCallback(null);
});
}, []);
From 848a13e875680efcea2fb66e96e321888851abb6 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 10:59:34 +0100
Subject: [PATCH 12/30] update assignment: alternate and/or states as in the
theory
---
teaching/mosis25/assignment.html | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/teaching/mosis25/assignment.html b/teaching/mosis25/assignment.html
index 1b41ba9..8f623c4 100644
--- a/teaching/mosis25/assignment.html
+++ b/teaching/mosis25/assignment.html
@@ -363,8 +363,14 @@ meaning: "as long as the top-right button is pressed, the light should be on, an
Additional resources
From 9646d716c696a3805c4609d7ea8df198331ecc5a Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 13:05:22 +0100
Subject: [PATCH 13/30] editor: shift or ctrl key to append selection
---
src/App/VisualEditor/VisualEditor.tsx | 28 ++--
src/App/VisualEditor/hooks/useMouse.tsx | 179 ++++++++++++++----------
2 files changed, 118 insertions(+), 89 deletions(-)
diff --git a/src/App/VisualEditor/VisualEditor.tsx b/src/App/VisualEditor/VisualEditor.tsx
index 8f2a191..850108e 100644
--- a/src/App/VisualEditor/VisualEditor.tsx
+++ b/src/App/VisualEditor/VisualEditor.tsx
@@ -30,21 +30,19 @@ export type VisualEditorState = ConcreteSyntax & {
};
export type RountangleSelectable = {
- // kind: "rountangle";
- parts: RectSide[];
+ part: RectSide;
uid: string;
}
type ArrowSelectable = {
- // kind: "arrow";
- parts: ArrowPart[];
+ part: ArrowPart;
uid: string;
}
type TextSelectable = {
- parts: ["text"];
+ part: "text";
uid: string;
}
type HistorySelectable = {
- parts: ["history"];
+ part: "history";
uid: string;
}
type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable | HistorySelectable;
@@ -113,12 +111,10 @@ export const VisualEditor = memo(function VisualEditor({state, setState, conns,
for (const textUid of texts) {
textsToHighlight[textUid] = true;
}
- for (const part of selected.parts) {
- const arrows = conns.side2ArrowMap.get(selected.uid + '/' + part) || [];
- if (arrows) {
- for (const [arrowPart, arrowUid] of arrows) {
- arrowsToHighlight[arrowUid] = true;
- }
+ const arrows = conns.side2ArrowMap.get(selected.uid + '/' + selected.part) || [];
+ if (arrows) {
+ for (const [arrowPart, arrowUid] of arrows) {
+ arrowsToHighlight[arrowUid] = true;
}
}
const arrow2 = conns.text2ArrowMap.get(selected.uid);
@@ -229,7 +225,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, conns,
return a.uid === arrow.uid)?.parts as ArrowPart[] || []}
+ selected={selection.filter(a => a.uid === arrow.uid).map(({part})=> part as ArrowPart)}
error={errors
.filter(({shapeUid}) => shapeUid === arrow.uid)
.map(({message}) => message).join(', ')}
@@ -252,7 +248,7 @@ const Rountangles = memo(function Rountangles({rountangles, selection, sidesToHi
return r.uid === rountangle.uid)?.parts as RectSide[] || []}
+ selected={selection.filter(r => r.uid === rountangle.uid).map(({part}) => part as RectSide)}
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RectSide[]]}
error={errors
.filter(({shapeUid}) => shapeUid === rountangle.uid)
@@ -273,7 +269,7 @@ const Diamonds = memo(function Diamonds({diamonds, selection, sidesToHighlight,
r.uid === diamond.uid)?.parts as RectSide[] || []}
+ selected={selection.filter(r => r.uid === diamond.uid).map(({part})=>part as RectSide)}
highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RectSide[]]}
error={errors
.filter(({shapeUid}) => shapeUid === diamond.uid)
@@ -294,7 +290,7 @@ const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, o
key={txt.uid}
error={errors.find(({shapeUid}) => txt.uid === shapeUid)}
text={txt}
- selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)}
+ selected={Boolean(selection.filter(s => s.uid === txt.uid).length)}
highlight={textsToHighlight.hasOwnProperty(txt.uid)}
onEdit={onEditText}
setModal={setModal}
diff --git a/src/App/VisualEditor/hooks/useMouse.tsx b/src/App/VisualEditor/hooks/useMouse.tsx
index 1e917f7..4c1168f 100644
--- a/src/App/VisualEditor/hooks/useMouse.tsx
+++ b/src/App/VisualEditor/hooks/useMouse.tsx
@@ -9,6 +9,9 @@ import { Selection, VisualEditorState } from "../VisualEditor";
export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoom: number, refSVG: {current: SVGSVGElement|null}, state: VisualEditorState, setState: Dispatch<(v: VisualEditorState) => VisualEditorState>, deleteSelection: () => void) {
const [dragging, setDragging] = useState(false);
+ const [shiftOrCtrlPressed, setShiftOrCtrlPressed] = useState(false);
+
+ console.log(shiftOrCtrlPressed);
// not null while the user is making a selection
const [selectingState, setSelectingState] = useState(null);
@@ -29,7 +32,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
const currentPointer = getCurrentPointer(e);
if (e.button === 2) {
makeCheckPoint();
- // ignore selection, middle mouse button always inserts
+ // ignore selection, right mouse button always inserts
setState(state => {
const newID = state.nextID.toString();
if (insertMode === "and" || insertMode === "or") {
@@ -43,7 +46,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
kind: insertMode,
}],
nextID: state.nextID+1,
- selection: [{uid: newID, parts: ["bottom", "right"]}],
+ selection: [{uid: newID, part: "bottom"}, {uid: newID, part: "right"}],
};
}
else if (insertMode === "pseudo") {
@@ -55,7 +58,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
size: MIN_ROUNTANGLE_SIZE,
}],
nextID: state.nextID+1,
- selection: [{uid: newID, parts: ["bottom", "right"]}],
+ selection: [{uid: newID, part: "bottom"}, {uid: newID, part: "right"}],
};
}
else if (insertMode === "shallow" || insertMode === "deep") {
@@ -67,7 +70,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
topLeft: currentPointer,
}],
nextID: state.nextID+1,
- selection: [{uid: newID, parts: ["history"]}],
+ selection: [{uid: newID, part: "history"}],
}
}
else if (insertMode === "transition") {
@@ -79,7 +82,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
end: currentPointer,
}],
nextID: state.nextID+1,
- selection: [{uid: newID, parts: ["end"]}],
+ selection: [{uid: newID, part: "end"}],
}
}
else if (insertMode === "text") {
@@ -91,7 +94,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
topLeft: currentPointer,
}],
nextID: state.nextID+1,
- selection: [{uid: newID, parts: ["text"]}],
+ selection: [{uid: newID, part: "text"}],
}
}
throw new Error("unreachable, mode=" + insertMode); // shut up typescript
@@ -101,38 +104,40 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
}
if (e.button === 0) {
- // left mouse button on a shape will drag that shape (and everything else that's selected). if the shape under the pointer was not in the selection then the selection is reset to contain only that shape.
- const uid = e.target?.dataset.uid;
- const parts: string[] = e.target?.dataset.parts?.split(' ').filter((p:string) => p!=="") || [];
- if (uid && parts.length > 0) {
- makeCheckPoint();
+ if (!shiftOrCtrlPressed) {
+ // left mouse button on a shape will drag that shape (and everything else that's selected). if the shape under the pointer was not in the selection then the selection is reset to contain only that shape.
+ const uid = e.target?.dataset.uid;
+ const parts: string[] = e.target?.dataset.parts?.split(' ').filter((p:string) => p!=="") || [];
+ if (uid && parts.length > 0) {
+ makeCheckPoint();
- // if the mouse button is pressed outside of the current selection, we reset the selection to whatever shape the mouse is on
- let allPartsInSelection = true;
- for (const part of parts) {
- if (!(selection.find(s => s.uid === uid)?.parts || [] as string[]).includes(part)) {
- allPartsInSelection = false;
- break;
+ // if the mouse button is pressed outside of the current selection, we reset the selection to whatever shape the mouse is on
+ let allPartsInSelection = true;
+ for (const part of parts) {
+ // is there anything in our existing selection that is not under the cursor?
+ if (!(selection.some(s => (s.uid === uid) && (s.part === part)))) {
+ allPartsInSelection = false;
+ break;
+ }
}
+ if (!allPartsInSelection) {
+ if (e.target.classList.contains("helper")) {
+ setSelection(() => parts.map(part => ({uid, part})) as Selection);
+ }
+ else {
+ setDragging(false);
+ setSelectingState({
+ topLeft: currentPointer,
+ size: {x: 0, y: 0},
+ });
+ setSelection(() => []);
+ return;
+ }
+ }
+ // start dragging
+ setDragging(true);
+ return;
}
- if (!allPartsInSelection) {
- if (e.target.classList.contains("helper")) {
- setSelection(() => [{uid, parts}] as Selection);
- }
- else {
- setDragging(false);
- setSelectingState({
- topLeft: currentPointer,
- size: {x: 0, y: 0},
- });
- setSelection(() => []);
- return;
- }
- }
-
- // start dragging
- setDragging(true);
- return;
}
}
@@ -142,40 +147,45 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
topLeft: currentPointer,
size: {x: 0, y: 0},
});
- setSelection(() => []);
- }, [getCurrentPointer, makeCheckPoint, insertMode, selection]);
+ if (!shiftOrCtrlPressed) {
+ setSelection(() => []);
+ }
+ }, [getCurrentPointer, makeCheckPoint, insertMode, selection, shiftOrCtrlPressed]);
const onMouseMove = useCallback((e: {pageX: number, pageY: number, movementX: number, movementY: number}) => {
const currentPointer = getCurrentPointer(e);
if (dragging) {
// const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos);
const pointerDelta = {x: e.movementX/zoom, y: e.movementY/zoom};
+ const getParts = (uid: string) => {
+ return state.selection.filter(s => s.uid === uid).map(s => s.part);
+ }
setState(state => ({
...state,
rountangles: state.rountangles.map(r => {
- const parts = state.selection.find(selected => selected.uid === r.uid)?.parts || [];
- if (parts.length === 0) {
+ const selectedParts = getParts(r.uid);
+ if (selectedParts.length === 0) {
return r;
}
return {
...r,
- ...transformRect(r, parts, pointerDelta),
+ ...transformRect(r, selectedParts, pointerDelta),
};
})
.toSorted((a,b) => area(b) - area(a)), // sort: smaller rountangles are drawn on top
diamonds: state.diamonds.map(d => {
- const parts = state.selection.find(selected => selected.uid === d.uid)?.parts || [];
- if (parts.length === 0) {
+ const selectedParts = getParts(d.uid);
+ if (selectedParts.length === 0) {
return d;
}
return {
...d,
- ...transformRect(d, parts, pointerDelta),
+ ...transformRect(d, selectedParts, pointerDelta),
}
}),
history: state.history.map(h => {
- const parts = state.selection.find(selected => selected.uid === h.uid)?.parts || [];
- if (parts.length === 0) {
+ const selectedParts = getParts(h.uid);
+ if (selectedParts.length === 0) {
return h;
}
return {
@@ -184,18 +194,18 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
}
}),
arrows: state.arrows.map(a => {
- const parts = state.selection.find(selected => selected.uid === a.uid)?.parts || [];
- if (parts.length === 0) {
+ const selectedParts = getParts(a.uid);
+ if (selectedParts.length === 0) {
return a;
}
return {
...a,
- ...transformLine(a, parts, pointerDelta),
+ ...transformLine(a, selectedParts, pointerDelta),
}
}),
texts: state.texts.map(t => {
- const parts = state.selection.find(selected => selected.uid === t.uid)?.parts || [];
- if (parts.length === 0) {
+ const selectedParts = getParts(t.uid);
+ if (selectedParts.length === 0) {
return t;
}
return {
@@ -239,12 +249,12 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
if (selectingState.size.x === 0 && selectingState.size.y === 0) {
const uid = e.target?.dataset.uid;
if (uid) {
- const parts = e.target?.dataset.parts.split(' ').filter((p: string) => p!=="");
+ const parts = e.target?.dataset.parts.split(' ').filter((p: string) => p!=="") || [];
if (uid) {
- setSelection(() => [{
- uid,
- parts,
- }]);
+ setSelection(oldSelection => [
+ ...oldSelection,
+ ...parts.map((part: string) => ({uid, part})),
+ ]);
}
}
}
@@ -261,26 +271,45 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
return isEntirelyWithin(scaledBBox, normalizedSS);
}).filter(el => !el.classList.contains("corner"));
- const uidToParts = new Map();
- for (const shape of shapesInSelection) {
- const uid = shape.dataset.uid;
- if (uid) {
- const parts: Set = uidToParts.get(uid) || new Set();
- for (const part of shape.dataset.parts?.split(' ') || []) {
- parts.add(part);
+ // @ts-ignore
+ setSelection(oldSelection => {
+ const newSelection = [];
+ const common = [];
+ for (const shape of shapesInSelection) {
+ const uid = shape.dataset.uid;
+ if (uid) {
+ const parts = shape.dataset.parts?.split(' ') || [];
+ for (const part of parts) {
+ if (oldSelection.some(({uid: oldUid, part: oldPart}) =>
+ uid === oldUid && part === oldPart)) {
+ common.push({uid, part});
+
+ }
+ else {
+ newSelection.push({uid, part});
+ }
+ }
}
- uidToParts.set(uid, parts);
}
- }
- setSelection(() => [...uidToParts.entries()].map(([uid,parts]) => ({
- uid,
- parts: [...parts],
- })));
+ return [...oldSelection, ...newSelection];
+ })
}
}
setSelectingState(null); // no longer making a selection
}, [dragging, selectingState, refSVG.current]);
+ const trackShiftKey = useCallback((e: KeyboardEvent) => {
+ // @ts-ignore
+ if (["INPUT", "TEXTAREA", "SELECT"].includes(e.target?.tagName)) return;
+
+ if (e.shiftKey || e.ctrlKey) {
+ setShiftOrCtrlPressed(true);
+ }
+ else {
+ setShiftOrCtrlPressed(false);
+ }
+ }, []);
+
const onKeyDown = useCallback((e: KeyboardEvent) => {
// don't capture keyboard events when focused on an input element:
// @ts-ignore
@@ -318,11 +347,11 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
...state,
// @ts-ignore
selection: [
- ...state.rountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
- ...state.diamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})),
- ...state.arrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
- ...state.texts.map(t => ({uid: t.uid, parts: ["text"]})),
- ...state.history.map(h => ({uid: h.uid, parts: ["history"]})),
+ ...state.rountangles.map(r => ["left", "top", "right", "bottom"].map(part => ({uid: r.uid, part}))),
+ ...state.diamonds.map(d => ["left", "top", "right", "bottom"].map(part => ({uid: d.uid, part}))),
+ ...state.arrows.map(a => ["start", "end"].map(part => ({uid: a.uid, part}))),
+ ...state.texts.map(t => ({uid: t.uid, part: "text"})),
+ ...state.history.map(h => ({uid: h.uid, part: "history"})),
]
}))
}
@@ -334,10 +363,14 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
window.addEventListener("mouseup", onMouseUp);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("keydown", onKeyDown);
+ window.addEventListener("keydown", trackShiftKey);
+ window.addEventListener("keyup", trackShiftKey);
return () => {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
window.removeEventListener("keydown", onKeyDown);
+ window.removeEventListener("keydown", trackShiftKey);
+ window.removeEventListener("keyup", trackShiftKey);
};
}, [selectingState, dragging]);
From 6efc27453eb311b8fadfe4d650e16c0641b71b8a Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 19:25:43 +0100
Subject: [PATCH 14/30] new featuregit status dark mode
---
src/App/App.css | 64 ++++++++++--------------
src/App/App.tsx | 13 +++--
src/App/BottomPanel/BottomPanel.css | 6 +--
src/App/BottomPanel/BottomPanel.tsx | 4 +-
src/App/Modals/About.tsx | 4 +-
src/App/SideBar/SideBar.css | 66 ++++++++-----------------
src/App/SideBar/check_property.ts | 4 +-
src/App/TopPanel/Icons.tsx | 11 ++++-
src/App/TopPanel/TopPanel.tsx | 24 ++++++++-
src/App/VisualEditor/DiamondSVG.tsx | 3 +-
src/App/VisualEditor/VisualEditor.css | 66 ++++++++++++-------------
src/App/VisualEditor/hooks/useMouse.tsx | 16 +++---
src/Logo/Logo.tsx | 11 +++++
src/index.css | 46 +++++++++++++++--
14 files changed, 189 insertions(+), 149 deletions(-)
create mode 100644 src/Logo/Logo.tsx
diff --git a/src/App/App.css b/src/App/App.css
index 51defe8..73965d8 100644
--- a/src/App/App.css
+++ b/src/App/App.css
@@ -1,61 +1,44 @@
-/* details {
- padding-left: 20;
-}
-summary {
- margin-left: -20;
-} */
-
details:has(+ details) {
- border-bottom: 1px lightgrey solid;
+ border-bottom: 1px var(--separator-color) solid;
}
.runtimeState {
padding: 4px;
- /* padding-left: 4px;
- padding-right: 4px;
- padding-top: 2px;
- padding-bottom: 2px; */
}
.runtimeState:has(+.runtimeState) {
- border-bottom: 1px lightgrey solid;
+ border-bottom: 1px var(--separator-color) solid;
}
.runtimeState:has(+.runtimeState.active) {
border-bottom: 0;
}
.runtimeState:hover {
- /* background-color: rgba(255, 140, 0, 0.2); */
- background-color: rgba(0,0,255,0.2);
+ background-color: var(--light-accent-color);
cursor: pointer;
}
.runtimeState.active {
- background-color: rgba(0,0,255,0.2);
- border: solid blue 1px;
+ background-color: var(--light-accent-color);
+ border: solid var(--accent-border-color) 1px;
}
.runtimeState.plantStep:not(.active) {
- background-color: #f7f7f7;
+ background-color: var(--inactive-bg-color);
}
.runtimeState.plantStep * {
- color: grey;
+ color: var(--inactive-fg-color);
}
.runtimeState.runtimeError {
- background-color: lightpink;
- color: darkred;
+ background-color: var(--error-bg-color);
+ color: var(--error-color); /* used to be darkred, but this one's a bit lighter */
}
.runtimeState.runtimeError.active {
- border-color: darkred;
+ border-color: var(--error-color);/* used to be darkred, but this one's a bit lighter */
}
-/* details:not(:has(details)) > summary::marker {
- color: white;
-} */
-
.readonlyTextBox {
width: 56;
- background-color:"#eee";
text-align: "right";
}
@@ -78,20 +61,23 @@ details:has(+ details) {
}
button {
- background-color: #fcfcfc;
- border: 1px lightgrey solid;
+ background-color: var(--button-bg-color);
+ border: 1px var(--separator-color) solid;
}
button:not(:disabled):hover {
- background-color: rgba(0, 0, 255, 0.2);
+ background-color: var(--light-accent-color);
+}
+
+button:disabled {
+ background-color: var(--inactive-bg-color);
+ color: var(--inactive-fg-color);
}
button.active {
- border: solid blue 1px;
- background-color: rgba(0,0,255,0.2);
- /* margin-right: 1px; */
- /* margin-left: 0; */
- color: black;
+ border: solid var(--accent-border-color) 1px;
+ background-color: var(--light-accent-color);
+ color: var(--text-color);
}
.modalOuter {
@@ -102,7 +88,7 @@ button.active {
justify-content: center;
align-items: center;
text-align: center;
- background-color: rgba(200,200,200,0.7);
+ background-color: var(--modal-backdrop-color);
z-index: 1000;
}
@@ -110,7 +96,7 @@ button.active {
position: relative;
text-align: center;
display: inline-block;
- background-color: white;
+ background-color: var(--background-color);
max-height: 100vh;
overflow: auto;
}
@@ -128,7 +114,7 @@ div.stackHorizontal {
div.status {
display: inline-block;
vertical-align: middle;
- background-color: grey;
+ background-color: var(--status-inactive-color);
border-radius: 50%;
height: 12px;
width: 12px;
@@ -141,6 +127,6 @@ div.status.violated {
}
div.status.satisfied {
- background-color: forestgreen;
+ background-color: var(--status-ok-color);
}
diff --git a/src/App/App.tsx b/src/App/App.tsx
index 9b77e2e..aa38f9a 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -17,6 +17,7 @@ import { useUrlHashState } from "../hooks/useUrlHashState";
import { plants } from "./plants";
import { emptyState } from "@/statecharts/concrete_syntax";
import { ModalOverlay } from "./Modals/ModalOverlay";
+import { usePersistentState } from "@/hooks/usePersistentState";
export type EditHistory = {
current: VisualEditorState,
@@ -38,10 +39,14 @@ const defaultAppState: AppState = {
...defaultSideBarState,
}
+export type LightMode = "light" | "auto" | "dark";
+
export function App() {
const [editHistory, setEditHistory] = useState(null);
const [modal, setModal] = useState(null);
+ const [lightMode, setLightMode] = usePersistentState("lightMode", "auto");
+
const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
const editorState = editHistory && editHistory.current;
@@ -135,7 +140,8 @@ export function App() {
const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1];
- return
+ return
+
{/* top-to-bottom: everything -> bottom panel */}
@@ -150,7 +156,7 @@ export function App() {
style={{flex: '0 0 content'}}
>
{editHistory && }
{/* Editor */}
@@ -179,7 +185,8 @@ export function App() {
{syntaxErrors && }
- ;
+
+
;
}
export default App;
diff --git a/src/App/BottomPanel/BottomPanel.css b/src/App/BottomPanel/BottomPanel.css
index 92c1a48..d0922b3 100644
--- a/src/App/BottomPanel/BottomPanel.css
+++ b/src/App/BottomPanel/BottomPanel.css
@@ -5,6 +5,6 @@
}
.bottom {
- border-top: 1px lightgrey solid;
- background-color: rgb(255, 249, 235);
-}
\ No newline at end of file
+ border-top: 1px var(--separator-color) solid;
+ background-color: var(--bottom-panel-bg-color);
+}
diff --git a/src/App/BottomPanel/BottomPanel.tsx b/src/App/BottomPanel/BottomPanel.tsx
index c0d02ae..51dcd81 100644
--- a/src/App/BottomPanel/BottomPanel.tsx
+++ b/src/App/BottomPanel/BottomPanel.tsx
@@ -3,14 +3,14 @@ import { TraceableError } from "../../statecharts/parser";
import "./BottomPanel.css";
-import logo from "../../../artwork/logo-playful.svg";
import { PersistentDetailsLocalStorage } from "../PersistentDetails";
+import { Logo } from "@/Logo/Logo";
export function BottomPanel(props: {errors: TraceableError[]}) {
const [greeting, setGreeting] = useState(
- Welcome to
+ Welcome to
);
diff --git a/src/App/Modals/About.tsx b/src/App/Modals/About.tsx
index 9595c81..b270b8e 100644
--- a/src/App/Modals/About.tsx
+++ b/src/App/Modals/About.tsx
@@ -1,9 +1,9 @@
+import { Logo } from "@/Logo/Logo";
import { Dispatch, ReactElement, SetStateAction } from "react";
-import logo from "../../../artwork/logo-playful.svg";
export function About(props: {setModal: Dispatch
>}) {
return
-
+
StateBuddy is an open source tool for Statechart editing, simulation, (omniscient) debugging and testing.
diff --git a/src/App/SideBar/SideBar.css b/src/App/SideBar/SideBar.css
index f2081f6..b2212da 100644
--- a/src/App/SideBar/SideBar.css
+++ b/src/App/SideBar/SideBar.css
@@ -1,78 +1,51 @@
-details.active {
- border: rgb(192, 125, 0);
- background-color:rgb(255, 251, 244);
- filter: drop-shadow( 0px 0px 3px rgba(192, 125, 0, 0.856));
-}
details > summary {
padding: 2px;
}
-/* these two rules add a bit of padding to an opened
node */
-/* details:open > summary:has(+ *) {
- margin-bottom: 4px;
-}
-details:open:has(>summary:has(+ *)) {
- padding-bottom: 8px;
-} */
-
details > summary:hover {
- background-color: #eee;
+ background-color: var(--summary-hover-bg-color);
cursor: default;
}
.errorStatus details > summary:hover {
- background-color: rgb(102, 0, 0);
+ background-color: var(--error-hover-bg-color);
}
.stateTree > * {
padding-left: 10px;
- /* border: 1px black solid; */
background-color: white;
- /* margin-bottom: 4px; */
- /* padding-right: 2px; */
- /* padding-top: 2px; */
- /* padding-bottom: 2px; */
- /* color: black; */
- /* width: fit-content; */
- /* border-radius: 10px; */
}
-/* if has no children (besides the obvious child), then hide the marker */
-/* details:not(:has(:not(summary))) > summary::marker {
- content: " ";
-} */
-
.outputEvent {
- border: 1px black solid;
+ cursor: default;
+ border: 1px var(--separator-color) solid;
border-radius: 6px;
- /* margin-left: 4px; */
padding-left: 2px;
padding-right: 2px;
- background-color: rgb(230, 249, 255);
- color: black;
+ background-color: var(--output-event-bg-color);
+ color: var(--text-color);
display: inline-block;
}
.internalEvent {
- border: 1px black solid;
+ cursor: default;
+ border: 1px var(--separator-color) solid;
border-radius: 6px;
- /* margin-left: 4px; */
padding-left: 2px;
padding-right: 2px;
- background-color: rgb(255, 218, 252);
- color: black;
+ background-color: var(--internal-event-bg-color);
+ color: var(--text-color);
display: inline-block;
}
.inputEvent {
- border: 1px black solid;
+ border: 1px var(--separator-color) solid;
border-radius: 6px;
- /* margin-left: 4px; */
padding-left: 2px;
padding-right: 2px;
- background-color: rgb(224, 247, 209);
- color: black;
+ background-color: var(--input-event-bg-color);
+ color: var(--text-color);
display: inline-block;
}
.inputEvent:disabled {
@@ -82,21 +55,24 @@ details > summary:hover {
vertical-align: middle;
}
button.inputEvent:hover:not(:disabled) {
- background-color: rgb(195, 224, 176);
+ background-color: var(--input-event-hover-bg-color);
}
button.inputEvent:active:not(:disabled) {
- background-color: rgb(176, 204, 158);
+ background-color: var(--input-event-active-bg-color);
}
.activeState {
- border: rgb(192, 125, 0);
- background-color:rgb(255, 251, 244);
- filter: drop-shadow( 0px 0px 3px rgba(192, 125, 0, 0.856));
+ /* border: rgb(192, 125, 0); */
+ /* background-color:rgb(255, 251, 244); */
+ border: var(--active-state-border-color) 1px solid;
+ background-color: var(--active-state-bg-color);
+ /* filter: drop-shadow( 0px 0px 3px rgba(192, 125, 0, 0.856)); */
border-radius: 6px;
margin-left: 4px;
padding-left: 2px;
padding-right: 2px;
display: inline-block;
+ color: var(--text-color);
}
/* hr {
diff --git a/src/App/SideBar/check_property.ts b/src/App/SideBar/check_property.ts
index 24a41cb..f0c779e 100644
--- a/src/App/SideBar/check_property.ts
+++ b/src/App/SideBar/check_property.ts
@@ -2,8 +2,8 @@ import { RT_Statechart } from "@/statecharts/runtime_types";
import { Plant } from "../Plant/Plant";
import { TraceItem } from "../hooks/useSimulator";
-// const endpoint = "http://localhost:15478/check_property";
-const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
+const endpoint = "http://localhost:15478/check_property";
+// const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
export type PropertyTrace = {
timestamp: number,
diff --git a/src/App/TopPanel/Icons.tsx b/src/App/TopPanel/Icons.tsx
index 97ae50c..0d308e8 100644
--- a/src/App/TopPanel/Icons.tsx
+++ b/src/App/TopPanel/Icons.tsx
@@ -18,12 +18,19 @@ export function PseudoStateIcon(props: {}) {
${w - 1} ${h / 2},
${w / 2} ${h - 1},
${1} ${h / 2},
- `} fill="white" stroke="black" strokeWidth={1.2} />
+ `}
+ style={{
+ fill: 'var(--and-state-bg-color',
+ stroke: 'var(--rountangle-stroke-color',
+ }} strokeWidth={1.2} />
;
}
export function HistoryIcon(props: { kind: "shallow" | "deep"; }) {
const w = 20, h = 20;
const text = props.kind === "shallow" ? "H" : "H*";
- return {text} ;
+ return {text} ;
}
diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx
index a8272b2..eeb0048 100644
--- a/src/App/TopPanel/TopPanel.tsx
+++ b/src/App/TopPanel/TopPanel.tsx
@@ -3,12 +3,16 @@ import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types";
import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time";
import { InsertMode } from "./InsertModes";
import { About } from "../Modals/About";
-import { EditHistory } from "../App";
+import { EditHistory, LightMode } from "../App";
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
import { UndoRedoButtons } from "./UndoRedoButtons";
import { ZoomButtons } from "./ZoomButtons";
import { formatTime } from "../../util/util";
+import DarkModeIcon from '@mui/icons-material/DarkMode';
+import LightModeIcon from '@mui/icons-material/LightMode';
+import BrightnessAutoIcon from '@mui/icons-material/BrightnessAuto';
+
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
import CachedIcon from '@mui/icons-material/Cached';
import InfoOutlineIcon from '@mui/icons-material/InfoOutline';
@@ -33,6 +37,8 @@ export type TopPanelProps = {
onInit: () => void,
onClear: () => void,
onBack: () => void,
+ lightMode: LightMode,
+ setLightMode: Dispatch>,
insertMode: InsertMode,
setInsertMode: Dispatch>,
setModal: Dispatch>,
@@ -45,7 +51,7 @@ export type TopPanelProps = {
const ShortCutShowKeys = ~ ;
-export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) {
+export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, lightMode, setLightMode, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) {
const [displayTime, setDisplayTime] = useState("0.000");
const [timescale, setTimescale] = usePersistentState("timescale", 1);
@@ -146,6 +152,20 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
}, [config, time, onInit, onChangePaused, setShowKeys, onSkip, onBack, onClear]);
return
+ {/* light / dark mode */}
+
+ setLightMode("light")}>
+
+
+ setLightMode("auto")}>
+
+
+ setLightMode("dark")}>
+
+
+
+
+
{/* shortcuts / about */}
diff --git a/src/App/VisualEditor/DiamondSVG.tsx b/src/App/VisualEditor/DiamondSVG.tsx
index da7b289..f051319 100644
--- a/src/App/VisualEditor/DiamondSVG.tsx
+++ b/src/App/VisualEditor/DiamondSVG.tsx
@@ -14,7 +14,8 @@ export const DiamondShape = memo(function DiamondShape(props: {size: Vec2D, extr
${minSize.x/2} ${minSize.y},
${0} ${minSize.y/2}
`}
- fill="white"
+ style={{fill: 'var(--and-state-bg-color', stroke: 'var(--rountangle-stroke-color)'}}
+ // fill="white"
stroke="black"
strokeWidth={2}
{...props.extraAttrs}
diff --git a/src/App/VisualEditor/VisualEditor.css b/src/App/VisualEditor/VisualEditor.css
index 19801a4..4153853 100644
--- a/src/App/VisualEditor/VisualEditor.css
+++ b/src/App/VisualEditor/VisualEditor.css
@@ -1,6 +1,6 @@
.svgCanvas {
cursor: crosshair;
- background-color: #eee;
+ background-color: var(--or-state-bg-color);
}
.svgCanvas.dragging {
@@ -19,16 +19,15 @@
/* rectangle drawn while a selection is being made */
.selecting {
- fill: blue;
- fill-opacity: 0.2;
+ fill: var(--light-accent-color);
stroke-width: 1px;
- stroke:black;
+ stroke: var(--accent-border-color);
stroke-dasharray: 7 6;
}
.rountangle {
- fill: white;
- stroke: black;
+ fill: var(--and-state-bg-color);
+ stroke: var(--rountangle-stroke-color);
stroke-width: 2px;
}
@@ -39,9 +38,10 @@
stroke: var(--error-color);
}
.rountangle.active {
- stroke: rgb(205, 133, 0);
+ stroke: var(--active-state-border-color);
/* stroke: none; */
- fill:rgb(255, 240, 214);
+ /* fill:rgb(255, 240, 214); */
+ fill: var(--active-state-bg-color);
/* filter: drop-shadow( 2px 2px 2px rgba(124, 37, 10, 0.729)); */
}
@@ -54,8 +54,7 @@ line.helper {
stroke-width: 16px;
}
line.helper:hover:not(:active) {
- stroke: blue;
- stroke-opacity: 0.2;
+ stroke: var(--light-accent-color);
cursor: grab;
}
@@ -65,8 +64,7 @@ path.helper {
stroke-width: 16px;
}
path.helper:hover:not(:active) {
- stroke: blue;
- stroke-opacity: 0.2;
+ stroke: var(--light-accent-color);
cursor: grab;
}
@@ -74,23 +72,22 @@ circle.helper {
fill: rgba(0, 0, 0, 0);
}
circle.helper:hover:not(:active) {
- fill: blue;
- fill-opacity: 0.2;
+ fill: var(--light-accent-color);
cursor: grab;
}
.rountangle.or {
stroke-dasharray: 7 6;
- fill: #eee;
+ fill: var(--or-state-bg-color);
}
.arrow {
fill: none;
- stroke: black;
+ stroke: var(--rountangle-stroke-color);
stroke-width: 2px;
}
.arrow.selected {
- stroke: blue;
+ stroke: var(--accent-border-color);
stroke-width: 3px;
}
@@ -110,34 +107,29 @@ circle.helper:hover:not(:active) {
}
line.selected, circle.selected {
- fill: rgba(0, 0, 255, 0.2);
- /* stroke-dasharray: 7 6; */
- stroke: blue;
+ fill: var(--light-accent-color);
+ stroke: var(--accent-border-color);
stroke-width: 4px;
}
.draggableText.selected, .draggableText.selected:hover {
- fill: blue;
+ fill: var(--accent-border-color);
font-weight: 600;
}
-.draggableText:hover:not(:active) {
- /* fill: blue; */
- /* cursor: grab; */
-}
text.helper {
fill: rgba(0,0,0,0);
stroke: rgba(0,0,0,0);
stroke-width: 6px;
}
text.helper:hover {
- stroke: blue;
- stroke-opacity: 0.2;
+ stroke: var(--light-accent-color);
cursor: grab;
}
.draggableText, .draggableText.highlight {
paint-order: stroke;
- stroke: white;
+ fill: var(--text-color);
+ stroke: var(--background-color);
stroke-width: 4px;
stroke-linecap: butt;
stroke-linejoin: miter;
@@ -146,12 +138,16 @@ text.helper:hover {
}
.draggableText.highlight:not(.selected) {
- fill: green;
+ fill: var(--associated-color);
font-weight: 600;
}
+.draggableText.selected {
+ fill: var(--accent-border-color);
+}
+
.highlight:not(.selected):not(text) {
- stroke: green;
+ stroke: var(--associated-color);
stroke-width: 3px;
fill: none;
}
@@ -160,19 +156,19 @@ text.helper:hover {
stroke: var(--error-color);
}
.arrow.fired {
- stroke: rgb(160 0 168);
+ stroke: var(--fired-transition-color);
stroke-width: 3px;
animation: blinkTransition 1s;
}
@keyframes blinkTransition {
0% {
- stroke: rgb(255, 128, 9);
+ stroke: var(--firing-transition-color);
stroke-width: 6px;
- filter: drop-shadow(0 0 5px rgba(255, 128, 9, 1));
+ filter: drop-shadow(0 0 5px var(--firing-transition-color));
}
100% {
- stroke: rgb(160 0 168);
+ stroke: var(--fired-transition-color);
}
}
@@ -191,5 +187,5 @@ g:hover > .errorHover {
}
text.uid {
- fill: lightgrey;
+ fill: var(--separator-color);
}
\ No newline at end of file
diff --git a/src/App/VisualEditor/hooks/useMouse.tsx b/src/App/VisualEditor/hooks/useMouse.tsx
index 4c1168f..335a6f0 100644
--- a/src/App/VisualEditor/hooks/useMouse.tsx
+++ b/src/App/VisualEditor/hooks/useMouse.tsx
@@ -11,8 +11,6 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
const [dragging, setDragging] = useState(false);
const [shiftOrCtrlPressed, setShiftOrCtrlPressed] = useState(false);
- console.log(shiftOrCtrlPressed);
-
// not null while the user is making a selection
const [selectingState, setSelectingState] = useState(null);
@@ -347,17 +345,19 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
...state,
// @ts-ignore
selection: [
- ...state.rountangles.map(r => ["left", "top", "right", "bottom"].map(part => ({uid: r.uid, part}))),
- ...state.diamonds.map(d => ["left", "top", "right", "bottom"].map(part => ({uid: d.uid, part}))),
- ...state.arrows.map(a => ["start", "end"].map(part => ({uid: a.uid, part}))),
- ...state.texts.map(t => ({uid: t.uid, part: "text"})),
- ...state.history.map(h => ({uid: h.uid, part: "history"})),
- ]
+ ...state.rountangles.flatMap(r => ["left", "top", "right", "bottom"].map(part => ({uid: r.uid, part}))),
+ ...state.diamonds.flatMap(d => ["left", "top", "right", "bottom"].map(part => ({uid: d.uid, part}))),
+ ...state.arrows.flatMap(a => ["start", "end"].map(part => ({uid: a.uid, part}))),
+ ...state.texts.map(t => ({uid: t.uid, part: "text"})),
+ ...state.history.map(h => ({uid: h.uid, part: "history"})),
+ ]
}))
}
}
}, [makeCheckPoint, deleteSelection, setState, setDragging]);
+ console.log(state.selection);
+
useEffect(() => {
// mousemove and mouseup are global event handlers so they keep working when pointer is outside of browser window
window.addEventListener("mouseup", onMouseUp);
diff --git a/src/Logo/Logo.tsx b/src/Logo/Logo.tsx
new file mode 100644
index 0000000..5d064ad
--- /dev/null
+++ b/src/Logo/Logo.tsx
@@ -0,0 +1,11 @@
+// i couldn't find a better way to make the text in the logo adapt to light/dark mode...
+export function Logo() {
+ return
+
+
+ ;
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index a9a5d3e..512a0d0 100644
--- a/src/index.css
+++ b/src/index.css
@@ -5,9 +5,44 @@ html, body {
font-size: 10pt;
}
-body {
- /* --error-color: darkred; */
- --error-color: rgb(163, 0, 0);
+:root {
+ color-scheme: light dark;
+
+ --background-color: light-dark(white, rgb(26, 26, 26));
+ --text-color: light-dark(black, white);
+ --error-color: light-dark(rgb(163, 0, 0), rgb(255, 82, 82));
+ --error-bg-color: light-dark(lightpink, rgb(75, 0, 0));
+ --error-hover-bg-color: light-dark(rgb(102, 0, 0), rgb(102, 0, 0));
+ --light-accent-color: light-dark(rgba(0,0,255,0.2), rgba(78, 186, 248, 0.377));
+ --accent-border-color: light-dark(blue, rgb(64, 185, 255));
+ --separator-color: light-dark(lightgrey, rgb(44, 44, 44));
+ --inactive-bg-color: light-dark(#f7f7f7, rgb(29, 29, 29));
+ --inactive-fg-color: light-dark(grey, rgb(70, 70, 70));
+ --button-bg-color: light-dark(#fcfcfc, rgb(20, 20, 20));
+ --modal-backdrop-color: light-dark(rgba(200,200,200,0.7), rgba(59, 7, 7, 0.849));
+ --status-inactive-color: light-dark(grey, grey);
+ --status-ok-color: light-dark(forestgreen, forestgreen);
+ --or-state-bg-color: light-dark(#eee, #1a1a1a);
+ --and-state-bg-color: light-dark(white, rgb(0, 0, 0));
+ --rountangle-stroke-color: light-dark(black, #d4d4d4);
+ --active-state-bg-color: light-dark(rgb(255, 240, 214), rgb(53, 37, 18));
+ --active-state-border-color: light-dark(rgb(205, 133, 0), rgb(235, 124, 21));
+ --fired-transition-color: light-dark(rgb(160, 0, 168), rgb(160, 0, 168));
+ --firing-transition-color: light-dark(rgba(255, 128, 9, 1), rgba(255, 128, 9, 1));
+ --associated-color: light-dark(green, rgb(186, 245, 119));
+ --bottom-panel-bg-color: light-dark(rgb(255, 249, 235), rgb(41, 4, 4));
+ --summary-hover-bg-color: light-dark(#eee, #242424);
+ --internal-event-bg-color: light-dark(rgb(255, 218, 252), rgb(99, 27, 94));
+ --input-event-bg-color: light-dark(rgb(224, 247, 209), rgb(59, 95, 37));
+ --input-event-hover-bg-color: light-dark(rgb(195, 224, 176), rgb(59, 88, 40));
+ --input-event-active-bg-color: light-dark(rgb(176, 204, 158), rgb(77, 117, 53));
+ --output-event-bg-color: light-dark(rgb(230, 249, 255), rgb(28, 83, 104));
+}
+
+/* for some reason i need to add this or some elements are rendered in OS color rather than 'forced' color */
+div {
+ color: var(--text-color);
+ background-color: var(--background-color);
}
div#root {
@@ -27,17 +62,18 @@ kbd {
box-shadow: inset 0 -1.5px 0 #aaa;
vertical-align: middle;
user-select: none;
+ color: black;
}
kbd:active { transform: translateY(1px); }
input {
/* border: solid blue 2px; */
- accent-color: rgba(0,0,255,0.2);
+ accent-color: var(--light-accent-color);
}
::selection {
- background-color: rgba(0,0,255,0.2);
+ background-color: var(--light-accent-color);
}
label {
From 02cbbe601ad966dd842c7d24e3279bf0805849b8 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 19:50:33 +0100
Subject: [PATCH 15/30] disable forcing light/dark mode (doesn't work properly)
---
src/App/App.tsx | 11 +-
src/App/BottomPanel/BottomPanel.css | 2 +-
src/App/SideBar/SideBar.css | 15 -
src/App/TopPanel/TopPanel.tsx | 11 +-
src/App/VisualEditor/hooks/useMouse.tsx | 2 -
src/Logo/Logo.tsx | 1572 ++++++++++++++++++++++-
src/index.css | 10 +-
7 files changed, 1586 insertions(+), 37 deletions(-)
diff --git a/src/App/App.tsx b/src/App/App.tsx
index aa38f9a..d0ac4d8 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -45,7 +45,8 @@ export function App() {
const [editHistory, setEditHistory] = useState(null);
const [modal, setModal] = useState(null);
- const [lightMode, setLightMode] = usePersistentState("lightMode", "auto");
+ // const [lightMode, setLightMode] = usePersistentState("lightMode", "auto");
+ const lightMode = "auto";
const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
@@ -140,7 +141,11 @@ export function App() {
const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1];
- return
+ return
{/* top-to-bottom: everything -> bottom panel */}
@@ -156,7 +161,7 @@ export function App() {
style={{flex: '0 0 content'}}
>
{editHistory && }
{/* Editor */}
diff --git a/src/App/BottomPanel/BottomPanel.css b/src/App/BottomPanel/BottomPanel.css
index d0922b3..4a93ad9 100644
--- a/src/App/BottomPanel/BottomPanel.css
+++ b/src/App/BottomPanel/BottomPanel.css
@@ -1,7 +1,7 @@
.errorStatus {
/* background-color: rgb(230,0,0); */
background-color: var(--error-color);
- color: white;
+ color: var(--background-color);
}
.bottom {
diff --git a/src/App/SideBar/SideBar.css b/src/App/SideBar/SideBar.css
index b2212da..4f52df7 100644
--- a/src/App/SideBar/SideBar.css
+++ b/src/App/SideBar/SideBar.css
@@ -62,11 +62,8 @@ button.inputEvent:active:not(:disabled) {
}
.activeState {
- /* border: rgb(192, 125, 0); */
- /* background-color:rgb(255, 251, 244); */
border: var(--active-state-border-color) 1px solid;
background-color: var(--active-state-bg-color);
- /* filter: drop-shadow( 0px 0px 3px rgba(192, 125, 0, 0.856)); */
border-radius: 6px;
margin-left: 4px;
padding-left: 2px;
@@ -75,18 +72,6 @@ button.inputEvent:active:not(:disabled) {
color: var(--text-color);
}
-/* hr {
- display: block;
- height: 1px;
- border: 0;
- border-top: 1px solid #ccc;
- margin: 0;
- margin-top: -1px;
- margin-bottom: -5px;
- padding: 0;
- z-index: -1;
-} */
-
ul {
list-style-type: circle;
margin-block-start: 0;
diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx
index eeb0048..7e922c4 100644
--- a/src/App/TopPanel/TopPanel.tsx
+++ b/src/App/TopPanel/TopPanel.tsx
@@ -37,8 +37,8 @@ export type TopPanelProps = {
onInit: () => void,
onClear: () => void,
onBack: () => void,
- lightMode: LightMode,
- setLightMode: Dispatch>,
+ // lightMode: LightMode,
+ // setLightMode: Dispatch>,
insertMode: InsertMode,
setInsertMode: Dispatch>,
setModal: Dispatch>,
@@ -51,7 +51,7 @@ export type TopPanelProps = {
const ShortCutShowKeys = ~ ;
-export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, lightMode, setLightMode, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) {
+export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) {
const [displayTime, setDisplayTime] = useState("0.000");
const [timescale, setTimescale] = usePersistentState("timescale", 1);
@@ -152,7 +152,8 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
}, [config, time, onInit, onChangePaused, setShowKeys, onSkip, onBack, onClear]);
return
- {/* light / dark mode */}
+
+ {/* light / dark mode
setLightMode("light")}>
@@ -164,7 +165,7 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
-
+
*/}
{/* shortcuts / about */}
diff --git a/src/App/VisualEditor/hooks/useMouse.tsx b/src/App/VisualEditor/hooks/useMouse.tsx
index 335a6f0..a49f8db 100644
--- a/src/App/VisualEditor/hooks/useMouse.tsx
+++ b/src/App/VisualEditor/hooks/useMouse.tsx
@@ -356,8 +356,6 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
}
}, [makeCheckPoint, deleteSelection, setState, setDragging]);
- console.log(state.selection);
-
useEffect(() => {
// mousemove and mouseup are global event handlers so they keep working when pointer is outside of browser window
window.addEventListener("mouseup", onMouseUp);
diff --git a/src/Logo/Logo.tsx b/src/Logo/Logo.tsx
index 5d064ad..f524103 100644
--- a/src/Logo/Logo.tsx
+++ b/src/Logo/Logo.tsx
@@ -6,6 +6,1572 @@ export function Logo() {
fill: var(--text-color);
}
`}
-
- ;
-}
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ;
+}
diff --git a/src/index.css b/src/index.css
index 512a0d0..a7da777 100644
--- a/src/index.css
+++ b/src/index.css
@@ -8,11 +8,11 @@ html, body {
:root {
color-scheme: light dark;
- --background-color: light-dark(white, rgb(26, 26, 26));
+ --background-color: light-dark(white, rgb(0, 0, 0));
--text-color: light-dark(black, white);
--error-color: light-dark(rgb(163, 0, 0), rgb(255, 82, 82));
--error-bg-color: light-dark(lightpink, rgb(75, 0, 0));
- --error-hover-bg-color: light-dark(rgb(102, 0, 0), rgb(102, 0, 0));
+ --error-hover-bg-color: light-dark(rgb(102, 0, 0), rgb(238, 153, 153));
--light-accent-color: light-dark(rgba(0,0,255,0.2), rgba(78, 186, 248, 0.377));
--accent-border-color: light-dark(blue, rgb(64, 185, 255));
--separator-color: light-dark(lightgrey, rgb(44, 44, 44));
@@ -39,12 +39,6 @@ html, body {
--output-event-bg-color: light-dark(rgb(230, 249, 255), rgb(28, 83, 104));
}
-/* for some reason i need to add this or some elements are rendered in OS color rather than 'forced' color */
-div {
- color: var(--text-color);
- background-color: var(--background-color);
-}
-
div#root {
height: 100%;
}
From af934c6767b943be92fa03ea37bf3944671a1c54 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 19:55:52 +0100
Subject: [PATCH 16/30] improve colors
---
src/index.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/index.css b/src/index.css
index a7da777..4ef1abf 100644
--- a/src/index.css
+++ b/src/index.css
@@ -19,7 +19,7 @@ html, body {
--inactive-bg-color: light-dark(#f7f7f7, rgb(29, 29, 29));
--inactive-fg-color: light-dark(grey, rgb(70, 70, 70));
--button-bg-color: light-dark(#fcfcfc, rgb(20, 20, 20));
- --modal-backdrop-color: light-dark(rgba(200,200,200,0.7), rgba(59, 7, 7, 0.849));
+ --modal-backdrop-color: light-dark(rgba(200,200,200,0.7), rgba(23, 22, 32, 0.849));
--status-inactive-color: light-dark(grey, grey);
--status-ok-color: light-dark(forestgreen, forestgreen);
--or-state-bg-color: light-dark(#eee, #1a1a1a);
From ee899cea2d91bbe55f3869e8c0ad836a9e6ad6d5 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 21:01:50 +0100
Subject: [PATCH 17/30] fix selection
---
src/App/VisualEditor/hooks/useMouse.tsx | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/App/VisualEditor/hooks/useMouse.tsx b/src/App/VisualEditor/hooks/useMouse.tsx
index a49f8db..54b198a 100644
--- a/src/App/VisualEditor/hooks/useMouse.tsx
+++ b/src/App/VisualEditor/hooks/useMouse.tsx
@@ -271,16 +271,16 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
// @ts-ignore
setSelection(oldSelection => {
- const newSelection = [];
+ const newSelection = [...oldSelection];
const common = [];
for (const shape of shapesInSelection) {
const uid = shape.dataset.uid;
if (uid) {
const parts = shape.dataset.parts?.split(' ') || [];
for (const part of parts) {
- if (oldSelection.some(({uid: oldUid, part: oldPart}) =>
+ if (newSelection.some(({uid: oldUid, part: oldPart}) =>
uid === oldUid && part === oldPart)) {
- common.push({uid, part});
+ // common.push({uid, part});
}
else {
@@ -289,7 +289,9 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
}
}
}
- return [...oldSelection, ...newSelection];
+ // console.log({newSelection, oldSelection, common});
+ // return [...oldSelection, ...newSelection];
+ return newSelection;xxxxxxxx
})
}
}
From e3b88b7d897233d7ad2ad620109deaaa92e2df45 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 21:06:19 +0100
Subject: [PATCH 18/30] blue is better for bottom panel in dark mode
---
src/index.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/index.css b/src/index.css
index 4ef1abf..9b7ef7d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -30,7 +30,7 @@ html, body {
--fired-transition-color: light-dark(rgb(160, 0, 168), rgb(160, 0, 168));
--firing-transition-color: light-dark(rgba(255, 128, 9, 1), rgba(255, 128, 9, 1));
--associated-color: light-dark(green, rgb(186, 245, 119));
- --bottom-panel-bg-color: light-dark(rgb(255, 249, 235), rgb(41, 4, 4));
+ --bottom-panel-bg-color: light-dark(rgb(255, 249, 235), rgb(24, 40, 70));
--summary-hover-bg-color: light-dark(#eee, #242424);
--internal-event-bg-color: light-dark(rgb(255, 218, 252), rgb(99, 27, 94));
--input-event-bg-color: light-dark(rgb(224, 247, 209), rgb(59, 95, 37));
From e29559e46dfbd7c639083b5c01e063b8a0530f94 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 22:01:02 +0100
Subject: [PATCH 19/30] improve dark mode colors + fix dark logo in chrome
---
src/App/App.css | 2 +-
src/App/App.tsx | 2 +-
src/Logo/Logo.tsx | 15 ++++++++-------
src/index.css | 19 +++++++++++++++----
4 files changed, 25 insertions(+), 13 deletions(-)
diff --git a/src/App/App.css b/src/App/App.css
index 73965d8..5bc6051 100644
--- a/src/App/App.css
+++ b/src/App/App.css
@@ -47,7 +47,7 @@ details:has(+ details) {
}
.toolbar input {
- height: 20px;
+ height: 22px;
}
.toolbar div {
vertical-align: bottom;
diff --git a/src/App/App.tsx b/src/App/App.tsx
index d0ac4d8..bd89a9b 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -174,7 +174,7 @@ export function App() {
{/* Right: sidebar */}
-
+
+
+
+
+
+
+
@@ -1563,12 +1569,7 @@ export function Logo() {
-
-
-
-
-
-
+
diff --git a/src/index.css b/src/index.css
index 9b7ef7d..14779eb 100644
--- a/src/index.css
+++ b/src/index.css
@@ -8,17 +8,19 @@ html, body {
:root {
color-scheme: light dark;
- --background-color: light-dark(white, rgb(0, 0, 0));
+ --background-color: light-dark(white, rgb(31, 33, 36));
--text-color: light-dark(black, white);
--error-color: light-dark(rgb(163, 0, 0), rgb(255, 82, 82));
--error-bg-color: light-dark(lightpink, rgb(75, 0, 0));
--error-hover-bg-color: light-dark(rgb(102, 0, 0), rgb(238, 153, 153));
--light-accent-color: light-dark(rgba(0,0,255,0.2), rgba(78, 186, 248, 0.377));
--accent-border-color: light-dark(blue, rgb(64, 185, 255));
+ --accent-opaque-color: rgba(78, 186, 248, 1);
--separator-color: light-dark(lightgrey, rgb(44, 44, 44));
--inactive-bg-color: light-dark(#f7f7f7, rgb(29, 29, 29));
--inactive-fg-color: light-dark(grey, rgb(70, 70, 70));
- --button-bg-color: light-dark(#fcfcfc, rgb(20, 20, 20));
+ --button-bg-color: light-dark(#fcfcfc, rgb(44, 50, 63));
+ --textbox-bg-color: light-dark(white, rgb(36, 41, 40));
--modal-backdrop-color: light-dark(rgba(200,200,200,0.7), rgba(23, 22, 32, 0.849));
--status-inactive-color: light-dark(grey, grey);
--status-ok-color: light-dark(forestgreen, forestgreen);
@@ -31,14 +33,23 @@ html, body {
--firing-transition-color: light-dark(rgba(255, 128, 9, 1), rgba(255, 128, 9, 1));
--associated-color: light-dark(green, rgb(186, 245, 119));
--bottom-panel-bg-color: light-dark(rgb(255, 249, 235), rgb(24, 40, 70));
- --summary-hover-bg-color: light-dark(#eee, #242424);
+ --summary-hover-bg-color: light-dark(#eee, #313131);
--internal-event-bg-color: light-dark(rgb(255, 218, 252), rgb(99, 27, 94));
--input-event-bg-color: light-dark(rgb(224, 247, 209), rgb(59, 95, 37));
--input-event-hover-bg-color: light-dark(rgb(195, 224, 176), rgb(59, 88, 40));
--input-event-active-bg-color: light-dark(rgb(176, 204, 158), rgb(77, 117, 53));
--output-event-bg-color: light-dark(rgb(230, 249, 255), rgb(28, 83, 104));
+
+ background-color: var(--background-color);
+ color: var(--text-color);
}
+input {
+ background-color: var(--textbox-bg-color);
+ border: 1px solid var(--separator-color);
+}
+
+
div#root {
height: 100%;
}
@@ -63,7 +74,7 @@ kbd:active { transform: translateY(1px); }
input {
/* border: solid blue 2px; */
- accent-color: var(--light-accent-color);
+ accent-color: var(--accent-opaque-color);
}
::selection {
From c825830a143a4c6b77b56189905a973281a16565 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 22:09:49 +0100
Subject: [PATCH 20/30] better looking checkboxes
---
src/index.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/index.css b/src/index.css
index 14779eb..67e5114 100644
--- a/src/index.css
+++ b/src/index.css
@@ -15,7 +15,7 @@ html, body {
--error-hover-bg-color: light-dark(rgb(102, 0, 0), rgb(238, 153, 153));
--light-accent-color: light-dark(rgba(0,0,255,0.2), rgba(78, 186, 248, 0.377));
--accent-border-color: light-dark(blue, rgb(64, 185, 255));
- --accent-opaque-color: rgba(78, 186, 248, 1);
+ --accent-opaque-color: light-dark(#ccccff, #305b73);
--separator-color: light-dark(lightgrey, rgb(44, 44, 44));
--inactive-bg-color: light-dark(#f7f7f7, rgb(29, 29, 29));
--inactive-fg-color: light-dark(grey, rgb(70, 70, 70));
From dc948629a70963c30ba3fc6a1e58307a6bbfe746 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 22:27:13 +0100
Subject: [PATCH 21/30] fix history color in dark mode + flip colors of and/or
---
src/App/VisualEditor/HistorySVG.tsx | 7 +++++--
src/App/VisualEditor/hooks/useMouse.tsx | 3 ++-
src/index.css | 4 ++--
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/App/VisualEditor/HistorySVG.tsx b/src/App/VisualEditor/HistorySVG.tsx
index f0fccb4..565e9ac 100644
--- a/src/App/VisualEditor/HistorySVG.tsx
+++ b/src/App/VisualEditor/HistorySVG.tsx
@@ -9,8 +9,10 @@ export const HistorySVG = memo(function HistorySVG(props: {uid: string, topLeft:
cx={props.topLeft.x+HISTORY_RADIUS}
cy={props.topLeft.y+HISTORY_RADIUS}
r={HISTORY_RADIUS}
- fill="white"
- stroke="black"
+ style={{
+ fill: 'var(--and-state-bg-color)',
+ stroke: 'var(--rountangle-stroke-color)'
+ }}
strokeWidth={2}
data-uid={props.uid}
data-parts="history"
@@ -20,6 +22,7 @@ export const HistorySVG = memo(function HistorySVG(props: {uid: string, topLeft:
y={props.topLeft.y+HISTORY_RADIUS+5}
textAnchor="middle"
fontWeight={500}
+ style={{fill: 'var(--rountangle-stroke-color)'}}
>{text}
void, insertMode: InsertMode, zoo
}
else {
+ // @ts-ignore
newSelection.push({uid, part});
}
}
@@ -291,7 +292,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
}
// console.log({newSelection, oldSelection, common});
// return [...oldSelection, ...newSelection];
- return newSelection;xxxxxxxx
+ return newSelection;
})
}
}
diff --git a/src/index.css b/src/index.css
index 67e5114..ac053c8 100644
--- a/src/index.css
+++ b/src/index.css
@@ -24,8 +24,8 @@ html, body {
--modal-backdrop-color: light-dark(rgba(200,200,200,0.7), rgba(23, 22, 32, 0.849));
--status-inactive-color: light-dark(grey, grey);
--status-ok-color: light-dark(forestgreen, forestgreen);
- --or-state-bg-color: light-dark(#eee, #1a1a1a);
- --and-state-bg-color: light-dark(white, rgb(0, 0, 0));
+ --or-state-bg-color: light-dark(#eee, #000000);
+ --and-state-bg-color: light-dark(white, rgb(46, 46, 46));
--rountangle-stroke-color: light-dark(black, #d4d4d4);
--active-state-bg-color: light-dark(rgb(255, 240, 214), rgb(53, 37, 18));
--active-state-border-color: light-dark(rgb(205, 133, 0), rgb(235, 124, 21));
From 74361eb162280d3b05cf1ca3007e9495191bed76 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 22:47:10 +0100
Subject: [PATCH 22/30] accidentally committed debug endpoint for property
checking
---
src/App/SideBar/check_property.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/App/SideBar/check_property.ts b/src/App/SideBar/check_property.ts
index f0c779e..24a41cb 100644
--- a/src/App/SideBar/check_property.ts
+++ b/src/App/SideBar/check_property.ts
@@ -2,8 +2,8 @@ import { RT_Statechart } from "@/statecharts/runtime_types";
import { Plant } from "../Plant/Plant";
import { TraceItem } from "../hooks/useSimulator";
-const endpoint = "http://localhost:15478/check_property";
-// const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
+// const endpoint = "http://localhost:15478/check_property";
+const endpoint = "https://deemz.org/apis/mtl-aas/check_property";
export type PropertyTrace = {
timestamp: number,
From bdc2a66b1cb9e21f912fa167bef08de3b525221d Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 22:49:16 +0100
Subject: [PATCH 23/30] remove debug output
---
src/App/SideBar/check_property.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/App/SideBar/check_property.ts b/src/App/SideBar/check_property.ts
index 24a41cb..54d3a5e 100644
--- a/src/App/SideBar/check_property.ts
+++ b/src/App/SideBar/check_property.ts
@@ -52,8 +52,6 @@ export async function checkProperty(plant: Plant, property:
}
}
- console.log({cleanPlantStates, traces});
-
try {
const response = await fetch(endpoint, {
method: "POST",
From 632cf9b542e3dfcf256eaad8ea3129d8a058e8a9 Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Thu, 13 Nov 2025 23:56:52 +0100
Subject: [PATCH 24/30] progress bar shows how long until next timer elapse
---
src/App/App.tsx | 1 -
src/App/TopPanel/SpeedControl.tsx | 4 +++-
src/App/TopPanel/TopPanel.tsx | 32 ++++++++++++++++++++++++-------
3 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/src/App/App.tsx b/src/App/App.tsx
index bd89a9b..9faad11 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -17,7 +17,6 @@ import { useUrlHashState } from "../hooks/useUrlHashState";
import { plants } from "./plants";
import { emptyState } from "@/statecharts/concrete_syntax";
import { ModalOverlay } from "./Modals/ModalOverlay";
-import { usePersistentState } from "@/hooks/usePersistentState";
export type EditHistory = {
current: VisualEditorState,
diff --git a/src/App/TopPanel/SpeedControl.tsx b/src/App/TopPanel/SpeedControl.tsx
index 9e936af..0343d1a 100644
--- a/src/App/TopPanel/SpeedControl.tsx
+++ b/src/App/TopPanel/SpeedControl.tsx
@@ -2,6 +2,8 @@ import { Dispatch, memo, SetStateAction, useCallback, useEffect } from "react";
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
import { setRealtime, TimeMode } from "@/statecharts/time";
+import SpeedIcon from '@mui/icons-material/Speed';
+
export const SpeedControl = memo(function SpeedControl({showKeys, timescale, setTimescale, setTime}: {showKeys: boolean, timescale: number, setTimescale: Dispatch>, setTime: Dispatch>}) {
const onTimeScaleChange = useCallback((newValue: string, wallclktime: number) => {
@@ -51,7 +53,7 @@ export const SpeedControl = memo(function SpeedControl({showKeys, timescale, set
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
return <>
- speed
+
S}>
÷2
diff --git a/src/App/TopPanel/TopPanel.tsx b/src/App/TopPanel/TopPanel.tsx
index 7e922c4..1974f83 100644
--- a/src/App/TopPanel/TopPanel.tsx
+++ b/src/App/TopPanel/TopPanel.tsx
@@ -1,4 +1,4 @@
-import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useState } from "react";
+import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
import { TimerElapseEvent, Timers } from "../../statecharts/runtime_types";
import { getSimTime, setPaused, setRealtime, TimeMode } from "../../statecharts/time";
import { InsertMode } from "./InsertModes";
@@ -13,6 +13,9 @@ import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import BrightnessAutoIcon from '@mui/icons-material/BrightnessAuto';
+import SpeedIcon from '@mui/icons-material/Speed';
+import AccessTimeIcon from '@mui/icons-material/AccessTime';
+
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
import CachedIcon from '@mui/icons-material/Cached';
import InfoOutlineIcon from '@mui/icons-material/InfoOutline';
@@ -52,7 +55,7 @@ export type TopPanelProps = {
const ShortCutShowKeys = ~ ;
export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}: TopPanelProps) {
- const [displayTime, setDisplayTime] = useState("0.000");
+ const [displayTime, setDisplayTime] = useState(0);
const [timescale, setTimescale] = usePersistentState("timescale", 1);
const config = trace && trace.trace[trace.idx];
@@ -62,9 +65,16 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
const updateDisplayedTime = useCallback(() => {
const now = Math.round(performance.now());
const timeMs = getSimTime(time, now);
- setDisplayTime(formatTime(timeMs));
+ setDisplayTime((timeMs));
}, [time, setDisplayTime]);
+ const formattedDisplayTime = useMemo(() => formatTime(displayTime), [displayTime]);
+
+ // const lastSimTime = useMemo(() => time.kind === "realtime" ? time.since.simtime : time.simtime, [time]);
+
+ const lastSimTime = config?.simtime || 0;
+
+
useEffect(() => {
// This has no effect on statechart execution. In between events, the statechart is doing nothing. However, by updating the displayed time, we give the illusion of continuous progress.
const interval = setInterval(() => {
@@ -105,6 +115,9 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
}
}, [nextTimedTransition, setTime]);
+
+ console.log({lastSimTime, displayTime, nxt: nextTimedTransition?.[0]});
+
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
// don't capture keyboard events when focused on an input element:
@@ -228,15 +241,20 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
{/* time, next */}
+
From 8fa430846ba1abc3e06f0321c005c2eb3814b97b Mon Sep 17 00:00:00 2001
From: Joeri Exelmans
Date: Fri, 14 Nov 2025 09:27:19 +0100
Subject: [PATCH 25/30] edit css, make microwave background transparent
---
src/App/BottomPanel/BottomPanel.tsx | 2 +-
src/{ => App}/Logo/Logo.tsx | 0
src/App/Modals/About.tsx | 2 +-
src/App/Modals/TextDialog.tsx | 10 ++++++----
src/App/Plant/Microwave/Microwave.tsx | 2 +-
.../Plant/Microwave/originals/microwave.xcf | Bin 0 -> 802965 bytes
.../transparent_small_closed_off.png | Bin 0 -> 76659 bytes
.../originals/transparent_small_closed_on.png | Bin 0 -> 88287 bytes
.../transparent_small_opened_off.png | Bin 0 -> 81557 bytes
.../originals/transparent_small_opened_on.png | Bin 0 -> 70135 bytes
src/App/Plant/Microwave/small_closed_off.webp | Bin 17958 -> 16836 bytes
src/App/Plant/Microwave/small_closed_on.webp | Bin 10498 -> 10476 bytes
src/App/Plant/Microwave/small_opened_off.webp | Bin 13076 -> 15386 bytes
src/App/Plant/Microwave/small_opened_on.webp | Bin 12594 -> 15176 bytes
src/index.css | 2 +-
15 files changed, 10 insertions(+), 8 deletions(-)
rename src/{ => App}/Logo/Logo.tsx (100%)
create mode 100644 src/App/Plant/Microwave/originals/microwave.xcf
create mode 100644 src/App/Plant/Microwave/originals/transparent_small_closed_off.png
create mode 100644 src/App/Plant/Microwave/originals/transparent_small_closed_on.png
create mode 100644 src/App/Plant/Microwave/originals/transparent_small_opened_off.png
create mode 100644 src/App/Plant/Microwave/originals/transparent_small_opened_on.png
diff --git a/src/App/BottomPanel/BottomPanel.tsx b/src/App/BottomPanel/BottomPanel.tsx
index 51dcd81..ad1cd93 100644
--- a/src/App/BottomPanel/BottomPanel.tsx
+++ b/src/App/BottomPanel/BottomPanel.tsx
@@ -4,7 +4,7 @@ import { TraceableError } from "../../statecharts/parser";
import "./BottomPanel.css";
import { PersistentDetailsLocalStorage } from "../PersistentDetails";
-import { Logo } from "@/Logo/Logo";
+import { Logo } from "@/App/Logo/Logo";
export function BottomPanel(props: {errors: TraceableError[]}) {
const [greeting, setGreeting] = useState(
diff --git a/src/Logo/Logo.tsx b/src/App/Logo/Logo.tsx
similarity index 100%
rename from src/Logo/Logo.tsx
rename to src/App/Logo/Logo.tsx
diff --git a/src/App/Modals/About.tsx b/src/App/Modals/About.tsx
index b270b8e..cd0605b 100644
--- a/src/App/Modals/About.tsx
+++ b/src/App/Modals/About.tsx
@@ -1,4 +1,4 @@
-import { Logo } from "@/Logo/Logo";
+import { Logo } from "@/App/Logo/Logo";
import { Dispatch, ReactElement, SetStateAction } from "react";
export function About(props: {setModal: Dispatch>}) {
diff --git a/src/App/Modals/TextDialog.tsx b/src/App/Modals/TextDialog.tsx
index a478877..6ab731b 100644
--- a/src/App/Modals/TextDialog.tsx
+++ b/src/App/Modals/TextDialog.tsx
@@ -29,12 +29,14 @@ export function TextDialog(props: {setModal: Dispatch
- Text label:
+ {/* Text label: */}
;
}
\ No newline at end of file
diff --git a/src/App/Plant/Microwave/Microwave.tsx b/src/App/Plant/Microwave/Microwave.tsx
index bc50993..078aeb2 100644
--- a/src/App/Plant/Microwave/Microwave.tsx
+++ b/src/App/Plant/Microwave/Microwave.tsx
@@ -92,7 +92,7 @@ export const Microwave = memo(function Microwave({state: {bellRinging, magnetron
src: url(${fontDigital});
}
`}
-
+
{/* @ts-ignore */}
diff --git a/src/App/Plant/Microwave/originals/microwave.xcf b/src/App/Plant/Microwave/originals/microwave.xcf
new file mode 100644
index 0000000000000000000000000000000000000000..b0c8c6d5f93a1b8eaec844d74a6856c011313f94
GIT binary patch
literal 802965
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