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,1303 @@
1
+ /* eslint-env worker */
2
+ /* globals self */
3
+ /**
4
+ * ForceWorker — Force-directed layout in a Web Worker.
5
+ *
6
+ * Pure implementation (zero dependencies) of proven graph layout algorithms:
7
+ *
8
+ * 1. Barnes-Hut N-body repulsion — O(n log n) via quadtree
9
+ * Paper: Barnes & Hut, "A hierarchical O(N log N) force-calculation algorithm", Nature 1986
10
+ *
11
+ * 2. Quadtree collision detection — prevents node overlap
12
+ * Based on d3-force forceCollide approach: traverse quadtree, push apart overlapping rectangles
13
+ *
14
+ * 3. Hooke's law spring forces — edges pull connected nodes together
15
+ * F = -k * (distance - restLength)
16
+ *
17
+ * 4. Center gravity — prevents drift
18
+ * Weak force pulling all nodes toward centroid
19
+ *
20
+ * Protocol:
21
+ * Main → Worker: { type: 'init', nodes, edges, groups, options }
22
+ * Worker → Main: { type: 'tick', positions, energy, iteration }
23
+ * Worker → Main: { type: 'done', positions, iterations }
24
+ * Main → Worker: { type: 'stop' }
25
+ *
26
+ * Continuous mode (options.mode = 'continuous'):
27
+ * Main → Worker: { type: 'pause' } — freeze simulation, keep state
28
+ * Main → Worker: { type: 'resume' } — unfreeze with gentle reheat
29
+ * Main → Worker: { type: 'pin', id, x, y } — fix node at position (drag)
30
+ * Main → Worker: { type: 'unpin', id } — release pinned node
31
+ * Worker → Main: { type: 'tick', packed: Float32Array } — packed positions
32
+ *
33
+ * @module symbiote-node/canvas/ForceWorker
34
+ */
35
+
36
+
37
+ /**
38
+ * Adaptive quadtree supporting both charge computation and collision detection.
39
+ * Each leaf stores a linked list of bodies at same position (handles coincident nodes).
40
+ */
41
+
42
+
43
+ function quadtreeCreate(nodes) {
44
+ let x0 = Infinity,
45
+ y0 = Infinity,
46
+ x1 = -Infinity,
47
+ y1 = -Infinity;
48
+ for (const n of nodes) {
49
+ if (n.x < x0) x0 = n.x;
50
+ if (n.y < y0) y0 = n.y;
51
+ if (n.x > x1) x1 = n.x;
52
+ if (n.y > y1) y1 = n.y;
53
+ }
54
+
55
+ let dx = x1 - x0,
56
+ dy = y1 - y0;
57
+ let size = Math.max(dx, dy, 1) + 200;
58
+ let cx = (x0 + x1) / 2,
59
+ cy = (y0 + y1) / 2;
60
+
61
+ let tree = {
62
+ x0: cx - size / 2,
63
+ y0: cy - size / 2,
64
+ x1: cx + size / 2,
65
+ y1: cy + size / 2,
66
+ root: null,
67
+ };
68
+
69
+ for (const n of nodes) {
70
+ qtInsert(tree, n);
71
+ }
72
+ return tree;
73
+ }
74
+
75
+ function qtInsert(tree, body) {
76
+ let node = tree.root;
77
+ if (!node) {
78
+ tree.root = { data: body, next: null };
79
+ return;
80
+ }
81
+
82
+ let x0 = tree.x0,
83
+ y0 = tree.y0,
84
+ x1 = tree.x1,
85
+ y1 = tree.y1;
86
+ let parent, i;
87
+
88
+
89
+ while (node.length) {
90
+
91
+ let mx = (x0 + x1) / 2,
92
+ my = (y0 + y1) / 2;
93
+ i = (body.x >= mx ? 1 : 0) | (body.y >= my ? 2 : 0);
94
+ parent = node;
95
+ if (body.x >= mx) x0 = mx;
96
+ else x1 = mx;
97
+ if (body.y >= my) y0 = my;
98
+ else y1 = my;
99
+ node = node[i];
100
+ if (!node) {
101
+ parent[i] = { data: body, next: null };
102
+ return;
103
+ }
104
+ }
105
+
106
+
107
+ let existing = node.data;
108
+ if (Math.abs(existing.x - body.x) < 0.01 && Math.abs(existing.y - body.y) < 0.01) {
109
+
110
+ body._qtNext = node.data;
111
+ node.data = body;
112
+ return;
113
+ }
114
+
115
+
116
+ let leaf = node;
117
+ while (true) {
118
+ let mx = (x0 + x1) / 2,
119
+ my = (y0 + y1) / 2;
120
+ let iNew = (body.x >= mx ? 1 : 0) | (body.y >= my ? 2 : 0);
121
+ let iOld = (existing.x >= mx ? 1 : 0) | (existing.y >= my ? 2 : 0);
122
+
123
+ let internal = [null, null, null, null];
124
+ internal.length = 4;
125
+ if (parent) parent[i] = internal;
126
+ else tree.root = internal;
127
+
128
+ if (iNew !== iOld) {
129
+ internal[iNew] = { data: body, next: null };
130
+ internal[iOld] = leaf;
131
+ return;
132
+ }
133
+
134
+
135
+ parent = internal;
136
+ i = iNew;
137
+ if (body.x >= mx) x0 = mx;
138
+ else x1 = mx;
139
+ if (body.y >= my) y0 = my;
140
+ else y1 = my;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Visit each node in the quadtree (post-order for aggregation).
146
+ * callback(node, x0, y0, x1, y1) → return true to skip children.
147
+ */
148
+ function qtVisitAfter(tree, callback) {
149
+ let quads = [];
150
+ if (tree.root)
151
+ quads.push({ node: tree.root, x0: tree.x0, y0: tree.y0, x1: tree.x1, y1: tree.y1 });
152
+
153
+ let stack = [];
154
+ while (quads.length) {
155
+ let q = quads.pop();
156
+ stack.push(q);
157
+ if (q.node.length) {
158
+ let { x0, y0, x1, y1 } = q;
159
+ let mx = (x0 + x1) / 2,
160
+ my = (y0 + y1) / 2;
161
+ if (q.node[0]) quads.push({ node: q.node[0], x0, y0, x1: mx, y1: my });
162
+ if (q.node[1]) quads.push({ node: q.node[1], x0: mx, y0, x1, y1: my });
163
+ if (q.node[2]) quads.push({ node: q.node[2], x0, y0: my, x1: mx, y1 });
164
+ if (q.node[3]) quads.push({ node: q.node[3], x0: mx, y0: my, x1, y1 });
165
+ }
166
+ }
167
+
168
+ while (stack.length) {
169
+ let q = stack.pop();
170
+ callback(q.node, q.x0, q.y0, q.x1, q.y1);
171
+ }
172
+ }
173
+
174
+ function qtVisit(tree, callback) {
175
+ let quads = [];
176
+ if (tree.root)
177
+ quads.push({ node: tree.root, x0: tree.x0, y0: tree.y0, x1: tree.x1, y1: tree.y1 });
178
+ while (quads.length) {
179
+ let q = quads.pop();
180
+ if (callback(q.node, q.x0, q.y0, q.x1, q.y1)) continue;
181
+ if (q.node.length) {
182
+ let { x0, y0, x1, y1 } = q;
183
+ let mx = (x0 + x1) / 2,
184
+ my = (y0 + y1) / 2;
185
+ if (q.node[3]) quads.push({ node: q.node[3], x0: mx, y0: my, x1, y1 });
186
+ if (q.node[2]) quads.push({ node: q.node[2], x0, y0: my, x1: mx, y1 });
187
+ if (q.node[1]) quads.push({ node: q.node[1], x0: mx, y0, x1, y1: my });
188
+ if (q.node[0]) quads.push({ node: q.node[0], x0, y0, x1: mx, y1: my });
189
+ }
190
+ }
191
+ }
192
+
193
+
194
+ /**
195
+ * Barnes-Hut charge force (Coulomb-like repulsion).
196
+ * Aggregates mass and center-of-mass up the quadtree.
197
+ * θ (theta) controls accuracy vs speed: region_size/distance < θ → treat as point mass.
198
+ * @param {Array<Object>} nodes
199
+ * @param {number} strength
200
+ * @param {number} theta
201
+ * @returns {void}
202
+ */
203
+ function applyChargeForce(nodes, strength, theta) {
204
+ let tree = quadtreeCreate(nodes);
205
+
206
+
207
+ qtVisitAfter(tree, (node) => {
208
+ if (!node.length) {
209
+
210
+ let current = node.data;
211
+ let count = 0;
212
+ while (current) {
213
+ count++;
214
+ current = current._qtNext;
215
+ }
216
+ node.value = strength * count;
217
+ node.x = node.data.x;
218
+ node.y = node.data.y;
219
+ return;
220
+ }
221
+
222
+ let value = 0,
223
+ x = 0,
224
+ y = 0,
225
+ weight = 0;
226
+ for (let i = 0; i < 4; i++) {
227
+ let child = node[i];
228
+ if (!child || !child.value) continue;
229
+ let w = Math.abs(child.value);
230
+ value += child.value;
231
+ x += child.x * w;
232
+ y += child.y * w;
233
+ weight += w;
234
+ }
235
+ node.value = value;
236
+ node.x = weight > 0 ? x / weight : 0;
237
+ node.y = weight > 0 ? y / weight : 0;
238
+ });
239
+
240
+
241
+ let thetaSq = theta * theta;
242
+
243
+ let avgSize = 20;
244
+ if (nodes.length > 0) {
245
+ avgSize = nodes.reduce((s, n) => s + Math.max(n.w, n.h), 0) / nodes.length;
246
+ }
247
+ let distMin2 = Math.max(1, avgSize * avgSize * 0.25);
248
+ for (const body of nodes) {
249
+ qtVisit(tree, (node, x0, y0, x1) => {
250
+ if (!node.value) return true;
251
+
252
+ let dx = node.x - body.x;
253
+ let dy = node.y - body.y;
254
+ let w = x1 - x0;
255
+
256
+
257
+ if (dx === 0 && dy === 0) {
258
+ dx = (Math.random() - 0.5) * 20;
259
+ dy = (Math.random() - 0.5) * 20;
260
+ }
261
+
262
+ let distSq = dx * dx + dy * dy;
263
+ if (distSq < distMin2) distSq = distMin2;
264
+
265
+
266
+ if ((w * w) / distSq < thetaSq) {
267
+ if (distSq < 1000 * 1000) {
268
+
269
+ let force = node.value / distSq;
270
+ body.vx -= dx * force;
271
+ body.vy -= dy * force;
272
+ }
273
+ return true;
274
+ }
275
+
276
+
277
+ if (!node.length) {
278
+ let current = node.data;
279
+ while (current) {
280
+ if (current !== body) {
281
+ let dxLeaf = current.x - body.x;
282
+ let dyLeaf = current.y - body.y;
283
+ if (dxLeaf === 0 && dyLeaf === 0) {
284
+ dxLeaf = (Math.random() - 0.5) * 20;
285
+ dyLeaf = (Math.random() - 0.5) * 20;
286
+ }
287
+ let distSqLeaf = dxLeaf * dxLeaf + dyLeaf * dyLeaf;
288
+ if (distSqLeaf < distMin2) distSqLeaf = distMin2;
289
+ let force = strength / distSqLeaf;
290
+ body.vx -= dxLeaf * force;
291
+ body.vy -= dyLeaf * force;
292
+ }
293
+ current = current._qtNext;
294
+ }
295
+ return true;
296
+ }
297
+
298
+ return false;
299
+ });
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Collision force — prevents node overlap.
305
+ * Uses spatial hash grid for O(n) neighbor detection.
306
+ * Applies POSITIONAL separation (not just velocity) for hard constraints.
307
+ * Multi-pass (3 iterations) to resolve chain collisions.
308
+ * @param {Array<Object>} nodes
309
+ * @param {number} strength
310
+ * @param {number} iterations
311
+ */
312
+ function applyCollisionForce(nodes, strength, iterations) {
313
+ let iters = iterations || 3;
314
+
315
+ let padX = 8;
316
+ let padY = 4;
317
+
318
+ let maxW = 0;
319
+ let maxH = 0;
320
+ for (const n of nodes) {
321
+ if (n.w > maxW) maxW = n.w;
322
+ if (n.h > maxH) maxH = n.h;
323
+ }
324
+
325
+ if (maxW < 20) maxW = 20;
326
+ if (maxH < 20) maxH = 20;
327
+
328
+ for (let pass = 0; pass < iters; pass++) {
329
+
330
+ let cellW = maxW * 1.5;
331
+ let cellH = maxH * 3;
332
+ let grid = new Map();
333
+
334
+ for (let i = 0; i < nodes.length; i++) {
335
+ let n = nodes[i];
336
+ let gx = Math.floor(n.x / cellW);
337
+ let gy = Math.floor(n.y / cellH);
338
+ let key = `${gx},${gy}`;
339
+ if (!grid.has(key)) grid.set(key, []);
340
+ grid.get(key).push(i);
341
+ }
342
+
343
+
344
+ for (let i = 0; i < nodes.length; i++) {
345
+ let n = nodes[i];
346
+ let gx = Math.floor(n.x / cellW);
347
+ let gy = Math.floor(n.y / cellH);
348
+ for (let dx = -1; dx <= 1; dx++) {
349
+ for (let dy = -1; dy <= 1; dy++) {
350
+ let neighbors = grid.get(`${gx + dx},${gy + dy}`);
351
+ if (!neighbors) continue;
352
+ for (const j of neighbors) {
353
+ if (j <= i) continue;
354
+ resolveOverlap(nodes, i, j, padX, padY, strength);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Resolve overlap between two nodes
364
+ * @param {Array<Object>} nodes
365
+ * @param {number} i
366
+ * @param {number} j
367
+ * @param {number} padX
368
+ * @param {number} padY
369
+ * @param {number} strength
370
+ */
371
+ function resolveOverlap(nodes, i, j, padX, padY, strength) {
372
+ let a = nodes[i],
373
+ b = nodes[j];
374
+
375
+
376
+ if (a.parentId !== b.parentId) {
377
+ if (a.id !== config.activeGroupId && b.id !== config.activeGroupId) {
378
+ return;
379
+ }
380
+
381
+ if (
382
+ (a.id === config.activeGroupId && b.parentId === a.id) ||
383
+ (b.id === config.activeGroupId && a.parentId === b.id)
384
+ ) {
385
+ return;
386
+ }
387
+ }
388
+
389
+
390
+ let dx = b.x - a.x;
391
+ let dy = b.y - a.y;
392
+
393
+ let hwA = a.w / 2 + padX;
394
+ let hhA = a.h / 2 + padY;
395
+ let hwB = b.w / 2 + padX;
396
+ let hhB = b.h / 2 + padY;
397
+
398
+ let overlapX = hwA + hwB - Math.abs(dx);
399
+ let overlapY = hhA + hhB - Math.abs(dy);
400
+
401
+ if (overlapX > 0 && overlapY > 0) {
402
+
403
+
404
+ if (overlapX < overlapY) {
405
+ let sign = dx < 0 ? -1 : dx > 0 ? 1 : Math.random() < 0.5 ? -1 : 1;
406
+ let push = overlapX * strength * 0.5;
407
+
408
+ a.x -= sign * push;
409
+ b.x += sign * push;
410
+
411
+
412
+ if (Math.sign(a.vx) === sign) a.vx = 0;
413
+ if (Math.sign(b.vx) === -sign) b.vx = 0;
414
+
415
+
416
+ let jitter = (Math.random() - 0.5) * 0.5;
417
+ a.y -= jitter;
418
+ b.y += jitter;
419
+ } else {
420
+ let sign = dy < 0 ? -1 : dy > 0 ? 1 : Math.random() < 0.5 ? -1 : 1;
421
+ let push = overlapY * strength * 0.5;
422
+
423
+ a.y -= sign * push;
424
+ b.y += sign * push;
425
+
426
+
427
+ if (Math.sign(a.vy) === sign) a.vy = 0;
428
+ if (Math.sign(b.vy) === -sign) b.vy = 0;
429
+
430
+
431
+ let jitter = (Math.random() - 0.5) * 0.5;
432
+ a.x -= jitter;
433
+ b.x += jitter;
434
+ }
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Count overlapping node pairs using spatial hash. O(n) average.
440
+ * @param {Array<Object>} nodes
441
+ * @returns {number} Number of overlapping pairs
442
+ */
443
+ function countOverlaps(nodes) {
444
+ let maxW = 260,
445
+ maxH = 40;
446
+ for (const n of nodes) {
447
+ if (n.w > maxW) maxW = n.w;
448
+ if (n.h > maxH) maxH = n.h;
449
+ }
450
+ let cellW = maxW * 1.5;
451
+ let cellH = maxH * 3;
452
+ let grid = new Map();
453
+
454
+ for (let i = 0; i < nodes.length; i++) {
455
+ let n = nodes[i];
456
+ let key = `${Math.floor(n.x / cellW)},${Math.floor(n.y / cellH)}`;
457
+ if (!grid.has(key)) grid.set(key, []);
458
+ grid.get(key).push(i);
459
+ }
460
+
461
+ let count = 0;
462
+ for (let i = 0; i < nodes.length; i++) {
463
+ let n = nodes[i];
464
+ let gx = Math.floor(n.x / cellW);
465
+ let gy = Math.floor(n.y / cellH);
466
+ for (let dx = -1; dx <= 1; dx++) {
467
+ for (let dy = -1; dy <= 1; dy++) {
468
+ let neighbors = grid.get(`${gx + dx},${gy + dy}`);
469
+ if (!neighbors) continue;
470
+ for (const j of neighbors) {
471
+ if (j <= i) continue;
472
+ let b = nodes[j];
473
+ let hwA = n.w / 2,
474
+ hhA = n.h / 2;
475
+ let hwB = b.w / 2,
476
+ hhB = b.h / 2;
477
+ if (Math.abs(n.x - b.x) < hwA + hwB && Math.abs(n.y - b.y) < hhA + hhB) count++;
478
+ }
479
+ }
480
+ }
481
+ }
482
+ return count;
483
+ }
484
+
485
+ /**
486
+ * Jitter only nodes that are actually overlapping. Uses spatial hash for O(n).
487
+ * Small random displacement breaks deadlocks in post-convergence cleanup.
488
+ * @param {Array<Object>} nodes
489
+ */
490
+
491
+
492
+ /**
493
+ * Spring force (Hooke's law) for linked nodes.
494
+ * F = strength * (distance - restLength)
495
+ * @param {Array<Object>} nodes
496
+ * @param {Array<Object>} edges
497
+ * @param {number} alpha
498
+ */
499
+ function applyLinkForce(nodes, edges, alpha) {
500
+ for (const e of edges) {
501
+ let s = nodes[e.source];
502
+ let t = nodes[e.target];
503
+ if (!s || !t) continue;
504
+
505
+ let dx = t.x + t.vx - s.x - s.vx;
506
+ let dy = t.y + t.vy - s.y - s.vy;
507
+ if (dx === 0 && dy === 0) {
508
+ dx = (Math.random() - 0.5) * 1e-6;
509
+ dy = dx;
510
+ }
511
+
512
+ let dist = Math.sqrt(dx * dx + dy * dy) || 1;
513
+ let force = ((dist - e.restLength) / dist) * alpha * e.strength;
514
+ let fx = dx * force;
515
+ let fy = dy * force;
516
+
517
+
518
+ let bias = e.bias;
519
+ t.vx -= fx * bias;
520
+ t.vy -= fy * bias;
521
+ s.vx += fx * (1 - bias);
522
+ s.vy += fy * (1 - bias);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Center force: pulls all nodes toward centroid or attractors.
528
+ * External nodes → global center (0,0). Internal nodes → parent center (bx,by).
529
+ * @param {Array<Object>} nodes
530
+ * @param {number} strength
531
+ * @param {Object} attractors
532
+ * @param {number} [bx=0]
533
+ * @param {number} [by=0]
534
+ */
535
+
536
+
537
+ /**
538
+ * Boundary force: pushes nodes back if they escape the boundary circle.
539
+ * @param {Array<Object>} nodes
540
+ * @param {number} radius
541
+ * @param {number} strength
542
+ * @param {number} bx
543
+ * @param {number} by
544
+ * @param {string} activeGroupId
545
+ */
546
+
547
+
548
+ let nodes = [];
549
+ let edges = [];
550
+ let running = false;
551
+ let paused = false;
552
+ let galacticSuns = [];
553
+ let planets = [];
554
+ let simMode = 'converge';
555
+ let continuousTimer = null;
556
+
557
+ let config = {
558
+ chargeStrength: -250,
559
+ theta: 0.7,
560
+ linkDistance: 180,
561
+ linkStrength: 0.15,
562
+ groupDistance: 120,
563
+ groupStrength: 0.05,
564
+ collideStrength: 0.95,
565
+ centerStrength: 0.01,
566
+ velocityDecay: 0.92,
567
+ alphaDecay: 0.015,
568
+ alphaMin: 0.001,
569
+ alphaTarget: 0,
570
+
571
+ contAlphaFloor: 0.001,
572
+ contAlphaTarget: 0.001,
573
+ brownian: 0.005,
574
+ brownianThresh: 0.005,
575
+ pinReheat: 0.03,
576
+ pinCap: 0.1,
577
+ resumeReheat: 0.05,
578
+ resumeCap: 0.1,
579
+
580
+
581
+ activeGroupId: null,
582
+ boundaryRadius: null,
583
+ boundaryStrength: 0.2,
584
+ attractors: null,
585
+
586
+ wellStrength: 0.8,
587
+ centerPull: 0.3,
588
+ wellRepulsion: 5.0,
589
+ crossLinkScale: 0.2,
590
+ };
591
+
592
+ function initSimulation(data) {
593
+ let { nodes: rawNodes, edges: rawEdges, groups = {}, options = {} } = data;
594
+
595
+
596
+ Object.assign(config, options);
597
+ simMode = options.mode || 'converge';
598
+
599
+
600
+ nodes = rawNodes.map((n, i) => {
601
+ let angle = (2 * Math.PI * i) / rawNodes.length;
602
+ let radius = Math.sqrt(rawNodes.length) * 50;
603
+ let w = n.w || options.nodeWidth || 260;
604
+ let h = n.h || options.nodeHeight || 40;
605
+
606
+
607
+ let hasPos = n.x !== undefined && n.y !== undefined;
608
+ return {
609
+ id: n.id,
610
+ x: hasPos ? n.x : Math.cos(angle) * radius + (Math.random() - 0.5) * 100,
611
+ y: hasPos ? n.y : Math.sin(angle) * radius + (Math.random() - 0.5) * 100,
612
+ _hadPos: hasPos,
613
+ vx: 0,
614
+ vy: 0,
615
+ group: n.group || null,
616
+ type: n.type || null,
617
+ parentId: n.parentId || null,
618
+ isGroup: n.isGroup || false,
619
+ children: n.children || [],
620
+ index: i,
621
+ w,
622
+ h,
623
+ };
624
+ });
625
+
626
+
627
+ if (options.activeGroupId) {
628
+ let parentNode = nodes.find((n) => n.id === options.activeGroupId);
629
+ if (parentNode) {
630
+
631
+ let newChildren = nodes.filter((n) => n.parentId === options.activeGroupId && !n._hadPos);
632
+ for (let i = 0; i < newChildren.length; i++) {
633
+ let n = newChildren[i];
634
+
635
+ let angle = (2 * Math.PI * i) / newChildren.length + (Math.random() - 0.5) * 0.5;
636
+ let spread = parentNode.w * 0.3;
637
+ n.x = parentNode.x + Math.cos(angle) * spread;
638
+ n.y = parentNode.y + Math.sin(angle) * spread;
639
+
640
+ n.vx = Math.cos(angle) * 15;
641
+ n.vy = Math.sin(angle) * 15;
642
+ }
643
+ }
644
+ }
645
+
646
+ let nodeIndex = {};
647
+ nodes.forEach((n, i) => {
648
+ nodeIndex[n.id] = i;
649
+ });
650
+
651
+
652
+ let rawDegree = new Array(nodes.length).fill(0);
653
+ rawEdges.forEach((e) => {
654
+ let si = nodeIndex[e.from],
655
+ ti = nodeIndex[e.to];
656
+ if (si !== undefined) rawDegree[si]++;
657
+ if (ti !== undefined) rawDegree[ti]++;
658
+ });
659
+
660
+
661
+ let degree = new Array(nodes.length).fill(0);
662
+
663
+
664
+ edges = rawEdges
665
+ .map((e) => {
666
+ let si = nodeIndex[e.from],
667
+ ti = nodeIndex[e.to];
668
+ if (si === undefined || ti === undefined) return null;
669
+ degree[si]++;
670
+ degree[ti]++;
671
+ return {
672
+ source: si,
673
+ target: ti,
674
+ strength: config.linkStrength,
675
+ restLength: config.linkDistance,
676
+ bias: 0.5,
677
+ };
678
+ })
679
+ .filter(Boolean);
680
+
681
+
682
+ for (const [, memberIds] of Object.entries(groups)) {
683
+ if (memberIds.length < 2) continue;
684
+
685
+
686
+ let bestHubId = memberIds[0];
687
+ let maxConnections = -1;
688
+ for (const mId of memberIds) {
689
+ let idx = nodeIndex[mId];
690
+ if (idx !== undefined && rawDegree[idx] > maxConnections) {
691
+ maxConnections = rawDegree[idx];
692
+ bestHubId = mId;
693
+ }
694
+ }
695
+
696
+ let hubIdx = nodeIndex[bestHubId];
697
+ if (hubIdx === undefined) continue;
698
+
699
+
700
+ for (const mId of memberIds) {
701
+ if (mId === bestHubId) continue;
702
+ let ti = nodeIndex[mId];
703
+ if (ti !== undefined) {
704
+ degree[hubIdx]++;
705
+ degree[ti]++;
706
+ edges.push({
707
+ source: hubIdx,
708
+ target: ti,
709
+ strength: config.groupStrength,
710
+ restLength: config.groupDistance,
711
+ bias: 0.5,
712
+ });
713
+ }
714
+ }
715
+ }
716
+
717
+
718
+ for (const e of edges) {
719
+ let ds = degree[e.source] || 1;
720
+ let dt = degree[e.target] || 1;
721
+ e.bias = ds / (ds + dt);
722
+ }
723
+
724
+
725
+ computeGravityWells(degree);
726
+ }
727
+
728
+ /**
729
+ * Galactic Physics: classify nodes as Suns (hubs) or Planets (leaves).
730
+ * Suns = group nodes OR high-degree nodes (> median * 1.5).
731
+ * Planets are assigned to the nearest connected Sun.
732
+ * Orphans are promoted to micro-suns.
733
+ */
734
+ function computeGravityWells(degree) {
735
+ galacticSuns = [];
736
+ planets = [];
737
+
738
+
739
+ for (const n of nodes) {
740
+ n.isSun = false;
741
+ n.mySun = null;
742
+ }
743
+
744
+
745
+ let medianDeg =
746
+ degree.length > 0 ? [...degree].sort((a, b) => a - b)[Math.floor(degree.length / 2)] : 1;
747
+ let hubThreshold = Math.max(3, medianDeg * 1.5);
748
+
749
+ for (let i = 0; i < nodes.length; i++) {
750
+ let n = nodes[i];
751
+ let deg = degree[i] || 0;
752
+
753
+ if (n.parentId && n.parentId === config.activeGroupId) continue;
754
+ if (n.id === config.activeGroupId) continue;
755
+
756
+ if (n.isGroup || deg >= hubThreshold || (!n.parentId && n.children && n.children.length > 0)) {
757
+ n.isSun = true;
758
+ n.mass = deg + 5;
759
+ galacticSuns.push(n);
760
+ } else {
761
+ n.isSun = false;
762
+ n.mass = 1;
763
+ }
764
+ }
765
+
766
+
767
+ for (const e of edges) {
768
+ let s = nodes[e.source],
769
+ t = nodes[e.target];
770
+ if (s.isSun && !t.isSun && !t.mySun) t.mySun = s;
771
+ else if (t.isSun && !s.isSun && !s.mySun) s.mySun = t;
772
+ }
773
+
774
+
775
+ for (const n of nodes) {
776
+ if (n.id === config.activeGroupId) continue;
777
+ if (!n.isSun) {
778
+ if (n.mySun) planets.push(n);
779
+ else {
780
+
781
+ n.isSun = true;
782
+ n.mass = 2;
783
+ galacticSuns.push(n);
784
+ }
785
+ }
786
+ }
787
+
788
+
789
+ for (const e of edges) {
790
+ let s = nodes[e.source],
791
+ t = nodes[e.target];
792
+ if (!s || !t) continue;
793
+
794
+
795
+ if (e._origStrength === undefined) {
796
+ e._origStrength = e.strength;
797
+ e._origRestLength = e.restLength;
798
+ }
799
+
800
+
801
+ e._isCrossGalactic = false;
802
+ if (s.isSun && t.isSun) e._isCrossGalactic = true;
803
+ else if (s.mySun && t.mySun && s.mySun !== t.mySun) e._isCrossGalactic = true;
804
+ else if (s.mySun && t.isSun && s.mySun !== t) e._isCrossGalactic = true;
805
+ else if (t.mySun && s.isSun && t.mySun !== s) e._isCrossGalactic = true;
806
+
807
+ if (e._isCrossGalactic) {
808
+ e.strength = e._origStrength * config.crossLinkScale;
809
+
810
+ e.restLength = e._origRestLength * (1 + 0.5 * (1 - config.crossLinkScale));
811
+ } else {
812
+ e.strength = e._origStrength;
813
+ e.restLength = e._origRestLength;
814
+ }
815
+ }
816
+ }
817
+
818
+ function tick(alpha) {
819
+
820
+
821
+ applyChargeForce(nodes, config.chargeStrength * alpha, config.theta);
822
+
823
+
824
+ applyLinkForce(nodes, edges, alpha);
825
+
826
+
827
+ applyCollisionForce(nodes, config.collideStrength, 4);
828
+
829
+
830
+ for (const sun of galacticSuns) {
831
+ sun.dynamicRadius = sun.w || 20;
832
+ sun.smoothRadius = sun.smoothRadius || sun.dynamicRadius;
833
+ }
834
+ for (const p of planets) {
835
+ if (p.mySun) {
836
+ let dx = p.x - p.mySun.x;
837
+ let dy = p.y - p.mySun.y;
838
+ let dist = Math.sqrt(dx * dx + dy * dy);
839
+ if (dist > p.mySun.dynamicRadius) {
840
+ p.mySun.dynamicRadius = dist;
841
+ }
842
+ }
843
+ }
844
+ for (const sun of galacticSuns) {
845
+ sun.smoothRadius += (sun.dynamicRadius - sun.smoothRadius) * 0.08;
846
+ }
847
+
848
+
849
+ for (const sun of galacticSuns) {
850
+ if (sun.id === config.activeGroupId) continue;
851
+ sun.vx -= sun.x * config.centerPull * alpha;
852
+ sun.vy -= sun.y * config.centerPull * alpha;
853
+ }
854
+
855
+
856
+ for (let i = 0; i < galacticSuns.length; i++) {
857
+ for (let j = i + 1; j < galacticSuns.length; j++) {
858
+ let si = galacticSuns[i],
859
+ sj = galacticSuns[j];
860
+ let dx = sj.x - si.x;
861
+ let dy = sj.y - si.y;
862
+ let dist = Math.sqrt(dx * dx + dy * dy) + 1;
863
+ let combinedRadius = si.smoothRadius + sj.smoothRadius;
864
+ if (dist < combinedRadius) {
865
+
866
+ let overlapRatio = (combinedRadius - dist) / combinedRadius;
867
+ let rawForce = overlapRatio * config.wellRepulsion * alpha;
868
+
869
+ let force = Math.min(rawForce, 50);
870
+ let nx = dx / dist,
871
+ ny = dy / dist;
872
+ si.vx -= nx * force;
873
+ si.vy -= ny * force;
874
+ sj.vx += nx * force;
875
+ sj.vy += ny * force;
876
+ }
877
+ }
878
+ }
879
+
880
+
881
+ for (const p of planets) {
882
+ let dx = p.x - p.mySun.x;
883
+ let dy = p.y - p.mySun.y;
884
+ p.vx -= dx * config.wellStrength * alpha;
885
+ p.vy -= dy * config.wellStrength * alpha;
886
+ }
887
+
888
+
889
+ let energy = 0;
890
+ let decay = 1 - config.velocityDecay;
891
+ let vMax = Math.max(200, Math.sqrt(nodes.length) * 10);
892
+ for (const n of nodes) {
893
+ if (n.fx !== undefined) {
894
+ n.x = n.fx;
895
+ n.vx = 0;
896
+ } else {
897
+ n.vx *= decay;
898
+ if (n.vx > vMax) n.vx = vMax;
899
+ else if (n.vx < -vMax) n.vx = -vMax;
900
+ n.x += n.vx;
901
+ }
902
+ if (n.fy !== undefined) {
903
+ n.y = n.fy;
904
+ n.vy = 0;
905
+ } else {
906
+ n.vy *= decay;
907
+ if (n.vy > vMax) n.vy = vMax;
908
+ else if (n.vy < -vMax) n.vy = -vMax;
909
+ n.y += n.vy;
910
+ }
911
+ energy += n.vx * n.vx + n.vy * n.vy;
912
+ }
913
+
914
+ return energy;
915
+ }
916
+
917
+ function getPositions() {
918
+ let positions = {};
919
+ for (const n of nodes) {
920
+ positions[n.id] = { x: Math.round(n.x - n.w / 2), y: Math.round(n.y - n.h / 2) };
921
+ }
922
+ return positions;
923
+ }
924
+
925
+ /**
926
+ * Pack positions into a Float32Array for efficient transfer.
927
+ * Layout: [x0, y0, x1, y1, ...] in node index order.
928
+ * The ID-to-index mapping is stable from initSimulation.
929
+ * @returns {Float32Array}
930
+ */
931
+ function getPositionsPacked() {
932
+ let buf = new Float32Array(nodes.length * 2);
933
+ for (let i = 0; i < nodes.length; i++) {
934
+ buf[i * 2] = nodes[i].x - nodes[i].w / 2;
935
+ buf[i * 2 + 1] = nodes[i].y - nodes[i].h / 2;
936
+ }
937
+ return buf;
938
+ }
939
+
940
+ /**
941
+ * Get ordered node IDs (sent once at init, used to unpack Float32Array).
942
+ * @returns {string[]}
943
+ */
944
+ function getNodeIds() {
945
+ return nodes.map((n) => n.id);
946
+ }
947
+
948
+
949
+ self.onmessage = function (e) {
950
+ let { type } = e.data;
951
+
952
+ if (type === 'init') {
953
+ running = true;
954
+ paused = false;
955
+ initSimulation(e.data);
956
+
957
+ if (simMode === 'continuous') {
958
+ startContinuous();
959
+ } else {
960
+ startConverge();
961
+ }
962
+ }
963
+
964
+ if (type === 'pause') {
965
+ paused = true;
966
+ if (continuousTimer !== null) {
967
+ clearTimeout(continuousTimer);
968
+ continuousTimer = null;
969
+ }
970
+ }
971
+
972
+ if (type === 'resume') {
973
+ if (!running || !paused) return;
974
+ paused = false;
975
+
976
+ continuousAlpha = Math.min(continuousAlpha + config.resumeReheat, config.resumeCap);
977
+ startContinuousLoop();
978
+ }
979
+
980
+ if (type === 'pin') {
981
+ let { id, x, y } = e.data;
982
+ let node = nodes.find((n) => n.id === id);
983
+ if (node) {
984
+
985
+ node.fx = x + node.w / 2;
986
+ node.fy = y + node.h / 2;
987
+
988
+ if (simMode === 'continuous') {
989
+ continuousAlpha = Math.min(continuousAlpha + config.pinReheat, config.pinCap);
990
+ if (paused) {
991
+ paused = false;
992
+ startContinuousLoop();
993
+ }
994
+ }
995
+ }
996
+ }
997
+
998
+ if (type === 'unpin') {
999
+ let { id } = e.data;
1000
+ let node = nodes.find((n) => n.id === id);
1001
+ if (node) {
1002
+ delete node.fx;
1003
+ delete node.fy;
1004
+ if (simMode === 'continuous') {
1005
+ continuousAlpha = Math.min(continuousAlpha + config.pinReheat, config.pinCap);
1006
+ if (paused) {
1007
+ paused = false;
1008
+ startContinuousLoop();
1009
+ }
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ if (type === 'updateConfig') {
1015
+ let updates = e.data.config;
1016
+ if (updates) {
1017
+ Object.assign(config, updates);
1018
+
1019
+ if (updates.linkDistance !== undefined || updates.linkStrength !== undefined) {
1020
+ for (const edge of edges) {
1021
+ if (edge.restLength === config.groupDistance && edge.strength === config.groupStrength)
1022
+ continue;
1023
+ if (updates.linkDistance !== undefined) edge.restLength = config.linkDistance;
1024
+ if (updates.linkStrength !== undefined) edge.strength = config.linkStrength;
1025
+ }
1026
+ }
1027
+ if (updates.groupDistance !== undefined || updates.groupStrength !== undefined) {
1028
+ for (const edge of edges) {
1029
+
1030
+ if (edge.restLength !== config.linkDistance || edge.strength !== config.linkStrength) {
1031
+ if (updates.groupDistance !== undefined) edge.restLength = config.groupDistance;
1032
+ if (updates.groupStrength !== undefined) edge.strength = config.groupStrength;
1033
+ }
1034
+ }
1035
+ }
1036
+
1037
+ if (updates.crossLinkScale !== undefined) {
1038
+ for (const edge of edges) {
1039
+ if (edge._isCrossGalactic && edge._origStrength !== undefined) {
1040
+ edge.strength = edge._origStrength * config.crossLinkScale;
1041
+ edge.restLength = edge._origRestLength * (1 + 0.5 * (1 - config.crossLinkScale));
1042
+ }
1043
+ }
1044
+ }
1045
+
1046
+
1047
+ if (simMode === 'continuous') {
1048
+ continuousAlpha = Math.min(continuousAlpha + config.resumeReheat, config.resumeCap);
1049
+ if (!paused && continuousTimer === null) {
1050
+ startContinuousLoop();
1051
+ }
1052
+ }
1053
+ }
1054
+ }
1055
+
1056
+ if (type === 'stop') {
1057
+ running = false;
1058
+ paused = false;
1059
+ if (continuousTimer !== null) {
1060
+ clearTimeout(continuousTimer);
1061
+ continuousTimer = null;
1062
+ }
1063
+ self.postMessage({
1064
+ type: 'done',
1065
+ positions: getPositions(),
1066
+ energy: 0,
1067
+ iteration: -1,
1068
+ });
1069
+ }
1070
+ };
1071
+
1072
+
1073
+ function startConverge() {
1074
+ let totalNodes = nodes.length;
1075
+ let adaptiveAlphaDecay = config.alphaDecay;
1076
+ let alpha = 1;
1077
+ let iteration = 0;
1078
+ let maxIter = Math.ceil(Math.log(config.alphaMin) / Math.log(1 - config.alphaDecay)) + 1;
1079
+ let batchSize = totalNodes > 1000 ? 8 : 4;
1080
+
1081
+ function runBatch() {
1082
+ if (!running) return;
1083
+
1084
+ for (let i = 0; i < batchSize && alpha > config.alphaMin && iteration < maxIter; i++) {
1085
+ tick(alpha);
1086
+ alpha += (config.alphaTarget - alpha) * adaptiveAlphaDecay;
1087
+ iteration++;
1088
+ }
1089
+
1090
+ if (iteration % 20 === 0) {
1091
+ let overlaps = countOverlaps(nodes);
1092
+ if (overlaps > 0 && alpha > 0.05) {
1093
+ adaptiveAlphaDecay = Math.max(0.005, adaptiveAlphaDecay * 0.9);
1094
+ }
1095
+ }
1096
+
1097
+ let isDone = alpha <= config.alphaMin || iteration >= maxIter;
1098
+
1099
+ if (!isDone) {
1100
+ self.postMessage({
1101
+ type: 'tick',
1102
+ positions: getPositions(),
1103
+ energy: Math.round(alpha * 1000) / 1000,
1104
+ iteration,
1105
+ overlaps: countOverlaps(nodes),
1106
+ });
1107
+ setTimeout(runBatch, 0);
1108
+ } else {
1109
+
1110
+ let attempt = 0;
1111
+ let maxExpansionAttempts = 2000;
1112
+ let expansionBatchSize = totalNodes > 1000 ? 10 : 20;
1113
+
1114
+ function runExpansionBatch() {
1115
+ if (!running) return;
1116
+
1117
+ let overlaps = countOverlaps(nodes);
1118
+ let bIter = 0;
1119
+
1120
+ while (overlaps > 0 && attempt < maxExpansionAttempts && bIter < expansionBatchSize) {
1121
+ applyCollisionForce(nodes, 1.0, 4);
1122
+
1123
+ let maxW = 260,
1124
+ maxH = 40;
1125
+ for (const n of nodes) {
1126
+ if (n.w > maxW) maxW = n.w;
1127
+ if (n.h > maxH) maxH = n.h;
1128
+ }
1129
+ let cellW = maxW * 1.5;
1130
+ let cellH = maxH * 3;
1131
+ let grid = new Map();
1132
+ for (let i = 0; i < nodes.length; i++) {
1133
+ let n = nodes[i];
1134
+ let key = `${Math.floor(n.x / cellW)},${Math.floor(n.y / cellH)}`;
1135
+ if (!grid.has(key)) grid.set(key, []);
1136
+ grid.get(key).push(i);
1137
+ }
1138
+
1139
+ for (let i = 0; i < nodes.length; i++) {
1140
+ let a = nodes[i];
1141
+ let gx = Math.floor(a.x / cellW);
1142
+ let gy = Math.floor(a.y / cellH);
1143
+ for (let dx = -1; dx <= 1; dx++) {
1144
+ for (let dy = -1; dy <= 1; dy++) {
1145
+ let neighbors = grid.get(`${gx + dx},${gy + dy}`);
1146
+ if (!neighbors) continue;
1147
+ for (const j of neighbors) {
1148
+ if (j <= i) continue;
1149
+ let b = nodes[j];
1150
+ let ddx = b.x - a.x;
1151
+ let ddy = b.y - a.y;
1152
+ let limitX = (a.w + b.w) / 2;
1153
+ let limitY = (a.h + b.h) / 2;
1154
+ if (Math.abs(ddx) < limitX && Math.abs(ddy) < limitY) {
1155
+ let len = Math.sqrt(ddx * ddx + ddy * ddy);
1156
+ if (len === 0) {
1157
+ ddx = Math.random() - 0.5;
1158
+ ddy = Math.random() - 0.5;
1159
+ len = Math.sqrt(ddx * ddx + ddy * ddy) || 1;
1160
+ }
1161
+ let push = 2 / len;
1162
+ a.vx -= ddx * push;
1163
+ b.vx += ddx * push;
1164
+ a.vy -= ddy * push;
1165
+ b.vy += ddy * push;
1166
+ }
1167
+ }
1168
+ }
1169
+ }
1170
+ }
1171
+
1172
+ let decay = 0.8;
1173
+ for (const n of nodes) {
1174
+ n.vx *= decay;
1175
+ n.vy *= decay;
1176
+ if (n.vx > 10) n.vx = 10;
1177
+ else if (n.vx < -10) n.vx = -10;
1178
+ if (n.vy > 10) n.vy = 10;
1179
+ else if (n.vy < -10) n.vy = -10;
1180
+ n.x += n.vx;
1181
+ n.y += n.vy;
1182
+ }
1183
+
1184
+ overlaps = countOverlaps(nodes);
1185
+ attempt++;
1186
+ bIter++;
1187
+ }
1188
+
1189
+ if (overlaps > 0 && attempt < maxExpansionAttempts) {
1190
+ self.postMessage({
1191
+ type: 'tick',
1192
+ positions: getPositions(),
1193
+ energy: 0,
1194
+ iteration: iteration + attempt,
1195
+ overlaps,
1196
+ });
1197
+ setTimeout(runExpansionBatch, 0);
1198
+ } else {
1199
+ running = false;
1200
+ self.postMessage({
1201
+ type: 'done',
1202
+ positions: getPositions(),
1203
+ iterations: iteration + attempt,
1204
+ });
1205
+ }
1206
+ }
1207
+
1208
+ runExpansionBatch();
1209
+ }
1210
+ }
1211
+
1212
+ runBatch();
1213
+ }
1214
+
1215
+
1216
+ let continuousAlpha = 1;
1217
+ let continuousIteration = 0;
1218
+
1219
+ function startContinuous() {
1220
+ continuousAlpha = 1;
1221
+ continuousIteration = 0;
1222
+ self._initialDoneSent = false;
1223
+
1224
+
1225
+ self.postMessage({ type: 'nodeIds', ids: getNodeIds() });
1226
+
1227
+ startContinuousLoop();
1228
+ }
1229
+
1230
+ function startContinuousLoop() {
1231
+ if (continuousTimer !== null) return;
1232
+
1233
+ function runTick() {
1234
+ if (!running || paused) {
1235
+ continuousTimer = null;
1236
+ return;
1237
+ }
1238
+
1239
+
1240
+ let energy = tick(continuousAlpha);
1241
+
1242
+
1243
+ if (config.brownian > 0 && continuousAlpha < config.brownianThresh) {
1244
+ let bStr = config.brownian;
1245
+ for (const n of nodes) {
1246
+ if (n.fx === undefined) n.vx += (Math.random() - 0.5) * bStr;
1247
+ if (n.fy === undefined) n.vy += (Math.random() - 0.5) * bStr;
1248
+ }
1249
+ }
1250
+
1251
+
1252
+ continuousAlpha += (config.contAlphaTarget - continuousAlpha) * config.alphaDecay;
1253
+ if (continuousAlpha < config.contAlphaFloor) continuousAlpha = config.contAlphaFloor;
1254
+
1255
+
1256
+ if (continuousAlpha < config.contAlphaTarget + 0.001 && config.brownian === 0) {
1257
+ for (const n of nodes) {
1258
+ n.vx *= 0.5;
1259
+ n.vy *= 0.5;
1260
+ }
1261
+ }
1262
+
1263
+ continuousIteration++;
1264
+
1265
+
1266
+ let packed = getPositionsPacked();
1267
+ self.postMessage(
1268
+ {
1269
+ type: 'tick',
1270
+ packed: packed.buffer,
1271
+ alpha: continuousAlpha,
1272
+ energy: energy,
1273
+ iteration: continuousIteration,
1274
+ },
1275
+ [packed.buffer],
1276
+ );
1277
+
1278
+
1279
+ if (!self._initialDoneSent && Math.abs(continuousAlpha - config.contAlphaTarget) < 0.05) {
1280
+ self._initialDoneSent = true;
1281
+ self.postMessage({
1282
+ type: 'done',
1283
+ positions: getPositions(),
1284
+ iterations: continuousIteration,
1285
+ });
1286
+ }
1287
+
1288
+
1289
+ if (
1290
+ Math.abs(continuousAlpha - config.contAlphaTarget) < 1e-4 &&
1291
+ energy < nodes.length * 0.01 &&
1292
+ config.brownian === 0
1293
+ ) {
1294
+ paused = true;
1295
+ continuousTimer = null;
1296
+ return;
1297
+ }
1298
+
1299
+ continuousTimer = setTimeout(runTick, 16);
1300
+ }
1301
+
1302
+ runTick();
1303
+ }