@zenfg/inspector 0.1.0-beta.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE.md +182 -0
- package/README.md +61 -89
- package/THEMING.md +124 -0
- package/dist/FrameGraphInspector.d.ts +23 -4
- package/dist/FrameGraphInspector.d.ts.map +1 -1
- package/dist/FrameGraphInspector.js +137 -29
- package/dist/FrameGraphInspector.js.map +1 -1
- package/dist/capture.d.ts +5 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +2 -0
- package/dist/capture.js.map +1 -0
- package/dist/debugCaptureModel.d.ts +19 -0
- package/dist/debugCaptureModel.d.ts.map +1 -1
- package/dist/debugCaptureModel.js +36 -2
- package/dist/debugCaptureModel.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/panelAliasAnalysis.d.ts +1 -0
- package/dist/panelAliasAnalysis.d.ts.map +1 -1
- package/dist/panelAliasAnalysis.js +23 -54
- package/dist/panelAliasAnalysis.js.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.d.ts +15 -2
- package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.js +145 -23
- package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
- package/dist/panelDetailLayout.d.ts +27 -0
- package/dist/panelDetailLayout.d.ts.map +1 -0
- package/dist/panelDetailLayout.js +174 -0
- package/dist/panelDetailLayout.js.map +1 -0
- package/dist/panelDetailStyles.d.ts +2 -0
- package/dist/panelDetailStyles.d.ts.map +1 -0
- package/dist/panelDetailStyles.js +57 -0
- package/dist/panelDetailStyles.js.map +1 -0
- package/dist/panelDiagnosticsView.d.ts +10 -1
- package/dist/panelDiagnosticsView.d.ts.map +1 -1
- package/dist/panelDiagnosticsView.js +166 -57
- package/dist/panelDiagnosticsView.js.map +1 -1
- package/dist/panelDomHelpers.d.ts.map +1 -1
- package/dist/panelDomHelpers.js +6 -5
- package/dist/panelDomHelpers.js.map +1 -1
- package/dist/panelGraphLayout.d.ts +3 -2
- package/dist/panelGraphLayout.d.ts.map +1 -1
- package/dist/panelGraphLayout.js +6 -5
- package/dist/panelGraphLayout.js.map +1 -1
- package/dist/panelGraphRenderer.d.ts +10 -0
- package/dist/panelGraphRenderer.d.ts.map +1 -1
- package/dist/panelGraphRenderer.js.map +1 -1
- package/dist/panelGraphScene.d.ts +1 -0
- package/dist/panelGraphScene.d.ts.map +1 -1
- package/dist/panelGraphScene.js +69 -32
- package/dist/panelGraphScene.js.map +1 -1
- package/dist/panelGraphSearch.d.ts +16 -0
- package/dist/panelGraphSearch.d.ts.map +1 -0
- package/dist/panelGraphSearch.js +93 -0
- package/dist/panelGraphSearch.js.map +1 -0
- package/dist/panelGraphView.d.ts +82 -0
- package/dist/panelGraphView.d.ts.map +1 -1
- package/dist/panelGraphView.js +15 -4
- package/dist/panelGraphView.js.map +1 -1
- package/dist/panelGraphVisuals.d.ts +14 -6
- package/dist/panelGraphVisuals.d.ts.map +1 -1
- package/dist/panelGraphVisuals.js +126 -51
- package/dist/panelGraphVisuals.js.map +1 -1
- package/dist/panelInspectorView.d.ts +8 -0
- package/dist/panelInspectorView.d.ts.map +1 -1
- package/dist/panelInspectorView.js +242 -48
- package/dist/panelInspectorView.js.map +1 -1
- package/dist/panelListStyles.d.ts +3 -0
- package/dist/panelListStyles.d.ts.map +1 -0
- package/dist/panelListStyles.js +155 -0
- package/dist/panelListStyles.js.map +1 -0
- package/dist/panelMemoryStyles.d.ts +3 -0
- package/dist/panelMemoryStyles.d.ts.map +1 -0
- package/dist/panelMemoryStyles.js +42 -0
- package/dist/panelMemoryStyles.js.map +1 -0
- package/dist/panelMemoryView.d.ts +13 -1
- package/dist/panelMemoryView.d.ts.map +1 -1
- package/dist/panelMemoryView.js +161 -41
- package/dist/panelMemoryView.js.map +1 -1
- package/dist/panelPassesView.d.ts +15 -0
- package/dist/panelPassesView.d.ts.map +1 -1
- package/dist/panelPassesView.js +202 -72
- package/dist/panelPassesView.js.map +1 -1
- package/dist/panelRawView.d.ts +15 -0
- package/dist/panelRawView.d.ts.map +1 -0
- package/dist/panelRawView.js +105 -0
- package/dist/panelRawView.js.map +1 -0
- package/dist/panelResourcesView.d.ts +8 -0
- package/dist/panelResourcesView.d.ts.map +1 -1
- package/dist/panelResourcesView.js +62 -31
- package/dist/panelResourcesView.js.map +1 -1
- package/dist/panelSelection.d.ts +9 -1
- package/dist/panelSelection.d.ts.map +1 -1
- package/dist/panelSelection.js +34 -4
- package/dist/panelSelection.js.map +1 -1
- package/dist/panelTheme.d.ts +13 -0
- package/dist/panelTheme.d.ts.map +1 -0
- package/dist/panelTheme.js +104 -0
- package/dist/panelTheme.js.map +1 -0
- package/dist/panelTypes.d.ts +10 -1
- package/dist/panelTypes.d.ts.map +1 -1
- package/dist/panelTypes.js.map +1 -1
- package/dist/panelVisualTheme.d.ts +147 -50
- package/dist/panelVisualTheme.d.ts.map +1 -1
- package/dist/panelVisualTheme.js +34 -44
- package/dist/panelVisualTheme.js.map +1 -1
- package/dist/panelWorkbenchHelpers.d.ts +12 -1
- package/dist/panelWorkbenchHelpers.d.ts.map +1 -1
- package/dist/panelWorkbenchHelpers.js +62 -1
- package/dist/panelWorkbenchHelpers.js.map +1 -1
- package/dist/panelWorkbenchStyles.d.ts +2 -0
- package/dist/panelWorkbenchStyles.d.ts.map +1 -0
- package/dist/panelWorkbenchStyles.js +101 -0
- package/dist/panelWorkbenchStyles.js.map +1 -0
- package/dist/panelWorkbenchView.d.ts +16 -0
- package/dist/panelWorkbenchView.d.ts.map +1 -1
- package/dist/panelWorkbenchView.js +228 -50
- package/dist/panelWorkbenchView.js.map +1 -1
- package/dist/query.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +147 -195
- package/dist/styles.js.map +1 -1
- package/dist/theme.d.ts +22 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +10 -0
- package/dist/theme.js.map +1 -0
- package/dist/themeDefinitions.d.ts +251 -0
- package/dist/themeDefinitions.d.ts.map +1 -0
- package/dist/themeDefinitions.js +96 -0
- package/dist/themeDefinitions.js.map +1 -0
- package/dist/themePalette.d.ts +66 -0
- package/dist/themePalette.d.ts.map +1 -0
- package/dist/themePalette.js +37 -0
- package/dist/themePalette.js.map +1 -0
- package/dist/themes.css +165 -0
- package/package.json +44 -10
- package/src/FrameGraphInspector.ts +130 -31
- package/src/capture.ts +4 -0
- package/src/debugCaptureModel.ts +53 -2
- package/src/index.ts +6 -2
- package/src/panelAliasAnalysis.ts +24 -70
- package/src/panelCytoscapeGraphRenderer.ts +140 -23
- package/src/panelDetailLayout.ts +155 -0
- package/src/panelDetailStyles.ts +56 -0
- package/src/panelDiagnosticsView.ts +166 -85
- package/src/panelDomHelpers.ts +4 -5
- package/src/panelGraphLayout.ts +6 -5
- package/src/panelGraphRenderer.ts +7 -0
- package/src/panelGraphScene.ts +68 -30
- package/src/panelGraphSearch.ts +96 -0
- package/src/panelGraphView.ts +14 -4
- package/src/panelGraphVisuals.ts +122 -52
- package/src/panelInspectorView.ts +227 -48
- package/src/panelListStyles.ts +154 -0
- package/src/panelMemoryStyles.ts +41 -0
- package/src/panelMemoryView.ts +178 -53
- package/src/panelPassesView.ts +190 -137
- package/src/panelRawView.ts +99 -0
- package/src/panelResourcesView.ts +69 -56
- package/src/panelSelection.ts +37 -5
- package/src/panelTheme.ts +85 -0
- package/src/panelTypes.ts +7 -1
- package/src/panelVisualTheme.ts +38 -49
- package/src/panelWorkbenchHelpers.ts +60 -2
- package/src/panelWorkbenchStyles.ts +100 -0
- package/src/panelWorkbenchView.ts +201 -52
- package/src/query.ts +11 -11
- package/src/styles.ts +148 -195
- package/src/theme.ts +24 -0
- package/src/themeDefinitions.ts +103 -0
- package/src/themePalette.ts +44 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
|
|
2
|
+
import { labelNode, labelResource } from './panelDomHelpers.ts';
|
|
3
|
+
import type { Selection } from './panelTypes.ts';
|
|
4
|
+
|
|
5
|
+
type SearchEntry = { readonly label: string; readonly kind: string; readonly text: string; readonly selection: Selection };
|
|
6
|
+
|
|
7
|
+
export class GraphSearch {
|
|
8
|
+
readonly root = document.createElement('div');
|
|
9
|
+
private readonly toggle = document.createElement('button');
|
|
10
|
+
private readonly popover = document.createElement('div');
|
|
11
|
+
private readonly input = document.createElement('input');
|
|
12
|
+
private readonly results = document.createElement('div');
|
|
13
|
+
private entries: SearchEntry[] = [];
|
|
14
|
+
|
|
15
|
+
constructor(private readonly onReveal: (selection: Selection) => void, id: string) {
|
|
16
|
+
this.root.className = 'zenfg-inspector-graph-search';
|
|
17
|
+
this.toggle.type = 'button';
|
|
18
|
+
this.toggle.textContent = 'Search';
|
|
19
|
+
this.toggle.setAttribute('aria-expanded', 'false');
|
|
20
|
+
this.toggle.setAttribute('aria-controls', id);
|
|
21
|
+
this.toggle.addEventListener('click', () => this.setOpen(this.popover.hidden !== false));
|
|
22
|
+
this.popover.className = 'zenfg-inspector-graph-search-popover';
|
|
23
|
+
this.popover.id = id;
|
|
24
|
+
this.popover.hidden = true;
|
|
25
|
+
this.input.type = 'search';
|
|
26
|
+
this.input.placeholder = 'Find pass, resource, group or output';
|
|
27
|
+
this.input.setAttribute('aria-label', 'Find in graph');
|
|
28
|
+
this.results.className = 'zenfg-inspector-graph-search-results';
|
|
29
|
+
this.results.setAttribute('aria-label', 'Graph search results');
|
|
30
|
+
this.results.hidden = true;
|
|
31
|
+
this.input.addEventListener('input', () => this.render());
|
|
32
|
+
this.root.addEventListener('keydown', (event) => {
|
|
33
|
+
if (event.key === 'Escape' && !this.popover.hidden) {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
this.setOpen(false);
|
|
37
|
+
} else if (event.key === 'ArrowDown' && event.target === this.input) {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
this.results.querySelector('button')?.focus();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
this.popover.append(this.input, this.results);
|
|
43
|
+
this.root.append(this.toggle, this.popover);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private setOpen(open: boolean): void {
|
|
47
|
+
this.popover.hidden = !open;
|
|
48
|
+
this.toggle.setAttribute('aria-expanded', String(open));
|
|
49
|
+
if (open) this.input.focus();
|
|
50
|
+
else {
|
|
51
|
+
this.input.value = '';
|
|
52
|
+
this.results.hidden = true;
|
|
53
|
+
this.toggle.focus();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setSnapshot(snapshot: FrameGraphDebugViewModel): void {
|
|
58
|
+
this.entries = [
|
|
59
|
+
...snapshot.nodes.map((node): SearchEntry => ({ label: labelNode(node), kind: 'Pass',
|
|
60
|
+
text: `${node.id} ${labelNode(node)} ${node.debugGroupId ? snapshot.groupById.get(node.debugGroupId)?.path.join(' / ') : ''}`,
|
|
61
|
+
selection: { kind: 'node', id: node.id } })),
|
|
62
|
+
...snapshot.resources.map((resource): SearchEntry => ({ label: labelResource(resource), kind: 'Resource',
|
|
63
|
+
text: `${resource.id} ${labelResource(resource)}`, selection: { kind: 'resource', id: resource.id } })),
|
|
64
|
+
...snapshot.debugGroups.map((group): SearchEntry => ({ label: group.path.join(' / '), kind: 'Group',
|
|
65
|
+
text: `${group.id} ${group.path.join(' / ')}`, selection: { kind: 'group', pathKey: group.pathKey } })),
|
|
66
|
+
...snapshot.roots.filter((root) => root.reason !== 'side-effect').map((root): SearchEntry => ({
|
|
67
|
+
label: `${root.reason} · ${root.resource ? labelResource(root.resource) : root.key}`, kind: 'Output',
|
|
68
|
+
text: `${root.reason} ${root.resource?.id} ${root.resource?.label}`, selection: { kind: 'root', key: root.key },
|
|
69
|
+
})),
|
|
70
|
+
];
|
|
71
|
+
this.render();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private render(): void {
|
|
75
|
+
const query = this.input.value.trim().toLocaleLowerCase();
|
|
76
|
+
this.results.replaceChildren();
|
|
77
|
+
this.results.hidden = !query;
|
|
78
|
+
if (!query) return;
|
|
79
|
+
const matches = this.entries.filter((entry) => entry.text.toLocaleLowerCase().includes(query));
|
|
80
|
+
const count = document.createElement('p');
|
|
81
|
+
count.setAttribute('role', 'status');
|
|
82
|
+
count.textContent = matches.length > 50 ? `Showing 50 of ${matches.length} results. Refine your search.` : `${matches.length} results`;
|
|
83
|
+
this.results.append(count);
|
|
84
|
+
for (const entry of matches.slice(0, 50)) {
|
|
85
|
+
const button = document.createElement('button');
|
|
86
|
+
button.type = 'button';
|
|
87
|
+
button.textContent = `${entry.kind} · ${entry.label}`;
|
|
88
|
+
button.title = `Show in Graph · ${entry.label}`;
|
|
89
|
+
button.addEventListener('click', () => {
|
|
90
|
+
this.setOpen(false);
|
|
91
|
+
this.onReveal(entry.selection);
|
|
92
|
+
});
|
|
93
|
+
this.results.append(button);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/panelGraphView.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GRAPH_VISUAL_THEME } from './panelVisualTheme.ts';
|
|
1
2
|
import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
|
|
2
3
|
import { CytoscapeGraphRenderer } from './panelCytoscapeGraphRenderer.ts';
|
|
3
4
|
import { createGraphScene, type GraphScene } from './panelGraphScene.ts';
|
|
@@ -22,7 +23,8 @@ export function renderGraphView(
|
|
|
22
23
|
onToggleGroup: (pathKey: string) => void,
|
|
23
24
|
): void {
|
|
24
25
|
const scene = resolveGraphScene(graphView, snapshot);
|
|
25
|
-
|
|
26
|
+
if (!graphView.theme) graphView.refreshTheme?.();
|
|
27
|
+
renderGraphLegend(graphView.legend, snapshot, graphView.theme, graphView.showResourceDeclarations);
|
|
26
28
|
const elementCount = scene.nodes.length + scene.edges.length;
|
|
27
29
|
const layoutElementBudget = graphView.layoutElementBudget ?? Number.MAX_SAFE_INTEGER;
|
|
28
30
|
if (elementCount > layoutElementBudget) {
|
|
@@ -39,16 +41,21 @@ export function renderGraphView(
|
|
|
39
41
|
graphView.renderer ??= new CytoscapeGraphRenderer(graphView.host);
|
|
40
42
|
graphView.renderer.render({
|
|
41
43
|
scene,
|
|
44
|
+
theme: graphView.theme,
|
|
45
|
+
onVisible: graphView.refreshTheme,
|
|
42
46
|
selected,
|
|
43
47
|
hovered,
|
|
44
48
|
fit: graphView.fitOnNextRender,
|
|
45
49
|
anchorElementId: graphView.anchorElementIdOnNextRender,
|
|
50
|
+
reveal: graphView.revealOnNextRender,
|
|
51
|
+
captureRevision: graphView.captureRevision,
|
|
46
52
|
onSelect,
|
|
47
53
|
onHover,
|
|
48
54
|
onToggleGroup,
|
|
49
55
|
});
|
|
50
56
|
graphView.fitOnNextRender = false;
|
|
51
57
|
graphView.anchorElementIdOnNextRender = undefined;
|
|
58
|
+
graphView.revealOnNextRender = undefined;
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
export function resolveGraphScene(
|
|
@@ -57,12 +64,14 @@ export function resolveGraphScene(
|
|
|
57
64
|
): GraphScene {
|
|
58
65
|
const optionsKey = JSON.stringify([
|
|
59
66
|
graphView.groupsEnabled,
|
|
67
|
+
graphView.showResourceDeclarations ?? true,
|
|
60
68
|
[...graphView.expandedGroupPaths].sort(),
|
|
61
69
|
]);
|
|
62
70
|
const cached = graphSceneCache.get(graphView);
|
|
63
71
|
if (cached?.snapshot === snapshot && cached.optionsKey === optionsKey) return cached.scene;
|
|
64
72
|
const scene = createGraphScene(snapshot, {
|
|
65
73
|
groupsEnabled: graphView.groupsEnabled,
|
|
74
|
+
showResourceDeclarations: graphView.showResourceDeclarations,
|
|
66
75
|
expandedGroupPaths: graphView.expandedGroupPaths,
|
|
67
76
|
});
|
|
68
77
|
graphSceneCache.set(graphView, { snapshot, optionsKey, scene });
|
|
@@ -74,6 +83,7 @@ export function fitGraph(graphView: GraphViewState): void {
|
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
export function resizeGraph(graphView: GraphViewState): void {
|
|
86
|
+
graphView.refreshTheme?.();
|
|
77
87
|
graphView.renderer?.resize();
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -83,9 +93,9 @@ export function destroyGraph(graphView: GraphViewState): void {
|
|
|
83
93
|
graphSceneCache.delete(graphView);
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
function renderGraphLegend(host: HTMLElement | undefined, snapshot: FrameGraphDebugViewModel): void {
|
|
96
|
+
export function renderGraphLegend(host: HTMLElement | undefined, snapshot: FrameGraphDebugViewModel, theme = GRAPH_VISUAL_THEME, showResourceDeclarations = true): void {
|
|
87
97
|
if (!host) return;
|
|
88
|
-
const entries = createGraphLegend(snapshot);
|
|
98
|
+
const entries = createGraphLegend(snapshot, theme, showResourceDeclarations);
|
|
89
99
|
const key = JSON.stringify(entries);
|
|
90
100
|
if (host.dataset.legendKey === key) return;
|
|
91
101
|
host.dataset.legendKey = key;
|
|
@@ -117,7 +127,7 @@ function renderGraphLegend(host: HTMLElement | undefined, snapshot: FrameGraphDe
|
|
|
117
127
|
svg.setAttribute('aria-hidden', 'true');
|
|
118
128
|
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
|
119
129
|
polygon.setAttribute('points', entry.shape === 'tag'
|
|
120
|
-
? '1,1
|
|
130
|
+
? '1,1 16,1 19,7 16,13 1,13'
|
|
121
131
|
: '4,1 16,1 19,4 19,10 16,13 4,13 1,10 1,4');
|
|
122
132
|
svg.appendChild(polygon);
|
|
123
133
|
swatch.appendChild(svg);
|
package/src/panelGraphVisuals.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type cytoscape from 'cytoscape';
|
|
2
2
|
|
|
3
3
|
import type { GraphScene, GraphSceneEdge, GraphSceneNode } from './panelGraphScene.ts';
|
|
4
|
-
import { GRAPH_VISUAL_THEME } from './panelVisualTheme.ts';
|
|
4
|
+
import { GRAPH_VISUAL_THEME, type GraphVisualTheme } from './panelVisualTheme.ts';
|
|
5
5
|
import { declarationEntrances, type FrameGraphDebugViewModel } from './debugCaptureModel.ts';
|
|
6
6
|
|
|
7
7
|
export { GRAPH_VISUAL_THEME } from './panelVisualTheme.ts';
|
|
@@ -17,8 +17,9 @@ export const GRAPH_GEOMETRY = {
|
|
|
17
17
|
groupPadding: 32,
|
|
18
18
|
elkGroupPadding: { top: 46, right: 24, bottom: 24, left: 24 },
|
|
19
19
|
edgeCornerRadius: 6,
|
|
20
|
-
outputWidth:
|
|
21
|
-
|
|
20
|
+
outputWidth: 208,
|
|
21
|
+
outputTipWidth: 24,
|
|
22
|
+
outputLabelWidth: 160,
|
|
22
23
|
} as const;
|
|
23
24
|
|
|
24
25
|
const ENDPOINT_EDGE_DISTANCES = 'endpoints' as unknown as cytoscape.Css.Edge['edge-distances'];
|
|
@@ -34,49 +35,57 @@ export type GraphLegendEntry = {
|
|
|
34
35
|
readonly hollowArrow?: boolean;
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
export function createGraphLegend(snapshot: FrameGraphDebugViewModel, theme: GraphVisualTheme = GRAPH_VISUAL_THEME, showResourceDeclarations = true): readonly GraphLegendEntry[] {
|
|
39
|
+
const nodeEntries = {
|
|
40
|
+
render: nodeLegend('render', 'Render', theme.render.stroke),
|
|
41
|
+
compute: nodeLegend('compute', 'Compute', theme.compute.stroke),
|
|
42
|
+
copy: nodeLegend('copy', 'Copy', theme.copy.stroke),
|
|
43
|
+
'clear-buffer': nodeLegend('clear', 'Clear', theme.clear.stroke),
|
|
44
|
+
command: nodeLegend('command', 'Command', theme.command.stroke),
|
|
45
|
+
'external-submission': { ...nodeLegend('external', 'External', theme.external.stroke), shape: 'cut-rectangle' as const },
|
|
46
|
+
} as const;
|
|
45
47
|
|
|
46
|
-
export function createGraphLegend(snapshot: FrameGraphDebugViewModel): readonly GraphLegendEntry[] {
|
|
47
48
|
const entries: GraphLegendEntry[] = [];
|
|
48
49
|
const passKinds = new Set(snapshot.nodes.map((node) => node.kind));
|
|
49
50
|
for (const kind of ['render', 'compute', 'copy', 'clear-buffer', 'command', 'external-submission'] as const) {
|
|
50
|
-
if (passKinds.has(kind)) entries.push(
|
|
51
|
+
if (passKinds.has(kind)) entries.push(nodeEntries[kind]);
|
|
51
52
|
}
|
|
52
53
|
const retained = new Set(snapshot.nodes.map((node) => node.id));
|
|
53
54
|
const used = new Set([
|
|
54
55
|
...snapshot.accessEdges.filter((access) => retained.has(access.nodeId)).map((access) => access.resource.id),
|
|
55
56
|
...snapshot.roots.flatMap((root) => root.resource ? [root.resource.id] : []),
|
|
56
57
|
]);
|
|
57
|
-
const resources = snapshot.resources.filter((resource) => used.has(resource.id));
|
|
58
|
-
if (resources.length) entries.push({ group: 'Resources', key: 'declaration', label: 'Declaration', color:
|
|
59
|
-
if (snapshot.roots.some((root) => root.resource)) entries.push({ group: 'Resources', key: 'output', label: 'Output', color:
|
|
58
|
+
const resources = showResourceDeclarations ? snapshot.resources.filter((resource) => used.has(resource.id)) : [];
|
|
59
|
+
if (resources.length) entries.push({ group: 'Resources', key: 'declaration', label: 'Declaration', color: theme.declaration.stroke, shape: 'ellipse' });
|
|
60
|
+
if (snapshot.roots.some((root) => root.resource)) entries.push({ group: 'Resources', key: 'output', label: 'Output', color: theme.output.stroke, shape: 'tag' });
|
|
60
61
|
if ([...snapshot.nodes, ...resources].some((item) => item.debugGroupId !== undefined)) {
|
|
61
|
-
entries.push({ group: 'Relationships', key: 'group', label: 'Group', color:
|
|
62
|
+
entries.push({ group: 'Relationships', key: 'group', label: 'Group', color: theme.group.stroke, shape: 'group' });
|
|
62
63
|
}
|
|
63
64
|
if (snapshot.edges.some((edge) => edge.kind === 'value')
|
|
64
|
-
|| declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges).length
|
|
65
|
-
|| snapshot.roots.some((root) => root.resource && root.resolution && (root.resolution.usesInitialContents || root.resolution.producerNodeIds.length))) {
|
|
66
|
-
entries.push(edgeLegend('flow', 'Resource Flow',
|
|
65
|
+
|| (showResourceDeclarations && declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges).length)
|
|
66
|
+
|| snapshot.roots.some((root) => root.resource && root.resolution && ((showResourceDeclarations && root.resolution.usesInitialContents) || root.resolution.producerNodeIds.length))) {
|
|
67
|
+
entries.push(edgeLegend('flow', 'Resource Flow', theme.dependency.value, 'solid'));
|
|
67
68
|
}
|
|
68
69
|
if (snapshot.edges.some((edge) => edge.kind === 'ordering')) {
|
|
69
70
|
entries.push({
|
|
70
|
-
...edgeLegend('ordering', 'Order',
|
|
71
|
+
...edgeLegend('ordering', 'Order', theme.dependency.ordering, 'dotted'),
|
|
71
72
|
hollowArrow: true,
|
|
72
73
|
});
|
|
73
74
|
}
|
|
74
75
|
return entries;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
78
|
-
const theme = GRAPH_VISUAL_THEME;
|
|
78
|
+
export function createGraphStyles(theme: GraphVisualTheme = GRAPH_VISUAL_THEME): cytoscape.StylesheetJson {
|
|
79
79
|
return [
|
|
80
|
+
{
|
|
81
|
+
selector: 'core',
|
|
82
|
+
style: {
|
|
83
|
+
'active-bg-color': theme.activeBackground.color,
|
|
84
|
+
'active-bg-opacity': theme.activeBackground.opacity,
|
|
85
|
+
'active-bg-size': theme.activeBackground.size,
|
|
86
|
+
// Cytoscape accepts partial core styles; its Core type marks every property required.
|
|
87
|
+
} satisfies Partial<cytoscape.Css.Core> as cytoscape.Css.Core,
|
|
88
|
+
},
|
|
80
89
|
{
|
|
81
90
|
selector: 'node',
|
|
82
91
|
style: {
|
|
@@ -85,14 +94,15 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
85
94
|
'height': 'data(height)',
|
|
86
95
|
'label': 'data(displayLabel)',
|
|
87
96
|
'text-wrap': 'wrap',
|
|
88
|
-
'text-max-width': '
|
|
89
|
-
'font-family':
|
|
90
|
-
'font-size':
|
|
97
|
+
'text-max-width': 'data(labelMaxWidth)',
|
|
98
|
+
'font-family': theme.fontFamily,
|
|
99
|
+
'font-size': (element: cytoscape.SingularElementArgument) => (element.data('labelFontSize') as number | undefined) ?? theme.fontSize,
|
|
100
|
+
'line-height': 1.2,
|
|
91
101
|
'min-zoomed-font-size': GRAPH_GEOMETRY.minimumZoomedFontSize,
|
|
92
102
|
'color': theme.text,
|
|
93
103
|
'background-color': theme.surfaceRaised,
|
|
94
104
|
'border-color': theme.group.stroke,
|
|
95
|
-
'border-width':
|
|
105
|
+
'border-width': theme.nodeBorderWidth,
|
|
96
106
|
'border-style': 'solid',
|
|
97
107
|
'text-valign': 'center',
|
|
98
108
|
'text-halign': 'center',
|
|
@@ -125,12 +135,13 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
125
135
|
{
|
|
126
136
|
selector: 'node[kind = "root"]',
|
|
127
137
|
style: {
|
|
128
|
-
'shape': '
|
|
138
|
+
'shape': 'polygon',
|
|
139
|
+
'shape-polygon-points': `-1 -1 ${1 - 2 * GRAPH_GEOMETRY.outputTipWidth / GRAPH_GEOMETRY.outputWidth} -1 1 0 ${1 - 2 * GRAPH_GEOMETRY.outputTipWidth / GRAPH_GEOMETRY.outputWidth} 1 -1 1`,
|
|
129
140
|
'background-color': theme.output.fill,
|
|
130
141
|
'border-color': theme.output.stroke,
|
|
131
|
-
'text-max-width':
|
|
132
|
-
//
|
|
133
|
-
'text-margin-x': -GRAPH_GEOMETRY.
|
|
142
|
+
'text-max-width': 'data(labelMaxWidth)',
|
|
143
|
+
// Keep a short tip and center the label in the rectangular body.
|
|
144
|
+
'text-margin-x': -GRAPH_GEOMETRY.outputTipWidth / 2 * theme.fontSize / GRAPH_GEOMETRY.baseFontSize,
|
|
134
145
|
},
|
|
135
146
|
},
|
|
136
147
|
{
|
|
@@ -144,13 +155,13 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
144
155
|
style: {
|
|
145
156
|
'background-color': theme.group.fill,
|
|
146
157
|
'border-color': theme.group.stroke,
|
|
147
|
-
'border-width':
|
|
158
|
+
'border-width': theme.groupBorderWidth,
|
|
148
159
|
'color': theme.text,
|
|
149
160
|
},
|
|
150
161
|
},
|
|
151
162
|
{
|
|
152
163
|
selector: 'node[kind = "group"][collapsed = 0]',
|
|
153
|
-
style: expandedGroupStyle(theme.group.fill),
|
|
164
|
+
style: expandedGroupStyle(theme.group.fill, theme),
|
|
154
165
|
},
|
|
155
166
|
{
|
|
156
167
|
selector: 'node[kind = "group"][collapsed = 0][depthBand = 1]',
|
|
@@ -159,16 +170,16 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
159
170
|
{
|
|
160
171
|
selector: 'edge',
|
|
161
172
|
style: {
|
|
162
|
-
'width':
|
|
173
|
+
'width': theme.edgeWidth,
|
|
163
174
|
'line-color': theme.dependency.value,
|
|
164
|
-
'line-opacity':
|
|
175
|
+
'line-opacity': theme.edgeOpacity,
|
|
165
176
|
'target-arrow-color': theme.dependency.value,
|
|
166
177
|
'target-arrow-shape': 'triangle',
|
|
167
178
|
'target-arrow-fill': 'filled',
|
|
168
|
-
'arrow-scale':
|
|
179
|
+
'arrow-scale': theme.arrowScale,
|
|
169
180
|
'curve-style': 'straight',
|
|
170
181
|
'label': 'data(displayLabel)',
|
|
171
|
-
'font-family':
|
|
182
|
+
'font-family': theme.fontFamily,
|
|
172
183
|
'font-size': 10,
|
|
173
184
|
'min-zoomed-font-size': GRAPH_GEOMETRY.minimumZoomedFontSize,
|
|
174
185
|
'color': theme.text,
|
|
@@ -187,7 +198,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
187
198
|
'target-arrow-color': theme.dependency.ordering,
|
|
188
199
|
'line-style': 'dotted',
|
|
189
200
|
'target-arrow-fill': 'hollow',
|
|
190
|
-
'line-opacity':
|
|
201
|
+
'line-opacity': theme.orderingOpacity,
|
|
191
202
|
},
|
|
192
203
|
},
|
|
193
204
|
{
|
|
@@ -211,7 +222,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
211
222
|
selector: 'node.semantic-hover',
|
|
212
223
|
style: {
|
|
213
224
|
'border-color': theme.hover,
|
|
214
|
-
'border-width':
|
|
225
|
+
'border-width': theme.hoverWidth,
|
|
215
226
|
'z-index': 15,
|
|
216
227
|
},
|
|
217
228
|
},
|
|
@@ -221,7 +232,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
221
232
|
'line-color': theme.hover,
|
|
222
233
|
'target-arrow-color': theme.hover,
|
|
223
234
|
'line-opacity': 1,
|
|
224
|
-
'width':
|
|
235
|
+
'width': theme.hoverWidth,
|
|
225
236
|
'z-index': 15,
|
|
226
237
|
},
|
|
227
238
|
},
|
|
@@ -229,7 +240,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
229
240
|
selector: 'node.semantic-selected',
|
|
230
241
|
style: {
|
|
231
242
|
'border-color': theme.selected,
|
|
232
|
-
'border-width':
|
|
243
|
+
'border-width': theme.selectedWidth,
|
|
233
244
|
'opacity': 1,
|
|
234
245
|
'z-index': 20,
|
|
235
246
|
},
|
|
@@ -240,14 +251,14 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
240
251
|
'line-color': theme.selected,
|
|
241
252
|
'target-arrow-color': theme.selected,
|
|
242
253
|
'line-opacity': 1,
|
|
243
|
-
'width':
|
|
254
|
+
'width': theme.selectedWidth,
|
|
244
255
|
'z-index': 20,
|
|
245
256
|
},
|
|
246
257
|
},
|
|
247
258
|
];
|
|
248
259
|
}
|
|
249
260
|
|
|
250
|
-
|
|
261
|
+
function baseNodeDimensions(node: GraphSceneNode): { readonly width: number; readonly height: number } {
|
|
251
262
|
switch (node.kind) {
|
|
252
263
|
case 'pass':
|
|
253
264
|
return { width: 184, height: node.label.includes('\n') ? 62 : 48 };
|
|
@@ -256,10 +267,68 @@ export function nodeDimensions(node: GraphSceneNode): { readonly width: number;
|
|
|
256
267
|
case 'resource':
|
|
257
268
|
return { width: 184, height: Math.max(58, node.label.split('\n').length * 17 + 24) };
|
|
258
269
|
case 'group':
|
|
259
|
-
return node.collapsed ? { width:
|
|
270
|
+
return node.collapsed ? { width: 240, height: 80 } : { width: 120, height: 80 };
|
|
260
271
|
}
|
|
261
272
|
}
|
|
262
273
|
|
|
274
|
+
export function nodeDimensions(node: GraphSceneNode, theme: GraphVisualTheme = GRAPH_VISUAL_THEME): { readonly width: number; readonly height: number } {
|
|
275
|
+
const base = baseNodeDimensions(node);
|
|
276
|
+
const scale = theme.fontSize / GRAPH_GEOMETRY.baseFontSize;
|
|
277
|
+
return { width: base.width * scale, height: base.height * scale };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Enlarge short labels within the existing box; retain the theme size for dense text. */
|
|
281
|
+
export function graphLabelFontSize(label: string, width: number, height: number, theme: GraphVisualTheme): number {
|
|
282
|
+
const context = graphTextContext();
|
|
283
|
+
if (context) context.font = theme.fontSize + 'px ' + theme.fontFamily;
|
|
284
|
+
const lines = label.split('\n');
|
|
285
|
+
const widest = Math.max(1, ...lines.map(line => context?.measureText(line).width ?? [...line].length * theme.fontSize * .62));
|
|
286
|
+
const widthScale = width / widest;
|
|
287
|
+
const heightScale = height / (lines.length * theme.fontSize * 1.2);
|
|
288
|
+
return Math.max(theme.fontSize, Math.floor(theme.fontSize * Math.min(1.25, widthScale, heightScale) * 4) / 4);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Wrap only the group name (up to two lines); keep the node-count row distinct. */
|
|
292
|
+
export function fitCollapsedGroupLabel(label: string, width: number, theme: GraphVisualTheme): string {
|
|
293
|
+
const [name = '', ...summary] = label.split('\n');
|
|
294
|
+
const context = graphTextContext();
|
|
295
|
+
if (context) context.font = theme.fontSize + 'px ' + theme.fontFamily;
|
|
296
|
+
const measure = (text: string) => context?.measureText(text).width ?? [...text].length * theme.fontSize * .62;
|
|
297
|
+
const chars = [...name];
|
|
298
|
+
let split = 0;
|
|
299
|
+
while (split < chars.length && measure(chars.slice(0, split + 1).join('')) <= width) split++;
|
|
300
|
+
if (split === chars.length) return fitGraphLabel(label, width, theme);
|
|
301
|
+
// Prefer a word/path boundary without losing characters, including Unicode.
|
|
302
|
+
const prefix = chars.slice(0, split).join('');
|
|
303
|
+
const boundary = Math.max(prefix.lastIndexOf(' '), prefix.lastIndexOf('.'), prefix.lastIndexOf('/'));
|
|
304
|
+
if (boundary > prefix.length / 2) split = [...prefix.slice(0, boundary + 1)].length;
|
|
305
|
+
return fitGraphLabel([
|
|
306
|
+
chars.slice(0, split).join('').trimEnd(),
|
|
307
|
+
chars.slice(split).join('').trimStart(),
|
|
308
|
+
...summary,
|
|
309
|
+
].join('\n'), width, theme);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Fit the already semantic label to its available width after font changes. */
|
|
313
|
+
export function fitGraphLabel(label: string, width: number, theme: GraphVisualTheme): string {
|
|
314
|
+
const context = graphTextContext();
|
|
315
|
+
if (context) context.font = theme.fontSize + 'px ' + theme.fontFamily;
|
|
316
|
+
const measure = (text: string) => context?.measureText(text).width ?? [...text].length * theme.fontSize * .62;
|
|
317
|
+
return label.split('\n').map(line => {
|
|
318
|
+
if (measure(line) <= width) return line;
|
|
319
|
+
const chars = [...line];
|
|
320
|
+
while (chars.length && measure(chars.join('') + '…') > width) chars.pop();
|
|
321
|
+
return chars.join('') + '…';
|
|
322
|
+
}).join('\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let textContext: CanvasRenderingContext2D | null | undefined;
|
|
326
|
+
function graphTextContext(): CanvasRenderingContext2D | null {
|
|
327
|
+
if (typeof document === 'undefined') return null;
|
|
328
|
+
if (textContext === undefined) textContext = document.createElement('canvas').getContext('2d');
|
|
329
|
+
return textContext;
|
|
330
|
+
}
|
|
331
|
+
|
|
263
332
|
export function graphLayoutGeometryKey(scene: GraphScene): string {
|
|
264
333
|
return JSON.stringify({
|
|
265
334
|
topology: scene.topologyKey,
|
|
@@ -278,8 +347,8 @@ export function expandedGroupLabelMaxWidth(outerWidth: number): number {
|
|
|
278
347
|
return Math.max(80, Math.floor(outerWidth - GRAPH_GEOMETRY.groupTitleInset * 2));
|
|
279
348
|
}
|
|
280
349
|
|
|
281
|
-
export function isOverviewGraphScale(zoom: number): boolean {
|
|
282
|
-
return
|
|
350
|
+
export function isOverviewGraphScale(zoom: number, theme: GraphVisualTheme = GRAPH_VISUAL_THEME): boolean {
|
|
351
|
+
return theme.fontSize * zoom < GRAPH_GEOMETRY.overviewFontThreshold;
|
|
283
352
|
}
|
|
284
353
|
|
|
285
354
|
function nodeLegend(key: string, label: string, color: string): GraphLegendEntry {
|
|
@@ -305,15 +374,16 @@ function passStyle(
|
|
|
305
374
|
};
|
|
306
375
|
}
|
|
307
376
|
|
|
308
|
-
function expandedGroupStyle(backgroundColor: string): cytoscape.Css.Node {
|
|
377
|
+
function expandedGroupStyle(backgroundColor: string, theme: GraphVisualTheme): cytoscape.Css.Node {
|
|
309
378
|
const geometry = GRAPH_GEOMETRY;
|
|
310
379
|
return {
|
|
311
380
|
'background-color': backgroundColor,
|
|
312
|
-
'background-opacity':
|
|
313
|
-
'border-color':
|
|
314
|
-
'border-width':
|
|
381
|
+
'background-opacity': theme.groupOpacity,
|
|
382
|
+
'border-color': theme.group.stroke,
|
|
383
|
+
'border-width': theme.groupBorderWidth,
|
|
315
384
|
'padding': `${geometry.groupPadding}px`,
|
|
316
|
-
'compound-sizing-wrt-labels': '
|
|
385
|
+
'compound-sizing-wrt-labels': 'exclude',
|
|
386
|
+
'text-wrap': 'ellipsis',
|
|
317
387
|
'min-width': '120px',
|
|
318
388
|
'min-height': '80px',
|
|
319
389
|
'text-valign': 'top-inside',
|
|
@@ -322,7 +392,7 @@ function expandedGroupStyle(backgroundColor: string): cytoscape.Css.Node {
|
|
|
322
392
|
'text-margin-x': geometry.groupTitleInset - geometry.groupPadding * 2,
|
|
323
393
|
'text-margin-y': geometry.groupTitleInset - geometry.groupPadding * 2,
|
|
324
394
|
'line-height': geometry.groupTitleLineHeight,
|
|
325
|
-
'color':
|
|
395
|
+
'color': theme.text,
|
|
326
396
|
'font-weight': 600,
|
|
327
397
|
};
|
|
328
398
|
}
|