move the parsing from VisualEditor to App component

This commit is contained in:
Joeri Exelmans 2025-10-23 23:13:09 +02:00
parent 65b6a343d1
commit 4f9a546fd1
11 changed files with 107 additions and 116 deletions

View file

@ -1,8 +1,7 @@
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { emptyStatechart, Statechart, Transition } from "../statecharts/abstract_syntax";
import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter"; import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter";
import { BigStep, BigStepOutput, RT_Event } from "../statecharts/runtime_types"; import { BigStep, RT_Event } from "../statecharts/runtime_types";
import { InsertMode, VisualEditor, VisualEditorState } from "../VisualEditor/VisualEditor"; import { InsertMode, VisualEditor, VisualEditorState } from "../VisualEditor/VisualEditor";
import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time"; import { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
@ -13,7 +12,7 @@ import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { TopPanel } from "./TopPanel"; import { TopPanel } from "./TopPanel";
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST"; import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
import { TraceableError } from "../statecharts/parser"; import { parseStatechart } from "../statecharts/parser";
import { getKeyHandler } from "./shortcut_handler"; import { getKeyHandler } from "./shortcut_handler";
import { BottomPanel } from "./BottomPanel"; import { BottomPanel } from "./BottomPanel";
import { emptyState } from "@/statecharts/concrete_syntax"; import { emptyState } from "@/statecharts/concrete_syntax";
@ -23,6 +22,7 @@ import { DummyPlant } from "@/Plant/Dummy/Dummy";
import { Plant } from "@/Plant/Plant"; import { Plant } from "@/Plant/Plant";
import { usePersistentState } from "@/util/persistent_state"; import { usePersistentState } from "@/util/persistent_state";
import { RTHistory } from "./RTHistory"; import { RTHistory } from "./RTHistory";
import { detectConnections } from "@/statecharts/detect_connections";
export type EditHistory = { export type EditHistory = {
current: VisualEditorState, current: VisualEditorState,
@ -70,10 +70,8 @@ function getPlantState<T>(plant: Plant<T>, trace: TraceItem[], idx: number): T |
} }
export function App() { export function App() {
const [mode, setMode] = useState<InsertMode>("and"); const [insertMode, setInsertMode] = useState<InsertMode>("and");
const [historyState, setHistoryState] = useState<EditHistory>({current: emptyState, history: [], future: []}); const [historyState, setHistoryState] = useState<EditHistory>({current: emptyState, history: [], future: []});
const [ast, setAST] = useState<Statechart>(emptyStatechart);
const [errors, setErrors] = useState<TraceableError[]>([]);
const [trace, setTrace] = useState<TraceState|null>(null); const [trace, setTrace] = useState<TraceState|null>(null);
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0}); const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
const [modal, setModal] = useState<ReactElement|null>(null); const [modal, setModal] = useState<ReactElement|null>(null);
@ -91,6 +89,10 @@ export function App() {
const refRightSideBar = useRef<HTMLDivElement>(null); const refRightSideBar = useRef<HTMLDivElement>(null);
// parse concrete syntax always:
const conns = useMemo(() => detectConnections(editorState), [editorState]);
const [ast, syntaxErrors] = useMemo(() => parseStatechart(editorState, conns), [editorState, conns]);
// append editor state to undo history // append editor state to undo history
const makeCheckPoint = useCallback(() => { const makeCheckPoint = useCallback(() => {
setHistoryState(historyState => ({ setHistoryState(historyState => ({
@ -261,7 +263,7 @@ export function App() {
}, []); }, []);
useEffect(() => { useEffect(() => {
const onKeyDown = getKeyHandler(setMode); const onKeyDown = getKeyHandler(setInsertMode);
window.addEventListener("keydown", onKeyDown); window.addEventListener("keydown", onKeyDown);
return () => { return () => {
window.removeEventListener("keydown", onKeyDown); window.removeEventListener("keydown", onKeyDown);
@ -322,12 +324,12 @@ export function App() {
}} }}
> >
<TopPanel <TopPanel
{...{trace, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history: historyState}} {...{trace, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, history: historyState}}
/> />
</Box> </Box>
{/* Below the top bar: Editor */} {/* Below the top bar: Editor */}
<Box sx={{flexGrow:1, overflow: "auto"}}> <Box sx={{flexGrow:1, overflow: "auto"}}>
<VisualEditor {...{state: editorState, setState: setEditorState, ast, setAST, trace, setTrace, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}}/> <VisualEditor {...{state: editorState, setState: setEditorState, conns, trace, setTrace, syntaxErrors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}}/>
</Box> </Box>
</Stack> </Stack>
</Box> </Box>
@ -408,7 +410,7 @@ export function App() {
{/* Bottom panel */} {/* Bottom panel */}
<Box sx={{flex: '0 0 content'}}> <Box sx={{flex: '0 0 content'}}>
<BottomPanel {...{errors}}/> <BottomPanel {...{errors: syntaxErrors}}/>
</Box> </Box>
</Stack> </Stack>
</>; </>;

View file

@ -1,9 +1,9 @@
import { Dispatch, Ref, SetStateAction } from "react"; import { Dispatch, memo, Ref, SetStateAction, useCallback } from "react";
import { Statechart, stateDescription } from "../statecharts/abstract_syntax"; import { Statechart, stateDescription } from "../statecharts/abstract_syntax";
import { BigStep, Environment, Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types"; import { BigStep, Environment, Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types";
import { formatTime } from "./util"; import { formatTime } from "./util";
import { TimeMode } from "../statecharts/time"; import { TimeMode } from "../statecharts/time";
import { TraceState } from "./App"; import { TraceItem, TraceState } from "./App";
type RTHistoryProps = { type RTHistoryProps = {
trace: TraceState|null, trace: TraceState|null,
@ -13,52 +13,55 @@ type RTHistoryProps = {
} }
export function RTHistory({trace, setTrace, ast, setTime}: RTHistoryProps) { export function RTHistory({trace, setTrace, ast, setTime}: RTHistoryProps) {
function gotoRt(idx: number, timestamp: number) { const onMouseDown = useCallback((idx: number, timestamp: number) => {
setTrace(trace => trace && { setTrace(trace => trace && {
...trace, ...trace,
idx, idx,
}); });
setTime({kind: "paused", simtime: timestamp}); setTime({kind: "paused", simtime: timestamp});
} }, [setTrace, setTime]);
if (trace === null) { if (trace === null) {
return <></>; return <></>;
} }
return <div> return <div>
{trace.trace.map((item, i) => { {trace.trace.map((item, i) => <RTHistoryItem ast={ast} idx={i} item={item} prevItem={trace.trace[i-1]} active={i === trace.idx} onMouseDown={onMouseDown}/>)}
if (item.kind === "bigstep") {
const newStates = item.mode.difference(trace.trace[i-1]?.mode || new Set());
return <div
className={"runtimeState" + (i === trace.idx ? " active" : "")}
onMouseDown={() => gotoRt(i, item.simtime)}>
<div>
{formatTime(item.simtime)}
&emsp;
<div className="inputEvent">{item.inputEvent || "<init>"}</div>
</div>
<ShowMode mode={newStates} statechart={ast}/>
<ShowEnvironment environment={item.environment}/>
{item.outputEvents.length>0 && <>^
{item.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
</>}
</div>;
}
else {
return <div className="runtimeState runtimeError">
<div>
{formatTime(item.simtime)}
&emsp;
<div className="inputEvent">{item.inputEvent}</div>
</div>
<div>
{item.error.message}
</div>
</div>;
}
})}
</div>; </div>;
} }
export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevItem, active, onMouseDown}: {idx: number, ast: Statechart, item: TraceItem, prevItem?: TraceItem, active: boolean, onMouseDown: (idx: number, timestamp: number) => void}) {
if (item.kind === "bigstep") {
// @ts-ignore
const newStates = item.mode.difference(prevItem?.mode || new Set());
return <div
className={"runtimeState" + (active ? " active" : "")}
onMouseDown={useCallback(() => onMouseDown(idx, item.simtime), [idx, item.simtime])}>
<div>
{formatTime(item.simtime)}
&emsp;
<div className="inputEvent">{item.inputEvent || "<init>"}</div>
</div>
<ShowMode mode={newStates} statechart={ast}/>
<ShowEnvironment environment={item.environment}/>
{item.outputEvents.length>0 && <>^
{item.outputEvents.map((e:RaisedEvent) => <span className="outputEvent">{e.name}</span>)}
</>}
</div>;
}
else {
return <div className="runtimeState runtimeError">
<div>
{formatTime(item.simtime)}
&emsp;
<div className="inputEvent">{item.inputEvent}</div>
</div>
<div>
{item.error.message}
</div>
</div>;
}
});
function ShowEnvironment(props: {environment: Environment}) { function ShowEnvironment(props: {environment: Environment}) {
return <div>{ return <div>{

View file

@ -32,7 +32,7 @@ export function ShowAction(props: {action: Action}) {
} }
} }
export const ShowAST = memo(function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map<string, Transition[]>, trace: TraceState | null, highlightActive: Set<string>}) { export const ShowAST = memo(function ShowASTx(props: {root: ConcreteState | PseudoState}) {
const description = stateDescription(props.root); const description = stateDescription(props.root);
// const outgoing = props.transitions.get(props.root.uid) || []; // const outgoing = props.transitions.get(props.root.uid) || [];
@ -40,7 +40,7 @@ export const ShowAST = memo(function ShowAST(props: {root: ConcreteState | Pseud
{props.root.kind !== "pseudo" && props.root.children.length>0 && {props.root.kind !== "pseudo" && props.root.children.length>0 &&
<ul> <ul>
{props.root.children.map(child => {props.root.children.map(child =>
<ShowAST key={child.uid} root={child} transitions={props.transitions} trace={props.trace} highlightActive={props.highlightActive} /> <ShowAST key={child.uid} root={child} />
)} )}
</ul> </ul>
} }

View file

@ -34,8 +34,8 @@ export type TopPanelProps = {
// onRaise: (e: string, p: any) => void, // onRaise: (e: string, p: any) => void,
onBack: () => void, onBack: () => void,
// ast: Statechart, // ast: Statechart,
mode: InsertMode, insertMode: InsertMode,
setMode: Dispatch<SetStateAction<InsertMode>>, setInsertMode: Dispatch<SetStateAction<InsertMode>>,
setModal: Dispatch<SetStateAction<ReactElement|null>>, setModal: Dispatch<SetStateAction<ReactElement|null>>,
zoom: number, zoom: number,
setZoom: Dispatch<SetStateAction<number>>, setZoom: Dispatch<SetStateAction<number>>,
@ -56,7 +56,7 @@ const insertModes: [InsertMode, string, ReactElement, ReactElement][] = [
["text", "text", <>&nbsp;T&nbsp;</>, <kbd>X</kbd>], ["text", "text", <>&nbsp;T&nbsp;</>, <kbd>X</kbd>],
]; ];
export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history}: TopPanelProps) { export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, history}: TopPanelProps) {
const [displayTime, setDisplayTime] = useState("0.000"); const [displayTime, setDisplayTime] = useState("0.000");
const [timescale, setTimescale] = useState(1); const [timescale, setTimescale] = useState(1);
@ -226,9 +226,9 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
<KeyInfo key={m} keyInfo={keyInfo}> <KeyInfo key={m} keyInfo={keyInfo}>
<button <button
title={"insert "+hint} title={"insert "+hint}
disabled={mode===m} disabled={insertMode===m}
className={mode===m ? "active":""} className={insertMode===m ? "active":""}
onClick={() => setMode(m)} onClick={() => setInsertMode(m)}
>{buttonTxt}</button></KeyInfo>)} >{buttonTxt}</button></KeyInfo>)}
&emsp; &emsp;
</div> </div>

View file

@ -1,4 +1,4 @@
import { Diamond, RountanglePart } from "@/statecharts/concrete_syntax"; import { Diamond, RectSide } from "@/statecharts/concrete_syntax";
import { rountangleMinSize } from "./VisualEditor"; import { rountangleMinSize } from "./VisualEditor";
import { Vec2D } from "./geometry"; import { Vec2D } from "./geometry";
import { RectHelper } from "./RectHelpers"; import { RectHelper } from "./RectHelpers";
@ -21,8 +21,7 @@ export const DiamondShape = memo(function DiamondShape(props: {size: Vec2D, extr
/>; />;
}); });
export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; selected: RountanglePart[]; highlight: RountanglePart[]; error?: string; active: boolean; }) { export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; selected: RectSide[]; highlight: RectSide[]; error?: string; active: boolean; }) {
console.log('render diamond', props.diamond.uid);
const minSize = rountangleMinSize(props.diamond.size); const minSize = rountangleMinSize(props.diamond.size);
const extraAttrs = { const extraAttrs = {
className: '' className: ''

View file

@ -1,9 +1,9 @@
import { memo } from "react"; import { memo } from "react";
import { RountanglePart } from "../statecharts/concrete_syntax"; import { RectSide } from "../statecharts/concrete_syntax";
import { Vec2D } from "./geometry"; import { Vec2D } from "./geometry";
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters"; import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters";
function lineGeometryProps(size: Vec2D): [RountanglePart, object][] { function lineGeometryProps(size: Vec2D): [RectSide, object][] {
return [ return [
["top", {x1: 0, y1: 0, x2: size.x, y2: 0 }], ["top", {x1: 0, y1: 0, x2: size.x, y2: 0 }],
["right", {x1: size.x, y1: 0, x2: size.x, y2: size.y}], ["right", {x1: size.x, y1: 0, x2: size.x, y2: size.y}],
@ -13,7 +13,7 @@ function lineGeometryProps(size: Vec2D): [RountanglePart, object][] {
} }
// no need to memo() this component, the parent component is already memoized // no need to memo() this component, the parent component is already memoized
export const RectHelper = function RectHelper(props: { uid: string, size: Vec2D, selected: RountanglePart[], highlight: string[] }) { export const RectHelper = function RectHelper(props: { uid: string, size: Vec2D, selected: RectSide[], highlight: string[] }) {
const geomProps = lineGeometryProps(props.size); const geomProps = lineGeometryProps(props.size);
return <> return <>
{geomProps.map(([side, ps]) => <g key={side}> {geomProps.map(([side, ps]) => <g key={side}>

View file

@ -1,14 +1,12 @@
import { memo } from "react"; import { memo } from "react";
import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax"; import { Rountangle, RectSide } from "../statecharts/concrete_syntax";
import { ROUNTANGLE_RADIUS } from "./parameters"; import { ROUNTANGLE_RADIUS } from "./parameters";
import { RectHelper } from "./RectHelpers"; import { RectHelper } from "./RectHelpers";
import { rountangleMinSize } from "./VisualEditor"; import { rountangleMinSize } from "./VisualEditor";
import { arraysEqual } from "@/App/util"; import { arraysEqual } from "@/App/util";
export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: RountanglePart[]; highlight: RountanglePart[]; error?: string; active: boolean; }) { export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: RectSide[]; highlight: RectSide[]; error?: string; active: boolean; }) {
console.log('render rountangle', props.rountangle.uid);
const { topLeft, size, uid } = props.rountangle; const { topLeft, size, uid } = props.rountangle;
// always draw a rountangle with a minimum size // always draw a rountangle with a minimum size
// during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it // during resizing, rountangle can be smaller than this size and even have a negative size, but we don't show it

View file

@ -1,7 +1,7 @@
import { ClipboardEvent, Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ClipboardEvent, Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Statechart } from "../statecharts/abstract_syntax"; import { Statechart } from "../statecharts/abstract_syntax";
import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text } from "../statecharts/concrete_syntax"; import { Arrow, ArrowPart, Diamond, History, Rountangle, RectSide, Text } from "../statecharts/concrete_syntax";
import { parseStatechart, TraceableError } from "../statecharts/parser"; import { parseStatechart, TraceableError } from "../statecharts/parser";
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "./geometry"; import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "./geometry";
import { MIN_ROUNTANGLE_SIZE } from "./parameters"; import { MIN_ROUNTANGLE_SIZE } from "./parameters";
@ -11,7 +11,7 @@ import { RountangleSVG } from "./RountangleSVG";
import { TextSVG } from "./TextSVG"; import { TextSVG } from "./TextSVG";
import { DiamondSVG } from "./DiamondSVG"; import { DiamondSVG } from "./DiamondSVG";
import { HistorySVG } from "./HistorySVG"; import { HistorySVG } from "./HistorySVG";
import { detectConnections } from "../statecharts/detect_connections"; import { Connections, detectConnections } from "../statecharts/detect_connections";
import "./VisualEditor.css"; import "./VisualEditor.css";
import { TraceState } from "@/App/App"; import { TraceState } from "@/App/App";
@ -30,7 +30,7 @@ type SelectingState = Rect2D | null;
export type RountangleSelectable = { export type RountangleSelectable = {
// kind: "rountangle"; // kind: "rountangle";
parts: RountanglePart[]; parts: RectSide[];
uid: string; uid: string;
} }
type ArrowSelectable = { type ArrowSelectable = {
@ -51,7 +51,7 @@ type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable | Hist
type Selection = Selectable[]; type Selection = Selectable[];
export const sides: [RountanglePart, (r:Rect2D)=>Line2D][] = [ export const sides: [RectSide, (r:Rect2D)=>Line2D][] = [
["left", getLeftSide], ["left", getLeftSide],
["top", getTopSide], ["top", getTopSide],
["right", getRightSide], ["right", getRightSide],
@ -63,12 +63,10 @@ export type InsertMode = "and"|"or"|"pseudo"|"shallow"|"deep"|"transition"|"text
type VisualEditorProps = { type VisualEditorProps = {
state: VisualEditorState, state: VisualEditorState,
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>, setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
// ast: Statechart, conns: Connections,
setAST: Dispatch<SetStateAction<Statechart>>, syntaxErrors: TraceableError[],
trace: TraceState | null, trace: TraceState | null,
errors: TraceableError[], insertMode: InsertMode,
setErrors: Dispatch<SetStateAction<TraceableError[]>>,
mode: InsertMode,
highlightActive: Set<string>, highlightActive: Set<string>,
highlightTransitions: string[], highlightTransitions: string[],
setModal: Dispatch<SetStateAction<ReactElement|null>>, setModal: Dispatch<SetStateAction<ReactElement|null>>,
@ -76,7 +74,7 @@ type VisualEditorProps = {
zoom: number; zoom: number;
}; };
export const VisualEditor = memo(function VisualEditor({state, setState, setAST, trace, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) { export const VisualEditor = memo(function VisualEditor({state, setState, trace, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
@ -153,31 +151,22 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [state]); }, [state]);
const conns = useMemo(() => detectConnections(state), [state]); const getCurrentPointer = useCallback((e: {pageX: number, pageY: number}) => {
useEffect(() => {
const [statechart, errors] = parseStatechart(state, conns);
setErrors(errors);
setAST(statechart);
}, [state])
function getCurrentPointer(e: {pageX: number, pageY: number}) {
const bbox = refSVG.current!.getBoundingClientRect(); const bbox = refSVG.current!.getBoundingClientRect();
return { return {
x: (e.pageX - bbox.left)/zoom, x: (e.pageX - bbox.left)/zoom,
y: (e.pageY - bbox.top)/zoom, y: (e.pageY - bbox.top)/zoom,
} }
} }, [refSVG.current]);
const onMouseDown = (e: {button: number, target: any, pageX: number, pageY: number}) => { const onMouseDown = useCallback((e: {button: number, target: any, pageX: number, pageY: number}) => {
const currentPointer = getCurrentPointer(e); const currentPointer = getCurrentPointer(e);
if (e.button === 2) { if (e.button === 2) {
makeCheckPoint(); makeCheckPoint();
// ignore selection, middle mouse button always inserts // ignore selection, middle mouse button always inserts
setState(state => { setState(state => {
const newID = state.nextID.toString(); const newID = state.nextID.toString();
if (mode === "and" || mode === "or") { if (insertMode === "and" || insertMode === "or") {
// insert rountangle // insert rountangle
return { return {
...state, ...state,
@ -185,13 +174,13 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
uid: newID, uid: newID,
topLeft: currentPointer, topLeft: currentPointer,
size: MIN_ROUNTANGLE_SIZE, size: MIN_ROUNTANGLE_SIZE,
kind: mode, kind: insertMode,
}], }],
nextID: state.nextID+1, nextID: state.nextID+1,
selection: [{uid: newID, parts: ["bottom", "right"]}], selection: [{uid: newID, parts: ["bottom", "right"]}],
}; };
} }
else if (mode === "pseudo") { else if (insertMode === "pseudo") {
return { return {
...state, ...state,
diamonds: [...state.diamonds, { diamonds: [...state.diamonds, {
@ -203,19 +192,19 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
selection: [{uid: newID, parts: ["bottom", "right"]}], selection: [{uid: newID, parts: ["bottom", "right"]}],
}; };
} }
else if (mode === "shallow" || mode === "deep") { else if (insertMode === "shallow" || insertMode === "deep") {
return { return {
...state, ...state,
history: [...state.history, { history: [...state.history, {
uid: newID, uid: newID,
kind: mode, kind: insertMode,
topLeft: currentPointer, topLeft: currentPointer,
}], }],
nextID: state.nextID+1, nextID: state.nextID+1,
selection: [{uid: newID, parts: ["history"]}], selection: [{uid: newID, parts: ["history"]}],
} }
} }
else if (mode === "transition") { else if (insertMode === "transition") {
return { return {
...state, ...state,
arrows: [...state.arrows, { arrows: [...state.arrows, {
@ -227,7 +216,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
selection: [{uid: newID, parts: ["end"]}], selection: [{uid: newID, parts: ["end"]}],
} }
} }
else if (mode === "text") { else if (insertMode === "text") {
return { return {
...state, ...state,
texts: [...state.texts, { texts: [...state.texts, {
@ -239,7 +228,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
selection: [{uid: newID, parts: ["text"]}], selection: [{uid: newID, parts: ["text"]}],
} }
} }
throw new Error("unreachable, mode=" + mode); // shut up typescript throw new Error("unreachable, mode=" + insertMode); // shut up typescript
}); });
setDragging(true); setDragging(true);
return; return;
@ -288,9 +277,9 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
size: {x: 0, y: 0}, size: {x: 0, y: 0},
}); });
setSelection(() => []); setSelection(() => []);
}; }, [getCurrentPointer, makeCheckPoint, insertMode, selection]);
const onMouseMove = (e: {pageX: number, pageY: number, movementX: number, movementY: number}) => { const onMouseMove = useCallback((e: {pageX: number, pageY: number, movementX: number, movementY: number}) => {
const currentPointer = getCurrentPointer(e); const currentPointer = getCurrentPointer(e);
if (dragging) { if (dragging) {
// const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos); // const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos);
@ -298,7 +287,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
setState(state => ({ setState(state => ({
...state, ...state,
rountangles: state.rountangles.map(r => { rountangles: state.rountangles.map(r => {
const parts = selection.find(selected => selected.uid === r.uid)?.parts || []; const parts = state.selection.find(selected => selected.uid === r.uid)?.parts || [];
if (parts.length === 0) { if (parts.length === 0) {
return r; return r;
} }
@ -309,7 +298,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
}) })
.toSorted((a,b) => area(b) - area(a)), // sort: smaller rountangles are drawn on top .toSorted((a,b) => area(b) - area(a)), // sort: smaller rountangles are drawn on top
diamonds: state.diamonds.map(d => { diamonds: state.diamonds.map(d => {
const parts = selection.find(selected => selected.uid === d.uid)?.parts || []; const parts = state.selection.find(selected => selected.uid === d.uid)?.parts || [];
if (parts.length === 0) { if (parts.length === 0) {
return d; return d;
} }
@ -319,7 +308,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
} }
}), }),
history: state.history.map(h => { history: state.history.map(h => {
const parts = selection.find(selected => selected.uid === h.uid)?.parts || []; const parts = state.selection.find(selected => selected.uid === h.uid)?.parts || [];
if (parts.length === 0) { if (parts.length === 0) {
return h; return h;
} }
@ -329,7 +318,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
} }
}), }),
arrows: state.arrows.map(a => { arrows: state.arrows.map(a => {
const parts = selection.find(selected => selected.uid === a.uid)?.parts || []; const parts = state.selection.find(selected => selected.uid === a.uid)?.parts || [];
if (parts.length === 0) { if (parts.length === 0) {
return a; return a;
} }
@ -339,7 +328,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
} }
}), }),
texts: state.texts.map(t => { texts: state.texts.map(t => {
const parts = selection.find(selected => selected.uid === t.uid)?.parts || []; const parts = state.selection.find(selected => selected.uid === t.uid)?.parts || [];
if (parts.length === 0) { if (parts.length === 0) {
return t; return t;
} }
@ -360,9 +349,9 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
}; };
}); });
} }
}; }, [getCurrentPointer, selectingState, dragging]);
const onMouseUp = (e: {target: any, pageX: number, pageY: number}) => { const onMouseUp = useCallback((e: {target: any, pageX: number, pageY: number}) => {
if (dragging) { if (dragging) {
setDragging(false); setDragging(false);
// do not persist sizes smaller than 40x40 // do not persist sizes smaller than 40x40
@ -424,7 +413,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
} }
} }
setSelectingState(null); // no longer making a selection setSelectingState(null); // no longer making a selection
}; }, [dragging, selectingState, refSVG.current]);
function deleteSelection() { function deleteSelection() {
setState(state => ({ setState(state => ({
@ -499,7 +488,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
}, [selectingState, dragging]); }, [selectingState, dragging]);
// for visual feedback, when selecting/moving one thing, we also highlight (in green) all the things that belong to the thing we selected. // for visual feedback, when selecting/moving one thing, we also highlight (in green) all the things that belong to the thing we selected.
const sidesToHighlight: {[key: string]: RountanglePart[]} = {}; const sidesToHighlight: {[key: string]: RectSide[]} = {};
const arrowsToHighlight: {[key: string]: boolean} = {}; const arrowsToHighlight: {[key: string]: boolean} = {};
const textsToHighlight: {[key: string]: boolean} = {}; const textsToHighlight: {[key: string]: boolean} = {};
const rountanglesToHighlight: {[key: string]: boolean} = {}; const rountanglesToHighlight: {[key: string]: boolean} = {};
@ -716,8 +705,8 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
return <RountangleSVG return <RountangleSVG
key={rountangle.uid} key={rountangle.uid}
rountangle={rountangle} rountangle={rountangle}
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []} selected={selection.find(r => r.uid === rountangle.uid)?.parts as RectSide[] || []}
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]} highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RectSide[]]}
error={errors error={errors
.filter(({shapeUid}) => shapeUid === rountangle.uid) .filter(({shapeUid}) => shapeUid === rountangle.uid)
.map(({message}) => message).join(', ')} .map(({message}) => message).join(', ')}
@ -728,8 +717,8 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
<DiamondSVG <DiamondSVG
key={diamond.uid} key={diamond.uid}
diamond={diamond} diamond={diamond}
selected={selection.find(r => r.uid === diamond.uid)?.parts || []} selected={selection.find(r => r.uid === diamond.uid)?.parts as RectSide[] || []}
highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]} highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RectSide[]]}
error={errors error={errors
.filter(({shapeUid}) => shapeUid === diamond.uid) .filter(({shapeUid}) => shapeUid === diamond.uid)
.map(({message}) => message).join(', ')} .map(({message}) => message).join(', ')}

View file

@ -1,4 +1,4 @@
import { RountanglePart } from "../statecharts/concrete_syntax"; import { RectSide } from "../statecharts/concrete_syntax";
export type Vec2D = { export type Vec2D = {
x: number; x: number;
@ -166,7 +166,7 @@ export function getBottomSide(rect: Rect2D): Line2D {
export type ArcDirection = "no" | "cw" | "ccw"; export type ArcDirection = "no" | "cw" | "ccw";
export function arcDirection(start: RountanglePart, end: RountanglePart): ArcDirection { export function arcDirection(start: RectSide, end: RectSide): ArcDirection {
if (start === end) { if (start === end) {
if (start === "left" || start === "top") { if (start === "left" || start === "top") {
return "ccw"; return "ccw";

View file

@ -28,7 +28,7 @@ export type History = {
}; };
// independently moveable parts of our shapes: // independently moveable parts of our shapes:
export type RountanglePart = "left" | "top" | "right" | "bottom"; export type RectSide = "left" | "top" | "right" | "bottom";
export type ArrowPart = "start" | "end"; export type ArrowPart = "start" | "end";
export const emptyState: VisualEditorState = { export const emptyState: VisualEditorState = {
@ -36,9 +36,9 @@ export const emptyState: VisualEditorState = {
}; };
// used to find which rountangle an arrow connects to (src/tgt) // used to find which rountangle an arrow connects to (src/tgt)
export function findNearestSide(arrow: Line2D, arrowPart: "start" | "end", candidates: (Rountangle|Diamond)[]): {uid: string, part: RountanglePart} | undefined { export function findNearestSide(arrow: Line2D, arrowPart: "start" | "end", candidates: (Rountangle|Diamond)[]): {uid: string, part: RectSide} | undefined {
let best = Infinity; let best = Infinity;
let bestSide: undefined | {uid: string, part: RountanglePart}; let bestSide: undefined | {uid: string, part: RectSide};
for (const rountangle of candidates) { for (const rountangle of candidates) {
for (const [side, getSide] of sides) { for (const [side, getSide] of sides) {
const asLine = getSide(rountangle); const asLine = getSide(rountangle);

View file

@ -1,8 +1,8 @@
import { VisualEditorState } from "@/VisualEditor/VisualEditor"; import { VisualEditorState } from "@/VisualEditor/VisualEditor";
import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RountanglePart } from "./concrete_syntax"; import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RectSide } from "./concrete_syntax";
export type Connections = { export type Connections = {
arrow2SideMap: Map<string,[{ uid: string; part: RountanglePart; } | undefined, { uid: string; part: RountanglePart; } | undefined]>, arrow2SideMap: Map<string,[{ uid: string; part: RectSide; } | undefined, { uid: string; part: RectSide; } | undefined]>,
side2ArrowMap: Map<string, Set<["start"|"end", string]>>, side2ArrowMap: Map<string, Set<["start"|"end", string]>>,
text2ArrowMap: Map<string,string>, text2ArrowMap: Map<string,string>,
arrow2TextMap: Map<string,string[]>, arrow2TextMap: Map<string,string[]>,
@ -14,7 +14,7 @@ export type Connections = {
export function detectConnections(state: VisualEditorState): Connections { export function detectConnections(state: VisualEditorState): Connections {
// detect what is 'connected' // detect what is 'connected'
const arrow2SideMap = new Map<string,[{ uid: string; part: RountanglePart; } | undefined, { uid: string; part: RountanglePart; } | undefined]>(); const arrow2SideMap = new Map<string,[{ uid: string; part: RectSide; } | undefined, { uid: string; part: RectSide; } | undefined]>();
const side2ArrowMap = new Map<string, Set<["start"|"end", string]>>(); const side2ArrowMap = new Map<string, Set<["start"|"end", string]>>();
const text2ArrowMap = new Map<string,string>(); const text2ArrowMap = new Map<string,string>();
const arrow2TextMap = new Map<string,string[]>(); const arrow2TextMap = new Map<string,string[]>();