project-graph-mcp 2.3.2 → 2.4.0

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 (279) hide show
  1. package/package.json +3 -2
  2. package/src/analysis/analysis-cache.ctx +9 -0
  3. package/src/analysis/analysis-cache.js +1 -1
  4. package/src/analysis/complexity.ctx +6 -0
  5. package/src/analysis/complexity.js +1 -1
  6. package/src/analysis/custom-rules.ctx +14 -0
  7. package/src/analysis/custom-rules.js +1 -1
  8. package/src/analysis/db-analysis.ctx +7 -0
  9. package/src/analysis/db-analysis.js +1 -1
  10. package/src/analysis/dead-code.ctx +6 -0
  11. package/src/analysis/dead-code.js +1 -1
  12. package/src/analysis/full-analysis.ctx +9 -0
  13. package/src/analysis/full-analysis.js +1 -1
  14. package/src/analysis/jsdoc-checker.ctx +10 -0
  15. package/src/analysis/jsdoc-checker.js +1 -1
  16. package/src/analysis/jsdoc-generator.ctx +9 -0
  17. package/src/analysis/jsdoc-generator.js +1 -1
  18. package/src/analysis/large-files.ctx +6 -0
  19. package/src/analysis/large-files.js +1 -1
  20. package/src/analysis/outdated-patterns.ctx +7 -0
  21. package/src/analysis/outdated-patterns.js +1 -1
  22. package/src/analysis/similar-functions.ctx +6 -0
  23. package/src/analysis/similar-functions.js +1 -1
  24. package/src/analysis/test-annotations.ctx +11 -0
  25. package/src/analysis/test-annotations.js +1 -1
  26. package/src/analysis/type-checker.ctx +6 -0
  27. package/src/analysis/type-checker.js +1 -1
  28. package/src/analysis/undocumented.ctx +8 -0
  29. package/src/analysis/undocumented.js +1 -1
  30. package/src/cli/cli-handlers.ctx +7 -0
  31. package/src/cli/cli-handlers.js +1 -1
  32. package/src/cli/cli.ctx +6 -0
  33. package/src/cli/cli.js +1 -1
  34. package/src/compact/ai-context.ctx +6 -0
  35. package/src/compact/ai-context.js +1 -1
  36. package/src/compact/compact-migrate.ctx +8 -0
  37. package/src/compact/compact-migrate.js +1 -1
  38. package/src/compact/compact.ctx +11 -0
  39. package/src/compact/compact.js +1 -1
  40. package/src/compact/compress.ctx +7 -0
  41. package/src/compact/compress.js +1 -1
  42. package/src/compact/ctx-resolver.ctx +2 -0
  43. package/src/compact/ctx-resolver.js +1 -1
  44. package/src/compact/ctx-to-jsdoc.ctx +11 -0
  45. package/src/compact/ctx-to-jsdoc.js +1 -1
  46. package/src/compact/doc-dialect.ctx +11 -0
  47. package/src/compact/doc-dialect.js +2 -2
  48. package/src/compact/expand.ctx +14 -0
  49. package/src/compact/expand.js +1 -1
  50. package/src/compact/framework-references.ctx +7 -0
  51. package/src/compact/framework-references.js +1 -1
  52. package/src/compact/instructions.ctx +6 -0
  53. package/src/compact/instructions.js +1 -1
  54. package/src/compact/jsdoc-builder.ctx +4 -0
  55. package/src/compact/jsdoc-builder.js +1 -1
  56. package/src/compact/mode-config.ctx +8 -0
  57. package/src/compact/mode-config.js +1 -1
  58. package/src/compact/split-declarations.ctx +6 -0
  59. package/src/compact/split-declarations.js +1 -1
  60. package/src/compact/validate-pipeline.ctx +12 -0
  61. package/src/compact/validate-pipeline.js +1 -1
  62. package/src/core/event-bus.ctx +9 -0
  63. package/src/core/event-bus.js +1 -1
  64. package/src/core/file-walker.ctx +1 -0
  65. package/src/core/file-walker.js +1 -1
  66. package/src/core/filters.ctx +12 -0
  67. package/src/core/filters.js +1 -1
  68. package/src/core/graph-builder.ctx +7 -0
  69. package/src/core/graph-builder.js +1 -1
  70. package/src/core/parser.ctx +12 -0
  71. package/src/core/parser.js +1 -1
  72. package/src/core/utils.ctx +1 -0
  73. package/src/core/utils.js +1 -1
  74. package/src/core/workspace.ctx +7 -0
  75. package/src/core/workspace.js +1 -1
  76. package/src/lang/lang-go.ctx +8 -0
  77. package/src/lang/lang-go.js +1 -1
  78. package/src/lang/lang-python.ctx +5 -0
  79. package/src/lang/lang-python.js +1 -1
  80. package/src/lang/lang-sql.ctx +10 -0
  81. package/src/lang/lang-sql.js +1 -1
  82. package/src/lang/lang-typescript.ctx +6 -0
  83. package/src/lang/lang-typescript.js +1 -1
  84. package/src/lang/lang-utils.ctx +5 -0
  85. package/src/lang/lang-utils.js +1 -1
  86. package/src/mcp/mcp-server.ctx +6 -0
  87. package/src/mcp/mcp-server.js +1 -1
  88. package/src/mcp/tool-defs.ctx +2 -0
  89. package/src/mcp/tool-defs.js +1 -1
  90. package/src/mcp/tools.ctx +13 -0
  91. package/src/mcp/tools.js +1 -1
  92. package/src/network/backend-lifecycle.ctx +10 -0
  93. package/src/network/backend-lifecycle.js +1 -1
  94. package/src/network/backend.ctx +5 -0
  95. package/src/network/backend.js +1 -1
  96. package/src/network/local-gateway.ctx +9 -0
  97. package/src/network/local-gateway.js +1 -1
  98. package/src/network/mdns.ctx +6 -0
  99. package/src/network/mdns.js +1 -1
  100. package/src/network/server.ctx +2 -0
  101. package/src/network/server.js +2 -2
  102. package/src/network/web-server.ctx +17 -0
  103. package/src/network/web-server.js +2 -2
  104. package/web/follow-controller.js +94 -25
  105. package/web/panels/dep-graph.js +207 -21
  106. package/project-graph-mcp-2.3.0.tgz +0 -0
  107. package/vendor/symbiote-node/CHANGELOG.md +0 -31
  108. package/vendor/symbiote-node/LICENSE +0 -21
  109. package/vendor/symbiote-node/README.md +0 -206
  110. package/vendor/symbiote-node/canvas/AutoLayout.js +0 -725
  111. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +0 -73
  112. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +0 -93
  113. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +0 -9
  114. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +0 -962
  115. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +0 -1468
  116. package/vendor/symbiote-node/canvas/FlowSimulator.js +0 -323
  117. package/vendor/symbiote-node/canvas/ForceLayout.js +0 -189
  118. package/vendor/symbiote-node/canvas/ForceWorker.js +0 -1325
  119. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +0 -97
  120. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +0 -176
  121. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +0 -12
  122. package/vendor/symbiote-node/canvas/LODManager.js +0 -88
  123. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +0 -71
  124. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +0 -207
  125. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +0 -9
  126. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +0 -261
  127. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +0 -1840
  128. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +0 -22
  129. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +0 -97
  130. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +0 -132
  131. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +0 -21
  132. package/vendor/symbiote-node/canvas/NodeViewManager.js +0 -584
  133. package/vendor/symbiote-node/canvas/PinExpansion.js +0 -131
  134. package/vendor/symbiote-node/canvas/PseudoConnection.js +0 -80
  135. package/vendor/symbiote-node/canvas/SubgraphManager.js +0 -201
  136. package/vendor/symbiote-node/canvas/SubgraphRouter.js +0 -443
  137. package/vendor/symbiote-node/canvas/ViewportActions.js +0 -446
  138. package/vendor/symbiote-node/core/Connection.js +0 -45
  139. package/vendor/symbiote-node/core/Editor.js +0 -451
  140. package/vendor/symbiote-node/core/Frame.js +0 -31
  141. package/vendor/symbiote-node/core/GraphMermaid.js +0 -348
  142. package/vendor/symbiote-node/core/GraphText.js +0 -210
  143. package/vendor/symbiote-node/core/Node.js +0 -143
  144. package/vendor/symbiote-node/core/Portal.js +0 -104
  145. package/vendor/symbiote-node/core/Socket.js +0 -185
  146. package/vendor/symbiote-node/core/SubgraphNode.js +0 -125
  147. package/vendor/symbiote-node/engine/AgentUICommands.js +0 -100
  148. package/vendor/symbiote-node/engine/Executor.js +0 -371
  149. package/vendor/symbiote-node/engine/Graph.js +0 -314
  150. package/vendor/symbiote-node/engine/GraphServer.js +0 -353
  151. package/vendor/symbiote-node/engine/HandlerLoader.js +0 -145
  152. package/vendor/symbiote-node/engine/History.js +0 -83
  153. package/vendor/symbiote-node/engine/Lifecycle.js +0 -118
  154. package/vendor/symbiote-node/engine/Persistence.js +0 -84
  155. package/vendor/symbiote-node/engine/Registry.js +0 -264
  156. package/vendor/symbiote-node/engine/SocketTypes.js +0 -79
  157. package/vendor/symbiote-node/engine/cli.js +0 -404
  158. package/vendor/symbiote-node/engine/index.js +0 -56
  159. package/vendor/symbiote-node/engine/nanoid.js +0 -28
  160. package/vendor/symbiote-node/engine/package.json +0 -26
  161. package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +0 -215
  162. package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +0 -238
  163. package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +0 -287
  164. package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +0 -565
  165. package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +0 -414
  166. package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +0 -343
  167. package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +0 -164
  168. package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +0 -341
  169. package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +0 -241
  170. package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +0 -191
  171. package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +0 -67
  172. package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +0 -281
  173. package/vendor/symbiote-node/engine/packs/data/personas.handler.js +0 -160
  174. package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +0 -193
  175. package/vendor/symbiote-node/engine/packs/data/roles.handler.js +0 -216
  176. package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +0 -244
  177. package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +0 -52
  178. package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +0 -73
  179. package/vendor/symbiote-node/engine/packs/flow/if.handler.js +0 -107
  180. package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +0 -58
  181. package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +0 -60
  182. package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +0 -65
  183. package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +0 -64
  184. package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +0 -39
  185. package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +0 -82
  186. package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +0 -60
  187. package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +0 -63
  188. package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +0 -494
  189. package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +0 -417
  190. package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +0 -43
  191. package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +0 -339
  192. package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +0 -432
  193. package/vendor/symbiote-node/engine/packs/transform/set.handler.js +0 -57
  194. package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +0 -134
  195. package/vendor/symbiote-node/engine/packs/transform/template.handler.js +0 -79
  196. package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +0 -399
  197. package/vendor/symbiote-node/engine/packs/util/delay.handler.js +0 -39
  198. package/vendor/symbiote-node/engine/packs/util/log.handler.js +0 -44
  199. package/vendor/symbiote-node/engine/packs/video-pack.js +0 -323
  200. package/vendor/symbiote-node/index.js +0 -103
  201. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +0 -361
  202. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +0 -332
  203. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +0 -96
  204. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +0 -104
  205. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +0 -133
  206. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +0 -33
  207. package/vendor/symbiote-node/interactions/ConnectFlow.js +0 -307
  208. package/vendor/symbiote-node/interactions/Drag.js +0 -102
  209. package/vendor/symbiote-node/interactions/Selector.js +0 -132
  210. package/vendor/symbiote-node/interactions/SnapGrid.js +0 -65
  211. package/vendor/symbiote-node/interactions/Zoom.js +0 -140
  212. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +0 -88
  213. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +0 -254
  214. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +0 -11
  215. package/vendor/symbiote-node/layout/Layout/Layout.css.js +0 -88
  216. package/vendor/symbiote-node/layout/Layout/Layout.js +0 -622
  217. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +0 -25
  218. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +0 -293
  219. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +0 -467
  220. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +0 -33
  221. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +0 -46
  222. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +0 -102
  223. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +0 -6
  224. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +0 -156
  225. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +0 -250
  226. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +0 -379
  227. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +0 -263
  228. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +0 -20
  229. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +0 -183
  230. package/vendor/symbiote-node/layout/LayoutTree.js +0 -246
  231. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +0 -43
  232. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +0 -89
  233. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +0 -14
  234. package/vendor/symbiote-node/layout/index.js +0 -16
  235. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +0 -61
  236. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +0 -79
  237. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +0 -19
  238. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +0 -41
  239. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +0 -24
  240. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +0 -16
  241. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +0 -65
  242. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +0 -29
  243. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +0 -13
  244. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +0 -683
  245. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +0 -92
  246. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +0 -17
  247. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +0 -25
  248. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +0 -7
  249. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +0 -90
  250. package/vendor/symbiote-node/node/PortItem/PortItem.js +0 -87
  251. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +0 -10
  252. package/vendor/symbiote-node/package.json +0 -59
  253. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +0 -143
  254. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +0 -131
  255. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +0 -16
  256. package/vendor/symbiote-node/plugins/History.js +0 -384
  257. package/vendor/symbiote-node/plugins/Readonly.js +0 -59
  258. package/vendor/symbiote-node/shapes/CircleShape.js +0 -80
  259. package/vendor/symbiote-node/shapes/CommentShape.js +0 -35
  260. package/vendor/symbiote-node/shapes/DiamondShape.js +0 -115
  261. package/vendor/symbiote-node/shapes/NodeShape.js +0 -80
  262. package/vendor/symbiote-node/shapes/PillShape.js +0 -91
  263. package/vendor/symbiote-node/shapes/RectShape.js +0 -72
  264. package/vendor/symbiote-node/shapes/SVGShape.js +0 -494
  265. package/vendor/symbiote-node/shapes/index.js +0 -53
  266. package/vendor/symbiote-node/themes/Palette.js +0 -32
  267. package/vendor/symbiote-node/themes/Skin.js +0 -113
  268. package/vendor/symbiote-node/themes/Theme.js +0 -84
  269. package/vendor/symbiote-node/themes/carbon.js +0 -137
  270. package/vendor/symbiote-node/themes/dark.js +0 -137
  271. package/vendor/symbiote-node/themes/ebook.js +0 -138
  272. package/vendor/symbiote-node/themes/grey.js +0 -137
  273. package/vendor/symbiote-node/themes/light.js +0 -137
  274. package/vendor/symbiote-node/themes/neon.js +0 -138
  275. package/vendor/symbiote-node/themes/pcb.js +0 -273
  276. package/vendor/symbiote-node/themes/synthwave.js +0 -137
  277. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +0 -86
  278. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +0 -128
  279. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +0 -29
