@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,69 +1,60 @@
1
- import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
1
+ import type { FrameGraphDebugResource, FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
2
  import { createCell, labelResource } from './panelDomHelpers.ts';
3
3
  import type { Selection } from './panelTypes.ts';
4
4
  import {
5
- createEmptyTableRow,
6
- createKindCell,
7
- createFilterSelect,
8
- createSearchInput,
9
- createSelectionCell,
10
- createTableScroller,
11
- createViewToolbar,
12
- formatEstimatedBytes,
13
- formatResourceDescriptor,
14
- groupPath,
15
- registerSelectable,
16
- resourceAccessCounts,
17
- type WorkbenchCallbacks,
18
- updateSelectedRows,
5
+ createEmptyTableRow, createKindCell, createFilterSelect, createSearchInput, createSelectionCell,
6
+ createTableScroller, createViewToolbar, formatEstimatedBytes, groupPath, registerSelectable,
7
+ resourceAccessCounts, selectionKey, type WorkbenchCallbacks, updateSelectedRows,
19
8
  } from './panelWorkbenchHelpers.ts';
20
9
 
10
+ type ResourceSort = 'name' | 'size' | 'lifetime';
11
+
21
12
  export class ResourcesView {
22
13
  readonly root = document.createElement('section');
23
14
  private readonly table = createTableScroller([
24
- { label: 'Resource / Group' },
15
+ { label: 'Name' },
25
16
  { label: 'Type / Origin', column: 'kind' },
26
- { label: 'Descriptor', column: 'code' },
27
17
  { label: 'Estimated', column: 'numeric' },
28
18
  { label: 'Lifetime', column: 'code' },
29
- { label: 'Physical', column: 'numeric' },
30
19
  { label: 'R / W', column: 'numeric' },
31
20
  ]);
32
21
  private readonly rows = new Map<string, HTMLElement[]>();
22
+ private readonly count = document.createElement('span');
23
+ private readonly searchInput: HTMLInputElement;
24
+ private readonly kindSelect: HTMLSelectElement;
25
+ private readonly originSelect: HTMLSelectElement;
33
26
  private snapshot: FrameGraphDebugViewModel | undefined;
34
27
  private selected: Selection | undefined;
35
28
  private search = '';
36
29
  private kind = 'all';
37
30
  private origin = 'all';
31
+ private sort: ResourceSort = 'name';
38
32
 
39
33
  constructor(private readonly callbacks: WorkbenchCallbacks, idPrefix: string) {
40
34
  this.root.className = 'zenfg-inspector-view zenfg-inspector-resources-view';
41
35
  this.root.id = `${idPrefix}-view-resources`;
42
36
  this.root.setAttribute('role', 'tabpanel');
43
- const toolbar = createViewToolbar('Resource filters');
44
- toolbar.append(
45
- createSearchInput('Search resource or group', this.search, (value) => {
46
- this.search = value.trim().toLocaleLowerCase();
47
- this.renderRows();
48
- }),
49
- createFilterSelect('Resource kind', this.kind, [
50
- ['all', 'All kinds'],
51
- ['texture', 'Texture'],
52
- ['buffer', 'Buffer'],
53
- ], (value) => {
54
- this.kind = value;
55
- this.renderRows();
56
- }),
57
- createFilterSelect('Resource origin', this.origin, [
58
- ['all', 'All origins'],
59
- ['transient', 'Transient'],
60
- ['imported', 'Imported'],
61
- ['swapchain', 'Swapchain'],
62
- ], (value) => {
63
- this.origin = value;
64
- this.renderRows();
65
- }),
66
- );
37
+ const toolbar = createViewToolbar('Resource filters and sorting');
38
+ this.searchInput = createSearchInput('Search resource, ID or group', this.search, (value) => {
39
+ this.search = value.trim().toLocaleLowerCase();
40
+ this.renderRows();
41
+ });
42
+ this.kindSelect = createFilterSelect('Resource kind', this.kind, [
43
+ ['all', 'All kinds'], ['texture', 'Texture'], ['buffer', 'Buffer'],
44
+ ], (value) => { this.kind = value; this.renderRows(); });
45
+ this.originSelect = createFilterSelect('Resource origin', this.origin, [
46
+ ['all', 'All origins'], ['transient', 'Transient'], ['imported', 'Imported'], ['surface', 'Surface'],
47
+ ], (value) => { this.origin = value; this.renderRows(); });
48
+ const sort = createFilterSelect('Sort resources', this.sort, [
49
+ ['name', 'Name'], ['size', 'Size: largest first'], ['lifetime', 'Lifetime: first use'],
50
+ ], (value) => { this.sort = value as ResourceSort; this.renderRows(); });
51
+ const clear = document.createElement('button');
52
+ clear.type = 'button';
53
+ clear.textContent = 'Clear filters';
54
+ clear.addEventListener('click', () => { this.clearFilters(); this.renderRows(); });
55
+ this.count.className = 'zenfg-inspector-result-count';
56
+ this.count.setAttribute('role', 'status');
57
+ toolbar.append(this.searchInput, this.kindSelect, this.originSelect, sort, clear, this.count);
67
58
  this.root.append(toolbar, this.table.scroller);
68
59
  }
69
60
 
@@ -77,6 +68,18 @@ export class ResourcesView {
77
68
  updateSelectedRows(this.rows, selected);
78
69
  }
79
70
 
71
+ reveal(selection: Selection): void {
72
+ if (selection.kind !== 'resource') return;
73
+ this.clearFilters();
74
+ this.renderRows();
75
+ this.rows.get(selectionKey(selection))?.[0]?.scrollIntoView?.({ block: 'nearest' });
76
+ }
77
+
78
+ private clearFilters(): void {
79
+ this.search = ''; this.kind = 'all'; this.origin = 'all';
80
+ this.searchInput.value = ''; this.kindSelect.value = 'all'; this.originSelect.value = 'all';
81
+ }
82
+
80
83
  private renderRows(): void {
81
84
  const snapshot = this.snapshot;
82
85
  if (!snapshot) return;
@@ -85,31 +88,41 @@ export class ResourcesView {
85
88
  const resources = snapshot.resources.filter((resource) => {
86
89
  if (this.kind !== 'all' && resource.kind !== this.kind) return false;
87
90
  if (this.origin !== 'all' && resource.origin !== this.origin) return false;
88
- if (this.search.length === 0) return true;
89
- return `${labelResource(resource)} ${groupPath(snapshot, resource.debugGroupId)}`
91
+ return !this.search || `${labelResource(resource)} ${resource.id} ${groupPath(snapshot, resource.debugGroupId)}`
90
92
  .toLocaleLowerCase().includes(this.search);
91
- });
93
+ }).sort((a, b) => this.compareResources(a, b));
94
+ this.count.textContent = `${resources.length} / ${snapshot.resources.length} resources`;
92
95
  for (const resource of resources) {
93
96
  const selection: Selection = { kind: 'resource', id: resource.id };
94
97
  const row = document.createElement('tr');
95
98
  registerSelectable(this.rows, row, selection, this.callbacks);
96
99
  const [reads, writes] = resourceAccessCounts(snapshot, resource.id);
97
100
  const resourceCell = createSelectionCell(labelResource(resource), selection, this.callbacks);
98
- const group = document.createElement('small');
99
- group.textContent = groupPath(snapshot, resource.debugGroupId);
100
- resourceCell.appendChild(group);
101
- row.append(
102
- resourceCell,
101
+ resourceCell.title = resource.id;
102
+ if (resource.debugGroupId !== undefined) {
103
+ const group = document.createElement('small');
104
+ group.textContent = groupPath(snapshot, resource.debugGroupId);
105
+ resourceCell.appendChild(group);
106
+ }
107
+ row.append(resourceCell,
103
108
  createKindCell(resource.kind, `${resource.kind} · ${resource.origin}`),
104
- createCell(formatResourceDescriptor(resource), { column: 'code' }),
105
109
  createCell(formatEstimatedBytes(resource.estimatedByteSize), { column: 'numeric' }),
106
- createCell(resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : '-', { column: 'code' }),
107
- createCell(resource.physicalResourceId === undefined ? '-' : `#${resource.physicalResourceId}`, { column: 'numeric' }),
108
- createCell(`${reads} / ${writes}`, { column: 'numeric' }),
109
- );
110
+ createCell(resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : 'Unknown', { column: 'code' }),
111
+ createCell(`${reads} / ${writes}`, { column: 'numeric' }));
110
112
  this.table.body.appendChild(row);
111
113
  }
112
- if (resources.length === 0) this.table.body.appendChild(createEmptyTableRow(7, 'No resources match the current filters.'));
114
+ if (resources.length === 0) this.table.body.appendChild(createEmptyTableRow(5, 'No resources match the current filters.'));
113
115
  updateSelectedRows(this.rows, this.selected);
114
116
  }
117
+
118
+ private compareResources(a: FrameGraphDebugResource, b: FrameGraphDebugResource): number {
119
+ const byName = () => labelResource(a).localeCompare(labelResource(b)) || a.id.localeCompare(b.id);
120
+ const knownFirst = (left: number | undefined, right: number | undefined, descending: boolean): number => (
121
+ left === undefined ? right === undefined ? 0 : 1
122
+ : right === undefined ? -1 : descending ? right - left : left - right
123
+ );
124
+ if (this.sort === 'size') return knownFirst(a.estimatedByteSize, b.estimatedByteSize, true) || byName();
125
+ if (this.sort === 'lifetime') return knownFirst(a.lifetime?.firstUse, b.lifetime?.firstUse, false) || byName();
126
+ return byName();
127
+ }
115
128
  }
