@excalidraw/excalidraw 0.17.1-c0b80a0 → 0.17.1-c329470
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/dev/excalidraw-assets-dev/{chunk-JGDL4H2X.js → chunk-3DLVY5XU.js} +8272 -6864
- package/dist/browser/dev/excalidraw-assets-dev/chunk-3DLVY5XU.js.map +7 -0
- package/dist/browser/dev/excalidraw-assets-dev/{chunk-V7NFEZA6.js → chunk-NOAEU4NM.js} +9 -2
- package/dist/browser/dev/excalidraw-assets-dev/chunk-NOAEU4NM.js.map +7 -0
- package/dist/browser/dev/excalidraw-assets-dev/{en-ZSVWGT55.js → en-7IBTMWBG.js} +2 -2
- package/dist/browser/dev/excalidraw-assets-dev/{image-RJG3J34Y.js → image-N5AC7SEK.js} +2 -6
- package/dist/browser/dev/index.css +85 -50
- package/dist/browser/dev/index.css.map +3 -3
- package/dist/browser/dev/index.js +4375 -3766
- package/dist/browser/dev/index.js.map +4 -4
- package/dist/browser/prod/excalidraw-assets/{chunk-LDVEIXGO.js → chunk-7CSIPVOW.js} +2 -2
- package/dist/browser/prod/excalidraw-assets/chunk-TX3BU7T2.js +47 -0
- package/dist/browser/prod/excalidraw-assets/{en-UPNEHLDS.js → en-LOGQBETY.js} +1 -1
- package/dist/browser/prod/excalidraw-assets/image-3V4U7GZE.js +1 -0
- package/dist/browser/prod/index.css +1 -1
- package/dist/browser/prod/index.js +40 -40
- package/dist/dev/index.css +85 -50
- package/dist/dev/index.css.map +3 -3
- package/dist/dev/index.js +8688 -6706
- package/dist/dev/index.js.map +4 -4
- package/dist/{prod/locales/en-ZXYG7GCR.json → dev/locales/en-V6KXFSCK.json} +8 -1
- package/dist/excalidraw/actions/actionAlign.d.ts +7 -6
- package/dist/excalidraw/actions/actionAlign.js +14 -14
- package/dist/excalidraw/actions/actionClipboard.d.ts +7 -3
- package/dist/excalidraw/actions/actionDeleteSelected.d.ts +7 -3
- package/dist/excalidraw/actions/actionDeleteSelected.js +103 -34
- package/dist/excalidraw/actions/actionDuplicateSelection.js +105 -95
- package/dist/excalidraw/actions/actionFlip.js +16 -7
- package/dist/excalidraw/actions/actionFrame.d.ts +493 -0
- package/dist/excalidraw/actions/actionFrame.js +45 -2
- package/dist/excalidraw/actions/actionGroup.js +6 -4
- package/dist/excalidraw/actions/actionProperties.js +145 -116
- package/dist/excalidraw/actions/actionSelectAll.js +4 -3
- package/dist/excalidraw/actions/shortcuts.d.ts +1 -1
- package/dist/excalidraw/actions/shortcuts.js +1 -0
- package/dist/excalidraw/actions/types.d.ts +1 -1
- package/dist/excalidraw/align.d.ts +2 -1
- package/dist/excalidraw/align.js +15 -6
- package/dist/excalidraw/clipboard.d.ts +27 -5
- package/dist/excalidraw/clipboard.js +55 -28
- package/dist/excalidraw/components/Actions.d.ts +2 -1
- package/dist/excalidraw/components/Actions.js +4 -2
- package/dist/excalidraw/components/ActiveConfirmDialog.d.ts +1 -1
- package/dist/excalidraw/components/ActiveConfirmDialog.js +2 -3
- package/dist/excalidraw/components/App.d.ts +1 -0
- package/dist/excalidraw/components/App.js +216 -111
- package/dist/excalidraw/components/ColorPicker/ColorInput.js +2 -3
- package/dist/excalidraw/components/ColorPicker/ColorPicker.js +2 -3
- package/dist/excalidraw/components/ColorPicker/CustomColorList.js +1 -1
- package/dist/excalidraw/components/ColorPicker/Picker.js +1 -1
- package/dist/excalidraw/components/ColorPicker/PickerColorList.js +1 -1
- package/dist/excalidraw/components/ColorPicker/ShadeList.js +1 -1
- package/dist/excalidraw/components/ColorPicker/colorPickerUtils.d.ts +1 -1
- package/dist/excalidraw/components/ColorPicker/colorPickerUtils.js +1 -1
- package/dist/excalidraw/components/CommandPalette/CommandPalette.js +3 -3
- package/dist/excalidraw/components/ConfirmDialog.js +17 -5
- package/dist/excalidraw/components/Dialog.js +2 -3
- package/dist/excalidraw/components/EyeDropper.d.ts +1 -1
- package/dist/excalidraw/components/EyeDropper.js +1 -1
- package/dist/excalidraw/components/IconPicker.d.ts +2 -2
- package/dist/excalidraw/components/IconPicker.js +56 -53
- package/dist/excalidraw/components/LayerUI.js +6 -6
- package/dist/excalidraw/components/LibraryMenu.d.ts +2 -16
- package/dist/excalidraw/components/LibraryMenu.js +70 -28
- package/dist/excalidraw/components/LibraryMenuHeaderContent.js +4 -5
- package/dist/excalidraw/components/MobileMenu.js +1 -1
- package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirm.js +2 -3
- package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.d.ts +1 -1
- package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.js +2 -3
- package/dist/excalidraw/components/Range.d.ts +9 -0
- package/dist/excalidraw/components/Range.js +24 -0
- package/dist/excalidraw/components/SearchMenu.d.ts +1 -1
- package/dist/excalidraw/components/SearchMenu.js +3 -4
- package/dist/excalidraw/components/Sidebar/Sidebar.d.ts +1 -1
- package/dist/excalidraw/components/Sidebar/Sidebar.js +2 -3
- package/dist/excalidraw/components/Stats/Collapsible.d.ts +2 -1
- package/dist/excalidraw/components/Stats/Collapsible.js +2 -2
- package/dist/excalidraw/components/Stats/Dimension.js +94 -8
- package/dist/excalidraw/components/Stats/MultiDimension.js +8 -5
- package/dist/excalidraw/components/Stats/Position.js +63 -3
- package/dist/excalidraw/components/Stats/index.js +21 -4
- package/dist/excalidraw/components/Stats/utils.d.ts +1 -1
- package/dist/excalidraw/components/Stats/utils.js +2 -55
- package/dist/excalidraw/components/TTDDialog/TTDDialog.js +1 -1
- package/dist/excalidraw/components/ToolButton.js +4 -9
- package/dist/excalidraw/components/hoc/withInternalFallback.js +3 -3
- package/dist/excalidraw/components/hyperlink/Hyperlink.js +6 -12
- package/dist/excalidraw/components/icons.d.ts +9 -0
- package/dist/excalidraw/components/icons.js +4 -4
- package/dist/excalidraw/components/main-menu/DefaultItems.js +2 -3
- package/dist/excalidraw/constants.d.ts +5 -1
- package/dist/excalidraw/constants.js +9 -1
- package/dist/excalidraw/context/tunnels.d.ts +2 -1
- package/dist/excalidraw/context/tunnels.js +3 -1
- package/dist/excalidraw/data/blob.d.ts +1 -0
- package/dist/excalidraw/data/blob.js +7 -3
- package/dist/excalidraw/data/filesystem.d.ts +2 -1
- package/dist/excalidraw/data/filesystem.js +1 -0
- package/dist/excalidraw/data/image.d.ts +0 -6
- package/dist/excalidraw/data/image.js +1 -43
- package/dist/excalidraw/data/index.js +6 -6
- package/dist/excalidraw/data/library.d.ts +9 -3
- package/dist/excalidraw/data/library.js +43 -6
- package/dist/excalidraw/data/restore.js +26 -8
- package/dist/excalidraw/data/url.d.ts +0 -1
- package/dist/excalidraw/data/url.js +2 -4
- package/dist/excalidraw/editor-jotai.d.ts +56 -0
- package/dist/excalidraw/editor-jotai.js +8 -0
- package/dist/excalidraw/element/binding.d.ts +9 -6
- package/dist/excalidraw/element/binding.js +124 -44
- package/dist/excalidraw/element/bounds.js +10 -0
- package/dist/excalidraw/element/cropElement.d.ts +5 -0
- package/dist/excalidraw/element/cropElement.js +28 -1
- package/dist/excalidraw/element/dragElements.js +13 -7
- package/dist/excalidraw/element/elbowArrow.d.ts +16 -0
- package/dist/excalidraw/element/elbowArrow.js +1268 -0
- package/dist/excalidraw/element/embeddable.js +4 -5
- package/dist/excalidraw/element/flowchart.d.ts +1 -1
- package/dist/excalidraw/element/flowchart.js +25 -9
- package/dist/excalidraw/element/heading.d.ts +5 -1
- package/dist/excalidraw/element/heading.js +5 -1
- package/dist/excalidraw/element/image.js +19 -5
- package/dist/excalidraw/element/linearElementEditor.d.ts +9 -10
- package/dist/excalidraw/element/linearElementEditor.js +97 -38
- package/dist/excalidraw/element/mutateElement.d.ts +3 -1
- package/dist/excalidraw/element/mutateElement.js +31 -4
- package/dist/excalidraw/element/newElement.d.ts +8 -12
- package/dist/excalidraw/element/newElement.js +36 -21
- package/dist/excalidraw/element/resizeElements.d.ts +20 -5
- package/dist/excalidraw/element/resizeElements.js +593 -361
- package/dist/excalidraw/element/sortElements.js +1 -4
- package/dist/excalidraw/element/types.d.ts +23 -1
- package/dist/excalidraw/fonts/Fonts.d.ts +0 -16
- package/dist/excalidraw/fonts/Fonts.js +6 -31
- package/dist/excalidraw/frame.d.ts +11 -5
- package/dist/excalidraw/frame.js +146 -35
- package/dist/excalidraw/groups.js +3 -0
- package/dist/excalidraw/hooks/useLibraryItemSvg.d.ts +1 -1
- package/dist/excalidraw/hooks/useLibraryItemSvg.js +2 -3
- package/dist/excalidraw/hooks/useScrollPosition.js +1 -1
- package/dist/excalidraw/i18n.js +3 -4
- package/dist/excalidraw/index.js +3 -4
- package/dist/excalidraw/locales/en.json +8 -1
- package/dist/excalidraw/renderer/interactiveScene.js +43 -32
- package/dist/excalidraw/renderer/staticScene.js +6 -4
- package/dist/excalidraw/renderer/staticSvgScene.js +1 -1
- package/dist/excalidraw/scene/Shape.js +40 -17
- package/dist/excalidraw/scene/comparisons.d.ts +0 -477
- package/dist/excalidraw/scene/comparisons.js +0 -37
- package/dist/excalidraw/scene/export.d.ts +7 -0
- package/dist/excalidraw/scene/export.js +107 -43
- package/dist/excalidraw/scene/index.d.ts +1 -1
- package/dist/excalidraw/scene/index.js +1 -1
- package/dist/excalidraw/scene/selection.js +4 -1
- package/dist/excalidraw/types.d.ts +15 -0
- package/dist/excalidraw/utility-types.d.ts +1 -0
- package/dist/excalidraw/utils.d.ts +8 -1
- package/dist/excalidraw/utils.js +9 -0
- package/dist/excalidraw/visualdebug.d.ts +8 -1
- package/dist/excalidraw/visualdebug.js +3 -0
- package/dist/math/line.d.ts +19 -0
- package/dist/math/line.js +32 -3
- package/dist/math/point.d.ts +10 -0
- package/dist/math/point.js +12 -1
- package/dist/prod/index.css +1 -1
- package/dist/prod/index.js +29 -44
- package/dist/{dev/locales/en-ZXYG7GCR.json → prod/locales/en-V6KXFSCK.json} +8 -1
- package/package.json +5 -2
- package/dist/browser/dev/excalidraw-assets-dev/chunk-JGDL4H2X.js.map +0 -7
- package/dist/browser/dev/excalidraw-assets-dev/chunk-V7NFEZA6.js.map +0 -7
- package/dist/browser/prod/excalidraw-assets/chunk-S2XKB3DE.js +0 -62
- package/dist/browser/prod/excalidraw-assets/image-OFI2YYMP.js +0 -1
- package/dist/excalidraw/element/routing.d.ts +0 -12
- package/dist/excalidraw/element/routing.js +0 -642
- package/dist/excalidraw/jotai.d.ts +0 -34
- package/dist/excalidraw/jotai.js +0 -18
- /package/dist/browser/dev/excalidraw-assets-dev/{en-ZSVWGT55.js.map → en-7IBTMWBG.js.map} +0 -0
- /package/dist/browser/dev/excalidraw-assets-dev/{image-RJG3J34Y.js.map → image-N5AC7SEK.js.map} +0 -0
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
import { pointDistance, pointFrom, pointScaleFromOrigin, pointsEqual, pointTranslate, vector, vectorCross, vectorFromPoint, vectorScale, } from "../../math";
|
|
2
|
+
import BinaryHeap from "../binaryheap";
|
|
3
|
+
import { getSizeFromPoints } from "../points";
|
|
4
|
+
import { aabbForElement, pointInsideBounds } from "../shapes";
|
|
5
|
+
import { invariant, isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
|
|
6
|
+
import { bindPointToSnapToElementOutline, distanceToBindableElement, avoidRectangularCorner, FIXED_BINDING_DISTANCE, getHeadingForElbowArrowSnap, getGlobalFixedPointForBindableElement, snapToMid, getHoveredElementForBinding, } from "./binding";
|
|
7
|
+
import { compareHeading, flipHeading, HEADING_DOWN, HEADING_LEFT, HEADING_RIGHT, HEADING_UP, headingForPointIsHorizontal, headingIsHorizontal, vectorToHeading, headingForPoint, } from "./heading";
|
|
8
|
+
import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
|
9
|
+
const DEDUP_TRESHOLD = 1;
|
|
10
|
+
export const BASE_PADDING = 40;
|
|
11
|
+
const handleSegmentRenormalization = (arrow, elementsMap) => {
|
|
12
|
+
const nextFixedSegments = arrow.fixedSegments
|
|
13
|
+
? structuredClone(arrow.fixedSegments)
|
|
14
|
+
: null;
|
|
15
|
+
if (nextFixedSegments) {
|
|
16
|
+
const _nextPoints = [];
|
|
17
|
+
arrow.points
|
|
18
|
+
.map((p) => pointFrom(arrow.x + p[0], arrow.y + p[1]))
|
|
19
|
+
.forEach((p, i, points) => {
|
|
20
|
+
if (i < 2) {
|
|
21
|
+
return _nextPoints.push(p);
|
|
22
|
+
}
|
|
23
|
+
const currentSegmentIsHorizontal = headingForPoint(p, points[i - 1]);
|
|
24
|
+
const prevSegmentIsHorizontal = headingForPoint(points[i - 1], points[i - 2]);
|
|
25
|
+
if (
|
|
26
|
+
// Check if previous two points are on the same line
|
|
27
|
+
compareHeading(currentSegmentIsHorizontal, prevSegmentIsHorizontal)) {
|
|
28
|
+
const prevSegmentIdx = nextFixedSegments?.findIndex((segment) => segment.index === i - 1) ?? -1;
|
|
29
|
+
const segmentIdx = nextFixedSegments?.findIndex((segment) => segment.index === i) ??
|
|
30
|
+
-1;
|
|
31
|
+
// If the current segment is a fixed segment, update its start point
|
|
32
|
+
if (segmentIdx !== -1) {
|
|
33
|
+
nextFixedSegments[segmentIdx].start = pointFrom(points[i - 2][0] - arrow.x, points[i - 2][1] - arrow.y);
|
|
34
|
+
}
|
|
35
|
+
// Remove the fixed segment status from the previous segment if it is
|
|
36
|
+
// a fixed segment, because we are going to unify that segment with
|
|
37
|
+
// the current one
|
|
38
|
+
if (prevSegmentIdx !== -1) {
|
|
39
|
+
nextFixedSegments.splice(prevSegmentIdx, 1);
|
|
40
|
+
}
|
|
41
|
+
// Remove the duplicate point
|
|
42
|
+
_nextPoints.splice(-1, 1);
|
|
43
|
+
// Update fixed point indices
|
|
44
|
+
nextFixedSegments.forEach((segment) => {
|
|
45
|
+
if (segment.index > i - 1) {
|
|
46
|
+
segment.index -= 1;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return _nextPoints.push(p);
|
|
51
|
+
});
|
|
52
|
+
const nextPoints = [];
|
|
53
|
+
_nextPoints.forEach((p, i, points) => {
|
|
54
|
+
if (i < 3) {
|
|
55
|
+
return nextPoints.push(p);
|
|
56
|
+
}
|
|
57
|
+
if (
|
|
58
|
+
// Remove segments that are too short
|
|
59
|
+
pointDistance(points[i - 2], points[i - 1]) < DEDUP_TRESHOLD) {
|
|
60
|
+
const prevPrevSegmentIdx = nextFixedSegments?.findIndex((segment) => segment.index === i - 2) ??
|
|
61
|
+
-1;
|
|
62
|
+
const prevSegmentIdx = nextFixedSegments?.findIndex((segment) => segment.index === i - 1) ??
|
|
63
|
+
-1;
|
|
64
|
+
// Remove the previous fixed segment if it exists (i.e. the segment
|
|
65
|
+
// which will be removed due to being parallel or too short)
|
|
66
|
+
if (prevSegmentIdx !== -1) {
|
|
67
|
+
nextFixedSegments.splice(prevSegmentIdx, 1);
|
|
68
|
+
}
|
|
69
|
+
// Remove the fixed segment status from the segment 2 steps back
|
|
70
|
+
// if it is a fixed segment, because we are going to unify that
|
|
71
|
+
// segment with the current one
|
|
72
|
+
if (prevPrevSegmentIdx !== -1) {
|
|
73
|
+
nextFixedSegments.splice(prevPrevSegmentIdx, 1);
|
|
74
|
+
}
|
|
75
|
+
nextPoints.splice(-2, 2);
|
|
76
|
+
// Since we have to remove two segments, update any fixed segment
|
|
77
|
+
nextFixedSegments.forEach((segment) => {
|
|
78
|
+
if (segment.index > i - 2) {
|
|
79
|
+
segment.index -= 2;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Remove aligned segment points
|
|
83
|
+
const isHorizontal = headingForPointIsHorizontal(p, points[i - 1]);
|
|
84
|
+
return nextPoints.push(pointFrom(!isHorizontal ? points[i - 2][0] : p[0], isHorizontal ? points[i - 2][1] : p[1]));
|
|
85
|
+
}
|
|
86
|
+
nextPoints.push(p);
|
|
87
|
+
});
|
|
88
|
+
const filteredNextFixedSegments = nextFixedSegments.filter((segment) => segment.index !== 1 && segment.index !== nextPoints.length - 1);
|
|
89
|
+
if (filteredNextFixedSegments.length === 0) {
|
|
90
|
+
return normalizeArrowElementUpdate(getElbowArrowCornerPoints(removeElbowArrowShortSegments(routeElbowArrow(arrow, getElbowArrowData(arrow, elementsMap, nextPoints.map((p) => pointFrom(p[0] - arrow.x, p[1] - arrow.y)))) ?? [])), filteredNextFixedSegments, null, null);
|
|
91
|
+
}
|
|
92
|
+
import.meta.env.DEV &&
|
|
93
|
+
invariant(validateElbowPoints(nextPoints), "Invalid elbow points with fixed segments");
|
|
94
|
+
return normalizeArrowElementUpdate(nextPoints, filteredNextFixedSegments, arrow.startIsSpecial, arrow.endIsSpecial);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
x: arrow.x,
|
|
98
|
+
y: arrow.y,
|
|
99
|
+
points: arrow.points,
|
|
100
|
+
fixedSegments: arrow.fixedSegments,
|
|
101
|
+
startIsSpecial: arrow.startIsSpecial,
|
|
102
|
+
endIsSpecial: arrow.endIsSpecial,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
const handleSegmentRelease = (arrow, fixedSegments, elementsMap) => {
|
|
106
|
+
const newFixedSegmentIndices = fixedSegments.map((segment) => segment.index);
|
|
107
|
+
const oldFixedSegmentIndices = arrow.fixedSegments?.map((segment) => segment.index) ?? [];
|
|
108
|
+
const deletedSegmentIdx = oldFixedSegmentIndices.findIndex((idx) => !newFixedSegmentIndices.includes(idx));
|
|
109
|
+
if (deletedSegmentIdx === -1 || !arrow.fixedSegments?.[deletedSegmentIdx]) {
|
|
110
|
+
return {
|
|
111
|
+
points: arrow.points,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const deletedIdx = arrow.fixedSegments[deletedSegmentIdx].index;
|
|
115
|
+
// Find prev and next fixed segments
|
|
116
|
+
const prevSegment = arrow.fixedSegments[deletedSegmentIdx - 1];
|
|
117
|
+
const nextSegment = arrow.fixedSegments[deletedSegmentIdx + 1];
|
|
118
|
+
// We need to render a sub-arrow path to restore deleted segments
|
|
119
|
+
const x = arrow.x + (prevSegment ? prevSegment.end[0] : 0);
|
|
120
|
+
const y = arrow.y + (prevSegment ? prevSegment.end[1] : 0);
|
|
121
|
+
const { startHeading, endHeading, startGlobalPoint, endGlobalPoint, hoveredStartElement, hoveredEndElement, ...rest } = getElbowArrowData({
|
|
122
|
+
x,
|
|
123
|
+
y,
|
|
124
|
+
startBinding: prevSegment ? null : arrow.startBinding,
|
|
125
|
+
endBinding: nextSegment ? null : arrow.endBinding,
|
|
126
|
+
startArrowhead: null,
|
|
127
|
+
endArrowhead: null,
|
|
128
|
+
}, elementsMap, [
|
|
129
|
+
pointFrom(0, 0),
|
|
130
|
+
pointFrom(arrow.x +
|
|
131
|
+
(nextSegment?.start[0] ?? arrow.points[arrow.points.length - 1][0]) -
|
|
132
|
+
x, arrow.y +
|
|
133
|
+
(nextSegment?.start[1] ?? arrow.points[arrow.points.length - 1][1]) -
|
|
134
|
+
y),
|
|
135
|
+
], { isDragging: false });
|
|
136
|
+
const { points: restoredPoints } = normalizeArrowElementUpdate(getElbowArrowCornerPoints(removeElbowArrowShortSegments(routeElbowArrow(arrow, {
|
|
137
|
+
startHeading,
|
|
138
|
+
endHeading,
|
|
139
|
+
startGlobalPoint,
|
|
140
|
+
endGlobalPoint,
|
|
141
|
+
hoveredStartElement,
|
|
142
|
+
hoveredEndElement,
|
|
143
|
+
...rest,
|
|
144
|
+
}) ?? [])), fixedSegments, null, null);
|
|
145
|
+
const nextPoints = [];
|
|
146
|
+
// First part of the arrow are the old points
|
|
147
|
+
if (prevSegment) {
|
|
148
|
+
for (let i = 0; i < prevSegment.index; i++) {
|
|
149
|
+
nextPoints.push(pointFrom(arrow.x + arrow.points[i][0], arrow.y + arrow.points[i][1]));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
restoredPoints.forEach((p) => {
|
|
153
|
+
nextPoints.push(pointFrom(arrow.x + (prevSegment ? prevSegment.end[0] : 0) + p[0], arrow.y + (prevSegment ? prevSegment.end[1] : 0) + p[1]));
|
|
154
|
+
});
|
|
155
|
+
// Last part of the arrow are the old points too
|
|
156
|
+
if (nextSegment) {
|
|
157
|
+
for (let i = nextSegment.index; i < arrow.points.length; i++) {
|
|
158
|
+
nextPoints.push(pointFrom(arrow.x + arrow.points[i][0], arrow.y + arrow.points[i][1]));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Update nextFixedSegments
|
|
162
|
+
const originalSegmentCountDiff = (nextSegment?.index ?? arrow.points.length) - (prevSegment?.index ?? 0) - 1;
|
|
163
|
+
const nextFixedSegments = fixedSegments.map((segment) => {
|
|
164
|
+
if (segment.index > deletedIdx) {
|
|
165
|
+
return {
|
|
166
|
+
...segment,
|
|
167
|
+
index: segment.index -
|
|
168
|
+
originalSegmentCountDiff +
|
|
169
|
+
(restoredPoints.length - 1),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return segment;
|
|
173
|
+
});
|
|
174
|
+
const simplifiedPoints = nextPoints.flatMap((p, i) => {
|
|
175
|
+
const prev = nextPoints[i - 1];
|
|
176
|
+
const next = nextPoints[i + 1];
|
|
177
|
+
if (prev && next) {
|
|
178
|
+
const prevHeading = headingForPoint(p, prev);
|
|
179
|
+
const nextHeading = headingForPoint(next, p);
|
|
180
|
+
if (compareHeading(prevHeading, nextHeading)) {
|
|
181
|
+
// Update subsequent fixed segment indices
|
|
182
|
+
nextFixedSegments.forEach((segment) => {
|
|
183
|
+
if (segment.index > i) {
|
|
184
|
+
segment.index -= 1;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
else if (compareHeading(prevHeading, flipHeading(nextHeading))) {
|
|
190
|
+
// Update subsequent fixed segment indices
|
|
191
|
+
nextFixedSegments.forEach((segment) => {
|
|
192
|
+
if (segment.index > i) {
|
|
193
|
+
segment.index += 1;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return [p, p];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return [p];
|
|
200
|
+
});
|
|
201
|
+
return normalizeArrowElementUpdate(simplifiedPoints, nextFixedSegments, false, false);
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
*
|
|
205
|
+
*/
|
|
206
|
+
const handleSegmentMove = (arrow, fixedSegments, startHeading, endHeading, hoveredStartElement, hoveredEndElement) => {
|
|
207
|
+
const activelyModifiedSegmentIdx = fixedSegments
|
|
208
|
+
.map((segment, i) => {
|
|
209
|
+
if (arrow.fixedSegments == null ||
|
|
210
|
+
arrow.fixedSegments[i] === undefined ||
|
|
211
|
+
arrow.fixedSegments[i].index !== segment.index) {
|
|
212
|
+
return i;
|
|
213
|
+
}
|
|
214
|
+
return (segment.start[0] !== arrow.fixedSegments[i].start[0] &&
|
|
215
|
+
segment.end[0] !== arrow.fixedSegments[i].end[0]) !==
|
|
216
|
+
(segment.start[1] !== arrow.fixedSegments[i].start[1] &&
|
|
217
|
+
segment.end[1] !== arrow.fixedSegments[i].end[1])
|
|
218
|
+
? i
|
|
219
|
+
: null;
|
|
220
|
+
})
|
|
221
|
+
.filter((idx) => idx !== null)
|
|
222
|
+
.shift();
|
|
223
|
+
if (activelyModifiedSegmentIdx == null) {
|
|
224
|
+
return { points: arrow.points };
|
|
225
|
+
}
|
|
226
|
+
const firstSegmentIdx = arrow.fixedSegments?.findIndex((segment) => segment.index === 1) ?? -1;
|
|
227
|
+
const lastSegmentIdx = arrow.fixedSegments?.findIndex((segment) => segment.index === arrow.points.length - 1) ?? -1;
|
|
228
|
+
// Handle special case for first segment move
|
|
229
|
+
const segmentLength = pointDistance(fixedSegments[activelyModifiedSegmentIdx].start, fixedSegments[activelyModifiedSegmentIdx].end);
|
|
230
|
+
const segmentIsTooShort = segmentLength < BASE_PADDING + 5;
|
|
231
|
+
if (firstSegmentIdx === -1 &&
|
|
232
|
+
fixedSegments[activelyModifiedSegmentIdx].index === 1 &&
|
|
233
|
+
hoveredStartElement) {
|
|
234
|
+
const startIsHorizontal = headingIsHorizontal(startHeading);
|
|
235
|
+
const startIsPositive = startIsHorizontal
|
|
236
|
+
? compareHeading(startHeading, HEADING_RIGHT)
|
|
237
|
+
: compareHeading(startHeading, HEADING_DOWN);
|
|
238
|
+
const padding = startIsPositive
|
|
239
|
+
? segmentIsTooShort
|
|
240
|
+
? segmentLength / 2
|
|
241
|
+
: BASE_PADDING
|
|
242
|
+
: segmentIsTooShort
|
|
243
|
+
? -segmentLength / 2
|
|
244
|
+
: -BASE_PADDING;
|
|
245
|
+
fixedSegments[activelyModifiedSegmentIdx].start = pointFrom(fixedSegments[activelyModifiedSegmentIdx].start[0] +
|
|
246
|
+
(startIsHorizontal ? padding : 0), fixedSegments[activelyModifiedSegmentIdx].start[1] +
|
|
247
|
+
(!startIsHorizontal ? padding : 0));
|
|
248
|
+
}
|
|
249
|
+
// Handle special case for last segment move
|
|
250
|
+
if (lastSegmentIdx === -1 &&
|
|
251
|
+
fixedSegments[activelyModifiedSegmentIdx].index ===
|
|
252
|
+
arrow.points.length - 1 &&
|
|
253
|
+
hoveredEndElement) {
|
|
254
|
+
const endIsHorizontal = headingIsHorizontal(endHeading);
|
|
255
|
+
const endIsPositive = endIsHorizontal
|
|
256
|
+
? compareHeading(endHeading, HEADING_RIGHT)
|
|
257
|
+
: compareHeading(endHeading, HEADING_DOWN);
|
|
258
|
+
const padding = endIsPositive
|
|
259
|
+
? segmentIsTooShort
|
|
260
|
+
? segmentLength / 2
|
|
261
|
+
: BASE_PADDING
|
|
262
|
+
: segmentIsTooShort
|
|
263
|
+
? -segmentLength / 2
|
|
264
|
+
: -BASE_PADDING;
|
|
265
|
+
fixedSegments[activelyModifiedSegmentIdx].end = pointFrom(fixedSegments[activelyModifiedSegmentIdx].end[0] +
|
|
266
|
+
(endIsHorizontal ? padding : 0), fixedSegments[activelyModifiedSegmentIdx].end[1] +
|
|
267
|
+
(!endIsHorizontal ? padding : 0));
|
|
268
|
+
}
|
|
269
|
+
// Translate all fixed segments to global coordinates
|
|
270
|
+
const nextFixedSegments = fixedSegments.map((segment) => ({
|
|
271
|
+
...segment,
|
|
272
|
+
start: pointFrom(arrow.x + segment.start[0], arrow.y + segment.start[1]),
|
|
273
|
+
end: pointFrom(arrow.x + segment.end[0], arrow.y + segment.end[1]),
|
|
274
|
+
}));
|
|
275
|
+
// For start, clone old arrow points
|
|
276
|
+
const newPoints = arrow.points.map((p, i) => pointFrom(arrow.x + p[0], arrow.y + p[1]));
|
|
277
|
+
const startIdx = nextFixedSegments[activelyModifiedSegmentIdx].index - 1;
|
|
278
|
+
const endIdx = nextFixedSegments[activelyModifiedSegmentIdx].index;
|
|
279
|
+
const start = nextFixedSegments[activelyModifiedSegmentIdx].start;
|
|
280
|
+
const end = nextFixedSegments[activelyModifiedSegmentIdx].end;
|
|
281
|
+
const prevSegmentIsHorizontal = newPoints[startIdx - 1] &&
|
|
282
|
+
!pointsEqual(newPoints[startIdx], newPoints[startIdx - 1])
|
|
283
|
+
? headingForPointIsHorizontal(newPoints[startIdx - 1], newPoints[startIdx])
|
|
284
|
+
: undefined;
|
|
285
|
+
const nextSegmentIsHorizontal = newPoints[endIdx + 1] &&
|
|
286
|
+
!pointsEqual(newPoints[endIdx], newPoints[endIdx + 1])
|
|
287
|
+
? headingForPointIsHorizontal(newPoints[endIdx + 1], newPoints[endIdx])
|
|
288
|
+
: undefined;
|
|
289
|
+
// Override the segment points with the actively moved fixed segment
|
|
290
|
+
if (prevSegmentIsHorizontal !== undefined) {
|
|
291
|
+
const dir = prevSegmentIsHorizontal ? 1 : 0;
|
|
292
|
+
newPoints[startIdx - 1][dir] = start[dir];
|
|
293
|
+
}
|
|
294
|
+
newPoints[startIdx] = start;
|
|
295
|
+
newPoints[endIdx] = end;
|
|
296
|
+
if (nextSegmentIsHorizontal !== undefined) {
|
|
297
|
+
const dir = nextSegmentIsHorizontal ? 1 : 0;
|
|
298
|
+
newPoints[endIdx + 1][dir] = end[dir];
|
|
299
|
+
}
|
|
300
|
+
// Override neighboring fixedSegment start/end points, if any
|
|
301
|
+
const prevSegmentIdx = nextFixedSegments.findIndex((segment) => segment.index === startIdx);
|
|
302
|
+
if (prevSegmentIdx !== -1) {
|
|
303
|
+
// Align the next segment points with the moved segment
|
|
304
|
+
const dir = headingForPointIsHorizontal(nextFixedSegments[prevSegmentIdx].end, nextFixedSegments[prevSegmentIdx].start)
|
|
305
|
+
? 1
|
|
306
|
+
: 0;
|
|
307
|
+
nextFixedSegments[prevSegmentIdx].start[dir] = start[dir];
|
|
308
|
+
nextFixedSegments[prevSegmentIdx].end = start;
|
|
309
|
+
}
|
|
310
|
+
const nextSegmentIdx = nextFixedSegments.findIndex((segment) => segment.index === endIdx + 1);
|
|
311
|
+
if (nextSegmentIdx !== -1) {
|
|
312
|
+
// Align the next segment points with the moved segment
|
|
313
|
+
const dir = headingForPointIsHorizontal(nextFixedSegments[nextSegmentIdx].end, nextFixedSegments[nextSegmentIdx].start)
|
|
314
|
+
? 1
|
|
315
|
+
: 0;
|
|
316
|
+
nextFixedSegments[nextSegmentIdx].end[dir] = end[dir];
|
|
317
|
+
nextFixedSegments[nextSegmentIdx].start = end;
|
|
318
|
+
}
|
|
319
|
+
// First segment move needs an additional segment
|
|
320
|
+
if (firstSegmentIdx === -1 && startIdx === 0) {
|
|
321
|
+
const startIsHorizontal = hoveredStartElement
|
|
322
|
+
? headingIsHorizontal(startHeading)
|
|
323
|
+
: headingForPointIsHorizontal(newPoints[1], newPoints[0]);
|
|
324
|
+
newPoints.unshift(pointFrom(startIsHorizontal ? start[0] : arrow.x + arrow.points[0][0], !startIsHorizontal ? start[1] : arrow.y + arrow.points[0][1]));
|
|
325
|
+
if (hoveredStartElement) {
|
|
326
|
+
newPoints.unshift(pointFrom(arrow.x + arrow.points[0][0], arrow.y + arrow.points[0][1]));
|
|
327
|
+
}
|
|
328
|
+
for (const segment of nextFixedSegments) {
|
|
329
|
+
segment.index += hoveredStartElement ? 2 : 1;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Last segment move needs an additional segment
|
|
333
|
+
if (lastSegmentIdx === -1 && endIdx === arrow.points.length - 1) {
|
|
334
|
+
const endIsHorizontal = headingIsHorizontal(endHeading);
|
|
335
|
+
newPoints.push(pointFrom(endIsHorizontal
|
|
336
|
+
? end[0]
|
|
337
|
+
: arrow.x + arrow.points[arrow.points.length - 1][0], !endIsHorizontal
|
|
338
|
+
? end[1]
|
|
339
|
+
: arrow.y + arrow.points[arrow.points.length - 1][1]));
|
|
340
|
+
if (hoveredEndElement) {
|
|
341
|
+
newPoints.push(pointFrom(arrow.x + arrow.points[arrow.points.length - 1][0], arrow.y + arrow.points[arrow.points.length - 1][1]));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return normalizeArrowElementUpdate(newPoints, nextFixedSegments.map((segment) => ({
|
|
345
|
+
...segment,
|
|
346
|
+
start: pointFrom(segment.start[0] - arrow.x, segment.start[1] - arrow.y),
|
|
347
|
+
end: pointFrom(segment.end[0] - arrow.x, segment.end[1] - arrow.y),
|
|
348
|
+
})), false, // If you move a segment, there is no special point anymore
|
|
349
|
+
false);
|
|
350
|
+
};
|
|
351
|
+
const handleEndpointDrag = (arrow, updatedPoints, fixedSegments, startHeading, endHeading, startGlobalPoint, endGlobalPoint, hoveredStartElement, hoveredEndElement) => {
|
|
352
|
+
let startIsSpecial = arrow.startIsSpecial ?? null;
|
|
353
|
+
let endIsSpecial = arrow.endIsSpecial ?? null;
|
|
354
|
+
const globalUpdatedPoints = updatedPoints.map((p, i) => i === 0
|
|
355
|
+
? pointFrom(arrow.x + p[0], arrow.y + p[1])
|
|
356
|
+
: i === updatedPoints.length - 1
|
|
357
|
+
? pointFrom(arrow.x + p[0], arrow.y + p[1])
|
|
358
|
+
: pointFrom(arrow.x + arrow.points[i][0], arrow.y + arrow.points[i][1]));
|
|
359
|
+
const nextFixedSegments = fixedSegments.map((segment) => ({
|
|
360
|
+
...segment,
|
|
361
|
+
start: pointFrom(arrow.x + (segment.start[0] - updatedPoints[0][0]), arrow.y + (segment.start[1] - updatedPoints[0][1])),
|
|
362
|
+
end: pointFrom(arrow.x + (segment.end[0] - updatedPoints[0][0]), arrow.y + (segment.end[1] - updatedPoints[0][1])),
|
|
363
|
+
}));
|
|
364
|
+
const newPoints = [];
|
|
365
|
+
// Add the inside points
|
|
366
|
+
const offset = 2 + (startIsSpecial ? 1 : 0);
|
|
367
|
+
const endOffset = 2 + (endIsSpecial ? 1 : 0);
|
|
368
|
+
while (newPoints.length + offset < globalUpdatedPoints.length - endOffset) {
|
|
369
|
+
newPoints.push(globalUpdatedPoints[newPoints.length + offset]);
|
|
370
|
+
}
|
|
371
|
+
// Calculate the moving second point connection and add the start point
|
|
372
|
+
{
|
|
373
|
+
const secondPoint = globalUpdatedPoints[startIsSpecial ? 2 : 1];
|
|
374
|
+
const thirdPoint = globalUpdatedPoints[startIsSpecial ? 3 : 2];
|
|
375
|
+
const startIsHorizontal = headingIsHorizontal(startHeading);
|
|
376
|
+
const secondIsHorizontal = headingIsHorizontal(vectorToHeading(vectorFromPoint(secondPoint, thirdPoint)));
|
|
377
|
+
if (hoveredStartElement && startIsHorizontal === secondIsHorizontal) {
|
|
378
|
+
const positive = startIsHorizontal
|
|
379
|
+
? compareHeading(startHeading, HEADING_RIGHT)
|
|
380
|
+
: compareHeading(startHeading, HEADING_DOWN);
|
|
381
|
+
newPoints.unshift(pointFrom(!secondIsHorizontal
|
|
382
|
+
? thirdPoint[0]
|
|
383
|
+
: startGlobalPoint[0] + (positive ? BASE_PADDING : -BASE_PADDING), secondIsHorizontal
|
|
384
|
+
? thirdPoint[1]
|
|
385
|
+
: startGlobalPoint[1] + (positive ? BASE_PADDING : -BASE_PADDING)));
|
|
386
|
+
newPoints.unshift(pointFrom(startIsHorizontal
|
|
387
|
+
? startGlobalPoint[0] + (positive ? BASE_PADDING : -BASE_PADDING)
|
|
388
|
+
: startGlobalPoint[0], !startIsHorizontal
|
|
389
|
+
? startGlobalPoint[1] + (positive ? BASE_PADDING : -BASE_PADDING)
|
|
390
|
+
: startGlobalPoint[1]));
|
|
391
|
+
if (!startIsSpecial) {
|
|
392
|
+
startIsSpecial = true;
|
|
393
|
+
for (const segment of nextFixedSegments) {
|
|
394
|
+
if (segment.index > 1) {
|
|
395
|
+
segment.index += 1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
newPoints.unshift(pointFrom(!secondIsHorizontal ? secondPoint[0] : startGlobalPoint[0], secondIsHorizontal ? secondPoint[1] : startGlobalPoint[1]));
|
|
402
|
+
if (startIsSpecial) {
|
|
403
|
+
startIsSpecial = false;
|
|
404
|
+
for (const segment of nextFixedSegments) {
|
|
405
|
+
if (segment.index > 1) {
|
|
406
|
+
segment.index -= 1;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
newPoints.unshift(startGlobalPoint);
|
|
412
|
+
}
|
|
413
|
+
// Calculate the moving second to last point connection
|
|
414
|
+
{
|
|
415
|
+
const secondToLastPoint = globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 3 : 2)];
|
|
416
|
+
const thirdToLastPoint = globalUpdatedPoints[globalUpdatedPoints.length - (endIsSpecial ? 4 : 3)];
|
|
417
|
+
const endIsHorizontal = headingIsHorizontal(endHeading);
|
|
418
|
+
const secondIsHorizontal = headingForPointIsHorizontal(thirdToLastPoint, secondToLastPoint);
|
|
419
|
+
if (hoveredEndElement && endIsHorizontal === secondIsHorizontal) {
|
|
420
|
+
const positive = endIsHorizontal
|
|
421
|
+
? compareHeading(endHeading, HEADING_RIGHT)
|
|
422
|
+
: compareHeading(endHeading, HEADING_DOWN);
|
|
423
|
+
newPoints.push(pointFrom(!secondIsHorizontal
|
|
424
|
+
? thirdToLastPoint[0]
|
|
425
|
+
: endGlobalPoint[0] + (positive ? BASE_PADDING : -BASE_PADDING), secondIsHorizontal
|
|
426
|
+
? thirdToLastPoint[1]
|
|
427
|
+
: endGlobalPoint[1] + (positive ? BASE_PADDING : -BASE_PADDING)));
|
|
428
|
+
newPoints.push(pointFrom(endIsHorizontal
|
|
429
|
+
? endGlobalPoint[0] + (positive ? BASE_PADDING : -BASE_PADDING)
|
|
430
|
+
: endGlobalPoint[0], !endIsHorizontal
|
|
431
|
+
? endGlobalPoint[1] + (positive ? BASE_PADDING : -BASE_PADDING)
|
|
432
|
+
: endGlobalPoint[1]));
|
|
433
|
+
if (!endIsSpecial) {
|
|
434
|
+
endIsSpecial = true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
newPoints.push(pointFrom(!secondIsHorizontal ? secondToLastPoint[0] : endGlobalPoint[0], secondIsHorizontal ? secondToLastPoint[1] : endGlobalPoint[1]));
|
|
439
|
+
if (endIsSpecial) {
|
|
440
|
+
endIsSpecial = false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
newPoints.push(endGlobalPoint);
|
|
445
|
+
return normalizeArrowElementUpdate(newPoints, nextFixedSegments
|
|
446
|
+
.map(({ index }) => ({
|
|
447
|
+
index,
|
|
448
|
+
start: newPoints[index - 1],
|
|
449
|
+
end: newPoints[index],
|
|
450
|
+
}))
|
|
451
|
+
.map((segment) => ({
|
|
452
|
+
...segment,
|
|
453
|
+
start: pointFrom(segment.start[0] - startGlobalPoint[0], segment.start[1] - startGlobalPoint[1]),
|
|
454
|
+
end: pointFrom(segment.end[0] - startGlobalPoint[0], segment.end[1] - startGlobalPoint[1]),
|
|
455
|
+
})), startIsSpecial, endIsSpecial);
|
|
456
|
+
};
|
|
457
|
+
/**
|
|
458
|
+
*
|
|
459
|
+
*/
|
|
460
|
+
export const updateElbowArrowPoints = (arrow, elementsMap, updates, options) => {
|
|
461
|
+
if (arrow.points.length < 2) {
|
|
462
|
+
return { points: updates.points ?? arrow.points };
|
|
463
|
+
}
|
|
464
|
+
if (!import.meta.env.PROD) {
|
|
465
|
+
invariant(!updates.points || updates.points.length >= 2, "Updated point array length must match the arrow point length, contain " +
|
|
466
|
+
"exactly the new start and end points or not be specified at all (i.e. " +
|
|
467
|
+
"you can't add new points between start and end manually to elbow arrows)");
|
|
468
|
+
invariant(!arrow.fixedSegments ||
|
|
469
|
+
arrow.fixedSegments
|
|
470
|
+
.map((s) => s.start[0] === s.end[0] || s.start[1] === s.end[1])
|
|
471
|
+
.every(Boolean), "Fixed segments must be either horizontal or vertical");
|
|
472
|
+
invariant(!updates.fixedSegments ||
|
|
473
|
+
updates.fixedSegments
|
|
474
|
+
.map((s) => s.start[0] === s.end[0] || s.start[1] === s.end[1])
|
|
475
|
+
.every(Boolean), "Updates to fixed segments must be either horizontal or vertical");
|
|
476
|
+
invariant(arrow.points
|
|
477
|
+
.slice(1)
|
|
478
|
+
.map((p, i) => p[0] === arrow.points[i][0] || p[1] === arrow.points[i][1]), "Elbow arrow segments must be either horizontal or vertical");
|
|
479
|
+
}
|
|
480
|
+
// 0. During all element replacement in the scene, we just need to renormalize
|
|
481
|
+
// the arrow
|
|
482
|
+
// TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed
|
|
483
|
+
if (elementsMap.size === 0 &&
|
|
484
|
+
updates.points &&
|
|
485
|
+
validateElbowPoints(updates.points)) {
|
|
486
|
+
return normalizeArrowElementUpdate(updates.points.map((p) => pointFrom(arrow.x + p[0], arrow.y + p[1])), arrow.fixedSegments, arrow.startIsSpecial, arrow.endIsSpecial);
|
|
487
|
+
}
|
|
488
|
+
const updatedPoints = updates.points
|
|
489
|
+
? updates.points && updates.points.length === 2
|
|
490
|
+
? arrow.points.map((p, idx) => idx === 0
|
|
491
|
+
? updates.points[0]
|
|
492
|
+
: idx === arrow.points.length - 1
|
|
493
|
+
? updates.points[1]
|
|
494
|
+
: p)
|
|
495
|
+
: structuredClone(updates.points)
|
|
496
|
+
: structuredClone(arrow.points);
|
|
497
|
+
const { startHeading, endHeading, startGlobalPoint, endGlobalPoint, hoveredStartElement, hoveredEndElement, ...rest } = getElbowArrowData({
|
|
498
|
+
x: arrow.x,
|
|
499
|
+
y: arrow.y,
|
|
500
|
+
startBinding: typeof updates.startBinding !== "undefined"
|
|
501
|
+
? updates.startBinding
|
|
502
|
+
: arrow.startBinding,
|
|
503
|
+
endBinding: typeof updates.endBinding !== "undefined"
|
|
504
|
+
? updates.endBinding
|
|
505
|
+
: arrow.endBinding,
|
|
506
|
+
startArrowhead: arrow.startArrowhead,
|
|
507
|
+
endArrowhead: arrow.endArrowhead,
|
|
508
|
+
}, elementsMap, updatedPoints, options);
|
|
509
|
+
const fixedSegments = updates.fixedSegments ?? arrow.fixedSegments ?? [];
|
|
510
|
+
////
|
|
511
|
+
// 1. Renormalize the arrow
|
|
512
|
+
////
|
|
513
|
+
if (!updates.points &&
|
|
514
|
+
!updates.fixedSegments &&
|
|
515
|
+
!updates.startBinding &&
|
|
516
|
+
!updates.endBinding) {
|
|
517
|
+
return handleSegmentRenormalization(arrow, elementsMap);
|
|
518
|
+
}
|
|
519
|
+
// Short circuit on no-op to avoid huge performance hit
|
|
520
|
+
if (updates.startBinding === arrow.startBinding &&
|
|
521
|
+
updates.endBinding === arrow.endBinding) {
|
|
522
|
+
return {};
|
|
523
|
+
}
|
|
524
|
+
////
|
|
525
|
+
// 2. Just normal elbow arrow things
|
|
526
|
+
////
|
|
527
|
+
if (fixedSegments.length === 0) {
|
|
528
|
+
return normalizeArrowElementUpdate(getElbowArrowCornerPoints(removeElbowArrowShortSegments(routeElbowArrow(arrow, {
|
|
529
|
+
startHeading,
|
|
530
|
+
endHeading,
|
|
531
|
+
startGlobalPoint,
|
|
532
|
+
endGlobalPoint,
|
|
533
|
+
hoveredStartElement,
|
|
534
|
+
hoveredEndElement,
|
|
535
|
+
...rest,
|
|
536
|
+
}) ?? [])), fixedSegments, null, null);
|
|
537
|
+
}
|
|
538
|
+
////
|
|
539
|
+
// 3. Handle releasing a fixed segment
|
|
540
|
+
if ((arrow.fixedSegments?.length ?? 0) > fixedSegments.length) {
|
|
541
|
+
return handleSegmentRelease(arrow, fixedSegments, elementsMap);
|
|
542
|
+
}
|
|
543
|
+
////
|
|
544
|
+
// 4. Handle manual segment move
|
|
545
|
+
////
|
|
546
|
+
if (!updates.points) {
|
|
547
|
+
return handleSegmentMove(arrow, fixedSegments, startHeading, endHeading, hoveredStartElement, hoveredEndElement);
|
|
548
|
+
}
|
|
549
|
+
////
|
|
550
|
+
// 5. Handle resize
|
|
551
|
+
////
|
|
552
|
+
if (updates.points && updates.fixedSegments) {
|
|
553
|
+
return updates;
|
|
554
|
+
}
|
|
555
|
+
////
|
|
556
|
+
// 6. One or more segments are fixed and endpoints are moved
|
|
557
|
+
//
|
|
558
|
+
// The key insights are:
|
|
559
|
+
// - When segments are fixed, the arrow will keep the exact amount of segments
|
|
560
|
+
// - Fixed segments are "replacements" for exactly one segment in the old arrow
|
|
561
|
+
////
|
|
562
|
+
return handleEndpointDrag(arrow, updatedPoints, fixedSegments, startHeading, endHeading, startGlobalPoint, endGlobalPoint, hoveredStartElement, hoveredEndElement);
|
|
563
|
+
};
|
|
564
|
+
/**
|
|
565
|
+
* Retrieves data necessary for calculating the elbow arrow path.
|
|
566
|
+
*
|
|
567
|
+
* @param arrow - The arrow object containing its properties.
|
|
568
|
+
* @param elementsMap - A map of elements in the scene.
|
|
569
|
+
* @param nextPoints - The next set of points for the arrow.
|
|
570
|
+
* @param options - Optional parameters for the calculation.
|
|
571
|
+
* @param options.isDragging - Indicates if the arrow is being dragged.
|
|
572
|
+
* @param options.startIsMidPoint - Indicates if the start point is a midpoint.
|
|
573
|
+
* @param options.endIsMidPoint - Indicates if the end point is a midpoint.
|
|
574
|
+
*
|
|
575
|
+
* @returns An object containing various properties needed for elbow arrow calculations:
|
|
576
|
+
* - dynamicAABBs: Dynamically generated axis-aligned bounding boxes.
|
|
577
|
+
* - startDonglePosition: The position of the start dongle.
|
|
578
|
+
* - startGlobalPoint: The global coordinates of the start point.
|
|
579
|
+
* - startHeading: The heading direction from the start point.
|
|
580
|
+
* - endDonglePosition: The position of the end dongle.
|
|
581
|
+
* - endGlobalPoint: The global coordinates of the end point.
|
|
582
|
+
* - endHeading: The heading direction from the end point.
|
|
583
|
+
* - commonBounds: The common bounding box that encompasses both start and end points.
|
|
584
|
+
* - hoveredStartElement: The element being hovered over at the start point.
|
|
585
|
+
* - hoveredEndElement: The element being hovered over at the end point.
|
|
586
|
+
*/
|
|
587
|
+
const getElbowArrowData = (arrow, elementsMap, nextPoints, options) => {
|
|
588
|
+
const origStartGlobalPoint = pointTranslate(nextPoints[0], vector(arrow.x, arrow.y));
|
|
589
|
+
const origEndGlobalPoint = pointTranslate(nextPoints[nextPoints.length - 1], vector(arrow.x, arrow.y));
|
|
590
|
+
const startElement = arrow.startBinding &&
|
|
591
|
+
getBindableElementForId(arrow.startBinding.elementId, elementsMap);
|
|
592
|
+
const endElement = arrow.endBinding &&
|
|
593
|
+
getBindableElementForId(arrow.endBinding.elementId, elementsMap);
|
|
594
|
+
const [hoveredStartElement, hoveredEndElement] = options?.isDragging
|
|
595
|
+
? getHoveredElements(origStartGlobalPoint, origEndGlobalPoint, elementsMap, options?.zoom)
|
|
596
|
+
: [startElement, endElement];
|
|
597
|
+
const startGlobalPoint = getGlobalPoint(arrow.startBinding?.fixedPoint, origStartGlobalPoint, origEndGlobalPoint, elementsMap, startElement, hoveredStartElement, options?.isDragging);
|
|
598
|
+
const endGlobalPoint = getGlobalPoint(arrow.endBinding?.fixedPoint, origEndGlobalPoint, origStartGlobalPoint, elementsMap, endElement, hoveredEndElement, options?.isDragging);
|
|
599
|
+
const startHeading = getBindPointHeading(startGlobalPoint, endGlobalPoint, elementsMap, hoveredStartElement, origStartGlobalPoint);
|
|
600
|
+
const endHeading = getBindPointHeading(endGlobalPoint, startGlobalPoint, elementsMap, hoveredEndElement, origEndGlobalPoint);
|
|
601
|
+
const startPointBounds = [
|
|
602
|
+
startGlobalPoint[0] - 2,
|
|
603
|
+
startGlobalPoint[1] - 2,
|
|
604
|
+
startGlobalPoint[0] + 2,
|
|
605
|
+
startGlobalPoint[1] + 2,
|
|
606
|
+
];
|
|
607
|
+
const endPointBounds = [
|
|
608
|
+
endGlobalPoint[0] - 2,
|
|
609
|
+
endGlobalPoint[1] - 2,
|
|
610
|
+
endGlobalPoint[0] + 2,
|
|
611
|
+
endGlobalPoint[1] + 2,
|
|
612
|
+
];
|
|
613
|
+
const startElementBounds = hoveredStartElement
|
|
614
|
+
? aabbForElement(hoveredStartElement, offsetFromHeading(startHeading, arrow.startArrowhead
|
|
615
|
+
? FIXED_BINDING_DISTANCE * 6
|
|
616
|
+
: FIXED_BINDING_DISTANCE * 2, 1))
|
|
617
|
+
: startPointBounds;
|
|
618
|
+
const endElementBounds = hoveredEndElement
|
|
619
|
+
? aabbForElement(hoveredEndElement, offsetFromHeading(endHeading, arrow.endArrowhead
|
|
620
|
+
? FIXED_BINDING_DISTANCE * 6
|
|
621
|
+
: FIXED_BINDING_DISTANCE * 2, 1))
|
|
622
|
+
: endPointBounds;
|
|
623
|
+
const boundsOverlap = pointInsideBounds(startGlobalPoint, hoveredEndElement
|
|
624
|
+
? aabbForElement(hoveredEndElement, offsetFromHeading(endHeading, BASE_PADDING, BASE_PADDING))
|
|
625
|
+
: endPointBounds) ||
|
|
626
|
+
pointInsideBounds(endGlobalPoint, hoveredStartElement
|
|
627
|
+
? aabbForElement(hoveredStartElement, offsetFromHeading(startHeading, BASE_PADDING, BASE_PADDING))
|
|
628
|
+
: startPointBounds);
|
|
629
|
+
const commonBounds = commonAABB(boundsOverlap
|
|
630
|
+
? [startPointBounds, endPointBounds]
|
|
631
|
+
: [startElementBounds, endElementBounds]);
|
|
632
|
+
const dynamicAABBs = generateDynamicAABBs(boundsOverlap ? startPointBounds : startElementBounds, boundsOverlap ? endPointBounds : endElementBounds, commonBounds, boundsOverlap
|
|
633
|
+
? offsetFromHeading(startHeading, !hoveredStartElement && !hoveredEndElement ? 0 : BASE_PADDING, 0)
|
|
634
|
+
: offsetFromHeading(startHeading, !hoveredStartElement && !hoveredEndElement
|
|
635
|
+
? 0
|
|
636
|
+
: BASE_PADDING -
|
|
637
|
+
(arrow.startArrowhead
|
|
638
|
+
? FIXED_BINDING_DISTANCE * 6
|
|
639
|
+
: FIXED_BINDING_DISTANCE * 2), BASE_PADDING), boundsOverlap
|
|
640
|
+
? offsetFromHeading(endHeading, !hoveredStartElement && !hoveredEndElement ? 0 : BASE_PADDING, 0)
|
|
641
|
+
: offsetFromHeading(endHeading, !hoveredStartElement && !hoveredEndElement
|
|
642
|
+
? 0
|
|
643
|
+
: BASE_PADDING -
|
|
644
|
+
(arrow.endArrowhead
|
|
645
|
+
? FIXED_BINDING_DISTANCE * 6
|
|
646
|
+
: FIXED_BINDING_DISTANCE * 2), BASE_PADDING), boundsOverlap, hoveredStartElement && aabbForElement(hoveredStartElement), hoveredEndElement && aabbForElement(hoveredEndElement));
|
|
647
|
+
const startDonglePosition = getDonglePosition(dynamicAABBs[0], startHeading, startGlobalPoint);
|
|
648
|
+
const endDonglePosition = getDonglePosition(dynamicAABBs[1], endHeading, endGlobalPoint);
|
|
649
|
+
return {
|
|
650
|
+
dynamicAABBs,
|
|
651
|
+
startDonglePosition,
|
|
652
|
+
startGlobalPoint,
|
|
653
|
+
startHeading,
|
|
654
|
+
endDonglePosition,
|
|
655
|
+
endGlobalPoint,
|
|
656
|
+
endHeading,
|
|
657
|
+
commonBounds,
|
|
658
|
+
hoveredStartElement,
|
|
659
|
+
hoveredEndElement,
|
|
660
|
+
boundsOverlap,
|
|
661
|
+
startElementBounds,
|
|
662
|
+
endElementBounds,
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
/**
|
|
666
|
+
* Generate the elbow arrow segments
|
|
667
|
+
*
|
|
668
|
+
* @param arrow
|
|
669
|
+
* @param elementsMap
|
|
670
|
+
* @param nextPoints
|
|
671
|
+
* @param options
|
|
672
|
+
* @returns
|
|
673
|
+
*/
|
|
674
|
+
const routeElbowArrow = (arrow, elbowArrowData) => {
|
|
675
|
+
const { dynamicAABBs, startDonglePosition, startGlobalPoint, startHeading, endDonglePosition, endGlobalPoint, endHeading, commonBounds, hoveredEndElement, } = elbowArrowData;
|
|
676
|
+
// Canculate Grid positions
|
|
677
|
+
const grid = calculateGrid(dynamicAABBs, startDonglePosition ? startDonglePosition : startGlobalPoint, startHeading, endDonglePosition ? endDonglePosition : endGlobalPoint, endHeading, commonBounds);
|
|
678
|
+
const startDongle = startDonglePosition && pointToGridNode(startDonglePosition, grid);
|
|
679
|
+
const endDongle = endDonglePosition && pointToGridNode(endDonglePosition, grid);
|
|
680
|
+
// Do not allow stepping on the true end or true start points
|
|
681
|
+
const endNode = pointToGridNode(endGlobalPoint, grid);
|
|
682
|
+
if (endNode && hoveredEndElement) {
|
|
683
|
+
endNode.closed = true;
|
|
684
|
+
}
|
|
685
|
+
const startNode = pointToGridNode(startGlobalPoint, grid);
|
|
686
|
+
if (startNode && arrow.startBinding) {
|
|
687
|
+
startNode.closed = true;
|
|
688
|
+
}
|
|
689
|
+
const dongleOverlap = startDongle &&
|
|
690
|
+
endDongle &&
|
|
691
|
+
(pointInsideBounds(startDongle.pos, dynamicAABBs[1]) ||
|
|
692
|
+
pointInsideBounds(endDongle.pos, dynamicAABBs[0]));
|
|
693
|
+
// Create path to end dongle from start dongle
|
|
694
|
+
const path = astar(startDongle ? startDongle : startNode, endDongle ? endDongle : endNode, grid, startHeading ? startHeading : HEADING_RIGHT, endHeading ? endHeading : HEADING_RIGHT, dongleOverlap ? [] : dynamicAABBs);
|
|
695
|
+
if (path) {
|
|
696
|
+
const points = path.map((node) => [
|
|
697
|
+
node.pos[0],
|
|
698
|
+
node.pos[1],
|
|
699
|
+
]);
|
|
700
|
+
startDongle && points.unshift(startGlobalPoint);
|
|
701
|
+
endDongle && points.push(endGlobalPoint);
|
|
702
|
+
return points;
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
};
|
|
706
|
+
const offsetFromHeading = (heading, head, side) => {
|
|
707
|
+
switch (heading) {
|
|
708
|
+
case HEADING_UP:
|
|
709
|
+
return [head, side, side, side];
|
|
710
|
+
case HEADING_RIGHT:
|
|
711
|
+
return [side, head, side, side];
|
|
712
|
+
case HEADING_DOWN:
|
|
713
|
+
return [side, side, head, side];
|
|
714
|
+
}
|
|
715
|
+
return [side, side, side, head];
|
|
716
|
+
};
|
|
717
|
+
/**
|
|
718
|
+
* Routing algorithm based on the A* path search algorithm.
|
|
719
|
+
* @see https://www.geeksforgeeks.org/a-search-algorithm/
|
|
720
|
+
*
|
|
721
|
+
* Binary heap is used to optimize node lookup.
|
|
722
|
+
* See {@link calculateGrid} for the grid calculation details.
|
|
723
|
+
*
|
|
724
|
+
* Additional modifications added due to aesthetic route reasons:
|
|
725
|
+
* 1) Arrow segment direction change is penalized by specific linear constant (bendMultiplier)
|
|
726
|
+
* 2) Arrow segments are not allowed to go "backwards", overlapping with the previous segment
|
|
727
|
+
*/
|
|
728
|
+
const astar = (start, end, grid, startHeading, endHeading, aabbs) => {
|
|
729
|
+
const bendMultiplier = m_dist(start.pos, end.pos);
|
|
730
|
+
const open = new BinaryHeap((node) => node.f);
|
|
731
|
+
open.push(start);
|
|
732
|
+
while (open.size() > 0) {
|
|
733
|
+
// Grab the lowest f(x) to process next. Heap keeps this sorted for us.
|
|
734
|
+
const current = open.pop();
|
|
735
|
+
if (!current || current.closed) {
|
|
736
|
+
// Current is not passable, continue with next element
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
// End case -- result has been found, return the traced path.
|
|
740
|
+
if (current === end) {
|
|
741
|
+
return pathTo(start, current);
|
|
742
|
+
}
|
|
743
|
+
// Normal case -- move current from open to closed, process each of its neighbors.
|
|
744
|
+
current.closed = true;
|
|
745
|
+
// Find all neighbors for the current node.
|
|
746
|
+
const neighbors = getNeighbors(current.addr, grid);
|
|
747
|
+
for (let i = 0; i < 4; i++) {
|
|
748
|
+
const neighbor = neighbors[i];
|
|
749
|
+
if (!neighbor || neighbor.closed) {
|
|
750
|
+
// Not a valid node to process, skip to next neighbor.
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
// Intersect
|
|
754
|
+
const neighborHalfPoint = pointScaleFromOrigin(neighbor.pos, current.pos, 0.5);
|
|
755
|
+
if (isAnyTrue(...aabbs.map((aabb) => pointInsideBounds(neighborHalfPoint, aabb)))) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
// The g score is the shortest distance from start to current node.
|
|
759
|
+
// We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.
|
|
760
|
+
const neighborHeading = neighborIndexToHeading(i);
|
|
761
|
+
const previousDirection = current.parent
|
|
762
|
+
? vectorToHeading(vectorFromPoint(current.pos, current.parent.pos))
|
|
763
|
+
: startHeading;
|
|
764
|
+
// Do not allow going in reverse
|
|
765
|
+
const reverseHeading = flipHeading(previousDirection);
|
|
766
|
+
const neighborIsReverseRoute = compareHeading(reverseHeading, neighborHeading) ||
|
|
767
|
+
(gridAddressesEqual(start.addr, neighbor.addr) &&
|
|
768
|
+
compareHeading(neighborHeading, startHeading)) ||
|
|
769
|
+
(gridAddressesEqual(end.addr, neighbor.addr) &&
|
|
770
|
+
compareHeading(neighborHeading, endHeading));
|
|
771
|
+
if (neighborIsReverseRoute) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
const directionChange = previousDirection !== neighborHeading;
|
|
775
|
+
const gScore = current.g +
|
|
776
|
+
m_dist(neighbor.pos, current.pos) +
|
|
777
|
+
(directionChange ? Math.pow(bendMultiplier, 3) : 0);
|
|
778
|
+
const beenVisited = neighbor.visited;
|
|
779
|
+
if (!beenVisited || gScore < neighbor.g) {
|
|
780
|
+
const estBendCount = estimateSegmentCount(neighbor, end, neighborHeading, endHeading);
|
|
781
|
+
// Found an optimal (so far) path to this node. Take score for node to see how good it is.
|
|
782
|
+
neighbor.visited = true;
|
|
783
|
+
neighbor.parent = current;
|
|
784
|
+
neighbor.h =
|
|
785
|
+
m_dist(end.pos, neighbor.pos) +
|
|
786
|
+
estBendCount * Math.pow(bendMultiplier, 2);
|
|
787
|
+
neighbor.g = gScore;
|
|
788
|
+
neighbor.f = neighbor.g + neighbor.h;
|
|
789
|
+
if (!beenVisited) {
|
|
790
|
+
// Pushing to heap will put it in proper place based on the 'f' value.
|
|
791
|
+
open.push(neighbor);
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
// Already seen the node, but since it has been rescored we need to reorder it in the heap
|
|
795
|
+
open.rescoreElement(neighbor);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return null;
|
|
801
|
+
};
|
|
802
|
+
const pathTo = (start, node) => {
|
|
803
|
+
let curr = node;
|
|
804
|
+
const path = [];
|
|
805
|
+
while (curr.parent) {
|
|
806
|
+
path.unshift(curr);
|
|
807
|
+
curr = curr.parent;
|
|
808
|
+
}
|
|
809
|
+
path.unshift(start);
|
|
810
|
+
return path;
|
|
811
|
+
};
|
|
812
|
+
const m_dist = (a, b) => Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
|
|
813
|
+
/**
|
|
814
|
+
* Create dynamically resizing, always touching
|
|
815
|
+
* bounding boxes having a minimum extent represented
|
|
816
|
+
* by the given static bounds.
|
|
817
|
+
*/
|
|
818
|
+
const generateDynamicAABBs = (a, b, common, startDifference, endDifference, disableSideHack, startElementBounds, endElementBounds) => {
|
|
819
|
+
const startEl = startElementBounds ?? a;
|
|
820
|
+
const endEl = endElementBounds ?? b;
|
|
821
|
+
const [startUp, startRight, startDown, startLeft] = startDifference ?? [
|
|
822
|
+
0, 0, 0, 0,
|
|
823
|
+
];
|
|
824
|
+
const [endUp, endRight, endDown, endLeft] = endDifference ?? [0, 0, 0, 0];
|
|
825
|
+
const first = [
|
|
826
|
+
a[0] > b[2]
|
|
827
|
+
? a[1] > b[3] || a[3] < b[1]
|
|
828
|
+
? Math.min((startEl[0] + endEl[2]) / 2, a[0] - startLeft)
|
|
829
|
+
: (startEl[0] + endEl[2]) / 2
|
|
830
|
+
: a[0] > b[0]
|
|
831
|
+
? a[0] - startLeft
|
|
832
|
+
: common[0] - startLeft,
|
|
833
|
+
a[1] > b[3]
|
|
834
|
+
? a[0] > b[2] || a[2] < b[0]
|
|
835
|
+
? Math.min((startEl[1] + endEl[3]) / 2, a[1] - startUp)
|
|
836
|
+
: (startEl[1] + endEl[3]) / 2
|
|
837
|
+
: a[1] > b[1]
|
|
838
|
+
? a[1] - startUp
|
|
839
|
+
: common[1] - startUp,
|
|
840
|
+
a[2] < b[0]
|
|
841
|
+
? a[1] > b[3] || a[3] < b[1]
|
|
842
|
+
? Math.max((startEl[2] + endEl[0]) / 2, a[2] + startRight)
|
|
843
|
+
: (startEl[2] + endEl[0]) / 2
|
|
844
|
+
: a[2] < b[2]
|
|
845
|
+
? a[2] + startRight
|
|
846
|
+
: common[2] + startRight,
|
|
847
|
+
a[3] < b[1]
|
|
848
|
+
? a[0] > b[2] || a[2] < b[0]
|
|
849
|
+
? Math.max((startEl[3] + endEl[1]) / 2, a[3] + startDown)
|
|
850
|
+
: (startEl[3] + endEl[1]) / 2
|
|
851
|
+
: a[3] < b[3]
|
|
852
|
+
? a[3] + startDown
|
|
853
|
+
: common[3] + startDown,
|
|
854
|
+
];
|
|
855
|
+
const second = [
|
|
856
|
+
b[0] > a[2]
|
|
857
|
+
? b[1] > a[3] || b[3] < a[1]
|
|
858
|
+
? Math.min((endEl[0] + startEl[2]) / 2, b[0] - endLeft)
|
|
859
|
+
: (endEl[0] + startEl[2]) / 2
|
|
860
|
+
: b[0] > a[0]
|
|
861
|
+
? b[0] - endLeft
|
|
862
|
+
: common[0] - endLeft,
|
|
863
|
+
b[1] > a[3]
|
|
864
|
+
? b[0] > a[2] || b[2] < a[0]
|
|
865
|
+
? Math.min((endEl[1] + startEl[3]) / 2, b[1] - endUp)
|
|
866
|
+
: (endEl[1] + startEl[3]) / 2
|
|
867
|
+
: b[1] > a[1]
|
|
868
|
+
? b[1] - endUp
|
|
869
|
+
: common[1] - endUp,
|
|
870
|
+
b[2] < a[0]
|
|
871
|
+
? b[1] > a[3] || b[3] < a[1]
|
|
872
|
+
? Math.max((endEl[2] + startEl[0]) / 2, b[2] + endRight)
|
|
873
|
+
: (endEl[2] + startEl[0]) / 2
|
|
874
|
+
: b[2] < a[2]
|
|
875
|
+
? b[2] + endRight
|
|
876
|
+
: common[2] + endRight,
|
|
877
|
+
b[3] < a[1]
|
|
878
|
+
? b[0] > a[2] || b[2] < a[0]
|
|
879
|
+
? Math.max((endEl[3] + startEl[1]) / 2, b[3] + endDown)
|
|
880
|
+
: (endEl[3] + startEl[1]) / 2
|
|
881
|
+
: b[3] < a[3]
|
|
882
|
+
? b[3] + endDown
|
|
883
|
+
: common[3] + endDown,
|
|
884
|
+
];
|
|
885
|
+
const c = commonAABB([first, second]);
|
|
886
|
+
if (!disableSideHack &&
|
|
887
|
+
first[2] - first[0] + second[2] - second[0] > c[2] - c[0] + 0.00000000001 &&
|
|
888
|
+
first[3] - first[1] + second[3] - second[1] > c[3] - c[1] + 0.00000000001) {
|
|
889
|
+
const [endCenterX, endCenterY] = [
|
|
890
|
+
(second[0] + second[2]) / 2,
|
|
891
|
+
(second[1] + second[3]) / 2,
|
|
892
|
+
];
|
|
893
|
+
if (b[0] > a[2] && a[1] > b[3]) {
|
|
894
|
+
// BOTTOM LEFT
|
|
895
|
+
const cX = first[2] + (second[0] - first[2]) / 2;
|
|
896
|
+
const cY = second[3] + (first[1] - second[3]) / 2;
|
|
897
|
+
if (vectorCross(vector(a[2] - endCenterX, a[1] - endCenterY), vector(a[0] - endCenterX, a[3] - endCenterY)) > 0) {
|
|
898
|
+
return [
|
|
899
|
+
[first[0], first[1], cX, first[3]],
|
|
900
|
+
[cX, second[1], second[2], second[3]],
|
|
901
|
+
];
|
|
902
|
+
}
|
|
903
|
+
return [
|
|
904
|
+
[first[0], cY, first[2], first[3]],
|
|
905
|
+
[second[0], second[1], second[2], cY],
|
|
906
|
+
];
|
|
907
|
+
}
|
|
908
|
+
else if (a[2] < b[0] && a[3] < b[1]) {
|
|
909
|
+
// TOP LEFT
|
|
910
|
+
const cX = first[2] + (second[0] - first[2]) / 2;
|
|
911
|
+
const cY = first[3] + (second[1] - first[3]) / 2;
|
|
912
|
+
if (vectorCross(vector(a[0] - endCenterX, a[1] - endCenterY), vector(a[2] - endCenterX, a[3] - endCenterY)) > 0) {
|
|
913
|
+
return [
|
|
914
|
+
[first[0], first[1], first[2], cY],
|
|
915
|
+
[second[0], cY, second[2], second[3]],
|
|
916
|
+
];
|
|
917
|
+
}
|
|
918
|
+
return [
|
|
919
|
+
[first[0], first[1], cX, first[3]],
|
|
920
|
+
[cX, second[1], second[2], second[3]],
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
else if (a[0] > b[2] && a[3] < b[1]) {
|
|
924
|
+
// TOP RIGHT
|
|
925
|
+
const cX = second[2] + (first[0] - second[2]) / 2;
|
|
926
|
+
const cY = first[3] + (second[1] - first[3]) / 2;
|
|
927
|
+
if (vectorCross(vector(a[2] - endCenterX, a[1] - endCenterY), vector(a[0] - endCenterX, a[3] - endCenterY)) > 0) {
|
|
928
|
+
return [
|
|
929
|
+
[cX, first[1], first[2], first[3]],
|
|
930
|
+
[second[0], second[1], cX, second[3]],
|
|
931
|
+
];
|
|
932
|
+
}
|
|
933
|
+
return [
|
|
934
|
+
[first[0], first[1], first[2], cY],
|
|
935
|
+
[second[0], cY, second[2], second[3]],
|
|
936
|
+
];
|
|
937
|
+
}
|
|
938
|
+
else if (a[0] > b[2] && a[1] > b[3]) {
|
|
939
|
+
// BOTTOM RIGHT
|
|
940
|
+
const cX = second[2] + (first[0] - second[2]) / 2;
|
|
941
|
+
const cY = second[3] + (first[1] - second[3]) / 2;
|
|
942
|
+
if (vectorCross(vector(a[0] - endCenterX, a[1] - endCenterY), vector(a[2] - endCenterX, a[3] - endCenterY)) > 0) {
|
|
943
|
+
return [
|
|
944
|
+
[cX, first[1], first[2], first[3]],
|
|
945
|
+
[second[0], second[1], cX, second[3]],
|
|
946
|
+
];
|
|
947
|
+
}
|
|
948
|
+
return [
|
|
949
|
+
[first[0], cY, first[2], first[3]],
|
|
950
|
+
[second[0], second[1], second[2], cY],
|
|
951
|
+
];
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return [first, second];
|
|
955
|
+
};
|
|
956
|
+
/**
|
|
957
|
+
* Calculates the grid which is used as nodes at
|
|
958
|
+
* the grid line intersections by the A* algorithm.
|
|
959
|
+
*
|
|
960
|
+
* NOTE: This is not a uniform grid. It is built at
|
|
961
|
+
* various intersections of bounding boxes.
|
|
962
|
+
*/
|
|
963
|
+
const calculateGrid = (aabbs, start, startHeading, end, endHeading, common) => {
|
|
964
|
+
const horizontal = new Set();
|
|
965
|
+
const vertical = new Set();
|
|
966
|
+
if (startHeading === HEADING_LEFT || startHeading === HEADING_RIGHT) {
|
|
967
|
+
vertical.add(start[1]);
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
horizontal.add(start[0]);
|
|
971
|
+
}
|
|
972
|
+
if (endHeading === HEADING_LEFT || endHeading === HEADING_RIGHT) {
|
|
973
|
+
vertical.add(end[1]);
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
horizontal.add(end[0]);
|
|
977
|
+
}
|
|
978
|
+
aabbs.forEach((aabb) => {
|
|
979
|
+
horizontal.add(aabb[0]);
|
|
980
|
+
horizontal.add(aabb[2]);
|
|
981
|
+
vertical.add(aabb[1]);
|
|
982
|
+
vertical.add(aabb[3]);
|
|
983
|
+
});
|
|
984
|
+
horizontal.add(common[0]);
|
|
985
|
+
horizontal.add(common[2]);
|
|
986
|
+
vertical.add(common[1]);
|
|
987
|
+
vertical.add(common[3]);
|
|
988
|
+
const _vertical = Array.from(vertical).sort((a, b) => a - b);
|
|
989
|
+
const _horizontal = Array.from(horizontal).sort((a, b) => a - b);
|
|
990
|
+
return {
|
|
991
|
+
row: _vertical.length,
|
|
992
|
+
col: _horizontal.length,
|
|
993
|
+
data: _vertical.flatMap((y, row) => _horizontal.map((x, col) => ({
|
|
994
|
+
f: 0,
|
|
995
|
+
g: 0,
|
|
996
|
+
h: 0,
|
|
997
|
+
closed: false,
|
|
998
|
+
visited: false,
|
|
999
|
+
parent: null,
|
|
1000
|
+
addr: [col, row],
|
|
1001
|
+
pos: [x, y],
|
|
1002
|
+
}))),
|
|
1003
|
+
};
|
|
1004
|
+
};
|
|
1005
|
+
const getDonglePosition = (bounds, heading, p) => {
|
|
1006
|
+
switch (heading) {
|
|
1007
|
+
case HEADING_UP:
|
|
1008
|
+
return pointFrom(p[0], bounds[1]);
|
|
1009
|
+
case HEADING_RIGHT:
|
|
1010
|
+
return pointFrom(bounds[2], p[1]);
|
|
1011
|
+
case HEADING_DOWN:
|
|
1012
|
+
return pointFrom(p[0], bounds[3]);
|
|
1013
|
+
}
|
|
1014
|
+
return pointFrom(bounds[0], p[1]);
|
|
1015
|
+
};
|
|
1016
|
+
const estimateSegmentCount = (start, end, startHeading, endHeading) => {
|
|
1017
|
+
if (endHeading === HEADING_RIGHT) {
|
|
1018
|
+
switch (startHeading) {
|
|
1019
|
+
case HEADING_RIGHT: {
|
|
1020
|
+
if (start.pos[0] >= end.pos[0]) {
|
|
1021
|
+
return 4;
|
|
1022
|
+
}
|
|
1023
|
+
if (start.pos[1] === end.pos[1]) {
|
|
1024
|
+
return 0;
|
|
1025
|
+
}
|
|
1026
|
+
return 2;
|
|
1027
|
+
}
|
|
1028
|
+
case HEADING_UP:
|
|
1029
|
+
if (start.pos[1] > end.pos[1] && start.pos[0] < end.pos[0]) {
|
|
1030
|
+
return 1;
|
|
1031
|
+
}
|
|
1032
|
+
return 3;
|
|
1033
|
+
case HEADING_DOWN:
|
|
1034
|
+
if (start.pos[1] < end.pos[1] && start.pos[0] < end.pos[0]) {
|
|
1035
|
+
return 1;
|
|
1036
|
+
}
|
|
1037
|
+
return 3;
|
|
1038
|
+
case HEADING_LEFT:
|
|
1039
|
+
if (start.pos[1] === end.pos[1]) {
|
|
1040
|
+
return 4;
|
|
1041
|
+
}
|
|
1042
|
+
return 2;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
else if (endHeading === HEADING_LEFT) {
|
|
1046
|
+
switch (startHeading) {
|
|
1047
|
+
case HEADING_RIGHT:
|
|
1048
|
+
if (start.pos[1] === end.pos[1]) {
|
|
1049
|
+
return 4;
|
|
1050
|
+
}
|
|
1051
|
+
return 2;
|
|
1052
|
+
case HEADING_UP:
|
|
1053
|
+
if (start.pos[1] > end.pos[1] && start.pos[0] > end.pos[0]) {
|
|
1054
|
+
return 1;
|
|
1055
|
+
}
|
|
1056
|
+
return 3;
|
|
1057
|
+
case HEADING_DOWN:
|
|
1058
|
+
if (start.pos[1] < end.pos[1] && start.pos[0] > end.pos[0]) {
|
|
1059
|
+
return 1;
|
|
1060
|
+
}
|
|
1061
|
+
return 3;
|
|
1062
|
+
case HEADING_LEFT:
|
|
1063
|
+
if (start.pos[0] <= end.pos[0]) {
|
|
1064
|
+
return 4;
|
|
1065
|
+
}
|
|
1066
|
+
if (start.pos[1] === end.pos[1]) {
|
|
1067
|
+
return 0;
|
|
1068
|
+
}
|
|
1069
|
+
return 2;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
else if (endHeading === HEADING_UP) {
|
|
1073
|
+
switch (startHeading) {
|
|
1074
|
+
case HEADING_RIGHT:
|
|
1075
|
+
if (start.pos[1] > end.pos[1] && start.pos[0] < end.pos[0]) {
|
|
1076
|
+
return 1;
|
|
1077
|
+
}
|
|
1078
|
+
return 3;
|
|
1079
|
+
case HEADING_UP:
|
|
1080
|
+
if (start.pos[1] >= end.pos[1]) {
|
|
1081
|
+
return 4;
|
|
1082
|
+
}
|
|
1083
|
+
if (start.pos[0] === end.pos[0]) {
|
|
1084
|
+
return 0;
|
|
1085
|
+
}
|
|
1086
|
+
return 2;
|
|
1087
|
+
case HEADING_DOWN:
|
|
1088
|
+
if (start.pos[0] === end.pos[0]) {
|
|
1089
|
+
return 4;
|
|
1090
|
+
}
|
|
1091
|
+
return 2;
|
|
1092
|
+
case HEADING_LEFT:
|
|
1093
|
+
if (start.pos[1] > end.pos[1] && start.pos[0] > end.pos[0]) {
|
|
1094
|
+
return 1;
|
|
1095
|
+
}
|
|
1096
|
+
return 3;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
else if (endHeading === HEADING_DOWN) {
|
|
1100
|
+
switch (startHeading) {
|
|
1101
|
+
case HEADING_RIGHT:
|
|
1102
|
+
if (start.pos[1] < end.pos[1] && start.pos[0] < end.pos[0]) {
|
|
1103
|
+
return 1;
|
|
1104
|
+
}
|
|
1105
|
+
return 3;
|
|
1106
|
+
case HEADING_UP:
|
|
1107
|
+
if (start.pos[0] === end.pos[0]) {
|
|
1108
|
+
return 4;
|
|
1109
|
+
}
|
|
1110
|
+
return 2;
|
|
1111
|
+
case HEADING_DOWN:
|
|
1112
|
+
if (start.pos[1] <= end.pos[1]) {
|
|
1113
|
+
return 4;
|
|
1114
|
+
}
|
|
1115
|
+
if (start.pos[0] === end.pos[0]) {
|
|
1116
|
+
return 0;
|
|
1117
|
+
}
|
|
1118
|
+
return 2;
|
|
1119
|
+
case HEADING_LEFT:
|
|
1120
|
+
if (start.pos[1] < end.pos[1] && start.pos[0] > end.pos[0]) {
|
|
1121
|
+
return 1;
|
|
1122
|
+
}
|
|
1123
|
+
return 3;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return 0;
|
|
1127
|
+
};
|
|
1128
|
+
/**
|
|
1129
|
+
* Get neighboring points for a gived grid address
|
|
1130
|
+
*/
|
|
1131
|
+
const getNeighbors = ([col, row], grid) => [
|
|
1132
|
+
gridNodeFromAddr([col, row - 1], grid),
|
|
1133
|
+
gridNodeFromAddr([col + 1, row], grid),
|
|
1134
|
+
gridNodeFromAddr([col, row + 1], grid),
|
|
1135
|
+
gridNodeFromAddr([col - 1, row], grid),
|
|
1136
|
+
];
|
|
1137
|
+
const gridNodeFromAddr = ([col, row], grid) => {
|
|
1138
|
+
if (col < 0 || col >= grid.col || row < 0 || row >= grid.row) {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
return grid.data[row * grid.col + col] ?? null;
|
|
1142
|
+
};
|
|
1143
|
+
/**
|
|
1144
|
+
* Get node for global point on canvas (if exists)
|
|
1145
|
+
*/
|
|
1146
|
+
const pointToGridNode = (point, grid) => {
|
|
1147
|
+
for (let col = 0; col < grid.col; col++) {
|
|
1148
|
+
for (let row = 0; row < grid.row; row++) {
|
|
1149
|
+
const candidate = gridNodeFromAddr([col, row], grid);
|
|
1150
|
+
if (candidate &&
|
|
1151
|
+
point[0] === candidate.pos[0] &&
|
|
1152
|
+
point[1] === candidate.pos[1]) {
|
|
1153
|
+
return candidate;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return null;
|
|
1158
|
+
};
|
|
1159
|
+
const commonAABB = (aabbs) => [
|
|
1160
|
+
Math.min(...aabbs.map((aabb) => aabb[0])),
|
|
1161
|
+
Math.min(...aabbs.map((aabb) => aabb[1])),
|
|
1162
|
+
Math.max(...aabbs.map((aabb) => aabb[2])),
|
|
1163
|
+
Math.max(...aabbs.map((aabb) => aabb[3])),
|
|
1164
|
+
];
|
|
1165
|
+
/// #region Utils
|
|
1166
|
+
const getBindableElementForId = (id, elementsMap) => {
|
|
1167
|
+
const element = elementsMap.get(id);
|
|
1168
|
+
if (element && isBindableElement(element)) {
|
|
1169
|
+
return element;
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1172
|
+
};
|
|
1173
|
+
const normalizeArrowElementUpdate = (global, nextFixedSegments, startIsSpecial, endIsSpecial) => {
|
|
1174
|
+
const offsetX = global[0][0];
|
|
1175
|
+
const offsetY = global[0][1];
|
|
1176
|
+
const points = global.map((p) => pointTranslate(p, vectorScale(vectorFromPoint(global[0]), -1)));
|
|
1177
|
+
return {
|
|
1178
|
+
points,
|
|
1179
|
+
x: offsetX,
|
|
1180
|
+
y: offsetY,
|
|
1181
|
+
fixedSegments: (nextFixedSegments?.length ?? 0) > 0 ? nextFixedSegments : null,
|
|
1182
|
+
...getSizeFromPoints(points),
|
|
1183
|
+
startIsSpecial,
|
|
1184
|
+
endIsSpecial,
|
|
1185
|
+
};
|
|
1186
|
+
};
|
|
1187
|
+
const getElbowArrowCornerPoints = (points) => {
|
|
1188
|
+
if (points.length > 1) {
|
|
1189
|
+
let previousHorizontal = Math.abs(points[0][1] - points[1][1]) <
|
|
1190
|
+
Math.abs(points[0][0] - points[1][0]);
|
|
1191
|
+
return points.filter((p, idx) => {
|
|
1192
|
+
// The very first and last points are always kept
|
|
1193
|
+
if (idx === 0 || idx === points.length - 1) {
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
const next = points[idx + 1];
|
|
1197
|
+
const nextHorizontal = Math.abs(p[1] - next[1]) < Math.abs(p[0] - next[0]);
|
|
1198
|
+
if (previousHorizontal === nextHorizontal) {
|
|
1199
|
+
previousHorizontal = nextHorizontal;
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
previousHorizontal = nextHorizontal;
|
|
1203
|
+
return true;
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
return points;
|
|
1207
|
+
};
|
|
1208
|
+
const removeElbowArrowShortSegments = (points) => {
|
|
1209
|
+
if (points.length >= 4) {
|
|
1210
|
+
return points.filter((p, idx) => {
|
|
1211
|
+
if (idx === 0 || idx === points.length - 1) {
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
const prev = points[idx - 1];
|
|
1215
|
+
const prevDist = pointDistance(prev, p);
|
|
1216
|
+
return prevDist > DEDUP_TRESHOLD;
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
return points;
|
|
1220
|
+
};
|
|
1221
|
+
const neighborIndexToHeading = (idx) => {
|
|
1222
|
+
switch (idx) {
|
|
1223
|
+
case 0:
|
|
1224
|
+
return HEADING_UP;
|
|
1225
|
+
case 1:
|
|
1226
|
+
return HEADING_RIGHT;
|
|
1227
|
+
case 2:
|
|
1228
|
+
return HEADING_DOWN;
|
|
1229
|
+
}
|
|
1230
|
+
return HEADING_LEFT;
|
|
1231
|
+
};
|
|
1232
|
+
const getGlobalPoint = (fixedPointRatio, initialPoint, otherPoint, elementsMap, boundElement, hoveredElement, isDragging) => {
|
|
1233
|
+
if (isDragging) {
|
|
1234
|
+
if (hoveredElement) {
|
|
1235
|
+
const snapPoint = getSnapPoint(initialPoint, otherPoint, hoveredElement, elementsMap);
|
|
1236
|
+
return snapToMid(hoveredElement, snapPoint);
|
|
1237
|
+
}
|
|
1238
|
+
return initialPoint;
|
|
1239
|
+
}
|
|
1240
|
+
if (boundElement) {
|
|
1241
|
+
const fixedGlobalPoint = getGlobalFixedPointForBindableElement(fixedPointRatio || [0, 0], boundElement);
|
|
1242
|
+
// NOTE: Resize scales the binding position point too, so we need to update it
|
|
1243
|
+
return Math.abs(distanceToBindableElement(boundElement, fixedGlobalPoint, elementsMap) -
|
|
1244
|
+
FIXED_BINDING_DISTANCE) > 0.01
|
|
1245
|
+
? getSnapPoint(initialPoint, otherPoint, boundElement, elementsMap)
|
|
1246
|
+
: fixedGlobalPoint;
|
|
1247
|
+
}
|
|
1248
|
+
return initialPoint;
|
|
1249
|
+
};
|
|
1250
|
+
const getSnapPoint = (p, otherPoint, element, elementsMap) => bindPointToSnapToElementOutline(isRectanguloidElement(element) ? avoidRectangularCorner(element, p) : p, otherPoint, element, elementsMap);
|
|
1251
|
+
const getBindPointHeading = (p, otherPoint, elementsMap, hoveredElement, origPoint) => getHeadingForElbowArrowSnap(p, otherPoint, hoveredElement, hoveredElement &&
|
|
1252
|
+
aabbForElement(hoveredElement, Array(4).fill(distanceToBindableElement(hoveredElement, p, elementsMap))), elementsMap, origPoint);
|
|
1253
|
+
const getHoveredElements = (origStartGlobalPoint, origEndGlobalPoint, elementsMap, zoom) => {
|
|
1254
|
+
// TODO: Might be a performance bottleneck and the Map type
|
|
1255
|
+
// remembers the insertion order anyway...
|
|
1256
|
+
const nonDeletedSceneElementsMap = toBrandedType(new Map([...elementsMap].filter((el) => !el[1].isDeleted)));
|
|
1257
|
+
const elements = Array.from(elementsMap.values());
|
|
1258
|
+
return [
|
|
1259
|
+
getHoveredElementForBinding(tupleToCoors(origStartGlobalPoint), elements, nonDeletedSceneElementsMap, zoom, true, true),
|
|
1260
|
+
getHoveredElementForBinding(tupleToCoors(origEndGlobalPoint), elements, nonDeletedSceneElementsMap, zoom, true, true),
|
|
1261
|
+
];
|
|
1262
|
+
};
|
|
1263
|
+
const gridAddressesEqual = (a, b) => a[0] === b[0] && a[1] === b[1];
|
|
1264
|
+
const validateElbowPoints = (points, tolerance = DEDUP_TRESHOLD) => points
|
|
1265
|
+
.slice(1)
|
|
1266
|
+
.map((p, i) => Math.abs(p[0] - points[i][0]) < tolerance ||
|
|
1267
|
+
Math.abs(p[1] - points[i][1]) < tolerance)
|
|
1268
|
+
.every(Boolean);
|