@@ -1,725 +0,0 @@
1
- /**
2
- * AutoLayout — Macro-Micro hierarchical graph layout
3
- *
4
- * Employs a 2-level strategy:
5
- * 1. Micro-Layout: Sugiyama-style layering with per-node dimensions.
6
- * 2. Macro-Layout: Radial Hub-and-Spoke spiraling to pack Group Bounds.
7
- *
8
- * Features (v2):
9
- * - Per-node width/height via `nodeSizes` map
10
- * - Bi-directional crossing minimization (forward + backward sweeps)
11
- * - Per-layer X offset based on actual max node width
12
- * - Per-node height-aware overlap resolution
13
- * - Layout direction: 'LR' (left-right) or 'TB' (top-bottom)
14
- *
15
- * @module symbiote-node/canvas/AutoLayout
16
- */
17
-
18
- export function computeAutoLayout(editor, options = {}) {
19
- const perfId = 'AutoLayout-' + Math.random().toString(36).slice(2, 6);
20
- console.time(perfId);
21
- let cycleCount = 0;
22
-
23
- const {
24
- nodeWidth = 180,
25
- nodeHeight = 140,
26
- gapX = 60,
27
- gapY = 30,
28
- startX = 60,
29
- startY = 60,
30
- crossingPasses = 4,
31
- existingPositions = null,
32
- groups = null, // { [groupId]: [nodeId, ...] }
33
- nodeSizes = null, // { [nodeId]: { w, h } } — per-node dimensions
34
- direction = 'LR' // 'LR' or 'TB'
35
- } = options;
36
-
37
- // Per-node dimension resolver with fallback to global defaults
38
- function getSize(nodeId) {
39
- if (nodeSizes && nodeSizes[nodeId]) {
40
- // Use measured size, but enforce minimum dimensions (DOM might not be fully rendered)
41
- return {
42
- w: Math.max(nodeSizes[nodeId].w, nodeWidth),
43
- h: Math.max(nodeSizes[nodeId].h, nodeHeight),
44
- };
45
- }
46
- return { w: nodeWidth, h: nodeHeight };
47
- }
48
-
49
- const nodes = [...editor.getNodes()];
50
- const connections = [...editor.getConnections()];
51
- if (nodes.length === 0) return {};
52
-
53
- const outgoing = new Map();
54
- const incoming = new Map();
55
- for (const node of nodes) {
56
- outgoing.set(node.id, []);
57
- incoming.set(node.id, []);
58
- }
59
-
60
- for (const conn of connections) {
61
- const from = conn.from;
62
- const to = conn.to;
63
- if (outgoing.has(from) && incoming.has(to)) {
64
- outgoing.get(from).push(to);
65
- incoming.get(to).push(from);
66
- }
67
- }
68
-
69
- // --- 1. Partition into Groups ---
70
- const nodeGroupId = new Map();
71
- const groupNodes = new Map();
72
- if (groups) {
73
- for (const [gId, gNodes] of Object.entries(groups)) {
74
- groupNodes.set(gId, []);
75
- for (const n of gNodes) {
76
- nodeGroupId.set(n, gId);
77
- }
78
- }
79
- }
80
- for (const n of nodes) {
81
- let gId = nodeGroupId.get(n.id);
82
- if (!gId) {
83
- gId = '__root__';
84
- nodeGroupId.set(n.id, gId);
85
- }
86
- if (!groupNodes.has(gId)) groupNodes.set(gId, []);
87
- groupNodes.get(gId).push(n.id);
88
- }
89
-
90
- // Calculate inter-group connections
91
- const groupCrossLinks = new Map();
92
- const groupDegrees = new Map();
93
- for (const gId of groupNodes.keys()) {
94
- groupDegrees.set(gId, { in: 0, out: 0, total: 0 });
95
- groupCrossLinks.set(gId, { incoming: new Map(), outgoing: new Map() });
96
- }
97
-
98
- for (const [fromId, targets] of outgoing.entries()) {
99
- const gFrom = nodeGroupId.get(fromId);
100
- for (const toId of targets) {
101
- const gTo = nodeGroupId.get(toId);
102
- if (gFrom !== gTo) {
103
- groupDegrees.get(gFrom).out++;
104
- groupDegrees.get(gFrom).total++;
105
- groupDegrees.get(gTo).in++;
106
- groupDegrees.get(gTo).total++;
107
-
108
- const outMap = groupCrossLinks.get(gFrom).outgoing;
109
- outMap.set(gTo, (outMap.get(gTo) || 0) + 1);
110
-
111
- const inMap = groupCrossLinks.get(gTo).incoming;
112
- inMap.set(gFrom, (inMap.get(gFrom) || 0) + 1);
113
- }
114
- }
115
- }
116
-
117
- // Identify center hub group
118
- let centerGroup = null;
119
- let maxCross = -1;
120
- for (const [gId, deg] of groupDegrees.entries()) {
121
- if (deg.total > maxCross || (deg.total === maxCross && gId === './')) {
122
- maxCross = deg.total;
123
- centerGroup = gId;
124
- }
125
- }
126
-
127
- // --- 2. Micro Layout Function ---
128
- // Sugiyama-style LTR layering with per-node dimensions
129
- function computeMicroLayout(gId, subNodes) {
130
- const finalOut = new Map();
131
- const internalDegree = new Map();
132
- for (const n of subNodes) {
133
- finalOut.set(n, []);
134
- internalDegree.set(n, 0);
135
- }
136
-
137
- // Calculate accurate internal degree
138
- for (const n of subNodes) {
139
- for (const child of outgoing.get(n) || []) {
140
- if (finalOut.has(child)) {
141
- internalDegree.set(n, internalDegree.get(n) + 1);
142
- internalDegree.set(child, internalDegree.get(child) + 1);
143
- }
144
- }
145
- }
146
-
147
- // Partition into Linked vs Isolated
148
- const linkedNodes = [];
149
- const isolatedNodes = [];
150
- for (const n of subNodes) {
151
- if (internalDegree.get(n) === 0) isolatedNodes.push(n);
152
- else linkedNodes.push(n);
153
- }
154
-
155
- const localPositions = {};
156
- let maxLinkedW = 0, maxLinkedH = 0;
157
-
158
- // --- Linked Subgraph Layout ---
159
- if (linkedNodes.length > 0) {
160
- const state = new Map();
161
- for (const n of linkedNodes) state.set(n, 0);
162
-
163
- function dfs(nId) {
164
- state.set(nId, 1);
165
- for (const child of outgoing.get(nId) || []) {
166
- if (!finalOut.has(child)) continue; // ignore cross-group
167
- if (state.get(child) === 1) continue;
168
- finalOut.get(nId).push(child);
169
- if (state.get(child) === 0) dfs(child);
170
- }
171
- state.set(nId, 2);
172
- }
173
- for (const n of linkedNodes) {
174
- if (state.get(n) === 0) dfs(n);
175
- }
176
-
177
- const layers = new Map();
178
- for (const n of linkedNodes) layers.set(n, 0);
179
-
180
- for (let i = 0; i < linkedNodes.length; i++) {
181
- let changed = false;
182
- for (const n of linkedNodes) {
183
- const cur = layers.get(n);
184
- for (const child of finalOut.get(n)) {
185
- if (layers.get(child) < cur + 1) {
186
- layers.set(child, cur + 1);
187
- changed = true;
188
- }
189
- }
190
- }
191
- if (!changed) break;
192
- }
193
-
194
- let minL = Infinity, maxL = -Infinity;
195
- for (const n of linkedNodes) {
196
- const l = layers.get(n);
197
- if (l < minL) minL = l;
198
- if (l > maxL) maxL = l;
199
- }
200
- if (minL === Infinity) { minL = 0; maxL = 0; }
201
-
202
- const layerArr = [];
203
- for (let l = 0; l <= (maxL - minL); l++) layerArr.push([]);
204
- for (const n of linkedNodes) layerArr[layers.get(n) - minL].push(n);
205
-
206
- // --- Per-node height-aware Y positioning ---
207
- const yPos = new Map();
208
- for (let l = 0; l < layerArr.length; l++) {
209
- let curY = 0;
210
- for (let i = 0; i < layerArr[l].length; i++) {
211
- yPos.set(layerArr[l][i], curY);
212
- curY += getSize(layerArr[l][i]).h + gapY;
213
- }
214
- }
215
-
216
- // Per-node height-aware overlap resolution
217
- function resolveOverlaps(layer, yMap) {
218
- if (layer.length === 0) return;
219
- // Forward sweep: ensure each node starts after previous node ends
220
- for (let i = 1; i < layer.length; i++) {
221
- const prevId = layer[i - 1];
222
- const curId = layer[i];
223
- const prevBottom = yMap.get(prevId) + getSize(prevId).h + gapY;
224
- if (yMap.get(curId) < prevBottom) {
225
- yMap.set(curId, prevBottom);
226
- }
227
- }
228
- // Backward sweep: pull nodes up if there's slack
229
- for (let i = layer.length - 2; i >= 0; i--) {
230
- const curId = layer[i];
231
- const nextId = layer[i + 1];
232
- const maxY = yMap.get(nextId) - getSize(curId).h - gapY;
233
- if (yMap.get(curId) > maxY) {
234
- yMap.set(curId, maxY);
235
- }
236
- }
237
- }
238
-
239
- // --- Bi-directional crossing minimization ---
240
- // Uses the declared crossingPasses parameter (was unused before)
241
- for (let pass = 0; pass < crossingPasses; pass++) {
242
- // Forward sweep: layer 1 → last
243
- for (let l = 1; l < layerArr.length; l++) {
244
- for (let i = 0; i < layerArr[l].length; i++) {
245
- const node = layerArr[l][i];
246
- const parents = (incoming.get(node) || []).filter(n => layerArr[l - 1].includes(n));
247
- if (parents.length > 0) {
248
- parents.sort((a, b) => yPos.get(a) - yPos.get(b));
249
- const mid = Math.floor(parents.length / 2);
250
- let tY = yPos.get(parents[mid]);
251
- if (parents.length % 2 === 0) tY = (yPos.get(parents[mid - 1]) + yPos.get(parents[mid])) / 2;
252
- yPos.set(node, tY);
253
- }
254
- }
255
- resolveOverlaps(layerArr[l], yPos);
256
- }
257
- // Backward sweep: last layer → layer 1
258
- for (let l = layerArr.length - 2; l >= 0; l--) {
259
- for (let i = 0; i < layerArr[l].length; i++) {
260
- const node = layerArr[l][i];
261
- const children = (finalOut.get(node) || []).filter(n => layerArr[l + 1].includes(n));
262
- if (children.length > 0) {
263
- children.sort((a, b) => yPos.get(a) - yPos.get(b));
264
- const mid = Math.floor(children.length / 2);
265
- let tY = yPos.get(children[mid]);
266
- if (children.length % 2 === 0) tY = (yPos.get(children[mid - 1]) + yPos.get(children[mid])) / 2;
267
- yPos.set(node, tY);
268
- }
269
- }
270
- resolveOverlaps(layerArr[l], yPos);
271
- }
272
- }
273
-
274
- let minLocalY = Infinity, maxLocalY = -Infinity;
275
- for (const [nId, y] of yPos.entries()) {
276
- if (y < minLocalY) minLocalY = y;
277
- const bottom = y + getSize(nId).h;
278
- if (bottom > maxLocalY) maxLocalY = bottom;
279
- }
280
- if (minLocalY === Infinity) { minLocalY = 0; maxLocalY = 0; }
281
-
282
- // --- Per-layer X offset based on max node width ---
283
- // Each layer's X position accounts for the widest node in the previous layer
284
- const layerXOffsets = [];
285
- let xAccum = 0;
286
- for (let l = 0; l < layerArr.length; l++) {
287
- layerXOffsets.push(xAccum);
288
- // Find the widest node in this layer
289
- let maxW = 0;
290
- for (const node of layerArr[l]) {
291
- const nw = getSize(node).w;
292
- if (nw > maxW) maxW = nw;
293
- }
294
- xAccum += maxW + gapX;
295
- }
296
-
297
- for (let l = 0; l < layerArr.length; l++) {
298
- for (const node of layerArr[l]) {
299
- localPositions[node] = {
300
- x: layerXOffsets[l],
301
- y: yPos.get(node) - minLocalY
302
- };
303
- }
304
- }
305
-
306
- maxLinkedW = xAccum;
307
- maxLinkedH = (maxLocalY - minLocalY) + gapY;
308
- }
309
-
310
- // --- Isolated Subgraph Layout (Grid Wrap) ---
311
- // Uses per-node dimensions for row/column sizing
312
- let isolatedW = 0, isolatedH = 0;
313
- if (isolatedNodes.length > 0) {
314
- const MAX_COLS = 6;
315
- // Calculate column widths and row heights based on actual node sizes
316
- const colWidths = [];
317
- const rowHeights = [];
318
- for (let i = 0; i < isolatedNodes.length; i++) {
319
- const col = i % MAX_COLS;
320
- const row = Math.floor(i / MAX_COLS);
321
- const size = getSize(isolatedNodes[i]);
322
- if (!colWidths[col] || size.w > colWidths[col]) colWidths[col] = size.w;
323
- if (!rowHeights[row] || size.h > rowHeights[row]) rowHeights[row] = size.h;
324
- }
325
-
326
- // Compute cumulative X offsets per column
327
- const colX = [0];
328
- for (let c = 1; c < colWidths.length; c++) {
329
- colX[c] = colX[c - 1] + (colWidths[c - 1] || nodeWidth) + gapX;
330
- }
331
- // Compute cumulative Y offsets per row
332
- const rowY = [0];
333
- for (let r = 1; r < rowHeights.length; r++) {
334
- rowY[r] = rowY[r - 1] + (rowHeights[r - 1] || nodeHeight) + gapY;
335
- }
336
-
337
- for (let i = 0; i < isolatedNodes.length; i++) {
338
- const node = isolatedNodes[i];
339
- const col = i % MAX_COLS;
340
- const row = Math.floor(i / MAX_COLS);
341
-
342
- localPositions[node] = {
343
- x: colX[col] || 0,
344
- y: maxLinkedH + (rowY[row] || 0)
345
- };
346
- }
347
-
348
- const lastCol = Math.min(isolatedNodes.length, MAX_COLS) - 1;
349
- const lastRow = rowHeights.length - 1;
350
- isolatedW = (colX[lastCol] || 0) + (colWidths[lastCol] || nodeWidth) + gapX;
351
- isolatedH = (rowY[lastRow] || 0) + (rowHeights[lastRow] || nodeHeight) + gapY;
352
- }
353
-
354
- const w = Math.max(maxLinkedW, isolatedW || (nodeWidth + gapX));
355
- const h = maxLinkedH + isolatedH;
356
-
357
- return { localPositions, bounds: { w, h } };
358
- }
359
-
360
-
361
- // --- 3. Run Micro Layout for all groups ---
362
- const groupResults = new Map();
363
- for (const [gId, subNodes] of groupNodes.entries()) {
364
- groupResults.set(gId, computeMicroLayout(gId, subNodes));
365
- }
366
-
367
- // --- 4. Macro Layout (Vector Radial Packing) ---
368
- const M_PI = Math.PI;
369
- const macroPositions = new Map(); // gId -> {x, y}
370
- const placedRects = [];
371
-
372
- function hitTest(r1, r2, padding = 40) {
373
- return !(r2.x >= r1.x + r1.w + padding ||
374
- r2.x + r2.w + padding <= r1.x ||
375
- r2.y >= r1.y + r1.h + padding ||
376
- r2.y + r2.h + padding <= r1.y);
377
- }
378
-
379
- function placeGroup(gId) {
380
- const res = groupResults.get(gId);
381
- let prefAngle = 0; // default East
382
-
383
- // Calculate preferred vector based on connections to ALREADY placed groups
384
- let vecX = 0, vecY = 0;
385
- const links = groupCrossLinks.get(gId);
386
- for (const p of placedRects) {
387
- const pId = p.id;
388
- const toPlaced = links.outgoing.get(pId) || 0; // I export to Placed -> I want to be West of Placed
389
- const fromPlaced = links.incoming.get(pId) || 0; // Placed exports to me -> I want to be East of Placed
390
-
391
- const netForce = fromPlaced - toPlaced; // > 0 goes right, < 0 goes left
392
- if (netForce !== 0) {
393
- // Find angle toward placed center
394
- const cx = p.x + p.w / 2;
395
- const cy = p.y + p.h / 2;
396
- // Apply force outward
397
- vecX += Math.cos(Math.atan2(cy, cx)) * netForce;
398
- vecY += Math.sin(Math.atan2(cy, cx)) * netForce;
399
- }
400
- }
401
- if (vecX !== 0 || vecY !== 0) prefAngle = Math.atan2(vecY, vecX);
402
-
403
- // Dynamic step based on group size — large groups skip faster
404
- let step = Math.max(20, Math.min(res.bounds.w, res.bounds.h) * 0.2);
405
- let maxR = 6000;
406
- const angularStep = M_PI / 12; // 24 angles for finer placement
407
- for (let r = 0; r < maxR; r += step) {
408
- for (let delta = 0; delta <= M_PI; delta += angularStep) {
409
- for (const sign of [1, -1]) {
410
- cycleCount++;
411
- let a = prefAngle + delta * sign;
412
- let x = Math.round(Math.cos(a) * r);
413
- let y = Math.round(Math.sin(a) * r);
414
-
415
- let rect = { x, y, w: res.bounds.w, h: res.bounds.h, id: gId };
416
- let overlap = false;
417
- for (const p of placedRects) {
418
- if (hitTest(rect, p)) { overlap = true; break; }
419
- }
420
- if (!overlap) {
421
- macroPositions.set(gId, { x, y });
422
- placedRects.push(rect);
423
- return;
424
- }
425
- if (delta === 0) break;
426
- }
427
- }
428
- // Increase step as we spiral outward (no point checking every 20px at radius 2000)
429
- if (r > 500) step = Math.max(step, 60);
430
- if (r > 1500) step = Math.max(step, 120);
431
- }
432
- // Fallback if packed too tight, just shove it way out
433
- macroPositions.set(gId, { x: placedRects.length * 300, y: placedRects.length * 300 });
434
- placedRects.push({ x: placedRects.length*300, y: placedRects.length*300, w: res.bounds.w, h: res.bounds.h, id: gId });
435
- }
436
-
437
- // Place center hub first
438
- if (centerGroup) {
439
- macroPositions.set(centerGroup, { x: 0, y: 0 });
440
- const cRes = groupResults.get(centerGroup);
441
- placedRects.push({ x: 0, y: 0, w: cRes.bounds.w, h: cRes.bounds.h, id: centerGroup });
442
- }
443
-
444
- // Sort remaining groups by descending total edges to ensure large interconnected clusters are packed tight
445
- const remainingGroups = Array.from(groupNodes.keys()).filter(id => id !== centerGroup);
446
- remainingGroups.sort((a, b) => groupDegrees.get(b).total - groupDegrees.get(a).total);
447
-
448
- for (const gId of remainingGroups) {
449
- placeGroup(gId);
450
- }
451
-
452
- // --- 5. Assemble Final Positions ---
453
- const finalPositions = {};
454
- for (const [gId, res] of groupResults.entries()) {
455
- const macro = macroPositions.get(gId);
456
- for (const [nId, loc] of Object.entries(res.localPositions)) {
457
- finalPositions[nId] = {
458
- x: startX + macro.x + loc.x,
459
- y: startY + macro.y + loc.y
460
- };
461
- }
462
- }
463
-
464
- // --- 6. Direction Transform ---
465
- // If TB (top-bottom), swap x↔y so layers go vertically
466
- if (direction === 'TB') {
467
- for (const id in finalPositions) {
468
- const p = finalPositions[id];
469
- const tmp = p.x;
470
- p.x = p.y;
471
- p.y = tmp;
472
- }
473
- }
474
-
475
- // --- 7. Anchor Stabilization ---
476
- if (existingPositions) {
477
- let sumDx = 0, sumDy = 0, count = 0;
478
- for (const [id, oldPos] of Object.entries(existingPositions)) {
479
- if (finalPositions[id] && !isNaN(oldPos.x) && !isNaN(oldPos.y)) {
480
- sumDx += oldPos.x - finalPositions[id].x;
481
- sumDy += oldPos.y - finalPositions[id].y;
482
- count++;
483
- }
484
- }
485
- if (count > 0) {
486
- const avgDx = sumDx / count;
487
- const avgDy = sumDy / count;
488
- for (const id in finalPositions) {
489
- finalPositions[id].x += avgDx;
490
- finalPositions[id].y += avgDy;
491
- }
492
-
493
- // Post-anchor overlap resolution using per-node dimensions
494
- const ids = Object.keys(finalPositions);
495
- for (let pass = 0; pass < 3; pass++) {
496
- let overlaps = false;
497
- cycleCount++;
498
- for (let i = 0; i < ids.length; i++) {
499
- for (let j = i + 1; j < ids.length; j++) {
500
- const p1 = finalPositions[ids[i]];
501
- const p2 = finalPositions[ids[j]];
502
- const s1 = getSize(ids[i]);
503
- const s2 = getSize(ids[j]);
504
- const dx = p1.x - p2.x, dy = p1.y - p2.y;
505
- const absDx = Math.abs(dx), absDy = Math.abs(dy);
506
-
507
- // Check overlap using actual node dimensions
508
- const overlapX = (s1.w + s2.w) / 2 + gapX * 0.3;
509
- const overlapY = (s1.h + s2.h) / 2 + gapY * 0.3;
510
-
511
- if (absDx < overlapX && absDy < overlapY) {
512
- overlaps = true;
513
- // Push apart along the axis with more penetration depth (less distance)
514
- const penX = overlapX - absDx;
515
- const penY = overlapY - absDy;
516
-
517
- if (penX < penY) {
518
- // Less X penetration → push apart on X
519
- const fix = penX / 2 + 1;
520
- p1.x += dx >= 0 ? fix : -fix;
521
- p2.x += dx >= 0 ? -fix : fix;
522
- } else {
523
- // Less Y penetration → push apart on Y
524
- const fix = penY / 2 + 1;
525
- p1.y += dy >= 0 ? fix : -fix;
526
- p2.y += dy >= 0 ? -fix : fix;
527
- }
528
- }
529
- }
530
- }
531
- if (!overlaps) break;
532
- }
533
- }
534
- }
535
-
536
- for (const k in finalPositions) {
537
- if (isNaN(finalPositions[k].x) || isNaN(finalPositions[k].y)) {
538
- console.error("[AutoLayout] NaN intercepted for node:", k);
539
- finalPositions[k] = { x: 0, y: 0 };
540
- }
541
- }
542
-
543
- console.timeEnd(perfId);
544
- console.log(`[AutoLayout] v2 Macro-Micro Groups: ${groupNodes.size}, Nodes: ${nodes.length}, Edges: ${connections.length}`);
545
- console.log(`[AutoLayout] Cycles: ${cycleCount}, crossingPasses: ${crossingPasses}, direction: ${direction}`);
546
-
547
- return finalPositions;
548
- }
549
-
550
-
551
- /**
552
- * Tree Layout — positions nodes like a directory tree / file explorer.
553
- *
554
- * Algorithm: Compact tree (Reingold-Tilford inspired) with per-node dimensions.
555
- * - Builds a tree from either: (a) dirPaths parent-child hierarchy, or (b) DAG edges
556
- * - Positions root at top-left, children below with indentation
557
- * - Sibling subtrees are packed tightly without overlap
558
- * - Supports per-node dimensions via `nodeSizes`
559
- *
560
- * @param {NodeEditor} editor - The node editor
561
- * @param {object} options
562
- * @param {Object<string, { w: number, h: number }>} [options.nodeSizes] - Per-node dimensions
563
- * @param {number} [options.gapX=40] - Horizontal indentation per depth level
564
- * @param {number} [options.gapY=20] - Vertical gap between sibling nodes
565
- * @param {number} [options.nodeWidth=250] - Default node width
566
- * @param {number} [options.nodeHeight=100] - Default node height
567
- * @param {number} [options.startX=60] - Starting X
568
- * @param {number} [options.startY=60] - Starting Y
569
- * @param {Object<string, string>} [options.dirPaths] - { nodeId: dirPath } — enables directory hierarchy detection
570
- * @returns {Object<string, { x: number, y: number }>}
571
- */
572
- export function computeTreeLayout(editor, options = {}) {
573
- const perfId = 'TreeLayout-' + Math.random().toString(36).slice(2, 6);
574
- console.time(perfId);
575
-
576
- const {
577
- gapX = 40,
578
- gapY = 20,
579
- nodeWidth = 250,
580
- nodeHeight = 100,
581
- startX = 60,
582
- startY = 60,
583
- nodeSizes = null,
584
- dirPaths = null, // { nodeId: dirPath } — if provided, uses directory hierarchy
585
- } = options;
586
-
587
- function getSize(nodeId) {
588
- if (nodeSizes && nodeSizes[nodeId]) {
589
- // Use measured size, but enforce minimum dimensions (DOM might not be fully rendered)
590
- return {
591
- w: Math.max(nodeSizes[nodeId].w, nodeWidth),
592
- h: Math.max(nodeSizes[nodeId].h, nodeHeight),
593
- };
594
- }
595
- return { w: nodeWidth, h: nodeHeight };
596
- }
597
-
598
- const nodes = [...editor.getNodes()];
599
- const connections = [...editor.getConnections()];
600
- if (nodes.length === 0) return {};
601
-
602
- // --- Build tree structure ---
603
- // children: Map<nodeId, nodeId[]>
604
- // parent: Map<nodeId, nodeId>
605
- const children = new Map();
606
- const parent = new Map();
607
- const nodeIds = new Set(nodes.map(n => n.id));
608
-
609
- for (const id of nodeIds) {
610
- children.set(id, []);
611
- }
612
-
613
- if (dirPaths) {
614
- // Build tree from directory path hierarchy
615
- // e.g. "src/core/" is child of "src/"
616
- const pathToId = new Map();
617
- for (const [nodeId, path] of Object.entries(dirPaths)) {
618
- pathToId.set(path, nodeId);
619
- }
620
-
621
- // Sort paths by depth (shorter first = parents first)
622
- const sortedPaths = [...pathToId.keys()].sort((a, b) => {
623
- const depthA = a.split('/').filter(Boolean).length;
624
- const depthB = b.split('/').filter(Boolean).length;
625
- return depthA - depthB || a.localeCompare(b);
626
- });
627
-
628
- for (const path of sortedPaths) {
629
- const nodeId = pathToId.get(path);
630
- // Find parent: strip last segment
631
- // "src/core/" → "src/", "vendor/symbiote-node/canvas/" → "vendor/symbiote-node/"
632
- const segments = path.replace(/\/$/, '').split('/');
633
- segments.pop();
634
-
635
- let foundParent = false;
636
- // Walk up the path tree until we find an existing parent
637
- while (segments.length > 0) {
638
- const parentPath = segments.join('/') + '/';
639
- const parentId = pathToId.get(parentPath);
640
- if (parentId && parentId !== nodeId) {
641
- parent.set(nodeId, parentId);
642
- children.get(parentId).push(nodeId);
643
- foundParent = true;
644
- break;
645
- }
646
- segments.pop();
647
- }
648
- // Also try "./" as root
649
- if (!foundParent) {
650
- const rootId = pathToId.get('./');
651
- if (rootId && rootId !== nodeId) {
652
- parent.set(nodeId, rootId);
653
- children.get(rootId).push(nodeId);
654
- }
655
- }
656
- }
657
- } else {
658
- // Build tree from DAG edges (use outgoing connections)
659
- // Simple: treat each connection as parent→child
660
- for (const conn of connections) {
661
- const from = conn.from;
662
- const to = conn.to;
663
- if (nodeIds.has(from) && nodeIds.has(to) && !parent.has(to)) {
664
- parent.set(to, from);
665
- children.get(from).push(to);
666
- }
667
- }
668
- }
669
-
670
- // Find roots (nodes without parents)
671
- const roots = [];
672
- for (const id of nodeIds) {
673
- if (!parent.has(id)) roots.push(id);
674
- }
675
-
676
- // Sort roots: directories first, then files, alphabetically within each group
677
- const nodeMap = new Map(nodes.map(n => [n.id, n]));
678
- const dirIdSet = dirPaths ? new Set(Object.keys(dirPaths)) : new Set();
679
- roots.sort((a, b) => {
680
- const aIsDir = dirIdSet.has(a) || nodeMap.get(a)?._isSubgraph;
681
- const bIsDir = dirIdSet.has(b) || nodeMap.get(b)?._isSubgraph;
682
- if (aIsDir && !bIsDir) return -1;
683
- if (!aIsDir && bIsDir) return 1;
684
- const la = nodeMap.get(a)?.label || '';
685
- const lb = nodeMap.get(b)?.label || '';
686
- return la.localeCompare(lb);
687
- });
688
-
689
- // Sort children alphabetically too
690
- for (const [, kids] of children) {
691
- kids.sort((a, b) => {
692
- const la = nodeMap.get(a)?.label || '';
693
- const lb = nodeMap.get(b)?.label || '';
694
- return la.localeCompare(lb);
695
- });
696
- }
697
-
698
- // --- Compute positions: DFS tree walk ---
699
- const positions = {};
700
- let cursorY = startY;
701
-
702
- function layoutSubtree(nodeId, depth) {
703
- const size = getSize(nodeId);
704
- const x = startX + depth * (gapX + nodeWidth);
705
- const y = cursorY;
706
-
707
- positions[nodeId] = { x, y };
708
- cursorY += size.h + gapY;
709
-
710
- // Layout children below
711
- const kids = children.get(nodeId) || [];
712
- for (const childId of kids) {
713
- layoutSubtree(childId, depth + 1);
714
- }
715
- }
716
-
717
- for (const rootId of roots) {
718
- layoutSubtree(rootId, 0);
719
- }
720
-
721
- console.timeEnd(perfId);
722
- console.log(`[TreeLayout] Nodes: ${nodes.length}, Roots: ${roots.length}, Edges: ${connections.length}`);
723
-
724
- return positions;
725
- }