@zenfg/inspector 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/FrameGraphInspector.d.ts +153 -0
- package/dist/FrameGraphInspector.d.ts.map +1 -0
- package/dist/FrameGraphInspector.js +580 -0
- package/dist/FrameGraphInspector.js.map +1 -0
- package/dist/debugCaptureModel.d.ts +166 -0
- package/dist/debugCaptureModel.d.ts.map +1 -0
- package/dist/debugCaptureModel.js +364 -0
- package/dist/debugCaptureModel.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/panelAliasAnalysis.d.ts +20 -0
- package/dist/panelAliasAnalysis.d.ts.map +1 -0
- package/dist/panelAliasAnalysis.js +113 -0
- package/dist/panelAliasAnalysis.js.map +1 -0
- package/dist/panelCytoscapeGraphRenderer.d.ts +71 -0
- package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -0
- package/dist/panelCytoscapeGraphRenderer.js +559 -0
- package/dist/panelCytoscapeGraphRenderer.js.map +1 -0
- package/dist/panelDiagnosticsView.d.ts +22 -0
- package/dist/panelDiagnosticsView.d.ts.map +1 -0
- package/dist/panelDiagnosticsView.js +123 -0
- package/dist/panelDiagnosticsView.js.map +1 -0
- package/dist/panelDomHelpers.d.ts +26 -0
- package/dist/panelDomHelpers.d.ts.map +1 -0
- package/dist/panelDomHelpers.js +129 -0
- package/dist/panelDomHelpers.js.map +1 -0
- package/dist/panelGraphLayout.d.ts +28 -0
- package/dist/panelGraphLayout.d.ts.map +1 -0
- package/dist/panelGraphLayout.js +223 -0
- package/dist/panelGraphLayout.js.map +1 -0
- package/dist/panelGraphRenderer.d.ts +20 -0
- package/dist/panelGraphRenderer.d.ts.map +1 -0
- package/dist/panelGraphRenderer.js +2 -0
- package/dist/panelGraphRenderer.js.map +1 -0
- package/dist/panelGraphScene.d.ts +98 -0
- package/dist/panelGraphScene.d.ts.map +1 -0
- package/dist/panelGraphScene.js +559 -0
- package/dist/panelGraphScene.js.map +1 -0
- package/dist/panelGraphView.d.ts +9 -0
- package/dist/panelGraphView.d.ts.map +1 -0
- package/dist/panelGraphView.js +88 -0
- package/dist/panelGraphView.js.map +1 -0
- package/dist/panelGraphVisuals.d.ts +38 -0
- package/dist/panelGraphVisuals.d.ts.map +1 -0
- package/dist/panelGraphVisuals.js +330 -0
- package/dist/panelGraphVisuals.js.map +1 -0
- package/dist/panelIcons.d.ts +4 -0
- package/dist/panelIcons.d.ts.map +1 -0
- package/dist/panelIcons.js +43 -0
- package/dist/panelIcons.js.map +1 -0
- package/dist/panelInspectorView.d.ts +33 -0
- package/dist/panelInspectorView.d.ts.map +1 -0
- package/dist/panelInspectorView.js +357 -0
- package/dist/panelInspectorView.js.map +1 -0
- package/dist/panelMemoryView.d.ts +21 -0
- package/dist/panelMemoryView.d.ts.map +1 -0
- package/dist/panelMemoryView.js +134 -0
- package/dist/panelMemoryView.js.map +1 -0
- package/dist/panelPassesView.d.ts +31 -0
- package/dist/panelPassesView.d.ts.map +1 -0
- package/dist/panelPassesView.js +183 -0
- package/dist/panelPassesView.js.map +1 -0
- package/dist/panelResourcesView.d.ts +19 -0
- package/dist/panelResourcesView.d.ts.map +1 -0
- package/dist/panelResourcesView.js +89 -0
- package/dist/panelResourcesView.js.map +1 -0
- package/dist/panelSelection.d.ts +5 -0
- package/dist/panelSelection.d.ts.map +1 -0
- package/dist/panelSelection.js +88 -0
- package/dist/panelSelection.js.map +1 -0
- package/dist/panelTypes.d.ts +40 -0
- package/dist/panelTypes.d.ts.map +1 -0
- package/dist/panelTypes.js +2 -0
- package/dist/panelTypes.js.map +1 -0
- package/dist/panelVisualTheme.d.ts +76 -0
- package/dist/panelVisualTheme.d.ts.map +1 -0
- package/dist/panelVisualTheme.js +39 -0
- package/dist/panelVisualTheme.js.map +1 -0
- package/dist/panelWorkbenchHelpers.d.ts +34 -0
- package/dist/panelWorkbenchHelpers.d.ts.map +1 -0
- package/dist/panelWorkbenchHelpers.js +162 -0
- package/dist/panelWorkbenchHelpers.js.map +1 -0
- package/dist/panelWorkbenchView.d.ts +77 -0
- package/dist/panelWorkbenchView.d.ts.map +1 -0
- package/dist/panelWorkbenchView.js +409 -0
- package/dist/panelWorkbenchView.js.map +1 -0
- package/dist/query.d.ts +11 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +21 -0
- package/dist/query.js.map +1 -0
- package/dist/styles.d.ts +8 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +1073 -0
- package/dist/styles.js.map +1 -0
- package/package.json +29 -0
- package/src/FrameGraphInspector.ts +632 -0
- package/src/debugCaptureModel.ts +583 -0
- package/src/index.ts +21 -0
- package/src/panelAliasAnalysis.ts +164 -0
- package/src/panelCytoscapeGraphRenderer.ts +617 -0
- package/src/panelDiagnosticsView.ts +160 -0
- package/src/panelDomHelpers.ts +151 -0
- package/src/panelGraphLayout.ts +281 -0
- package/src/panelGraphRenderer.ts +21 -0
- package/src/panelGraphScene.ts +787 -0
- package/src/panelGraphView.ts +108 -0
- package/src/panelGraphVisuals.ts +364 -0
- package/src/panelIcons.ts +56 -0
- package/src/panelInspectorView.ts +377 -0
- package/src/panelMemoryView.ts +154 -0
- package/src/panelPassesView.ts +222 -0
- package/src/panelResourcesView.ts +115 -0
- package/src/panelSelection.ts +99 -0
- package/src/panelTypes.ts +28 -0
- package/src/panelVisualTheme.ts +39 -0
- package/src/panelWorkbenchHelpers.ts +216 -0
- package/src/panelWorkbenchView.ts +465 -0
- package/src/query.ts +22 -0
- package/src/styles.ts +1076 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { FrameGraphSnapshotValidationError, decodeFrameGraphSnapshot, parseFrameGraphSnapshot, stringifyFrameGraphSnapshot, } from '@zenfg/snapshot';
|
|
2
|
+
import { createDebugViewModel, } from "./debugCaptureModel.js";
|
|
3
|
+
import { createToolbarButton } from "./panelDomHelpers.js";
|
|
4
|
+
import { createPanelIcon } from "./panelIcons.js";
|
|
5
|
+
import { destroyGraph, fitGraph, } from "./panelGraphView.js";
|
|
6
|
+
import { graphGroupElementId } from "./panelGraphScene.js";
|
|
7
|
+
import { selectionExists } from "./panelSelection.js";
|
|
8
|
+
import { ensureFrameGraphInspectorStyles } from "./styles.js";
|
|
9
|
+
import { sameSelection } from "./panelWorkbenchHelpers.js";
|
|
10
|
+
import { FrameGraphDebugWorkbench } from "./panelWorkbenchView.js";
|
|
11
|
+
const DEFAULT_MAX_IMPORT_BYTES = 64 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_MAX_GRAPH_ELEMENTS = 5_000;
|
|
13
|
+
const DEFAULT_BRANDING = 'ZenFG Inspector';
|
|
14
|
+
/**
|
|
15
|
+
* Browser lifecycle controller for one embeddable FrameGraph inspector.
|
|
16
|
+
*
|
|
17
|
+
* @remarks The constructor creates, but does not append, {@link dom}. Use
|
|
18
|
+
* {@link mountFrameGraphInspector} for the common append-and-return workflow.
|
|
19
|
+
* Async capture, import, and clipboard failures are reported in the UI. Call
|
|
20
|
+
* {@link destroy} when the controller is no longer needed.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const inspector = new FrameGraphInspector({ captureSnapshot: capture });
|
|
25
|
+
* document.body.append(inspector.dom);
|
|
26
|
+
* // Later:
|
|
27
|
+
* inspector.destroy();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export class FrameGraphInspector {
|
|
31
|
+
/** Root element owned by this inspector instance. */
|
|
32
|
+
dom = document.createElement('section');
|
|
33
|
+
body = document.createElement('div');
|
|
34
|
+
content = document.createElement('div');
|
|
35
|
+
dropOverlay = document.createElement('div');
|
|
36
|
+
passesGraphModeButton;
|
|
37
|
+
resourcesGraphModeButton;
|
|
38
|
+
groupsButton;
|
|
39
|
+
collapseGroupsButton;
|
|
40
|
+
graphView;
|
|
41
|
+
workbench;
|
|
42
|
+
captureSnapshotCallback;
|
|
43
|
+
protocolSnapshot;
|
|
44
|
+
operationRevision = 0;
|
|
45
|
+
viewModel;
|
|
46
|
+
selected;
|
|
47
|
+
hovered;
|
|
48
|
+
capturing = false;
|
|
49
|
+
importing = false;
|
|
50
|
+
copying = false;
|
|
51
|
+
copied = false;
|
|
52
|
+
initialAutoCaptureAttempted = false;
|
|
53
|
+
statusMessage;
|
|
54
|
+
statusTone = 'neutral';
|
|
55
|
+
destroyed = false;
|
|
56
|
+
copyFeedbackTimeout;
|
|
57
|
+
maxImportBytes;
|
|
58
|
+
dragDepth = 0;
|
|
59
|
+
handleDragEnter = (event) => {
|
|
60
|
+
if (!isFileDrag(event))
|
|
61
|
+
return;
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
this.dragDepth += 1;
|
|
64
|
+
this.setDropActive(true);
|
|
65
|
+
};
|
|
66
|
+
handleDragOver = (event) => {
|
|
67
|
+
if (!isFileDrag(event))
|
|
68
|
+
return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
if (event.dataTransfer)
|
|
71
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
72
|
+
this.setDropActive(true);
|
|
73
|
+
};
|
|
74
|
+
handleDragLeave = (event) => {
|
|
75
|
+
if (!isFileDrag(event) && this.dragDepth === 0)
|
|
76
|
+
return;
|
|
77
|
+
this.dragDepth = Math.max(0, this.dragDepth - 1);
|
|
78
|
+
if (this.dragDepth === 0)
|
|
79
|
+
this.setDropActive(false);
|
|
80
|
+
};
|
|
81
|
+
handleDragEnd = () => this.clearDropState();
|
|
82
|
+
handleDrop = (event) => {
|
|
83
|
+
const file = event.dataTransfer?.files?.[0];
|
|
84
|
+
if (!file)
|
|
85
|
+
return;
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
this.clearDropState();
|
|
88
|
+
void this.importSnapshot(file);
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Creates a detached inspector UI.
|
|
92
|
+
*
|
|
93
|
+
* @throws If a configured numeric limit is not a non-negative safe integer,
|
|
94
|
+
* or if browser DOM globals are unavailable.
|
|
95
|
+
*/
|
|
96
|
+
constructor(options = {}) {
|
|
97
|
+
ensureFrameGraphInspectorStyles();
|
|
98
|
+
this.captureSnapshotCallback = options.captureSnapshot;
|
|
99
|
+
this.maxImportBytes = normalizeMaxImportBytes(options.maxImportBytes);
|
|
100
|
+
const branding = options.branding ?? DEFAULT_BRANDING;
|
|
101
|
+
this.dom.className = 'zenfg-inspector';
|
|
102
|
+
this.dom.id = createInspectorId();
|
|
103
|
+
this.dom.setAttribute('role', 'region');
|
|
104
|
+
this.dom.setAttribute('aria-label', branding === false ? DEFAULT_BRANDING : branding);
|
|
105
|
+
this.body.className = 'zenfg-inspector-body';
|
|
106
|
+
this.content.className = 'zenfg-inspector-content';
|
|
107
|
+
this.dropOverlay.className = 'zenfg-inspector-drop-overlay';
|
|
108
|
+
this.dropOverlay.hidden = true;
|
|
109
|
+
this.dropOverlay.setAttribute('aria-hidden', 'true');
|
|
110
|
+
const dropTitle = document.createElement('strong');
|
|
111
|
+
dropTitle.textContent = 'Drop snapshot to open';
|
|
112
|
+
const dropDetail = document.createElement('span');
|
|
113
|
+
dropDetail.textContent = 'The first file will be imported locally.';
|
|
114
|
+
this.dropOverlay.append(dropTitle, dropDetail);
|
|
115
|
+
const graphLegend = document.createElement('div');
|
|
116
|
+
graphLegend.className = 'zenfg-inspector-graph-legend';
|
|
117
|
+
graphLegend.setAttribute('aria-label', 'Graph legend');
|
|
118
|
+
this.graphView = {
|
|
119
|
+
host: document.createElement('div'),
|
|
120
|
+
toolbar: document.createElement('div'),
|
|
121
|
+
legend: graphLegend,
|
|
122
|
+
layoutElementBudget: normalizeLimit(options.maxGraphElements, DEFAULT_MAX_GRAPH_ELEMENTS, 'maxGraphElements'),
|
|
123
|
+
graphMode: 'passes',
|
|
124
|
+
groupsEnabled: true,
|
|
125
|
+
expandedGroupPaths: new Set(),
|
|
126
|
+
fitOnNextRender: true,
|
|
127
|
+
};
|
|
128
|
+
this.graphView.host.className = 'zenfg-inspector-graph';
|
|
129
|
+
this.graphView.toolbar.className = 'zenfg-inspector-graph-toolbar';
|
|
130
|
+
this.graphView.toolbar.setAttribute('role', 'toolbar');
|
|
131
|
+
this.graphView.toolbar.setAttribute('aria-label', 'Frame graph view controls');
|
|
132
|
+
this.passesGraphModeButton = createToolbarButton('Pass dependency', 'Show pass dependency graph', () => this.setGraphMode('passes'));
|
|
133
|
+
this.resourcesGraphModeButton = createToolbarButton('Resource access', 'Show resource access graph', () => this.setGraphMode('resources'));
|
|
134
|
+
this.groupsButton = createToolbarButton('Groups', 'Toggle diagnostic group projection', () => this.toggleGroups());
|
|
135
|
+
this.collapseGroupsButton = createToolbarButton('Collapse All', 'Collapse every diagnostic group', () => this.collapseAllGroups());
|
|
136
|
+
this.passesGraphModeButton.classList.add('zenfg-inspector-mode-button');
|
|
137
|
+
this.resourcesGraphModeButton.classList.add('zenfg-inspector-mode-button');
|
|
138
|
+
const modeControls = document.createElement('div');
|
|
139
|
+
modeControls.className = 'zenfg-inspector-graph-mode-controls';
|
|
140
|
+
modeControls.setAttribute('role', 'group');
|
|
141
|
+
modeControls.setAttribute('aria-label', 'Graph mode');
|
|
142
|
+
modeControls.append(this.passesGraphModeButton, this.resourcesGraphModeButton);
|
|
143
|
+
const actionControls = document.createElement('div');
|
|
144
|
+
actionControls.className = 'zenfg-inspector-graph-action-controls';
|
|
145
|
+
actionControls.append(this.groupsButton, this.collapseGroupsButton, createGraphIconButton('fit', 'Fit graph to view', () => fitGraph(this.graphView)));
|
|
146
|
+
this.graphView.toolbar.append(modeControls, graphLegend, actionControls);
|
|
147
|
+
const callbacks = {
|
|
148
|
+
onSelect: (selection) => this.handleSelect(selection),
|
|
149
|
+
onHover: (selection) => this.handleHover(selection),
|
|
150
|
+
onGroupToggle: (pathKey) => this.toggleGroup(pathKey),
|
|
151
|
+
isGroupExpanded: (pathKey) => this.graphView.expandedGroupPaths.has(pathKey),
|
|
152
|
+
};
|
|
153
|
+
this.workbench = new FrameGraphDebugWorkbench(this.graphView, callbacks, {
|
|
154
|
+
onCapture: () => { void this.captureSnapshot(); },
|
|
155
|
+
onImport: (file) => { void this.importSnapshot(file); },
|
|
156
|
+
onDownload: () => this.downloadSnapshot(),
|
|
157
|
+
onCopyJson: () => { void this.copySnapshotJson(); },
|
|
158
|
+
}, {
|
|
159
|
+
branding,
|
|
160
|
+
idPrefix: this.dom.id,
|
|
161
|
+
});
|
|
162
|
+
this.content.append(this.workbench.root);
|
|
163
|
+
this.body.appendChild(this.content);
|
|
164
|
+
this.dom.append(this.body, this.dropOverlay);
|
|
165
|
+
this.dom.addEventListener('dragenter', this.handleDragEnter);
|
|
166
|
+
this.dom.addEventListener('dragover', this.handleDragOver);
|
|
167
|
+
this.dom.addEventListener('dragleave', this.handleDragLeave);
|
|
168
|
+
this.dom.addEventListener('dragend', this.handleDragEnd);
|
|
169
|
+
this.dom.addEventListener('drop', this.handleDrop);
|
|
170
|
+
this.updateCaptureActions();
|
|
171
|
+
this.updateGraphModeButtonState();
|
|
172
|
+
this.showEmptyState();
|
|
173
|
+
queueMicrotask(() => this.maybeAutoCapture());
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Replaces or removes the live-capture provider.
|
|
177
|
+
*
|
|
178
|
+
* @remarks Installing a provider may immediately begin an asynchronous
|
|
179
|
+
* capture when no snapshot is displayed.
|
|
180
|
+
*/
|
|
181
|
+
setCaptureSnapshotProvider(provider) {
|
|
182
|
+
const changed = this.captureSnapshotCallback !== provider;
|
|
183
|
+
this.captureSnapshotCallback = provider;
|
|
184
|
+
if (changed && !this.viewModel)
|
|
185
|
+
this.initialAutoCaptureAttempted = false;
|
|
186
|
+
if (!provider && !this.viewModel)
|
|
187
|
+
this.showEmptyState();
|
|
188
|
+
this.updateCaptureActions();
|
|
189
|
+
this.maybeAutoCapture();
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Requests and displays a live snapshot from the configured provider.
|
|
193
|
+
*
|
|
194
|
+
* @remarks Concurrent requests are coalesced. Provider errors, invalid
|
|
195
|
+
* snapshots, and an unavailable provider are displayed in the inspector and
|
|
196
|
+
* do not reject the returned promise.
|
|
197
|
+
*/
|
|
198
|
+
async captureSnapshot() {
|
|
199
|
+
if (this.destroyed || this.capturing)
|
|
200
|
+
return;
|
|
201
|
+
if (!this.captureSnapshotCallback) {
|
|
202
|
+
this.reportCaptureIssue('Waiting for a FrameGraph capture source.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const revision = ++this.operationRevision;
|
|
206
|
+
this.importing = false;
|
|
207
|
+
this.capturing = true;
|
|
208
|
+
this.statusMessage = undefined;
|
|
209
|
+
this.statusTone = 'neutral';
|
|
210
|
+
if (!this.viewModel)
|
|
211
|
+
this.workbench.showEmptyState('capturing', 'Capturing the next rendered frame…');
|
|
212
|
+
this.updateCaptureActions();
|
|
213
|
+
try {
|
|
214
|
+
const snapshot = await this.captureSnapshotCallback();
|
|
215
|
+
if (this.destroyed || revision !== this.operationRevision)
|
|
216
|
+
return;
|
|
217
|
+
if (!snapshot) {
|
|
218
|
+
this.reportCaptureIssue('No snapshot was produced. Capture again when rendering is active.');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const decoded = decodeFrameGraphSnapshot(snapshot);
|
|
222
|
+
if (!decoded.ok) {
|
|
223
|
+
this.reportCaptureIssue(`Failed to capture FrameGraph: ${formatIssues(decoded.issues)}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.applySnapshot(decoded.snapshot, { kind: 'live', label: 'Live Capture' });
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!this.destroyed && revision === this.operationRevision) {
|
|
230
|
+
this.reportCaptureIssue(`Failed to capture FrameGraph: ${error instanceof Error ? error.message : String(error)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
if (revision === this.operationRevision)
|
|
235
|
+
this.capturing = false;
|
|
236
|
+
if (!this.destroyed && revision === this.operationRevision)
|
|
237
|
+
this.updateCaptureActions();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Validates and synchronously displays a programmatic Snapshot 1.0 value.
|
|
242
|
+
*
|
|
243
|
+
* @throws {@link FrameGraphSnapshotValidationError} if `snapshot` is invalid.
|
|
244
|
+
*/
|
|
245
|
+
setSnapshot(snapshot) {
|
|
246
|
+
if (this.destroyed)
|
|
247
|
+
return;
|
|
248
|
+
this.operationRevision += 1;
|
|
249
|
+
this.capturing = false;
|
|
250
|
+
this.importing = false;
|
|
251
|
+
const decoded = decodeFrameGraphSnapshot(snapshot);
|
|
252
|
+
if (!decoded.ok)
|
|
253
|
+
throw new FrameGraphSnapshotValidationError(decoded.issues);
|
|
254
|
+
this.applySnapshot(decoded.snapshot, { kind: 'programmatic', label: 'Programmatic' });
|
|
255
|
+
}
|
|
256
|
+
/** Returns the currently displayed canonical snapshot, if any. */
|
|
257
|
+
getSnapshot() {
|
|
258
|
+
return this.protocolSnapshot;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Reads, migrates, validates, and displays a snapshot JSON file.
|
|
262
|
+
*
|
|
263
|
+
* @remarks Files above `maxImportBytes`, read failures, invalid JSON, and
|
|
264
|
+
* validation failures are displayed in the UI and do not reject the promise.
|
|
265
|
+
*/
|
|
266
|
+
async importSnapshot(file) {
|
|
267
|
+
if (this.destroyed)
|
|
268
|
+
return;
|
|
269
|
+
const revision = ++this.operationRevision;
|
|
270
|
+
this.capturing = false;
|
|
271
|
+
this.importing = false;
|
|
272
|
+
if (file.size > this.maxImportBytes) {
|
|
273
|
+
this.reportCaptureIssue(`Import exceeds the ${formatImportLimit(this.maxImportBytes)} limit.`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.importing = true;
|
|
277
|
+
this.statusMessage = undefined;
|
|
278
|
+
this.statusTone = 'neutral';
|
|
279
|
+
this.updateCaptureActions();
|
|
280
|
+
try {
|
|
281
|
+
const text = await file.text();
|
|
282
|
+
if (this.destroyed || revision !== this.operationRevision)
|
|
283
|
+
return;
|
|
284
|
+
const decoded = parseFrameGraphSnapshot(text);
|
|
285
|
+
if (!decoded.ok) {
|
|
286
|
+
this.reportCaptureIssue(`Failed to import snapshot: ${formatIssues(decoded.issues)}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.applySnapshot(decoded.snapshot, {
|
|
290
|
+
kind: 'file',
|
|
291
|
+
label: file.name || 'Imported JSON',
|
|
292
|
+
});
|
|
293
|
+
const sourceFormat = decoded.snapshot.capture.migration?.sourceFormat;
|
|
294
|
+
if (sourceFormat) {
|
|
295
|
+
const sourceLabel = sourceFormat === 'legacy-v0' ? 'Legacy V0' : 'Legacy Candidate V1';
|
|
296
|
+
this.statusMessage = decoded.migrated
|
|
297
|
+
? `Imported ${sourceLabel} and migrated it to ZenFG Snapshot V1.`
|
|
298
|
+
: `Imported Snapshot V1 with ${sourceLabel} migration provenance.`;
|
|
299
|
+
this.statusTone = 'neutral';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (!this.destroyed && revision === this.operationRevision) {
|
|
304
|
+
this.reportCaptureIssue(`Failed to import snapshot: ${error instanceof Error ? error.message : String(error)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
if (revision === this.operationRevision)
|
|
309
|
+
this.importing = false;
|
|
310
|
+
if (!this.destroyed && revision === this.operationRevision)
|
|
311
|
+
this.updateCaptureActions();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** Downloads the displayed snapshot as pretty-printed canonical JSON. */
|
|
315
|
+
downloadSnapshot() {
|
|
316
|
+
if (this.destroyed || !this.protocolSnapshot)
|
|
317
|
+
return;
|
|
318
|
+
const json = stringifyFrameGraphSnapshot(this.protocolSnapshot, { pretty: true });
|
|
319
|
+
const url = URL.createObjectURL(new Blob([json], { type: 'application/json;charset=utf-8' }));
|
|
320
|
+
const anchor = document.createElement('a');
|
|
321
|
+
anchor.href = url;
|
|
322
|
+
anchor.download = `frame-graph-${this.protocolSnapshot.capture.frameIndex}.fgsnapshot.json`;
|
|
323
|
+
anchor.click();
|
|
324
|
+
URL.revokeObjectURL(url);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Copies the displayed snapshot as pretty-printed canonical JSON.
|
|
328
|
+
*
|
|
329
|
+
* @remarks Clipboard failures are shown in the UI and do not reject the
|
|
330
|
+
* returned promise. A legacy `document.execCommand` fallback is used when the
|
|
331
|
+
* async Clipboard API is unavailable.
|
|
332
|
+
*/
|
|
333
|
+
async copySnapshotJson() {
|
|
334
|
+
if (this.destroyed || this.copying || !this.protocolSnapshot)
|
|
335
|
+
return;
|
|
336
|
+
const snapshot = this.protocolSnapshot;
|
|
337
|
+
const revision = this.operationRevision;
|
|
338
|
+
this.copying = true;
|
|
339
|
+
this.copied = false;
|
|
340
|
+
this.statusMessage = undefined;
|
|
341
|
+
this.statusTone = 'neutral';
|
|
342
|
+
this.updateCaptureActions();
|
|
343
|
+
try {
|
|
344
|
+
await writeTextToClipboard(stringifyFrameGraphSnapshot(snapshot, { pretty: true }));
|
|
345
|
+
if (this.destroyed || revision !== this.operationRevision)
|
|
346
|
+
return;
|
|
347
|
+
this.copied = true;
|
|
348
|
+
if (this.copyFeedbackTimeout !== undefined)
|
|
349
|
+
window.clearTimeout(this.copyFeedbackTimeout);
|
|
350
|
+
this.copyFeedbackTimeout = window.setTimeout(() => {
|
|
351
|
+
this.copied = false;
|
|
352
|
+
this.copyFeedbackTimeout = undefined;
|
|
353
|
+
this.updateCaptureActions();
|
|
354
|
+
}, 1200);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (!this.destroyed && revision === this.operationRevision) {
|
|
358
|
+
this.statusMessage = `Failed to copy snapshot: ${error instanceof Error ? error.message : String(error)}`;
|
|
359
|
+
}
|
|
360
|
+
console.warn('Failed to copy FrameGraph Snapshot.', error);
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
this.copying = false;
|
|
364
|
+
if (!this.destroyed)
|
|
365
|
+
this.updateCaptureActions();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
applySnapshot(snapshot, source) {
|
|
369
|
+
const viewModel = createDebugViewModel(snapshot, source);
|
|
370
|
+
this.copied = false;
|
|
371
|
+
if (this.copyFeedbackTimeout !== undefined) {
|
|
372
|
+
window.clearTimeout(this.copyFeedbackTimeout);
|
|
373
|
+
this.copyFeedbackTimeout = undefined;
|
|
374
|
+
}
|
|
375
|
+
this.protocolSnapshot = snapshot;
|
|
376
|
+
this.viewModel = viewModel;
|
|
377
|
+
this.graphView.fitOnNextRender = true;
|
|
378
|
+
this.hovered = undefined;
|
|
379
|
+
this.initialAutoCaptureAttempted = true;
|
|
380
|
+
this.statusMessage = undefined;
|
|
381
|
+
this.statusTone = 'neutral';
|
|
382
|
+
if (!this.selected || !selectionExists(viewModel, this.selected)) {
|
|
383
|
+
this.selected = viewModel.nodes[0]
|
|
384
|
+
? { kind: 'node', id: viewModel.nodes[0].id }
|
|
385
|
+
: undefined;
|
|
386
|
+
}
|
|
387
|
+
this.workbench.setSnapshot(viewModel, this.selected);
|
|
388
|
+
this.updateCaptureActions();
|
|
389
|
+
this.updateGraphModeButtonState();
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Idempotently cancels pending UI results, releases graph resources, and
|
|
393
|
+
* removes the inspector workbench. Do not reuse the controller afterward.
|
|
394
|
+
*/
|
|
395
|
+
destroy() {
|
|
396
|
+
if (this.destroyed)
|
|
397
|
+
return;
|
|
398
|
+
this.destroyed = true;
|
|
399
|
+
this.operationRevision += 1;
|
|
400
|
+
if (this.copyFeedbackTimeout !== undefined)
|
|
401
|
+
window.clearTimeout(this.copyFeedbackTimeout);
|
|
402
|
+
this.clearDropState();
|
|
403
|
+
this.dom.removeEventListener('dragenter', this.handleDragEnter);
|
|
404
|
+
this.dom.removeEventListener('dragover', this.handleDragOver);
|
|
405
|
+
this.dom.removeEventListener('dragleave', this.handleDragLeave);
|
|
406
|
+
this.dom.removeEventListener('dragend', this.handleDragEnd);
|
|
407
|
+
this.dom.removeEventListener('drop', this.handleDrop);
|
|
408
|
+
destroyGraph(this.graphView);
|
|
409
|
+
this.dom.remove();
|
|
410
|
+
}
|
|
411
|
+
updateCaptureActions() {
|
|
412
|
+
this.workbench.setSnapshotActionState({
|
|
413
|
+
providerAvailable: Boolean(this.captureSnapshotCallback),
|
|
414
|
+
hasCapture: Boolean(this.viewModel),
|
|
415
|
+
capturing: this.capturing,
|
|
416
|
+
importing: this.importing,
|
|
417
|
+
copying: this.copying,
|
|
418
|
+
copied: this.copied,
|
|
419
|
+
message: this.viewModel ? this.statusMessage : undefined,
|
|
420
|
+
messageTone: this.statusTone,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
maybeAutoCapture() {
|
|
424
|
+
if (this.destroyed
|
|
425
|
+
|| this.viewModel
|
|
426
|
+
|| this.initialAutoCaptureAttempted
|
|
427
|
+
|| this.capturing
|
|
428
|
+
|| this.importing
|
|
429
|
+
|| !this.captureSnapshotCallback)
|
|
430
|
+
return;
|
|
431
|
+
this.initialAutoCaptureAttempted = true;
|
|
432
|
+
void this.captureSnapshot();
|
|
433
|
+
}
|
|
434
|
+
handleSelect(selection) {
|
|
435
|
+
this.selected = selection;
|
|
436
|
+
this.workbench.setSelection(selection);
|
|
437
|
+
}
|
|
438
|
+
handleHover(selection) {
|
|
439
|
+
if (!this.viewModel || sameSelection(this.hovered, selection))
|
|
440
|
+
return;
|
|
441
|
+
this.hovered = selection;
|
|
442
|
+
this.workbench.setHovered(selection);
|
|
443
|
+
}
|
|
444
|
+
setGraphMode(mode) {
|
|
445
|
+
if (this.graphView.graphMode === mode)
|
|
446
|
+
return;
|
|
447
|
+
this.graphView.graphMode = mode;
|
|
448
|
+
this.graphView.fitOnNextRender = true;
|
|
449
|
+
this.updateGraphModeButtonState();
|
|
450
|
+
this.workbench.refreshGraphStructure();
|
|
451
|
+
}
|
|
452
|
+
updateGraphModeButtonState() {
|
|
453
|
+
const passesActive = this.graphView.graphMode === 'passes';
|
|
454
|
+
const hasGroups = (this.viewModel?.debugGroups.length ?? 0) > 0;
|
|
455
|
+
this.passesGraphModeButton.classList.toggle('active', passesActive);
|
|
456
|
+
this.resourcesGraphModeButton.classList.toggle('active', !passesActive);
|
|
457
|
+
this.passesGraphModeButton.setAttribute('aria-pressed', passesActive ? 'true' : 'false');
|
|
458
|
+
this.resourcesGraphModeButton.setAttribute('aria-pressed', passesActive ? 'false' : 'true');
|
|
459
|
+
this.groupsButton.disabled = !passesActive || !hasGroups;
|
|
460
|
+
this.groupsButton.classList.toggle('active', passesActive && hasGroups && this.graphView.groupsEnabled);
|
|
461
|
+
this.groupsButton.setAttribute('aria-pressed', passesActive && hasGroups && this.graphView.groupsEnabled ? 'true' : 'false');
|
|
462
|
+
const hasExpanded = this.viewModel?.debugGroups.some((group) => this.graphView.expandedGroupPaths.has(group.pathKey)) ?? false;
|
|
463
|
+
this.collapseGroupsButton.disabled = !passesActive || !hasGroups || !this.graphView.groupsEnabled || !hasExpanded;
|
|
464
|
+
}
|
|
465
|
+
toggleGroups() {
|
|
466
|
+
if (this.graphView.graphMode !== 'passes' || !this.viewModel?.debugGroups.length)
|
|
467
|
+
return;
|
|
468
|
+
this.graphView.groupsEnabled = !this.graphView.groupsEnabled;
|
|
469
|
+
this.graphView.fitOnNextRender = true;
|
|
470
|
+
this.updateGraphModeButtonState();
|
|
471
|
+
this.workbench.refreshGraphStructure();
|
|
472
|
+
}
|
|
473
|
+
toggleGroup(pathKey) {
|
|
474
|
+
if (this.graphView.expandedGroupPaths.has(pathKey))
|
|
475
|
+
this.graphView.expandedGroupPaths.delete(pathKey);
|
|
476
|
+
else
|
|
477
|
+
this.graphView.expandedGroupPaths.add(pathKey);
|
|
478
|
+
this.graphView.anchorElementIdOnNextRender = graphGroupElementId(pathKey);
|
|
479
|
+
this.updateGraphModeButtonState();
|
|
480
|
+
this.workbench.refreshGraphStructure();
|
|
481
|
+
}
|
|
482
|
+
collapseAllGroups() {
|
|
483
|
+
if (this.graphView.expandedGroupPaths.size === 0)
|
|
484
|
+
return;
|
|
485
|
+
this.graphView.expandedGroupPaths.clear();
|
|
486
|
+
this.graphView.fitOnNextRender = true;
|
|
487
|
+
this.updateGraphModeButtonState();
|
|
488
|
+
this.workbench.refreshGraphStructure();
|
|
489
|
+
}
|
|
490
|
+
showEmptyState() {
|
|
491
|
+
const providerAvailable = Boolean(this.captureSnapshotCallback);
|
|
492
|
+
this.workbench.showEmptyState('empty', providerAvailable
|
|
493
|
+
? 'No capture yet. Use Capture to request the next rendered frame.'
|
|
494
|
+
: 'Drop a ZenFG Snapshot here or choose Import.', providerAvailable ? undefined : 'Files are processed locally in your browser.');
|
|
495
|
+
}
|
|
496
|
+
setDropActive(active) {
|
|
497
|
+
if (this.destroyed)
|
|
498
|
+
return;
|
|
499
|
+
this.dropOverlay.hidden = !active;
|
|
500
|
+
this.dom.classList.toggle('drop-active', active);
|
|
501
|
+
}
|
|
502
|
+
clearDropState() {
|
|
503
|
+
this.dragDepth = 0;
|
|
504
|
+
this.dropOverlay.hidden = true;
|
|
505
|
+
this.dom.classList.remove('drop-active');
|
|
506
|
+
}
|
|
507
|
+
reportCaptureIssue(message) {
|
|
508
|
+
this.statusMessage = message;
|
|
509
|
+
this.statusTone = 'error';
|
|
510
|
+
if (!this.viewModel)
|
|
511
|
+
this.workbench.showEmptyState('error', message);
|
|
512
|
+
this.updateCaptureActions();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Creates an inspector, appends its root element to `host`, and returns its
|
|
517
|
+
* lifecycle controller.
|
|
518
|
+
*
|
|
519
|
+
* @throws If inspector construction fails; see {@link FrameGraphInspector}.
|
|
520
|
+
*/
|
|
521
|
+
export function mountFrameGraphInspector(host, options) {
|
|
522
|
+
const inspector = new FrameGraphInspector(options);
|
|
523
|
+
host.appendChild(inspector.dom);
|
|
524
|
+
return inspector;
|
|
525
|
+
}
|
|
526
|
+
function createGraphIconButton(icon, title, onClick) {
|
|
527
|
+
const button = createToolbarButton('', title, onClick);
|
|
528
|
+
button.classList.add('zenfg-inspector-icon-button');
|
|
529
|
+
button.appendChild(createPanelIcon(icon));
|
|
530
|
+
return button;
|
|
531
|
+
}
|
|
532
|
+
async function writeTextToClipboard(text) {
|
|
533
|
+
if (navigator.clipboard?.writeText) {
|
|
534
|
+
await navigator.clipboard.writeText(text);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const textArea = document.createElement('textarea');
|
|
538
|
+
textArea.value = text;
|
|
539
|
+
textArea.setAttribute('readonly', 'true');
|
|
540
|
+
textArea.style.position = 'fixed';
|
|
541
|
+
textArea.style.left = '-9999px';
|
|
542
|
+
textArea.style.top = '0';
|
|
543
|
+
document.body.appendChild(textArea);
|
|
544
|
+
textArea.select();
|
|
545
|
+
try {
|
|
546
|
+
if (!document.execCommand('copy'))
|
|
547
|
+
throw new Error('document.execCommand("copy") returned false.');
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
textArea.remove();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function normalizeMaxImportBytes(value) {
|
|
554
|
+
return normalizeLimit(value, DEFAULT_MAX_IMPORT_BYTES, 'maxImportBytes');
|
|
555
|
+
}
|
|
556
|
+
function normalizeLimit(value, fallback, name) {
|
|
557
|
+
if (value === undefined)
|
|
558
|
+
return fallback;
|
|
559
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
560
|
+
throw new Error(`${name} must be a non-negative safe integer.`);
|
|
561
|
+
}
|
|
562
|
+
return value;
|
|
563
|
+
}
|
|
564
|
+
function formatImportLimit(bytes) {
|
|
565
|
+
return bytes >= 1024 * 1024 && bytes % (1024 * 1024) === 0
|
|
566
|
+
? `${bytes / (1024 * 1024)} MiB`
|
|
567
|
+
: `${bytes} bytes`;
|
|
568
|
+
}
|
|
569
|
+
function formatIssues(issues) {
|
|
570
|
+
return issues.slice(0, 3).map((issue) => `${issue.path || '/'}: ${issue.message}`).join('; ');
|
|
571
|
+
}
|
|
572
|
+
function isFileDrag(event) {
|
|
573
|
+
return Array.from(event.dataTransfer?.types ?? []).includes('Files');
|
|
574
|
+
}
|
|
575
|
+
let nextInspectorId = 0;
|
|
576
|
+
function createInspectorId() {
|
|
577
|
+
nextInspectorId += 1;
|
|
578
|
+
return `zenfg-inspector-${nextInspectorId}`;
|
|
579
|
+
}
|
|
580
|
+
//# sourceMappingURL=FrameGraphInspector.js.map
|