@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,617 @@
1
+ import type cytoscape from 'cytoscape';
2
+ import type { ELK } from 'elkjs/lib/elk-api';
3
+
4
+ import type { GraphRenderer, GraphRenderRequest } from './panelGraphRenderer.ts';
5
+ import {
6
+ applyGraphLayout,
7
+ layoutGraphScene,
8
+ syncExpandedGroupLabelGeometry,
9
+ type GraphLayoutResult,
10
+ type GraphPosition,
11
+ } from './panelGraphLayout.ts';
12
+ import {
13
+ selectionKey,
14
+ type GraphScene,
15
+ type GraphSceneEdge,
16
+ type GraphSceneElementId,
17
+ type GraphSceneNode,
18
+ } from './panelGraphScene.ts';
19
+ import {
20
+ createGraphStyles,
21
+ expandedGroupLabelMaxWidth,
22
+ GRAPH_GEOMETRY,
23
+ graphEdgeDisplayLabel,
24
+ graphLayoutGeometryKey,
25
+ isOverviewGraphScale,
26
+ nodeDimensions,
27
+ } from './panelGraphVisuals.ts';
28
+
29
+ type Viewport = { readonly pan: GraphPosition; readonly zoom: number };
30
+
31
+ type GraphRendererRuntime = {
32
+ readonly createCore: (options: cytoscape.CytoscapeOptions) => cytoscape.Core;
33
+ readonly elk: ELK;
34
+ };
35
+
36
+ export type GraphResizeSubscription = { disconnect(): void };
37
+
38
+ export type GraphRendererEnvironment = {
39
+ readonly createElement: (tagName: string) => HTMLElement;
40
+ readonly loadRuntime: () => Promise<GraphRendererRuntime>;
41
+ readonly layoutScene: (elk: ELK, scene: GraphScene) => Promise<GraphLayoutResult>;
42
+ readonly observeResize?: (element: HTMLElement, onResize: () => void) => GraphResizeSubscription;
43
+ };
44
+
45
+ export class CytoscapeGraphRenderer implements GraphRenderer {
46
+ private readonly canvasHost: HTMLElement;
47
+ private readonly tooltip: HTMLElement;
48
+ private readonly status: HTMLElement;
49
+ private readonly statusMessage: HTMLElement;
50
+ private readonly retryButton: HTMLButtonElement;
51
+ private readonly resizeSubscription: GraphResizeSubscription | undefined;
52
+ private core: cytoscape.Core | undefined;
53
+ private elk: ELK | undefined;
54
+ private loading: Promise<void> | undefined;
55
+ private processing: Promise<void> | undefined;
56
+ private latestRequest: GraphRenderRequest | undefined;
57
+ private appliedScene: GraphScene | undefined;
58
+ private requestVersion = 0;
59
+ private processedVersion = 0;
60
+ private destroyed = false;
61
+ private fitTargetContentKey: string | undefined;
62
+ private forceRelayout = false;
63
+ private anchorElementId: GraphSceneElementId | undefined;
64
+ private anchorTargetContentKey: string | undefined;
65
+ private failedSceneContentKey: string | undefined;
66
+ private overview = false;
67
+ private lastTap: { readonly id: string; readonly at: number } | undefined;
68
+
69
+ constructor(
70
+ private readonly host: HTMLElement,
71
+ private readonly environment: GraphRendererEnvironment = browserGraphRendererEnvironment,
72
+ ) {
73
+ this.canvasHost = environment.createElement('div');
74
+ this.canvasHost.className = 'zenfg-inspector-graph-canvas';
75
+ this.tooltip = environment.createElement('div');
76
+ this.tooltip.className = 'zenfg-inspector-graph-tooltip';
77
+ this.tooltip.hidden = true;
78
+ this.status = environment.createElement('div');
79
+ this.status.className = 'zenfg-inspector-graph-status';
80
+ this.status.hidden = true;
81
+ this.status.setAttribute('role', 'status');
82
+ this.status.setAttribute('aria-live', 'polite');
83
+ this.statusMessage = environment.createElement('span');
84
+ this.retryButton = environment.createElement('button') as HTMLButtonElement;
85
+ this.retryButton.type = 'button';
86
+ this.retryButton.textContent = 'Retry';
87
+ this.retryButton.hidden = true;
88
+ this.retryButton.addEventListener('click', () => this.relayout());
89
+ this.status.append(this.statusMessage, this.retryButton);
90
+ this.host.replaceChildren(this.canvasHost, this.status, this.tooltip);
91
+ this.resizeSubscription = environment.observeResize?.(host, () => this.core?.resize());
92
+ }
93
+
94
+ render(request: GraphRenderRequest): void {
95
+ if (this.destroyed) return;
96
+ this.latestRequest = request;
97
+ if (request.fit) {
98
+ this.fitTargetContentKey = request.scene.contentKey;
99
+ } else if (this.fitTargetContentKey !== request.scene.contentKey) {
100
+ this.fitTargetContentKey = undefined;
101
+ }
102
+ if (request.anchorElementId !== undefined) {
103
+ this.anchorElementId = request.anchorElementId;
104
+ this.anchorTargetContentKey = request.scene.contentKey;
105
+ } else if (this.anchorTargetContentKey !== request.scene.contentKey) {
106
+ this.anchorElementId = undefined;
107
+ this.anchorTargetContentKey = undefined;
108
+ }
109
+ if (this.failedSceneContentKey && this.failedSceneContentKey !== request.scene.contentKey) {
110
+ this.failedSceneContentKey = undefined;
111
+ this.hideStatus();
112
+ }
113
+
114
+ const version = ++this.requestVersion;
115
+ if (this.failedSceneContentKey === request.scene.contentKey) {
116
+ this.syncInteraction(request);
117
+ this.processedVersion = version;
118
+ return;
119
+ }
120
+ if (!this.processing && this.core && this.appliedScene?.contentKey === request.scene.contentKey && !this.forceRelayout) {
121
+ this.appliedScene = request.scene;
122
+ this.syncInteraction(request);
123
+ this.consumeFitIfCurrentTarget();
124
+ this.consumeAnchorIfCurrentTarget();
125
+ this.processedVersion = version;
126
+ return;
127
+ }
128
+ this.scheduleProcessing();
129
+ }
130
+
131
+ resize(): void {
132
+ this.core?.resize();
133
+ }
134
+
135
+ fit(): void {
136
+ const targetContentKey = this.latestRequest?.scene.contentKey;
137
+ if (targetContentKey) this.fitTargetContentKey = targetContentKey;
138
+ if (!this.core || !this.appliedScene) return;
139
+ this.core.resize();
140
+ this.core.fit(undefined, GRAPH_GEOMETRY.fitPadding);
141
+ this.updateSemanticZoom();
142
+ if (!this.targetNeedsApply()) this.fitTargetContentKey = undefined;
143
+ }
144
+
145
+ relayout(): void {
146
+ if (!this.latestRequest || this.destroyed) return;
147
+ this.failedSceneContentKey = undefined;
148
+ this.forceRelayout = true;
149
+ this.fitTargetContentKey = this.latestRequest.scene.contentKey;
150
+ this.anchorElementId = undefined;
151
+ this.anchorTargetContentKey = undefined;
152
+ this.hideTooltip();
153
+ ++this.requestVersion;
154
+ this.scheduleProcessing();
155
+ }
156
+
157
+ destroy(): void {
158
+ if (this.destroyed) return;
159
+ this.destroyed = true;
160
+ this.requestVersion++;
161
+ this.resizeSubscription?.disconnect();
162
+ this.hideTooltip();
163
+ this.core?.destroy();
164
+ this.core = undefined;
165
+ this.elk = undefined;
166
+ this.loading = undefined;
167
+ this.latestRequest = undefined;
168
+ this.appliedScene = undefined;
169
+ this.fitTargetContentKey = undefined;
170
+ this.anchorElementId = undefined;
171
+ this.anchorTargetContentKey = undefined;
172
+ this.host.replaceChildren();
173
+ }
174
+
175
+ private scheduleProcessing(): void {
176
+ if (this.processing || this.destroyed) return;
177
+ this.processing = this.processRequests().finally(() => {
178
+ this.processing = undefined;
179
+ if (!this.destroyed
180
+ && !this.failedSceneContentKey
181
+ && this.processedVersion !== this.requestVersion) {
182
+ this.scheduleProcessing();
183
+ }
184
+ });
185
+ }
186
+
187
+ private async processRequests(): Promise<void> {
188
+ while (!this.destroyed && this.processedVersion !== this.requestVersion) {
189
+ let version = this.requestVersion;
190
+ let request = this.latestRequest;
191
+ if (!request) return;
192
+
193
+ if (request.scene.nodes.length === 0) {
194
+ this.hideTooltip();
195
+ if (this.core) replaceElements(this.core, request.scene);
196
+ this.appliedScene = request.scene;
197
+ this.forceRelayout = false;
198
+ if (this.fitTargetContentKey === request.scene.contentKey) this.fitTargetContentKey = undefined;
199
+ this.consumeAnchorIfCurrentTarget();
200
+ this.syncInteraction(request);
201
+ this.showStatus('empty', 'No graph elements in this view.');
202
+ this.processedVersion = version;
203
+ continue;
204
+ }
205
+
206
+ try {
207
+ if (!this.core) this.showStatus('loading', 'Loading graph runtime…');
208
+ await this.ensureRuntime();
209
+ const runtimeRequest = this.resolveCurrentRequest(version, request.scene.contentKey);
210
+ if (!runtimeRequest) continue;
211
+ ({ version, request } = runtimeRequest);
212
+ const core = this.core!;
213
+ if (this.appliedScene?.contentKey === request.scene.contentKey && !this.forceRelayout) {
214
+ this.appliedScene = request.scene;
215
+ this.syncInteraction(request);
216
+ this.consumeFitIfCurrentTarget();
217
+ this.consumeAnchorIfCurrentTarget();
218
+ this.hideStatus();
219
+ this.processedVersion = version;
220
+ continue;
221
+ }
222
+
223
+ this.hideTooltip();
224
+ const previousViewport = viewport(core);
225
+ const anchorElementId = this.anchorTargetContentKey === request.scene.contentKey
226
+ ? this.anchorElementId
227
+ : undefined;
228
+ const anchorBefore = anchorElementId === undefined
229
+ ? undefined
230
+ : renderedPosition(core, anchorElementId);
231
+ const topologyChanged = this.appliedScene?.topologyKey !== request.scene.topologyKey;
232
+ const geometryChanged = !this.appliedScene
233
+ || graphLayoutGeometryKey(this.appliedScene) !== graphLayoutGeometryKey(request.scene);
234
+ let laidOut = false;
235
+
236
+ if (geometryChanged || this.forceRelayout) {
237
+ this.showStatus('layout', 'Laying out graph…');
238
+ const result = await this.environment.layoutScene(this.elk!, request.scene);
239
+ const layoutRequest = this.resolveCurrentRequest(version, request.scene.contentKey);
240
+ if (!layoutRequest) continue;
241
+ ({ version, request } = layoutRequest);
242
+ if (topologyChanged || !this.appliedScene) replaceElements(core, request.scene);
243
+ else updateElementData(core, request.scene);
244
+ applyGraphLayout(core, request.scene, result);
245
+ laidOut = true;
246
+ } else {
247
+ updateElementData(core, request.scene);
248
+ }
249
+
250
+ syncExpandedGroupLabelGeometry(core);
251
+ this.appliedScene = request.scene;
252
+ this.forceRelayout = false;
253
+ this.failedSceneContentKey = undefined;
254
+ this.syncInteraction(request);
255
+ core.resize();
256
+ if (this.fitTargetContentKey === request.scene.contentKey) {
257
+ core.fit(undefined, GRAPH_GEOMETRY.fitPadding);
258
+ this.fitTargetContentKey = undefined;
259
+ } else if (laidOut && anchorBefore && anchorElementId) {
260
+ restoreViewport(core, previousViewport);
261
+ const anchorAfter = renderedPosition(core, anchorElementId);
262
+ if (anchorAfter) core.panBy({ x: anchorBefore.x - anchorAfter.x, y: anchorBefore.y - anchorAfter.y });
263
+ } else {
264
+ restoreViewport(core, previousViewport);
265
+ }
266
+ this.consumeAnchorIfCurrentTarget();
267
+ this.updateSemanticZoom(true);
268
+ this.hideStatus();
269
+ this.processedVersion = version;
270
+ } catch (error) {
271
+ const failedRequest = this.resolveCurrentRequest(version, request.scene.contentKey);
272
+ if (!failedRequest) continue;
273
+ ({ version, request } = failedRequest);
274
+ this.forceRelayout = false;
275
+ this.failedSceneContentKey = request.scene.contentKey;
276
+ this.processedVersion = version;
277
+ this.showStatus(
278
+ 'error',
279
+ `Failed to render graph: ${error instanceof Error ? error.message : String(error)}`,
280
+ true,
281
+ );
282
+ return;
283
+ }
284
+ }
285
+ }
286
+
287
+ private async ensureRuntime(): Promise<void> {
288
+ if (this.core || this.destroyed) return;
289
+ this.loading ??= this.loadRuntime().catch((error) => {
290
+ this.loading = undefined;
291
+ throw error;
292
+ });
293
+ await this.loading;
294
+ }
295
+
296
+ private async loadRuntime(): Promise<void> {
297
+ const runtime = await this.environment.loadRuntime();
298
+ if (this.destroyed) return;
299
+ this.canvasHost.textContent = '';
300
+ this.elk = runtime.elk;
301
+ this.core = runtime.createCore({
302
+ container: this.canvasHost,
303
+ elements: [],
304
+ style: createGraphStyles(),
305
+ layout: { name: 'preset' },
306
+ minZoom: 0.03,
307
+ maxZoom: 4,
308
+ boxSelectionEnabled: false,
309
+ autoungrabify: true,
310
+ autounselectify: true,
311
+ });
312
+ this.bindEvents(this.core);
313
+ }
314
+
315
+ private bindEvents(core: cytoscape.Core): void {
316
+ const selectElement = (event: cytoscape.EventObject) => {
317
+ const request = this.interactiveRequest();
318
+ if (!request) return;
319
+ const id = event.target.id() as GraphSceneElementId;
320
+ const selection = request.scene.interaction.selectionByElementId.get(id);
321
+ if (selection) request.onSelect(selection);
322
+ const now = performance.now();
323
+ if (this.lastTap?.id === id && now - this.lastTap.at <= 350 && selection?.kind === 'group') {
324
+ request.onToggleGroup(selection.pathKey);
325
+ this.lastTap = undefined;
326
+ } else {
327
+ this.lastTap = { id, at: now };
328
+ }
329
+ };
330
+ core.on('tap', 'node', selectElement);
331
+ core.on('tap', 'edge', selectElement);
332
+ core.on('tap', (event) => {
333
+ if (event.target === core) this.lastTap = undefined;
334
+ });
335
+ const hoverElement = (event: cytoscape.EventObject) => {
336
+ const request = this.interactiveRequest();
337
+ if (!request) return;
338
+ const id = event.target.id() as GraphSceneElementId;
339
+ const selection = request.scene.interaction.selectionByElementId.get(id);
340
+ if (selection) request.onHover(selection);
341
+ const title = event.target.data('tooltip') as string | undefined;
342
+ if (title) {
343
+ this.tooltip.textContent = title;
344
+ this.tooltip.hidden = false;
345
+ this.positionTooltip(event.renderedPosition);
346
+ }
347
+ };
348
+ core.on('mouseover', 'node', hoverElement);
349
+ core.on('mouseover', 'edge', hoverElement);
350
+ const moveTooltip = (event: cytoscape.EventObject) => this.positionTooltip(event.renderedPosition);
351
+ core.on('mousemove', 'node', moveTooltip);
352
+ core.on('mousemove', 'edge', moveTooltip);
353
+ const clearHover = () => {
354
+ this.hideTooltip();
355
+ this.latestRequest?.onHover(undefined);
356
+ };
357
+ core.on('mouseout', 'node', clearHover);
358
+ core.on('mouseout', 'edge', clearHover);
359
+ core.on('zoom', () => this.updateSemanticZoom());
360
+ }
361
+
362
+ private interactiveRequest(): GraphRenderRequest | undefined {
363
+ const request = this.latestRequest;
364
+ return request && this.appliedScene?.contentKey === request.scene.contentKey ? request : undefined;
365
+ }
366
+
367
+ private syncInteraction(request: GraphRenderRequest): void {
368
+ const core = this.core;
369
+ if (!core || this.appliedScene?.topologyKey !== request.scene.topologyKey) return;
370
+ core.batch(() => {
371
+ core.elements().removeClass('semantic-selected semantic-hover');
372
+ if (request.hovered) {
373
+ for (const id of request.scene.interaction.relatedElementIdsBySelection.get(selectionKey(request.hovered)) ?? []) {
374
+ core.getElementById(id).addClass('semantic-hover');
375
+ }
376
+ }
377
+ if (request.selected) {
378
+ for (const id of request.scene.interaction.primaryElementIdsBySelection.get(selectionKey(request.selected)) ?? []) {
379
+ core.getElementById(id).addClass('semantic-selected');
380
+ }
381
+ }
382
+ });
383
+ }
384
+
385
+ private consumeFitIfCurrentTarget(): void {
386
+ const targetContentKey = this.latestRequest?.scene.contentKey;
387
+ if (!targetContentKey
388
+ || this.fitTargetContentKey !== targetContentKey
389
+ || !this.core
390
+ || this.targetNeedsApply()) return;
391
+ this.core.resize();
392
+ this.core.fit(undefined, GRAPH_GEOMETRY.fitPadding);
393
+ this.fitTargetContentKey = undefined;
394
+ this.updateSemanticZoom();
395
+ }
396
+
397
+ private consumeAnchorIfCurrentTarget(): void {
398
+ const targetContentKey = this.latestRequest?.scene.contentKey;
399
+ if (!targetContentKey || this.anchorTargetContentKey !== targetContentKey) return;
400
+ this.anchorElementId = undefined;
401
+ this.anchorTargetContentKey = undefined;
402
+ }
403
+
404
+ private targetNeedsApply(): boolean {
405
+ const target = this.latestRequest?.scene;
406
+ if (!target || !this.appliedScene) return target !== undefined;
407
+ return this.forceRelayout || this.appliedScene.contentKey !== target.contentKey;
408
+ }
409
+
410
+ private updateSemanticZoom(force = false): void {
411
+ const core = this.core;
412
+ if (!core) return;
413
+ const overview = isOverviewGraphScale(core.zoom());
414
+ if (!force && overview === this.overview) return;
415
+ this.overview = overview;
416
+ core.batch(() => {
417
+ for (const node of core.nodes()) {
418
+ node.data('displayLabel', overview ? node.data('overviewLabel') : node.data('detailLabel'));
419
+ }
420
+ for (const edge of core.edges()) {
421
+ edge.data('displayLabel', overview ? '' : edge.data('detailLabel'));
422
+ }
423
+ });
424
+ }
425
+
426
+ private positionTooltip(position: GraphPosition): void {
427
+ const maxX = Math.max(8, this.host.clientWidth - this.tooltip.offsetWidth - 8);
428
+ const maxY = Math.max(8, this.host.clientHeight - this.tooltip.offsetHeight - 8);
429
+ this.tooltip.style.left = `${Math.max(8, Math.min(maxX, position.x + 12))}px`;
430
+ this.tooltip.style.top = `${Math.max(8, Math.min(maxY, position.y + 12))}px`;
431
+ }
432
+
433
+ private hideTooltip(): void {
434
+ this.tooltip.hidden = true;
435
+ this.tooltip.textContent = '';
436
+ this.lastTap = undefined;
437
+ }
438
+
439
+ private showStatus(kind: 'loading' | 'layout' | 'empty' | 'error', message: string, retry = false): void {
440
+ this.status.dataset.state = kind;
441
+ this.statusMessage.textContent = message;
442
+ this.retryButton.hidden = !retry;
443
+ this.status.hidden = false;
444
+ this.host.setAttribute('aria-busy', kind === 'loading' || kind === 'layout' ? 'true' : 'false');
445
+ }
446
+
447
+ private hideStatus(): void {
448
+ this.status.hidden = true;
449
+ this.retryButton.hidden = true;
450
+ this.host.setAttribute('aria-busy', 'false');
451
+ }
452
+
453
+ private resolveCurrentRequest(
454
+ version: number,
455
+ contentKey: string,
456
+ ): { readonly version: number; readonly request: GraphRenderRequest } | undefined {
457
+ if (this.destroyed) return undefined;
458
+ const request = this.latestRequest;
459
+ if (!request) return undefined;
460
+ if (version === this.requestVersion || request.scene.contentKey === contentKey) {
461
+ return { version: this.requestVersion, request };
462
+ }
463
+ return undefined;
464
+ }
465
+ }
466
+
467
+ const browserGraphRendererEnvironment: GraphRendererEnvironment = {
468
+ createElement: (tagName) => document.createElement(tagName),
469
+ loadRuntime: async () => {
470
+ const [cytoscapeModule, elkModule] = await Promise.all([
471
+ import('cytoscape'),
472
+ import('elkjs/lib/elk.bundled.js'),
473
+ ]);
474
+ return {
475
+ createCore: cytoscapeModule.default,
476
+ elk: new elkModule.default(),
477
+ };
478
+ },
479
+ layoutScene: layoutGraphScene,
480
+ observeResize: (element, onResize) => {
481
+ if (typeof ResizeObserver === 'undefined') return { disconnect: () => undefined };
482
+ const view = element.ownerDocument.defaultView;
483
+ let animationFrame: number | undefined;
484
+ const observer = new ResizeObserver(() => {
485
+ if (!view || animationFrame !== undefined) return;
486
+ animationFrame = view.requestAnimationFrame(() => {
487
+ animationFrame = undefined;
488
+ onResize();
489
+ });
490
+ });
491
+ observer.observe(element);
492
+ return {
493
+ disconnect: () => {
494
+ observer.disconnect();
495
+ if (animationFrame !== undefined) {
496
+ view?.cancelAnimationFrame(animationFrame);
497
+ animationFrame = undefined;
498
+ }
499
+ },
500
+ };
501
+ },
502
+ };
503
+
504
+ function replaceElements(core: cytoscape.Core, scene: GraphScene): void {
505
+ const definitions: cytoscape.ElementDefinition[] = [
506
+ ...scene.nodes.map(nodeDefinition),
507
+ ...scene.edges.map(edgeDefinition),
508
+ ];
509
+ core.startBatch();
510
+ core.elements().remove();
511
+ core.add(definitions);
512
+ core.endBatch();
513
+ }
514
+
515
+ export function updateElementData(core: cytoscape.Core, scene: GraphScene): void {
516
+ core.batch(() => {
517
+ for (const node of scene.nodes) {
518
+ const element = core.getElementById(node.id);
519
+ if (element.nonempty()) element.data(nodeRenderableData(node));
520
+ }
521
+ for (const edge of scene.edges) {
522
+ const element = core.getElementById(edge.id);
523
+ if (element.nonempty()) element.data(edgeRenderableData(edge));
524
+ }
525
+ });
526
+ }
527
+
528
+ function nodeDefinition(node: GraphSceneNode): cytoscape.ElementDefinition {
529
+ return {
530
+ group: 'nodes',
531
+ data: {
532
+ id: node.id,
533
+ parent: node.parentId,
534
+ ...nodeRenderableData(node),
535
+ },
536
+ };
537
+ }
538
+
539
+ function nodeRenderableData(node: GraphSceneNode): Record<string, unknown> {
540
+ const dimensions = nodeDimensions(node);
541
+ const labelMaxWidth = node.kind === 'group' && node.collapsed
542
+ ? expandedGroupLabelMaxWidth(dimensions.width)
543
+ : 160;
544
+ return {
545
+ kind: node.kind,
546
+ passKind: node.kind === 'pass' || node.kind === 'culled-pass' ? node.passKind : undefined,
547
+ resourceKind: node.kind === 'resource' ? node.resourceKind : undefined,
548
+ collapsed: node.kind === 'group' && node.collapsed ? 1 : 0,
549
+ hasCulled: node.kind === 'group' && node.culledNodeCount > 0 ? 1 : 0,
550
+ depthBand: node.kind === 'group' ? node.depthBand : undefined,
551
+ detailLabel: node.label,
552
+ overviewLabel: node.overviewLabel,
553
+ displayLabel: node.label,
554
+ tooltip: node.title,
555
+ labelMaxWidth,
556
+ width: dimensions.width,
557
+ height: dimensions.height,
558
+ };
559
+ }
560
+
561
+ function edgeDefinition(edge: GraphSceneEdge): cytoscape.ElementDefinition {
562
+ return {
563
+ group: 'edges',
564
+ data: {
565
+ id: edge.id,
566
+ source: edge.from,
567
+ target: edge.to,
568
+ ...edgeRenderableData(edge),
569
+ },
570
+ };
571
+ }
572
+
573
+ function edgeRenderableData(edge: GraphSceneEdge): Record<string, unknown> {
574
+ const detailLabel = graphEdgeDisplayLabel(edge);
575
+ return {
576
+ kind: edge.kind,
577
+ dependencyKind: edge.kind === 'dependency' ? edge.dependencyKind : undefined,
578
+ accessMode: edge.kind === 'access' ? edge.accessMode : undefined,
579
+ dashed: edge.kind === 'access' && edge.dashed ? 1 : 0,
580
+ detailLabel,
581
+ displayLabel: detailLabel,
582
+ tooltip: edge.title,
583
+ };
584
+ }
585
+
586
+ function viewport(core: cytoscape.Core): Viewport {
587
+ return { pan: { ...core.pan() }, zoom: core.zoom() };
588
+ }
589
+
590
+ function restoreViewport(core: cytoscape.Core, state: Viewport): void {
591
+ core.zoom(state.zoom);
592
+ core.pan(state.pan);
593
+ }
594
+
595
+ function renderedPosition(core: cytoscape.Core, id: GraphSceneElementId): GraphPosition | undefined {
596
+ const element = core.getElementById(id);
597
+ return element.nonempty() && element.isNode() ? { ...element.renderedPosition() } : undefined;
598
+ }
599
+
600
+ export {
601
+ createCytoscapeEdgeRouteData,
602
+ createElkLayoutGraph,
603
+ layoutGraphScene,
604
+ } from './panelGraphLayout.ts';
605
+ export type {
606
+ CytoscapeEdgeRouteData,
607
+ GraphEdgeRoute,
608
+ GraphLayoutResult,
609
+ GraphPosition,
610
+ } from './panelGraphLayout.ts';
611
+ export {
612
+ createGraphStyles,
613
+ expandedGroupLabelMaxWidth,
614
+ graphEdgeDisplayLabel,
615
+ graphLayoutGeometryKey,
616
+ isOverviewGraphScale,
617
+ } from './panelGraphVisuals.ts';