@zenfg/inspector 0.1.0-beta.2 → 0.1.0-beta.4

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 (172) hide show
  1. package/GUIDE.md +182 -0
  2. package/README.md +60 -26
  3. package/THEMING.md +124 -0
  4. package/dist/FrameGraphInspector.d.ts +24 -8
  5. package/dist/FrameGraphInspector.d.ts.map +1 -1
  6. package/dist/FrameGraphInspector.js +151 -65
  7. package/dist/FrameGraphInspector.js.map +1 -1
  8. package/dist/capture.d.ts +5 -0
  9. package/dist/capture.d.ts.map +1 -0
  10. package/dist/capture.js +2 -0
  11. package/dist/capture.js.map +1 -0
  12. package/dist/debugCaptureModel.d.ts +28 -4
  13. package/dist/debugCaptureModel.d.ts.map +1 -1
  14. package/dist/debugCaptureModel.js +72 -16
  15. package/dist/debugCaptureModel.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/panelAliasAnalysis.d.ts +1 -0
  20. package/dist/panelAliasAnalysis.d.ts.map +1 -1
  21. package/dist/panelAliasAnalysis.js +23 -54
  22. package/dist/panelAliasAnalysis.js.map +1 -1
  23. package/dist/panelCytoscapeGraphRenderer.d.ts +18 -3
  24. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
  25. package/dist/panelCytoscapeGraphRenderer.js +183 -41
  26. package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
  27. package/dist/panelDetailLayout.d.ts +27 -0
  28. package/dist/panelDetailLayout.d.ts.map +1 -0
  29. package/dist/panelDetailLayout.js +174 -0
  30. package/dist/panelDetailLayout.js.map +1 -0
  31. package/dist/panelDetailStyles.d.ts +2 -0
  32. package/dist/panelDetailStyles.d.ts.map +1 -0
  33. package/dist/panelDetailStyles.js +57 -0
  34. package/dist/panelDetailStyles.js.map +1 -0
  35. package/dist/panelDiagnosticsView.d.ts +10 -1
  36. package/dist/panelDiagnosticsView.d.ts.map +1 -1
  37. package/dist/panelDiagnosticsView.js +167 -58
  38. package/dist/panelDiagnosticsView.js.map +1 -1
  39. package/dist/panelDomHelpers.d.ts.map +1 -1
  40. package/dist/panelDomHelpers.js +6 -5
  41. package/dist/panelDomHelpers.js.map +1 -1
  42. package/dist/panelGraphLayout.d.ts +3 -2
  43. package/dist/panelGraphLayout.d.ts.map +1 -1
  44. package/dist/panelGraphLayout.js +20 -5
  45. package/dist/panelGraphLayout.js.map +1 -1
  46. package/dist/panelGraphRenderer.d.ts +10 -0
  47. package/dist/panelGraphRenderer.d.ts.map +1 -1
  48. package/dist/panelGraphRenderer.js.map +1 -1
  49. package/dist/panelGraphScene.d.ts +15 -26
  50. package/dist/panelGraphScene.d.ts.map +1 -1
  51. package/dist/panelGraphScene.js +191 -208
  52. package/dist/panelGraphScene.js.map +1 -1
  53. package/dist/panelGraphSearch.d.ts +16 -0
  54. package/dist/panelGraphSearch.d.ts.map +1 -0
  55. package/dist/panelGraphSearch.js +93 -0
  56. package/dist/panelGraphSearch.js.map +1 -0
  57. package/dist/panelGraphView.d.ts +82 -0
  58. package/dist/panelGraphView.d.ts.map +1 -1
  59. package/dist/panelGraphView.js +42 -9
  60. package/dist/panelGraphView.js.map +1 -1
  61. package/dist/panelGraphVisuals.d.ts +18 -6
  62. package/dist/panelGraphVisuals.d.ts.map +1 -1
  63. package/dist/panelGraphVisuals.js +156 -116
  64. package/dist/panelGraphVisuals.js.map +1 -1
  65. package/dist/panelInspectorView.d.ts +13 -0
  66. package/dist/panelInspectorView.d.ts.map +1 -1
  67. package/dist/panelInspectorView.js +326 -46
  68. package/dist/panelInspectorView.js.map +1 -1
  69. package/dist/panelListStyles.d.ts +3 -0
  70. package/dist/panelListStyles.d.ts.map +1 -0
  71. package/dist/panelListStyles.js +155 -0
  72. package/dist/panelListStyles.js.map +1 -0
  73. package/dist/panelMemoryStyles.d.ts +3 -0
  74. package/dist/panelMemoryStyles.d.ts.map +1 -0
  75. package/dist/panelMemoryStyles.js +42 -0
  76. package/dist/panelMemoryStyles.js.map +1 -0
  77. package/dist/panelMemoryView.d.ts +13 -1
  78. package/dist/panelMemoryView.d.ts.map +1 -1
  79. package/dist/panelMemoryView.js +161 -41
  80. package/dist/panelMemoryView.js.map +1 -1
  81. package/dist/panelPassesView.d.ts +15 -0
  82. package/dist/panelPassesView.d.ts.map +1 -1
  83. package/dist/panelPassesView.js +202 -72
  84. package/dist/panelPassesView.js.map +1 -1
  85. package/dist/panelRawView.d.ts +15 -0
  86. package/dist/panelRawView.d.ts.map +1 -0
  87. package/dist/panelRawView.js +105 -0
  88. package/dist/panelRawView.js.map +1 -0
  89. package/dist/panelResourcesView.d.ts +8 -0
  90. package/dist/panelResourcesView.d.ts.map +1 -1
  91. package/dist/panelResourcesView.js +62 -31
  92. package/dist/panelResourcesView.js.map +1 -1
  93. package/dist/panelSelection.d.ts +9 -1
  94. package/dist/panelSelection.d.ts.map +1 -1
  95. package/dist/panelSelection.js +36 -6
  96. package/dist/panelSelection.js.map +1 -1
  97. package/dist/panelTheme.d.ts +13 -0
  98. package/dist/panelTheme.d.ts.map +1 -0
  99. package/dist/panelTheme.js +104 -0
  100. package/dist/panelTheme.js.map +1 -0
  101. package/dist/panelTypes.d.ts +18 -4
  102. package/dist/panelTypes.d.ts.map +1 -1
  103. package/dist/panelTypes.js.map +1 -1
  104. package/dist/panelVisualTheme.d.ts +176 -71
  105. package/dist/panelVisualTheme.d.ts.map +1 -1
  106. package/dist/panelVisualTheme.js +34 -36
  107. package/dist/panelVisualTheme.js.map +1 -1
  108. package/dist/panelWorkbenchHelpers.d.ts +12 -1
  109. package/dist/panelWorkbenchHelpers.d.ts.map +1 -1
  110. package/dist/panelWorkbenchHelpers.js +63 -2
  111. package/dist/panelWorkbenchHelpers.js.map +1 -1
  112. package/dist/panelWorkbenchStyles.d.ts +2 -0
  113. package/dist/panelWorkbenchStyles.d.ts.map +1 -0
  114. package/dist/panelWorkbenchStyles.js +101 -0
  115. package/dist/panelWorkbenchStyles.js.map +1 -0
  116. package/dist/panelWorkbenchView.d.ts +16 -0
  117. package/dist/panelWorkbenchView.d.ts.map +1 -1
  118. package/dist/panelWorkbenchView.js +228 -50
  119. package/dist/panelWorkbenchView.js.map +1 -1
  120. package/dist/query.js.map +1 -1
  121. package/dist/styles.d.ts.map +1 -1
  122. package/dist/styles.js +161 -205
  123. package/dist/styles.js.map +1 -1
  124. package/dist/theme.d.ts +22 -0
  125. package/dist/theme.d.ts.map +1 -0
  126. package/dist/theme.js +10 -0
  127. package/dist/theme.js.map +1 -0
  128. package/dist/themeDefinitions.d.ts +251 -0
  129. package/dist/themeDefinitions.d.ts.map +1 -0
  130. package/dist/themeDefinitions.js +96 -0
  131. package/dist/themeDefinitions.js.map +1 -0
  132. package/dist/themePalette.d.ts +66 -0
  133. package/dist/themePalette.d.ts.map +1 -0
  134. package/dist/themePalette.js +37 -0
  135. package/dist/themePalette.js.map +1 -0
  136. package/dist/themes.css +165 -0
  137. package/package.json +44 -10
  138. package/src/FrameGraphInspector.ts +145 -68
  139. package/src/capture.ts +4 -0
  140. package/src/debugCaptureModel.ts +97 -21
  141. package/src/index.ts +6 -2
  142. package/src/panelAliasAnalysis.ts +24 -70
  143. package/src/panelCytoscapeGraphRenderer.ts +177 -40
  144. package/src/panelDetailLayout.ts +155 -0
  145. package/src/panelDetailStyles.ts +56 -0
  146. package/src/panelDiagnosticsView.ts +167 -86
  147. package/src/panelDomHelpers.ts +4 -5
  148. package/src/panelGraphLayout.ts +20 -5
  149. package/src/panelGraphRenderer.ts +7 -0
  150. package/src/panelGraphScene.ts +204 -292
  151. package/src/panelGraphSearch.ts +96 -0
  152. package/src/panelGraphView.ts +41 -9
  153. package/src/panelGraphVisuals.ts +152 -118
  154. package/src/panelInspectorView.ts +315 -48
  155. package/src/panelListStyles.ts +154 -0
  156. package/src/panelMemoryStyles.ts +41 -0
  157. package/src/panelMemoryView.ts +178 -53
  158. package/src/panelPassesView.ts +190 -137
  159. package/src/panelRawView.ts +99 -0
  160. package/src/panelResourcesView.ts +69 -56
  161. package/src/panelSelection.ts +39 -7
  162. package/src/panelTheme.ts +85 -0
  163. package/src/panelTypes.ts +16 -4
  164. package/src/panelVisualTheme.ts +38 -38
  165. package/src/panelWorkbenchHelpers.ts +62 -4
  166. package/src/panelWorkbenchStyles.ts +100 -0
  167. package/src/panelWorkbenchView.ts +201 -52
  168. package/src/query.ts +11 -11
  169. package/src/styles.ts +162 -205
  170. package/src/theme.ts +24 -0
  171. package/src/themeDefinitions.ts +103 -0
  172. package/src/themePalette.ts +44 -0
