@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,108 @@
1
+ import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
+ import { CytoscapeGraphRenderer } from './panelCytoscapeGraphRenderer.ts';
3
+ import { createGraphScene, type GraphScene } from './panelGraphScene.ts';
4
+ import { createGraphLegend } from './panelGraphVisuals.ts';
5
+ import type { GraphViewState, Selection } from './panelTypes.ts';
6
+
7
+ type GraphSceneCacheEntry = {
8
+ readonly snapshot: FrameGraphDebugViewModel;
9
+ readonly optionsKey: string;
10
+ readonly scene: GraphScene;
11
+ };
12
+
13
+ const graphSceneCache = new WeakMap<GraphViewState, GraphSceneCacheEntry>();
14
+
15
+ export function renderGraphView(
16
+ graphView: GraphViewState,
17
+ snapshot: FrameGraphDebugViewModel,
18
+ selected: Selection | undefined,
19
+ hovered: Selection | undefined,
20
+ onSelect: (selection: Selection) => void,
21
+ onHover: (selection: Selection | undefined) => void,
22
+ onToggleGroup: (pathKey: string) => void,
23
+ ): void {
24
+ const scene = resolveGraphScene(graphView, snapshot);
25
+ renderGraphLegend(graphView.legend, scene);
26
+ const elementCount = scene.nodes.length + scene.edges.length;
27
+ const layoutElementBudget = graphView.layoutElementBudget ?? Number.MAX_SAFE_INTEGER;
28
+ if (elementCount > layoutElementBudget) {
29
+ graphView.renderer?.destroy();
30
+ graphView.renderer = undefined;
31
+ const notice = document.createElement('div');
32
+ notice.className = 'zenfg-inspector-graph-status';
33
+ notice.dataset.state = 'empty';
34
+ notice.setAttribute('role', 'status');
35
+ notice.textContent = `Automatic layout disabled: ${elementCount} graph elements exceed the ${layoutElementBudget} element budget. Passes, Resources, Memory, Diagnostics, and raw data remain available.`;
36
+ graphView.host.replaceChildren(notice);
37
+ return;
38
+ }
39
+ graphView.renderer ??= new CytoscapeGraphRenderer(graphView.host);
40
+ graphView.renderer.render({
41
+ scene,
42
+ selected,
43
+ hovered,
44
+ fit: graphView.fitOnNextRender,
45
+ anchorElementId: graphView.anchorElementIdOnNextRender,
46
+ onSelect,
47
+ onHover,
48
+ onToggleGroup,
49
+ });
50
+ graphView.fitOnNextRender = false;
51
+ graphView.anchorElementIdOnNextRender = undefined;
52
+ }
53
+
54
+ export function resolveGraphScene(
55
+ graphView: GraphViewState,
56
+ snapshot: FrameGraphDebugViewModel,
57
+ ): GraphScene {
58
+ const optionsKey = JSON.stringify([
59
+ graphView.graphMode,
60
+ graphView.groupsEnabled,
61
+ [...graphView.expandedGroupPaths].sort(),
62
+ ]);
63
+ const cached = graphSceneCache.get(graphView);
64
+ if (cached?.snapshot === snapshot && cached.optionsKey === optionsKey) return cached.scene;
65
+ const scene = createGraphScene(snapshot, {
66
+ mode: graphView.graphMode,
67
+ groupsEnabled: graphView.groupsEnabled,
68
+ expandedGroupPaths: graphView.expandedGroupPaths,
69
+ });
70
+ graphSceneCache.set(graphView, { snapshot, optionsKey, scene });
71
+ return scene;
72
+ }
73
+
74
+ export function fitGraph(graphView: GraphViewState): void {
75
+ graphView.renderer?.fit();
76
+ }
77
+
78
+ export function resizeGraph(graphView: GraphViewState): void {
79
+ graphView.renderer?.resize();
80
+ }
81
+
82
+ export function destroyGraph(graphView: GraphViewState): void {
83
+ graphView.renderer?.destroy();
84
+ graphView.renderer = undefined;
85
+ graphSceneCache.delete(graphView);
86
+ }
87
+
88
+ function renderGraphLegend(host: HTMLElement | undefined, scene: GraphScene): void {
89
+ if (!host) return;
90
+ const entries = createGraphLegend(scene);
91
+ const key = entries.map((entry) => entry.key).join('|');
92
+ if (host.dataset.legendKey === key) return;
93
+ host.dataset.legendKey = key;
94
+ host.replaceChildren(...entries.map((entry) => {
95
+ const item = document.createElement('span');
96
+ item.className = 'zenfg-inspector-legend-item';
97
+ const swatch = document.createElement('span');
98
+ swatch.className = 'zenfg-inspector-legend-swatch';
99
+ swatch.dataset.shape = entry.shape;
100
+ if (entry.lineStyle) swatch.dataset.lineStyle = entry.lineStyle;
101
+ if (entry.hollowArrow) swatch.dataset.hollowArrow = 'true';
102
+ swatch.style.setProperty('--zenfg-inspector-legend-color', entry.color);
103
+ const label = document.createElement('span');
104
+ label.textContent = entry.label;
105
+ item.append(swatch, label);
106
+ return item;
107
+ }));
108
+ }
@@ -0,0 +1,364 @@
1
+ import type cytoscape from 'cytoscape';
2
+
3
+ import type { GraphScene, GraphSceneEdge, GraphSceneNode } from './panelGraphScene.ts';
4
+ import { GRAPH_VISUAL_THEME } from './panelVisualTheme.ts';
5
+
6
+ export { GRAPH_VISUAL_THEME } from './panelVisualTheme.ts';
7
+
8
+
9
+ export const GRAPH_GEOMETRY = {
10
+ fitPadding: 28,
11
+ baseFontSize: 13,
12
+ overviewFontThreshold: 9,
13
+ minimumZoomedFontSize: 5,
14
+ groupTitleInset: 8,
15
+ groupTitleLineHeight: 1.2,
16
+ groupPadding: 32,
17
+ elkGroupPadding: { top: 46, right: 24, bottom: 24, left: 24 },
18
+ edgeCornerRadius: 6,
19
+ } as const;
20
+
21
+ const ENDPOINT_EDGE_DISTANCES = 'endpoints' as unknown as cytoscape.Css.Edge['edge-distances'];
22
+ const EDGE_SEGMENT_RADII = `${GRAPH_GEOMETRY.edgeCornerRadius}px` as unknown as cytoscape.Css.Edge['segment-radii'];
23
+
24
+ export type GraphLegendEntry = {
25
+ readonly key: string;
26
+ readonly label: string;
27
+ readonly color: string;
28
+ readonly shape: 'box' | 'ellipse' | 'group' | 'line';
29
+ readonly lineStyle?: 'solid' | 'dotted' | 'dashed';
30
+ readonly hollowArrow?: boolean;
31
+ };
32
+
33
+ const NODE_LEGEND_ENTRIES = {
34
+ render: nodeLegend('render', 'Render', GRAPH_VISUAL_THEME.render.stroke),
35
+ compute: nodeLegend('compute', 'Compute', GRAPH_VISUAL_THEME.compute.stroke),
36
+ copy: nodeLegend('copy', 'Copy', GRAPH_VISUAL_THEME.copy.stroke),
37
+ 'clear-buffer': nodeLegend('clear', 'Clear', GRAPH_VISUAL_THEME.clear.stroke),
38
+ command: nodeLegend('command', 'Command', GRAPH_VISUAL_THEME.command.stroke),
39
+ 'external-submission': nodeLegend('external', 'External', GRAPH_VISUAL_THEME.external.stroke),
40
+ } as const;
41
+
42
+ export function createGraphLegend(scene: GraphScene): readonly GraphLegendEntry[] {
43
+ const entries: GraphLegendEntry[] = [];
44
+ const passKinds = new Set(scene.nodes.flatMap((node) => node.kind === 'pass' || node.kind === 'culled-pass'
45
+ ? [node.passKind]
46
+ : []));
47
+ for (const kind of ['render', 'compute', 'copy', 'clear-buffer', 'command', 'external-submission'] as const) {
48
+ if (passKinds.has(kind)) entries.push(NODE_LEGEND_ENTRIES[kind]);
49
+ }
50
+ if (scene.nodes.some((node) => node.kind === 'group')) {
51
+ entries.push({ key: 'group', label: 'Group', color: GRAPH_VISUAL_THEME.group.stroke, shape: 'group' });
52
+ }
53
+ if (scene.nodes.some((node) => node.kind === 'resource' && node.resourceKind === 'texture')) {
54
+ entries.push({ key: 'texture', label: 'Texture', color: GRAPH_VISUAL_THEME.texture.stroke, shape: 'ellipse' });
55
+ }
56
+ if (scene.nodes.some((node) => node.kind === 'resource' && node.resourceKind === 'buffer')) {
57
+ entries.push({ key: 'buffer', label: 'Buffer', color: GRAPH_VISUAL_THEME.buffer.stroke, shape: 'ellipse' });
58
+ }
59
+ if (scene.nodes.some((node) => node.kind === 'culled-pass')) {
60
+ entries.push({
61
+ key: 'culled',
62
+ label: 'Culled',
63
+ color: GRAPH_VISUAL_THEME.culled.stroke,
64
+ shape: 'box',
65
+ lineStyle: 'dashed',
66
+ });
67
+ }
68
+ if (scene.edges.some((edge) => edge.kind === 'dependency' && edge.dependencyKind === 'value')) {
69
+ entries.push(edgeLegend('value', 'Value', GRAPH_VISUAL_THEME.dependency.value, 'solid'));
70
+ }
71
+ if (scene.edges.some((edge) => edge.kind === 'dependency' && edge.dependencyKind === 'ordering')) {
72
+ entries.push({
73
+ ...edgeLegend('ordering', 'Order', GRAPH_VISUAL_THEME.dependency.ordering, 'dotted'),
74
+ hollowArrow: true,
75
+ });
76
+ }
77
+ if (scene.edges.some((edge) => edge.kind === 'access' && edge.accessMode === 'read')) {
78
+ entries.push(edgeLegend('read', 'Read', GRAPH_VISUAL_THEME.access.read, 'solid'));
79
+ }
80
+ if (scene.edges.some((edge) => edge.kind === 'access' && edge.accessMode === 'write')) {
81
+ entries.push(edgeLegend('write', 'Write', GRAPH_VISUAL_THEME.access.write, 'solid'));
82
+ }
83
+ return entries;
84
+ }
85
+
86
+ export function createGraphStyles(): cytoscape.StylesheetJson {
87
+ const theme = GRAPH_VISUAL_THEME;
88
+ return [
89
+ {
90
+ selector: 'node',
91
+ style: {
92
+ 'shape': 'round-rectangle',
93
+ 'width': 'data(width)',
94
+ 'height': 'data(height)',
95
+ 'label': 'data(displayLabel)',
96
+ 'text-wrap': 'wrap',
97
+ 'text-max-width': '160px',
98
+ 'font-family': 'ui-monospace, SFMono-Regular, Consolas, Liberation Mono, monospace',
99
+ 'font-size': GRAPH_GEOMETRY.baseFontSize,
100
+ 'min-zoomed-font-size': GRAPH_GEOMETRY.minimumZoomedFontSize,
101
+ 'color': theme.text,
102
+ 'background-color': theme.surfaceRaised,
103
+ 'border-color': theme.group.stroke,
104
+ 'border-width': 1.25,
105
+ 'text-valign': 'center',
106
+ 'text-halign': 'center',
107
+ 'overlay-opacity': 0,
108
+ 'underlay-opacity': 0,
109
+ },
110
+ },
111
+ passStyle('render', theme.render),
112
+ passStyle('compute', theme.compute),
113
+ passStyle('copy', theme.copy),
114
+ passStyle('clear-buffer', theme.clear),
115
+ passStyle('command', theme.command),
116
+ {
117
+ selector: 'node[passKind = "external-submission"]',
118
+ style: {
119
+ 'background-color': theme.external.fill,
120
+ 'border-color': theme.external.stroke,
121
+ 'border-style': 'double',
122
+ 'border-width': 3,
123
+ },
124
+ },
125
+ {
126
+ selector: 'node[kind = "culled-pass"]',
127
+ style: {
128
+ 'background-color': theme.culled.fill,
129
+ 'border-color': theme.culled.stroke,
130
+ 'color': theme.muted,
131
+ 'border-style': 'dashed',
132
+ 'opacity': 0.72,
133
+ },
134
+ },
135
+ {
136
+ selector: 'node[kind = "resource"][resourceKind = "texture"]',
137
+ style: {
138
+ 'shape': 'ellipse',
139
+ 'background-color': theme.texture.fill,
140
+ 'border-color': theme.texture.stroke,
141
+ },
142
+ },
143
+ {
144
+ selector: 'node[kind = "resource"][resourceKind = "buffer"]',
145
+ style: {
146
+ 'shape': 'ellipse',
147
+ 'background-color': theme.buffer.fill,
148
+ 'border-color': theme.buffer.stroke,
149
+ },
150
+ },
151
+ {
152
+ selector: 'node[kind = "group"]',
153
+ style: {
154
+ 'text-max-width': (element: cytoscape.SingularElementArgument) => `${element.data('labelMaxWidth') as number}px`,
155
+ },
156
+ },
157
+ {
158
+ selector: 'node[kind = "group"][collapsed = 1]',
159
+ style: {
160
+ 'background-color': theme.group.fill,
161
+ 'border-color': theme.group.stroke,
162
+ 'border-width': 2,
163
+ 'color': theme.text,
164
+ },
165
+ },
166
+ {
167
+ selector: 'node[kind = "group"][collapsed = 0]',
168
+ style: expandedGroupStyle(theme.group.fill),
169
+ },
170
+ {
171
+ selector: 'node[kind = "group"][collapsed = 0][depthBand = 1]',
172
+ style: { 'background-color': theme.group.alternateFill },
173
+ },
174
+ {
175
+ selector: 'edge',
176
+ style: {
177
+ 'width': 1.5,
178
+ 'line-color': theme.dependency.value,
179
+ 'line-opacity': 0.82,
180
+ 'target-arrow-color': theme.dependency.value,
181
+ 'target-arrow-shape': 'triangle',
182
+ 'target-arrow-fill': 'filled',
183
+ 'arrow-scale': 0.8,
184
+ 'curve-style': 'straight',
185
+ 'label': 'data(displayLabel)',
186
+ 'font-family': 'ui-monospace, SFMono-Regular, Consolas, Liberation Mono, monospace',
187
+ 'font-size': 10,
188
+ 'min-zoomed-font-size': GRAPH_GEOMETRY.minimumZoomedFontSize,
189
+ 'color': theme.text,
190
+ 'text-background-color': theme.canvas,
191
+ 'text-background-opacity': 0.82,
192
+ 'text-background-padding': '2px',
193
+ 'text-rotation': 'autorotate',
194
+ 'overlay-opacity': 0,
195
+ 'underlay-opacity': 0,
196
+ },
197
+ },
198
+ {
199
+ selector: 'edge[dependencyKind = "ordering"]',
200
+ style: {
201
+ 'line-color': theme.dependency.ordering,
202
+ 'target-arrow-color': theme.dependency.ordering,
203
+ 'line-style': 'dotted',
204
+ 'target-arrow-fill': 'hollow',
205
+ 'line-opacity': 0.72,
206
+ },
207
+ },
208
+ {
209
+ selector: 'edge[accessMode = "read"]',
210
+ style: {
211
+ 'line-color': theme.access.read,
212
+ 'target-arrow-color': theme.access.read,
213
+ },
214
+ },
215
+ {
216
+ selector: 'edge[accessMode = "write"]',
217
+ style: {
218
+ 'line-color': theme.access.write,
219
+ 'target-arrow-color': theme.access.write,
220
+ },
221
+ },
222
+ {
223
+ selector: 'edge[dashed = 1]',
224
+ style: { 'line-style': 'dashed', 'line-opacity': 0.46, 'opacity': 0.58 },
225
+ },
226
+ {
227
+ selector: 'edge.elk-route',
228
+ style: {
229
+ 'source-endpoint': 'data(sourceEndpoint)',
230
+ 'target-endpoint': 'data(targetEndpoint)',
231
+ 'edge-distances': ENDPOINT_EDGE_DISTANCES,
232
+ },
233
+ },
234
+ {
235
+ selector: 'edge.elk-segments',
236
+ style: {
237
+ 'curve-style': 'round-segments',
238
+ 'segment-distances': 'data(segmentDistances)',
239
+ 'segment-weights': 'data(segmentWeights)',
240
+ 'segment-radii': EDGE_SEGMENT_RADII,
241
+ },
242
+ },
243
+ {
244
+ selector: 'node.semantic-hover',
245
+ style: {
246
+ 'border-color': theme.hover,
247
+ 'border-width': 2,
248
+ 'z-index': 15,
249
+ },
250
+ },
251
+ {
252
+ selector: 'edge.semantic-hover',
253
+ style: {
254
+ 'line-color': theme.hover,
255
+ 'target-arrow-color': theme.hover,
256
+ 'line-opacity': 1,
257
+ 'width': 2.5,
258
+ 'z-index': 15,
259
+ },
260
+ },
261
+ {
262
+ selector: 'node.semantic-selected',
263
+ style: {
264
+ 'border-color': theme.selected,
265
+ 'border-width': 3,
266
+ 'underlay-color': theme.selected,
267
+ 'underlay-padding': 5,
268
+ 'underlay-opacity': 0.18,
269
+ 'opacity': 1,
270
+ 'z-index': 20,
271
+ },
272
+ },
273
+ {
274
+ selector: 'edge.semantic-selected',
275
+ style: {
276
+ 'line-color': theme.selected,
277
+ 'target-arrow-color': theme.selected,
278
+ 'line-opacity': 1,
279
+ 'width': 3,
280
+ 'z-index': 20,
281
+ },
282
+ },
283
+ ];
284
+ }
285
+
286
+ export function nodeDimensions(node: GraphSceneNode): { readonly width: number; readonly height: number } {
287
+ switch (node.kind) {
288
+ case 'pass':
289
+ return { width: 184, height: node.label.includes('\n') ? 62 : 48 };
290
+ case 'culled-pass':
291
+ return { width: 184, height: 64 };
292
+ case 'resource':
293
+ return { width: 166, height: node.label.includes('\n') ? 58 : 46 };
294
+ case 'group':
295
+ return node.collapsed ? { width: 224, height: 68 } : { width: 120, height: 80 };
296
+ }
297
+ }
298
+
299
+ export function graphLayoutGeometryKey(scene: GraphScene): string {
300
+ return JSON.stringify({
301
+ topology: scene.topologyKey,
302
+ dimensions: scene.nodes.map((node) => {
303
+ const dimensions = nodeDimensions(node);
304
+ return [node.id, dimensions.width, dimensions.height];
305
+ }),
306
+ });
307
+ }
308
+
309
+ export function graphEdgeDisplayLabel(edge: GraphSceneEdge): string {
310
+ return edge.kind === 'access' ? '' : edge.label ?? '';
311
+ }
312
+
313
+ export function expandedGroupLabelMaxWidth(outerWidth: number): number {
314
+ return Math.max(80, Math.floor(outerWidth - GRAPH_GEOMETRY.groupTitleInset * 2));
315
+ }
316
+
317
+ export function isOverviewGraphScale(zoom: number): boolean {
318
+ return GRAPH_GEOMETRY.baseFontSize * zoom < GRAPH_GEOMETRY.overviewFontThreshold;
319
+ }
320
+
321
+ function nodeLegend(key: string, label: string, color: string): GraphLegendEntry {
322
+ return { key, label, color, shape: 'box' };
323
+ }
324
+
325
+ function edgeLegend(
326
+ key: string,
327
+ label: string,
328
+ color: string,
329
+ lineStyle: NonNullable<GraphLegendEntry['lineStyle']>,
330
+ ): GraphLegendEntry {
331
+ return { key, label, color, shape: 'line', lineStyle };
332
+ }
333
+
334
+ function passStyle(
335
+ kind: string,
336
+ colors: { readonly stroke: string; readonly fill: string },
337
+ ): cytoscape.StylesheetStyle {
338
+ return {
339
+ selector: `node[passKind = "${kind}"]`,
340
+ style: { 'background-color': colors.fill, 'border-color': colors.stroke },
341
+ };
342
+ }
343
+
344
+ function expandedGroupStyle(backgroundColor: string): cytoscape.Css.Node {
345
+ const geometry = GRAPH_GEOMETRY;
346
+ return {
347
+ 'background-color': backgroundColor,
348
+ 'background-opacity': 0.34,
349
+ 'border-color': GRAPH_VISUAL_THEME.group.stroke,
350
+ 'border-width': 1.5,
351
+ 'padding': `${geometry.groupPadding}px`,
352
+ 'compound-sizing-wrt-labels': 'include',
353
+ 'min-width': '120px',
354
+ 'min-height': '80px',
355
+ 'text-valign': 'top-inside',
356
+ 'text-halign': 'left-inside',
357
+ 'text-justification': 'left',
358
+ 'text-margin-x': geometry.groupTitleInset - geometry.groupPadding * 2,
359
+ 'text-margin-y': geometry.groupTitleInset - geometry.groupPadding * 2,
360
+ 'line-height': geometry.groupTitleLineHeight,
361
+ 'color': GRAPH_VISUAL_THEME.text,
362
+ 'font-weight': 600,
363
+ };
364
+ }
@@ -0,0 +1,56 @@
1
+ export type PanelIconName =
2
+ | 'capture'
3
+ | 'check'
4
+ | 'close'
5
+ | 'copy'
6
+ | 'empty'
7
+ | 'error'
8
+ | 'fit'
9
+ | 'inspector'
10
+ | 'spinner'
11
+ | 'waiting';
12
+
13
+ const ICON_PATHS: Record<PanelIconName, readonly string[]> = {
14
+ capture: [
15
+ 'M6 2.5H2.5V6',
16
+ 'M10 2.5h3.5V6',
17
+ 'M6 13.5H2.5V10',
18
+ 'M10 13.5h3.5V10',
19
+ 'M8 6.25a1.75 1.75 0 1 1 0 3.5 1.75 1.75 0 0 1 0-3.5',
20
+ ],
21
+ check: ['M3 8.2 6.3 11.5 13 4.8'],
22
+ close: ['M4 4l8 8', 'M12 4l-8 8'],
23
+ copy: ['M5.5 4h7.5v9H5.5z', 'M3 11V2.5h7.5'],
24
+ empty: ['M3 3h10v10H3z', 'M5.5 8h5'],
25
+ error: ['M8 2.5l5.5 10H2.5z', 'M8 6v3', 'M8 11.2v.1'],
26
+ fit: ['M6 2.5H2.5V6', 'M10 2.5h3.5V6', 'M6 13.5H2.5V10', 'M10 13.5h3.5V10'],
27
+ inspector: ['M2.5 3h11v10h-11z', 'M9.5 3v10'],
28
+ spinner: ['M13 8a5 5 0 1 1-2-4'],
29
+ waiting: ['M5.5 5v6', 'M10.5 5v6'],
30
+ };
31
+
32
+ export function createPanelIcon(name: PanelIconName): SVGSVGElement {
33
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
34
+ svg.classList.add('zenfg-inspector-control-icon');
35
+ svg.dataset.icon = name;
36
+ svg.setAttribute('viewBox', '0 0 16 16');
37
+ svg.setAttribute('width', '14');
38
+ svg.setAttribute('height', '14');
39
+ svg.setAttribute('aria-hidden', 'true');
40
+ svg.setAttribute('focusable', 'false');
41
+ for (const data of ICON_PATHS[name]) {
42
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
43
+ path.setAttribute('d', data);
44
+ path.setAttribute('fill', 'none');
45
+ path.setAttribute('stroke', 'currentColor');
46
+ path.setAttribute('stroke-linecap', 'round');
47
+ path.setAttribute('stroke-linejoin', 'round');
48
+ path.setAttribute('stroke-width', '1.4');
49
+ svg.appendChild(path);
50
+ }
51
+ return svg;
52
+ }
53
+
54
+ export function setPanelButtonContent(button: HTMLButtonElement, icon: PanelIconName, label: string): void {
55
+ button.replaceChildren(createPanelIcon(icon), document.createTextNode(label));
56
+ }