@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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/FrameGraphInspector.d.ts +153 -0
  4. package/dist/FrameGraphInspector.d.ts.map +1 -0
  5. package/dist/FrameGraphInspector.js +580 -0
  6. package/dist/FrameGraphInspector.js.map +1 -0
  7. package/dist/debugCaptureModel.d.ts +166 -0
  8. package/dist/debugCaptureModel.d.ts.map +1 -0
  9. package/dist/debugCaptureModel.js +364 -0
  10. package/dist/debugCaptureModel.js.map +1 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +15 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/panelAliasAnalysis.d.ts +20 -0
  16. package/dist/panelAliasAnalysis.d.ts.map +1 -0
  17. package/dist/panelAliasAnalysis.js +113 -0
  18. package/dist/panelAliasAnalysis.js.map +1 -0
  19. package/dist/panelCytoscapeGraphRenderer.d.ts +71 -0
  20. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -0
  21. package/dist/panelCytoscapeGraphRenderer.js +559 -0
  22. package/dist/panelCytoscapeGraphRenderer.js.map +1 -0
  23. package/dist/panelDiagnosticsView.d.ts +22 -0
  24. package/dist/panelDiagnosticsView.d.ts.map +1 -0
  25. package/dist/panelDiagnosticsView.js +123 -0
  26. package/dist/panelDiagnosticsView.js.map +1 -0
  27. package/dist/panelDomHelpers.d.ts +26 -0
  28. package/dist/panelDomHelpers.d.ts.map +1 -0
  29. package/dist/panelDomHelpers.js +129 -0
  30. package/dist/panelDomHelpers.js.map +1 -0
  31. package/dist/panelGraphLayout.d.ts +28 -0
  32. package/dist/panelGraphLayout.d.ts.map +1 -0
  33. package/dist/panelGraphLayout.js +223 -0
  34. package/dist/panelGraphLayout.js.map +1 -0
  35. package/dist/panelGraphRenderer.d.ts +20 -0
  36. package/dist/panelGraphRenderer.d.ts.map +1 -0
  37. package/dist/panelGraphRenderer.js +2 -0
  38. package/dist/panelGraphRenderer.js.map +1 -0
  39. package/dist/panelGraphScene.d.ts +98 -0
  40. package/dist/panelGraphScene.d.ts.map +1 -0
  41. package/dist/panelGraphScene.js +559 -0
  42. package/dist/panelGraphScene.js.map +1 -0
  43. package/dist/panelGraphView.d.ts +9 -0
  44. package/dist/panelGraphView.d.ts.map +1 -0
  45. package/dist/panelGraphView.js +88 -0
  46. package/dist/panelGraphView.js.map +1 -0
  47. package/dist/panelGraphVisuals.d.ts +38 -0
  48. package/dist/panelGraphVisuals.d.ts.map +1 -0
  49. package/dist/panelGraphVisuals.js +330 -0
  50. package/dist/panelGraphVisuals.js.map +1 -0
  51. package/dist/panelIcons.d.ts +4 -0
  52. package/dist/panelIcons.d.ts.map +1 -0
  53. package/dist/panelIcons.js +43 -0
  54. package/dist/panelIcons.js.map +1 -0
  55. package/dist/panelInspectorView.d.ts +33 -0
  56. package/dist/panelInspectorView.d.ts.map +1 -0
  57. package/dist/panelInspectorView.js +357 -0
  58. package/dist/panelInspectorView.js.map +1 -0
  59. package/dist/panelMemoryView.d.ts +21 -0
  60. package/dist/panelMemoryView.d.ts.map +1 -0
  61. package/dist/panelMemoryView.js +134 -0
  62. package/dist/panelMemoryView.js.map +1 -0
  63. package/dist/panelPassesView.d.ts +31 -0
  64. package/dist/panelPassesView.d.ts.map +1 -0
  65. package/dist/panelPassesView.js +183 -0
  66. package/dist/panelPassesView.js.map +1 -0
  67. package/dist/panelResourcesView.d.ts +19 -0
  68. package/dist/panelResourcesView.d.ts.map +1 -0
  69. package/dist/panelResourcesView.js +89 -0
  70. package/dist/panelResourcesView.js.map +1 -0
  71. package/dist/panelSelection.d.ts +5 -0
  72. package/dist/panelSelection.d.ts.map +1 -0
  73. package/dist/panelSelection.js +88 -0
  74. package/dist/panelSelection.js.map +1 -0
  75. package/dist/panelTypes.d.ts +40 -0
  76. package/dist/panelTypes.d.ts.map +1 -0
  77. package/dist/panelTypes.js +2 -0
  78. package/dist/panelTypes.js.map +1 -0
  79. package/dist/panelVisualTheme.d.ts +76 -0
  80. package/dist/panelVisualTheme.d.ts.map +1 -0
  81. package/dist/panelVisualTheme.js +39 -0
  82. package/dist/panelVisualTheme.js.map +1 -0
  83. package/dist/panelWorkbenchHelpers.d.ts +34 -0
  84. package/dist/panelWorkbenchHelpers.d.ts.map +1 -0
  85. package/dist/panelWorkbenchHelpers.js +162 -0
  86. package/dist/panelWorkbenchHelpers.js.map +1 -0
  87. package/dist/panelWorkbenchView.d.ts +77 -0
  88. package/dist/panelWorkbenchView.d.ts.map +1 -0
  89. package/dist/panelWorkbenchView.js +409 -0
  90. package/dist/panelWorkbenchView.js.map +1 -0
  91. package/dist/query.d.ts +11 -0
  92. package/dist/query.d.ts.map +1 -0
  93. package/dist/query.js +21 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/styles.d.ts +8 -0
  96. package/dist/styles.d.ts.map +1 -0
  97. package/dist/styles.js +1073 -0
  98. package/dist/styles.js.map +1 -0
  99. package/package.json +29 -0
  100. package/src/FrameGraphInspector.ts +632 -0
  101. package/src/debugCaptureModel.ts +583 -0
  102. package/src/index.ts +21 -0
  103. package/src/panelAliasAnalysis.ts +164 -0
  104. package/src/panelCytoscapeGraphRenderer.ts +617 -0
  105. package/src/panelDiagnosticsView.ts +160 -0
  106. package/src/panelDomHelpers.ts +151 -0
  107. package/src/panelGraphLayout.ts +281 -0
  108. package/src/panelGraphRenderer.ts +21 -0
  109. package/src/panelGraphScene.ts +787 -0
  110. package/src/panelGraphView.ts +108 -0
  111. package/src/panelGraphVisuals.ts +364 -0
  112. package/src/panelIcons.ts +56 -0
  113. package/src/panelInspectorView.ts +377 -0
  114. package/src/panelMemoryView.ts +154 -0
  115. package/src/panelPassesView.ts +222 -0
  116. package/src/panelResourcesView.ts +115 -0
  117. package/src/panelSelection.ts +99 -0
  118. package/src/panelTypes.ts +28 -0
  119. package/src/panelVisualTheme.ts +39 -0
  120. package/src/panelWorkbenchHelpers.ts +216 -0
  121. package/src/panelWorkbenchView.ts +465 -0
  122. package/src/query.ts +22 -0
  123. 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