symbiote-ui 0.3.0-alpha.4

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 (322) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/LICENSE +21 -0
  3. package/README.md +76 -0
  4. package/canvas/AutoLayout.js +731 -0
  5. package/canvas/Breadcrumb/Breadcrumb.css.js +75 -0
  6. package/canvas/Breadcrumb/Breadcrumb.js +96 -0
  7. package/canvas/Breadcrumb/Breadcrumb.tpl.js +7 -0
  8. package/canvas/CanvasConnectionRenderer.js +971 -0
  9. package/canvas/CanvasGraph/CanvasGraph.css.js +29 -0
  10. package/canvas/CanvasGraph/CanvasGraph.js +1697 -0
  11. package/canvas/CanvasGraph/CanvasGraphDrawState.js +280 -0
  12. package/canvas/CanvasGraph/CanvasGraphGeometry.js +194 -0
  13. package/canvas/CanvasViewport.js +550 -0
  14. package/canvas/ConnectionRenderer.js +1283 -0
  15. package/canvas/FlowSimulator.js +326 -0
  16. package/canvas/ForceLayout.js +226 -0
  17. package/canvas/ForceWorker.js +1303 -0
  18. package/canvas/FrameManager.js +223 -0
  19. package/canvas/GraphExplorerShell/GraphExplorerShell.css.js +136 -0
  20. package/canvas/GraphExplorerShell/GraphExplorerShell.js +129 -0
  21. package/canvas/GraphExplorerShell/GraphExplorerShell.tpl.js +12 -0
  22. package/canvas/GraphTabs/GraphTabs.css.js +101 -0
  23. package/canvas/GraphTabs/GraphTabs.js +189 -0
  24. package/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  25. package/canvas/LODManager.js +88 -0
  26. package/canvas/Minimap/Minimap.css.js +73 -0
  27. package/canvas/Minimap/Minimap.js +210 -0
  28. package/canvas/Minimap/Minimap.tpl.js +7 -0
  29. package/canvas/NodeCanvas/NodeCanvas.css.js +398 -0
  30. package/canvas/NodeCanvas/NodeCanvas.js +1499 -0
  31. package/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  32. package/canvas/NodeSearch/NodeSearch.css.js +97 -0
  33. package/canvas/NodeSearch/NodeSearch.js +140 -0
  34. package/canvas/NodeSearch/NodeSearch.tpl.js +25 -0
  35. package/canvas/NodeViewManager.js +748 -0
  36. package/canvas/PcbRouteDiagnostics.js +463 -0
  37. package/canvas/PcbRouter.js +1127 -0
  38. package/canvas/PinExpansion.js +134 -0
  39. package/canvas/PseudoConnection.js +84 -0
  40. package/canvas/SelectionSync.js +163 -0
  41. package/canvas/SubgraphManager.js +203 -0
  42. package/canvas/SubgraphRouter.js +452 -0
  43. package/canvas/ViewportActions.js +473 -0
  44. package/canvas/graph-explorer.js +339 -0
  45. package/canvas/graph-layout.js +148 -0
  46. package/canvas/graph-model.js +68 -0
  47. package/canvas/html-in-canvas.js +202 -0
  48. package/canvas/project-graph-builder.js +440 -0
  49. package/canvas/project-graph-model.js +183 -0
  50. package/chat/ChatComposer/ChatComposer.css.js +652 -0
  51. package/chat/ChatComposer/ChatComposer.js +304 -0
  52. package/chat/ChatList/ChatList.css.js +102 -0
  53. package/chat/ChatList/ChatList.js +99 -0
  54. package/chat/ChatList/ChatList.tpl.js +20 -0
  55. package/chat/ChatListItem/ChatListItem.css.js +117 -0
  56. package/chat/ChatListItem/ChatListItem.js +32 -0
  57. package/chat/ChatListItem/ChatListItem.tpl.js +17 -0
  58. package/chat/ChatMessageItem/ChatMessageItem.css.js +628 -0
  59. package/chat/ChatMessageItem/ChatMessageItem.js +156 -0
  60. package/chat/ChatSidebar/ChatSidebar.css.js +150 -0
  61. package/chat/ChatSidebar/ChatSidebar.js +230 -0
  62. package/chat/ChatSidebar/ChatSidebar.tpl.js +18 -0
  63. package/chat/ChatSidebar/constants.js +11 -0
  64. package/chat/ChatSidebarItem/ChatSidebarItem.css.js +445 -0
  65. package/chat/ChatSidebarItem/ChatSidebarItem.js +304 -0
  66. package/chat/ChatTranscript/ChatTranscript.css.js +90 -0
  67. package/chat/ChatTranscript/ChatTranscript.js +244 -0
  68. package/chat/chat-context.js +123 -0
  69. package/chat/message-model.js +156 -0
  70. package/cli.js +20 -0
  71. package/control/Button/Button.css.js +93 -0
  72. package/control/Button/Button.js +78 -0
  73. package/control/Button/Button.tpl.js +3 -0
  74. package/control/Field/Field.css.js +91 -0
  75. package/control/Field/Field.js +17 -0
  76. package/control/Field/Field.tpl.js +3 -0
  77. package/core/Connection.js +47 -0
  78. package/core/Editor.js +449 -0
  79. package/core/Frame.js +33 -0
  80. package/core/GraphMermaid.js +348 -0
  81. package/core/GraphText.js +228 -0
  82. package/core/Node.js +145 -0
  83. package/core/Portal.js +106 -0
  84. package/core/Socket.js +187 -0
  85. package/core/SubgraphNode.js +121 -0
  86. package/core/base-path.js +55 -0
  87. package/core/dom-utils.js +14 -0
  88. package/core/index.js +18 -0
  89. package/core/local-cache.js +26 -0
  90. package/core/state-sync.js +227 -0
  91. package/custom-elements.json +6380 -0
  92. package/discover.js +240 -0
  93. package/display/Badge/Badge.css.js +44 -0
  94. package/display/Badge/Badge.js +17 -0
  95. package/display/Badge/Badge.tpl.js +3 -0
  96. package/display/Banner/Banner.css.js +61 -0
  97. package/display/Banner/Banner.js +17 -0
  98. package/display/Banner/Banner.tpl.js +3 -0
  99. package/display/CodeBlock/CodeBlock.css.js +194 -0
  100. package/display/CodeBlock/CodeBlock.js +220 -0
  101. package/display/CodeBlock/CodeBlock.tpl.js +11 -0
  102. package/display/DataTable/DataTable.css.js +101 -0
  103. package/display/DataTable/DataTable.js +136 -0
  104. package/display/DataTable/DataTable.tpl.js +13 -0
  105. package/display/EmptyState/EmptyState.css.js +33 -0
  106. package/display/EmptyState/EmptyState.js +17 -0
  107. package/display/EmptyState/EmptyState.tpl.js +3 -0
  108. package/display/EventFeed/EventFeed.css.js +145 -0
  109. package/display/EventFeed/EventFeed.js +64 -0
  110. package/display/EventFeed/EventFeed.tpl.js +14 -0
  111. package/display/EventFeed/EventFeedItem.js +116 -0
  112. package/display/EventFeed/EventFeedItem.tpl.js +22 -0
  113. package/display/LoadingOverlay/LoadingOverlay.css.js +91 -0
  114. package/display/LoadingOverlay/LoadingOverlay.js +48 -0
  115. package/display/LoadingOverlay/LoadingOverlay.tpl.js +12 -0
  116. package/display/Metric/Metric.css.js +60 -0
  117. package/display/Metric/Metric.js +17 -0
  118. package/display/Metric/Metric.tpl.js +6 -0
  119. package/display/OutputGraphPreview/OutputGraphPreview.css.js +122 -0
  120. package/display/OutputGraphPreview/OutputGraphPreview.js +89 -0
  121. package/display/OutputGraphPreview/OutputGraphPreview.tpl.js +13 -0
  122. package/display/OutputListPreview/OutputListPreview.css.js +109 -0
  123. package/display/OutputListPreview/OutputListPreview.js +77 -0
  124. package/display/OutputListPreview/OutputListPreview.tpl.js +13 -0
  125. package/display/SourceEditor/SourceEditor.css.js +39 -0
  126. package/display/SourceEditor/SourceEditor.js +129 -0
  127. package/display/SourceEditor/SourceEditor.tpl.js +10 -0
  128. package/display/SourceViewer/SourceViewer.css.js +80 -0
  129. package/display/SourceViewer/SourceViewer.js +418 -0
  130. package/display/SourceViewer/SourceViewer.tpl.js +17 -0
  131. package/display/StatusRibbon/StatusRibbon.css.js +73 -0
  132. package/display/StatusRibbon/StatusRibbon.js +87 -0
  133. package/display/StatusRibbon/StatusRibbon.tpl.js +7 -0
  134. package/display/event-feed-adapter.js +72 -0
  135. package/display/format-utils.js +29 -0
  136. package/display/highlight.js +659 -0
  137. package/display/icons.js +37 -0
  138. package/display/markdown-formatter.js +60 -0
  139. package/display/network-approval-page.js +487 -0
  140. package/display/output-preview.js +261 -0
  141. package/effects/CellBg/CellBg.css.js +33 -0
  142. package/effects/CellBg/CellBg.js +410 -0
  143. package/effects/CellBg/CellBg.tpl.js +5 -0
  144. package/graph/canvas-adapter.js +223 -0
  145. package/graph/graph-algorithms.js +31 -0
  146. package/graph/index.js +46 -0
  147. package/graph/model.js +176 -0
  148. package/graph/project-graph-build.js +66 -0
  149. package/graph/project-graph-metadata.js +253 -0
  150. package/graph/project-package.js +128 -0
  151. package/graph/project-runtime.js +116 -0
  152. package/graph/project-transaction.js +284 -0
  153. package/graph/skeleton-utils.js +84 -0
  154. package/graph/theme-contract.js +36 -0
  155. package/graph/transaction-parser.js +56 -0
  156. package/icons/MaterialSymbols.js +69 -0
  157. package/icons/material-symbols-outlined-400.ttf +0 -0
  158. package/icons/material-symbols.css +24 -0
  159. package/index.js +95 -0
  160. package/inspector/InspectorPanel/InspectorPanel.css.js +375 -0
  161. package/inspector/InspectorPanel/InspectorPanel.js +368 -0
  162. package/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  163. package/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  164. package/inspector/TemplatePreview/TemplatePreview.js +145 -0
  165. package/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  166. package/interactions/ConnectFlow.js +304 -0
  167. package/interactions/Drag.js +104 -0
  168. package/interactions/Selector.js +133 -0
  169. package/interactions/SnapGrid.js +66 -0
  170. package/interactions/Zoom.js +139 -0
  171. package/layout/ActionZone/ActionZone.css.js +88 -0
  172. package/layout/ActionZone/ActionZone.js +261 -0
  173. package/layout/ActionZone/ActionZone.tpl.js +11 -0
  174. package/layout/CrossLayoutPortalBridge/CrossLayoutPortalBridge.js +255 -0
  175. package/layout/Layout/Layout.css.js +91 -0
  176. package/layout/Layout/Layout.js +637 -0
  177. package/layout/Layout/Layout.tpl.js +27 -0
  178. package/layout/LayoutNode/LayoutNode.css.js +302 -0
  179. package/layout/LayoutNode/LayoutNode.js +509 -0
  180. package/layout/LayoutNode/LayoutNode.tpl.js +39 -0
  181. package/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  182. package/layout/LayoutPreview/LayoutPreview.js +102 -0
  183. package/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  184. package/layout/LayoutRouter/LayoutRouter.js +274 -0
  185. package/layout/LayoutRouter/SectionRegistry.js +135 -0
  186. package/layout/LayoutRouter/routerSync.js +250 -0
  187. package/layout/LayoutSidebar/LayoutSidebar.css.js +411 -0
  188. package/layout/LayoutSidebar/LayoutSidebar.js +368 -0
  189. package/layout/LayoutSidebar/LayoutSidebar.tpl.js +26 -0
  190. package/layout/LayoutSidebar/SidebarSection.css.js +20 -0
  191. package/layout/LayoutSidebar/SidebarSection.js +184 -0
  192. package/layout/LayoutSidebar/SidebarSection.tpl.js +22 -0
  193. package/layout/LayoutTree.js +373 -0
  194. package/layout/PanelMenu/PanelMenu.css.js +43 -0
  195. package/layout/PanelMenu/PanelMenu.js +95 -0
  196. package/layout/PanelMenu/PanelMenu.tpl.js +17 -0
  197. package/layout/ProjectTabs/ProjectTabs.css.js +188 -0
  198. package/layout/ProjectTabs/ProjectTabs.js +77 -0
  199. package/layout/ProjectTabs/ProjectTabs.tpl.js +15 -0
  200. package/layout/index.js +40 -0
  201. package/list/ListDetailShell/ListDetailShell.css.js +128 -0
  202. package/list/ListDetailShell/ListDetailShell.js +72 -0
  203. package/list/ListDetailShell/ListDetailShell.tpl.js +36 -0
  204. package/list/ListItem/ListItem.css.js +111 -0
  205. package/list/ListItem/ListItem.js +66 -0
  206. package/list/ListItem/ListItem.tpl.js +18 -0
  207. package/locale/index.js +503 -0
  208. package/manifest/component-registry.js +2446 -0
  209. package/manifest/graph-schema.js +285 -0
  210. package/manifest/index.js +6 -0
  211. package/manifest/project-schema-catalog.js +246 -0
  212. package/manifest/rule-catalog.js +201 -0
  213. package/manifest/theme-catalog.js +2149 -0
  214. package/manifest/ui-schema-catalog.js +334 -0
  215. package/menu/ContextMenu/ContextMenu.css.js +61 -0
  216. package/menu/ContextMenu/ContextMenu.js +82 -0
  217. package/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  218. package/navigation/QuickOpen/QuickOpen.css.js +92 -0
  219. package/navigation/QuickOpen/QuickOpen.js +185 -0
  220. package/navigation/QuickOpen/QuickOpen.tpl.js +15 -0
  221. package/navigation/quick-open-utils.js +101 -0
  222. package/node/CtrlItem/CtrlItem.css.js +41 -0
  223. package/node/CtrlItem/CtrlItem.js +24 -0
  224. package/node/CtrlItem/CtrlItem.tpl.js +17 -0
  225. package/node/GraphFrame/GraphFrame.css.js +66 -0
  226. package/node/GraphFrame/GraphFrame.js +32 -0
  227. package/node/GraphFrame/GraphFrame.tpl.js +13 -0
  228. package/node/GraphNode/GraphNode.css.js +815 -0
  229. package/node/GraphNode/GraphNode.js +173 -0
  230. package/node/GraphNode/GraphNode.tpl.js +33 -0
  231. package/node/NodeCallout/NodeCallout.css.js +91 -0
  232. package/node/NodeCallout/NodeCallout.js +281 -0
  233. package/node/NodeCallout/NodeCallout.tpl.js +8 -0
  234. package/node/NodeSocket/NodeSocket.css.js +68 -0
  235. package/node/NodeSocket/NodeSocket.js +26 -0
  236. package/node/NodeSocket/NodeSocket.tpl.js +7 -0
  237. package/node/PortItem/PortItem.css.js +93 -0
  238. package/node/PortItem/PortItem.js +87 -0
  239. package/node/PortItem/PortItem.tpl.js +10 -0
  240. package/package.json +165 -0
  241. package/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  242. package/palette/PaletteBrowser/PaletteBrowser.js +152 -0
  243. package/palette/PaletteBrowser/PaletteBrowser.tpl.js +23 -0
  244. package/plugins/History.js +408 -0
  245. package/plugins/Readonly.js +60 -0
  246. package/rules/symbiote-3x.json +170 -0
  247. package/schemas/component-descriptor-v1.json +91 -0
  248. package/schemas/component-descriptor-v2.json +145 -0
  249. package/schemas/graph-model-v1.json +179 -0
  250. package/schemas/graph-v1.json +91 -0
  251. package/schemas/project-package-v1.json +102 -0
  252. package/schemas/project-transaction-v1.json +114 -0
  253. package/schemas/runtime-ui-v1.json +80 -0
  254. package/schemas/theme-rule-block-v1.json +73 -0
  255. package/shapes/CircleShape.js +79 -0
  256. package/shapes/CommentShape.js +35 -0
  257. package/shapes/DiamondShape.js +130 -0
  258. package/shapes/NodeShape.js +79 -0
  259. package/shapes/PillShape.js +91 -0
  260. package/shapes/RectShape.js +84 -0
  261. package/shapes/SVGShape.js +525 -0
  262. package/shapes/index.js +63 -0
  263. package/surface/Card/Card.css.js +57 -0
  264. package/surface/Card/Card.js +17 -0
  265. package/surface/Card/Card.tpl.js +3 -0
  266. package/themes/Palette.js +30 -0
  267. package/themes/Skin.js +113 -0
  268. package/themes/Theme.js +82 -0
  269. package/themes/carbon.js +135 -0
  270. package/themes/dark.js +140 -0
  271. package/themes/default-dark.js +714 -0
  272. package/themes/default-provider.css +635 -0
  273. package/themes/default-provider.js +718 -0
  274. package/themes/ebook.js +136 -0
  275. package/themes/grey.js +137 -0
  276. package/themes/light.js +139 -0
  277. package/themes/neon.js +138 -0
  278. package/themes/pcb.js +273 -0
  279. package/themes/synthwave.js +138 -0
  280. package/tokens/base.json +29 -0
  281. package/tokens/themes/carbon.json +11 -0
  282. package/tokens/themes/dark.json +12 -0
  283. package/tokens/themes/default-dark.json +1543 -0
  284. package/tokens/themes/default-provider.json +1543 -0
  285. package/tokens/themes/ebook.json +11 -0
  286. package/tokens/themes/grey.json +11 -0
  287. package/tokens/themes/light.json +12 -0
  288. package/tokens/themes/neon.json +11 -0
  289. package/tokens/themes/pcb.json +11 -0
  290. package/tokens/themes/synthwave.json +11 -0
  291. package/toolbar/QuickToolbar/QuickToolbar.css.js +152 -0
  292. package/toolbar/QuickToolbar/QuickToolbar.js +529 -0
  293. package/toolbar/QuickToolbar/QuickToolbar.tpl.js +34 -0
  294. package/tree/TreePanel/TreePanel.css.js +112 -0
  295. package/tree/TreePanel/TreePanel.js +147 -0
  296. package/tree/TreePanel/TreePanel.tpl.js +18 -0
  297. package/tree/TreeView/TreeView.css.js +122 -0
  298. package/tree/TreeView/TreeView.js +365 -0
  299. package/tree/TreeView/TreeView.tpl.js +10 -0
  300. package/ui/dialogs.js +221 -0
  301. package/ui/host-adapters.js +114 -0
  302. package/ui/index.js +660 -0
  303. package/ui/locale.js +50 -0
  304. package/ui/overlay-stack.js +89 -0
  305. package/ui/shared-styles.js +26 -0
  306. package/webmcp.js +37 -0
  307. package/xr/deep-graph.js +646 -0
  308. package/xr/emulation.js +198 -0
  309. package/xr/gesture.js +228 -0
  310. package/xr/html-canvas-renderer.js +472 -0
  311. package/xr/index.js +15 -0
  312. package/xr/layout-projection.js +1046 -0
  313. package/xr/panel-frame.js +128 -0
  314. package/xr/panel-host.js +267 -0
  315. package/xr/pointer.js +258 -0
  316. package/xr/scene-controller.js +242 -0
  317. package/xr/spatial-scene.js +212 -0
  318. package/xr/theme-bridge.js +105 -0
  319. package/xr/three-webxr-adapter.js +3439 -0
  320. package/xr/webgl-layer-renderer.js +419 -0
  321. package/xr/webxr.js +679 -0
  322. package/xr/workbench.js +516 -0
