project-graph-mcp 2.2.6 → 2.3.1
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/ARCHITECTURE.md +81 -0
- package/CHANGELOG.md +57 -0
- package/README.md +9 -4
- package/package.json +4 -13
- package/project-graph-mcp-2.3.0.tgz +0 -0
- package/src/compact/expand.js +1 -1
- package/src/core/graph-builder.js +2 -2
- package/src/core/parser.js +2 -2
- package/src/network/server.js +1 -2
- package/src/network/web-server.js +1 -1
- package/vendor/symbiote-node/CHANGELOG.md +31 -0
- package/vendor/symbiote-node/LICENSE +21 -0
- package/vendor/symbiote-node/README.md +206 -0
- package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
- package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
- package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
- package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
- package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
- package/vendor/symbiote-node/canvas/LODManager.js +88 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
- package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
- package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
- package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
- package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
- package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
- package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
- package/vendor/symbiote-node/core/Connection.js +45 -0
- package/vendor/symbiote-node/core/Editor.js +451 -0
- package/vendor/symbiote-node/core/Frame.js +31 -0
- package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
- package/vendor/symbiote-node/core/GraphText.js +210 -0
- package/vendor/symbiote-node/core/Node.js +143 -0
- package/vendor/symbiote-node/core/Portal.js +104 -0
- package/vendor/symbiote-node/core/Socket.js +185 -0
- package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
- package/vendor/symbiote-node/index.js +103 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
- package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
- package/vendor/symbiote-node/interactions/Drag.js +102 -0
- package/vendor/symbiote-node/interactions/Selector.js +132 -0
- package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
- package/vendor/symbiote-node/interactions/Zoom.js +140 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
- package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
- package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
- package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
- package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
- package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
- package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
- package/vendor/symbiote-node/layout/index.js +16 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
- package/vendor/symbiote-node/package.json +59 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
- package/vendor/symbiote-node/plugins/History.js +384 -0
- package/vendor/symbiote-node/plugins/Readonly.js +59 -0
- package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
- package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
- package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
- package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
- package/vendor/symbiote-node/shapes/PillShape.js +91 -0
- package/vendor/symbiote-node/shapes/RectShape.js +72 -0
- package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
- package/vendor/symbiote-node/shapes/index.js +53 -0
- package/vendor/symbiote-node/themes/Palette.js +32 -0
- package/vendor/symbiote-node/themes/Skin.js +113 -0
- package/vendor/symbiote-node/themes/Theme.js +84 -0
- package/vendor/symbiote-node/themes/carbon.js +137 -0
- package/vendor/symbiote-node/themes/dark.js +137 -0
- package/vendor/symbiote-node/themes/ebook.js +138 -0
- package/vendor/symbiote-node/themes/grey.js +137 -0
- package/vendor/symbiote-node/themes/light.js +137 -0
- package/vendor/symbiote-node/themes/neon.js +138 -0
- package/vendor/symbiote-node/themes/pcb.js +273 -0
- package/vendor/symbiote-node/themes/synthwave.js +137 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
- package/web/app.js +9 -5
- package/web/components/canvas-graph.js +1705 -0
- package/web/components/code-block.js +1 -1
- package/web/components/event-feed/CodeWidget.js +32 -0
- package/web/components/event-feed/EventWidget.js +97 -0
- package/web/components/event-feed/ListWidget.js +57 -0
- package/web/components/event-feed/MiniGraphWidget.js +159 -0
- package/web/components/follow-ribbon.js +134 -0
- package/web/dashboard.js +1 -1
- package/web/follow-controller.js +241 -0
- package/web/index.html +4 -0
- package/web/panels/ActionBoard/ActionBoard.js +1 -1
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
- package/web/panels/code-viewer.js +50 -15
- package/web/panels/dep-graph.js +2691 -7
- package/web/panels/file-tree.js +5 -2
- package/web/panels/live-monitor.js +75 -3
- package/web/style.css +39 -0
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/src/.contextignore +0 -22
- package/src/.project-graph-cache.json +0 -1
- package/src/compact/.project-graph-cache.json +0 -1
- package/web/.project-graph-cache.json +0 -1
- package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
export class SubgraphRouter {
|
|
2
|
+
#canvas = null;
|
|
3
|
+
#config = {};
|
|
4
|
+
#isAutoRouting = false;
|
|
5
|
+
#canvasDepth = 0;
|
|
6
|
+
#listeners = [];
|
|
7
|
+
#destroyed = false;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {HTMLElement} canvas - NodeCanvas instance
|
|
11
|
+
* @param {Object} config - Configuration options
|
|
12
|
+
* @param {String} [config.hashPrefix='graph'] - URL hash routing prefix
|
|
13
|
+
* @param {Map} [config.fileMap] - Map of file paths to node IDs
|
|
14
|
+
* @param {Map} [config.dirNodeMap] - Map of directory paths to node IDs
|
|
15
|
+
* @param {Map} [config.symbolMap] - Map of symbol IDs to { name, file }
|
|
16
|
+
* @param {Set} [config.drillableFiles] - Set of file paths that contain symbol subgraphs
|
|
17
|
+
* @param {Function} [config.onNavigate] - Callback after successful navigation
|
|
18
|
+
*/
|
|
19
|
+
constructor(canvas, config = {}) {
|
|
20
|
+
this.#canvas = canvas;
|
|
21
|
+
this.updateConfig(config);
|
|
22
|
+
this.#bindListeners();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
updateConfig(config = {}) {
|
|
26
|
+
this.#config = {
|
|
27
|
+
hashPrefix: 'graph',
|
|
28
|
+
fileMap: new Map(),
|
|
29
|
+
dirNodeMap: new Map(),
|
|
30
|
+
symbolMap: new Map(),
|
|
31
|
+
drillableFiles: new Set(),
|
|
32
|
+
onNavigate: () => {},
|
|
33
|
+
...this.#config,
|
|
34
|
+
...config
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Internal router depth tracker
|
|
40
|
+
* @returns {number}
|
|
41
|
+
*/
|
|
42
|
+
get depth() {
|
|
43
|
+
return this.#canvasDepth;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prevent hash rewriting during automatic routing across layers
|
|
48
|
+
*/
|
|
49
|
+
#runAutoRouting(fn) {
|
|
50
|
+
this.#isAutoRouting = true;
|
|
51
|
+
fn();
|
|
52
|
+
this.#isAutoRouting = false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#bindListeners() {
|
|
56
|
+
const handleEnter = (e) => {
|
|
57
|
+
this.#canvasDepth++;
|
|
58
|
+
if (this.#isAutoRouting) return;
|
|
59
|
+
|
|
60
|
+
const nodeId = e.detail?.nodeId;
|
|
61
|
+
if (!nodeId) return;
|
|
62
|
+
|
|
63
|
+
// Find the path string for this node ID
|
|
64
|
+
let path = null;
|
|
65
|
+
for (const [key, id] of this.#config.dirNodeMap.entries()) {
|
|
66
|
+
if (id === nodeId) { path = key; break; }
|
|
67
|
+
}
|
|
68
|
+
if (!path) {
|
|
69
|
+
for (const [key, id] of this.#config.fileMap.entries()) {
|
|
70
|
+
if (id === nodeId) { path = key; break; }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (path) {
|
|
75
|
+
const hash = window.location.hash;
|
|
76
|
+
const [base, queryStr] = hash.split('?');
|
|
77
|
+
const params = new URLSearchParams(queryStr || '');
|
|
78
|
+
|
|
79
|
+
params.set('in', '1');
|
|
80
|
+
|
|
81
|
+
// Preserve symbol if it exists and path is drillable
|
|
82
|
+
if (!params.has('symbol') || !this.#config.drillableFiles.has(path)) {
|
|
83
|
+
params.delete('symbol');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const newQuery = params.toString();
|
|
87
|
+
history.replaceState(null, '', `#${this.#config.hashPrefix}/${path}?${newQuery}`);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleExit = (e) => {
|
|
92
|
+
const level = e.detail?.level;
|
|
93
|
+
this.#canvasDepth = (typeof level === 'number') ? level : Math.max(0, this.#canvasDepth - 1);
|
|
94
|
+
if (this.#isAutoRouting) return; // Prevent erasing URL when popping out to find hidden nested paths
|
|
95
|
+
|
|
96
|
+
// Extract the path we were drilled into BEFORE modifying the URL
|
|
97
|
+
const hashPath = window.location.hash.replace(`#${this.#config.hashPrefix}/`, '').split('?')[0].split('&')[0];
|
|
98
|
+
|
|
99
|
+
// Find the directory path we just exited from (to focus on it)
|
|
100
|
+
let exitedDirPath = hashPath;
|
|
101
|
+
if (this.#config.fileMap?.has(hashPath)) {
|
|
102
|
+
const parts = hashPath.split('/');
|
|
103
|
+
parts.pop();
|
|
104
|
+
exitedDirPath = parts.join('/') + '/';
|
|
105
|
+
}
|
|
106
|
+
// Walk up to find the nearest known directory
|
|
107
|
+
if (exitedDirPath && !this.#config.dirNodeMap?.has(exitedDirPath)) {
|
|
108
|
+
const segments = exitedDirPath.replace(/\/$/, '').split('/');
|
|
109
|
+
while (segments.length > 0) {
|
|
110
|
+
const candidate = segments.join('/') + '/';
|
|
111
|
+
if (this.#config.dirNodeMap?.has(candidate)) {
|
|
112
|
+
exitedDirPath = candidate;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
segments.pop();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const updateUrl = (newPath, setIn = false, setFocus = null) => {
|
|
120
|
+
const hash = window.location.hash;
|
|
121
|
+
const [base, queryStr] = hash.split('?');
|
|
122
|
+
const params = new URLSearchParams(queryStr || '');
|
|
123
|
+
|
|
124
|
+
let newBase = `#${this.#config.hashPrefix}`;
|
|
125
|
+
if (newPath) newBase += `/${newPath}`;
|
|
126
|
+
|
|
127
|
+
if (setIn) params.set('in', '1');
|
|
128
|
+
else params.delete('in');
|
|
129
|
+
|
|
130
|
+
if (setFocus) params.set('focus', setFocus);
|
|
131
|
+
else params.delete('focus');
|
|
132
|
+
|
|
133
|
+
params.delete('symbol'); // always clear symbol on exit
|
|
134
|
+
|
|
135
|
+
const newQuery = params.toString();
|
|
136
|
+
const newHash = newQuery ? `${newBase}?${newQuery}` : newBase;
|
|
137
|
+
history.replaceState(null, '', newHash);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (this.#canvasDepth > 0) {
|
|
141
|
+
// Still inside a subgraph — update URL to parent context
|
|
142
|
+
if (this.#config.dirNodeMap?.has(exitedDirPath)) {
|
|
143
|
+
updateUrl(exitedDirPath, true, null);
|
|
144
|
+
} else if (exitedDirPath) {
|
|
145
|
+
updateUrl(exitedDirPath, false, null);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// Back at root
|
|
149
|
+
if (exitedDirPath) {
|
|
150
|
+
updateUrl(null, false, exitedDirPath);
|
|
151
|
+
} else {
|
|
152
|
+
updateUrl(null, false, null);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Fly to the exited group node at ANY level
|
|
157
|
+
if (exitedDirPath) {
|
|
158
|
+
requestAnimationFrame(() => {
|
|
159
|
+
const nodeId = this.#config.dirNodeMap?.get(exitedDirPath) ||
|
|
160
|
+
this.#config.fileMap?.get(exitedDirPath);
|
|
161
|
+
if (nodeId && this.#canvas.flyToNode) {
|
|
162
|
+
this.#canvas.flyToNode(nodeId, { zoom: 0.8 });
|
|
163
|
+
} else if (this.#canvas.fitView) {
|
|
164
|
+
this.#canvas.fitView();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
} else if (this.#canvas.fitView) {
|
|
168
|
+
requestAnimationFrame(() => this.#canvas.fitView());
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
this.#canvas.addEventListener('subgraph-enter', handleEnter);
|
|
173
|
+
this.#canvas.addEventListener('subgraph-exit', handleExit);
|
|
174
|
+
|
|
175
|
+
this.#listeners.push(
|
|
176
|
+
{ name: 'subgraph-enter', fn: handleEnter },
|
|
177
|
+
{ name: 'subgraph-exit', fn: handleExit }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Reads URL hash and triggers initial drill down + focus sequence.
|
|
183
|
+
*
|
|
184
|
+
* Universal URL semantics:
|
|
185
|
+
* - `#graph` → root, fit view
|
|
186
|
+
* - `#graph?focus=src/analysis/` → root, fly to analysis node
|
|
187
|
+
* - `#graph/src/analysis/?in=1` → drill into analysis
|
|
188
|
+
* - `#graph/src/analysis/?in=1&focus=file.js` → drill into analysis, focus file.js
|
|
189
|
+
* - `#graph/src/analysis/file.js?in=1` → drill into analysis, drill into file
|
|
190
|
+
* - `#graph/src/analysis/file.js?in=1&symbol=name` → drill into file, focus symbol
|
|
191
|
+
* - `#graph/src/analysis/` → (legacy) focus analysis at root
|
|
192
|
+
*
|
|
193
|
+
* @param {NodeEditor} editor
|
|
194
|
+
*/
|
|
195
|
+
restoreFromHash(editor) {
|
|
196
|
+
if (this.#destroyed || !this.#canvas) return;
|
|
197
|
+
|
|
198
|
+
const hash = window.location.hash;
|
|
199
|
+
const prefix = `#${this.#config.hashPrefix}`;
|
|
200
|
+
if (!hash.startsWith(prefix)) return;
|
|
201
|
+
|
|
202
|
+
const afterPrefix = hash.slice(prefix.length); // e.g. '/src/analysis/?in=1&focus=file.js' or '?focus=src/analysis/'
|
|
203
|
+
|
|
204
|
+
// Parse query parameters from the hash
|
|
205
|
+
const qIdx = afterPrefix.indexOf('?');
|
|
206
|
+
const pathPart = qIdx >= 0 ? afterPrefix.slice(0, qIdx) : afterPrefix; // '/src/analysis/' or ''
|
|
207
|
+
const queryStr = qIdx >= 0 ? afterPrefix.slice(qIdx + 1) : '';
|
|
208
|
+
const params = new URLSearchParams(queryStr);
|
|
209
|
+
|
|
210
|
+
const drillPath = pathPart.replace(/^\//, ''); // strip leading /
|
|
211
|
+
const hasDrillFlag = params.get('in') === '1';
|
|
212
|
+
const focusParam = params.get('focus');
|
|
213
|
+
const symbolParam = params.get('symbol');
|
|
214
|
+
|
|
215
|
+
// Case 0: bare #graph — pop all subgraph layers and reset to root view
|
|
216
|
+
if (!drillPath && !focusParam && !hasDrillFlag && !symbolParam) {
|
|
217
|
+
// Event-driven pop: wait for each subgraph-exit before the next drillUp.
|
|
218
|
+
// rAF-polling was unreliable because canvasDepth updates only AFTER the exit
|
|
219
|
+
// event fires — which happens asynchronously during the canvas animation.
|
|
220
|
+
this.#isAutoRouting = true;
|
|
221
|
+
let safetyCounter = 10;
|
|
222
|
+
const doPopStep = () => {
|
|
223
|
+
if (this.#canvasDepth <= 0 || safetyCounter-- <= 0) {
|
|
224
|
+
this.#isAutoRouting = false;
|
|
225
|
+
this.#canvas.fitView?.();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Register exit listener FIRST, then trigger drillUp
|
|
229
|
+
const onExit = () => {
|
|
230
|
+
this.#canvas.removeEventListener('subgraph-exit', onExit);
|
|
231
|
+
// canvasDepth is now decremented by the main handler; recurse
|
|
232
|
+
requestAnimationFrame(doPopStep);
|
|
233
|
+
};
|
|
234
|
+
this.#canvas.addEventListener('subgraph-exit', onExit);
|
|
235
|
+
this.#canvas.drillUp?.();
|
|
236
|
+
};
|
|
237
|
+
doPopStep();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Case 1: #graph?focus=src/analysis/ (root-level focus, no path)
|
|
242
|
+
if (!drillPath && focusParam) {
|
|
243
|
+
this.navigateTo(decodeURIComponent(focusParam), 0, false);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Case 2: #graph/path?in=1 (drill into path)
|
|
248
|
+
if (drillPath && hasDrillFlag) {
|
|
249
|
+
const drilled = this.#restoreDrillDown(drillPath, editor, true);
|
|
250
|
+
|
|
251
|
+
// After drilling, handle &focus= (select node inside group)
|
|
252
|
+
if (drilled && focusParam) {
|
|
253
|
+
const fullFocusPath = drillPath + decodeURIComponent(focusParam);
|
|
254
|
+
requestAnimationFrame(() => {
|
|
255
|
+
this.navigateTo(fullFocusPath, 0, false);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Handle &symbol= (focus symbol inside file subgraph)
|
|
260
|
+
if (drilled && symbolParam) {
|
|
261
|
+
requestAnimationFrame(() => {
|
|
262
|
+
this.restoreSymbolFocus(drillPath);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Case 3: #graph/path (legacy — just focus the node at root)
|
|
269
|
+
if (drillPath) {
|
|
270
|
+
this.navigateTo(drillPath, 0, false);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#restoreDrillDown(targetPath, editor, autoDrill = false) {
|
|
275
|
+
if (!this.#canvas) return false;
|
|
276
|
+
|
|
277
|
+
// Try to find a directory SubgraphNode matching the path
|
|
278
|
+
for (const node of editor.getNodes()) {
|
|
279
|
+
if (!node._isSubgraph) continue;
|
|
280
|
+
const nodePath = node.params?.path;
|
|
281
|
+
if (!nodePath) continue;
|
|
282
|
+
|
|
283
|
+
// Exact directory match (e.g. 'src/core/')
|
|
284
|
+
if (nodePath === targetPath) {
|
|
285
|
+
if (autoDrill) {
|
|
286
|
+
this.#runAutoRouting(() => {
|
|
287
|
+
this.#canvas.drillDown(node.id);
|
|
288
|
+
});
|
|
289
|
+
if (this.#canvas.fitView) {
|
|
290
|
+
requestAnimationFrame(() => this.#canvas.fitView());
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
// Just focus on the directory node without drilling in
|
|
294
|
+
this.#canvas.flyToNode?.(node.id, { zoom: 0.8 }) ||
|
|
295
|
+
this.#canvas.selectNode?.(node.id);
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// File inside this directory — drill into dir, then focus file
|
|
301
|
+
if (targetPath.startsWith(nodePath)) {
|
|
302
|
+
this.#runAutoRouting(() => {
|
|
303
|
+
this.#canvas.drillDown(node.id);
|
|
304
|
+
});
|
|
305
|
+
// After transition, specifically focus the exact nested file node
|
|
306
|
+
requestAnimationFrame(() => {
|
|
307
|
+
this.navigateTo(targetPath, 0, autoDrill);
|
|
308
|
+
});
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Restore visual symbol focus from &symbol= URL parameter.
|
|
318
|
+
* Called after autoDrill into a file subgraph to select the target function/class node.
|
|
319
|
+
* @param {string} filePath - the file we drilled into
|
|
320
|
+
*/
|
|
321
|
+
restoreSymbolFocus(filePath) {
|
|
322
|
+
const hashParts = window.location.hash.split('&symbol=');
|
|
323
|
+
if (hashParts.length < 2) return;
|
|
324
|
+
const symbolName = decodeURIComponent(hashParts[1].split('&')[0]);
|
|
325
|
+
if (!symbolName || !this.#config.symbolMap) return;
|
|
326
|
+
|
|
327
|
+
for (const [nodeId, params] of this.#config.symbolMap) {
|
|
328
|
+
if (params.name === symbolName && params.file === filePath) {
|
|
329
|
+
this.#canvas?.selectNode(nodeId);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Focus viewport on a specific node by path
|
|
337
|
+
* @param {string} targetPath - e.g. 'src/core/event-bus.js'
|
|
338
|
+
* @param {number} depth - Internal recursion depth limit
|
|
339
|
+
* @param {boolean} autoDrill - Attempt to drill into target if it is a Subgraph
|
|
340
|
+
* @returns {boolean} true if node found and focused
|
|
341
|
+
*/
|
|
342
|
+
navigateTo(targetPath, depth = 0, autoDrill = false) {
|
|
343
|
+
if (this.#destroyed || !this.#canvas || !this.#config.fileMap || depth > 5) return false;
|
|
344
|
+
|
|
345
|
+
// Find node ID by file path string or directory path string
|
|
346
|
+
let targetId = null;
|
|
347
|
+
let isFile = true;
|
|
348
|
+
if (this.#config.fileMap.has(targetPath)) {
|
|
349
|
+
targetId = this.#config.fileMap.get(targetPath);
|
|
350
|
+
} else if (this.#config.dirNodeMap && this.#config.dirNodeMap.has(targetPath)) {
|
|
351
|
+
targetId = this.#config.dirNodeMap.get(targetPath);
|
|
352
|
+
isFile = false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!targetId) return false;
|
|
356
|
+
|
|
357
|
+
const positions = typeof this.#canvas.getPositions === 'function' ? this.#canvas.getPositions() : {};
|
|
358
|
+
const pos = positions[targetId];
|
|
359
|
+
|
|
360
|
+
// Auto-traversal engine: if target is not visible on current canvas layer
|
|
361
|
+
if (!pos && typeof this.#canvas.drillDown === 'function') {
|
|
362
|
+
if (this.#config.dirNodeMap) {
|
|
363
|
+
// Case 1: Target is a file or directory hidden inside a deeper subgraph.
|
|
364
|
+
// Walk up the path hierarchy to find the nearest visible ancestor directory.
|
|
365
|
+
let searchPath = targetPath;
|
|
366
|
+
if (isFile) {
|
|
367
|
+
// Start from the file's parent directory
|
|
368
|
+
const parts = targetPath.split('/');
|
|
369
|
+
parts.pop();
|
|
370
|
+
searchPath = parts.join('/') + '/';
|
|
371
|
+
if (searchPath === '/') searchPath = './';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Walk UP the directory tree to find the nearest visible ancestor
|
|
375
|
+
let segments = searchPath.replace(/\/$/, '').split('/');
|
|
376
|
+
while (segments.length > 0) {
|
|
377
|
+
const candidateDir = segments.join('/') + '/';
|
|
378
|
+
const dirId = this.#config.dirNodeMap.get(candidateDir);
|
|
379
|
+
if (dirId && positions[dirId]) {
|
|
380
|
+
// Found a visible ancestor — drill into it
|
|
381
|
+
this.#runAutoRouting(() => {
|
|
382
|
+
this.#canvas.drillDown(dirId);
|
|
383
|
+
});
|
|
384
|
+
requestAnimationFrame(() => this.navigateTo(targetPath, depth + 1, autoDrill));
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
segments.pop();
|
|
388
|
+
}
|
|
389
|
+
// Also try "./" as root
|
|
390
|
+
const rootId = this.#config.dirNodeMap.get('./');
|
|
391
|
+
if (rootId && positions[rootId]) {
|
|
392
|
+
this.#runAutoRouting(() => {
|
|
393
|
+
this.#canvas.drillDown(rootId);
|
|
394
|
+
});
|
|
395
|
+
requestAnimationFrame(() => this.navigateTo(targetPath, depth + 1, autoDrill));
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Case 2: Target is completely off-scope (we are inside wrong group). Drill UP to root.
|
|
400
|
+
if (this.#canvasDepth > 0) {
|
|
401
|
+
this.#runAutoRouting(() => {
|
|
402
|
+
this.#canvas.drillUp?.();
|
|
403
|
+
});
|
|
404
|
+
requestAnimationFrame(() => this.navigateTo(targetPath, depth + 1, autoDrill));
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return false; // Unable to locate on any layer
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// We found the node on the current layer. If target is a subgraph file and we are commanded to drill into it, do it.
|
|
412
|
+
if (autoDrill && isFile && this.#config.drillableFiles?.has(targetPath)) {
|
|
413
|
+
this.#runAutoRouting(() => {
|
|
414
|
+
this.#canvas.drillDown?.(targetId);
|
|
415
|
+
});
|
|
416
|
+
requestAnimationFrame(() => {
|
|
417
|
+
if (this.#canvas.fitView) this.#canvas.fitView();
|
|
418
|
+
// Restore &symbol= focus from deep-link URL
|
|
419
|
+
this.restoreSymbolFocus(targetPath);
|
|
420
|
+
});
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// SubgraphRouter delegates raw center/fly animations up to Canvas if possible
|
|
425
|
+
if (this.#canvas.flyToNode) {
|
|
426
|
+
this.#canvas.flyToNode(targetId, { zoom: 0.8 });
|
|
427
|
+
} else {
|
|
428
|
+
// Safe fallback just in case
|
|
429
|
+
this.#canvas.selectNode?.(targetId);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
this.#config.onNavigate(targetPath);
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
destroy() {
|
|
437
|
+
this.#destroyed = true;
|
|
438
|
+
for (const listener of this.#listeners) {
|
|
439
|
+
this.#canvas.removeEventListener(listener.name, listener.fn);
|
|
440
|
+
}
|
|
441
|
+
this.#listeners = [];
|
|
442
|
+
}
|
|
443
|
+
}
|