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 { emptyStatechart, Statechart, Transition } from "../statecharts/abstract_syntax";
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 { getSimTime, getWallClkDelay, TimeMode } from "../statecharts/time";
@ -13,7 +12,7 @@ import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { TopPanel } from "./TopPanel";
import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "./ShowAST";
import { TraceableError } from "../statecharts/parser";
import { parseStatechart } from "../statecharts/parser";
import { getKeyHandler } from "./shortcut_handler";
import { BottomPanel } from "./BottomPanel";
import { emptyState } from "@/statecharts/concrete_syntax";
@ -23,6 +22,7 @@ import { DummyPlant } from "@/Plant/Dummy/Dummy";
import { Plant } from "@/Plant/Plant";
import { usePersistentState } from "@/util/persistent_state";
import { RTHistory } from "./RTHistory";
import { detectConnections } from "@/statecharts/detect_connections";
export type EditHistory = {
current: VisualEditorState,
@ -70,10 +70,8 @@ function getPlantState<T>(plant: Plant<T>, trace: TraceItem[], idx: number): T |
}
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 [ast, setAST] = useState<Statechart>(emptyStatechart);
const [errors, setErrors] = useState<TraceableError[]>([]);
const [trace, setTrace] = useState<TraceState|null>(null);
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
const [modal, setModal] = useState<ReactElement|null>(null);
@ -91,6 +89,10 @@ export function App() {
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
const makeCheckPoint = useCallback(() => {
setHistoryState(historyState => ({
@ -261,7 +263,7 @@ export function App() {
}, []);
useEffect(() => {
const onKeyDown = getKeyHandler(setMode);
const onKeyDown = getKeyHandler(setInsertMode);
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
@ -322,12 +324,12 @@ export function App() {
}}
>
<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>
{/* Below the top bar: Editor */}
<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>
</Stack>
</Box>
@ -408,7 +410,7 @@ export function App() {
{/* Bottom panel */}
<Box sx={{flex: '0 0 content'}}>
<BottomPanel {...{errors}}/>
<BottomPanel {...{errors: syntaxErrors}}/>
</Box>
</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 { BigStep, Environment, Mode, RaisedEvent, RT_Event } from "../statecharts/runtime_types";
import { formatTime } from "./util";
import { TimeMode } from "../statecharts/time";
import { TraceState } from "./App";
import { TraceItem, TraceState } from "./App";
type RTHistoryProps = {
trace: TraceState|null,
@ -13,24 +13,29 @@ type 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 && {
...trace,
idx,
});
setTime({kind: "paused", simtime: timestamp});
}
}, [setTrace, setTime]);
if (trace === null) {
return <></>;
}
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}/>)}
</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") {
const newStates = item.mode.difference(trace.trace[i-1]?.mode || new Set());
// @ts-ignore
const newStates = item.mode.difference(prevItem?.mode || new Set());
return <div
className={"runtimeState" + (i === trace.idx ? " active" : "")}
onMouseDown={() => gotoRt(i, item.simtime)}>
className={"runtimeState" + (active ? " active" : "")}
onMouseDown={useCallback(() => onMouseDown(idx, item.simtime), [idx, item.simtime])}>
<div>
{formatTime(item.simtime)}
&emsp;
@ -55,9 +60,7 @@ export function RTHistory({trace, setTrace, ast, setTime}: RTHistoryProps) {
</div>
</div>;
}
})}
</div>;
}
});
function ShowEnvironment(props: {environment: Environment}) {

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 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 &&
<ul>
{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>
}

View file

@ -34,8 +34,8 @@ export type TopPanelProps = {
// onRaise: (e: string, p: any) => void,
onBack: () => void,
// ast: Statechart,
mode: InsertMode,
setMode: Dispatch<SetStateAction<InsertMode>>,
insertMode: InsertMode,
setInsertMode: Dispatch<SetStateAction<InsertMode>>,
setModal: Dispatch<SetStateAction<ReactElement|null>>,
zoom: number,
setZoom: Dispatch<SetStateAction<number>>,
@ -56,7 +56,7 @@ const insertModes: [InsertMode, string, ReactElement, ReactElement][] = [
["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 [timescale, setTimescale] = useState(1);
@ -226,9 +226,9 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
<KeyInfo key={m} keyInfo={keyInfo}>
<button
title={"insert "+hint}
disabled={mode===m}
className={mode===m ? "active":""}
onClick={() => setMode(m)}
disabled={insertMode===m}
className={insertMode===m ? "active":""}
onClick={() => setInsertMode(m)}
>{buttonTxt}</button></KeyInfo>)}
&emsp;
</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 { Vec2D } from "./geometry";
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; }) {
console.log('render diamond', props.diamond.uid);
export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; selected: RectSide[]; highlight: RectSide[]; error?: string; active: boolean; }) {
const minSize = rountangleMinSize(props.diamond.size);
const extraAttrs = {
className: ''

View file

@ -1,9 +1,9 @@
import { memo } from "react";
import { RountanglePart } from "../statecharts/concrete_syntax";
import { RectSide } from "../statecharts/concrete_syntax";
import { Vec2D } from "./geometry";
import { CORNER_HELPER_OFFSET, CORNER_HELPER_RADIUS } from "./parameters";
function lineGeometryProps(size: Vec2D): [RountanglePart, object][] {
function lineGeometryProps(size: Vec2D): [RectSide, object][] {
return [
["top", {x1: 0, y1: 0, x2: size.x, y2: 0 }],
["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
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);
return <>
{geomProps.map(([side, ps]) => <g key={side}>

View file

@ -1,14 +1,12 @@
import { memo } from "react";
import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax";
import { Rountangle, RectSide } from "../statecharts/concrete_syntax";
import { ROUNTANGLE_RADIUS } from "./parameters";
import { RectHelper } from "./RectHelpers";
import { rountangleMinSize } from "./VisualEditor";
import { arraysEqual } from "@/App/util";
export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: RountanglePart[]; highlight: RountanglePart[]; error?: string; active: boolean; }) {
console.log('render rountangle', props.rountangle.uid);
export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: RectSide[]; highlight: RectSide[]; error?: string; active: boolean; }) {
const { topLeft, size, uid } = props.rountangle;
// 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

View file

@ -1,7 +1,7 @@
import { ClipboardEvent, Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
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 { 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";
@ -11,7 +11,7 @@ import { RountangleSVG } from "./RountangleSVG";
import { TextSVG } from "./TextSVG";
import { DiamondSVG } from "./DiamondSVG";
import { HistorySVG } from "./HistorySVG";
import { detectConnections } from "../statecharts/detect_connections";
import { Connections, detectConnections } from "../statecharts/detect_connections";
import "./VisualEditor.css";
import { TraceState } from "@/App/App";
@ -30,7 +30,7 @@ type SelectingState = Rect2D | null;
export type RountangleSelectable = {
// kind: "rountangle";
parts: RountanglePart[];
parts: RectSide[];
uid: string;
}
type ArrowSelectable = {
@ -51,7 +51,7 @@ type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable | Hist
type Selection = Selectable[];
export const sides: [RountanglePart, (r:Rect2D)=>Line2D][] = [
export const sides: [RectSide, (r:Rect2D)=>Line2D][] = [
["left", getLeftSide],
["top", getTopSide],
["right", getRightSide],
@ -63,12 +63,10 @@ export type InsertMode = "and"|"or"|"pseudo"|"shallow"|"deep"|"transition"|"text
type VisualEditorProps = {
state: VisualEditorState,
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
// ast: Statechart,
setAST: Dispatch<SetStateAction<Statechart>>,
conns: Connections,
syntaxErrors: TraceableError[],
trace: TraceState | null,
errors: TraceableError[],
setErrors: Dispatch<SetStateAction<TraceableError[]>>,
mode: InsertMode,
insertMode: InsertMode,
highlightActive: Set<string>,
highlightTransitions: string[],
setModal: Dispatch<SetStateAction<ReactElement|null>>,
@ -76,7 +74,7 @@ type VisualEditorProps = {
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);
@ -153,31 +151,22 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
return () => clearTimeout(timeout);
}, [state]);
const conns = useMemo(() => detectConnections(state), [state]);
useEffect(() => {
const [statechart, errors] = parseStatechart(state, conns);
setErrors(errors);
setAST(statechart);
}, [state])
function getCurrentPointer(e: {pageX: number, pageY: number}) {
const getCurrentPointer = useCallback((e: {pageX: number, pageY: number}) => {
const bbox = refSVG.current!.getBoundingClientRect();
return {
x: (e.pageX - bbox.left)/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);
if (e.button === 2) {
makeCheckPoint();
// ignore selection, middle mouse button always inserts
setState(state => {
const newID = state.nextID.toString();
if (mode === "and" || mode === "or") {
if (insertMode === "and" || insertMode === "or") {
// insert rountangle
return {
...state,
@ -185,13 +174,13 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
uid: newID,
topLeft: currentPointer,
size: MIN_ROUNTANGLE_SIZE,
kind: mode,
kind: insertMode,
}],
nextID: state.nextID+1,
selection: [{uid: newID, parts: ["bottom", "right"]}],
};
}
else if (mode === "pseudo") {
else if (insertMode === "pseudo") {
return {
...state,
diamonds: [...state.diamonds, {
@ -203,19 +192,19 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
selection: [{uid: newID, parts: ["bottom", "right"]}],
};
}
else if (mode === "shallow" || mode === "deep") {
else if (insertMode === "shallow" || insertMode === "deep") {
return {
...state,
history: [...state.history, {
uid: newID,
kind: mode,
kind: insertMode,
topLeft: currentPointer,
}],
nextID: state.nextID+1,
selection: [{uid: newID, parts: ["history"]}],
}
}
else if (mode === "transition") {
else if (insertMode === "transition") {
return {
...state,
arrows: [...state.arrows, {
@ -227,7 +216,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
selection: [{uid: newID, parts: ["end"]}],
}
}
else if (mode === "text") {
else if (insertMode === "text") {
return {
...state,
texts: [...state.texts, {
@ -239,7 +228,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
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);
return;
@ -288,9 +277,9 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
size: {x: 0, y: 0},
});
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);
if (dragging) {
// const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos);
@ -298,7 +287,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
setState(state => ({
...state,
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) {
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
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) {
return d;
}
@ -319,7 +308,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
}
}),
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) {
return h;
}
@ -329,7 +318,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
}
}),
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) {
return a;
}
@ -339,7 +328,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
}
}),
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) {
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) {
setDragging(false);
// 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
};
}, [dragging, selectingState, refSVG.current]);
function deleteSelection() {
setState(state => ({
@ -499,7 +488,7 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
}, [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.
const sidesToHighlight: {[key: string]: RountanglePart[]} = {};
const sidesToHighlight: {[key: string]: RectSide[]} = {};
const arrowsToHighlight: {[key: string]: boolean} = {};
const textsToHighlight: {[key: string]: boolean} = {};
const rountanglesToHighlight: {[key: string]: boolean} = {};
@ -716,8 +705,8 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
return <RountangleSVG
key={rountangle.uid}
rountangle={rountangle}
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]}
selected={selection.find(r => r.uid === rountangle.uid)?.parts as RectSide[] || []}
highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RectSide[]]}
error={errors
.filter(({shapeUid}) => shapeUid === rountangle.uid)
.map(({message}) => message).join(', ')}
@ -728,8 +717,8 @@ export const VisualEditor = memo(function VisualEditor({state, setState, setAST,
<DiamondSVG
key={diamond.uid}
diamond={diamond}
selected={selection.find(r => r.uid === diamond.uid)?.parts || []}
highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RountanglePart[]]}
selected={selection.find(r => r.uid === diamond.uid)?.parts as RectSide[] || []}
highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RectSide[]]}
error={errors
.filter(({shapeUid}) => shapeUid === diamond.uid)
.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 = {
x: number;
@ -166,7 +166,7 @@ export function getBottomSide(rect: Rect2D): Line2D {
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 === "left" || start === "top") {
return "ccw";

View file

@ -28,7 +28,7 @@ export type History = {
};
// 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 const emptyState: VisualEditorState = {
@ -36,9 +36,9 @@ export const emptyState: VisualEditorState = {
};
// 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 bestSide: undefined | {uid: string, part: RountanglePart};
let bestSide: undefined | {uid: string, part: RectSide};
for (const rountangle of candidates) {
for (const [side, getSide] of sides) {
const asLine = getSide(rountangle);

View file

@ -1,8 +1,8 @@
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 = {
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]>>,
text2ArrowMap: Map<string,string>,
arrow2TextMap: Map<string,string[]>,
@ -14,7 +14,7 @@ export type Connections = {
export function detectConnections(state: VisualEditorState): Connections {
// 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 text2ArrowMap = new Map<string,string>();
const arrow2TextMap = new Map<string,string[]>();