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,971 @@
1
+ import { getShape } from '../shapes/index.js';
2
+ import { routePcbTrace } from './PcbRouter.js';
3
+
4
+ function resolveThemeSource(canvasLayer) {
5
+ return canvasLayer?.parentElement || canvasLayer?.getRootNode?.()?.host || canvasLayer || document.documentElement;
6
+ }
7
+
8
+ function resolveThemeValue(source, token, fallbackToken) {
9
+ let computed = getComputedStyle(source);
10
+ let value = computed.getPropertyValue(token).trim();
11
+ if (value) return value;
12
+ if (!fallbackToken) return '';
13
+ return computed.getPropertyValue(fallbackToken).trim();
14
+ }
15
+
16
+ function resolveCssLength(source, value, fallback) {
17
+ if (!value) return fallback;
18
+ if (!value.includes('var(') && !value.includes('calc(')) {
19
+ let direct = Number.parseFloat(value);
20
+ if (Number.isFinite(direct)) return direct;
21
+ }
22
+
23
+ let probe = document.createElement('span');
24
+ probe.style.position = 'absolute';
25
+ probe.style.visibility = 'hidden';
26
+ probe.style.pointerEvents = 'none';
27
+ probe.style.width = value;
28
+ (source.append ? source : document.documentElement).append(probe);
29
+ let resolved = Number.parseFloat(getComputedStyle(probe).width);
30
+ probe.remove();
31
+ return Number.isFinite(resolved) ? resolved : fallback;
32
+ }
33
+
34
+ function resolveThemeLength(source, token, fallback) {
35
+ let value = getComputedStyle(source).getPropertyValue(token).trim();
36
+ return resolveCssLength(source, value, fallback);
37
+ }
38
+
39
+ function resolveCssVars(source, value, seen = new Set()) {
40
+ if (!value || !value.includes('var(')) return value || '';
41
+ return value.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^)]+))?\)/g, (_match, token, fallback = '') => {
42
+ if (seen.has(token)) return fallback.trim();
43
+ seen.add(token);
44
+ let nextValue = getComputedStyle(source).getPropertyValue(token).trim() || fallback.trim();
45
+ return resolveCssVars(source, nextValue, seen);
46
+ });
47
+ }
48
+
49
+ function parseCssRgb(value, source = document.documentElement) {
50
+ if (!value) return null;
51
+ let probe = document.createElement('span');
52
+ probe.style.color = resolveCssVars(source, value);
53
+ document.documentElement.append(probe);
54
+ let normalized = getComputedStyle(probe).color;
55
+ probe.remove();
56
+ let match = normalized.match(/rgba?\(([^)]+)\)/);
57
+ if (!match) return null;
58
+ let [r, g, b] = match[1]
59
+ .split(',')
60
+ .slice(0, 3)
61
+ .map((part) => Number.parseFloat(part));
62
+ return [r, g, b].every(Number.isFinite) ? [r, g, b] : null;
63
+ }
64
+
65
+ function mixRgb(a, b, weight) {
66
+ return a.map((channel, index) => Math.round(channel * weight + b[index] * (1 - weight)));
67
+ }
68
+
69
+ function toRgba(rgb, alpha = 1) {
70
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
71
+ }
72
+
73
+ /**
74
+ * Parallel support for connection rendering via HTML5 Canvas API.
75
+ * This is used to test performance against the DOM-bound SVG renderer.
76
+ */
77
+ export class CanvasConnectionRenderer {
78
+ #canvasLayer;
79
+ #dotLayer;
80
+ #nodeViews;
81
+ #editor;
82
+ #onConnectionClick;
83
+ #getZoom;
84
+ #getPan;
85
+ #onDotDrag;
86
+
87
+ #pathStyle = 'bezier';
88
+ #connectionData = new Map();
89
+ #ctx;
90
+ #resizeObserver;
91
+ #animationFrameId;
92
+ #batchMode = false;
93
+ #batchDirty = false;
94
+ #pathCache = new Map();
95
+ #phantomSignature = '';
96
+
97
+ /** @type {Array<{id:string, x:number, y:number, w:number, h:number, degree:number, color:string, label:string}>} */
98
+ #phantomNodes = [];
99
+ /** @type {Map<string, Object>} Fast lookup for phantom proxy by nodeId */
100
+ #phantomMap = new Map();
101
+
102
+
103
+ #colorParams = {
104
+ normal: '',
105
+ selected: '',
106
+ outline: '',
107
+ bg: '',
108
+ text: '',
109
+ width: 2,
110
+ dotRadius: 7,
111
+ dotStrokeWidth: 2,
112
+ };
113
+
114
+ /**
115
+ * @param {Object} config
116
+ * @param {HTMLCanvasElement} config.canvasLayer
117
+ * @param {HTMLElement} config.dotLayer
118
+ * @param {Map<string, HTMLElement>} config.nodeViews
119
+ * @param {import('../core/GraphEditor.js').GraphEditor} config.editor
120
+ * @param {function(string, MouseEvent)} config.onConnectionClick
121
+ * @param {function(): number} config.getZoom
122
+ * @param {function(): {x: number, y: number}} config.getPan
123
+ * @param {function(Object)} config.onDotDrag
124
+ */
125
+ constructor(config = {}) {
126
+ this.#canvasLayer = config.canvasLayer || document.createElement('canvas');
127
+ this.#dotLayer = config.dotLayer;
128
+ this.#nodeViews = config.nodeViews;
129
+ this.#editor = config.editor;
130
+ this.#onConnectionClick = config.onConnectionClick;
131
+ this.#getZoom = config.getZoom || (() => 1);
132
+ this.#getPan = config.getPan || (() => ({ x: 0, y: 0 }));
133
+ this.#onDotDrag = config.onDotDrag;
134
+
135
+ this.#ctx = this.#canvasLayer.getContext('2d', { alpha: true, desynchronized: false });
136
+ this.#initResizeObserver();
137
+ this.#updateStyles();
138
+
139
+
140
+ this.#animationFrameId = requestAnimationFrame(this.#renderLoop);
141
+ }
142
+
143
+ /**
144
+ * Resize observer to keep the canvas 1:1 with device pixels
145
+ */
146
+ #initResizeObserver() {
147
+ let parent = this.#canvasLayer.parentElement;
148
+ if (!parent) return;
149
+
150
+ this.#resizeObserver = new ResizeObserver((entries) => {
151
+ let rect = entries[0].contentRect;
152
+ let dpr = window.devicePixelRatio || 1;
153
+
154
+ this.#canvasLayer.width = rect.width * dpr;
155
+ this.#canvasLayer.height = rect.height * dpr;
156
+
157
+ this.redraw();
158
+ });
159
+
160
+ this.#resizeObserver.observe(parent);
161
+ }
162
+
163
+ #updateStyles() {
164
+ let source = resolveThemeSource(this.#canvasLayer);
165
+ let computed = getComputedStyle(source);
166
+ this.#colorParams.normal = resolveThemeValue(source, '--sn-conn-color', '--sn-node-selected');
167
+ this.#colorParams.selected = resolveThemeValue(source, '--sn-conn-selected', '--sn-danger-color');
168
+ this.#colorParams.outline = resolveThemeValue(source, '--sn-port-outline', '--sn-node-bg');
169
+ this.#colorParams.bg = resolveThemeValue(source, '--sn-bg');
170
+ this.#colorParams.text = resolveThemeValue(source, '--sn-text');
171
+ this.#colorParams.width = parseFloat(computed.getPropertyValue('--sn-conn-width')) || 2;
172
+ let socketSize = resolveThemeLength(source, '--sn-socket-size', 12);
173
+ let socketBorderWidth = resolveThemeLength(source, '--sn-socket-border-width', 2);
174
+ this.#colorParams.dotStrokeWidth = resolveThemeLength(
175
+ source,
176
+ '--sn-conn-dot-stroke-width',
177
+ socketBorderWidth
178
+ );
179
+ this.#colorParams.dotRadius = resolveThemeLength(
180
+ source,
181
+ '--sn-conn-dot-r',
182
+ (socketSize + this.#colorParams.dotStrokeWidth) / 2
183
+ );
184
+ }
185
+
186
+ #resolveColor(value) {
187
+ let source = resolveThemeSource(this.#canvasLayer);
188
+ return resolveCssVars(source, value);
189
+ }
190
+
191
+ /** @param {'bezier'|'orthogonal'|'straight'|'pcb'} style */
192
+ setPathStyle(style) {
193
+ this.#pathStyle = style;
194
+ this.#invalidatePathCache();
195
+ this.redraw();
196
+ }
197
+
198
+ get data() {
199
+ return this.#connectionData;
200
+ }
201
+
202
+ addBatch(conns) {
203
+ for (const conn of conns) {
204
+ this.#connectionData.set(conn.id, conn);
205
+ }
206
+ this.#invalidatePathCache();
207
+ this.redraw();
208
+ }
209
+
210
+ refreshAll() {
211
+ this.#invalidatePathCache();
212
+ this.redraw();
213
+ }
214
+
215
+ refreshViewportTransform() {
216
+ this.redraw();
217
+ }
218
+
219
+ add(conn) {
220
+ this.#connectionData.set(conn.id, conn);
221
+ this.#invalidatePathCache();
222
+ this.redraw();
223
+ }
224
+
225
+ remove(conn) {
226
+ this.#connectionData.delete(conn.id);
227
+ this.#invalidatePathCache();
228
+ this.redraw();
229
+ }
230
+
231
+ updateForNode(_nodeId) {
232
+ this.#invalidatePathCache();
233
+ this.redraw();
234
+ }
235
+
236
+ setFlowing(connId, active) {
237
+ let conn = this.#connectionData.get(connId);
238
+ if (conn) conn.flowing = active;
239
+ }
240
+
241
+ setAllFlowing(active) {
242
+ for (const conn of this.#connectionData.values()) {
243
+ conn.flowing = active;
244
+ }
245
+ }
246
+
247
+ highlightDotsForNodes(_compatibleNodeIds) {}
248
+ clearDotHighlights() {}
249
+ renderFreeDots(_nodeId) {}
250
+ removeFreeDot(_nodeId, _key, _side) {}
251
+ refreshFreeDots(_nodeId) {}
252
+ findNearestDot(_wx, _wy, _radius = 20) {
253
+ return null;
254
+ }
255
+
256
+ clear() {
257
+ this.#connectionData.clear();
258
+ this.#phantomNodes = [];
259
+ this.#phantomMap.clear();
260
+ this.#phantomSignature = '';
261
+ this.#invalidatePathCache();
262
+ this.redraw();
263
+ }
264
+
265
+
266
+ /**
267
+ * Set phantom nodes — nodes without DOM that are rendered as Canvas dots.
268
+ * @param {Array<{id:string, x:number, y:number, w:number, h:number, degree:number, color:string, label:string}>} nodes
269
+ */
270
+ setPhantomNodes(nodes) {
271
+ let nextNodes = nodes || [];
272
+ let nextSignature = this.#getPhantomSignature(nextNodes);
273
+ if (nextSignature === this.#phantomSignature) return;
274
+
275
+ this.#phantomNodes = nextNodes;
276
+ this.#phantomSignature = nextSignature;
277
+ this.#phantomMap.clear();
278
+ for (const n of this.#phantomNodes) {
279
+ this.#phantomMap.set(n.id, n);
280
+ }
281
+ this.#invalidatePathCache();
282
+ this.redraw();
283
+ }
284
+
285
+ #getPhantomSignature(nodes) {
286
+ return nodes
287
+ .map((node) => [
288
+ node?.id || '',
289
+ node?.x ?? 0,
290
+ node?.y ?? 0,
291
+ node?.w ?? 0,
292
+ node?.h ?? 0,
293
+ node?.degree ?? 0,
294
+ node?.color || '',
295
+ node?.label || '',
296
+ ].join(':'))
297
+ .join('|');
298
+ }
299
+
300
+ #invalidatePathCache() {
301
+ this.#pathCache.clear();
302
+ }
303
+
304
+ /**
305
+ * Retrieve actual connector coordinate relative to the origin.
306
+ * @returns {{x: number, y: number}}
307
+ */
308
+ getSocketOffset(nodeEl, portKey, side, targetPos) {
309
+ if (!nodeEl) return { x: 0, y: 0 };
310
+ let w = nodeEl._cachedW || nodeEl.offsetWidth || 180;
311
+ let h = nodeEl._cachedH || nodeEl.offsetHeight || 100;
312
+
313
+ if (nodeEl._slotCache && nodeEl._slotCache.has(portKey)) {
314
+ let cached = nodeEl._slotCache.get(portKey);
315
+ return {
316
+ x: cached.x,
317
+ y: cached.y,
318
+ angle: cached.angle,
319
+ };
320
+ }
321
+
322
+ let nodeModel = this.#editor?.getNode(nodeEl.getAttribute?.('node-id') || nodeEl.id);
323
+ let portIndex = 0;
324
+ let totalPorts = 1;
325
+
326
+ if (nodeModel && nodeModel.type !== 'param') {
327
+ let portsData = side === 'output' ? nodeModel.outputs : nodeModel.inputs;
328
+ if (portsData) {
329
+ let keys = Object.keys(portsData);
330
+ totalPorts = keys.length || 1;
331
+ let idx = keys.indexOf(portKey);
332
+ if (idx !== -1) portIndex = idx;
333
+ }
334
+ }
335
+
336
+
337
+ let shapeConfig = getShape(nodeModel?.shape);
338
+ if (shapeConfig && shapeConfig.pathData && targetPos && shapeConfig.getEdgePoint) {
339
+ let portsData = side === 'output' ? nodeModel?.outputs : nodeModel?.inputs;
340
+ let keys = portsData ? Object.keys(portsData) : [portKey];
341
+ let index = Math.max(0, keys.indexOf(portKey));
342
+ let total = Math.max(1, keys.length);
343
+ let nodePos = nodeEl._position || { x: 0, y: 0 };
344
+ let cx = nodePos.x + w / 2;
345
+ let cy = nodePos.y + h / 2;
346
+ let baseAngle = Math.atan2(targetPos.y - cy, targetPos.x - cx);
347
+ let sideGap = Math.PI / 6;
348
+ let angle = baseAngle + (side === 'output' ? -sideGap : sideGap);
349
+ let shouldReverse = side === 'output' ? targetPos.y < cy : targetPos.y > cy;
350
+ let effectiveIndex = shouldReverse ? total - 1 - index : index;
351
+ if (total > 1) {
352
+ angle += (effectiveIndex - (total - 1) / 2) * ((2 * Math.PI) / (total * 2));
353
+ }
354
+ let step = Math.PI / 12;
355
+ angle = Math.round(angle / step) * step;
356
+ return shapeConfig.getEdgePoint(angle, { width: w, height: h });
357
+ }
358
+
359
+ if (shapeConfig && shapeConfig.pathData && shapeConfig.getSocketPosition) {
360
+ let pos = shapeConfig.getSocketPosition(
361
+ side,
362
+ portIndex,
363
+ totalPorts,
364
+ { width: w, height: h },
365
+ targetPos
366
+ );
367
+ if (pos) return pos;
368
+ }
369
+
370
+
371
+ let container =
372
+ side === 'output' ? nodeEl.querySelector('.outputs') : nodeEl.querySelector('.inputs');
373
+
374
+ if (container) {
375
+ let portItems = container.querySelectorAll('port-item');
376
+ for (const portItem of portItems) {
377
+ if (String(portItem.$.key) === String(portKey)) {
378
+ let socket = portItem.querySelector('.sn-socket');
379
+ if (socket) {
380
+ let nodeRect = nodeEl.getBoundingClientRect();
381
+ let socketRect = socket.getBoundingClientRect();
382
+ let z = this.#getZoom();
383
+ return {
384
+ x: (socketRect.left - nodeRect.left + socketRect.width / 2) / z,
385
+ y: (socketRect.top - nodeRect.top + socketRect.height / 2) / z,
386
+ };
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ return {
393
+ x: side === 'output' ? nodeEl._cachedW || nodeEl.offsetWidth || 180 : 0,
394
+ y: (nodeEl._cachedH || nodeEl.offsetHeight || 100) / 2,
395
+ };
396
+ }
397
+
398
+ #hasSelection = false;
399
+ #activeConnIds = new Set();
400
+
401
+ setSelectionState(hasSelection, activeConnIds) {
402
+ this.#hasSelection = hasSelection;
403
+ this.#activeConnIds = activeConnIds;
404
+ this.redraw();
405
+ }
406
+
407
+ /** Suppress redraws during batch operations (e.g. setEditor initialization) */
408
+ setBatchMode(on) {
409
+ this.#batchMode = on;
410
+ if (!on && this.#batchDirty) {
411
+ this.#batchDirty = false;
412
+ this.redraw();
413
+ }
414
+ }
415
+
416
+ /** Perform full synchronous redraw of all connections */
417
+ redraw() {
418
+ if (this.#batchMode) {
419
+ this.#batchDirty = true;
420
+ return;
421
+ }
422
+ let ctx = this.#ctx;
423
+ if (!ctx) return;
424
+
425
+
426
+ let dpr = window.devicePixelRatio || 1;
427
+ let zoom = this.#getZoom();
428
+ this._frameZoom = zoom;
429
+ let pan = this.#getPan();
430
+
431
+
432
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
433
+ ctx.clearRect(0, 0, this.#canvasLayer.width, this.#canvasLayer.height);
434
+
435
+
436
+ ctx.setTransform(dpr * zoom, 0, 0, dpr * zoom, dpr * pan.x, dpr * pan.y);
437
+
438
+
439
+ this.#updateStyles();
440
+
441
+ let time = Date.now();
442
+ let hasFlowing = false;
443
+
444
+
445
+ this._nodeRectMap = new Map();
446
+ for (const [nid, el] of this.#nodeViews) {
447
+ if (el && el._position) {
448
+ this._nodeRectMap.set(nid, {
449
+ id: nid,
450
+ x: el._position.x,
451
+ y: el._position.y,
452
+ w: el._cachedW || 180,
453
+ h: el._cachedH || 60,
454
+ el: el,
455
+ });
456
+ }
457
+ }
458
+
459
+ for (const node of this.#phantomNodes) {
460
+ if (node && !this._nodeRectMap.has(node.id)) {
461
+ this._nodeRectMap.set(node.id, {
462
+ id: node.id,
463
+ x: node.x || 0,
464
+ y: node.y || 0,
465
+ w: node.w || 180,
466
+ h: node.h || 60,
467
+ el: null,
468
+ });
469
+ }
470
+ }
471
+
472
+
473
+ let connIndexMap = new Map();
474
+ let ci = 0;
475
+ for (const key of this.#connectionData.keys()) {
476
+ connIndexMap.set(key, ci++);
477
+ }
478
+ this._connIndexMap = connIndexMap;
479
+
480
+
481
+ let socketsToDraw = new Map();
482
+
483
+ let drawConnection = (id, connection) => {
484
+
485
+ let isFlowing = connection.flowing;
486
+ let isActive = this.#activeConnIds ? this.#activeConnIds.has(connection.id) : false;
487
+ let isSelected = isActive;
488
+ let isDimmed = !isActive && this.#hasSelection;
489
+
490
+ let fromNode = this.#editor?.getNode(connection.from);
491
+ let toNode = this.#editor?.getNode(connection.to);
492
+ let fromColor = this.#resolveColor(fromNode?.outputs?.[connection.out]?.socket?.color);
493
+ let toColor = this.#resolveColor(toNode?.inputs?.[connection.in]?.socket?.color);
494
+
495
+
496
+ let baseWidth = this.#colorParams.width;
497
+ ctx.lineWidth = Math.max(baseWidth, 1.5 / zoom);
498
+ ctx.lineCap = 'round';
499
+ ctx.lineJoin = 'round';
500
+ ctx.globalAlpha = 1.0;
501
+
502
+ ctx.beginPath();
503
+ let coords = null;
504
+ try {
505
+ coords = this.#plotPath(ctx, connection);
506
+ } catch (err) {
507
+ if (CanvasConnectionRenderer.debug) {
508
+ console.warn('[CanvasConnectionRenderer] Path failed:', err);
509
+ }
510
+ }
511
+ if (!coords) return;
512
+
513
+
514
+ socketsToDraw.set(`${connection.from}:${connection.out}`, {
515
+ x: coords.startX,
516
+ y: coords.startY,
517
+ color: fromColor || this.#colorParams.normal,
518
+ });
519
+ socketsToDraw.set(`${connection.to}:${connection.in}`, {
520
+ x: coords.endX,
521
+ y: coords.endY,
522
+ color: toColor || this.#colorParams.normal,
523
+ });
524
+
525
+ let finalColor;
526
+ if (fromColor && toColor && fromColor !== toColor) {
527
+ let grad = ctx.createLinearGradient(coords.startX, coords.startY, coords.endX, coords.endY);
528
+ grad.addColorStop(0, fromColor);
529
+ grad.addColorStop(1, toColor);
530
+ finalColor = grad;
531
+ } else {
532
+ finalColor = fromColor || this.#colorParams.normal;
533
+ }
534
+
535
+ if (isDimmed) {
536
+
537
+ let baseColor = fromColor || this.#colorParams.normal;
538
+ let source = resolveThemeSource(this.#canvasLayer);
539
+ let baseRgb = parseCssRgb(baseColor, source);
540
+ let bgRgb = parseCssRgb(this.#colorParams.bg, source);
541
+ if (baseRgb && bgRgb) {
542
+ finalColor = toRgba(mixRgb(baseRgb, bgRgb, 0.15));
543
+ }
544
+ }
545
+
546
+ ctx.strokeStyle = finalColor;
547
+ ctx.fillStyle = finalColor;
548
+
549
+ if (isFlowing) {
550
+ ctx.setLineDash([10, 10]);
551
+ ctx.lineDashOffset = -(time / 20) % 20;
552
+ hasFlowing = true;
553
+ } else {
554
+ ctx.setLineDash([]);
555
+ }
556
+
557
+
558
+ if (isSelected && !isDimmed) {
559
+ ctx.shadowColor = ctx.strokeStyle;
560
+ ctx.shadowBlur = 8;
561
+ } else {
562
+ ctx.shadowBlur = 0;
563
+ }
564
+
565
+ ctx.stroke(coords.path2D);
566
+
567
+
568
+ if (coords.arrow) {
569
+ ctx.save();
570
+ ctx.translate(coords.arrow.x, coords.arrow.y);
571
+
572
+ ctx.rotate(coords.arrow.angle);
573
+ ctx.beginPath();
574
+ ctx.moveTo(-5, -3.5);
575
+ ctx.lineTo(5, 0);
576
+ ctx.lineTo(-5, 3.5);
577
+ ctx.closePath();
578
+ ctx.fillStyle = ctx.strokeStyle;
579
+ ctx.fill();
580
+ ctx.restore();
581
+ }
582
+ };
583
+
584
+ if (this.#hasSelection) {
585
+ for (const [id, connection] of this.#connectionData) {
586
+ if (!this.#activeConnIds.has(connection.id)) drawConnection(id, connection);
587
+ }
588
+ for (const [id, connection] of this.#connectionData) {
589
+ if (this.#activeConnIds.has(connection.id)) drawConnection(id, connection);
590
+ }
591
+ } else {
592
+ for (const [id, connection] of this.#connectionData) {
593
+ drawConnection(id, connection);
594
+ }
595
+ }
596
+
597
+
598
+ ctx.setLineDash([]);
599
+
600
+ for (const [, pos] of socketsToDraw) {
601
+ ctx.beginPath();
602
+
603
+
604
+ ctx.arc(pos.x, pos.y, this.#colorParams.dotRadius, 0, Math.PI * 2);
605
+ ctx.fillStyle = pos.color;
606
+ ctx.fill();
607
+ ctx.lineWidth = this.#colorParams.dotStrokeWidth;
608
+ ctx.strokeStyle = this.#colorParams.outline;
609
+ ctx.stroke();
610
+ }
611
+
612
+
613
+ this.#drawPhantomDots(ctx, zoom);
614
+
615
+
616
+ if (!hasFlowing && this.#animationFrameId) {
617
+ cancelAnimationFrame(this.#animationFrameId);
618
+ this.#animationFrameId = null;
619
+ } else if (hasFlowing && !this.#animationFrameId) {
620
+ this.#animationFrameId = requestAnimationFrame(this.#renderLoop);
621
+ }
622
+ }
623
+
624
+ /**
625
+ * Draw phantom nodes as colored dots with size proportional to degree.
626
+ * @param {CanvasRenderingContext2D} ctx
627
+ * @param {number} zoom
628
+ */
629
+ #drawPhantomDots(ctx, zoom) {
630
+ if (this.#phantomNodes.length === 0) return;
631
+
632
+ ctx.shadowBlur = 0;
633
+ ctx.setLineDash([]);
634
+
635
+
636
+ let minWorldW = Math.max(180, 8 / zoom);
637
+ let minWorldH = Math.max(60, 4 / zoom);
638
+ let showLabels = zoom > 0.15;
639
+ let labelFontSize = Math.max(9, Math.min(14, 12 / zoom));
640
+
641
+ for (const node of this.#phantomNodes) {
642
+ if (!node || node.w === undefined || node.h === undefined) continue;
643
+
644
+ let w = Math.max(minWorldW, node.w);
645
+ let h = Math.max(minWorldH, node.h);
646
+
647
+ let x = (node.x || 0) - (w - node.w) / 2;
648
+ let y = (node.y || 0) - (h - node.h) / 2;
649
+
650
+ ctx.beginPath();
651
+ try {
652
+ let r = Math.min(6, w * 0.1, h * 0.1);
653
+ if (ctx.roundRect) ctx.roundRect(x, y, w, h, r);
654
+ else ctx.rect(x, y, w, h);
655
+ } catch {
656
+ ctx.rect(x, y, w, h);
657
+ }
658
+
659
+ ctx.fillStyle = node.color || this.#colorParams.normal;
660
+ ctx.globalAlpha = 0.85;
661
+ ctx.fill();
662
+
663
+
664
+ ctx.lineWidth = Math.max(1.5, 1 / zoom);
665
+ ctx.strokeStyle = this.#colorParams.outline;
666
+ ctx.stroke();
667
+ ctx.globalAlpha = 1.0;
668
+
669
+
670
+ if (showLabels && node.label) {
671
+ ctx.fillStyle = this.#colorParams.text;
672
+ ctx.globalAlpha = 1;
673
+ ctx.font = `${labelFontSize}px sans-serif`;
674
+ ctx.textAlign = 'center';
675
+ ctx.textBaseline = 'middle';
676
+
677
+ ctx.save();
678
+ ctx.beginPath();
679
+ ctx.rect(x + 4, y, w - 8, h);
680
+ ctx.clip();
681
+ ctx.fillText(node.label, x + w / 2, y + h / 2);
682
+ ctx.restore();
683
+ }
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Create a minimal proxy object for a phantom node so #plotPath can work.
689
+ * Mimics the shape of a DOM nodeView element with _position and _cachedW/H.
690
+ * @returns {object|null}
691
+ */
692
+ #getPhantomProxy(nodeId) {
693
+ let phantom = this.#phantomMap.get(nodeId);
694
+ if (!phantom) return null;
695
+ return {
696
+ id: phantom.id,
697
+ _position: { x: phantom.x, y: phantom.y },
698
+ _cachedW: phantom.w,
699
+ _cachedH: phantom.h,
700
+ offsetWidth: phantom.w,
701
+ offsetHeight: phantom.h,
702
+ getAttribute: () => null,
703
+ querySelector: () => null,
704
+ querySelectorAll: () => [],
705
+ style: {},
706
+ };
707
+ }
708
+
709
+ #renderLoop = () => {
710
+ this.redraw();
711
+ this.#animationFrameId = requestAnimationFrame(this.#renderLoop);
712
+ };
713
+
714
+ #plotPath(ctx, conn) {
715
+ let cached = this.#pathCache.get(conn.id);
716
+ if (cached?.pathStyle === this.#pathStyle) {
717
+ return cached;
718
+ }
719
+
720
+ let fromElNodeView = this.#nodeViews.get(conn.from);
721
+ let toElNodeView = this.#nodeViews.get(conn.to);
722
+
723
+
724
+ if (!fromElNodeView) fromElNodeView = this.#getPhantomProxy(conn.from);
725
+ if (!toElNodeView) toElNodeView = this.#getPhantomProxy(conn.to);
726
+ if (!fromElNodeView || !toElNodeView) return;
727
+
728
+ let fromPos = fromElNodeView._position || { x: 0, y: 0 };
729
+ let toPos = toElNodeView._position || { x: 0, y: 0 };
730
+
731
+ let fromEl = fromElNodeView;
732
+ let toEl = toElNodeView;
733
+
734
+ if (this._nodeRectMap) {
735
+ let c1 = this._nodeRectMap.get(conn.from);
736
+ if (c1) {
737
+ fromPos = { x: c1.x, y: c1.y };
738
+ if (c1.el) fromEl = c1.el;
739
+ }
740
+ let c2 = this._nodeRectMap.get(conn.to);
741
+ if (c2) {
742
+ toPos = { x: c2.x, y: c2.y };
743
+ if (c2.el) toEl = c2.el;
744
+ }
745
+ }
746
+
747
+ let fromW = fromEl._cachedW || fromEl.offsetWidth || 180;
748
+ let fromH = fromEl._cachedH || fromEl.offsetHeight || 100;
749
+ let toW = toEl._cachedW || toEl.offsetWidth || 180;
750
+ let toH = toEl._cachedH || toEl.offsetHeight || 100;
751
+
752
+ let fromSize = { width: fromW, height: fromH };
753
+ let toSize = { width: toW, height: toH };
754
+ let fromNode = this.#editor?.getNode(conn.from);
755
+ let toNode = this.#editor?.getNode(conn.to);
756
+ let fromShape = getShape(fromNode?.shape);
757
+ let toShape = getShape(toNode?.shape);
758
+
759
+ let fromCenter = { x: fromPos.x + fromW / 2, y: fromPos.y + fromH / 2 };
760
+ let toCenter = { x: toPos.x + toW / 2, y: toPos.y + toH / 2 };
761
+
762
+ let fromOffset = this.getSocketOffset(fromEl, conn.out, 'output', toCenter);
763
+ let toOffset = this.getSocketOffset(toEl, conn.in, 'input', fromCenter);
764
+
765
+ let startX = fromPos.x + fromOffset.x;
766
+ let startY = fromPos.y + fromOffset.y;
767
+ let endX = toPos.x + toOffset.x;
768
+ let endY = toPos.y + toOffset.y;
769
+
770
+ let d;
771
+ let arrow = { x: endX, y: endY, angle: 0 };
772
+ let effectiveStyle = this.#pathStyle;
773
+ if (effectiveStyle === 'straight') {
774
+ d = `M ${startX} ${startY} L ${endX} ${endY}`;
775
+ arrow.x = (startX + endX) / 2;
776
+ arrow.y = (startY + endY) / 2;
777
+ arrow.angle = Math.atan2(endY - startY, endX - startX);
778
+ } else if (effectiveStyle === 'orthogonal') {
779
+ let connIndex = this._connIndexMap ? (this._connIndexMap.get(conn.id) ?? 0) : 0;
780
+ let traceOffset = (connIndex > -1 ? connIndex % 10 : 0) * 4;
781
+
782
+ let fromAngle = fromOffset.angle !== undefined ? fromOffset.angle : 0;
783
+ let toAngle = toOffset.angle !== undefined ? toOffset.angle : 180;
784
+
785
+ let stubLen = 20;
786
+ let getDxDy = (deg) => ({
787
+ dx: Math.round(Math.cos((deg * Math.PI) / 180)),
788
+ dy: Math.round(Math.sin((deg * Math.PI) / 180)),
789
+ });
790
+
791
+ let fDir = getDxDy(fromAngle);
792
+ let tDir = getDxDy(toAngle);
793
+
794
+ let p1x = startX + fDir.dx * stubLen;
795
+ let p1y = startY + fDir.dy * stubLen;
796
+ let p2x = endX + tDir.dx * stubLen;
797
+ let p2y = endY + tDir.dy * stubLen;
798
+
799
+ let fromH = fromEl._cachedH || 60;
800
+ let toH = toEl._cachedH || 60;
801
+
802
+ let pts = [
803
+ { x: startX, y: startY },
804
+ { x: p1x, y: p1y },
805
+ ];
806
+ let skipObstacles = this._nodeRectMap && this._nodeRectMap.size > 200;
807
+
808
+ if (endX < startX) {
809
+ let bottomY = Math.max(fromPos.y + fromH, toPos.y + toH) + 30 + traceOffset;
810
+ pts.push({ x: p1x, y: bottomY });
811
+ pts.push({ x: p2x, y: bottomY });
812
+ } else if (skipObstacles) {
813
+
814
+ let midX = (p1x + p2x) / 2 + traceOffset;
815
+ pts.push({ x: midX, y: p1y });
816
+ pts.push({ x: midX, y: p2y });
817
+ } else {
818
+ let maxH = Math.max(fromH, toH);
819
+ if (Math.abs(p1y - p2y) < maxH) {
820
+ let nodeBetween = false;
821
+ let obstacleIter = this._nodeRectMap ? this._nodeRectMap.values() : [];
822
+ for (const rect of obstacleIter) {
823
+ let nx = rect.x;
824
+ let ny = rect.y;
825
+ let nw = rect.w || 180;
826
+ let nh = rect.h || 60;
827
+ if (nx > p1x && nx + nw < p2x) {
828
+ if (Math.min(p1y, p2y) <= ny + nh && Math.max(p1y, p2y) >= ny) {
829
+ nodeBetween = true;
830
+ break;
831
+ }
832
+ }
833
+ }
834
+
835
+ if (nodeBetween) {
836
+ let detourY = Math.min(fromPos.y, toPos.y) - 30 - traceOffset;
837
+ pts.push({ x: p1x, y: detourY });
838
+ pts.push({ x: p2x, y: detourY });
839
+ } else {
840
+ let midX = (p1x + p2x) / 2 + traceOffset;
841
+ pts.push({ x: midX, y: p1y });
842
+ pts.push({ x: midX, y: p2y });
843
+ }
844
+ } else {
845
+ let midX = (p1x + p2x) / 2 + traceOffset;
846
+ let obstacleNode = null;
847
+ let minY = Math.min(p1y, p2y);
848
+ let maxY = Math.max(p1y, p2y);
849
+
850
+ let obstIter = this._nodeRectMap ? this._nodeRectMap.values() : [];
851
+ for (const rect of obstIter) {
852
+ let nx = rect.x;
853
+ let ny = rect.y;
854
+ let nw = rect.w || 180;
855
+ let nh = rect.h || 60;
856
+ if (midX >= nx && midX <= nx + nw) {
857
+ if (ny <= maxY && ny + nh >= minY) {
858
+ obstacleNode = { x: nx, w: nw };
859
+ break;
860
+ }
861
+ }
862
+ }
863
+
864
+ if (obstacleNode) {
865
+ let leftDist = Math.abs(midX - obstacleNode.x);
866
+ let rightDist = Math.abs(midX - (obstacleNode.x + obstacleNode.w));
867
+ if (leftDist < rightDist) {
868
+ midX = obstacleNode.x - 30 - traceOffset;
869
+ } else {
870
+ midX = obstacleNode.x + obstacleNode.w + 30 + traceOffset;
871
+ }
872
+ }
873
+
874
+ pts.push({ x: midX, y: p1y });
875
+ pts.push({ x: midX, y: p2y });
876
+ }
877
+ }
878
+
879
+ pts.push({ x: p2x, y: p2y });
880
+ pts.push({ x: endX, y: endY });
881
+
882
+ let path = `M ${pts[0].x} ${pts[0].y}`;
883
+ for (let i = 1; i < pts.length; i++) {
884
+ let prev = pts[i - 1];
885
+ let curr = pts[i];
886
+ if (curr.x === prev.x && curr.y === prev.y) continue;
887
+ if (curr.x !== prev.x && curr.y !== prev.y) {
888
+ path += ` H ${curr.x} V ${curr.y}`;
889
+ } else if (curr.x !== prev.x) {
890
+ path += ` H ${curr.x}`;
891
+ } else if (curr.y !== prev.y) {
892
+ path += ` V ${curr.y}`;
893
+ }
894
+ }
895
+ if (pts.length >= 2) {
896
+ let midIndex = Math.floor(pts.length / 2);
897
+ let p1 = pts[midIndex - 1];
898
+ let p2 = pts[midIndex];
899
+ if (p1 && p2) {
900
+ arrow.x = (p1.x + p2.x) / 2;
901
+ arrow.y = (p1.y + p2.y) / 2;
902
+ arrow.angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
903
+ }
904
+ }
905
+ d = path;
906
+ } else if (effectiveStyle === 'pcb') {
907
+ let routed = routePcbTrace({
908
+ start: { x: startX, y: startY },
909
+ end: { x: endX, y: endY },
910
+ fromRect: { id: conn.from, x: fromPos.x, y: fromPos.y, w: fromW, h: fromH },
911
+ toRect: { id: conn.to, x: toPos.x, y: toPos.y, w: toW, h: toH },
912
+ fromAngle: fromOffset.angle ?? 0,
913
+ toAngle: toOffset.angle ?? 180,
914
+ rects: this._nodeRectMap ? [...this._nodeRectMap.values()] : [],
915
+ connections: [...this.#connectionData.values()],
916
+ conn,
917
+ });
918
+ d = routed.path;
919
+ arrow = routed.arrow;
920
+ } else {
921
+
922
+ let fromAngleDeg, toAngleDeg;
923
+
924
+ if (fromOffset.angle !== undefined) {
925
+ fromAngleDeg = fromOffset.angle;
926
+ } else {
927
+ let fromPortIndex = fromNode ? Object.keys(fromNode.outputs).indexOf(conn.out) : 0;
928
+ let fromPortTotal = fromNode ? Object.keys(fromNode.outputs).length : 1;
929
+ let pos = fromShape?.getSocketPosition?.('output', fromPortIndex, fromPortTotal, fromSize);
930
+ fromAngleDeg = pos?.angle ?? 0;
931
+ }
932
+
933
+ if (toOffset.angle !== undefined) {
934
+ toAngleDeg = toOffset.angle;
935
+ } else {
936
+ let toPortIndex = toNode ? Object.keys(toNode.inputs).indexOf(conn.in) : 0;
937
+ let toPortTotal = toNode ? Object.keys(toNode.inputs).length : 1;
938
+ let pos = toShape?.getSocketPosition?.('input', toPortIndex, toPortTotal, toSize);
939
+ toAngleDeg = pos?.angle ?? 180;
940
+ }
941
+
942
+ let dist = Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2);
943
+ let cpLen = Math.max(50, dist * 0.4);
944
+ let fromRad = (fromAngleDeg * Math.PI) / 180;
945
+ let toRad = (toAngleDeg * Math.PI) / 180;
946
+
947
+ let cp1x = startX + Math.cos(fromRad) * cpLen;
948
+ let cp1y = startY + Math.sin(fromRad) * cpLen;
949
+ let cp2x = endX + Math.cos(toRad) * cpLen;
950
+ let cp2y = endY + Math.sin(toRad) * cpLen;
951
+
952
+ d = `M ${startX} ${startY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${endX} ${endY}`;
953
+
954
+ arrow.x = (startX + 3 * cp1x + 3 * cp2x + endX) / 8;
955
+ arrow.y = (startY + 3 * cp1y + 3 * cp2y + endY) / 8;
956
+ arrow.angle = Math.atan2(endY + cp2y - cp1y - startY, endX + cp2x - cp1x - startX);
957
+ }
958
+
959
+ let coords = { startX, startY, endX, endY, path2D: new Path2D(d), arrow, pathStyle: effectiveStyle };
960
+ this.#pathCache.set(conn.id, coords);
961
+ return coords;
962
+ }
963
+
964
+ destroy() {
965
+ if (this.#resizeObserver) this.#resizeObserver.disconnect();
966
+ if (this.#animationFrameId) cancelAnimationFrame(this.#animationFrameId);
967
+ this.#connectionData.clear();
968
+ this.#phantomSignature = '';
969
+ this.#invalidatePathCache();
970
+ }
971
+ }