@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,7 +1,8 @@
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
+ import { declarationEntrances, type FrameGraphDebugViewModel } from './debugCaptureModel.ts';
5
6
 
6
7
  export { GRAPH_VISUAL_THEME } from './panelVisualTheme.ts';
7
8
 
@@ -16,76 +17,75 @@ export const GRAPH_GEOMETRY = {
16
17
  groupPadding: 32,
17
18
  elkGroupPadding: { top: 46, right: 24, bottom: 24, left: 24 },
18
19
  edgeCornerRadius: 6,
20
+ outputWidth: 208,
21
+ outputTipWidth: 24,
22
+ outputLabelWidth: 160,
19
23
  } as const;
20
24
 
21
25
  const ENDPOINT_EDGE_DISTANCES = 'endpoints' as unknown as cytoscape.Css.Edge['edge-distances'];
22
26
  const EDGE_SEGMENT_RADII = `${GRAPH_GEOMETRY.edgeCornerRadius}px` as unknown as cytoscape.Css.Edge['segment-radii'];
23
27
 
24
28
  export type GraphLegendEntry = {
29
+ readonly group: 'Execution' | 'Resources' | 'Relationships';
25
30
  readonly key: string;
26
31
  readonly label: string;
27
32
  readonly color: string;
28
- readonly shape: 'box' | 'ellipse' | 'group' | 'line';
33
+ readonly shape: 'box' | 'cut-rectangle' | 'tag' | 'ellipse' | 'group' | 'line';
29
34
  readonly lineStyle?: 'solid' | 'dotted' | 'dashed';
30
35
  readonly hollowArrow?: boolean;
31
36
  };
32
37
 
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;
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;
41
47
 
42
- export function createGraphLegend(scene: GraphScene): readonly GraphLegendEntry[] {
43
48
  const entries: GraphLegendEntry[] = [];
44
- const passKinds = new Set(scene.nodes.flatMap((node) => node.kind === 'pass' || node.kind === 'culled-pass'
45
- ? [node.passKind]
46
- : []));
49
+ const passKinds = new Set(snapshot.nodes.map((node) => node.kind));
47
50
  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' });
51
+ if (passKinds.has(kind)) entries.push(nodeEntries[kind]);
52
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' });
53
+ const retained = new Set(snapshot.nodes.map((node) => node.id));
54
+ const used = new Set([
55
+ ...snapshot.accessEdges.filter((access) => retained.has(access.nodeId)).map((access) => access.resource.id),
56
+ ...snapshot.roots.flatMap((root) => root.resource ? [root.resource.id] : []),
57
+ ]);
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' });
61
+ if ([...snapshot.nodes, ...resources].some((item) => item.debugGroupId !== undefined)) {
62
+ entries.push({ group: 'Relationships', key: 'group', label: 'Group', color: theme.group.stroke, shape: 'group' });
55
63
  }
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' });
64
+ if (snapshot.edges.some((edge) => edge.kind === 'value')
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'));
58
68
  }
59
- if (scene.nodes.some((node) => node.kind === 'culled-pass')) {
69
+ if (snapshot.edges.some((edge) => edge.kind === 'ordering')) {
60
70
  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'),
71
+ ...edgeLegend('ordering', 'Order', theme.dependency.ordering, 'dotted'),
74
72
  hollowArrow: true,
75
73
  });
76
74
  }
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
75
  return entries;
84
76
  }
85
77
 