@@ -1,3 +1,6 @@
1
+ import type { FrameGraphCaptureRequest } from './capture.ts';
2
+ import type { InspectorTheme } from './theme.ts';
3
+ import { InspectorThemeController, resolveGraphTheme } from './panelTheme.ts';
1
4
  import {
2
5
  FrameGraphSnapshotValidationError,
3
6
  decodeFrameGraphSnapshot,
@@ -11,20 +14,23 @@ import {
11
14
  type FrameGraphDebugViewModel,
12
15
  } from './debugCaptureModel.ts';
13
16
  import { createToolbarButton } from './panelDomHelpers.ts';
14
- import { createPanelIcon } from './panelIcons.ts';
15
17
  import {
16
18
  destroyGraph,
17
19
  fitGraph,
20
+ resolveGraphScene,
21
+ renderGraphLegend,
18
22
  } from './panelGraphView.ts';
19
- import { graphGroupElementId } from './panelGraphScene.ts';
20
- import { selectionExists } from './panelSelection.ts';
21
- import type { GraphViewState, Selection } from './panelTypes.ts';
23
+ import { graphGroupElementId, selectionKey } from './panelGraphScene.ts';
24
+ import { resolveNodeSelection, selectionExists } from './panelSelection.ts';
25
+ import type { GraphViewState, Selection, WorkbenchTab } from './panelTypes.ts';
22
26
  import { ensureFrameGraphInspectorStyles } from './styles.ts';
23
27
  import { sameSelection, type WorkbenchCallbacks } from './panelWorkbenchHelpers.ts';
24
28
  import { FrameGraphDebugWorkbench } from './panelWorkbenchView.ts';
25
29
 
26
30
  /** Construction and safety limits for an embedded {@link FrameGraphInspector}. */
27
31
  export type FrameGraphInspectorOptions = {
32
+ /** Optional CSS theme. Without one, host variables inherit over the Storm defaults. */
33
+ theme?: InspectorTheme;
28
34
  /**
29
35
  * Product label shown in the workbench command bar. Pass `false` to hide
30
36
  * visible branding while retaining the inspector's accessible name.
@@ -36,7 +42,7 @@ export type FrameGraphInspectorOptions = {
36
42
  * Produces a live snapshot when capture is requested. The inspector awaits
37
43
  * promises and displays thrown or rejected errors in its status area.
38
44
  */
39
- captureSnapshot?: () => FrameGraphSnapshot | undefined | Promise<FrameGraphSnapshot | undefined>;
45
+ captureSnapshot?: (request: FrameGraphCaptureRequest) => FrameGraphSnapshot | undefined | Promise<FrameGraphSnapshot | undefined>;
40
46
  /**
41
47
  * Maximum accepted import size in bytes.
42
48
  *
@@ -74,13 +80,13 @@ const DEFAULT_BRANDING = 'ZenFG Inspector';
74
80
  export class FrameGraphInspector {
75
81
  /** Root element owned by this inspector instance. */
76
82
  readonly dom = document.createElement('section');
83
+ private readonly themeController = new InspectorThemeController(this.dom);
77
84
 
78
85
  private readonly body = document.createElement('div');
79
86
  private readonly content = document.createElement('div');
80
87
  private readonly dropOverlay = document.createElement('div');
81
- private readonly passesGraphModeButton: HTMLButtonElement;
82
- private readonly resourcesGraphModeButton: HTMLButtonElement;
83
88
  private readonly groupsButton: HTMLButtonElement;
89
+ private readonly declarationsButton: HTMLButtonElement;
84
90
  private readonly collapseGroupsButton: HTMLButtonElement;
85
91
  private readonly graphView: GraphViewState;
86
92
  private readonly workbench: FrameGraphDebugWorkbench;
@@ -101,6 +107,7 @@ export class FrameGraphInspector {
101
107
  private copyFeedbackTimeout: number | undefined;
102
108
  private readonly maxImportBytes: number;
103
109
  private dragDepth = 0;
110
+ private revealRevision = 0;
104
111
  private readonly handleDragEnter = (event: DragEvent): void => {
105
112
  if (!isFileDrag(event)) return;
106
113
  event.preventDefault();
@@ -160,9 +167,10 @@ export class FrameGraphInspector {
160
167
  host: document.createElement('div'),
161
168
  toolbar: document.createElement('div'),
162
169
  legend: graphLegend,
170
+ refreshTheme: () => this.refreshTheme(),
163
171
  layoutElementBudget: normalizeLimit(options.maxGraphElements, DEFAULT_MAX_GRAPH_ELEMENTS, 'maxGraphElements'),
164
- graphMode: 'passes',
165
172
  groupsEnabled: true,
173
+ showResourceDeclarations: true,
166
174
  expandedGroupPaths: new Set(),
167
175
  fitOnNextRender: true,
168
176
  };
@@ -171,32 +179,27 @@ export class FrameGraphInspector {
171
179
  this.graphView.toolbar.setAttribute('role', 'toolbar');
172
180
  this.graphView.toolbar.setAttribute('aria-label', 'Frame graph view controls');
173
181
 
174
- this.passesGraphModeButton = createToolbarButton('Pass dependency', 'Show pass dependency graph', () => this.setGraphMode('passes'));
175
- this.resourcesGraphModeButton = createToolbarButton('Resource access', 'Show resource access graph', () => this.setGraphMode('resources'));
182
+ this.declarationsButton = createToolbarButton('Declarations', 'Show resource declaration nodes', () => this.toggleDeclarations());
176
183
  this.groupsButton = createToolbarButton('Groups', 'Toggle diagnostic group projection', () => this.toggleGroups());
177
184
  this.collapseGroupsButton = createToolbarButton('Collapse All', 'Collapse every diagnostic group', () => this.collapseAllGroups());
178
- this.passesGraphModeButton.classList.add('zenfg-inspector-mode-button');
179
- this.resourcesGraphModeButton.classList.add('zenfg-inspector-mode-button');
180
-
181
- const modeControls = document.createElement('div');
182
- modeControls.className = 'zenfg-inspector-graph-mode-controls';
183
- modeControls.setAttribute('role', 'group');
184
- modeControls.setAttribute('aria-label', 'Graph mode');
185
- modeControls.append(this.passesGraphModeButton, this.resourcesGraphModeButton);
185
+
186
186
  const actionControls = document.createElement('div');
187
187
  actionControls.className = 'zenfg-inspector-graph-action-controls';
188
188
  actionControls.append(
189
+ this.declarationsButton,
189
190
  this.groupsButton,
190
191
  this.collapseGroupsButton,
191
- createGraphIconButton('fit', 'Fit graph to view', () => fitGraph(this.graphView)),
192
+ createToolbarButton('Fit', 'Fit graph to view', () => fitGraph(this.graphView)),
192
193
  );
193
- this.graphView.toolbar.append(modeControls, graphLegend, actionControls);
194
+ this.graphView.toolbar.append(actionControls);
194
195
 
195
196
  const callbacks: WorkbenchCallbacks = {
196
197
  onSelect: (selection) => this.handleSelect(selection),
197
198
  onHover: (selection) => this.handleHover(selection),
198
199
  onGroupToggle: (pathKey) => this.toggleGroup(pathKey),
199
200
  isGroupExpanded: (pathKey) => this.graphView.expandedGroupPaths.has(pathKey),
201
+ onReveal: (selection, tab) => this.handleReveal(selection, tab),
202
+ onNavigate: (tab, filter) => this.workbench.navigate(tab, filter),
200
203
  };
201
204
  this.workbench = new FrameGraphDebugWorkbench(this.graphView, callbacks, {
202
205
  onCapture: () => { void this.captureSnapshot(); },
@@ -216,22 +219,49 @@ export class FrameGraphInspector {
216
219
  this.dom.addEventListener('dragend', this.handleDragEnd);
217
220
  this.dom.addEventListener('drop', this.handleDrop);
218
221
  this.updateCaptureActions();
219
- this.updateGraphModeButtonState();
222
+ this.updateGraphControls();
220
223
  this.showEmptyState();
221
- queueMicrotask(() => this.maybeAutoCapture());
224
+ if (options.theme) this.setTheme(options.theme);
225
+ queueMicrotask(() => { if (!this.destroyed) { this.refreshTheme(); this.maybeAutoCapture(); } });
222
226
  }
223
227
 
228
+ /** Replace API-applied variables on this instance, then synchronize graph styles.
229
+ * Pass null to restore host CSS and defaults. Does not change capture or interaction state.
230
+ */
231
+ setTheme(theme: InspectorTheme | null): void {
232
+ if (this.destroyed) return;
233
+ this.themeController.apply(theme);
234
+ this.refreshTheme();
235
+ }
236
+
237
+ /** Re-read final CSS values after external CSS changes and refresh graph and legend.
238
+ * Does not write theme variables, recapture, fit, or rebuild the component.
239
+ */
240
+ refreshTheme(): void {
241
+ if (this.destroyed) return;
242
+ const theme = resolveGraphTheme(this.dom);
243
+ this.graphView.theme = theme;
244
+ this.graphView.renderer?.setTheme?.(theme);
245
+ if (this.viewModel) renderGraphLegend(this.graphView.legend, this.viewModel, theme, this.graphView.showResourceDeclarations);
246
+ }
247
+
224
248
  /**
225
249
  * Replaces or removes the live-capture provider.
226
250
  *
227
251
  * @remarks Installing a provider may immediately begin an asynchronous
228
- * capture when no snapshot is displayed.
252
+ * capture when no snapshot is displayed. Replacing or removing a provider
253
+ * invalidates its pending capture results without cancelling the provider
254
+ * promise. An already displayed snapshot and any pending import are preserved.
229
255
  */
230
256
  setCaptureSnapshotProvider(provider: FrameGraphInspectorOptions['captureSnapshot']): void {
231
- const changed = this.captureSnapshotCallback !== provider;
257
+ if (this.destroyed || this.captureSnapshotCallback === provider) return;
258
+ if (this.capturing) {
259
+ this.operationRevision += 1;
260
+ this.capturing = false;
261
+ }
232
262
  this.captureSnapshotCallback = provider;
233
- if (changed && !this.viewModel) this.initialAutoCaptureAttempted = false;
234
- if (!provider && !this.viewModel) this.showEmptyState();
263
+ if (!this.viewModel) this.initialAutoCaptureAttempted = false;
264
+ if (!provider && !this.viewModel && !this.importing) this.showEmptyState();
235
265
  this.updateCaptureActions();
236
266
  this.maybeAutoCapture();
237
267
  }
@@ -257,7 +287,8 @@ export class FrameGraphInspector {
257
287
  if (!this.viewModel) this.workbench.showEmptyState('capturing', 'Capturing the next rendered frame…');
258
288
  this.updateCaptureActions();
259
289
  try {
260
- const snapshot = await this.captureSnapshotCallback();
290
+ const request: FrameGraphCaptureRequest = { timing: 'both' };
291
+ const snapshot = await this.captureSnapshotCallback(request);
261
292
  if (this.destroyed || revision !== this.operationRevision) return;
262
293
  if (!snapshot) {
263
294
  this.reportCaptureIssue('No snapshot was produced. Capture again when rendering is active.');
@@ -282,9 +313,9 @@ export class FrameGraphInspector {
282
313
  }
283
314
 
284
315
  /**
285
- * Validates and synchronously displays a programmatic Snapshot 1.0 value.
316
+ * Validates and synchronously displays a programmatic Snapshot 1.2 value.
286
317
  *
287
- * @throws {@link FrameGraphSnapshotValidationError} if `snapshot` is invalid.
318
+ * @throws {@link @zenfg/snapshot!index.FrameGraphSnapshotValidationError | FrameGraphSnapshotValidationError} if `snapshot` is invalid.
288
319
  */
289
320
  setSnapshot(snapshot: FrameGraphSnapshot): void {
290
321
  if (this.destroyed) return;
@@ -334,10 +365,17 @@ export class FrameGraphInspector {
334
365
  });
335
366
  const sourceFormat = decoded.snapshot.capture.migration?.sourceFormat;
336
367
  if (sourceFormat) {
337
- const sourceLabel = sourceFormat === 'legacy-v0' ? 'Legacy V0' : 'Legacy Candidate V1';
368
+ // The current import format can differ from retained historical provenance.
369
+ const sourceLabel = {
370
+ 'legacy-v0': 'Legacy V0',
371
+ 'legacy-candidate-v1': 'Legacy Candidate V1',
372
+ 'snapshot-v1.1': 'Snapshot 1.1',
373
+ }[decoded.migrated && decoded.source !== 'v1' ? decoded.source : sourceFormat];
374
+ const version = decoded.snapshot.version;
375
+ const targetLabel = `Snapshot ${version.major}.${version.minor}`;
338
376
  this.statusMessage = decoded.migrated
339
- ? `Imported ${sourceLabel} and migrated it to ZenFG Snapshot V1.`
340
- : `Imported Snapshot V1 with ${sourceLabel} migration provenance.`;
377
+ ? `Imported ${sourceLabel} and migrated it to ZenFG ${targetLabel}.`
378
+ : `Imported ${targetLabel} with ${sourceLabel} migration provenance.`;
341
379
  this.statusTone = 'neutral';
342
380
  }
343
381
  }
@@ -413,18 +451,18 @@ export class FrameGraphInspector {
413
451
  this.protocolSnapshot = snapshot;
414
452
  this.viewModel = viewModel;
415
453
  this.graphView.fitOnNextRender = true;
454
+ this.graphView.captureRevision = (this.graphView.captureRevision ?? 0) + 1;
455
+ this.graphView.revealOnNextRender = undefined;
456
+ this.graphView.renderer?.cancelReveal?.();
416
457
  this.hovered = undefined;
417
458
  this.initialAutoCaptureAttempted = true;
418
459
  this.statusMessage = undefined;
419
460
  this.statusTone = 'neutral';
420
- if (!this.selected || !selectionExists(viewModel, this.selected)) {
421
- this.selected = viewModel.nodes[0]
422
- ? { kind: 'node', id: viewModel.nodes[0].id }
423
- : undefined;
424
- }
461
+ if (this.selected?.kind === 'node' || this.selected?.kind === 'culled') this.selected = resolveNodeSelection(viewModel, this.selected.id);
462
+ else if (this.selected && !selectionExists(viewModel, this.selected)) this.selected = undefined;
425
463
  this.workbench.setSnapshot(viewModel, this.selected);
426
464
  this.updateCaptureActions();
427
- this.updateGraphModeButtonState();
465
+ this.updateGraphControls();
428
466
  }
429
467
 
430
468
  /**
@@ -442,7 +480,9 @@ export class FrameGraphInspector {
442
480
  this.dom.removeEventListener('dragleave', this.handleDragLeave);
443
481
  this.dom.removeEventListener('dragend', this.handleDragEnd);
444
482
  this.dom.removeEventListener('drop', this.handleDrop);
483
+ this.themeController.destroy();
445
484
  destroyGraph(this.graphView);
485
+ this.workbench.destroy();
446
486
  this.dom.remove();
447
487
  }
448
488
 
@@ -472,59 +512,102 @@ export class FrameGraphInspector {
472
512
  void this.captureSnapshot();
473
513
  }
474
514
  private handleSelect(selection: Selection): void {
515
+ if (!this.viewModel || !selectionExists(this.viewModel, selection)) return;
475
516
  this.selected = selection;
476
517
  this.workbench.setSelection(selection);
477
518
  }
478
519
 
520
+ private handleReveal(selection: Selection, tab: WorkbenchTab): void {
521
+ const snapshot = this.viewModel;
522
+ if (!snapshot || !selectionExists(snapshot, selection)) return;
523
+ if (tab === 'graph') {
524
+ const expanded = new Set(this.graphView.expandedGroupPaths);
525
+ const groupsEnabled = this.graphView.groupsEnabled;
526
+ const showResourceDeclarations = this.graphView.showResourceDeclarations;
527
+ if (selection.kind === 'resource') this.graphView.showResourceDeclarations = true;
528
+ if (selection.kind === 'group') this.graphView.groupsEnabled = true;
529
+ let groupId: string | undefined;
530
+ if (selection.kind === 'node') groupId = snapshot.nodeById.get(selection.id)?.debugGroupId;
531
+ else if (selection.kind === 'resource') groupId = snapshot.resourceById.get(selection.id)?.debugGroupId;
532
+ else if (selection.kind === 'group') groupId = snapshot.groupByPathKey.get(selection.pathKey)?.parentId;
533
+ if (this.graphView.groupsEnabled && groupId) {
534
+ for (const id of snapshot.groupById.get(groupId)?.ancestorIds ?? []) {
535
+ const group = snapshot.groupById.get(id);
536
+ if (group) this.graphView.expandedGroupPaths.add(group.pathKey);
537
+ }
538
+ }
539
+ const scene = resolveGraphScene(this.graphView, snapshot);
540
+ const availableIds = new Set([...scene.nodes, ...scene.edges].map((entry) => entry.id));
541
+ if (!(scene.interaction.primaryElementIdsBySelection.get(selectionKey(selection))?.some((id) => availableIds.has(id)))) {
542
+ this.graphView.groupsEnabled = groupsEnabled;
543
+ this.graphView.showResourceDeclarations = showResourceDeclarations;
544
+ this.graphView.expandedGroupPaths.clear();
545
+ for (const path of expanded) this.graphView.expandedGroupPaths.add(path);
546
+ this.workbench.showNavigationIssue(selection.kind === 'culled' ? 'Culled passes are not part of Frame Flow.' : 'This object has no representation in Frame Flow.', selection);
547
+ return;
548
+ }
549
+ this.graphView.revealOnNextRender = { selection, revision: ++this.revealRevision };
550
+ this.updateGraphControls();
551
+ }
552
+ this.handleSelect(selection);
553
+ this.workbench.reveal(selection, tab);
554
+ }
555
+
479
556
  private handleHover(selection: Selection | undefined): void {
480
557
  if (!this.viewModel || sameSelection(this.hovered, selection)) return;
481
558
  this.hovered = selection;
482
559
  this.workbench.setHovered(selection);
483
560
  }
484
561
 
485
- private setGraphMode(mode: GraphViewState['graphMode']): void {
486
- if (this.graphView.graphMode === mode) return;
487
- this.graphView.graphMode = mode;
488
- this.graphView.fitOnNextRender = true;
489
- this.updateGraphModeButtonState();
490
- this.workbench.refreshGraphStructure();
491
- }
492
-
493
- private updateGraphModeButtonState(): void {
494
- const passesActive = this.graphView.graphMode === 'passes';
562
+ private updateGraphControls(): void {
563
+ const showDeclarations = this.graphView.showResourceDeclarations ?? true;
564
+ this.declarationsButton.classList.toggle('active', showDeclarations);
565
+ this.declarationsButton.setAttribute('aria-pressed', String(showDeclarations));
495
566
  const hasGroups = (this.viewModel?.debugGroups.length ?? 0) > 0;
496
- this.passesGraphModeButton.classList.toggle('active', passesActive);
497
- this.resourcesGraphModeButton.classList.toggle('active', !passesActive);
498
- this.passesGraphModeButton.setAttribute('aria-pressed', passesActive ? 'true' : 'false');
499
- this.resourcesGraphModeButton.setAttribute('aria-pressed', passesActive ? 'false' : 'true');
500
- this.groupsButton.disabled = !passesActive || !hasGroups;
501
- this.groupsButton.classList.toggle('active', passesActive && hasGroups && this.graphView.groupsEnabled);
502
- this.groupsButton.setAttribute('aria-pressed', passesActive && hasGroups && this.graphView.groupsEnabled ? 'true' : 'false');
567
+ this.groupsButton.disabled = !hasGroups;
568
+ this.groupsButton.hidden = !hasGroups;
569
+ this.collapseGroupsButton.hidden = !hasGroups;
570
+ this.groupsButton.classList.toggle('active', hasGroups && this.graphView.groupsEnabled);
571
+ this.groupsButton.setAttribute('aria-pressed', hasGroups && this.graphView.groupsEnabled ? 'true' : 'false');
503
572
  const hasExpanded = this.viewModel?.debugGroups.some((group) => this.graphView.expandedGroupPaths.has(group.pathKey)) ?? false;
504
- this.collapseGroupsButton.disabled = !passesActive || !hasGroups || !this.graphView.groupsEnabled || !hasExpanded;
573
+ this.collapseGroupsButton.disabled = !hasGroups || !this.graphView.groupsEnabled || !hasExpanded;
574
+ }
575
+
576
+ private toggleDeclarations(): void {
577
+ this.handleHover(undefined);
578
+ this.graphView.showResourceDeclarations = !(this.graphView.showResourceDeclarations ?? true);
579
+ this.graphView.renderer?.cancelReveal?.();
580
+ this.graphView.revealOnNextRender = undefined;
581
+ this.graphView.anchorElementIdOnNextRender = undefined;
582
+ this.graphView.fitOnNextRender = true;
583
+ this.updateGraphControls();
584
+ this.workbench.refreshGraphStructure();
505
585
  }
506
586
 
507
587
  private toggleGroups(): void {
508
- if (this.graphView.graphMode !== 'passes' || !this.viewModel?.debugGroups.length) return;
588
+ if (!this.viewModel?.debugGroups.length) return;
589
+ this.handleHover(undefined);
509
590
  this.graphView.groupsEnabled = !this.graphView.groupsEnabled;
510
591
  this.graphView.fitOnNextRender = true;
511
- this.updateGraphModeButtonState();
592
+ this.updateGraphControls();
512
593
  this.workbench.refreshGraphStructure();
513
594
  }
514
595
 
515
596
  private toggleGroup(pathKey: string): void {
597
+ this.handleHover(undefined);
516
598
  if (this.graphView.expandedGroupPaths.has(pathKey)) this.graphView.expandedGroupPaths.delete(pathKey);
517
599
  else this.graphView.expandedGroupPaths.add(pathKey);
518
600
  this.graphView.anchorElementIdOnNextRender = graphGroupElementId(pathKey);
519
- this.updateGraphModeButtonState();
601
+ this.updateGraphControls();
520
602
  this.workbench.refreshGraphStructure();
521
603
  }
522
604
 
523
605
  private collapseAllGroups(): void {
524
606
  if (this.graphView.expandedGroupPaths.size === 0) return;
607
+ this.handleHover(undefined);
525
608
  this.graphView.expandedGroupPaths.clear();
526
609
  this.graphView.fitOnNextRender = true;
527
- this.updateGraphModeButtonState();
610
+ this.updateGraphControls();
528
611
  this.workbench.refreshGraphStructure();
529
612
  }
530
613
 
@@ -568,16 +651,10 @@ export class FrameGraphInspector {
568
651
  export function mountFrameGraphInspector(host: HTMLElement, options?: FrameGraphInspectorOptions): FrameGraphInspector {
569
652
  const inspector = new FrameGraphInspector(options);
570
653
  host.appendChild(inspector.dom);
654
+ inspector.refreshTheme();
571
655
  return inspector;
572
656
  }
573
657
 
574
- function createGraphIconButton(icon: 'fit', title: string, onClick: () => void): HTMLButtonElement {
575
- const button = createToolbarButton('', title, onClick);
576
- button.classList.add('zenfg-inspector-icon-button');
577
- button.appendChild(createPanelIcon(icon));
578
- return button;
579
- }
580
-
581
658
  async function writeTextToClipboard(text: string): Promise<void> {
582
659
  if (navigator.clipboard?.writeText) {
583
660
  await navigator.clipboard.writeText(text);
@@ -618,7 +695,7 @@ function formatImportLimit(bytes: number): string {
618
695
  }
619
696
 
620
697
  function formatIssues(issues: readonly { readonly path: string; readonly message: string }[]): string {
621
- return issues.slice(0, 3).map((issue) => `${issue.path || '/'}: ${issue.message}`).join('; ');
698
+ return issues.map((issue) => `${issue.path || '/'}: ${issue.message}`).join('\n');
622
699
  }
623
700
 
624
701
  function isFileDrag(event: DragEvent): boolean {
package/src/capture.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Requested timing families for the next live capture; never changes imported data. */
2
+ export type FrameGraphCaptureRequest = {
3
+ readonly timing: 'cpu' | 'gpu' | 'both';
4
+ };