@@ -1,4 +1,4 @@
1
- import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
1
+ import { rootKey, type FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
2
  import type { Selection } from './panelTypes.ts';
3
3
 
4
4
  export function resolveSelectedDetail(
@@ -26,9 +26,9 @@ export function resolveSelectedDetail(
26
26
  case 'group':
27
27
  return snapshot.groupByPathKey.get(selected.pathKey);
28
28
  case 'root':
29
- return snapshot.roots[selected.index];
29
+ return snapshot.roots.find((root) => root.key === selected.key);
30
30
  case 'culled': {
31
- const culled = snapshot.culledNodes[selected.index];
31
+ const culled = snapshot.culledById.get(selected.id);
32
32
  return culled ? {
33
33
  ...culled,
34
34
  node: {
@@ -54,9 +54,9 @@ export function selectionExists(snapshot: FrameGraphDebugViewModel, selected: Se
54
54
  case 'resource':
55
55
  return snapshot.resourceById.has(selected.id);
56
56
  case 'root':
57
- return isValidIndex(selected.index, snapshot.roots.length);
57
+ return snapshot.roots.filter((root) => root.key === selected.key).length === 1;
58
58
  case 'culled':
59
- return isValidIndex(selected.index, snapshot.culledNodes.length);
59
+ return snapshot.culledById.has(selected.id);
60
60
  case 'allocation':
61
61
  return snapshot.allocationById.has(selected.id);
62
62
  case 'segment':
@@ -94,6 +94,38 @@ function debugGroupPath(snapshot: FrameGraphDebugViewModel, groupId: string | un
94
94
  return snapshot.groupById.get(groupId)?.path.join(' / ') ?? `#${groupId}`;
95
95
  }
96
96
 
97
- function isValidIndex(index: number, length: number): boolean {
98
- return Number.isInteger(index) && index >= 0 && index < length;
97
+ /** Resolve identity again after a capture, including retained/culled transitions. */
98
+ export function resolveNodeSelection(snapshot: FrameGraphDebugViewModel, id: string): Selection | undefined {
99
+ if (snapshot.nodeById.has(id)) return { kind: 'node', id };
100
+ if (snapshot.culledById.has(id)) return { kind: 'culled', id };
101
+ return undefined;
102
+ }
103
+
104
+ export type SelectedCanonicalDetail = { readonly path: string; readonly value: unknown };
105
+
106
+ /** Raw shows the canonical source object, never the presentation model. */
107
+ export function resolveSelectedCanonicalDetail(
108
+ snapshot: FrameGraphDebugViewModel,
109
+ selected: Selection | undefined,
110
+ ): SelectedCanonicalDetail | undefined {
111
+ if (!selected) return undefined;
112
+ const protocol = snapshot.protocol;
113
+ function at<T>(values: readonly T[], path: string, predicate: (value: T) => boolean): SelectedCanonicalDetail | undefined {
114
+ const index = values.findIndex(predicate);
115
+ return index < 0 ? undefined : { path: `${path}[${index}]`, value: values[index] };
116
+ }
117
+ switch (selected.kind) {
118
+ case 'node':
119
+ case 'culled': return at(protocol.graph.nodes, '$.graph.nodes', (node) => node.id === selected.id);
120
+ case 'resource': return at(protocol.graph.resources, '$.graph.resources', (resource) => resource.id === selected.id);
121
+ case 'group': {
122
+ const id = snapshot.groupByPathKey.get(selected.pathKey)?.id;
123
+ return at(protocol.graph.groups, '$.graph.groups', (group) => group.id === id);
124
+ }
125
+ case 'root': return at(protocol.graph.roots, '$.graph.roots', (root) => rootKey(root) === selected.key);
126
+ case 'allocation': return protocol.memory.allocationReport.status === 'available'
127
+ ? at(protocol.memory.allocationReport.allocations, '$.memory.allocationReport.allocations', (allocation) => allocation.id === selected.id)
128
+ : undefined;
129
+ case 'segment': return at(protocol.graph.segments, '$.graph.segments', (segment) => segment.order === selected.index);
130
+ }
99
131
  }
@@ -0,0 +1,85 @@
1
+ import type { InspectorTheme, InspectorThemeVariables } from './theme.ts';
2
+ import { internalThemeProperty, stormVariables } from './themeDefinitions.ts';
3
+ import { createGraphVisualTheme, defaultThemeValue, type GraphVisualTheme } from './panelVisualTheme.ts';
4
+
5
+ /** Own only declarations written by this API, restoring pre-existing inline styles on reset. */
6
+ export class InspectorThemeController {
7
+ private readonly owned = new Map<string, { previous: string; priority: string; applied: string }>();
8
+ constructor(private readonly root: HTMLElement) {}
9
+
10
+ apply(theme: InspectorTheme | null): void {
11
+ const style = this.root.style;
12
+ for (const [key, entry] of this.owned) {
13
+ if (style.getPropertyValue(key) !== entry.applied || style.getPropertyPriority(key)) continue;
14
+ if (entry.previous) style.setProperty(key, entry.previous, entry.priority);
15
+ else style.removeProperty(key);
16
+ }
17
+ this.owned.clear();
18
+ const entries = Object.entries(theme?.variables ?? {}).filter(([key]) => Object.hasOwn(stormVariables, key));
19
+ if (theme?.colorScheme) entries.push(['--zfgi-color-scheme', theme.colorScheme]);
20
+ for (const [key, value] of entries) {
21
+ if (typeof value !== 'string') continue;
22
+ const previous = style.getPropertyValue(key);
23
+ const priority = style.getPropertyPriority(key);
24
+ style.setProperty(key, value);
25
+ this.owned.set(key, { previous, priority, applied: style.getPropertyValue(key) });
26
+ }
27
+ }
28
+
29
+ destroy(): void { this.owned.clear(); }
30
+ }
31
+
32
+ /** Resolve through the DOM once per refresh, including var(), color-mix(), and CSS lengths. */
33
+ export function resolveGraphTheme(root: HTMLElement): GraphVisualTheme {
34
+ const document = root.ownerDocument;
35
+ const view = document?.defaultView;
36
+ if (!view || !root.isConnected) return createGraphVisualTheme();
37
+ const styles = view.getComputedStyle(root);
38
+ const probe = document.createElement('span');
39
+ probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;width:0;height:0;overflow:hidden;';
40
+ probe.setAttribute('aria-hidden', 'true');
41
+ root.appendChild(probe);
42
+ const cache = new Map<string, string>();
43
+ let context: CanvasRenderingContext2D | null | undefined;
44
+ const color = (value: string): string | undefined => {
45
+ probe.style.color = '';
46
+ probe.style.color = value;
47
+ if (!probe.style.color) return undefined;
48
+ const resolved = view.getComputedStyle(probe).color;
49
+ if (/^(#[\da-f]{3,8}|rgba?\([\d.,\s]+\))$/i.test(resolved)) return resolved;
50
+ // Normalize newer CSS color spaces into sRGB supported by Cytoscape.
51
+ if (context === undefined) {
52
+ const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
53
+ context = canvas.getContext('2d', { willReadFrequently: true });
54
+ }
55
+ if (!context) return undefined;
56
+ context.clearRect(0, 0, 1, 1);
57
+ context.fillStyle = resolved;
58
+ context.fillRect(0, 0, 1, 1);
59
+ const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
60
+ return `rgba(${r}, ${g}, ${b}, ${a! / 255})`;
61
+ };
62
+ const read = (key: keyof InspectorThemeVariables): string => {
63
+ const cached = cache.get(key);
64
+ if (cached !== undefined) return cached;
65
+ const value = styles.getPropertyValue(internalThemeProperty(key)).trim() || defaultThemeValue(key);
66
+ let result = value;
67
+ if (key.endsWith('font-family')) {
68
+ probe.style.fontFamily = value;
69
+ result = view.getComputedStyle(probe).fontFamily;
70
+ } else if (key.endsWith('size') || key.endsWith('width')) {
71
+ probe.style.fontSize = ''; probe.style.fontSize = value;
72
+ result = probe.style.fontSize ? view.getComputedStyle(probe).fontSize : defaultThemeValue(key);
73
+ if (!(parseFloat(result) > 0)) result = defaultThemeValue(key);
74
+ } else if (key.endsWith('opacity') || key.endsWith('scale')) {
75
+ const n = Number(value);
76
+ result = Number.isFinite(n) && n >= 0 && (!key.endsWith('opacity') || n <= 1) ? value : defaultThemeValue(key);
77
+ } else {
78
+ result = color(value) ?? defaultThemeValue(key);
79
+ }
80
+ cache.set(key, result);
81
+ return result;
82
+ };
83
+ try { return createGraphVisualTheme(read); }
84
+ finally { probe.remove(); }
85
+ }
package/src/panelTypes.ts CHANGED
@@ -1,28 +1,40 @@
1
+ import type { GraphVisualTheme } from './panelVisualTheme.ts';
1
2
  import type { GraphRenderer } from './panelGraphRenderer.ts';
3
+ import type { FrameGraphDebugEdge } from './debugCaptureModel.ts';
4
+
5
+ export type GraphFlowRelation = {
6
+ readonly role: 'declaration' | 'value' | 'ordering' | 'output-producer' | 'output-initial';
7
+ readonly nodeIds: readonly string[];
8
+ readonly rootKey?: string;
9
+ readonly dependency?: FrameGraphDebugEdge;
10
+ };
2
11
 
3
12
  export type Selection =
4
13
  | { kind: 'node'; id: string }
5
14
  | { kind: 'group'; pathKey: string }
6
15
  | { kind: 'resource'; id: string }
7
- | { kind: 'root'; index: number }
8
- | { kind: 'culled'; index: number }
16
+ | { kind: 'root'; key: string }
17
+ | { kind: 'culled'; id: string }
9
18
  | { kind: 'allocation'; id: string }
10
19
  | { kind: 'segment'; index: number };
11
20
 
12
21
  export type WorkbenchTab = 'overview' | 'graph' | 'passes' | 'resources' | 'memory' | 'diagnostics';
13
22
  export type PassesSubview = 'list' | 'groups';
14
23
  export type InspectorTab = 'summary' | 'relations' | 'raw';
15
- export type GraphViewMode = 'passes' | 'resources';
16
24
 
17
25
  export type GraphViewState = {
26
+ theme?: GraphVisualTheme;
27
+ readonly refreshTheme?: () => void;
18
28
  readonly host: HTMLElement;
19
29
  readonly toolbar: HTMLElement;
20
30
  readonly legend?: HTMLElement;
21
31
  readonly layoutElementBudget?: number;
22
- graphMode: GraphViewMode;
23
32
  groupsEnabled: boolean;
33
+ showResourceDeclarations?: boolean;
24
34
  readonly expandedGroupPaths: Set<string>;
25
35
  renderer?: GraphRenderer;
26
36
  fitOnNextRender: boolean;
27
37
  anchorElementIdOnNextRender?: string;
38
+ revealOnNextRender?: { readonly selection: Selection; readonly revision: number };
39
+ captureRevision?: number;
28
40
  };
@@ -1,39 +1,39 @@
1
+ import { stormVariables } from './themeDefinitions.ts';
2
+ import type { InspectorThemeVariables } from './theme.ts';
3
+ type Variable = keyof InspectorThemeVariables;
4
+ export function mixHex(front: string, back: string, amount: number): string {
5
+ return '#' + [1, 3, 5].map(offset => Math.round(parseInt(front.slice(offset, offset + 2), 16) * amount + parseInt(back.slice(offset, offset + 2), 16) * (1 - amount)).toString(16).padStart(2, '0')).join('');
6
+ }
7
+ export function defaultThemeValue(key: Variable, variables: InspectorThemeVariables = stormVariables): string {
8
+ let value: string = variables[key];
9
+ value = value.replace(/var\((--zfgi-[\w-]+), ([^()]+)\)/g, (_match, name: Variable) => defaultThemeValue(name, variables));
10
+ const mix = /^color-mix\(in srgb, (#[\da-f]+) ([\d.]+)%, (#[\da-f]+)\)$/i.exec(value);
11
+ return mix ? mixHex(mix[1]!, mix[3]!, Number(mix[2]) / 100) : value;
12
+ }
13
+ export function createGraphVisualTheme(read: (key: Variable) => string = defaultThemeValue) {
14
+ const category = (kind: 'render' | 'compute' | 'copy' | 'clear' | 'command' | 'external' | 'declaration' | 'output' | 'texture' | 'buffer') => ({
15
+ stroke: read(('--zfgi-graph-' + kind + '-stroke') as Variable), fill: read(('--zfgi-graph-' + kind + '-fill') as Variable),
16
+ });
17
+ const number = (key: Variable) => parseFloat(read(key));
18
+ return {
19
+ canvas: read('--zfgi-canvas'), surface: read('--zfgi-surface'), surfaceRaised: read('--zfgi-surface-raised'),
20
+ text: read('--zfgi-graph-text'), muted: read('--zfgi-graph-muted'),
21
+ render: category('render'), compute: category('compute'), copy: category('copy'), clear: category('clear'),
22
+ command: category('command'), external: category('external'), declaration: category('declaration'), output: category('output'), texture: category('texture'), buffer: category('buffer'),
23
+ group: { stroke: read('--zfgi-graph-group-stroke'), fill: read('--zfgi-graph-group-fill'), alternateFill: read('--zfgi-graph-group-alternate-fill') },
24
+ dependency: { value: read('--zfgi-graph-value'), ordering: read('--zfgi-graph-ordering') },
25
+ access: { read: read('--zfgi-graph-read'), write: read('--zfgi-graph-write') },
26
+ selected: read('--zfgi-graph-selected'), hover: read('--zfgi-graph-hover'),
27
+ activeBackground: { color: read('--zfgi-graph-active-bg'), opacity: number('--zfgi-graph-active-bg-opacity'), size: number('--zfgi-graph-active-bg-size') },
28
+ culled: { stroke: read('--zfgi-graph-culled-stroke'), fill: read('--zfgi-graph-culled-fill') },
29
+ fontFamily: read('--zfgi-graph-font-family'), fontSize: number('--zfgi-graph-font-size'),
30
+ nodeBorderWidth: number('--zfgi-graph-node-border-width'), groupBorderWidth: number('--zfgi-graph-group-border-width'),
31
+ edgeWidth: number('--zfgi-graph-edge-width'), edgeOpacity: number('--zfgi-graph-edge-opacity'), orderingOpacity: number('--zfgi-graph-ordering-opacity'), groupOpacity: number('--zfgi-graph-group-opacity'),
32
+ arrowScale: number('--zfgi-graph-arrow-scale'), hoverWidth: number('--zfgi-graph-hover-width'), selectedWidth: number('--zfgi-graph-selected-width'),
33
+ };
34
+ }
35
+ export type GraphVisualTheme = ReturnType<typeof createGraphVisualTheme>;
36
+ export const GRAPH_VISUAL_THEME = createGraphVisualTheme();
1
37
  export const FRAME_GRAPH_DEBUG_VISUAL_THEME = {
2
- canvas: '#0b0f14',
3
- panel: '#0f151d',
4
- surface: '#131b24',
5
- surfaceRaised: '#18222d',
6
- surfaceHover: '#1d2935',
7
- border: '#263341',
8
- text: '#e6edf3',
9
- textSecondary: '#b4beca',
10
- muted: '#8b98a5',
11
- accent: '#38bdf8',
12
- success: '#34d399',
13
- warning: '#fbbf24',
14
- danger: '#fb7185',
15
- fontUi: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
16
- fontMono: 'ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace',
17
- } as const;
18
-
19
- export const GRAPH_VISUAL_THEME = {
20
- canvas: FRAME_GRAPH_DEBUG_VISUAL_THEME.canvas,
21
- surface: FRAME_GRAPH_DEBUG_VISUAL_THEME.surface,
22
- surfaceRaised: FRAME_GRAPH_DEBUG_VISUAL_THEME.surfaceRaised,
23
- text: FRAME_GRAPH_DEBUG_VISUAL_THEME.text,
24
- muted: FRAME_GRAPH_DEBUG_VISUAL_THEME.muted,
25
- render: { stroke: '#34d399', fill: '#12342b' },
26
- compute: { stroke: '#60a5fa', fill: '#162c46' },
27
- copy: { stroke: '#fbbf24', fill: '#3a2d12' },
28
- clear: { stroke: '#94a3b8', fill: '#25303a' },
29
- command: { stroke: '#c084fc', fill: '#322044' },
30
- external: { stroke: '#fb923c', fill: '#402318' },
31
- texture: { stroke: '#f472b6', fill: '#3c1f32' },
32
- buffer: { stroke: '#2dd4bf', fill: '#123632' },
33
- group: { stroke: '#64748b', fill: '#141c29', alternateFill: '#172033' },
34
- dependency: { value: '#94a3b8', ordering: '#64748b' },
35
- access: { read: '#2dd4bf', write: '#f59e0b' },
36
- selected: FRAME_GRAPH_DEBUG_VISUAL_THEME.accent,
37
- hover: '#cbd5e1',
38
- culled: { stroke: '#64748b', fill: '#171d24' },
39
- } as const;
38
+ canvas: defaultThemeValue('--zfgi-canvas'), panel: defaultThemeValue('--zfgi-background'), surface: defaultThemeValue('--zfgi-surface'), surfaceRaised: defaultThemeValue('--zfgi-surface-raised'), surfaceHover: defaultThemeValue('--zfgi-surface-hover'), text: defaultThemeValue('--zfgi-text'), textSecondary: defaultThemeValue('--zfgi-text-secondary'), muted: defaultThemeValue('--zfgi-muted'),
39
+ };
@@ -3,13 +3,15 @@ import type {
3
3
  FrameGraphDebugViewModel,
4
4
  } from './debugCaptureModel.ts';
5
5
  import { createCell, formatBytes, labelResource } from './panelDomHelpers.ts';
6
- import type { Selection } from './panelTypes.ts';
6
+ import type { Selection, WorkbenchTab } from './panelTypes.ts';
7
7
 
8
8
  export type WorkbenchCallbacks = {
9
9
  readonly onSelect: (selection: Selection) => void;
10
10
  readonly onHover: (selection: Selection | undefined) => void;
11
11
  readonly onGroupToggle: (pathKey: string) => void;
12
12
  readonly isGroupExpanded: (pathKey: string) => boolean;
13
+ readonly onReveal?: (selection: Selection, tab: WorkbenchTab) => void;
14
+ readonly onNavigate?: (tab: WorkbenchTab, filter?: 'culled') => void;
13
15
  };
14
16
  export type WorkbenchTableColumn = {
15
17
  readonly label: string;
@@ -22,8 +24,8 @@ export function selectionKey(selection: Selection): string {
22
24
  case 'node': return `node:${selection.id}`;
23
25
  case 'group': return `group:${selection.pathKey}`;
24
26
  case 'resource': return `resource:${selection.id}`;
25
- case 'root': return `root:${selection.index}`;
26
- case 'culled': return `culled:${selection.index}`;
27
+ case 'root': return `root:${selection.key}`;
28
+ case 'culled': return `culled:${selection.id}`;
27
29
  case 'allocation': return `allocation:${selection.id}`;
28
30
  case 'segment': return `segment:${selection.index}`;
29
31
  }
@@ -161,7 +163,7 @@ export function createKindCell(kind: string, label = kind): HTMLTableCellElement
161
163
  cell.appendChild(marker);
162
164
  return cell;
163
165
  }
164
-
166
+
165
167
  export function groupPath(snapshot: FrameGraphDebugViewModel, groupId: string | undefined): string {
166
168
  if (groupId === undefined) return '-';
167
169
  return snapshot.groupById.get(groupId)?.path.join(' / ') ?? `group-${groupId}`;
@@ -214,3 +216,59 @@ export function resourceLabel(snapshot: FrameGraphDebugViewModel, resourceId: st
214
216
  const resource = snapshot.resourceById.get(resourceId);
215
217
  return resource ? labelResource(resource) : `resource-${resourceId}`;
216
218
  }
219
+
220
+ /** All tablists use a single tab stop and automatic keyboard activation. */
221
+ export function enableTabKeyboard(tabList: HTMLElement): void {
222
+ const buttons = () => Array.from(tabList.querySelectorAll<HTMLButtonElement>('[role="tab"]'));
223
+ const sync = () => {
224
+ for (const button of buttons()) button.tabIndex = button.getAttribute('aria-selected') === 'true' && !button.disabled ? 0 : -1;
225
+ };
226
+ tabList.addEventListener('click', sync);
227
+ tabList.addEventListener('keydown', (event) => {
228
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
229
+ const available = buttons().filter((button) => !button.disabled && !button.hidden);
230
+ if (!available.length) return;
231
+ const index = Math.max(0, available.indexOf(document.activeElement as HTMLButtonElement));
232
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? available.length - 1
233
+ : (index + (event.key === 'ArrowRight' ? 1 : -1) + available.length) % available.length;
234
+ event.preventDefault();
235
+ event.stopPropagation();
236
+ available[next]!.click();
237
+ available[next]!.focus();
238
+ sync();
239
+ });
240
+ sync();
241
+ }
242
+
243
+ export function formatTimingCoverage(timed: number, eligible: number): string {
244
+ if (eligible === 0) return 'Not applicable · no eligible passes';
245
+ if (timed === 0) return `Not collected · 0/${eligible}`;
246
+ return `${timed === eligible ? 'Complete' : 'Partial'} · ${timed}/${eligible} timed`;
247
+ }
248
+
249
+ export function formatMeasuredGpuWork(micros: number, timed: number, eligible: number): string {
250
+ if (eligible === 0) return 'Not applicable';
251
+ if (timed === 0) return 'Not collected';
252
+ return `${(micros / 1000).toFixed(3)} ms · ${formatTimingCoverage(timed, eligible)}`;
253
+ }
254
+
255
+ export function formatEstimateCoverage(bytes: number | undefined, coverage: { known: number; total: number }): string {
256
+ return bytes === undefined ? `Unknown · ${coverage.known}/${coverage.total} sizes known` : formatBytes(bytes);
257
+ }
258
+
259
+ export async function writeClipboardText(text: string): Promise<void> {
260
+ if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(text);
261
+ const input = document.createElement('textarea');
262
+ input.value = text;
263
+ input.readOnly = true;
264
+ input.style.cssText = 'position:fixed;left:-9999px;top:0';
265
+ document.body.append(input);
266
+ const previous = document.activeElement as HTMLElement | null;
267
+ input.select();
268
+ try {
269
+ if (!document.execCommand('copy')) throw new Error('Clipboard is unavailable.');
270
+ } finally {
271
+ input.remove();
272
+ previous?.focus();
273
+ }
274
+ }