86
- export function createGraphStyles(): cytoscape.StylesheetJson {
87
- const theme = GRAPH_VISUAL_THEME;
78
+ export function createGraphStyles(theme: GraphVisualTheme = GRAPH_VISUAL_THEME): cytoscape.StylesheetJson {
88
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
+ },
89
89
  {
90
90
  selector: 'node',
91
91
  style: {
@@ -94,14 +94,16 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
94
94
  'height': 'data(height)',
95
95
  'label': 'data(displayLabel)',
96
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,
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,
100
101
  'min-zoomed-font-size': GRAPH_GEOMETRY.minimumZoomedFontSize,
101
102
  'color': theme.text,
102
103
  'background-color': theme.surfaceRaised,
103
104
  'border-color': theme.group.stroke,
104
- 'border-width': 1.25,
105
+ 'border-width': theme.nodeBorderWidth,
106
+ 'border-style': 'solid',
105
107
  'text-valign': 'center',
106
108
  'text-halign': 'center',
107
109
  'overlay-opacity': 0,
@@ -118,34 +120,28 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
118
120
  style: {
119
121
  'background-color': theme.external.fill,
120
122
  'border-color': theme.external.stroke,
121
- 'border-style': 'double',
122
- 'border-width': 3,
123
+ 'shape': 'cut-rectangle',
124
+ 'corner-radius': '10px',
123
125
  },
124
126
  },
125
127
  {
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"]',
128
+ selector: 'node[kind = "resource"]',
137
129
  style: {
138
130
  'shape': 'ellipse',
139
- 'background-color': theme.texture.fill,
140
- 'border-color': theme.texture.stroke,
131
+ 'background-color': theme.declaration.fill,
132
+ 'border-color': theme.declaration.stroke,
141
133
  },
142
134
  },
143
135
  {
144
- selector: 'node[kind = "resource"][resourceKind = "buffer"]',
136
+ selector: 'node[kind = "root"]',
145
137
  style: {
146
- 'shape': 'ellipse',
147
- 'background-color': theme.buffer.fill,
148
- 'border-color': theme.buffer.stroke,
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`,
140
+ 'background-color': theme.output.fill,
141
+ 'border-color': theme.output.stroke,
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,
149
145
  },
150
146
  },
151
147
  {
@@ -159,13 +155,13 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
159
155
  style: {
160
156
  'background-color': theme.group.fill,
161
157
  'border-color': theme.group.stroke,
162
- 'border-width': 2,
158
+ 'border-width': theme.groupBorderWidth,
163
159
  'color': theme.text,
164
160
  },
165
161
  },
166
162
  {
167
163
  selector: 'node[kind = "group"][collapsed = 0]',
168
- style: expandedGroupStyle(theme.group.fill),
164
+ style: expandedGroupStyle(theme.group.fill, theme),
169
165
  },
170
166
  {
171
167
  selector: 'node[kind = "group"][collapsed = 0][depthBand = 1]',
@@ -174,16 +170,16 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
174
170
  {
175
171
  selector: 'edge',
176
172
  style: {
177
- 'width': 1.5,
173
+ 'width': theme.edgeWidth,
178
174
  'line-color': theme.dependency.value,
179
- 'line-opacity': 0.82,
175
+ 'line-opacity': theme.edgeOpacity,
180
176
  'target-arrow-color': theme.dependency.value,
181
177
  'target-arrow-shape': 'triangle',
182
178
  'target-arrow-fill': 'filled',
183
- 'arrow-scale': 0.8,
179
+ 'arrow-scale': theme.arrowScale,
184
180
  'curve-style': 'straight',
185
181
  'label': 'data(displayLabel)',
186
- 'font-family': 'ui-monospace, SFMono-Regular, Consolas, Liberation Mono, monospace',
182
+ 'font-family': theme.fontFamily,
187
183
  'font-size': 10,
188
184
  'min-zoomed-font-size': GRAPH_GEOMETRY.minimumZoomedFontSize,
189
185
  'color': theme.text,
@@ -196,33 +192,15 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
196
192
  },
197
193
  },
198
194
  {
199
- selector: 'edge[dependencyKind = "ordering"]',
195
+ selector: 'edge[kind = "ordering"]',
200
196
  style: {
201
197
  'line-color': theme.dependency.ordering,
202
198
  'target-arrow-color': theme.dependency.ordering,
203
199
  'line-style': 'dotted',
204
200
  '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,
201
+ 'line-opacity': theme.orderingOpacity,
213
202
  },
214
203
  },
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
204
  {
227
205
  selector: 'edge.elk-route',
228
206
  style: {
@@ -244,7 +222,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
244
222
  selector: 'node.semantic-hover',
245
223
  style: {
246
224
  'border-color': theme.hover,
247
- 'border-width': 2,
225
+ 'border-width': theme.hoverWidth,
248
226
  'z-index': 15,
249
227
  },
250
228
  },
@@ -254,7 +232,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
254
232
  'line-color': theme.hover,
255
233
  'target-arrow-color': theme.hover,
256
234
  'line-opacity': 1,
257
- 'width': 2.5,
235
+ 'width': theme.hoverWidth,
258
236
  'z-index': 15,
259
237
  },
260
238
  },
@@ -262,10 +240,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
262
240
  selector: 'node.semantic-selected',
263
241
  style: {
264
242
  'border-color': theme.selected,
265
- 'border-width': 3,
266
- 'underlay-color': theme.selected,
267
- 'underlay-padding': 5,
268
- 'underlay-opacity': 0.18,
243
+ 'border-width': theme.selectedWidth,
269
244
  'opacity': 1,
270
245
  'z-index': 20,
271
246
  },
@@ -276,50 +251,108 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
276
251
  'line-color': theme.selected,
277
252
  'target-arrow-color': theme.selected,
278
253
  'line-opacity': 1,
279
- 'width': 3,
254
+ 'width': theme.selectedWidth,
280
255
  'z-index': 20,
281
256
  },
282
257
  },
283
258
  ];
284
259
  }
285
260
 
286
- export function nodeDimensions(node: GraphSceneNode): { readonly width: number; readonly height: number } {
261
+ function baseNodeDimensions(node: GraphSceneNode): { readonly width: number; readonly height: number } {
287
262
  switch (node.kind) {
288
263
  case 'pass':
289
264
  return { width: 184, height: node.label.includes('\n') ? 62 : 48 };
290
- case 'culled-pass':
291
- return { width: 184, height: 64 };
265
+ case 'root':
266
+ return { width: GRAPH_GEOMETRY.outputWidth, height: Math.max(58, node.label.split('\n').length * 17 + 24) };
292
267
  case 'resource':
293
- return { width: 166, height: node.label.includes('\n') ? 58 : 46 };
268
+ return { width: 184, height: Math.max(58, node.label.split('\n').length * 17 + 24) };
294
269
  case 'group':
295
- return node.collapsed ? { width: 224, height: 68 } : { width: 120, height: 80 };
270
+ return node.collapsed ? { width: 240, height: 80 } : { width: 120, height: 80 };
296
271
  }
297
272
  }
298
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
+
299
332
  export function graphLayoutGeometryKey(scene: GraphScene): string {
300
333
  return JSON.stringify({
301
334
  topology: scene.topologyKey,
302
335
  dimensions: scene.nodes.map((node) => {
303
336
  const dimensions = nodeDimensions(node);
304
- return [node.id, dimensions.width, dimensions.height];
337
+ return [node.id, dimensions.width, dimensions.height, node.kind === 'pass' && node.passKind === 'external-submission'];
305
338
  }),
306
339
  });
307
340
  }
308
341
 
309
- export function graphEdgeDisplayLabel(edge: GraphSceneEdge): string {
310
- return edge.kind === 'access' ? '' : edge.label ?? '';
342
+ export function graphEdgeDisplayLabel(_edge: GraphSceneEdge): string {
343
+ return '';
311
344
  }
312
345
 
313
346
  export function expandedGroupLabelMaxWidth(outerWidth: number): number {
314
347
  return Math.max(80, Math.floor(outerWidth - GRAPH_GEOMETRY.groupTitleInset * 2));
315
348
  }
316
349
 
317
- export function isOverviewGraphScale(zoom: number): boolean {
318
- return GRAPH_GEOMETRY.baseFontSize * zoom < GRAPH_GEOMETRY.overviewFontThreshold;
350
+ export function isOverviewGraphScale(zoom: number, theme: GraphVisualTheme = GRAPH_VISUAL_THEME): boolean {
351
+ return theme.fontSize * zoom < GRAPH_GEOMETRY.overviewFontThreshold;
319
352
  }
320
353
 
321
354
  function nodeLegend(key: string, label: string, color: string): GraphLegendEntry {
322
- return { key, label, color, shape: 'box' };
355
+ return { group: 'Execution', key, label, color, shape: 'box' };
323
356
  }
324
357
 
325
358
  function edgeLegend(
@@ -328,7 +361,7 @@ function edgeLegend(
328
361
  color: string,
329
362
  lineStyle: NonNullable<GraphLegendEntry['lineStyle']>,
330
363
  ): GraphLegendEntry {
331
- return { key, label, color, shape: 'line', lineStyle };
364
+ return { group: 'Relationships', key, label, color, shape: 'line', lineStyle };
332
365
  }
333
366
 
334
367
  function passStyle(
@@ -341,15 +374,16 @@ function passStyle(
341
374
  };
342
375
  }
343
376
 
344
- function expandedGroupStyle(backgroundColor: string): cytoscape.Css.Node {
377
+ function expandedGroupStyle(backgroundColor: string, theme: GraphVisualTheme): cytoscape.Css.Node {
345
378
  const geometry = GRAPH_GEOMETRY;
346
379
  return {
347
380
  'background-color': backgroundColor,
348
- 'background-opacity': 0.34,
349
- 'border-color': GRAPH_VISUAL_THEME.group.stroke,
350
- 'border-width': 1.5,
381
+ 'background-opacity': theme.groupOpacity,
382
+ 'border-color': theme.group.stroke,
383
+ 'border-width': theme.groupBorderWidth,
351
384
  'padding': `${geometry.groupPadding}px`,
352
- 'compound-sizing-wrt-labels': 'include',
385
+ 'compound-sizing-wrt-labels': 'exclude',
386
+ 'text-wrap': 'ellipsis',
353
387
  'min-width': '120px',
354
388
  'min-height': '80px',
355
389
  'text-valign': 'top-inside',
@@ -358,7 +392,7 @@ function expandedGroupStyle(backgroundColor: string): cytoscape.Css.Node {
358
392
  'text-margin-x': geometry.groupTitleInset - geometry.groupPadding * 2,
359
393
  'text-margin-y': geometry.groupTitleInset - geometry.groupPadding * 2,
360
394
  'line-height': geometry.groupTitleLineHeight,
361
- 'color': GRAPH_VISUAL_THEME.text,
395
+ 'color': theme.text,
362
396
  'font-weight': 600,
363
397
  };
364
398
  }