@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.
Files changed (178) hide show
  1. package/dist/browser/dev/excalidraw-assets-dev/{chunk-JGDL4H2X.js → chunk-3DLVY5XU.js} +8272 -6864
  2. package/dist/browser/dev/excalidraw-assets-dev/chunk-3DLVY5XU.js.map +7 -0
  3. package/dist/browser/dev/excalidraw-assets-dev/{chunk-V7NFEZA6.js → chunk-NOAEU4NM.js} +9 -2
  4. package/dist/browser/dev/excalidraw-assets-dev/chunk-NOAEU4NM.js.map +7 -0
  5. package/dist/browser/dev/excalidraw-assets-dev/{en-ZSVWGT55.js → en-7IBTMWBG.js} +2 -2
  6. package/dist/browser/dev/excalidraw-assets-dev/{image-RJG3J34Y.js → image-N5AC7SEK.js} +2 -6
  7. package/dist/browser/dev/index.css +85 -50
  8. package/dist/browser/dev/index.css.map +3 -3
  9. package/dist/browser/dev/index.js +4375 -3766
  10. package/dist/browser/dev/index.js.map +4 -4
  11. package/dist/browser/prod/excalidraw-assets/{chunk-LDVEIXGO.js → chunk-7CSIPVOW.js} +2 -2
  12. package/dist/browser/prod/excalidraw-assets/chunk-TX3BU7T2.js +47 -0
  13. package/dist/browser/prod/excalidraw-assets/{en-UPNEHLDS.js → en-LOGQBETY.js} +1 -1
  14. package/dist/browser/prod/excalidraw-assets/image-3V4U7GZE.js +1 -0
  15. package/dist/browser/prod/index.css +1 -1
  16. package/dist/browser/prod/index.js +40 -40
  17. package/dist/dev/index.css +85 -50
  18. package/dist/dev/index.css.map +3 -3
  19. package/dist/dev/index.js +8688 -6706
  20. package/dist/dev/index.js.map +4 -4
  21. package/dist/{prod/locales/en-ZXYG7GCR.json → dev/locales/en-V6KXFSCK.json} +8 -1
  22. package/dist/excalidraw/actions/actionAlign.d.ts +7 -6
  23. package/dist/excalidraw/actions/actionAlign.js +14 -14
  24. package/dist/excalidraw/actions/actionClipboard.d.ts +7 -3
  25. package/dist/excalidraw/actions/actionDeleteSelected.d.ts +7 -3
  26. package/dist/excalidraw/actions/actionDeleteSelected.js +103 -34
  27. package/dist/excalidraw/actions/actionDuplicateSelection.js +105 -95
  28. package/dist/excalidraw/actions/actionFlip.js +16 -7
  29. package/dist/excalidraw/actions/actionFrame.d.ts +493 -0
  30. package/dist/excalidraw/actions/actionFrame.js +45 -2
  31. package/dist/excalidraw/actions/actionGroup.js +6 -4
  32. package/dist/excalidraw/actions/actionProperties.js +145 -116
  33. package/dist/excalidraw/actions/actionSelectAll.js +4 -3
  34. package/dist/excalidraw/actions/shortcuts.d.ts +1 -1
  35. package/dist/excalidraw/actions/shortcuts.js +1 -0
  36. package/dist/excalidraw/actions/types.d.ts +1 -1
  37. package/dist/excalidraw/align.d.ts +2 -1
  38. package/dist/excalidraw/align.js +15 -6
  39. package/dist/excalidraw/clipboard.d.ts +27 -5
  40. package/dist/excalidraw/clipboard.js +55 -28
  41. package/dist/excalidraw/components/Actions.d.ts +2 -1
  42. package/dist/excalidraw/components/Actions.js +4 -2
  43. package/dist/excalidraw/components/ActiveConfirmDialog.d.ts +1 -1
  44. package/dist/excalidraw/components/ActiveConfirmDialog.js +2 -3
  45. package/dist/excalidraw/components/App.d.ts +1 -0
  46. package/dist/excalidraw/components/App.js +216 -111
  47. package/dist/excalidraw/components/ColorPicker/ColorInput.js +2 -3
  48. package/dist/excalidraw/components/ColorPicker/ColorPicker.js +2 -3
  49. package/dist/excalidraw/components/ColorPicker/CustomColorList.js +1 -1
  50. package/dist/excalidraw/components/ColorPicker/Picker.js +1 -1
  51. package/dist/excalidraw/components/ColorPicker/PickerColorList.js +1 -1
  52. package/dist/excalidraw/components/ColorPicker/ShadeList.js +1 -1
  53. package/dist/excalidraw/components/ColorPicker/colorPickerUtils.d.ts +1 -1
  54. package/dist/excalidraw/components/ColorPicker/colorPickerUtils.js +1 -1
  55. package/dist/excalidraw/components/CommandPalette/CommandPalette.js +3 -3
  56. package/dist/excalidraw/components/ConfirmDialog.js +17 -5
  57. package/dist/excalidraw/components/Dialog.js +2 -3
  58. package/dist/excalidraw/components/EyeDropper.d.ts +1 -1
  59. package/dist/excalidraw/components/EyeDropper.js +1 -1
  60. package/dist/excalidraw/components/IconPicker.d.ts +2 -2
  61. package/dist/excalidraw/components/IconPicker.js +56 -53
  62. package/dist/excalidraw/components/LayerUI.js +6 -6
  63. package/dist/excalidraw/components/LibraryMenu.d.ts +2 -16
  64. package/dist/excalidraw/components/LibraryMenu.js +70 -28
  65. package/dist/excalidraw/components/LibraryMenuHeaderContent.js +4 -5
  66. package/dist/excalidraw/components/MobileMenu.js +1 -1
  67. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirm.js +2 -3
  68. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.d.ts +1 -1
  69. package/dist/excalidraw/components/OverwriteConfirm/OverwriteConfirmState.js +2 -3
  70. package/dist/excalidraw/components/Range.d.ts +9 -0
  71. package/dist/excalidraw/components/Range.js +24 -0
  72. package/dist/excalidraw/components/SearchMenu.d.ts +1 -1
  73. package/dist/excalidraw/components/SearchMenu.js +3 -4
  74. package/dist/excalidraw/components/Sidebar/Sidebar.d.ts +1 -1
  75. package/dist/excalidraw/components/Sidebar/Sidebar.js +2 -3
  76. package/dist/excalidraw/components/Stats/Collapsible.d.ts +2 -1
  77. package/dist/excalidraw/components/Stats/Collapsible.js +2 -2
  78. package/dist/excalidraw/components/Stats/Dimension.js +94 -8
  79. package/dist/excalidraw/components/Stats/MultiDimension.js +8 -5
  80. package/dist/excalidraw/components/Stats/Position.js +63 -3
  81. package/dist/excalidraw/components/Stats/index.js +21 -4
  82. package/dist/excalidraw/components/Stats/utils.d.ts +1 -1
  83. package/dist/excalidraw/components/Stats/utils.js +2 -55
  84. package/dist/excalidraw/components/TTDDialog/TTDDialog.js +1 -1
  85. package/dist/excalidraw/components/ToolButton.js +4 -9
  86. package/dist/excalidraw/components/hoc/withInternalFallback.js +3 -3
  87. package/dist/excalidraw/components/hyperlink/Hyperlink.js +6 -12
  88. package/dist/excalidraw/components/icons.d.ts +9 -0
  89. package/dist/excalidraw/components/icons.js +4 -4
  90. package/dist/excalidraw/components/main-menu/DefaultItems.js +2 -3
  91. package/dist/excalidraw/constants.d.ts +5 -1
  92. package/dist/excalidraw/constants.js +9 -1
  93. package/dist/excalidraw/context/tunnels.d.ts +2 -1
  94. package/dist/excalidraw/context/tunnels.js +3 -1
  95. package/dist/excalidraw/data/blob.d.ts +1 -0
  96. package/dist/excalidraw/data/blob.js +7 -3
  97. package/dist/excalidraw/data/filesystem.d.ts +2 -1
  98. package/dist/excalidraw/data/filesystem.js +1 -0
  99. package/dist/excalidraw/data/image.d.ts +0 -6
  100. package/dist/excalidraw/data/image.js +1 -43
  101. package/dist/excalidraw/data/index.js +6 -6
  102. package/dist/excalidraw/data/library.d.ts +9 -3
  103. package/dist/excalidraw/data/library.js +43 -6
  104. package/dist/excalidraw/data/restore.js +26 -8
  105. package/dist/excalidraw/data/url.d.ts +0 -1
  106. package/dist/excalidraw/data/url.js +2 -4
  107. package/dist/excalidraw/editor-jotai.d.ts +56 -0
  108. package/dist/excalidraw/editor-jotai.js +8 -0
  109. package/dist/excalidraw/element/binding.d.ts +9 -6
  110. package/dist/excalidraw/element/binding.js +124 -44
  111. package/dist/excalidraw/element/bounds.js +10 -0
  112. package/dist/excalidraw/element/cropElement.d.ts +5 -0
  113. package/dist/excalidraw/element/cropElement.js +28 -1
  114. package/dist/excalidraw/element/dragElements.js +13 -7
  115. package/dist/excalidraw/element/elbowArrow.d.ts +16 -0
  116. package/dist/excalidraw/element/elbowArrow.js +1268 -0
  117. package/dist/excalidraw/element/embeddable.js +4 -5
  118. package/dist/excalidraw/element/flowchart.d.ts +1 -1
  119. package/dist/excalidraw/element/flowchart.js +25 -9
  120. package/dist/excalidraw/element/heading.d.ts +5 -1
  121. package/dist/excalidraw/element/heading.js +5 -1
  122. package/dist/excalidraw/element/image.js +19 -5
  123. package/dist/excalidraw/element/linearElementEditor.d.ts +9 -10
  124. package/dist/excalidraw/element/linearElementEditor.js +97 -38
  125. package/dist/excalidraw/element/mutateElement.d.ts +3 -1
  126. package/dist/excalidraw/element/mutateElement.js +31 -4
  127. package/dist/excalidraw/element/newElement.d.ts +8 -12
  128. package/dist/excalidraw/element/newElement.js +36 -21
  129. package/dist/excalidraw/element/resizeElements.d.ts +20 -5
  130. package/dist/excalidraw/element/resizeElements.js +593 -361
  131. package/dist/excalidraw/element/sortElements.js +1 -4
  132. package/dist/excalidraw/element/types.d.ts +23 -1
  133. package/dist/excalidraw/fonts/Fonts.d.ts +0 -16
  134. package/dist/excalidraw/fonts/Fonts.js +6 -31
  135. package/dist/excalidraw/frame.d.ts +11 -5
  136. package/dist/excalidraw/frame.js +146 -35
  137. package/dist/excalidraw/groups.js +3 -0
  138. package/dist/excalidraw/hooks/useLibraryItemSvg.d.ts +1 -1
  139. package/dist/excalidraw/hooks/useLibraryItemSvg.js +2 -3
  140. package/dist/excalidraw/hooks/useScrollPosition.js +1 -1
  141. package/dist/excalidraw/i18n.js +3 -4
  142. package/dist/excalidraw/index.js +3 -4
  143. package/dist/excalidraw/locales/en.json +8 -1
  144. package/dist/excalidraw/renderer/interactiveScene.js +43 -32
  145. package/dist/excalidraw/renderer/staticScene.js +6 -4
  146. package/dist/excalidraw/renderer/staticSvgScene.js +1 -1
  147. package/dist/excalidraw/scene/Shape.js +40 -17
  148. package/dist/excalidraw/scene/comparisons.d.ts +0 -477
  149. package/dist/excalidraw/scene/comparisons.js +0 -37
  150. package/dist/excalidraw/scene/export.d.ts +7 -0
  151. package/dist/excalidraw/scene/export.js +107 -43
  152. package/dist/excalidraw/scene/index.d.ts +1 -1
  153. package/dist/excalidraw/scene/index.js +1 -1
  154. package/dist/excalidraw/scene/selection.js +4 -1
  155. package/dist/excalidraw/types.d.ts +15 -0
  156. package/dist/excalidraw/utility-types.d.ts +1 -0
  157. package/dist/excalidraw/utils.d.ts +8 -1
  158. package/dist/excalidraw/utils.js +9 -0
  159. package/dist/excalidraw/visualdebug.d.ts +8 -1
  160. package/dist/excalidraw/visualdebug.js +3 -0
  161. package/dist/math/line.d.ts +19 -0
  162. package/dist/math/line.js +32 -3
  163. package/dist/math/point.d.ts +10 -0
  164. package/dist/math/point.js +12 -1
  165. package/dist/prod/index.css +1 -1
  166. package/dist/prod/index.js +29 -44
  167. package/dist/{dev/locales/en-ZXYG7GCR.json → prod/locales/en-V6KXFSCK.json} +8 -1
  168. package/package.json +5 -2
  169. package/dist/browser/dev/excalidraw-assets-dev/chunk-JGDL4H2X.js.map +0 -7
  170. package/dist/browser/dev/excalidraw-assets-dev/chunk-V7NFEZA6.js.map +0 -7
  171. package/dist/browser/prod/excalidraw-assets/chunk-S2XKB3DE.js +0 -62
  172. package/dist/browser/prod/excalidraw-assets/image-OFI2YYMP.js +0 -1
  173. package/dist/excalidraw/element/routing.d.ts +0 -12
  174. package/dist/excalidraw/element/routing.js +0 -642
  175. package/dist/excalidraw/jotai.d.ts +0 -34
  176. package/dist/excalidraw/jotai.js +0 -18
  177. /package/dist/browser/dev/excalidraw-assets-dev/{en-ZSVWGT55.js.map → en-7IBTMWBG.js.map} +0 -0
  178. /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);