cleanup code a bit more

This commit is contained in:
Joeri Exelmans 2025-11-12 14:34:46 +01:00
parent 07b51dd2f2
commit 1f72542234
25 changed files with 146 additions and 122 deletions

View file

@ -5,16 +5,18 @@ import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from
import { detectConnections } from "@/statecharts/detect_connections"; import { detectConnections } from "@/statecharts/detect_connections";
import { parseStatechart } from "../statecharts/parser"; import { parseStatechart } from "../statecharts/parser";
import { BottomPanel } from "./BottomPanel"; import { BottomPanel } from "./BottomPanel/BottomPanel";
import { defaultSideBarState, SideBar, SideBarState } from "./SideBar"; import { defaultSideBarState, SideBar, SideBarState } from "./SideBar/SideBar";
import { InsertMode } from "./TopPanel/InsertModes"; import { InsertMode } from "./TopPanel/InsertModes";
import { TopPanel } from "./TopPanel/TopPanel"; import { TopPanel } from "./TopPanel/TopPanel";
import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
import { makeIndividualSetters } from "./makePartialSetter"; import { makeAllSetters } from "./makePartialSetter";
import { useEditor } from "./useEditor"; import { useEditor } from "./hooks/useEditor";
import { useSimulator } from "./useSimulator"; import { useSimulator } from "./hooks/useSimulator";
import { useUrlHashState } from "./useUrlHashState"; import { useUrlHashState } from "../hooks/useUrlHashState";
import { plants } from "./plants"; import { plants } from "./plants";
import { emptyState } from "@/statecharts/concrete_syntax";
import { ModalOverlay } from "./Modals/ModalOverlay";
export type EditHistory = { export type EditHistory = {
current: VisualEditorState, current: VisualEditorState,
@ -56,9 +58,12 @@ export function App() {
const persist = useUrlHashState<VisualEditorState | AppState & {editorState: VisualEditorState}>( const persist = useUrlHashState<VisualEditorState | AppState & {editorState: VisualEditorState}>(
recoveredState => { recoveredState => {
if (recoveredState === null) {
setEditHistory(() => ({current: emptyState, history: [], future: []}));
}
// we support two formats // we support two formats
// @ts-ignore // @ts-ignore
if (recoveredState.nextID) { else if (recoveredState.nextID) {
// old format // old format
setEditHistory(() => ({current: recoveredState as VisualEditorState, history: [], future: []})); setEditHistory(() => ({current: recoveredState as VisualEditorState, history: [], future: []}));
} }
@ -77,6 +82,7 @@ export function App() {
useEffect(() => { useEffect(() => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
if (editorState !== null) { if (editorState !== null) {
console.log('persisting state to url');
persist({editorState, ...appState}); persist({editorState, ...appState});
} }
}, 100); }, 100);
@ -104,7 +110,15 @@ export function App() {
const simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar); 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 syntaxErrors = parsed && parsed[1] || [];
const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx]; const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx];
@ -121,19 +135,7 @@ export function App() {
const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1]; const plantState = currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1];
return <> return <ModalOverlay modal={modal} setModal={setModal}>
{/* Modal dialog */}
{modal && <div
className="modalOuter"
onMouseDown={() => setModal(null)}>
<div className="modalInner">
<span onMouseDown={e => e.stopPropagation()}>
{modal}
</span>
</div>
</div>}
{/* top-to-bottom: everything -> bottom panel */} {/* top-to-bottom: everything -> bottom panel */}
<div className="stackVertical" style={{height:'100%'}}> <div className="stackVertical" style={{height:'100%'}}>
@ -177,7 +179,7 @@ export function App() {
{syntaxErrors && <BottomPanel {...{errors: syntaxErrors}}/>} {syntaxErrors && <BottomPanel {...{errors: syntaxErrors}}/>}
</div> </div>
</div> </div>
</>; </ModalOverlay>;
} }
export default App; export default App;

View file

@ -1,10 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { TraceableError } from "../statecharts/parser"; import { TraceableError } from "../../statecharts/parser";
import "./BottomPanel.css"; import "./BottomPanel.css";
import logo from "../../artwork/logo-playful.svg"; import logo from "../../../artwork/logo-playful.svg";
import { PersistentDetailsLocalStorage } from "./PersistentDetails"; import { PersistentDetailsLocalStorage } from "../PersistentDetails";
export function BottomPanel(props: {errors: TraceableError[]}) { export function BottomPanel(props: {errors: TraceableError[]}) {
const [greeting, setGreeting] = useState( const [greeting, setGreeting] = useState(

View file

@ -0,0 +1,17 @@
import { Dispatch, PropsWithChildren, ReactElement, SetStateAction } from "react";
export function ModalOverlay(props: PropsWithChildren<{modal: ReactElement|null, setModal: Dispatch<SetStateAction<ReactElement|null>>}>) {
return <>
{props.modal && <div
className="modalOuter"
onMouseDown={() => props.setModal(null)}>
<div className="modalInner">
<span onMouseDown={e => e.stopPropagation()}>
{props.modal}
</span>
</div>
</div>}
{props.children}
</>;
}

View file

@ -1,4 +1,4 @@
import { usePersistentState } from "@/App/persistent_state" import { usePersistentState } from "@/hooks/usePersistentState"
import { DetailsHTMLAttributes, Dispatch, PropsWithChildren, SetStateAction } from "react"; import { DetailsHTMLAttributes, Dispatch, PropsWithChildren, SetStateAction } from "react";
type Props = { type Props = {

View file

@ -1,4 +1,4 @@
import { useAudioContext } from "@/App/useAudioContext"; import { useAudioContext } from "@/hooks/useAudioContext";
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor"; import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
import { detectConnections } from "@/statecharts/detect_connections"; import { detectConnections } from "@/statecharts/detect_connections";
import { parseStatechart } from "@/statecharts/parser"; import { parseStatechart } from "@/statecharts/parser";

View file

@ -12,7 +12,7 @@ import { RT_Statechart } from "@/statecharts/runtime_types";
import { memo, useEffect } from "react"; import { memo, useEffect } from "react";
import "./Microwave.css"; import "./Microwave.css";
import { useAudioContext } from "../../useAudioContext"; import { useAudioContext } from "../../../hooks/useAudioContext";
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant"; import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
import { detectConnections } from "@/statecharts/detect_connections"; import { detectConnections } from "@/statecharts/detect_connections";
import { parseStatechart } from "@/statecharts/parser"; import { parseStatechart } from "@/statecharts/parser";

View file

@ -3,7 +3,6 @@ import { Statechart } from "@/statecharts/abstract_syntax";
import { EventTrigger } from "@/statecharts/label_ast"; import { EventTrigger } from "@/statecharts/label_ast";
import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types"; import { BigStep, RaisedEvent, RT_Statechart } from "@/statecharts/runtime_types";
import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive"; import { statechartExecution, TimedReactive } from "@/statecharts/timed_reactive";
import { setsEqual } from "@/util/util";
export type PlantRenderProps<StateType> = { export type PlantRenderProps<StateType> = {
state: StateType, state: StateType,

View file

@ -13,7 +13,7 @@ import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
import { detectConnections } from "@/statecharts/detect_connections"; import { detectConnections } from "@/statecharts/detect_connections";
import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant"; import { makeStatechartPlant, PlantRenderProps, StatechartPlantSpec } from "../Plant";
import { RT_Statechart } from "@/statecharts/runtime_types"; import { RT_Statechart } from "@/statecharts/runtime_types";
import { useAudioContext } from "@/App/useAudioContext"; import { useAudioContext } from "@/hooks/useAudioContext";
import { memo, useEffect } from "react"; import { memo, useEffect } from "react";
import { objectsEqual } from "@/util/util"; import { objectsEqual } from "@/util/util";

View file

@ -1,10 +1,10 @@
import { Dispatch, memo, SetStateAction, useCallback } from "react"; import { Dispatch, memo, SetStateAction, useCallback } from "react";
import { Statechart, stateDescription } from "../statecharts/abstract_syntax"; import { Statechart, stateDescription } from "../../statecharts/abstract_syntax";
import { Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types"; import { Mode, RaisedEvent, RT_Event } from "../../statecharts/runtime_types";
import { formatTime } from "../util/util"; import { formatTime } from "../../util/util";
import { TimeMode, timeTravel } from "../statecharts/time"; import { TimeMode, timeTravel } from "../../statecharts/time";
import { BigStepCause, TraceItem, TraceState } from "./App";
import { Environment } from "@/statecharts/environment"; import { Environment } from "@/statecharts/environment";
import { BigStepCause, TraceItem, TraceState } from "../hooks/useSimulator";
type RTHistoryProps = { type RTHistoryProps = {
trace: TraceState|null, trace: TraceState|null,

View file

@ -1,7 +1,9 @@
import { ConcreteState, UnstableState, stateDescription, Transition } from "../statecharts/abstract_syntax"; import BoltIcon from '@mui/icons-material/Bolt';
import { Action, EventTrigger, Expression } from "../statecharts/label_ast"; import { memo, useEffect } from "react";
import { usePersistentState } from "../../hooks/usePersistentState";
import "./AST.css"; 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}) { export function ShowTransition(props: {transition: Transition}) {
return <> {stateDescription(props.transition.tgt)}</>; return <> {stateDescription(props.transition.tgt)}</>;
@ -46,10 +48,6 @@ export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | Unst
</li>; </li>;
}); });
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}) { export function ShowInputEvents({inputEvents, onRaise, disabled, showKeys}: {inputEvents: EventTrigger[], onRaise: (e: string, p: any) => void, disabled: boolean, showKeys: boolean}) {
const raiseHandlers = inputEvents.map(({event}) => { const raiseHandlers = inputEvents.map(({event}) => {

View file

@ -8,14 +8,15 @@ import { Conns } from '@/statecharts/timed_reactive';
import { Dispatch, Ref, SetStateAction, useEffect, useRef, useState } from 'react'; import { Dispatch, Ref, SetStateAction, useEffect, useRef, useState } from 'react';
import { Statechart } from '@/statecharts/abstract_syntax'; import { Statechart } from '@/statecharts/abstract_syntax';
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from './ShowAST'; import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from './ShowAST';
import { Plant } from './Plant/Plant'; import { Plant } from '../Plant/Plant';
import { checkProperty, PropertyCheckResult } from './check_property'; import { checkProperty, PropertyCheckResult } from './check_property';
import { Setters } from './makePartialSetter'; import { Setters } from '../makePartialSetter';
import { RTHistory } from './RTHistory'; import { RTHistory } from './RTHistory';
import { BigStepCause, TraceState } from './useSimulator'; import { BigStepCause, TraceState } from '../hooks/useSimulator';
import { plants, UniversalPlantState } from './plants'; import { plants, UniversalPlantState } from '../plants';
import { TimeMode } from '@/statecharts/time'; import { TimeMode } from '@/statecharts/time';
import { PersistentDetails } from './PersistentDetails'; import { PersistentDetails } from '../PersistentDetails';
import "./SideBar.css";
type SavedTraces = [string, BigStepCause[]][]; type SavedTraces = [string, BigStepCause[]][];

View file

@ -1,6 +1,6 @@
import { RT_Statechart } from "@/statecharts/runtime_types"; import { RT_Statechart } from "@/statecharts/runtime_types";
import { Plant } from "./Plant/Plant"; import { Plant } from "../Plant/Plant";
import { TraceItem } from "./useSimulator"; import { TraceItem } from "../hooks/useSimulator";
// const endpoint = "http://localhost:15478/check_property"; // const endpoint = "http://localhost:15478/check_property";
const endpoint = "https://deemz.org/apis/mtl-aas/check_property"; const endpoint = "https://deemz.org/apis/mtl-aas/check_property";

View file

@ -18,10 +18,10 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import SkipNextIcon from '@mui/icons-material/SkipNext'; import SkipNextIcon from '@mui/icons-material/SkipNext';
import StopIcon from '@mui/icons-material/Stop'; import StopIcon from '@mui/icons-material/Stop';
import { InsertModes } from "./InsertModes"; import { InsertModes } from "./InsertModes";
import { usePersistentState } from "@/App/persistent_state"; import { usePersistentState } from "@/hooks/usePersistentState";
import { RotateButtons } from "./RotateButtons"; import { RotateButtons } from "./RotateButtons";
import { SpeedControl } from "./SpeedControl"; import { SpeedControl } from "./SpeedControl";
import { TraceState } from "../useSimulator"; import { TraceState } from "../hooks/useSimulator";
export type TopPanelProps = { export type TopPanelProps = {
trace: TraceState | null, trace: TraceState | null,

View file

@ -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 { Mode } from "@/statecharts/runtime_types";
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util"; import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
import { Arrow, ArrowPart, Diamond, History, RectSide, Rountangle, Text } from "../../statecharts/concrete_syntax"; import { Arrow, ArrowPart, Diamond, History, RectSide, Rountangle, Text } from "../../statecharts/concrete_syntax";
import { Connections } from "../../statecharts/detect_connections"; import { Connections } from "../../statecharts/detect_connections";
import { TraceableError } from "../../statecharts/parser"; import { TraceableError } from "../../statecharts/parser";
import { ArcDirection, arcDirection } from "../../util/geometry"; import { ArcDirection, arcDirection } from "../../util/geometry";
import { InsertMode } from "../TopPanel/InsertModes";
import { ArrowSVG } from "./ArrowSVG"; import { ArrowSVG } from "./ArrowSVG";
import { DiamondSVG } from "./DiamondSVG"; import { DiamondSVG } from "./DiamondSVG";
import { HistorySVG } from "./HistorySVG"; import { HistorySVG } from "./HistorySVG";
import { RountangleSVG } from "./RountangleSVG"; import { RountangleSVG } from "./RountangleSVG";
import { TextSVG } from "./TextSVG"; import { TextSVG } from "./TextSVG";
import { useCopyPaste } from "./useCopyPaste";
import "./VisualEditor.css"; import "./VisualEditor.css";
import { useMouse } from "./useMouse"; import { useCopyPaste } from "./hooks/useCopyPaste";
import { useMouse } from "./hooks/useMouse";
export type ConcreteSyntax = { export type ConcreteSyntax = {
rountangles: Rountangle[]; rountangles: Rountangle[];

View file

@ -1,6 +1,6 @@
import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concrete_syntax"; import { Arrow, Diamond, Rountangle, Text, History } from "@/statecharts/concrete_syntax";
import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react"; import { ClipboardEvent, Dispatch, SetStateAction, useCallback, useEffect } from "react";
import { Selection, VisualEditorState } from "./VisualEditor"; import { Selection, VisualEditorState } from "../VisualEditor";
import { addV2D } from "@/util/geometry"; import { addV2D } from "@/util/geometry";
// const offset = {x: 40, y: 40}; // const offset = {x: 40, y: 40};

View file

@ -2,10 +2,10 @@ import { rountangleMinSize } from "@/statecharts/concrete_syntax";
import { addV2D, area, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "@/util/geometry"; import { addV2D, area, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "@/util/geometry";
import { getBBoxInSvgCoords } from "@/util/svg_helper"; import { getBBoxInSvgCoords } from "@/util/svg_helper";
import { Dispatch, useCallback, useEffect, useState } from "react"; import { Dispatch, useCallback, useEffect, useState } from "react";
import { MIN_ROUNTANGLE_SIZE } from "../parameters"; import { MIN_ROUNTANGLE_SIZE } from "../../parameters";
import { InsertMode } from "../TopPanel/InsertModes"; import { InsertMode } from "../../TopPanel/InsertModes";
import { Selecting, SelectingState } from "./Selection"; import { Selecting, SelectingState } from "../Selection";
import { Selection, VisualEditorState } from "./VisualEditor"; 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) { 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 [dragging, setDragging] = useState(false);

View file

@ -1,8 +1,7 @@
import { addV2D, rotateLine90CCW, rotateLine90CW, rotatePoint90CCW, rotatePoint90CW, rotateRect90CCW, rotateRect90CW, scaleV2D, subtractV2D, Vec2D } from "@/util/geometry"; 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 { Dispatch, SetStateAction, useCallback, useEffect } from "react";
import { EditHistory } from "./App"; import { EditHistory } from "../App";
import { VisualEditorState } from "./VisualEditor/VisualEditor";
export function useEditor(setEditHistory: Dispatch<SetStateAction<EditHistory|null>>) { export function useEditor(setEditHistory: Dispatch<SetStateAction<EditHistory|null>>) {
useEffect(() => { useEffect(() => {

View file

@ -3,9 +3,9 @@ import { RuntimeError } from "@/statecharts/interpreter";
import { BigStep, RaisedEvent } from "@/statecharts/runtime_types"; import { BigStep, RaisedEvent } from "@/statecharts/runtime_types";
import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive"; import { Conns, coupledExecution, statechartExecution } from "@/statecharts/timed_reactive";
import { useCallback, useEffect, useMemo, useState } from "react"; 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 { getSimTime, getWallClkDelay, TimeMode } from "@/statecharts/time";
import { UniversalPlantState } from "./plants"; import { UniversalPlantState } from "../plants";
type CoupledState = { type CoupledState = {
sc: BigStep, sc: BigStep,
@ -107,6 +107,7 @@ export function useSimulator(ast: Statechart|null, plant: Plant<any, UniversalPl
// timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout) // timer elapse events are triggered by a change of the simulated time (possibly as a scheduled JS event loop timeout)
useEffect(() => { useEffect(() => {
// console.log('time effect:', time, currentTraceItem);
let timeout: NodeJS.Timeout | undefined; let timeout: NodeJS.Timeout | undefined;
if (currentTraceItem !== null && cE !== null) { if (currentTraceItem !== null && cE !== null) {
if (currentTraceItem.kind === "bigstep") { if (currentTraceItem.kind === "bigstep") {

View file

@ -3,16 +3,16 @@ import { Dispatch, SetStateAction, useCallback, useMemo } from "react";
export function makePartialSetter<T, K extends keyof T>(fullSetter: Dispatch<SetStateAction<T>>, key: K): Dispatch<SetStateAction<T[typeof key]>> { export function makePartialSetter<T, K extends keyof T>(fullSetter: Dispatch<SetStateAction<T>>, key: K): Dispatch<SetStateAction<T[typeof key]>> {
return (newValueOrCallback: T[K] | ((newValue: T[K]) => T[K])) => { return (newValueOrCallback: T[K] | ((newValue: T[K]) => T[K])) => {
fullSetter(oldFullValue => { 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 { return {
...oldFullValue, ...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<T extends {[key: string]: any}> = {
[K in keyof T as `set${Capitalize<Extract<K, string>>}`]: Dispatch<SetStateAction<T[K]>>; [K in keyof T as `set${Capitalize<Extract<K, string>>}`]: Dispatch<SetStateAction<T[K]>>;
} }
export function makeIndividualSetters<T extends {[key: string]: any}>( export function makeAllSetters<T extends {[key: string]: any}>(
fullSetter: Dispatch<SetStateAction<T>>, fullSetter: Dispatch<SetStateAction<T>>,
keys: (keyof T)[], keys: (keyof T)[],
): Setters<T> { ): Setters<T> {
// @ts-ignore // @ts-ignore
return useMemo(() => return useMemo(() => {
console.log('creating setters for App');
// @ts-ignore // @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)]; return [`set${key.charAt(0).toUpperCase()}${key.slice(1)}`, makePartialSetter(fullSetter, key)];
})), }));
[fullSetter] }, [fullSetter]);
);
} }

View file

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

View file

@ -1,17 +1,17 @@
import { useEffect } from "react"; import { useEffect, useLayoutEffect } from "react";
// persist state in URL hash // persist state in URL hash
export function useUrlHashState<T>(recoverCallback: (recoveredState: T) => void): (toPersist: T) => void { export function useUrlHashState<T>(recoverCallback: (recoveredState: (T|null)) => void): (toPersist: T) => void {
// recover editor state from URL - we need an effect here because decompression is asynchronous // 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...'); console.log('recovering state...');
const compressedState = window.location.hash.slice(1); const compressedState = window.location.hash.slice(1);
if (compressedState.length === 0) { if (compressedState.length === 0) {
// empty URL hash // empty URL hash
console.log("no state to recover"); console.log("no state to recover");
// setEditHistory(() => ({current: emptyState, history: [], future: []})); return recoverCallback(null);
return;
} }
let compressedBuffer; let compressedBuffer;
try { try {
@ -19,8 +19,7 @@ export function useUrlHashState<T>(recoverCallback: (recoveredState: T) => void)
} catch (e) { } catch (e) {
// probably invalid base64 // probably invalid base64
console.error("failed to recover state:", e); console.error("failed to recover state:", e);
// setEditHistory(() => ({current: emptyState, history: [], future: []})); return recoverCallback(null);
return;
} }
const ds = new DecompressionStream("deflate"); const ds = new DecompressionStream("deflate");
const writer = ds.writable.getWriter(); const writer = ds.writable.getWriter();
@ -29,12 +28,13 @@ export function useUrlHashState<T>(recoverCallback: (recoveredState: T) => void)
new Response(ds.readable).arrayBuffer() new Response(ds.readable).arrayBuffer()
.then(decompressedBuffer => { .then(decompressedBuffer => {
const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer)); const recoveredState = JSON.parse(new TextDecoder().decode(decompressedBuffer));
console.log('successfully recovered state');
recoverCallback(recoveredState); recoverCallback(recoveredState);
}) })
.catch(e => { .catch(e => {
// any other error: invalid JSON, or decompression failed. // any other error: invalid JSON, or decompression failed.
console.error("failed to recover state:", e); console.error("failed to recover state:", e);
// setEditHistory({current: emptyState, history: [], future: []}); recoverCallback(null);
}); });
}, []); }, []);