project-graph-mcp 2.3.1 → 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.
- package/package.json +3 -2
- package/src/analysis/analysis-cache.ctx +9 -0
- package/src/analysis/analysis-cache.js +1 -1
- package/src/analysis/complexity.ctx +6 -0
- package/src/analysis/complexity.js +1 -1
- package/src/analysis/custom-rules.ctx +14 -0
- package/src/analysis/custom-rules.js +1 -1
- package/src/analysis/db-analysis.ctx +7 -0
- package/src/analysis/db-analysis.js +1 -1
- package/src/analysis/dead-code.ctx +6 -0
- package/src/analysis/dead-code.js +1 -1
- package/src/analysis/full-analysis.ctx +9 -0
- package/src/analysis/full-analysis.js +1 -1
- package/src/analysis/jsdoc-checker.ctx +10 -0
- package/src/analysis/jsdoc-checker.js +1 -1
- package/src/analysis/jsdoc-generator.ctx +9 -0
- package/src/analysis/jsdoc-generator.js +1 -1
- package/src/analysis/large-files.ctx +6 -0
- package/src/analysis/large-files.js +1 -1
- package/src/analysis/outdated-patterns.ctx +7 -0
- package/src/analysis/outdated-patterns.js +1 -1
- package/src/analysis/similar-functions.ctx +6 -0
- package/src/analysis/similar-functions.js +1 -1
- package/src/analysis/test-annotations.ctx +11 -0
- package/src/analysis/test-annotations.js +1 -1
- package/src/analysis/type-checker.ctx +6 -0
- package/src/analysis/type-checker.js +1 -1
- package/src/analysis/undocumented.ctx +8 -0
- package/src/analysis/undocumented.js +1 -1
- package/src/cli/cli-handlers.ctx +7 -0
- package/src/cli/cli-handlers.js +1 -1
- package/src/cli/cli.ctx +6 -0
- package/src/cli/cli.js +1 -1
- package/src/compact/ai-context.ctx +6 -0
- package/src/compact/ai-context.js +1 -1
- package/src/compact/compact-migrate.ctx +8 -0
- package/src/compact/compact-migrate.js +1 -1
- package/src/compact/compact.ctx +11 -0
- package/src/compact/compact.js +1 -1
- package/src/compact/compress.ctx +7 -0
- package/src/compact/compress.js +1 -1
- package/src/compact/ctx-resolver.ctx +2 -0
- package/src/compact/ctx-resolver.js +1 -1
- package/src/compact/ctx-to-jsdoc.ctx +11 -0
- package/src/compact/ctx-to-jsdoc.js +1 -1
- package/src/compact/doc-dialect.ctx +11 -0
- package/src/compact/doc-dialect.js +2 -2
- package/src/compact/expand.ctx +14 -0
- package/src/compact/expand.js +1 -1
- package/src/compact/framework-references.ctx +7 -0
- package/src/compact/framework-references.js +1 -1
- package/src/compact/instructions.ctx +6 -0
- package/src/compact/instructions.js +1 -1
- package/src/compact/jsdoc-builder.ctx +4 -0
- package/src/compact/jsdoc-builder.js +1 -1
- package/src/compact/mode-config.ctx +8 -0
- package/src/compact/mode-config.js +1 -1
- package/src/compact/split-declarations.ctx +6 -0
- package/src/compact/split-declarations.js +1 -1
- package/src/compact/validate-pipeline.ctx +12 -0
- package/src/compact/validate-pipeline.js +1 -1
- package/src/core/event-bus.ctx +9 -0
- package/src/core/event-bus.js +1 -1
- package/src/core/file-walker.ctx +1 -0
- package/src/core/file-walker.js +1 -1
- package/src/core/filters.ctx +12 -0
- package/src/core/filters.js +1 -1
- package/src/core/graph-builder.ctx +7 -0
- package/src/core/graph-builder.js +1 -1
- package/src/core/parser.ctx +12 -0
- package/src/core/parser.js +1 -1
- package/src/core/utils.ctx +1 -0
- package/src/core/utils.js +1 -1
- package/src/core/workspace.ctx +7 -0
- package/src/core/workspace.js +1 -1
- package/src/lang/lang-go.ctx +8 -0
- package/src/lang/lang-go.js +1 -1
- package/src/lang/lang-python.ctx +5 -0
- package/src/lang/lang-python.js +1 -1
- package/src/lang/lang-sql.ctx +10 -0
- package/src/lang/lang-sql.js +1 -1
- package/src/lang/lang-typescript.ctx +6 -0
- package/src/lang/lang-typescript.js +1 -1
- package/src/lang/lang-utils.ctx +5 -0
- package/src/lang/lang-utils.js +1 -1
- package/src/mcp/mcp-server.ctx +6 -0
- package/src/mcp/mcp-server.js +1 -1
- package/src/mcp/tool-defs.ctx +2 -0
- package/src/mcp/tool-defs.js +1 -1
- package/src/mcp/tools.ctx +13 -0
- package/src/mcp/tools.js +1 -1
- package/src/network/backend-lifecycle.ctx +10 -0
- package/src/network/backend-lifecycle.js +1 -1
- package/src/network/backend.ctx +5 -0
- package/src/network/backend.js +1 -1
- package/src/network/local-gateway.ctx +9 -0
- package/src/network/local-gateway.js +1 -1
- package/src/network/mdns.ctx +6 -0
- package/src/network/mdns.js +1 -1
- package/src/network/server.ctx +2 -0
- package/src/network/server.js +2 -2
- package/src/network/web-server.ctx +17 -0
- package/src/network/web-server.js +2 -2
- package/web/follow-controller.js +94 -25
- package/web/panels/dep-graph.js +207 -21
- package/project-graph-mcp-2.3.0.tgz +0 -0
- package/vendor/symbiote-node/CHANGELOG.md +0 -31
- package/vendor/symbiote-node/LICENSE +0 -21
- package/vendor/symbiote-node/README.md +0 -206
- package/vendor/symbiote-node/canvas/AutoLayout.js +0 -725
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +0 -73
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +0 -93
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +0 -9
- package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +0 -962
- package/vendor/symbiote-node/canvas/ConnectionRenderer.js +0 -1468
- package/vendor/symbiote-node/canvas/FlowSimulator.js +0 -323
- package/vendor/symbiote-node/canvas/ForceLayout.js +0 -189
- package/vendor/symbiote-node/canvas/ForceWorker.js +0 -1325
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +0 -97
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +0 -176
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +0 -12
- package/vendor/symbiote-node/canvas/LODManager.js +0 -88
- package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +0 -71
- package/vendor/symbiote-node/canvas/Minimap/Minimap.js +0 -207
- package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +0 -9
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +0 -261
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +0 -1840
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +0 -22
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +0 -97
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +0 -132
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +0 -21
- package/vendor/symbiote-node/canvas/NodeViewManager.js +0 -584
- package/vendor/symbiote-node/canvas/PinExpansion.js +0 -131
- package/vendor/symbiote-node/canvas/PseudoConnection.js +0 -80
- package/vendor/symbiote-node/canvas/SubgraphManager.js +0 -201
- package/vendor/symbiote-node/canvas/SubgraphRouter.js +0 -443
- package/vendor/symbiote-node/canvas/ViewportActions.js +0 -446
- package/vendor/symbiote-node/core/Connection.js +0 -45
- package/vendor/symbiote-node/core/Editor.js +0 -451
- package/vendor/symbiote-node/core/Frame.js +0 -31
- package/vendor/symbiote-node/core/GraphMermaid.js +0 -348
- package/vendor/symbiote-node/core/GraphText.js +0 -210
- package/vendor/symbiote-node/core/Node.js +0 -143
- package/vendor/symbiote-node/core/Portal.js +0 -104
- package/vendor/symbiote-node/core/Socket.js +0 -185
- package/vendor/symbiote-node/core/SubgraphNode.js +0 -125
- package/vendor/symbiote-node/index.js +0 -103
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +0 -361
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +0 -332
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +0 -96
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +0 -104
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +0 -133
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +0 -33
- package/vendor/symbiote-node/interactions/ConnectFlow.js +0 -307
- package/vendor/symbiote-node/interactions/Drag.js +0 -102
- package/vendor/symbiote-node/interactions/Selector.js +0 -132
- package/vendor/symbiote-node/interactions/SnapGrid.js +0 -65
- package/vendor/symbiote-node/interactions/Zoom.js +0 -140
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +0 -88
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +0 -254
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +0 -11
- package/vendor/symbiote-node/layout/Layout/Layout.css.js +0 -88
- package/vendor/symbiote-node/layout/Layout/Layout.js +0 -622
- package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +0 -25
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +0 -293
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +0 -467
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +0 -33
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +0 -46
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +0 -102
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +0 -6
- package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +0 -156
- package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +0 -250
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +0 -379
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +0 -263
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +0 -20
- package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +0 -183
- package/vendor/symbiote-node/layout/LayoutTree.js +0 -246
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +0 -43
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +0 -89
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +0 -14
- package/vendor/symbiote-node/layout/index.js +0 -16
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +0 -61
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +0 -79
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +0 -19
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +0 -41
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +0 -24
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +0 -16
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +0 -65
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +0 -29
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +0 -13
- package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +0 -683
- package/vendor/symbiote-node/node/GraphNode/GraphNode.js +0 -92
- package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +0 -17
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +0 -25
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +0 -7
- package/vendor/symbiote-node/node/PortItem/PortItem.css.js +0 -90
- package/vendor/symbiote-node/node/PortItem/PortItem.js +0 -87
- package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +0 -10
- package/vendor/symbiote-node/package.json +0 -59
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +0 -143
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +0 -131
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +0 -16
- package/vendor/symbiote-node/plugins/History.js +0 -384
- package/vendor/symbiote-node/plugins/Readonly.js +0 -59
- package/vendor/symbiote-node/shapes/CircleShape.js +0 -80
- package/vendor/symbiote-node/shapes/CommentShape.js +0 -35
- package/vendor/symbiote-node/shapes/DiamondShape.js +0 -115
- package/vendor/symbiote-node/shapes/NodeShape.js +0 -80
- package/vendor/symbiote-node/shapes/PillShape.js +0 -91
- package/vendor/symbiote-node/shapes/RectShape.js +0 -72
- package/vendor/symbiote-node/shapes/SVGShape.js +0 -494
- package/vendor/symbiote-node/shapes/index.js +0 -53
- package/vendor/symbiote-node/themes/Palette.js +0 -32
- package/vendor/symbiote-node/themes/Skin.js +0 -113
- package/vendor/symbiote-node/themes/Theme.js +0 -84
- package/vendor/symbiote-node/themes/carbon.js +0 -137
- package/vendor/symbiote-node/themes/dark.js +0 -137
- package/vendor/symbiote-node/themes/ebook.js +0 -138
- package/vendor/symbiote-node/themes/grey.js +0 -137
- package/vendor/symbiote-node/themes/light.js +0 -137
- package/vendor/symbiote-node/themes/neon.js +0 -138
- package/vendor/symbiote-node/themes/pcb.js +0 -273
- package/vendor/symbiote-node/themes/synthwave.js +0 -137
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +0 -86
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +0 -128
- 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
|
-
}
|