@@ -0,0 +1,1127 @@
1
+ function snapGrid(value, grid) {
2
+ return Math.round(value / grid) * grid;
3
+ }
4
+
5
+ function snapOutside(value, grid, direction) {
6
+ return (direction < 0 ? Math.floor(value / grid) : Math.ceil(value / grid)) * grid;
7
+ }
8
+
9
+ function alignGrid(value, grid, snapToGrid) {
10
+ return snapToGrid ? snapGrid(value, grid) : value;
11
+ }
12
+
13
+ function alignOutside(value, grid, direction, snapToGrid) {
14
+ return snapToGrid ? snapOutside(value, grid, direction) : value;
15
+ }
16
+
17
+ function snapDir(deg) {
18
+ const r = ((deg % 360) + 360) % 360;
19
+ if (r < 45 || r >= 315) return { dx: 1, dy: 0 };
20
+ if (r >= 45 && r < 135) return { dx: 0, dy: 1 };
21
+ if (r >= 135 && r < 225) return { dx: -1, dy: 0 };
22
+ return { dx: 0, dy: -1 };
23
+ }
24
+
25
+ function dirAngle(dir) {
26
+ if (dir.dx > 0) return 0;
27
+ if (dir.dx < 0) return 180;
28
+ if (dir.dy > 0) return 90;
29
+ if (dir.dy < 0) return 270;
30
+ return 0;
31
+ }
32
+
33
+ function parallelGroup(connections, conn) {
34
+ return connections.filter((candidate) =>
35
+ candidate.from === conn.from ||
36
+ candidate.from === conn.to ||
37
+ candidate.to === conn.from ||
38
+ candidate.to === conn.to ||
39
+ (candidate.from === conn.from && candidate.to === conn.to)
40
+ );
41
+ }
42
+
43
+ function parallelShift(connections, conn, grid) {
44
+ const group = parallelGroup(connections, conn);
45
+ if (group.length <= 1) return 0;
46
+ const index = Math.max(0, group.findIndex((candidate) => candidate.id === conn.id));
47
+ return (index - (group.length - 1) / 2) * grid;
48
+ }
49
+
50
+ function parallelSpread(connections, conn, grid) {
51
+ const group = parallelGroup(connections, conn);
52
+ if (group.length <= 1) return 0;
53
+ return (group.length - 1) / 2 * grid;
54
+ }
55
+
56
+ function uniqueSorted(values) {
57
+ return [...new Set(values.map((value) => Math.round(value * 100) / 100))]
58
+ .filter(Number.isFinite)
59
+ .sort((a, b) => a - b);
60
+ }
61
+
62
+ function endpointStub(point, rect, dir, distance) {
63
+ if (dir.dx > 0) {
64
+ return { x: Math.max(point.x + distance, rect.x + rect.w + distance), y: point.y };
65
+ }
66
+ if (dir.dx < 0) {
67
+ return { x: Math.min(point.x - distance, rect.x - distance), y: point.y };
68
+ }
69
+ if (dir.dy > 0) {
70
+ return { x: point.x, y: Math.max(point.y + distance, rect.y + rect.h + distance) };
71
+ }
72
+ if (dir.dy < 0) {
73
+ return { x: point.x, y: Math.min(point.y - distance, rect.y - distance) };
74
+ }
75
+ return { ...point };
76
+ }
77
+
78
+ const ORTHOGONAL_DIRECTIONS = [
79
+ { dx: 1, dy: 0 },
80
+ { dx: 0, dy: 1 },
81
+ { dx: -1, dy: 0 },
82
+ { dx: 0, dy: -1 },
83
+ ];
84
+
85
+ function directionKey(dir) {
86
+ return `${dir.dx},${dir.dy}`;
87
+ }
88
+
89
+ function addDirection(result, seen, dir) {
90
+ const key = directionKey(dir);
91
+ if (seen.has(key)) return;
92
+ seen.add(key);
93
+ result.push(dir);
94
+ }
95
+
96
+ function orderedDirections(preferred, fromPoint, toPoint) {
97
+ const result = [];
98
+ const seen = new Set();
99
+ const delta = {
100
+ x: toPoint.x - fromPoint.x,
101
+ y: toPoint.y - fromPoint.y,
102
+ };
103
+
104
+ addDirection(result, seen, preferred);
105
+ if (preferred.dx !== 0) {
106
+ const verticalSign = Math.sign(delta.y) || 1;
107
+ addDirection(result, seen, { dx: 0, dy: verticalSign });
108
+ addDirection(result, seen, { dx: 0, dy: -verticalSign });
109
+ } else {
110
+ const horizontalSign = Math.sign(delta.x) || 1;
111
+ addDirection(result, seen, { dx: horizontalSign, dy: 0 });
112
+ addDirection(result, seen, { dx: -horizontalSign, dy: 0 });
113
+ }
114
+ addDirection(result, seen, { dx: -preferred.dx, dy: -preferred.dy });
115
+
116
+ for (const dir of ORTHOGONAL_DIRECTIONS) {
117
+ addDirection(result, seen, dir);
118
+ }
119
+
120
+ return result;
121
+ }
122
+
123
+ function compactPoints(points) {
124
+ const compact = [];
125
+ for (const point of points) {
126
+ const previous = compact.at(-1);
127
+ if (!previous || Math.abs(previous.x - point.x) > 0.5 || Math.abs(previous.y - point.y) > 0.5) {
128
+ compact.push(point);
129
+ }
130
+ }
131
+ return compact;
132
+ }
133
+
134
+ function simplifyCollinear(points) {
135
+ const simplified = compactPoints(points);
136
+ let changed = true;
137
+ while (changed) {
138
+ changed = false;
139
+ for (let index = 1; index < simplified.length - 1; index += 1) {
140
+ const prev = simplified[index - 1];
141
+ const curr = simplified[index];
142
+ const next = simplified[index + 1];
143
+ const sameVertical = Math.abs(prev.x - curr.x) < 0.5 && Math.abs(curr.x - next.x) < 0.5;
144
+ const sameHorizontal = Math.abs(prev.y - curr.y) < 0.5 && Math.abs(curr.y - next.y) < 0.5;
145
+ if (sameVertical || sameHorizontal) {
146
+ simplified.splice(index, 1);
147
+ changed = true;
148
+ break;
149
+ }
150
+ }
151
+ }
152
+ return simplified;
153
+ }
154
+
155
+ function pointInRect(point, rect) {
156
+ return (
157
+ point.x >= rect.x &&
158
+ point.x <= rect.x + rect.w &&
159
+ point.y >= rect.y &&
160
+ point.y <= rect.y + rect.h
161
+ );
162
+ }
163
+
164
+ function rectsOverlap(a, b, pad = 0) {
165
+ return !(
166
+ a.x + a.w + pad < b.x - pad ||
167
+ b.x + b.w + pad < a.x - pad ||
168
+ a.y + a.h + pad < b.y - pad ||
169
+ b.y + b.h + pad < a.y - pad
170
+ );
171
+ }
172
+
173
+ function orientation(a, b, c) {
174
+ const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
175
+ if (Math.abs(value) < 0.5) return 0;
176
+ return value > 0 ? 1 : 2;
177
+ }
178
+
179
+ function onSegment(a, b, c) {
180
+ return (
181
+ b.x <= Math.max(a.x, c.x) + 0.5 &&
182
+ b.x >= Math.min(a.x, c.x) - 0.5 &&
183
+ b.y <= Math.max(a.y, c.y) + 0.5 &&
184
+ b.y >= Math.min(a.y, c.y) - 0.5
185
+ );
186
+ }
187
+
188
+ function segmentsIntersect(a1, a2, b1, b2) {
189
+ const o1 = orientation(a1, a2, b1);
190
+ const o2 = orientation(a1, a2, b2);
191
+ const o3 = orientation(b1, b2, a1);
192
+ const o4 = orientation(b1, b2, a2);
193
+
194
+ if (o1 !== o2 && o3 !== o4) return true;
195
+ if (o1 === 0 && onSegment(a1, b1, a2)) return true;
196
+ if (o2 === 0 && onSegment(a1, b2, a2)) return true;
197
+ if (o3 === 0 && onSegment(b1, a1, b2)) return true;
198
+ if (o4 === 0 && onSegment(b1, a2, b2)) return true;
199
+ return false;
200
+ }
201
+
202
+ function samePoint(a, b) {
203
+ return Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) < 0.5;
204
+ }
205
+
206
+ function countSelfIntersections(points) {
207
+ let count = 0;
208
+ for (let aIndex = 0; aIndex < points.length - 1; aIndex += 1) {
209
+ for (let bIndex = aIndex + 2; bIndex < points.length - 1; bIndex += 1) {
210
+ const a1 = points[aIndex];
211
+ const a2 = points[aIndex + 1];
212
+ const b1 = points[bIndex];
213
+ const b2 = points[bIndex + 1];
214
+ if (samePoint(a2, b1)) continue;
215
+ if (aIndex === 0 && bIndex === points.length - 2 && samePoint(a1, b2)) continue;
216
+ if (segmentsIntersect(a1, a2, b1, b2)) count += 1;
217
+ }
218
+ }
219
+ return count;
220
+ }
221
+
222
+ function segmentIntersectsRect(a, b, rect, pad = 0) {
223
+ const expanded = {
224
+ x: rect.x - pad,
225
+ y: rect.y - pad,
226
+ w: rect.w + pad * 2,
227
+ h: rect.h + pad * 2,
228
+ };
229
+
230
+ if (pointInRect(a, expanded) || pointInRect(b, expanded)) return true;
231
+
232
+ const left = expanded.x;
233
+ const right = expanded.x + expanded.w;
234
+ const top = expanded.y;
235
+ const bottom = expanded.y + expanded.h;
236
+ const edges = [
237
+ [{ x: left, y: top }, { x: right, y: top }],
238
+ [{ x: right, y: top }, { x: right, y: bottom }],
239
+ [{ x: right, y: bottom }, { x: left, y: bottom }],
240
+ [{ x: left, y: bottom }, { x: left, y: top }],
241
+ ];
242
+
243
+ return edges.some(([edgeStart, edgeEnd]) => segmentsIntersect(a, b, edgeStart, edgeEnd));
244
+ }
245
+
246
+ function segmentCrossesRectInterior(a, b, rect, inset = 3) {
247
+ const inner = {
248
+ x: rect.x + inset,
249
+ y: rect.y + inset,
250
+ w: rect.w - inset * 2,
251
+ h: rect.h - inset * 2,
252
+ };
253
+ if (inner.w <= 0 || inner.h <= 0) return false;
254
+ return segmentIntersectsRect(a, b, inner, 0);
255
+ }
256
+
257
+ function pointOnRectBoundary(point, rect, tolerance = 0.25) {
258
+ const withinX = point.x >= rect.x - tolerance && point.x <= rect.x + rect.w + tolerance;
259
+ const withinY = point.y >= rect.y - tolerance && point.y <= rect.y + rect.h + tolerance;
260
+ if (!withinX || !withinY) return false;
261
+ return (
262
+ Math.abs(point.x - rect.x) <= tolerance ||
263
+ Math.abs(point.x - (rect.x + rect.w)) <= tolerance ||
264
+ Math.abs(point.y - rect.y) <= tolerance ||
265
+ Math.abs(point.y - (rect.y + rect.h)) <= tolerance
266
+ );
267
+ }
268
+
269
+ function mergeEndpointRects(rects, fromRect, toRect) {
270
+ const merged = new Map(rects.map((rect) => [rect.id, rect]));
271
+ merged.set(fromRect.id, fromRect);
272
+ merged.set(toRect.id, toRect);
273
+ return [...merged.values()];
274
+ }
275
+
276
+ function routeHitsBlockedArea(points, rects, fromRect, toRect, pad) {
277
+ const endpointsOverlap = rectsOverlap(fromRect, toRect, 0);
278
+ for (let index = 0; index < points.length - 1; index += 1) {
279
+ const a = points[index];
280
+ const b = points[index + 1];
281
+ for (const rect of rects) {
282
+ if (endpointsOverlap && (rect.id === fromRect.id || rect.id === toRect.id)) continue;
283
+ const crossesEndpointInterior = segmentCrossesRectInterior(a, b, rect);
284
+ const nearSourceEndpoint = rect.id === fromRect.id && index <= 1;
285
+ const nearTargetEndpoint = rect.id === toRect.id && index >= points.length - 3;
286
+ const endpointEdgeSegment =
287
+ (nearSourceEndpoint || nearTargetEndpoint) &&
288
+ !crossesEndpointInterior &&
289
+ (pointOnRectBoundary(a, rect) || pointOnRectBoundary(b, rect));
290
+ if (endpointEdgeSegment) continue;
291
+ const sourceStub = rect.id === fromRect.id && index === 0 && !crossesEndpointInterior;
292
+ const targetStub = rect.id === toRect.id && index === points.length - 2 && !crossesEndpointInterior;
293
+ if (sourceStub || targetStub) continue;
294
+ if (segmentIntersectsRect(a, b, rect, pad)) return true;
295
+ }
296
+ }
297
+ return false;
298
+ }
299
+
300
+ function segmentHitsAnyRect(a, b, rects, pad) {
301
+ return rects.some((rect) => segmentIntersectsRect(a, b, rect, pad));
302
+ }
303
+
304
+ function segmentHitsBlockingRect(a, b, rects, allowedId, pad) {
305
+ return rects.some((rect) => rect.id !== allowedId && segmentIntersectsRect(a, b, rect, pad));
306
+ }
307
+
308
+ function directionDeviation(preferred, dir) {
309
+ return 1 - (preferred.dx * dir.dx + preferred.dy * dir.dy);
310
+ }
311
+
312
+ function chooseCleanStubDirection(point, targetPoint, rect, preferred, distance, rects, pad) {
313
+ let best = null;
314
+ for (const dir of orderedDirections(preferred, point, targetPoint)) {
315
+ const stubPoint = endpointStub(point, rect, dir, distance);
316
+ const selfBlocked = segmentCrossesRectInterior(point, stubPoint, rect);
317
+ const blocked = segmentHitsBlockingRect(point, stubPoint, rects, rect.id, pad);
318
+ const score = (selfBlocked ? 2000 : 0) + (blocked ? 1000 : 0) + directionDeviation(preferred, dir) * 10;
319
+ if (!best || score < best.score) {
320
+ best = { dir, score };
321
+ }
322
+ if (!selfBlocked && !blocked && directionDeviation(preferred, dir) === 0) break;
323
+ }
324
+ return best?.dir || preferred;
325
+ }
326
+
327
+ function routeLength(points) {
328
+ let length = 0;
329
+ for (let index = 0; index < points.length - 1; index += 1) {
330
+ length += Math.abs(points[index + 1].x - points[index].x) + Math.abs(points[index + 1].y - points[index].y);
331
+ }
332
+ return length;
333
+ }
334
+
335
+ function segmentDirections(points) {
336
+ const directions = [];
337
+ for (let index = 0; index < points.length - 1; index += 1) {
338
+ const a = points[index];
339
+ const b = points[index + 1];
340
+ const dx = b.x - a.x;
341
+ const dy = b.y - a.y;
342
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) continue;
343
+ if (Math.abs(dx) >= Math.abs(dy)) {
344
+ directions.push({ axis: 'x', sign: Math.sign(dx), length: Math.abs(dx) });
345
+ } else {
346
+ directions.push({ axis: 'y', sign: Math.sign(dy), length: Math.abs(dy) });
347
+ }
348
+ }
349
+ return directions;
350
+ }
351
+
352
+ function countBends(points) {
353
+ const directions = segmentDirections(points);
354
+ let bends = 0;
355
+ for (let index = 1; index < directions.length; index += 1) {
356
+ if (directions[index].axis !== directions[index - 1].axis) bends += 1;
357
+ }
358
+ return bends;
359
+ }
360
+
361
+ function countReversals(points, grid) {
362
+ const directions = segmentDirections(points);
363
+ let reversals = 0;
364
+ for (let index = 1; index < directions.length; index += 1) {
365
+ const prev = directions[index - 1];
366
+ const curr = directions[index];
367
+ if (prev.axis === curr.axis && prev.sign === -curr.sign) {
368
+ reversals += 1;
369
+ }
370
+ const before = directions[index - 2];
371
+ if (
372
+ before &&
373
+ before.axis === curr.axis &&
374
+ before.sign === -curr.sign &&
375
+ prev.length <= grid * 2
376
+ ) {
377
+ reversals += 1;
378
+ }
379
+ }
380
+ return reversals;
381
+ }
382
+
383
+ function countShortJogs(points, grid) {
384
+ const directions = segmentDirections(points);
385
+ let jogs = 0;
386
+ for (let index = 1; index < directions.length - 1; index += 1) {
387
+ if (directions[index].length < grid) jogs += 1;
388
+ }
389
+ return jogs;
390
+ }
391
+
392
+ function routeScore(points, grid) {
393
+ return (
394
+ countReversals(points, grid) * 1_000_000 +
395
+ routeLength(points) +
396
+ countBends(points) * grid * 5 +
397
+ countShortJogs(points, grid) * grid * 8
398
+ );
399
+ }
400
+
401
+ function hasHardGeometryIssue(points, grid) {
402
+ return countReversals(points, grid) > 0 || countSelfIntersections(points) > 0;
403
+ }
404
+
405
+ function chooseBestRoute(candidates, rects, fromRect, toRect, grid, pad) {
406
+ let best = null;
407
+ for (const candidate of candidates) {
408
+ const rawPoints = compactPoints(candidate);
409
+ if (rawPoints.length < 2) continue;
410
+ if (routeHitsBlockedArea(rawPoints, rects, fromRect, toRect, pad)) continue;
411
+ const points = simplifyCollinear(rawPoints);
412
+ if (points.length < 2) continue;
413
+ if (routeHitsBlockedArea(points, rects, fromRect, toRect, pad)) continue;
414
+ if (hasHardGeometryIssue(points, grid)) continue;
415
+ const score = routeScore(points, grid);
416
+ if (!best || score < best.score) {
417
+ best = { points, score };
418
+ }
419
+ }
420
+ return best?.points || null;
421
+ }
422
+
423
+ function chooseFallbackRoute(candidates, rects, fromRect, toRect, grid, pad) {
424
+ let best = null;
425
+ for (const candidate of candidates) {
426
+ const points = simplifyCollinear(compactPoints(candidate));
427
+ if (points.length < 2) continue;
428
+ const hardPenalty = hasHardGeometryIssue(points, grid) ? 1_000_000_000 : 0;
429
+ const blockedPenalty = routeHitsBlockedArea(points, rects, fromRect, toRect, pad) ? 10_000_000 : 0;
430
+ const score = hardPenalty + blockedPenalty + routeScore(points, grid);
431
+ if (!best || score < best.score) {
432
+ best = { points, score };
433
+ }
434
+ }
435
+ return best?.points || null;
436
+ }
437
+
438
+ function buildPath(points, chamfer = 0) {
439
+ const pts = simplifyCollinear(points);
440
+ let path = `M ${pts[0].x} ${pts[0].y}`;
441
+
442
+ for (let index = 1; index < pts.length; index += 1) {
443
+ const prev = pts[index - 1];
444
+ const curr = pts[index];
445
+ const next = pts[index + 1];
446
+
447
+ if (next) {
448
+ const dx1 = curr.x - prev.x;
449
+ const dy1 = curr.y - prev.y;
450
+ const dx2 = next.x - curr.x;
451
+ const dy2 = next.y - curr.y;
452
+ const isH1 = Math.abs(dx1) > Math.abs(dy1);
453
+ const isH2 = Math.abs(dx2) > Math.abs(dy2);
454
+
455
+ if (chamfer > 0 && isH1 !== isH2) {
456
+ const len1 = Math.hypot(dx1, dy1);
457
+ const len2 = Math.hypot(dx2, dy2);
458
+ const minChamferSegment = chamfer * 3;
459
+ if (len1 >= minChamferSegment && len2 >= minChamferSegment) {
460
+ const c = Math.min(chamfer, len1 / 2, len2 / 2);
461
+ const preX = curr.x - (dx1 / len1) * c;
462
+ const preY = curr.y - (dy1 / len1) * c;
463
+ const postX = curr.x + (dx2 / len2) * c;
464
+ const postY = curr.y + (dy2 / len2) * c;
465
+ path += ` L ${preX} ${preY} L ${postX} ${postY}`;
466
+ continue;
467
+ }
468
+ }
469
+ }
470
+
471
+ if (Math.abs(curr.y - prev.y) < 0.5) {
472
+ path += ` H ${curr.x}`;
473
+ } else if (Math.abs(curr.x - prev.x) < 0.5) {
474
+ path += ` V ${curr.y}`;
475
+ } else {
476
+ path += ` L ${curr.x} ${curr.y}`;
477
+ }
478
+ }
479
+
480
+ return { path, points: pts };
481
+ }
482
+
483
+ function midpointArrow(points) {
484
+ const index = Math.max(1, Math.floor(points.length / 2));
485
+ const p1 = points[index - 1];
486
+ const p2 = points[index] || points[index - 1];
487
+ return {
488
+ x: (p1.x + p2.x) / 2,
489
+ y: (p1.y + p2.y) / 2,
490
+ angle: Math.atan2(p2.y - p1.y, p2.x - p1.x),
491
+ };
492
+ }
493
+
494
+ function pointsHaveMinimumSegments(points, minLength) {
495
+ return segmentDirections(points).every((segment) => segment.length >= minLength);
496
+ }
497
+
498
+ function collectLaneCandidates({ stubFrom, stubTo, obstacleRects, clearance, grid, shift, laneSpread, snapToGrid }) {
499
+ const minX = Math.min(...obstacleRects.map((rect) => rect.x));
500
+ const maxX = Math.max(...obstacleRects.map((rect) => rect.x + rect.w));
501
+ const minY = Math.min(...obstacleRects.map((rect) => rect.y));
502
+ const maxY = Math.max(...obstacleRects.map((rect) => rect.y + rect.h));
503
+ const xValues = [
504
+ alignGrid((stubFrom.x + stubTo.x) / 2, grid, snapToGrid) + shift,
505
+ alignOutside(minX - clearance - laneSpread, grid, -1, snapToGrid) + shift,
506
+ alignOutside(maxX + clearance + laneSpread, grid, 1, snapToGrid) + shift,
507
+ ];
508
+ const yValues = [
509
+ alignGrid((stubFrom.y + stubTo.y) / 2, grid, snapToGrid) + shift,
510
+ alignOutside(minY - clearance - laneSpread, grid, -1, snapToGrid) + shift,
511
+ alignOutside(maxY + clearance + laneSpread, grid, 1, snapToGrid) + shift,
512
+ ];
513
+
514
+ for (const rect of obstacleRects) {
515
+ xValues.push(
516
+ alignOutside(rect.x - clearance - laneSpread, grid, -1, snapToGrid) + shift,
517
+ alignOutside(rect.x + rect.w + clearance + laneSpread, grid, 1, snapToGrid) + shift
518
+ );
519
+ yValues.push(
520
+ alignOutside(rect.y - clearance - laneSpread, grid, -1, snapToGrid) + shift,
521
+ alignOutside(rect.y + rect.h + clearance + laneSpread, grid, 1, snapToGrid) + shift
522
+ );
523
+ }
524
+
525
+ return {
526
+ xLanes: uniqueSorted(xValues),
527
+ yLanes: uniqueSorted(yValues),
528
+ };
529
+ }
530
+
531
+ function addLaneCandidates(addCandidate, start, stubFrom, stubTo, end, xLanes, yLanes) {
532
+ for (const midX of xLanes) {
533
+ addCandidate([
534
+ start,
535
+ stubFrom,
536
+ { x: midX, y: stubFrom.y },
537
+ { x: midX, y: stubTo.y },
538
+ stubTo,
539
+ end,
540
+ ]);
541
+ }
542
+
543
+ for (const midY of yLanes) {
544
+ addCandidate([
545
+ start,
546
+ stubFrom,
547
+ { x: stubFrom.x, y: midY },
548
+ { x: stubTo.x, y: midY },
549
+ stubTo,
550
+ end,
551
+ ]);
552
+ }
553
+
554
+ for (const midX of xLanes) {
555
+ for (const midY of yLanes) {
556
+ addCandidate([
557
+ start,
558
+ stubFrom,
559
+ { x: stubFrom.x, y: midY },
560
+ { x: midX, y: midY },
561
+ { x: midX, y: stubTo.y },
562
+ stubTo,
563
+ end,
564
+ ]);
565
+ addCandidate([
566
+ start,
567
+ stubFrom,
568
+ { x: midX, y: stubFrom.y },
569
+ { x: midX, y: midY },
570
+ { x: stubTo.x, y: midY },
571
+ stubTo,
572
+ end,
573
+ ]);
574
+ }
575
+ }
576
+ }
577
+
578
+ function findGridLaneRoute({ stubFrom, stubTo, obstacleRects, xLanes, yLanes, pad }) {
579
+ const xs = uniqueSorted([stubFrom.x, stubTo.x, ...xLanes]);
580
+ const ys = uniqueSorted([stubFrom.y, stubTo.y, ...yLanes]);
581
+ const keyOf = (x, y) => `${x},${y}`;
582
+ const parseKey = (key) => {
583
+ const [x, y] = key.split(',').map(Number);
584
+ return { x, y };
585
+ };
586
+ const startKey = keyOf(stubFrom.x, stubFrom.y);
587
+ const endKey = keyOf(stubTo.x, stubTo.y);
588
+ const keys = [];
589
+
590
+ for (const x of xs) {
591
+ for (const y of ys) {
592
+ keys.push(keyOf(x, y));
593
+ }
594
+ }
595
+
596
+ const dist = new Map(keys.map((key) => [key, Number.POSITIVE_INFINITY]));
597
+ const prev = new Map();
598
+ const visited = new Set();
599
+ dist.set(startKey, 0);
600
+
601
+ const neighbors = (key) => {
602
+ const point = parseKey(key);
603
+ const xIndex = xs.indexOf(point.x);
604
+ const yIndex = ys.indexOf(point.y);
605
+ const result = [];
606
+ for (const nextXIndex of [xIndex - 1, xIndex + 1]) {
607
+ if (nextXIndex < 0 || nextXIndex >= xs.length) continue;
608
+ const next = { x: xs[nextXIndex], y: point.y };
609
+ if (!segmentHitsAnyRect(point, next, obstacleRects, pad)) {
610
+ result.push({ key: keyOf(next.x, next.y), cost: Math.abs(next.x - point.x) });
611
+ }
612
+ }
613
+ for (const nextYIndex of [yIndex - 1, yIndex + 1]) {
614
+ if (nextYIndex < 0 || nextYIndex >= ys.length) continue;
615
+ const next = { x: point.x, y: ys[nextYIndex] };
616
+ if (!segmentHitsAnyRect(point, next, obstacleRects, pad)) {
617
+ result.push({ key: keyOf(next.x, next.y), cost: Math.abs(next.y - point.y) });
618
+ }
619
+ }
620
+ return result;
621
+ };
622
+
623
+ while (visited.size < keys.length) {
624
+ let current = null;
625
+ let currentDist = Number.POSITIVE_INFINITY;
626
+ for (const key of keys) {
627
+ if (visited.has(key)) continue;
628
+ const value = dist.get(key);
629
+ if (value < currentDist) {
630
+ current = key;
631
+ currentDist = value;
632
+ }
633
+ }
634
+
635
+ if (!current || currentDist === Number.POSITIVE_INFINITY) break;
636
+ if (current === endKey) break;
637
+ visited.add(current);
638
+
639
+ for (const next of neighbors(current)) {
640
+ if (visited.has(next.key)) continue;
641
+ const nextDist = currentDist + next.cost;
642
+ if (nextDist < dist.get(next.key)) {
643
+ dist.set(next.key, nextDist);
644
+ prev.set(next.key, current);
645
+ }
646
+ }
647
+ }
648
+
649
+ if (!prev.has(endKey) && startKey !== endKey) return null;
650
+
651
+ const route = [];
652
+ let current = endKey;
653
+ route.unshift(parseKey(current));
654
+ while (current !== startKey) {
655
+ current = prev.get(current);
656
+ if (!current) return null;
657
+ route.unshift(parseKey(current));
658
+ }
659
+ return simplifyCollinear(route);
660
+ }
661
+
662
+ function shortOrthogonalCandidates(start, end, fDir, tDir, shift, grid, snapToGrid) {
663
+ const startHorizontal = fDir.dx !== 0;
664
+ const endHorizontal = tDir.dx !== 0;
665
+
666
+ if (startHorizontal && endHorizontal) {
667
+ const midX = alignGrid((start.x + end.x) / 2, grid, snapToGrid) + shift;
668
+ return [[start, { x: midX, y: start.y }, { x: midX, y: end.y }, end]];
669
+ }
670
+
671
+ if (!startHorizontal && !endHorizontal) {
672
+ const midY = alignGrid((start.y + end.y) / 2, grid, snapToGrid) + shift;
673
+ return [[start, { x: start.x, y: midY }, { x: end.x, y: midY }, end]];
674
+ }
675
+
676
+ return startHorizontal
677
+ ? [[start, { x: end.x, y: start.y }, end]]
678
+ : [[start, { x: start.x, y: end.y }, end]];
679
+ }
680
+
681
+ function localOrthogonalCandidates(start, end, grid, snapToGrid) {
682
+ const midX = alignGrid((start.x + end.x) / 2, grid, snapToGrid);
683
+ const midY = alignGrid((start.y + end.y) / 2, grid, snapToGrid);
684
+ return [
685
+ [start, { x: end.x, y: start.y }, end],
686
+ [start, { x: start.x, y: end.y }, end],
687
+ [start, { x: midX, y: start.y }, { x: midX, y: end.y }, end],
688
+ [start, { x: start.x, y: midY }, { x: end.x, y: midY }, end],
689
+ ];
690
+ }
691
+
692
+ function compactTraceRoute({
693
+ start,
694
+ end,
695
+ fDir,
696
+ tDir,
697
+ shift,
698
+ routeFromRect,
699
+ routeToRect,
700
+ obstacleRects,
701
+ grid,
702
+ stub,
703
+ chamfer,
704
+ snapToGrid,
705
+ }) {
706
+ const dx = Math.abs(end.x - start.x);
707
+ const dy = Math.abs(end.y - start.y);
708
+ const directLength = Math.hypot(dx, dy);
709
+ const manhattanLength = dx + dy;
710
+ const compactLimit = Math.max(stub * 2, grid * 5);
711
+ const straightLimit = Math.max(grid * 2, chamfer * 2.5);
712
+ const minKneeSegment = Math.max(grid * 1.5, chamfer * 2);
713
+
714
+ if (directLength > compactLimit && manhattanLength > compactLimit + grid) return null;
715
+
716
+ const limitedShift = Math.max(-grid, Math.min(grid, shift));
717
+ const candidates = shortOrthogonalCandidates(start, end, fDir, tDir, limitedShift, grid, snapToGrid)
718
+ .map((candidate) => simplifyCollinear(candidate))
719
+ .filter((candidate) => candidate.length > 2 && pointsHaveMinimumSegments(candidate, minKneeSegment));
720
+
721
+ if (candidates.length) {
722
+ const points = chooseBestRoute(candidates, obstacleRects, routeFromRect, routeToRect, grid, 2);
723
+ if (points) {
724
+ const routed = buildPath(points, 0);
725
+
726
+ return {
727
+ path: routed.path,
728
+ points: routed.points,
729
+ arrow: midpointArrow(routed.points),
730
+ strategy: 'compact-elbow',
731
+ };
732
+ }
733
+ }
734
+
735
+ if (directLength > straightLimit && manhattanLength > straightLimit + grid) return null;
736
+
737
+ const directPoints = [start, end];
738
+ if (routeHitsBlockedArea(directPoints, obstacleRects, routeFromRect, routeToRect, 2)) return null;
739
+ const routed = buildPath(directPoints, 0);
740
+
741
+ return {
742
+ path: routed.path,
743
+ points: routed.points,
744
+ arrow: midpointArrow(routed.points),
745
+ strategy: 'compact-direct',
746
+ };
747
+ }
748
+
749
+ function findAlternateDirectionRoute({
750
+ start,
751
+ end,
752
+ fromRect,
753
+ toRect,
754
+ preferredFDir,
755
+ preferredTDir,
756
+ rects,
757
+ connections,
758
+ conn,
759
+ grid,
760
+ stub,
761
+ clearance,
762
+ chamfer,
763
+ snapToGrid,
764
+ routeFromRect,
765
+ routeToRect,
766
+ obstacleRects,
767
+ }) {
768
+ let best = null;
769
+ const fromDirs = orderedDirections(preferredFDir, start, end);
770
+ const toDirs = orderedDirections(preferredTDir, end, start);
771
+
772
+ for (const fromDirection of fromDirs) {
773
+ for (const toDirection of toDirs) {
774
+ if (
775
+ fromDirection.dx === preferredFDir.dx &&
776
+ fromDirection.dy === preferredFDir.dy &&
777
+ toDirection.dx === preferredTDir.dx &&
778
+ toDirection.dy === preferredTDir.dy
779
+ ) {
780
+ continue;
781
+ }
782
+
783
+ const routed = routePcbTrace({
784
+ start,
785
+ end,
786
+ fromRect,
787
+ toRect,
788
+ fromAngle: dirAngle(fromDirection),
789
+ toAngle: dirAngle(toDirection),
790
+ rects,
791
+ connections,
792
+ conn,
793
+ grid,
794
+ stub,
795
+ clearance,
796
+ chamfer,
797
+ snapToGrid,
798
+ fromDirection,
799
+ toDirection,
800
+ allowDirectionAlternates: false,
801
+ });
802
+
803
+ if (routeHitsBlockedArea(routed.points, obstacleRects, routeFromRect, routeToRect, 2)) continue;
804
+ if (hasHardGeometryIssue(routed.points, grid)) continue;
805
+ const score = routeScore(routed.points, grid);
806
+ if (!best || score < best.score) {
807
+ best = { ...routed, score };
808
+ }
809
+ }
810
+ }
811
+
812
+ if (!best) return null;
813
+ const { score, ...routed } = best;
814
+ return { ...routed, alternateDirections: true };
815
+ }
816
+
817
+ export function routePcbTrace({
818
+ start,
819
+ end,
820
+ fromRect,
821
+ toRect,
822
+ fromAngle = 0,
823
+ toAngle = 180,
824
+ rects = [],
825
+ connections = [],
826
+ conn,
827
+ grid = 10,
828
+ stub = 28,
829
+ clearance = 28,
830
+ chamfer = 8,
831
+ snapToGrid = true,
832
+ fromDirection = null,
833
+ toDirection = null,
834
+ allowDirectionAlternates = true,
835
+ }) {
836
+ const preferredFDir = snapDir(fromAngle);
837
+ const preferredTDir = snapDir(toAngle);
838
+ const shift = parallelShift(connections, conn, grid);
839
+ const absShift = Math.abs(shift);
840
+ const laneSpread = parallelSpread(connections, conn, grid);
841
+ const rectById = new Map(rects.map((rect) => [rect.id, rect]));
842
+ const routeFromRect = rectById.get(fromRect.id) || fromRect;
843
+ const routeToRect = rectById.get(toRect.id) || toRect;
844
+ if (rectsOverlap(routeFromRect, routeToRect, 0)) {
845
+ const routed = buildPath([start, end], 0);
846
+ return {
847
+ path: routed.path,
848
+ points: routed.points,
849
+ arrow: midpointArrow(routed.points),
850
+ strategy: 'overlap-direct',
851
+ };
852
+ }
853
+ const stubDistance = stub + absShift;
854
+ const obstacleRects = mergeEndpointRects(rects, routeFromRect, routeToRect);
855
+ const fDir = fromDirection || chooseCleanStubDirection(
856
+ start,
857
+ end,
858
+ routeFromRect,
859
+ preferredFDir,
860
+ stubDistance,
861
+ obstacleRects,
862
+ 2
863
+ );
864
+ const tDir = toDirection || chooseCleanStubDirection(
865
+ end,
866
+ start,
867
+ routeToRect,
868
+ preferredTDir,
869
+ stubDistance,
870
+ obstacleRects,
871
+ 2
872
+ );
873
+ const stubFrom = endpointStub(start, routeFromRect, fDir, stubDistance);
874
+ const stubTo = endpointStub(end, routeToRect, tDir, stubDistance);
875
+ const localRouteLimit = Math.max(stub * 4, clearance * 3, grid * 12);
876
+ const endpointManhattanLength = Math.abs(end.x - start.x) + Math.abs(end.y - start.y);
877
+ const compactRoute = compactTraceRoute({
878
+ start,
879
+ end,
880
+ fDir,
881
+ tDir,
882
+ shift,
883
+ routeFromRect,
884
+ routeToRect,
885
+ obstacleRects,
886
+ grid,
887
+ stub,
888
+ chamfer,
889
+ snapToGrid,
890
+ });
891
+ if (compactRoute) return compactRoute;
892
+
893
+ const separatedDown = routeFromRect.y + routeFromRect.h + clearance < routeToRect.y - clearance;
894
+ const separatedUp = routeToRect.y + routeToRect.h + clearance < routeFromRect.y - clearance;
895
+ const minX = Math.min(routeFromRect.x, routeToRect.x);
896
+ const maxX = Math.max(routeFromRect.x + routeFromRect.w, routeToRect.x + routeToRect.w);
897
+ const leftLaneX = alignOutside(minX - clearance - laneSpread, grid, -1, snapToGrid) + shift;
898
+ const rightLaneX = alignOutside(maxX + clearance + laneSpread, grid, 1, snapToGrid) + shift;
899
+ const candidates = [];
900
+ const addCandidate = (points) => candidates.push(points);
901
+
902
+ if (end.x < start.x - grid && (separatedDown || separatedUp)) {
903
+ const gapTop = separatedDown
904
+ ? routeFromRect.y + routeFromRect.h + clearance
905
+ : routeToRect.y + routeToRect.h + clearance;
906
+ const gapBottom = separatedDown
907
+ ? routeToRect.y - clearance
908
+ : routeFromRect.y - clearance;
909
+ const sourceOverTarget =
910
+ routeFromRect.x < routeToRect.x + routeToRect.w && routeFromRect.x + routeFromRect.w > routeToRect.x;
911
+ const compactSource = routeFromRect.w < routeToRect.w * 0.75;
912
+
913
+ if (sourceOverTarget && compactSource) {
914
+ const crossY = alignGrid((gapTop + gapBottom) / 2, grid, snapToGrid) + shift;
915
+ addCandidate([
916
+ start,
917
+ stubFrom,
918
+ { x: stubFrom.x, y: crossY },
919
+ { x: leftLaneX, y: crossY },
920
+ { x: leftLaneX, y: stubTo.y },
921
+ stubTo,
922
+ end,
923
+ ]);
924
+ }
925
+
926
+ for (let attempt = 0; attempt < 5; attempt += 1) {
927
+ const lanePad = clearance + attempt * grid * 2 + laneSpread;
928
+ const attemptLeftLaneX = alignOutside(minX - lanePad, grid, -1, snapToGrid) + shift;
929
+ const attemptRightLaneX = alignOutside(maxX + lanePad, grid, 1, snapToGrid) + shift;
930
+ const crossY = alignGrid((gapTop + gapBottom) / 2, grid, snapToGrid) + shift;
931
+ addCandidate([
932
+ start,
933
+ stubFrom,
934
+ { x: attemptRightLaneX, y: stubFrom.y },
935
+ { x: attemptRightLaneX, y: crossY },
936
+ { x: attemptLeftLaneX, y: crossY },
937
+ { x: attemptLeftLaneX, y: stubTo.y },
938
+ stubTo,
939
+ end,
940
+ ]);
941
+ }
942
+ }
943
+
944
+ const laneCandidates = collectLaneCandidates({
945
+ stubFrom,
946
+ stubTo,
947
+ obstacleRects,
948
+ clearance,
949
+ grid,
950
+ shift,
951
+ laneSpread,
952
+ snapToGrid,
953
+ });
954
+ addLaneCandidates(
955
+ addCandidate,
956
+ start,
957
+ stubFrom,
958
+ stubTo,
959
+ end,
960
+ uniqueSorted([leftLaneX, rightLaneX, ...laneCandidates.xLanes]),
961
+ laneCandidates.yLanes
962
+ );
963
+
964
+ let points = chooseBestRoute(candidates, obstacleRects, routeFromRect, routeToRect, grid, 6);
965
+
966
+ if (!points) {
967
+ const gridRoute = findGridLaneRoute({
968
+ stubFrom,
969
+ stubTo,
970
+ obstacleRects,
971
+ xLanes: uniqueSorted([leftLaneX, rightLaneX, ...laneCandidates.xLanes]),
972
+ yLanes: laneCandidates.yLanes,
973
+ pad: 2,
974
+ });
975
+ if (gridRoute) {
976
+ const candidate = simplifyCollinear([start, ...gridRoute, end]);
977
+ if (
978
+ !routeHitsBlockedArea(candidate, obstacleRects, routeFromRect, routeToRect, 2) &&
979
+ !hasHardGeometryIssue(candidate, grid)
980
+ ) {
981
+ points = candidate;
982
+ }
983
+ }
984
+ }
985
+
986
+ if (!points && endpointManhattanLength <= localRouteLimit) {
987
+ points = chooseBestRoute(
988
+ localOrthogonalCandidates(start, end, grid, snapToGrid),
989
+ obstacleRects,
990
+ routeFromRect,
991
+ routeToRect,
992
+ grid,
993
+ 2
994
+ );
995
+ }
996
+
997
+ if (!points && (separatedDown || separatedUp)) {
998
+ const gapTop = separatedDown
999
+ ? routeFromRect.y + routeFromRect.h + clearance
1000
+ : routeToRect.y + routeToRect.h + clearance;
1001
+ const gapBottom = separatedDown
1002
+ ? routeToRect.y - clearance
1003
+ : routeFromRect.y - clearance;
1004
+ const crossY = alignGrid((gapTop + gapBottom) / 2, grid, snapToGrid) + shift;
1005
+ const localGapRoute = simplifyCollinear([
1006
+ start,
1007
+ stubFrom,
1008
+ { x: stubFrom.x, y: crossY },
1009
+ { x: stubTo.x, y: crossY },
1010
+ stubTo,
1011
+ end,
1012
+ ]);
1013
+ if (
1014
+ !routeHitsBlockedArea(localGapRoute, obstacleRects, routeFromRect, routeToRect, 2) &&
1015
+ !hasHardGeometryIssue(localGapRoute, grid)
1016
+ ) {
1017
+ points = localGapRoute;
1018
+ }
1019
+ }
1020
+
1021
+ if (!points) {
1022
+ for (let attempt = 0; attempt < 8; attempt += 1) {
1023
+ const lanePad = clearance + laneSpread + attempt * grid * 4;
1024
+ const minObstacleX = Math.min(...obstacleRects.map((rect) => rect.x));
1025
+ const maxObstacleX = Math.max(...obstacleRects.map((rect) => rect.x + rect.w));
1026
+ const minObstacleY = Math.min(...obstacleRects.map((rect) => rect.y));
1027
+ const maxObstacleY = Math.max(...obstacleRects.map((rect) => rect.y + rect.h));
1028
+ const rescueCandidates = [];
1029
+ addLaneCandidates(
1030
+ (candidate) => rescueCandidates.push(candidate),
1031
+ start,
1032
+ stubFrom,
1033
+ stubTo,
1034
+ end,
1035
+ [
1036
+ alignOutside(minObstacleX - lanePad, grid, -1, snapToGrid) + shift,
1037
+ alignOutside(maxObstacleX + lanePad, grid, 1, snapToGrid) + shift,
1038
+ ],
1039
+ [
1040
+ alignOutside(minObstacleY - lanePad, grid, -1, snapToGrid) + shift,
1041
+ alignOutside(maxObstacleY + lanePad, grid, 1, snapToGrid) + shift,
1042
+ ]
1043
+ );
1044
+ points = chooseBestRoute(rescueCandidates, obstacleRects, routeFromRect, routeToRect, grid, 2);
1045
+ if (points) break;
1046
+ }
1047
+ }
1048
+
1049
+ if (!points) {
1050
+ const maxBottom = Math.max(...obstacleRects.map((rect) => rect.y + rect.h));
1051
+ const bottomY = alignGrid(maxBottom + clearance + laneSpread, grid, snapToGrid) + shift;
1052
+ const bottomRoute = simplifyCollinear([
1053
+ start,
1054
+ stubFrom,
1055
+ { x: stubFrom.x, y: bottomY },
1056
+ { x: stubTo.x, y: bottomY },
1057
+ stubTo,
1058
+ end,
1059
+ ]);
1060
+ if (
1061
+ !routeHitsBlockedArea(bottomRoute, obstacleRects, routeFromRect, routeToRect, 2) &&
1062
+ !hasHardGeometryIssue(bottomRoute, grid)
1063
+ ) {
1064
+ points = bottomRoute;
1065
+ } else {
1066
+ const minTop = Math.min(...obstacleRects.map((rect) => rect.y));
1067
+ const topY = alignGrid(minTop - clearance - laneSpread, grid, snapToGrid) + shift;
1068
+ const topRoute = simplifyCollinear([
1069
+ start,
1070
+ stubFrom,
1071
+ { x: stubFrom.x, y: topY },
1072
+ { x: stubTo.x, y: topY },
1073
+ stubTo,
1074
+ end,
1075
+ ]);
1076
+ if (
1077
+ !routeHitsBlockedArea(topRoute, obstacleRects, routeFromRect, routeToRect, 2) &&
1078
+ !hasHardGeometryIssue(topRoute, grid)
1079
+ ) {
1080
+ points = topRoute;
1081
+ } else if (allowDirectionAlternates) {
1082
+ const alternate = findAlternateDirectionRoute({
1083
+ start,
1084
+ end,
1085
+ fromRect,
1086
+ toRect,
1087
+ preferredFDir,
1088
+ preferredTDir,
1089
+ rects,
1090
+ connections,
1091
+ conn,
1092
+ grid,
1093
+ stub,
1094
+ clearance,
1095
+ chamfer,
1096
+ snapToGrid,
1097
+ routeFromRect,
1098
+ routeToRect,
1099
+ obstacleRects,
1100
+ });
1101
+ if (alternate) return alternate;
1102
+ points = chooseFallbackRoute(
1103
+ [
1104
+ topRoute,
1105
+ bottomRoute,
1106
+ ...localOrthogonalCandidates(start, end, grid, snapToGrid),
1107
+ ],
1108
+ obstacleRects,
1109
+ routeFromRect,
1110
+ routeToRect,
1111
+ grid,
1112
+ 2
1113
+ ) || bottomRoute;
1114
+ } else {
1115
+ points = chooseFallbackRoute([topRoute, bottomRoute], obstacleRects, routeFromRect, routeToRect, grid, 2) || bottomRoute;
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ const routed = buildPath(points, chamfer);
1121
+ return {
1122
+ path: routed.path,
1123
+ points: routed.points,
1124
+ arrow: midpointArrow(routed.points),
1125
+ strategy: 'pcb-lane',
1126
+ };
1127
+ }