parser and editor use same code for figuring out what is connected to what
This commit is contained in:
parent
6dc7a2e9a7
commit
b55cba198e
6 changed files with 228 additions and 149 deletions
|
|
@ -1,11 +1,10 @@
|
||||||
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}) {
|
export 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
|
||||||
className={props.selected ? "selected":""}
|
|
||||||
cx={props.topLeft.x+HISTORY_RADIUS}
|
cx={props.topLeft.x+HISTORY_RADIUS}
|
||||||
cy={props.topLeft.y+HISTORY_RADIUS}
|
cy={props.topLeft.y+HISTORY_RADIUS}
|
||||||
r={HISTORY_RADIUS}
|
r={HISTORY_RADIUS}
|
||||||
|
|
@ -17,7 +16,7 @@ export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
x={props.topLeft.x+HISTORY_RADIUS}
|
x={props.topLeft.x+HISTORY_RADIUS}
|
||||||
y={props.topLeft.y+HISTORY_RADIUS+4}
|
y={props.topLeft.y+HISTORY_RADIUS+5}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fontWeight={500}
|
fontWeight={500}
|
||||||
>{text}</text>
|
>{text}</text>
|
||||||
|
|
@ -29,5 +28,14 @@ export function HistorySVG(props: {uid: string, topLeft: Vec2D, kind: "shallow"|
|
||||||
data-uid={props.uid}
|
data-uid={props.uid}
|
||||||
data-parts="history"
|
data-parts="history"
|
||||||
/>
|
/>
|
||||||
|
{(props.selected || props.highlight) &&
|
||||||
|
<circle
|
||||||
|
className={props.selected ? "selected" : props.highlight ? "highlight" : ""}
|
||||||
|
cx={props.topLeft.x+HISTORY_RADIUS}
|
||||||
|
cy={props.topLeft.y+HISTORY_RADIUS}
|
||||||
|
r={HISTORY_RADIUS}
|
||||||
|
data-uid={props.uid}
|
||||||
|
data-parts="history"
|
||||||
|
/>}
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +44,6 @@
|
||||||
.rountangle.active {
|
.rountangle.active {
|
||||||
fill: darkorange;
|
fill: darkorange;
|
||||||
fill-opacity: 0.2;
|
fill-opacity: 0.2;
|
||||||
/* filter: drop-shadow( 3px 3px 2px rgba(0, 0, 0, .7)); */
|
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,8 +130,6 @@ text.helper:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableText, .draggableText.highlight {
|
.draggableText, .draggableText.highlight {
|
||||||
/* text-shadow: 2px 0 #fff, -2px 0 #fff, 0 2px #fff, 0 -2px #fff, 1px 1px #fff, -1px -1px #fff, 1px -1px #fff, -1px 1px #fff; */
|
|
||||||
/* -webkit-text-stroke: 4px white; */
|
|
||||||
paint-order: stroke;
|
paint-order: stroke;
|
||||||
stroke: white;
|
stroke: white;
|
||||||
stroke-width: 4px;
|
stroke-width: 4px;
|
||||||
|
|
@ -140,7 +137,6 @@ text.helper:hover {
|
||||||
stroke-linejoin: miter;
|
stroke-linejoin: miter;
|
||||||
stroke-opacity: 1;
|
stroke-opacity: 1;
|
||||||
fill-opacity:1;
|
fill-opacity:1;
|
||||||
/* font-weight: 800; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggableText.highlight:not(.selected) {
|
.draggableText.highlight:not(.selected) {
|
||||||
|
|
@ -148,9 +144,10 @@ text.helper:hover {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.highlight:not(.selected) {
|
.highlight:not(.selected):not(text) {
|
||||||
stroke: green;
|
stroke: green;
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
|
fill: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow.error {
|
.arrow.error {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
import * as lz4 from "@nick/lz4";
|
import * as lz4 from "@nick/lz4";
|
||||||
import { Dispatch, SetStateAction, useEffect, useRef, useState, MouseEvent } from "react";
|
import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Statechart } from "../statecharts/abstract_syntax";
|
import { Statechart } from "../statecharts/abstract_syntax";
|
||||||
import { Arrow, ArrowPart, Diamond, Rountangle, RountanglePart, Text, VisualEditorState, emptyState, findNearestArrow, findNearestSide, findRountangle } from "../statecharts/concrete_syntax";
|
import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text, VisualEditorState, emptyState } from "../statecharts/concrete_syntax";
|
||||||
import { parseStatechart, TraceableError } from "../statecharts/parser";
|
import { parseStatechart, TraceableError } from "../statecharts/parser";
|
||||||
import { BigStep } from "../statecharts/runtime_types";
|
import { BigStep } from "../statecharts/runtime_types";
|
||||||
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry";
|
import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, 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";
|
||||||
|
|
||||||
import "./VisualEditor.css";
|
|
||||||
import { ArrowSVG } from "./ArrowSVG";
|
import { ArrowSVG } from "./ArrowSVG";
|
||||||
import { RountangleSVG } from "./RountangleSVG";
|
import { RountangleSVG } from "./RountangleSVG";
|
||||||
import { TextSVG } from "./TextSVG";
|
import { TextSVG } from "./TextSVG";
|
||||||
import { DiamondSVG } from "./DiamondSVG";
|
import { DiamondSVG } from "./DiamondSVG";
|
||||||
import { HistorySVG } from "./HistorySVG";
|
import { HistorySVG } from "./HistorySVG";
|
||||||
|
import { detectConnections } from "../statecharts/detect_connections";
|
||||||
|
|
||||||
|
import "./VisualEditor.css";
|
||||||
|
|
||||||
|
|
||||||
type DraggingState = {
|
type DraggingState = {
|
||||||
|
|
@ -142,29 +143,23 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// delay is necessary for 2 reasons:
|
|
||||||
// 1) it's a hack - prevents us from writing the initial state to localstorage (before having recovered the state that was in localstorage)
|
|
||||||
// 2) performance: only save when the user does nothing
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
const stateBuffer = new TextEncoder().encode(JSON.stringify(state));
|
const stateBuffer = new TextEncoder().encode(JSON.stringify(state));
|
||||||
const compressedStateBuffer = lz4.compress(stateBuffer);
|
const compressedStateBuffer = lz4.compress(stateBuffer);
|
||||||
const compressedStateString = compressedStateBuffer.toBase64();
|
const compressedStateString = compressedStateBuffer.toBase64();
|
||||||
window.location.hash = "#"+compressedStateString;
|
window.location.hash = "#"+compressedStateString;
|
||||||
|
|
||||||
// const [statechart, errors] = parseStatechart(state);
|
|
||||||
// setErrors(errors);
|
|
||||||
// setAST(statechart);
|
|
||||||
}, 200);
|
}, 200);
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
|
const conns = useMemo(() => detectConnections(state), [state]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const [statechart, errors] = parseStatechart(state);
|
const [statechart, errors] = parseStatechart(state, conns);
|
||||||
setErrors(errors);
|
setErrors(errors);
|
||||||
setAST(statechart);
|
setAST(statechart);
|
||||||
}, [state])
|
}, [state])
|
||||||
|
|
||||||
|
|
||||||
function getCurrentPointer(e: {pageX: number, pageY: number}) {
|
function getCurrentPointer(e: {pageX: number, pageY: number}) {
|
||||||
const bbox = refSVG.current!.getBoundingClientRect();
|
const bbox = refSVG.current!.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
|
|
@ -514,90 +509,50 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
};
|
};
|
||||||
}, [selectingState, dragging]);
|
}, [selectingState, dragging]);
|
||||||
|
|
||||||
// detect what is 'connected'
|
|
||||||
const arrow2SideMap = new Map<string,[{ uid: string; part: RountanglePart; } | undefined, { uid: string; part: RountanglePart; } | undefined]>();
|
|
||||||
const side2ArrowMap = new Map<string, Set<["start"|"end", string]>>();
|
|
||||||
const text2ArrowMap = new Map<string,string>();
|
|
||||||
const arrow2TextMap = new Map<string,string[]>();
|
|
||||||
const text2RountangleMap = new Map<string, string>();
|
|
||||||
const rountangle2TextMap = new Map<string, string[]>();
|
|
||||||
|
|
||||||
// arrow <-> (rountangle | diamond)
|
|
||||||
for (const arrow of state.arrows) {
|
|
||||||
const sides = [...state.rountangles, ...state.diamonds];
|
|
||||||
const startSide = findNearestSide(arrow, "start", sides);
|
|
||||||
const endSide = findNearestSide(arrow, "end", sides);
|
|
||||||
if (startSide || endSide) {
|
|
||||||
arrow2SideMap.set(arrow.uid, [startSide, endSide]);
|
|
||||||
}
|
|
||||||
if (startSide) {
|
|
||||||
const arrowConns = side2ArrowMap.get(startSide.uid + '/' + startSide.part) || new Set();
|
|
||||||
arrowConns.add(["start", arrow.uid]);
|
|
||||||
side2ArrowMap.set(startSide.uid + '/' + startSide.part, arrowConns);
|
|
||||||
}
|
|
||||||
if (endSide) {
|
|
||||||
const arrowConns = side2ArrowMap.get(endSide.uid + '/' + endSide.part) || new Set();
|
|
||||||
arrowConns.add(["end", arrow.uid]);
|
|
||||||
side2ArrowMap.set(endSide.uid + '/' + endSide.part, arrowConns);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// text <-> arrow
|
|
||||||
for (const text of state.texts) {
|
|
||||||
const nearestArrow = findNearestArrow(text.topLeft, state.arrows);
|
|
||||||
if (nearestArrow) {
|
|
||||||
// prioritize text belonging to arrows:
|
|
||||||
text2ArrowMap.set(text.uid, nearestArrow.uid);
|
|
||||||
const textsOfArrow = arrow2TextMap.get(nearestArrow.uid) || [];
|
|
||||||
textsOfArrow.push(text.uid);
|
|
||||||
arrow2TextMap.set(nearestArrow.uid, textsOfArrow);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// text <-> rountangle
|
|
||||||
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
|
||||||
if (rountangle) {
|
|
||||||
text2RountangleMap.set(text.uid, rountangle.uid);
|
|
||||||
const texts = rountangle2TextMap.get(rountangle.uid) || [];
|
|
||||||
texts.push(text.uid);
|
|
||||||
rountangle2TextMap.set(rountangle.uid, texts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for visual feedback, when selecting/moving one thing, we also highlight (in green) all the things that belong to the thing we selected.
|
// for visual feedback, when selecting/moving one thing, we also highlight (in green) all the things that belong to the thing we selected.
|
||||||
const sidesToHighlight: {[key: string]: RountanglePart[]} = {};
|
const sidesToHighlight: {[key: string]: RountanglePart[]} = {};
|
||||||
const arrowsToHighlight: {[key: string]: boolean} = {};
|
const arrowsToHighlight: {[key: string]: boolean} = {};
|
||||||
const textsToHighlight: {[key: string]: boolean} = {};
|
const textsToHighlight: {[key: string]: boolean} = {};
|
||||||
const rountanglesToHighlight: {[key: string]: boolean} = {};
|
const rountanglesToHighlight: {[key: string]: boolean} = {};
|
||||||
|
const historyToHighlight: {[key: string]: boolean} = {};
|
||||||
for (const selected of selection) {
|
for (const selected of selection) {
|
||||||
const sides = arrow2SideMap.get(selected.uid);
|
const sides = conns.arrow2SideMap.get(selected.uid);
|
||||||
if (sides) {
|
if (sides) {
|
||||||
const [startSide, endSide] = sides;
|
const [startSide, endSide] = sides;
|
||||||
if (startSide) sidesToHighlight[startSide.uid] = [...sidesToHighlight[startSide.uid]||[], startSide.part];
|
if (startSide) sidesToHighlight[startSide.uid] = [...sidesToHighlight[startSide.uid]||[], startSide.part];
|
||||||
if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part];
|
if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part];
|
||||||
}
|
}
|
||||||
const texts = [
|
const texts = [
|
||||||
...(arrow2TextMap.get(selected.uid) || []),
|
...(conns.arrow2TextMap.get(selected.uid) || []),
|
||||||
...(rountangle2TextMap.get(selected.uid) || []),
|
...(conns.rountangle2TextMap.get(selected.uid) || []),
|
||||||
];
|
];
|
||||||
for (const textUid of texts) {
|
for (const textUid of texts) {
|
||||||
textsToHighlight[textUid] = true;
|
textsToHighlight[textUid] = true;
|
||||||
}
|
}
|
||||||
for (const part of selected.parts) {
|
for (const part of selected.parts) {
|
||||||
const arrows = side2ArrowMap.get(selected.uid + '/' + part) || [];
|
const arrows = conns.side2ArrowMap.get(selected.uid + '/' + part) || [];
|
||||||
if (arrows) {
|
if (arrows) {
|
||||||
for (const [arrowPart, arrowUid] of arrows) {
|
for (const [arrowPart, arrowUid] of arrows) {
|
||||||
arrowsToHighlight[arrowUid] = true;
|
arrowsToHighlight[arrowUid] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const arrow2 = text2ArrowMap.get(selected.uid);
|
const arrow2 = conns.text2ArrowMap.get(selected.uid);
|
||||||
if (arrow2) {
|
if (arrow2) {
|
||||||
arrowsToHighlight[arrow2] = true;
|
arrowsToHighlight[arrow2] = true;
|
||||||
}
|
}
|
||||||
const rountangleUid = text2RountangleMap.get(selected.uid)
|
const rountangleUid = conns.text2RountangleMap.get(selected.uid)
|
||||||
if (rountangleUid) {
|
if (rountangleUid) {
|
||||||
rountanglesToHighlight[rountangleUid] = true;
|
rountanglesToHighlight[rountangleUid] = true;
|
||||||
}
|
}
|
||||||
|
const history = conns.arrow2HistoryMap.get(selected.uid);
|
||||||
|
if (history) {
|
||||||
|
historyToHighlight[history] = true;
|
||||||
|
}
|
||||||
|
const arrow3 = conns.history2ArrowMap.get(selected.uid) || [];
|
||||||
|
for (const arrow of arrow3) {
|
||||||
|
arrowsToHighlight[arrow] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPaste(e: ClipboardEvent) {
|
function onPaste(e: ClipboardEvent) {
|
||||||
|
|
@ -619,6 +574,11 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
uid: (nextID++).toString(),
|
uid: (nextID++).toString(),
|
||||||
topLeft: addV2D(r.topLeft, offset),
|
topLeft: addV2D(r.topLeft, offset),
|
||||||
} as Rountangle));
|
} as Rountangle));
|
||||||
|
const copiedDiamonds: Diamond[] = parsed.diamonds.map((r: Diamond) => ({
|
||||||
|
...r,
|
||||||
|
uid: (nextID++).toString(),
|
||||||
|
topLeft: addV2D(r.topLeft, offset),
|
||||||
|
} as Diamond));
|
||||||
const copiedArrows: Arrow[] = parsed.arrows.map((a: Arrow) => ({
|
const copiedArrows: Arrow[] = parsed.arrows.map((a: Arrow) => ({
|
||||||
...a,
|
...a,
|
||||||
uid: (nextID++).toString(),
|
uid: (nextID++).toString(),
|
||||||
|
|
@ -630,18 +590,27 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
uid: (nextID++).toString(),
|
uid: (nextID++).toString(),
|
||||||
topLeft: addV2D(t.topLeft, offset),
|
topLeft: addV2D(t.topLeft, offset),
|
||||||
} as Text));
|
} as Text));
|
||||||
|
const copiedHistories: History[] = parsed.history.map((h: History) => ({
|
||||||
|
...h,
|
||||||
|
uid: (nextID++).toString(),
|
||||||
|
topLeft: addV2D(h.topLeft, offset),
|
||||||
|
}))
|
||||||
setState(state => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: [...state.rountangles, ...copiedRountangles],
|
rountangles: [...state.rountangles, ...copiedRountangles],
|
||||||
|
diamonds: [...state.diamonds, ...copiedDiamonds],
|
||||||
arrows: [...state.arrows, ...copiedArrows],
|
arrows: [...state.arrows, ...copiedArrows],
|
||||||
texts: [...state.texts, ...copiedTexts],
|
texts: [...state.texts, ...copiedTexts],
|
||||||
|
history: [...state.history, ...copiedHistories],
|
||||||
nextID: nextID,
|
nextID: nextID,
|
||||||
}));
|
}));
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const newSelection: Selection = [
|
const newSelection: Selection = [
|
||||||
...copiedRountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
|
...copiedRountangles.map(r => ({uid: r.uid, parts: ["left", "top", "right", "bottom"]})),
|
||||||
|
...copiedDiamonds.map(d => ({uid: d.uid, parts: ["left", "top", "right", "bottom"]})),
|
||||||
...copiedArrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
...copiedArrows.map(a => ({uid: a.uid, parts: ["start", "end"]})),
|
||||||
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
|
...copiedTexts.map(t => ({uid: t.uid, parts: ["text"]})),
|
||||||
|
...copiedHistories.map(h => ({uid: h.uid, parts: ["history"]})),
|
||||||
];
|
];
|
||||||
setSelection(newSelection);
|
setSelection(newSelection);
|
||||||
// copyInternal(newSelection, e); // doesn't work
|
// copyInternal(newSelection, e); // doesn't work
|
||||||
|
|
@ -655,10 +624,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
function copyInternal(selection: Selection, e: ClipboardEvent) {
|
function copyInternal(selection: Selection, e: ClipboardEvent) {
|
||||||
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
const uidsToCopy = new Set(selection.map(shape => shape.uid));
|
||||||
const rountanglesToCopy = state.rountangles.filter(r => uidsToCopy.has(r.uid));
|
const rountanglesToCopy = state.rountangles.filter(r => uidsToCopy.has(r.uid));
|
||||||
|
const diamondsToCopy = state.diamonds.filter(d => uidsToCopy.has(d.uid));
|
||||||
|
const historiesToCopy = state.history.filter(h => uidsToCopy.has(h.uid));
|
||||||
const arrowsToCopy = state.arrows.filter(a => uidsToCopy.has(a.uid));
|
const arrowsToCopy = state.arrows.filter(a => uidsToCopy.has(a.uid));
|
||||||
const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid));
|
const textsToCopy = state.texts.filter(t => uidsToCopy.has(t.uid));
|
||||||
e.clipboardData?.setData("text/plain", JSON.stringify({
|
e.clipboardData?.setData("text/plain", JSON.stringify({
|
||||||
rountangles: rountanglesToCopy,
|
rountangles: rountanglesToCopy,
|
||||||
|
diamonds: diamondsToCopy,
|
||||||
|
history: historiesToCopy,
|
||||||
arrows: arrowsToCopy,
|
arrows: arrowsToCopy,
|
||||||
texts: textsToCopy,
|
texts: textsToCopy,
|
||||||
}));
|
}));
|
||||||
|
|
@ -739,7 +712,8 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
|
|
||||||
{(rootErrors.length>0) && <text className="error" x={5} y={20}>{rootErrors.join(' ')}</text>}
|
{(rootErrors.length>0) && <text className="error" x={5} y={20}>{rootErrors.join(' ')}</text>}
|
||||||
|
|
||||||
{state.rountangles.map(rountangle => <RountangleSVG
|
{state.rountangles.map(rountangle =>
|
||||||
|
<RountangleSVG
|
||||||
key={rountangle.uid}
|
key={rountangle.uid}
|
||||||
rountangle={rountangle}
|
rountangle={rountangle}
|
||||||
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
|
selected={selection.find(r => r.uid === rountangle.uid)?.parts || []}
|
||||||
|
|
@ -763,11 +737,14 @@ export function VisualEditor({setAST, rt, errors, setErrors, mode}: VisualEditor
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
{state.history.map(history => <>
|
{state.history.map(history => <>
|
||||||
<HistorySVG {...history} selected={selection.find(h => h.uid === history.uid)} />
|
<HistorySVG {...history}
|
||||||
|
selected={Boolean(selection.find(h => h.uid === history.uid))}
|
||||||
|
highlight={Boolean(historyToHighlight[history.uid])}
|
||||||
|
/>
|
||||||
</>)}
|
</>)}
|
||||||
|
|
||||||
{state.arrows.map(arrow => {
|
{state.arrows.map(arrow => {
|
||||||
const sides = arrow2SideMap.get(arrow.uid);
|
const sides = conns.arrow2SideMap.get(arrow.uid);
|
||||||
let arc = "no" as ArcDirection;
|
let arc = "no" as ArcDirection;
|
||||||
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0]!.uid !== undefined) {
|
if (sides && sides[0]?.uid === sides[1]?.uid && sides[0]!.uid !== undefined) {
|
||||||
arc = arcDirection(sides[0]!.part, sides[1]!.part);
|
arc = arcDirection(sides[0]!.part, sides[1]!.part);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox } from "../VisualEditor/geometry";
|
import { Rect2D, Vec2D, Line2D, euclideanDistance, intersectLines, isWithin, lineBBox, subtractV2D } from "../VisualEditor/geometry";
|
||||||
import { ARROW_SNAP_THRESHOLD, TEXT_SNAP_THRESHOLD } from "../VisualEditor/parameters";
|
import { ARROW_SNAP_THRESHOLD, HISTORY_RADIUS, TEXT_SNAP_THRESHOLD } from "../VisualEditor/parameters";
|
||||||
import { sides } from "../VisualEditor/VisualEditor";
|
import { sides } from "../VisualEditor/VisualEditor";
|
||||||
|
|
||||||
export type Rountangle = {
|
export type Rountangle = {
|
||||||
|
|
@ -115,3 +115,19 @@ export function findRountangle(point: Vec2D, candidates: Rountangle[]): Rountang
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findNearestHistory(point: Vec2D, candidates: History[]): History | undefined {
|
||||||
|
let best;
|
||||||
|
let bestDistance = Infinity;
|
||||||
|
for (const h of candidates) {
|
||||||
|
const diff = subtractV2D(point, {x: h.topLeft.x+HISTORY_RADIUS, y: h.topLeft.y+HISTORY_RADIUS});
|
||||||
|
const euclideanDistance = Math.hypot(diff.x, diff.y) - HISTORY_RADIUS;
|
||||||
|
if (euclideanDistance < ARROW_SNAP_THRESHOLD) {
|
||||||
|
if (euclideanDistance < bestDistance) {
|
||||||
|
best = h;
|
||||||
|
bestDistance = euclideanDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
|
||||||
84
src/statecharts/detect_connections.ts
Normal file
84
src/statecharts/detect_connections.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { findNearestArrow, findNearestHistory, findNearestSide, findRountangle, RountanglePart, VisualEditorState } from "./concrete_syntax";
|
||||||
|
|
||||||
|
export type Connections = {
|
||||||
|
arrow2SideMap: Map<string,[{ uid: string; part: RountanglePart; } | undefined, { uid: string; part: RountanglePart; } | undefined]>,
|
||||||
|
side2ArrowMap: Map<string, Set<["start"|"end", string]>>,
|
||||||
|
text2ArrowMap: Map<string,string>,
|
||||||
|
arrow2TextMap: Map<string,string[]>,
|
||||||
|
arrow2HistoryMap: Map<string,string>,
|
||||||
|
text2RountangleMap: Map<string, string>,
|
||||||
|
rountangle2TextMap: Map<string, string[]>,
|
||||||
|
history2ArrowMap: Map<string, string[]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 side2ArrowMap = new Map<string, Set<["start"|"end", string]>>();
|
||||||
|
const text2ArrowMap = new Map<string,string>();
|
||||||
|
const arrow2TextMap = new Map<string,string[]>();
|
||||||
|
const arrow2HistoryMap = new Map<string,string>();
|
||||||
|
const text2RountangleMap = new Map<string, string>();
|
||||||
|
const rountangle2TextMap = new Map<string, string[]>();
|
||||||
|
const history2ArrowMap = new Map<string, string[]>();
|
||||||
|
|
||||||
|
// arrow <-> (rountangle | diamond)
|
||||||
|
for (const arrow of state.arrows) {
|
||||||
|
// snap to history:
|
||||||
|
const historyTarget = findNearestHistory(arrow.end, state.history);
|
||||||
|
if (historyTarget) {
|
||||||
|
arrow2HistoryMap.set(arrow.uid, historyTarget.uid);
|
||||||
|
history2ArrowMap.set(historyTarget.uid, [...(history2ArrowMap.get(historyTarget.uid) || []), arrow.uid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// snap to rountangle/diamon side:
|
||||||
|
const sides = [...state.rountangles, ...state.diamonds];
|
||||||
|
const startSide = findNearestSide(arrow, "start", sides);
|
||||||
|
const endSide = historyTarget ? undefined : findNearestSide(arrow, "end", sides);
|
||||||
|
if (startSide || endSide) {
|
||||||
|
arrow2SideMap.set(arrow.uid, [startSide, endSide]);
|
||||||
|
}
|
||||||
|
if (startSide) {
|
||||||
|
const arrowConns = side2ArrowMap.get(startSide.uid + '/' + startSide.part) || new Set();
|
||||||
|
arrowConns.add(["start", arrow.uid]);
|
||||||
|
side2ArrowMap.set(startSide.uid + '/' + startSide.part, arrowConns);
|
||||||
|
}
|
||||||
|
if (endSide) {
|
||||||
|
const arrowConns = side2ArrowMap.get(endSide.uid + '/' + endSide.part) || new Set();
|
||||||
|
arrowConns.add(["end", arrow.uid]);
|
||||||
|
side2ArrowMap.set(endSide.uid + '/' + endSide.part, arrowConns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// text <-> arrow
|
||||||
|
for (const text of state.texts) {
|
||||||
|
const nearestArrow = findNearestArrow(text.topLeft, state.arrows);
|
||||||
|
if (nearestArrow) {
|
||||||
|
// prioritize text belonging to arrows:
|
||||||
|
text2ArrowMap.set(text.uid, nearestArrow.uid);
|
||||||
|
const textsOfArrow = arrow2TextMap.get(nearestArrow.uid) || [];
|
||||||
|
textsOfArrow.push(text.uid);
|
||||||
|
arrow2TextMap.set(nearestArrow.uid, textsOfArrow);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// text <-> rountangle
|
||||||
|
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
||||||
|
if (rountangle) {
|
||||||
|
text2RountangleMap.set(text.uid, rountangle.uid);
|
||||||
|
const texts = rountangle2TextMap.get(rountangle.uid) || [];
|
||||||
|
texts.push(text.uid);
|
||||||
|
rountangle2TextMap.set(rountangle.uid, texts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
arrow2SideMap,
|
||||||
|
side2ArrowMap,
|
||||||
|
text2ArrowMap,
|
||||||
|
arrow2TextMap,
|
||||||
|
arrow2HistoryMap,
|
||||||
|
text2RountangleMap,
|
||||||
|
rountangle2TextMap,
|
||||||
|
history2ArrowMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { AbstractState, ConcreteState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax";
|
import { ConcreteState, OrState, PseudoState, Statechart, Transition } from "./abstract_syntax";
|
||||||
import { findNearestArrow, findNearestSide, findRountangle, Rountangle, VisualEditorState } from "./concrete_syntax";
|
import { Rountangle, VisualEditorState } from "./concrete_syntax";
|
||||||
import { isEntirelyWithin } from "../VisualEditor/geometry";
|
import { isEntirelyWithin } from "../VisualEditor/geometry";
|
||||||
import { Action, EventTrigger, Expression, ParsedText } from "./label_ast";
|
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";
|
||||||
|
|
||||||
export type TraceableError = {
|
export type TraceableError = {
|
||||||
shapeUid: string;
|
shapeUid: string;
|
||||||
|
|
@ -29,7 +29,7 @@ function addEvent(events: EventTrigger[], e: EventTrigger, textUid: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseStatechart(state: VisualEditorState): [Statechart, TraceableError[]] {
|
export function parseStatechart(state: VisualEditorState, conns: Connections): [Statechart, TraceableError[]] {
|
||||||
const errors: TraceableError[] = [];
|
const errors: TraceableError[] = [];
|
||||||
|
|
||||||
// implicitly, the root is always an Or-state
|
// implicitly, the root is always an Or-state
|
||||||
|
|
@ -120,9 +120,8 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
const uid2Transition = new Map<string, Transition>();
|
const uid2Transition = new Map<string, Transition>();
|
||||||
|
|
||||||
for (const arr of state.arrows) {
|
for (const arr of state.arrows) {
|
||||||
const sides = [...state.rountangles, ...state.diamonds];
|
const srcUID = conns.arrow2SideMap.get(arr.uid)?.[0]?.uid;
|
||||||
const srcUID = findNearestSide(arr, "start", sides)?.uid;
|
const tgtUID = conns.arrow2SideMap.get(arr.uid)?.[1]?.uid;
|
||||||
const tgtUID = findNearestSide(arr, "end", sides)?.uid;
|
|
||||||
if (!srcUID) {
|
if (!srcUID) {
|
||||||
if (!tgtUID) {
|
if (!tgtUID) {
|
||||||
// dangling edge
|
// dangling edge
|
||||||
|
|
@ -222,9 +221,8 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const belongsToArrow = findNearestArrow(text.topLeft, state.arrows);
|
const belongsToArrowUID = conns.text2ArrowMap.get(text.uid);
|
||||||
if (belongsToArrow) {
|
const belongsToTransition = uid2Transition.get(belongsToArrowUID!);
|
||||||
const belongsToTransition = uid2Transition.get(belongsToArrow.uid);
|
|
||||||
if (belongsToTransition) {
|
if (belongsToTransition) {
|
||||||
const {src} = belongsToTransition;
|
const {src} = belongsToTransition;
|
||||||
belongsToTransition.label.push(parsed);
|
belongsToTransition.label.push(parsed);
|
||||||
|
|
@ -283,12 +281,11 @@ export function parseStatechart(state: VisualEditorState): [Statechart, Traceabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
// text does not belong to transition...
|
// text does not belong to transition...
|
||||||
// so it belongs to a rountangle (a state)
|
// so it belongs to a rountangle (a state)
|
||||||
const rountangle = findRountangle(text.topLeft, state.rountangles);
|
const rountangleUID = conns.text2RountangleMap.get(text.uid);
|
||||||
const belongsToState = rountangle ? uid2State.get(rountangle.uid)! as ConcreteState : root;
|
const belongsToState = uid2State.get(rountangleUID!) as ConcreteState || root;
|
||||||
if (parsed.kind === "transitionLabel") {
|
if (parsed.kind === "transitionLabel") {
|
||||||
// labels belonging to a rountangle (= a state) must by entry/exit actions
|
// labels belonging to a rountangle (= a state) must by entry/exit actions
|
||||||
// if we cannot find a containing state, then it belong to the root
|
// if we cannot find a containing state, then it belong to the root
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue