first of many performance improvements: no unnecessary TextSVG re-renders - more to follow
This commit is contained in:
parent
af60e811fc
commit
0fc3775a11
14 changed files with 116 additions and 74 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { ReactElement, useEffect, useMemo, useRef, useState } from "react";
|
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { emptyStatechart, Statechart, Transition } from "../statecharts/abstract_syntax";
|
import { emptyStatechart, Statechart, Transition } from "../statecharts/abstract_syntax";
|
||||||
import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter";
|
import { handleInputEvent, initialize, RuntimeError } from "../statecharts/interpreter";
|
||||||
|
|
@ -85,9 +85,9 @@ export function App() {
|
||||||
const plant = plants.find(([pn, p]) => pn === plantName)![1];
|
const plant = plants.find(([pn, p]) => pn === plantName)![1];
|
||||||
|
|
||||||
const editorState = historyState.current;
|
const editorState = historyState.current;
|
||||||
const setEditorState = (cb: (value: VisualEditorState) => VisualEditorState) => {
|
const setEditorState = useCallback((cb: (value: VisualEditorState) => VisualEditorState) => {
|
||||||
setHistoryState(historyState => ({...historyState, current: cb(historyState.current)}));
|
setHistoryState(historyState => ({...historyState, current: cb(historyState.current)}));
|
||||||
}
|
}, [setHistoryState]);
|
||||||
|
|
||||||
const refRightSideBar = useRef<HTMLDivElement>(null);
|
const refRightSideBar = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, Stack } from "@mui/material";
|
import { memo, PropsWithChildren, ReactElement } from "react";
|
||||||
|
|
||||||
export function KeyInfoVisible(props: {keyInfo, children, horizontal?: boolean}) {
|
export const KeyInfoVisible = memo(function KeyInfoVisible(props: PropsWithChildren<{keyInfo: ReactElement, horizontal?: boolean}>) {
|
||||||
return <div style={{display: 'inline-block'}}>
|
return <div style={{display: 'inline-block'}}>
|
||||||
{/* <Stack direction={props.horizontal ? "row" : "column"}> */}
|
{/* <Stack direction={props.horizontal ? "row" : "column"}> */}
|
||||||
<div style={{display: props.horizontal ? 'inline-block' : '', fontSize:11, height: 18, textAlign:"center", paddingLeft: 3, paddingRight: 3}}>
|
<div style={{display: props.horizontal ? 'inline-block' : '', fontSize:11, height: 18, textAlign:"center", paddingLeft: 3, paddingRight: 3}}>
|
||||||
|
|
@ -11,8 +11,8 @@ export function KeyInfoVisible(props: {keyInfo, children, horizontal?: boolean})
|
||||||
</div>
|
</div>
|
||||||
{/* </Stack> */}
|
{/* </Stack> */}
|
||||||
</div>
|
</div>
|
||||||
}
|
});
|
||||||
|
|
||||||
export function KeyInfoHidden(props: {children}) {
|
export const KeyInfoHidden = memo(function KeyInfoHidden(props: PropsWithChildren<{}>) {
|
||||||
return <>{props.children}</>;
|
return <>{props.children}</>;
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function ShowAction(props: {action: Action}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map<string, Transition[]>, trace: TraceState | null, highlightActive: Set<string>}) {
|
export const ShowAST = memo(function ShowAST(props: {root: ConcreteState | PseudoState, transitions: Map<string, Transition[]>, trace: TraceState | null, highlightActive: Set<string>}) {
|
||||||
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) || [];
|
||||||
|
|
||||||
|
|
@ -69,11 +69,11 @@ export function ShowAST(props: {root: ConcreteState | PseudoState, transitions:
|
||||||
// outgoing.map(transition => <> <ShowTransition transition={transition}/><br/></>)
|
// outgoing.map(transition => <> <ShowTransition transition={transition}/><br/></>)
|
||||||
// } */}
|
// } */}
|
||||||
// </details>;
|
// </details>;
|
||||||
}
|
});
|
||||||
|
|
||||||
import BoltIcon from '@mui/icons-material/Bolt';
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
import { KeyInfoHidden, KeyInfoVisible } from "./KeyInfo";
|
||||||
import { useEffect } from "react";
|
import { memo, useEffect } from "react";
|
||||||
import { TraceState } from "./App";
|
import { TraceState } from "./App";
|
||||||
|
|
||||||
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}) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Dispatch, ReactElement, SetStateAction, useEffect, useState } from "react";
|
import { Dispatch, memo, ReactElement, SetStateAction, useEffect, useState } from "react";
|
||||||
import { BigStep, TimerElapseEvent, Timers } from "../statecharts/runtime_types";
|
import { BigStep, TimerElapseEvent, Timers } from "../statecharts/runtime_types";
|
||||||
import { getSimTime, setPaused, setRealtime, TimeMode } from "../statecharts/time";
|
import { getSimTime, setPaused, setRealtime, TimeMode } from "../statecharts/time";
|
||||||
import { Statechart } from "../statecharts/abstract_syntax";
|
import { Statechart } from "../statecharts/abstract_syntax";
|
||||||
|
|
@ -11,12 +11,8 @@ import SkipNextIcon from '@mui/icons-material/SkipNext';
|
||||||
import SkipPreviousIcon from '@mui/icons-material/SkipPrevious';import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
|
import SkipPreviousIcon from '@mui/icons-material/SkipPrevious';import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
|
||||||
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
|
import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
|
||||||
import StopIcon from '@mui/icons-material/Stop';
|
import StopIcon from '@mui/icons-material/Stop';
|
||||||
import UndoIcon from '@mui/icons-material/Undo';
|
|
||||||
import RedoIcon from '@mui/icons-material/Redo';
|
|
||||||
import InfoOutlineIcon from '@mui/icons-material/InfoOutline';
|
import InfoOutlineIcon from '@mui/icons-material/InfoOutline';
|
||||||
import KeyboardIcon from '@mui/icons-material/Keyboard';
|
import KeyboardIcon from '@mui/icons-material/Keyboard';
|
||||||
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
|
||||||
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
|
|
||||||
|
|
||||||
import { formatTime } from "./util";
|
import { formatTime } from "./util";
|
||||||
import { InsertMode } from "../VisualEditor/VisualEditor";
|
import { InsertMode } from "../VisualEditor/VisualEditor";
|
||||||
|
|
@ -26,20 +22,20 @@ import { usePersistentState } from "@/util/persistent_state";
|
||||||
import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons";
|
import { RountangleIcon, PseudoStateIcon, HistoryIcon } from "./Icons";
|
||||||
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters";
|
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters";
|
||||||
import { EditHistory, TraceState } from "./App";
|
import { EditHistory, TraceState } from "./App";
|
||||||
|
import { ZoomButtons } from "./TopPanel/ZoomButtons";
|
||||||
|
import { UndoRedoButtons } from "./TopPanel/UndoRedoButtons";
|
||||||
|
|
||||||
export type TopPanelProps = {
|
export type TopPanelProps = {
|
||||||
trace: TraceState | null,
|
trace: TraceState | null,
|
||||||
// rt?: BigStep,
|
|
||||||
// rtIdx?: number,
|
|
||||||
time: TimeMode,
|
time: TimeMode,
|
||||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||||
onUndo: () => void,
|
onUndo: () => void,
|
||||||
onRedo: () => void,
|
onRedo: () => void,
|
||||||
onInit: () => void,
|
onInit: () => void,
|
||||||
onClear: () => void,
|
onClear: () => void,
|
||||||
onRaise: (e: string, p: any) => void,
|
// onRaise: (e: string, p: any) => void,
|
||||||
onBack: () => void,
|
onBack: () => void,
|
||||||
ast: Statechart,
|
// ast: Statechart,
|
||||||
mode: InsertMode,
|
mode: InsertMode,
|
||||||
setMode: Dispatch<SetStateAction<InsertMode>>,
|
setMode: Dispatch<SetStateAction<InsertMode>>,
|
||||||
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
||||||
|
|
@ -50,7 +46,7 @@ export type TopPanelProps = {
|
||||||
history: EditHistory,
|
history: EditHistory,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, ast, mode, setMode, setModal, zoom, setZoom, showKeys, setShowKeys, history}: TopPanelProps) {
|
export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, mode, setMode, 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);
|
||||||
|
|
||||||
|
|
@ -111,14 +107,6 @@ export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear,
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onRedo();
|
onRedo();
|
||||||
}
|
}
|
||||||
if (e.key === "+") {
|
|
||||||
e.preventDefault();
|
|
||||||
onZoomIn();
|
|
||||||
}
|
|
||||||
if (e.key === "-") {
|
|
||||||
e.preventDefault();
|
|
||||||
onZoomOut();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", onKeyDown);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
@ -143,12 +131,6 @@ export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear,
|
||||||
}
|
}
|
||||||
}, [time]);
|
}, [time]);
|
||||||
|
|
||||||
function onZoomIn() {
|
|
||||||
setZoom(zoom => Math.min(zoom * ZOOM_STEP, ZOOM_MAX));
|
|
||||||
}
|
|
||||||
function onZoomOut() {
|
|
||||||
setZoom(zoom => Math.max(zoom / ZOOM_STEP, ZOOM_MIN));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onChangePaused(paused: boolean, wallclktime: number) {
|
function onChangePaused(paused: boolean, wallclktime: number) {
|
||||||
setTime(time => {
|
setTime(time => {
|
||||||
|
|
@ -218,24 +200,13 @@ export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear,
|
||||||
|
|
||||||
{/* zoom */}
|
{/* zoom */}
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>-</kbd></>}>
|
<ZoomButtons showKeys={showKeys} zoom={zoom} setZoom={setZoom}/>
|
||||||
<button title="zoom out" onClick={onZoomOut} disabled={zoom <= ZOOM_MIN}><ZoomOutIcon fontSize="small"/></button>
|
|
||||||
</KeyInfo>
|
|
||||||
<input title="current zoom level" value={zoom.toFixed(3)} style={{width:40}} readOnly/>
|
|
||||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>+</kbd></>}>
|
|
||||||
<button title="zoom in" onClick={onZoomIn} disabled={zoom >= ZOOM_MAX}><ZoomInIcon fontSize="small"/></button>
|
|
||||||
</KeyInfo>
|
|
||||||
 
|
 
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* undo / redo */}
|
{/* undo / redo */}
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Z</kbd></>}>
|
<UndoRedoButtons showKeys={showKeys} onUndo={onUndo} onRedo={onRedo} historyLength={history.history.length} futureLength={history.future.length}/>
|
||||||
<button title="undo" onClick={onUndo} disabled={history.history.length === 0}><UndoIcon fontSize="small"/> ({history.history.length})</button>
|
|
||||||
</KeyInfo>
|
|
||||||
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></>}>
|
|
||||||
<button title="redo" onClick={onRedo} disabled={history.future.length === 0}><RedoIcon fontSize="small"/> ({history.future.length})</button>
|
|
||||||
</KeyInfo>
|
|
||||||
 
|
 
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -349,4 +320,4 @@ export function TopPanel({trace, time, setTime, onUndo, onRedo, onInit, onClear,
|
||||||
</div> */}
|
</div> */}
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
});
|
||||||
|
|
|
||||||
17
src/App/TopPanel/UndoRedoButtons.tsx
Normal file
17
src/App/TopPanel/UndoRedoButtons.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { memo } from "react";
|
||||||
|
import { KeyInfoHidden, KeyInfoVisible } from "../KeyInfo";
|
||||||
|
|
||||||
|
import UndoIcon from '@mui/icons-material/Undo';
|
||||||
|
import RedoIcon from '@mui/icons-material/Redo';
|
||||||
|
|
||||||
|
export const UndoRedoButtons = memo(function UndoRedoButtons({showKeys, onUndo, onRedo, historyLength, futureLength}: {showKeys: boolean, onUndo: () => void, onRedo: () => void, historyLength: number, futureLength: number}) {
|
||||||
|
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
|
return <>
|
||||||
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Z</kbd></>}>
|
||||||
|
<button title="undo" onClick={onUndo} disabled={historyLength === 0}><UndoIcon fontSize="small"/> ({historyLength})</button>
|
||||||
|
</KeyInfo>
|
||||||
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></>}>
|
||||||
|
<button title="redo" onClick={onRedo} disabled={futureLength === 0}><RedoIcon fontSize="small"/> ({futureLength})</button>
|
||||||
|
</KeyInfo>
|
||||||
|
</>;
|
||||||
|
});
|
||||||
47
src/App/TopPanel/ZoomButtons.tsx
Normal file
47
src/App/TopPanel/ZoomButtons.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from "@/VisualEditor/parameters";
|
||||||
|
import { Dispatch, memo, SetStateAction, useEffect } from "react";
|
||||||
|
import { KeyInfoHidden, KeyInfoVisible } from "../KeyInfo";
|
||||||
|
|
||||||
|
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
||||||
|
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
|
||||||
|
|
||||||
|
export const ZoomButtons = memo(function ZoomButtons({showKeys, zoom, setZoom}: {showKeys: boolean, zoom: number, setZoom: Dispatch<SetStateAction<number>>}) {
|
||||||
|
|
||||||
|
const KeyInfo = showKeys ? KeyInfoVisible : KeyInfoHidden;
|
||||||
|
|
||||||
|
function onZoomIn() {
|
||||||
|
setZoom(zoom => Math.min(zoom * ZOOM_STEP, ZOOM_MAX));
|
||||||
|
}
|
||||||
|
function onZoomOut() {
|
||||||
|
setZoom(zoom => Math.max(zoom / ZOOM_STEP, ZOOM_MIN));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
if (e.key === "+") {
|
||||||
|
e.preventDefault();
|
||||||
|
onZoomIn();
|
||||||
|
}
|
||||||
|
if (e.key === "-") {
|
||||||
|
e.preventDefault();
|
||||||
|
onZoomOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>-</kbd></>}>
|
||||||
|
<button title="zoom out" onClick={onZoomOut} disabled={zoom <= ZOOM_MIN}><ZoomOutIcon fontSize="small"/></button>
|
||||||
|
</KeyInfo>
|
||||||
|
<input title="current zoom level" value={zoom.toFixed(3)} style={{width:40}} readOnly/>
|
||||||
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>+</kbd></>}>
|
||||||
|
<button title="zoom in" onClick={onZoomIn} disabled={zoom >= ZOOM_MAX}><ZoomInIcon fontSize="small"/></button>
|
||||||
|
</KeyInfo>
|
||||||
|
</>;
|
||||||
|
});
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { memo } from "react";
|
||||||
import { Arrow } from "../statecharts/concrete_syntax";
|
import { Arrow } from "../statecharts/concrete_syntax";
|
||||||
import { ArcDirection, euclideanDistance } from "./geometry";
|
import { ArcDirection, euclideanDistance } from "./geometry";
|
||||||
import { CORNER_HELPER_RADIUS } from "./parameters";
|
import { CORNER_HELPER_RADIUS } from "./parameters";
|
||||||
|
|
||||||
|
|
||||||
export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: string[]; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) {
|
export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: string[]; errors: string[]; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) {
|
||||||
const { start, end, uid } = props.arrow;
|
const { start, end, uid } = props.arrow;
|
||||||
const radius = euclideanDistance(start, end) / 1.6;
|
const radius = euclideanDistance(start, end) / 1.6;
|
||||||
let largeArc = "1";
|
let largeArc = "1";
|
||||||
|
|
@ -78,4 +79,4 @@ export function ArrowSVG(props: { arrow: Arrow; selected: string[]; errors: stri
|
||||||
data-parts="end" />}
|
data-parts="end" />}
|
||||||
|
|
||||||
</g>;
|
</g>;
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Diamond, RountanglePart } from "@/statecharts/concrete_syntax";
|
import { Diamond, RountanglePart } from "@/statecharts/concrete_syntax";
|
||||||
import { rountangleMinSize } from "./VisualEditor";
|
import { rountangleMinSize } from "./VisualEditor";
|
||||||
import { Rect2D, Vec2D } from "./geometry";
|
import { Vec2D } from "./geometry";
|
||||||
import { RectHelper } from "./RectHelpers";
|
import { RectHelper } from "./RectHelpers";
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
export function DiamondShape(props: {size: Vec2D, extraAttrs: object}) {
|
export const DiamondShape = memo(function DiamondShape(props: {size: Vec2D, extraAttrs: object}) {
|
||||||
const minSize = rountangleMinSize(props.size);
|
const minSize = rountangleMinSize(props.size);
|
||||||
return <polygon
|
return <polygon
|
||||||
points={`
|
points={`
|
||||||
|
|
@ -17,9 +18,9 @@ export function DiamondShape(props: {size: Vec2D, extraAttrs: object}) {
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
{...props.extraAttrs}
|
{...props.extraAttrs}
|
||||||
/>;
|
/>;
|
||||||
}
|
});
|
||||||
|
|
||||||
export function DiamondSVG(props: { diamond: Diamond; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
|
export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
|
||||||
const minSize = rountangleMinSize(props.diamond.size);
|
const minSize = rountangleMinSize(props.diamond.size);
|
||||||
const extraAttrs = {
|
const extraAttrs = {
|
||||||
className: ''
|
className: ''
|
||||||
|
|
@ -38,4 +39,4 @@ export function DiamondSVG(props: { diamond: Diamond; selected: string[]; highli
|
||||||
|
|
||||||
<RectHelper uid={props.diamond.uid} size={minSize} highlight={props.highlight} selected={props.selected} />
|
<RectHelper uid={props.diamond.uid} size={minSize} highlight={props.highlight} selected={props.selected} />
|
||||||
</g>;
|
</g>;
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
import { memo } from "react";
|
||||||
import { Vec2D } from "./geometry";
|
import { Vec2D } from "./geometry";
|
||||||
import { HISTORY_RADIUS } from "./parameters";
|
import { HISTORY_RADIUS } from "./parameters";
|
||||||
|
|
||||||
export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|"deep", selected: boolean, highlight: boolean}) {
|
export const HistorySVG = memo(function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|"deep", selected: boolean, highlight: boolean}) {
|
||||||
const text = props.kind === "shallow" ? "H" : "H*";
|
const text = props.kind === "shallow" ? "H" : "H*";
|
||||||
return <>
|
return <>
|
||||||
<circle
|
<circle
|
||||||
|
|
@ -38,4 +39,4 @@ export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|
|
||||||
data-parts="history"
|
data-parts="history"
|
||||||
/>}
|
/>}
|
||||||
</>;
|
</>;
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { memo } from "react";
|
||||||
import { RountanglePart } from "../statecharts/concrete_syntax";
|
import { RountanglePart } 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";
|
||||||
|
|
@ -11,7 +12,7 @@ function lineGeometryProps(size: Vec2D): [RountanglePart, object][] {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RectHelper(props: { uid: string, size: Vec2D, selected: string[], highlight: RountanglePart[] }) {
|
export const RectHelper = memo(function RectHelper(props: { uid: string, size: Vec2D, selected: string[], highlight: RountanglePart[] }) {
|
||||||
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}>
|
||||||
|
|
@ -53,4 +54,4 @@ export function RectHelper(props: { uid: string, size: Vec2D, selected: string[]
|
||||||
data-uid={props.uid}
|
data-uid={props.uid}
|
||||||
data-parts="bottom left" />
|
data-parts="bottom left" />
|
||||||
</>;
|
</>;
|
||||||
}
|
});
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
import { memo } from "react";
|
||||||
import { Rountangle, RountanglePart } from "../statecharts/concrete_syntax";
|
import { Rountangle, RountanglePart } 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";
|
||||||
|
|
||||||
|
|
||||||
export function RountangleSVG(props: { rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
|
export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: string[]; highlight: RountanglePart[]; errors: string[]; active: boolean; }) {
|
||||||
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
|
||||||
|
|
@ -37,4 +38,4 @@ export function RountangleSVG(props: { rountangle: Rountangle; selected: string[
|
||||||
selected={props.selected}
|
selected={props.selected}
|
||||||
highlight={props.highlight} />
|
highlight={props.highlight} />
|
||||||
</g>;
|
</g>;
|
||||||
}
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { TextDialog } from "@/App/TextDialog";
|
import { TextDialog } from "@/App/TextDialog";
|
||||||
import { TraceableError } from "..//statecharts/parser";
|
import { TraceableError } from "..//statecharts/parser";
|
||||||
import {Text} from "../statecharts/concrete_syntax";
|
import {Text} from "../statecharts/concrete_syntax";
|
||||||
import { Dispatch, ReactElement, SetStateAction } from "react";
|
import { Dispatch, memo, ReactElement, SetStateAction } from "react";
|
||||||
|
|
||||||
export function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
|
export const TextSVG = memo(function TextSVG(props: {text: Text, error: TraceableError|undefined, selected: boolean, highlight: boolean, onEdit: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
"data-uid": props.text.uid,
|
"data-uid": props.text.uid,
|
||||||
"data-parts": "text",
|
"data-parts": "text",
|
||||||
|
|
@ -37,11 +37,11 @@ export function TextSVG(props: {text: Text, error: TraceableError|undefined, sel
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
props.setModal(<TextDialog setModal={props.setModal} text={props.text.text} done={newText => {
|
props.setModal(<TextDialog setModal={props.setModal} text={props.text.text} done={newText => {
|
||||||
if (newText) {
|
if (newText) {
|
||||||
props.onEdit(newText);
|
props.onEdit(props.text, newText);
|
||||||
}
|
}
|
||||||
}} />)
|
}} />)
|
||||||
}}>
|
}}>
|
||||||
{textNode}
|
{textNode}
|
||||||
<text className="draggableText helper" textAnchor="middle" data-uid={props.text.uid} data-parts="text" style={{whiteSpace: "preserve"}}>{props.text.text}</text>
|
<text className="draggableText helper" textAnchor="middle" data-uid={props.text.uid} data-parts="text" style={{whiteSpace: "preserve"}}>{props.text.text}</text>
|
||||||
</g>;
|
</g>;
|
||||||
}
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import { ClipboardEvent, Dispatch, ReactElement, SetStateAction, 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, RountanglePart, Text } from "../statecharts/concrete_syntax";
|
||||||
import { parseStatechart, TraceableError } from "../statecharts/parser";
|
import { parseStatechart, TraceableError } from "../statecharts/parser";
|
||||||
import { BigStep } from "../statecharts/runtime_types";
|
|
||||||
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";
|
||||||
import { getBBoxInSvgCoords } from "./svg_helper";
|
import { getBBoxInSvgCoords } from "./svg_helper";
|
||||||
|
|
@ -64,7 +63,7 @@ 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,
|
// ast: Statechart,
|
||||||
setAST: Dispatch<SetStateAction<Statechart>>,
|
setAST: Dispatch<SetStateAction<Statechart>>,
|
||||||
trace: TraceState | null,
|
trace: TraceState | null,
|
||||||
errors: TraceableError[],
|
errors: TraceableError[],
|
||||||
|
|
@ -77,7 +76,7 @@ type VisualEditorProps = {
|
||||||
zoom: number;
|
zoom: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function VisualEditor({state, setState, ast, setAST, trace, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
|
export const VisualEditor = memo(function VisualEditor({state, setState, setAST, trace, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
|
||||||
|
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
|
|
@ -642,7 +641,7 @@ export function VisualEditor({state, setState, ast, setAST, trace, errors, setEr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEditText(text: Text, newText: string) {
|
const onEditText = useCallback((text: Text, newText: string) => {
|
||||||
if (newText === "") {
|
if (newText === "") {
|
||||||
// delete text node
|
// delete text node
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
|
|
@ -666,8 +665,9 @@ export function VisualEditor({state, setState, ast, setAST, trace, errors, setEr
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}, [setState]);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const active = trace && trace.trace[trace.idx].mode || new Set();
|
const active = trace && trace.trace[trace.idx].mode || new Set();
|
||||||
|
|
||||||
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
||||||
|
|
@ -774,14 +774,14 @@ export function VisualEditor({state, setState, ast, setAST, trace, errors, setEr
|
||||||
text={txt}
|
text={txt}
|
||||||
selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)}
|
selected={Boolean(selection.find(s => s.uid === txt.uid)?.parts?.length)}
|
||||||
highlight={textsToHighlight.hasOwnProperty(txt.uid)}
|
highlight={textsToHighlight.hasOwnProperty(txt.uid)}
|
||||||
onEdit={newText => onEditText(txt, newText)}
|
onEdit={onEditText}
|
||||||
setModal={setModal}
|
setModal={setModal}
|
||||||
/>
|
/>
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{selectingState && <Selecting {...selectingState} />}
|
{selectingState && <Selecting {...selectingState} />}
|
||||||
</svg>;
|
</svg>;
|
||||||
}
|
});
|
||||||
|
|
||||||
export function rountangleMinSize(size: Vec2D): Vec2D {
|
export function rountangleMinSize(size: Vec2D): Vec2D {
|
||||||
if (size.x >= 40 && size.y >= 40) {
|
if (size.x >= 40 && size.y >= 40) {
|
||||||
|
|
|
||||||
2
todo.txt
2
todo.txt
|
|
@ -53,6 +53,8 @@ TODO
|
||||||
|
|
||||||
- experimental features:
|
- experimental features:
|
||||||
- multiverse execution history
|
- multiverse execution history
|
||||||
|
stable tree layout?
|
||||||
|
https://pub.dev/packages/ploeg_tree_layout
|
||||||
- local scopes
|
- local scopes
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue