cleanup the code a bit...
This commit is contained in:
parent
1bd801ce5d
commit
7994cd6eb0
19 changed files with 685 additions and 625 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import "../index.css";
|
import "../index.css";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
import { Dispatch, ReactElement, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { detectConnections } from "@/statecharts/detect_connections";
|
import { detectConnections } from "@/statecharts/detect_connections";
|
||||||
import { parseStatechart } from "../statecharts/parser";
|
import { parseStatechart } from "../statecharts/parser";
|
||||||
|
|
@ -50,10 +50,7 @@ export function App() {
|
||||||
const [editHistory, setEditHistory] = useState<EditHistory|null>(null);
|
const [editHistory, setEditHistory] = useState<EditHistory|null>(null);
|
||||||
const [modal, setModal] = useState<ReactElement|null>(null);
|
const [modal, setModal] = useState<ReactElement|null>(null);
|
||||||
|
|
||||||
// const [lightMode, setLightMode] = usePersistentState<LightMode>("lightMode", "auto");
|
const {commitState, replaceState, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
|
||||||
const lightMode = "auto";
|
|
||||||
|
|
||||||
const {makeCheckPoint, onRedo, onUndo, onRotate} = useEditor(setEditHistory);
|
|
||||||
|
|
||||||
const editorState = editHistory && editHistory.current;
|
const editorState = editHistory && editHistory.current;
|
||||||
const setEditorState = useCallback((cb: (value: VisualEditorState) => VisualEditorState) => {
|
const setEditorState = useCallback((cb: (value: VisualEditorState) => VisualEditorState) => {
|
||||||
|
|
@ -172,7 +169,7 @@ export function App() {
|
||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
<div style={{flexGrow: 1, overflow: "auto"}}>
|
<div style={{flexGrow: 1, overflow: "auto"}}>
|
||||||
{editorState && conns && syntaxErrors &&
|
{editorState && conns && syntaxErrors &&
|
||||||
<VisualEditor {...{state: editorState, setState: setEditorState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, makeCheckPoint, ...appState}}/>}
|
<VisualEditor {...{state: editorState, commitState, replaceState, conns, syntaxErrors: allErrors, highlightActive, highlightTransitions, setModal, ...appState}}/>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{appState.showFindReplace &&
|
{appState.showFindReplace &&
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAudioContext } from "@/hooks/useAudioContext";
|
import { useAudioContext } from "@/hooks/useAudioContext";
|
||||||
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
|
import { ConcreteSyntax } from "@/statecharts/concrete_syntax";
|
||||||
import { detectConnections } from "@/statecharts/detect_connections";
|
import { detectConnections } from "@/statecharts/detect_connections";
|
||||||
import { parseStatechart } from "@/statecharts/parser";
|
import { parseStatechart } from "@/statecharts/parser";
|
||||||
import { RT_Statechart } from "@/statecharts/runtime_types";
|
import { RT_Statechart } from "@/statecharts/runtime_types";
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { detectConnections } from "@/statecharts/detect_connections";
|
||||||
import { parseStatechart } from "@/statecharts/parser";
|
import { parseStatechart } from "@/statecharts/parser";
|
||||||
|
|
||||||
import microwaveConcreteSyntax from "./model.json";
|
import microwaveConcreteSyntax from "./model.json";
|
||||||
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
|
import { ConcreteSyntax } from "@/statecharts/concrete_syntax";
|
||||||
import { objectsEqual } from "@/util/util";
|
import { objectsEqual } from "@/util/util";
|
||||||
|
|
||||||
export const [microwaveAbstractSyntax, microwaveErrors] = parseStatechart(microwaveConcreteSyntax as ConcreteSyntax, detectConnections(microwaveConcreteSyntax as ConcreteSyntax));
|
export const [microwaveAbstractSyntax, microwaveErrors] = parseStatechart(microwaveConcreteSyntax as ConcreteSyntax, detectConnections(microwaveConcreteSyntax as ConcreteSyntax));
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { preload } from "react-dom";
|
||||||
|
|
||||||
import trafficLightConcreteSyntax from "./model.json";
|
import trafficLightConcreteSyntax from "./model.json";
|
||||||
import { parseStatechart } from "@/statecharts/parser";
|
import { parseStatechart } from "@/statecharts/parser";
|
||||||
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
|
import { ConcreteSyntax } from "@/statecharts/concrete_syntax";
|
||||||
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";
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ export function SideBar({showExecutionTrace, showConnections, plantName, showPla
|
||||||
inputEvents={ast.inputEvents}
|
inputEvents={ast.inputEvents}
|
||||||
onRaise={(e,p) => onRaise("debug."+e,p)}
|
onRaise={(e,p) => onRaise("debug."+e,p)}
|
||||||
disabled={trace===null || trace.trace[trace.idx].kind === "error"}
|
disabled={trace===null || trace.trace[trace.idx].kind === "error"}
|
||||||
showKeys={true}/>}
|
/>}
|
||||||
</PersistentDetails>
|
</PersistentDetails>
|
||||||
{/* Internal events */}
|
{/* Internal events */}
|
||||||
<PersistentDetails state={showInternalEvents} setState={setShowInternalEvents}>
|
<PersistentDetails state={showInternalEvents} setState={setShowInternalEvents}>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { memo } from "react";
|
||||||
import { Arrow, ArrowPart } from "../../statecharts/concrete_syntax";
|
import { Arrow, ArrowPart } from "../../statecharts/concrete_syntax";
|
||||||
import { ArcDirection, euclideanDistance } from "../../util/geometry";
|
import { ArcDirection, euclideanDistance } from "../../util/geometry";
|
||||||
import { CORNER_HELPER_RADIUS } from "../parameters";
|
import { CORNER_HELPER_RADIUS } from "../parameters";
|
||||||
import { arraysEqual } from "@/util/util";
|
import { arraysEqual, jsonDeepEqual } from "@/util/util";
|
||||||
|
|
||||||
|
|
||||||
export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: ArrowPart[]; error: string; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) {
|
export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: ArrowPart[]; error: string; highlight: boolean; fired: boolean; arc: ArcDirection; initialMarker: boolean }) {
|
||||||
|
|
@ -81,7 +81,7 @@ export const ArrowSVG = memo(function(props: { arrow: Arrow; selected: ArrowPart
|
||||||
|
|
||||||
</g>;
|
</g>;
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
return prevProps.arrow === nextProps.arrow
|
return jsonDeepEqual(prevProps.arrow, nextProps.arrow)
|
||||||
&& arraysEqual(prevProps.selected, nextProps.selected)
|
&& arraysEqual(prevProps.selected, nextProps.selected)
|
||||||
&& prevProps.highlight === nextProps.highlight
|
&& prevProps.highlight === nextProps.highlight
|
||||||
&& prevProps.error === nextProps.error
|
&& prevProps.error === nextProps.error
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { rountangleMinSize } from "@/statecharts/concrete_syntax";
|
||||||
import { Vec2D } from "../../util/geometry";
|
import { Vec2D } from "../../util/geometry";
|
||||||
import { RectHelper } from "./RectHelpers";
|
import { RectHelper } from "./RectHelpers";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { arraysEqual } from "@/util/util";
|
import { arraysEqual, jsonDeepEqual } from "@/util/util";
|
||||||
|
|
||||||
export const DiamondShape = memo(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);
|
||||||
|
|
@ -42,7 +42,7 @@ export const DiamondSVG = memo(function DiamondSVG(props: { diamond: Diamond; se
|
||||||
<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>;
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
return prevProps.diamond === nextProps.diamond
|
return jsonDeepEqual(prevProps.diamond, nextProps.diamond)
|
||||||
&& arraysEqual(prevProps.selected, nextProps.selected)
|
&& arraysEqual(prevProps.selected, nextProps.selected)
|
||||||
&& arraysEqual(prevProps.highlight, nextProps.highlight)
|
&& arraysEqual(prevProps.highlight, nextProps.highlight)
|
||||||
&& prevProps.error === nextProps.error
|
&& prevProps.error === nextProps.error
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ 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 "@/statecharts/concrete_syntax";
|
import { rountangleMinSize } from "@/statecharts/concrete_syntax";
|
||||||
import { arraysEqual } from "@/util/util";
|
import { arraysEqual, jsonDeepEqual } from "@/util/util";
|
||||||
|
|
||||||
|
|
||||||
export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: RectSide[]; highlight: RectSide[]; error?: string; active: boolean; }) {
|
export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rountangle; selected: RectSide[]; highlight: RectSide[]; error?: string; active: boolean; }) {
|
||||||
|
|
@ -40,7 +40,7 @@ export const RountangleSVG = memo(function RountangleSVG(props: {rountangle: Rou
|
||||||
highlight={props.highlight} />
|
highlight={props.highlight} />
|
||||||
</g>;
|
</g>;
|
||||||
}, (prevProps, nextProps) => {
|
}, (prevProps, nextProps) => {
|
||||||
return prevProps.rountangle === nextProps.rountangle
|
return jsonDeepEqual(prevProps.rountangle, nextProps.rountangle)
|
||||||
&& arraysEqual(prevProps.selected, nextProps.selected)
|
&& arraysEqual(prevProps.selected, nextProps.selected)
|
||||||
&& arraysEqual(prevProps.highlight, nextProps.highlight)
|
&& arraysEqual(prevProps.highlight, nextProps.highlight)
|
||||||
&& prevProps.error === nextProps.error
|
&& prevProps.error === nextProps.error
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { TextDialog } from "@/App/Modals/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, memo, ReactElement, SetStateAction } from "react";
|
import { Dispatch, memo, ReactElement, SetStateAction } from "react";
|
||||||
|
import { jsonDeepEqual } from "@/util/util";
|
||||||
|
|
||||||
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>>}) {
|
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 = {
|
||||||
|
|
@ -44,4 +45,11 @@ export const TextSVG = memo(function TextSVG(props: {text: Text, error: Traceabl
|
||||||
{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>;
|
||||||
|
}, (prevProps, newProps) => {
|
||||||
|
return jsonDeepEqual(prevProps.text, newProps)
|
||||||
|
&& prevProps.highlight === newProps.highlight
|
||||||
|
&& prevProps.onEdit === newProps.onEdit
|
||||||
|
&& prevProps.setModal === newProps.setModal
|
||||||
|
&& prevProps.error === newProps.error
|
||||||
|
&& prevProps.selected === newProps.selected
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef } from "react";
|
import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
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 { ArrowPart, ConcreteSyntax, Diamond, 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";
|
||||||
|
|
@ -16,14 +16,6 @@ import "./VisualEditor.css";
|
||||||
import { useCopyPaste } from "./hooks/useCopyPaste";
|
import { useCopyPaste } from "./hooks/useCopyPaste";
|
||||||
import { useMouse } from "./hooks/useMouse";
|
import { useMouse } from "./hooks/useMouse";
|
||||||
|
|
||||||
export type ConcreteSyntax = {
|
|
||||||
rountangles: Rountangle[];
|
|
||||||
texts: Text[];
|
|
||||||
arrows: Arrow[];
|
|
||||||
diamonds: Diamond[];
|
|
||||||
history: History[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type VisualEditorState = ConcreteSyntax & {
|
export type VisualEditorState = ConcreteSyntax & {
|
||||||
nextID: number;
|
nextID: number;
|
||||||
selection: Selection;
|
selection: Selection;
|
||||||
|
|
@ -51,23 +43,26 @@ export type Selection = Selectable[];
|
||||||
|
|
||||||
type VisualEditorProps = {
|
type VisualEditorProps = {
|
||||||
state: VisualEditorState,
|
state: VisualEditorState,
|
||||||
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
commitState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
||||||
|
replaceState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
||||||
conns: Connections,
|
conns: Connections,
|
||||||
syntaxErrors: TraceableError[],
|
syntaxErrors: TraceableError[],
|
||||||
// trace: TraceState | null,
|
|
||||||
// activeStates: Set<string>,
|
|
||||||
insertMode: InsertMode,
|
insertMode: InsertMode,
|
||||||
highlightActive: Set<string>,
|
highlightActive: Set<string>,
|
||||||
highlightTransitions: string[],
|
highlightTransitions: string[],
|
||||||
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
||||||
makeCheckPoint: () => void;
|
|
||||||
zoom: number;
|
zoom: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VisualEditor = memo(function VisualEditor({state, setState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) {
|
export const VisualEditor = memo(function VisualEditor({state, commitState, replaceState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, zoom}: VisualEditorProps) {
|
||||||
|
|
||||||
|
// While dragging, the editor is in a temporary state (a state that is not committed to the edit history). If the temporary state is not null, then this state will be what you see.
|
||||||
|
// const [temporaryState, setTemporaryState] = useState<VisualEditorState | null>(null);
|
||||||
|
|
||||||
|
// const state = temporaryState || committedState;
|
||||||
|
|
||||||
// uid's of selected rountangles
|
// uid's of selected rountangles
|
||||||
const selection = state.selection || [];
|
const selection = state.selection;
|
||||||
|
|
||||||
const refSVG = useRef<SVGSVGElement>(null);
|
const refSVG = useRef<SVGSVGElement>(null);
|
||||||
|
|
||||||
|
|
@ -86,9 +81,12 @@ export const VisualEditor = memo(function VisualEditor({state, setState, conns,
|
||||||
}, [highlightTransitions]);
|
}, [highlightTransitions]);
|
||||||
|
|
||||||
|
|
||||||
const {onCopy, onPaste, onCut, deleteSelection} = useCopyPaste(makeCheckPoint, state, setState, selection);
|
const {onCopy, onPaste, onCut} = useCopyPaste(state, commitState, selection);
|
||||||
|
|
||||||
const {onMouseDown, selectionRect} = useMouse(makeCheckPoint, insertMode, zoom, refSVG, state, setState, deleteSelection);
|
const {onMouseDown, selectionRect} = useMouse(insertMode, zoom, refSVG,
|
||||||
|
state,
|
||||||
|
commitState,
|
||||||
|
replaceState);
|
||||||
|
|
||||||
|
|
||||||
// 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.
|
||||||
|
|
@ -138,13 +136,13 @@ export const VisualEditor = memo(function VisualEditor({state, setState, conns,
|
||||||
const onEditText = useCallback((text: Text, newText: string) => {
|
const onEditText = useCallback((text: Text, newText: string) => {
|
||||||
if (newText === "") {
|
if (newText === "") {
|
||||||
// delete text node
|
// delete text node
|
||||||
setState(state => ({
|
commitState(state => ({
|
||||||
...state,
|
...state,
|
||||||
texts: state.texts.filter(t => t.uid !== text.uid),
|
texts: state.texts.filter(t => t.uid !== text.uid),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
setState(state => ({
|
commitState(state => ({
|
||||||
...state,
|
...state,
|
||||||
texts: state.texts.map(t => {
|
texts: state.texts.map(t => {
|
||||||
if (t.uid === text.uid) {
|
if (t.uid === text.uid) {
|
||||||
|
|
@ -159,14 +157,14 @@ export const VisualEditor = memo(function VisualEditor({state, setState, conns,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [setState]);
|
}, [commitState]);
|
||||||
|
|
||||||
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
||||||
|
|
||||||
const size = 4000*zoom;
|
const size = 4000*zoom;
|
||||||
|
|
||||||
return <svg width={size} height={size}
|
return <svg width={size} height={size}
|
||||||
className={"svgCanvas"+(highlightActive.has("root")?" active":"")/*+(dragging ? " dragging" : "")*/}
|
className={"svgCanvas"+(highlightActive.has("root")?" active":"")}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
ref={refSVG}
|
ref={refSVG}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,13 @@ import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
// const offset = {x: 40, y: 40};
|
// const offset = {x: 40, y: 40};
|
||||||
const offset = {x: 0, y: 0};
|
const offset = {x: 0, y: 0};
|
||||||
|
|
||||||
export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorState, setState: Dispatch<(v:VisualEditorState) => VisualEditorState>, selection: Selection) {
|
export function useCopyPaste(state: VisualEditorState, commitState: Dispatch<(v:VisualEditorState) => VisualEditorState>, selection: Selection) {
|
||||||
const onPaste = useCallback((e: ClipboardEvent) => {
|
const onPaste = useCallback((e: ClipboardEvent) => {
|
||||||
const data = e.clipboardData?.getData("text/plain");
|
const data = e.clipboardData?.getData("text/plain");
|
||||||
if (data) {
|
if (data) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data);
|
const parsed = JSON.parse(data);
|
||||||
makeCheckPoint();
|
commitState(state => {
|
||||||
setState(state => {
|
|
||||||
try {
|
try {
|
||||||
let nextID = state.nextID;
|
let nextID = state.nextID;
|
||||||
const copiedRountangles: Rountangle[] = parsed.rountangles.map((r: Rountangle) => ({
|
const copiedRountangles: Rountangle[] = parsed.rountangles.map((r: Rountangle) => ({
|
||||||
|
|
@ -73,7 +72,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}, [makeCheckPoint, setState]);
|
}, [commitState]);
|
||||||
|
|
||||||
const copyInternal = useCallback((state: VisualEditorState, selection: Selection, e: ClipboardEvent) => {
|
const copyInternal = useCallback((state: VisualEditorState, selection: Selection, e: ClipboardEvent) => {
|
||||||
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
||||||
|
|
@ -107,8 +106,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
|
||||||
}, [state, selection]);
|
}, [state, selection]);
|
||||||
|
|
||||||
const deleteSelection = useCallback(() => {
|
const deleteSelection = useCallback(() => {
|
||||||
makeCheckPoint();
|
commitState(state => ({
|
||||||
setState(state => ({
|
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)),
|
rountangles: state.rountangles.filter(r => !state.selection.some(rs => rs.uid === r.uid)),
|
||||||
diamonds: state.diamonds.filter(d => !state.selection.some(ds => ds.uid === d.uid)),
|
diamonds: state.diamonds.filter(d => !state.selection.some(ds => ds.uid === d.uid)),
|
||||||
|
|
@ -117,7 +115,7 @@ export function useCopyPaste(makeCheckPoint: () => void, state: VisualEditorStat
|
||||||
texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)),
|
texts: state.texts.filter(t => !state.selection.some(ts => ts.uid === t.uid)),
|
||||||
selection: [],
|
selection: [],
|
||||||
}));
|
}));
|
||||||
}, [makeCheckPoint, setState]);
|
}, [commitState]);
|
||||||
|
|
||||||
useShortcuts([
|
useShortcuts([
|
||||||
{keys: ["Delete"], action: deleteSelection},
|
{keys: ["Delete"], action: deleteSelection},
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,14 @@ import { Selecting, SelectingState } from "../Selection";
|
||||||
import { Selection, VisualEditorState } from "../VisualEditor";
|
import { Selection, VisualEditorState } from "../VisualEditor";
|
||||||
import { useShortcuts } from "@/hooks/useShortcuts";
|
import { useShortcuts } from "@/hooks/useShortcuts";
|
||||||
|
|
||||||
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(
|
||||||
|
insertMode: InsertMode,
|
||||||
|
zoom: number,
|
||||||
|
refSVG: {current: SVGSVGElement|null},
|
||||||
|
state: VisualEditorState,
|
||||||
|
commitState: Dispatch<(v: VisualEditorState) => VisualEditorState>,
|
||||||
|
replaceState: Dispatch<(v: VisualEditorState) => VisualEditorState>)
|
||||||
|
{
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
const [shiftOrCtrlPressed, setShiftOrCtrlPressed] = useState(false);
|
const [shiftOrCtrlPressed, setShiftOrCtrlPressed] = useState(false);
|
||||||
|
|
||||||
|
|
@ -16,8 +23,13 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
const [selectingState, setSelectingState] = useState<SelectingState>(null);
|
const [selectingState, setSelectingState] = useState<SelectingState>(null);
|
||||||
|
|
||||||
const selection = state.selection;
|
const selection = state.selection;
|
||||||
const setSelection = useCallback((cb: (oldSelection: Selection) => Selection) =>
|
const commitSelection = useCallback((cb: (oldSelection: Selection) => Selection) => {
|
||||||
setState(oldState => ({...oldState, selection: cb(oldState.selection)})),[setState]);
|
commitState(oldState => ({...oldState, selection: cb(oldState.selection)}));
|
||||||
|
},[commitState]);
|
||||||
|
|
||||||
|
const replaceSelection = useCallback((cb: (oldSelection: Selection) => Selection) =>
|
||||||
|
replaceState(oldState => ({...oldState, selection: cb(oldState.selection)})),[replaceState]);
|
||||||
|
|
||||||
|
|
||||||
const getCurrentPointer = useCallback((e: {pageX: number, pageY: number}) => {
|
const getCurrentPointer = useCallback((e: {pageX: number, pageY: number}) => {
|
||||||
const bbox = refSVG.current!.getBoundingClientRect();
|
const bbox = refSVG.current!.getBoundingClientRect();
|
||||||
|
|
@ -30,9 +42,8 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
const onMouseDown = useCallback((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();
|
|
||||||
// ignore selection, right mouse button always inserts
|
// ignore selection, right mouse button always inserts
|
||||||
setState(state => {
|
commitState(state => {
|
||||||
const newID = state.nextID.toString();
|
const newID = state.nextID.toString();
|
||||||
if (insertMode === "and" || insertMode === "or") {
|
if (insertMode === "and" || insertMode === "or") {
|
||||||
// insert rountangle
|
// insert rountangle
|
||||||
|
|
@ -102,64 +113,81 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.button === 0) {
|
let appendTo: Selection;
|
||||||
if (!shiftOrCtrlPressed) {
|
if (shiftOrCtrlPressed) {
|
||||||
// left mouse button on a shape will drag that shape (and everything else that's selected). if the shape under the pointer was not in the selection then the selection is reset to contain only that shape.
|
appendTo = selection;
|
||||||
const uid = e.target?.dataset.uid;
|
}
|
||||||
const parts: string[] = e.target?.dataset.parts?.split(' ').filter((p:string) => p!=="") || [];
|
else {
|
||||||
if (uid && parts.length > 0) {
|
appendTo = [];
|
||||||
makeCheckPoint();
|
}
|
||||||
|
|
||||||
// if the mouse button is pressed outside of the current selection, we reset the selection to whatever shape the mouse is on
|
const startMakingSelection = () => {
|
||||||
let allPartsInSelection = true;
|
setDragging(false);
|
||||||
for (const part of parts) {
|
setSelectingState({
|
||||||
// is there anything in our existing selection that is not under the cursor?
|
topLeft: currentPointer,
|
||||||
if (!(selection.some(s => (s.uid === uid) && (s.part === part)))) {
|
size: {x: 0, y: 0},
|
||||||
allPartsInSelection = false;
|
});
|
||||||
break;
|
commitSelection(_ => appendTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.button === 0) {
|
||||||
|
// left mouse button
|
||||||
|
const uid = e.target?.dataset.uid;
|
||||||
|
const parts: string[] = e.target?.dataset.parts?.split(' ').filter((p:string) => p!=="") || [];
|
||||||
|
if (uid && parts.length > 0) {
|
||||||
|
// mouse hovers over a shape or part of a shape
|
||||||
|
let allPartsInSelection = true;
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!(selection.some(s => (s.uid === uid) && (s.part === part)))) {
|
||||||
|
allPartsInSelection = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (!allPartsInSelection) {
|
}
|
||||||
if (e.target.classList.contains("helper")) {
|
if (!allPartsInSelection) {
|
||||||
setSelection(() => parts.map(part => ({uid, part})) as Selection);
|
// the part is not in existing selection
|
||||||
}
|
if (e.target.classList.contains("helper")) {
|
||||||
else {
|
// it's only a helper
|
||||||
setDragging(false);
|
// -> update selection by the part and start dragging it
|
||||||
setSelectingState({
|
commitSelection(() => [
|
||||||
topLeft: currentPointer,
|
...appendTo,
|
||||||
size: {x: 0, y: 0},
|
...parts.map(part => ({uid, part})) as Selection,
|
||||||
});
|
]);
|
||||||
setSelection(() => []);
|
setDragging(true);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// start dragging
|
else {
|
||||||
|
// it's an actual shape
|
||||||
|
// (we treat shapes differently from helpers because in a big hierarchical model it is nearly impossible to click anywhere without clicking inside a shape)
|
||||||
|
startMakingSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// the part is in existing selection
|
||||||
|
// -> just start dragging
|
||||||
|
commitSelection(s => s); // <-- but also create an undo-checkpoint!
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// mouse is not on any shape
|
||||||
|
startMakingSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
// otherwise, just start making a selection
|
// any other mouse button (e.g., middle mouse button)
|
||||||
setDragging(false);
|
// -> just start making a selection
|
||||||
setSelectingState({
|
startMakingSelection();
|
||||||
topLeft: currentPointer,
|
|
||||||
size: {x: 0, y: 0},
|
|
||||||
});
|
|
||||||
if (!shiftOrCtrlPressed) {
|
|
||||||
setSelection(() => []);
|
|
||||||
}
|
}
|
||||||
}, [getCurrentPointer, makeCheckPoint, insertMode, selection, shiftOrCtrlPressed]);
|
}, [commitState, commitSelection, getCurrentPointer, insertMode, selection, shiftOrCtrlPressed]);
|
||||||
|
|
||||||
const onMouseMove = useCallback((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);
|
// we're moving / resizing
|
||||||
const pointerDelta = {x: e.movementX/zoom, y: e.movementY/zoom};
|
const pointerDelta = {x: e.movementX/zoom, y: e.movementY/zoom};
|
||||||
const getParts = (uid: string) => {
|
const getParts = (uid: string) => {
|
||||||
return state.selection.filter(s => s.uid === uid).map(s => s.part);
|
return selection.filter(s => s.uid === uid).map(s => s.part);
|
||||||
}
|
}
|
||||||
setState(state => ({
|
replaceState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.map(r => {
|
rountangles: state.rountangles.map(r => {
|
||||||
const selectedParts = getParts(r.uid);
|
const selectedParts = getParts(r.uid);
|
||||||
|
|
@ -216,6 +244,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
}
|
}
|
||||||
else if (selectingState) {
|
else if (selectingState) {
|
||||||
|
// we're making a selection
|
||||||
setSelectingState(ss => {
|
setSelectingState(ss => {
|
||||||
const selectionSize = subtractV2D(currentPointer, ss!.topLeft);
|
const selectionSize = subtractV2D(currentPointer, ss!.topLeft);
|
||||||
return {
|
return {
|
||||||
|
|
@ -224,13 +253,15 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [getCurrentPointer, selectingState, dragging]);
|
}, [replaceState, getCurrentPointer, selectingState, setSelectingState, selection, dragging]);
|
||||||
|
|
||||||
const onMouseUp = useCallback((e: {target: any, pageX: number, pageY: number}) => {
|
const onMouseUp = useCallback((e: {target: any, pageX: number, pageY: number}) => {
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
|
// we were moving / resizing
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
|
|
||||||
// do not persist sizes smaller than 40x40
|
// do not persist sizes smaller than 40x40
|
||||||
setState(state => {
|
replaceState(state => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.map(r => ({
|
rountangles: state.rountangles.map(r => ({
|
||||||
|
|
@ -245,12 +276,16 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (selectingState) {
|
if (selectingState) {
|
||||||
|
// we were making a selection
|
||||||
if (selectingState.size.x === 0 && selectingState.size.y === 0) {
|
if (selectingState.size.x === 0 && selectingState.size.y === 0) {
|
||||||
|
// it was only a click (mouse didn't move)
|
||||||
|
// -> select the clicked part(s)
|
||||||
|
// (btw, this is only here to allow selecting rountangles by clicking inside them, all other shapes can be selected entirely by their 'helpers')
|
||||||
const uid = e.target?.dataset.uid;
|
const uid = e.target?.dataset.uid;
|
||||||
if (uid) {
|
if (uid) {
|
||||||
const parts = e.target?.dataset.parts.split(' ').filter((p: string) => p!=="") || [];
|
const parts = e.target?.dataset.parts.split(' ').filter((p: string) => p!=="") || [];
|
||||||
if (uid) {
|
if (uid) {
|
||||||
setSelection(oldSelection => [
|
replaceSelection(oldSelection => [
|
||||||
...oldSelection,
|
...oldSelection,
|
||||||
...parts.map((part: string) => ({uid, part})),
|
...parts.map((part: string) => ({uid, part})),
|
||||||
]);
|
]);
|
||||||
|
|
@ -258,7 +293,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// we were making a selection
|
// complete selection
|
||||||
const normalizedSS = normalizeRect(selectingState);
|
const normalizedSS = normalizeRect(selectingState);
|
||||||
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
|
const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
|
||||||
const shapesInSelection = shapes.filter(el => {
|
const shapesInSelection = shapes.filter(el => {
|
||||||
|
|
@ -271,9 +306,8 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
}).filter(el => !el.classList.contains("corner"));
|
}).filter(el => !el.classList.contains("corner"));
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
setSelection(oldSelection => {
|
replaceSelection(oldSelection => {
|
||||||
const newSelection = [...oldSelection];
|
const newSelection = [...oldSelection];
|
||||||
const common = [];
|
|
||||||
for (const shape of shapesInSelection) {
|
for (const shape of shapesInSelection) {
|
||||||
const uid = shape.dataset.uid;
|
const uid = shape.dataset.uid;
|
||||||
if (uid) {
|
if (uid) {
|
||||||
|
|
@ -281,8 +315,6 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (newSelection.some(({uid: oldUid, part: oldPart}) =>
|
if (newSelection.some(({uid: oldUid, part: oldPart}) =>
|
||||||
uid === oldUid && part === oldPart)) {
|
uid === oldUid && part === oldPart)) {
|
||||||
// common.push({uid, part});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -291,14 +323,12 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log({newSelection, oldSelection, common});
|
|
||||||
// return [...oldSelection, ...newSelection];
|
|
||||||
return newSelection;
|
return newSelection;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSelectingState(null); // no longer making a selection
|
setSelectingState(null); // no longer making a selection
|
||||||
}, [dragging, selectingState, refSVG.current]);
|
}, [replaceState, replaceSelection, dragging, selectingState, setSelectingState, refSVG.current]);
|
||||||
|
|
||||||
const trackShiftKey = useCallback((e: KeyboardEvent) => {
|
const trackShiftKey = useCallback((e: KeyboardEvent) => {
|
||||||
setShiftOrCtrlPressed(e.shiftKey || e.ctrlKey);
|
setShiftOrCtrlPressed(e.shiftKey || e.ctrlKey);
|
||||||
|
|
@ -306,7 +336,7 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
|
|
||||||
const onSelectAll = useCallback(() => {
|
const onSelectAll = useCallback(() => {
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
setState(state => ({
|
commitState(state => ({
|
||||||
...state,
|
...state,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
selection: [
|
selection: [
|
||||||
|
|
@ -317,15 +347,14 @@ export function useMouse(makeCheckPoint: () => void, insertMode: InsertMode, zoo
|
||||||
...state.history.map(h => ({uid: h.uid, part: "history"})),
|
...state.history.map(h => ({uid: h.uid, part: "history"})),
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
}, [setState, setDragging]);
|
}, [commitState, setDragging]);
|
||||||
|
|
||||||
const convertSelection = useCallback((kind: "or"|"and") => {
|
const convertSelection = useCallback((kind: "or"|"and") => {
|
||||||
makeCheckPoint();
|
commitState(state => ({
|
||||||
setState(state => ({
|
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind}) : r),
|
rountangles: state.rountangles.map(r => state.selection.some(rs => rs.uid === r.uid) ? ({...r, kind}) : r),
|
||||||
}));
|
}));
|
||||||
}, [makeCheckPoint, setState]);
|
}, [commitState]);
|
||||||
|
|
||||||
useShortcuts([
|
useShortcuts([
|
||||||
{keys: ["o"], action: useCallback(() => convertSelection("or"), [convertSelection])},
|
{keys: ["o"], action: useCallback(() => convertSelection("or"), [convertSelection])},
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { addV2D, rotateLine90CCW, rotateLine90CW, rotatePoint90CCW, rotatePoint9
|
||||||
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 { jsonDeepEqual } from "@/util/util";
|
||||||
|
import { VisualEditorState } from "../VisualEditor/VisualEditor";
|
||||||
|
|
||||||
export function useEditor(setEditHistory: Dispatch<SetStateAction<EditHistory|null>>) {
|
export function useEditor(setEditHistory: Dispatch<SetStateAction<EditHistory|null>>) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -11,13 +13,27 @@ export function useEditor(setEditHistory: Dispatch<SetStateAction<EditHistory|nu
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// append editor state to undo history
|
const commitState = useCallback((callback: (oldState: VisualEditorState) => VisualEditorState) => {
|
||||||
const makeCheckPoint = useCallback(() => {
|
setEditHistory(historyState => {
|
||||||
setEditHistory(historyState => historyState && ({
|
if (historyState === null) return null; // no change
|
||||||
...historyState,
|
const newEditorState = callback(historyState.current);
|
||||||
history: [...historyState.history, historyState.current],
|
return {
|
||||||
future: [],
|
current: newEditorState,
|
||||||
}));
|
history: [...historyState.history, historyState.current],
|
||||||
|
future: [],
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
});
|
||||||
|
}, [setEditHistory]);
|
||||||
|
const replaceState = useCallback((callback: (oldState: VisualEditorState) => VisualEditorState) => {
|
||||||
|
setEditHistory(historyState => {
|
||||||
|
if (historyState === null) return null; // no change
|
||||||
|
const newEditorState = callback(historyState.current);
|
||||||
|
return {
|
||||||
|
...historyState,
|
||||||
|
current: newEditorState,
|
||||||
|
};
|
||||||
|
});
|
||||||
}, [setEditHistory]);
|
}, [setEditHistory]);
|
||||||
const onUndo = useCallback(() => {
|
const onUndo = useCallback(() => {
|
||||||
setEditHistory(historyState => {
|
setEditHistory(historyState => {
|
||||||
|
|
@ -46,62 +62,54 @@ export function useEditor(setEditHistory: Dispatch<SetStateAction<EditHistory|nu
|
||||||
});
|
});
|
||||||
}, [setEditHistory]);
|
}, [setEditHistory]);
|
||||||
const onRotate = useCallback((direction: "ccw" | "cw") => {
|
const onRotate = useCallback((direction: "ccw" | "cw") => {
|
||||||
makeCheckPoint();
|
commitState(editorState => {
|
||||||
setEditHistory(historyState => {
|
const selection = editorState.selection;
|
||||||
if (historyState === null) return null;
|
|
||||||
|
|
||||||
const selection = historyState.current.selection;
|
|
||||||
|
|
||||||
if (selection.length === 0) {
|
if (selection.length === 0) {
|
||||||
return historyState;
|
return editorState;
|
||||||
}
|
}
|
||||||
|
|
||||||
// determine bounding box... in a convoluted manner
|
// determine bounding box... in a convoluted manner
|
||||||
let minX = -Infinity, minY = -Infinity, maxX = Infinity, maxY = Infinity;
|
let minX = -Infinity, minY = -Infinity, maxX = Infinity, maxY = Infinity;
|
||||||
|
|
||||||
function addPointToBBox({x,y}: Vec2D) {
|
function addPointToBBox({x,y}: Vec2D) {
|
||||||
minX = Math.max(minX, x);
|
minX = Math.max(minX, x);
|
||||||
minY = Math.max(minY, y);
|
minY = Math.max(minY, y);
|
||||||
maxX = Math.min(maxX, x);
|
maxX = Math.min(maxX, x);
|
||||||
maxY = Math.min(maxY, y);
|
maxY = Math.min(maxY, y);
|
||||||
}
|
}
|
||||||
|
for (const rt of editorState.rountangles) {
|
||||||
for (const rt of historyState.current.rountangles) {
|
|
||||||
if (selection.some(s => s.uid === rt.uid)) {
|
if (selection.some(s => s.uid === rt.uid)) {
|
||||||
addPointToBBox(rt.topLeft);
|
addPointToBBox(rt.topLeft);
|
||||||
addPointToBBox(addV2D(rt.topLeft, rt.size));
|
addPointToBBox(addV2D(rt.topLeft, rt.size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const d of historyState.current.diamonds) {
|
for (const d of editorState.diamonds) {
|
||||||
if (selection.some(s => s.uid === d.uid)) {
|
if (selection.some(s => s.uid === d.uid)) {
|
||||||
addPointToBBox(d.topLeft);
|
addPointToBBox(d.topLeft);
|
||||||
addPointToBBox(addV2D(d.topLeft, d.size));
|
addPointToBBox(addV2D(d.topLeft, d.size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const arr of historyState.current.arrows) {
|
for (const arr of editorState.arrows) {
|
||||||
if (selection.some(s => s.uid === arr.uid)) {
|
if (selection.some(s => s.uid === arr.uid)) {
|
||||||
addPointToBBox(arr.start);
|
addPointToBBox(arr.start);
|
||||||
addPointToBBox(arr.end);
|
addPointToBBox(arr.end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const txt of historyState.current.texts) {
|
for (const txt of editorState.texts) {
|
||||||
if (selection.some(s => s.uid === txt.uid)) {
|
if (selection.some(s => s.uid === txt.uid)) {
|
||||||
addPointToBBox(txt.topLeft);
|
addPointToBBox(txt.topLeft);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const historySize = {x: HISTORY_RADIUS, y: HISTORY_RADIUS};
|
const historySize = {x: HISTORY_RADIUS, y: HISTORY_RADIUS};
|
||||||
for (const h of historyState.current.history) {
|
for (const h of editorState.history) {
|
||||||
if (selection.some(s => s.uid === h.uid)) {
|
if (selection.some(s => s.uid === h.uid)) {
|
||||||
addPointToBBox(h.topLeft);
|
addPointToBBox(h.topLeft);
|
||||||
addPointToBBox(addV2D(h.topLeft, scaleV2D(historySize, 2)));
|
addPointToBBox(addV2D(h.topLeft, scaleV2D(historySize, 2)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const center: Vec2D = {
|
const center: Vec2D = {
|
||||||
x: (minX + maxX) / 2,
|
x: (minX + maxX) / 2,
|
||||||
y: (minY + maxY) / 2,
|
y: (minY + maxY) / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapIfSelected = (shape: {uid: string}, cb: (shape:any)=>any) => {
|
const mapIfSelected = (shape: {uid: string}, cb: (shape:any)=>any) => {
|
||||||
if (selection.some(s => s.uid === shape.uid)) {
|
if (selection.some(s => s.uid === shape.uid)) {
|
||||||
return cb(shape);
|
return cb(shape);
|
||||||
|
|
@ -110,56 +118,51 @@ export function useEditor(setEditHistory: Dispatch<SetStateAction<EditHistory|nu
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...historyState,
|
...editorState,
|
||||||
current: {
|
rountangles: editorState.rountangles.map(rt => mapIfSelected(rt, rt => {
|
||||||
...historyState.current,
|
return {
|
||||||
rountangles: historyState.current.rountangles.map(rt => mapIfSelected(rt, rt => {
|
...rt,
|
||||||
return {
|
...(direction === "ccw"
|
||||||
...rt,
|
? rotateRect90CCW(rt, center)
|
||||||
...(direction === "ccw"
|
: rotateRect90CW(rt, center)),
|
||||||
? rotateRect90CCW(rt, center)
|
}
|
||||||
: rotateRect90CW(rt, center)),
|
})),
|
||||||
}
|
arrows: editorState.arrows.map(arr => mapIfSelected(arr, arr => {
|
||||||
})),
|
return {
|
||||||
arrows: historyState.current.arrows.map(arr => mapIfSelected(arr, arr => {
|
...arr,
|
||||||
return {
|
...(direction === "ccw"
|
||||||
...arr,
|
? rotateLine90CCW(arr, center)
|
||||||
...(direction === "ccw"
|
: rotateLine90CW(arr, center)),
|
||||||
? rotateLine90CCW(arr, center)
|
};
|
||||||
: rotateLine90CW(arr, center)),
|
})),
|
||||||
};
|
diamonds: editorState.diamonds.map(d => mapIfSelected(d, d => {
|
||||||
})),
|
return {
|
||||||
diamonds: historyState.current.diamonds.map(d => mapIfSelected(d, d => {
|
...d,
|
||||||
return {
|
...(direction === "ccw"
|
||||||
...d,
|
? rotateRect90CCW(d, center)
|
||||||
...(direction === "ccw"
|
: rotateRect90CW(d, center)),
|
||||||
? rotateRect90CCW(d, center)
|
};
|
||||||
: rotateRect90CW(d, center)),
|
})),
|
||||||
};
|
texts: editorState.texts.map(txt => mapIfSelected(txt, txt => {
|
||||||
})),
|
return {
|
||||||
texts: historyState.current.texts.map(txt => mapIfSelected(txt, txt => {
|
...txt,
|
||||||
return {
|
topLeft: (direction === "ccw"
|
||||||
...txt,
|
? rotatePoint90CCW(txt.topLeft, center)
|
||||||
topLeft: (direction === "ccw"
|
: rotatePoint90CW(txt.topLeft, center)),
|
||||||
? rotatePoint90CCW(txt.topLeft, center)
|
};
|
||||||
: rotatePoint90CW(txt.topLeft, center)),
|
})),
|
||||||
};
|
history: editorState.history.map(h => mapIfSelected(h, h => {
|
||||||
})),
|
return {
|
||||||
history: historyState.current.history.map(h => mapIfSelected(h, h => {
|
...h,
|
||||||
return {
|
topLeft: (direction === "ccw"
|
||||||
...h,
|
? subtractV2D(rotatePoint90CCW(addV2D(h.topLeft, historySize), center), historySize)
|
||||||
topLeft: (direction === "ccw"
|
: subtractV2D(rotatePoint90CW(addV2D(h.topLeft, historySize), center), historySize)
|
||||||
? subtractV2D(rotatePoint90CCW(addV2D(h.topLeft, historySize), center), historySize)
|
),
|
||||||
: subtractV2D(rotatePoint90CW(addV2D(h.topLeft, historySize), center), historySize)
|
};
|
||||||
),
|
})),
|
||||||
};
|
};
|
||||||
})),
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [setEditHistory]);
|
}, [setEditHistory]);
|
||||||
|
return {commitState, replaceState, onUndo, onRedo, onRotate};
|
||||||
return {makeCheckPoint, onUndo, onRedo, onRotate};
|
|
||||||
}
|
}
|
||||||
|
|
@ -11,9 +11,9 @@ import { App } from "./App/App";
|
||||||
|
|
||||||
const elem = document.getElementById("root")!;
|
const elem = document.getElementById("root")!;
|
||||||
const app = (
|
const app = (
|
||||||
<StrictMode>
|
// <StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
// </StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (import.meta.hot) {
|
if (import.meta.hot) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,14 @@ export type History = {
|
||||||
topLeft: Vec2D;
|
topLeft: Vec2D;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ConcreteSyntax = {
|
||||||
|
rountangles: Rountangle[];
|
||||||
|
texts: Text[];
|
||||||
|
arrows: Arrow[];
|
||||||
|
diamonds: Diamond[];
|
||||||
|
history: History[];
|
||||||
|
};
|
||||||
|
|
||||||
// independently moveable parts of our shapes:
|
// independently moveable parts of our shapes:
|
||||||
export type RectSide = "left" | "top" | "right" | "bottom";
|
export type RectSide = "left" | "top" | "right" | "bottom";
|
||||||
export type ArrowPart = "start" | "end";
|
export type ArrowPart = "start" | "end";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ConcreteSyntax, VisualEditorState } from "@/App/VisualEditor/VisualEditor";
|
import { VisualEditorState } from "@/App/VisualEditor/VisualEditor";
|
||||||
|
import { ConcreteSyntax } from "./concrete_syntax";
|
||||||
import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RectSide } from "./concrete_syntax";
|
import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RectSide } from "./concrete_syntax";
|
||||||
|
|
||||||
export type Connections = {
|
export type Connections = {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
|
||||||
import { parse as parseLabel, SyntaxError } from "./label_parser";
|
import { parse as parseLabel, SyntaxError } from "./label_parser";
|
||||||
import { Connections } from "./detect_connections";
|
import { Connections } from "./detect_connections";
|
||||||
import { HISTORY_RADIUS } from "../App/parameters";
|
import { HISTORY_RADIUS } from "../App/parameters";
|
||||||
import { ConcreteSyntax } from "@/App/VisualEditor/VisualEditor";
|
import { ConcreteSyntax } from "./concrete_syntax";
|
||||||
import { memoize } from "@/util/util";
|
import { memoize } from "@/util/util";
|
||||||
|
|
||||||
export type TraceableError = {
|
export type TraceableError = {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,24 @@ export function memoize<InType,OutType>(fn: (i: InType) => OutType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// author: ChatGPT
|
||||||
|
export function jsonDeepEqual(a: any, b: any) {
|
||||||
|
if (a === b) return true;
|
||||||
|
if (a && b && typeof a === "object" && typeof b === "object") {
|
||||||
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
||||||
|
|
||||||
|
const keysA = Object.keys(a);
|
||||||
|
const keysB = Object.keys(b);
|
||||||
|
if (keysA.length !== keysB.length) return false;
|
||||||
|
|
||||||
|
for (const key of keysA) {
|
||||||
|
if (!jsonDeepEqual(a[key], b[key])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// compare arrays by value
|
// compare arrays by value
|
||||||
export function arraysEqual<T>(a: T[], b: T[], cmp: (a: T, b: T) => boolean = (a,b)=>a===b): boolean {
|
export function arraysEqual<T>(a: T[], b: T[], cmp: (a: T, b: T) => boolean = (a,b)=>a===b): boolean {
|
||||||
if (a === b)
|
if (a === b)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue