@perspective-dev/viewer-charts 4.5.2 → 5.0.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/dist/cdn/perspective-viewer-charts.js +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/charts/candlestick/candlestick.d.ts +8 -10
- package/dist/esm/charts/chart-base.d.ts +58 -4
- package/dist/esm/charts/chart.d.ts +45 -9
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +3 -2
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +8 -1
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +3 -2
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +5 -2
- package/dist/esm/charts/series/series-build.d.ts +11 -0
- package/dist/esm/charts/series/series-interact.d.ts +10 -0
- package/dist/esm/charts/series/series-render.d.ts +17 -1
- package/dist/esm/charts/series/series.d.ts +53 -10
- package/dist/esm/data/view-reader.d.ts +10 -1
- package/dist/esm/event-detail.d.ts +7 -0
- package/dist/esm/layout/ticks.d.ts +5 -0
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/charts.d.ts +6 -3
- package/dist/esm/plugin/plugin.d.ts +31 -2
- package/dist/esm/render/scheduler.d.ts +11 -8
- package/dist/esm/transport/protocol.d.ts +31 -2
- package/dist/esm/transport/renderer-transport.d.ts +74 -3
- package/dist/esm/webgl/instanced-attrs.d.ts +7 -1
- package/dist/esm/worker/renderer.worker.d.ts +12 -0
- package/package.json +1 -1
- package/src/ts/charts/candlestick/candlestick-render.ts +4 -1
- package/src/ts/charts/candlestick/candlestick.ts +12 -17
- package/src/ts/charts/cartesian/cartesian-render.ts +80 -63
- package/src/ts/charts/chart-base.ts +85 -5
- package/src/ts/charts/chart.ts +47 -9
- package/src/ts/charts/common/category-axis-resolver.ts +5 -1
- package/src/ts/charts/common/tree-data.ts +61 -16
- package/src/ts/charts/heatmap/heatmap-render.ts +6 -2
- package/src/ts/charts/series/glyphs/draw-areas.ts +10 -1
- package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
- package/src/ts/charts/series/glyphs/draw-lines.ts +10 -1
- package/src/ts/charts/series/glyphs/draw-scatter.ts +56 -3
- package/src/ts/charts/series/series-build.ts +21 -3
- package/src/ts/charts/series/series-interact.ts +86 -13
- package/src/ts/charts/series/series-render.ts +660 -17
- package/src/ts/charts/series/series.ts +96 -26
- package/src/ts/charts/sunburst/sunburst-interact.ts +2 -3
- package/src/ts/charts/sunburst/sunburst-render.ts +3 -1
- package/src/ts/charts/treemap/treemap-interact.ts +2 -5
- package/src/ts/charts/treemap/treemap-render.ts +3 -1
- package/src/ts/charts/treemap/treemap.ts +6 -1
- package/src/ts/data/view-reader.ts +22 -2
- package/src/ts/event-detail.ts +8 -0
- package/src/ts/index.ts +13 -7
- package/src/ts/interaction/raw-event-forwarder.ts +7 -0
- package/src/ts/interaction/zoom-controller.ts +8 -0
- package/src/ts/layout/ticks.ts +9 -0
- package/src/ts/plugin/charts.ts +20 -4
- package/src/ts/plugin/plugin.ts +86 -16
- package/src/ts/render/scheduler.ts +39 -13
- package/src/ts/transport/protocol.ts +33 -0
- package/src/ts/transport/renderer-transport.ts +185 -9
- package/src/ts/webgl/instanced-attrs.ts +15 -1
- package/src/ts/worker/dispatch.ts +4 -1
- package/src/ts/worker/renderer.worker.ts +32 -3
package/src/ts/plugin/plugin.ts
CHANGED
|
@@ -154,7 +154,6 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
154
154
|
private _renderer: RendererTransport | null = null;
|
|
155
155
|
private _rendererPromise: Promise<RendererTransport> | null = null;
|
|
156
156
|
private _rawEventForwarder: RawEventForwarder | null = null;
|
|
157
|
-
private _generation = 0;
|
|
158
157
|
private _resetClickAbort: AbortController | null = null;
|
|
159
158
|
|
|
160
159
|
/**
|
|
@@ -285,8 +284,7 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
285
284
|
// is the dispose signal: if `this._rendererPromise` no longer
|
|
286
285
|
// points at *this* build when it resolves, the element was
|
|
287
286
|
// deleted (or a reconnect started a newer build) and we destroy
|
|
288
|
-
// the orphan instead of adopting it. Promise identity
|
|
289
|
-
// `_generation`, which every `draw`/`update` bumps — is the
|
|
287
|
+
// the orphan instead of adopting it. Promise identity is the
|
|
290
288
|
// right token: a rapid draw→update must NOT tear down the
|
|
291
289
|
// single in-flight build it shares via this memoized promise.
|
|
292
290
|
const p: Promise<RendererTransport> = this._buildRenderer(view).then(
|
|
@@ -353,11 +351,16 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
353
351
|
|
|
354
352
|
private async _buildRenderer(view: View): Promise<RendererTransport> {
|
|
355
353
|
const viewer = this.parentElement as HTMLPerspectiveViewerElement;
|
|
356
|
-
|
|
354
|
+
// This chart's own panel (its `slot`). Used to scope the client/table
|
|
355
|
+
// to THIS panel rather than the host's active/seed panel (the bare
|
|
356
|
+
// getClient()/getTable() resolve element-wide in a multi-panel viewer),
|
|
357
|
+
// and to tag the dispatched interaction events with their source panel.
|
|
358
|
+
const panel = this.getAttribute("slot") ?? undefined;
|
|
359
|
+
const client = await viewer.getClient({ panel });
|
|
357
360
|
const viewer_class = customElements.get("perspective-viewer");
|
|
358
361
|
const clientWasm = viewer_class.get_wasm_module();
|
|
359
362
|
const clientWorkerURL = viewer_class.get_worker_url();
|
|
360
|
-
const table = await viewer?.getTable?.();
|
|
363
|
+
const table = await viewer?.getTable?.({ panel });
|
|
361
364
|
const tableName: string | undefined = table
|
|
362
365
|
? await table.get_name()
|
|
363
366
|
: undefined;
|
|
@@ -370,6 +373,7 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
370
373
|
client,
|
|
371
374
|
view,
|
|
372
375
|
tableName,
|
|
376
|
+
panel,
|
|
373
377
|
clientWorkerURL,
|
|
374
378
|
clientWasm,
|
|
375
379
|
chartTag: this._chartType.tag,
|
|
@@ -553,8 +557,16 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
553
557
|
return this._drawImpl(view);
|
|
554
558
|
}
|
|
555
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Shared body of `draw` / `update`. No re-entrancy guard: the host
|
|
562
|
+
* serializes every rendering call (`draw`, `update`, `render`,
|
|
563
|
+
* `resize`) and `delete` on its per-renderer draw lock, so this
|
|
564
|
+
* method never overlaps itself or a teardown. The one exception —
|
|
565
|
+
* an external DOM disconnect `delete()`ing the element mid-draw —
|
|
566
|
+
* is absorbed by `RendererTransport`, whose post-`destroy()`
|
|
567
|
+
* requests settle immediately instead of pending forever.
|
|
568
|
+
*/
|
|
556
569
|
private async _drawImpl(view: View): Promise<void> {
|
|
557
|
-
const gen = ++this._generation;
|
|
558
570
|
let renderer: RendererTransport;
|
|
559
571
|
try {
|
|
560
572
|
renderer = await this._ensureRenderer(view);
|
|
@@ -564,19 +576,17 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
564
576
|
return;
|
|
565
577
|
}
|
|
566
578
|
|
|
567
|
-
if (this._generation !== gen) {
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
579
|
renderer.setView(view);
|
|
572
580
|
renderer.setBufferMaxCapacity(this._chartType.max_cells);
|
|
581
|
+
const panel = this.getAttribute("slot") ?? undefined;
|
|
573
582
|
const viewer = this
|
|
574
583
|
.parentElement as HTMLPerspectiveViewerElement | null;
|
|
575
|
-
|
|
576
|
-
if (
|
|
584
|
+
|
|
585
|
+
if (viewer === null) {
|
|
577
586
|
return;
|
|
578
587
|
}
|
|
579
588
|
|
|
589
|
+
const viewerConfig = await viewer.getViewConfig({ panel });
|
|
580
590
|
await renderer.loadAndRender({
|
|
581
591
|
viewerConfig: {
|
|
582
592
|
group_by: viewerConfig?.group_by ?? [],
|
|
@@ -588,17 +598,78 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
588
598
|
}
|
|
589
599
|
|
|
590
600
|
async clear(): Promise<void> {
|
|
591
|
-
this._generation++;
|
|
592
601
|
this._renderer?.clear();
|
|
593
602
|
}
|
|
594
603
|
|
|
595
604
|
async resize(): Promise<void> {
|
|
596
|
-
|
|
605
|
+
// Hidden (an unslotted tab-stack panel, a `display: none` host):
|
|
606
|
+
// the 0×0 rect a hidden element reports would resize the worker
|
|
607
|
+
// canvas to zero — CLEARING the retained frame, so the next
|
|
608
|
+
// activation repaints from blank (the two-stage tab-switch
|
|
609
|
+
// artifact). Skip instead and keep the last frame for an instant,
|
|
610
|
+
// single-blit reveal — mirroring the datagrid's guard. The host's
|
|
611
|
+
// activation nudge is also a `resize()`, so a hidden panel's nudge
|
|
612
|
+
// is free by the same check.
|
|
613
|
+
if (!this.isConnected || this.offsetParent == null) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// AWAITED to the resized frame's PRESENT (the worker's
|
|
618
|
+
// `resizeAck`) — the host's presize protocol style-overrides
|
|
619
|
+
// this element to its target box and holds the layout commit on
|
|
620
|
+
// this promise, so resolving at message-post would commit the
|
|
621
|
+
// settings-pane layout against the old-dimensions bitmap (the
|
|
622
|
+
// aspect-ratio warp the datagrid's synchronous resize never
|
|
623
|
+
// shows).
|
|
624
|
+
await this._renderer?.resize();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* OPTIONAL host presize protocol: render at the TARGET element box
|
|
629
|
+
* `(width, height)` — the box the host's pending layout commit will
|
|
630
|
+
* produce — holding the resulting frame offscreen, so nothing on
|
|
631
|
+
* screen changes during the round-trip. Resolves, once the resized
|
|
632
|
+
* frame is staged, to a present closure; the host calls it in the
|
|
633
|
+
* same task as the layout commit, landing geometry and pixels in
|
|
634
|
+
* one paint. Plugins without this method get the host's held
|
|
635
|
+
* style-override presize path instead.
|
|
636
|
+
*/
|
|
637
|
+
async presize(width: number, height: number): Promise<(() => void) | void> {
|
|
638
|
+
if (
|
|
639
|
+
!this.isConnected ||
|
|
640
|
+
this.offsetParent == null ||
|
|
641
|
+
!this._renderer ||
|
|
642
|
+
!this._glCanvas
|
|
643
|
+
) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Target GL-canvas box = its current box shifted by the element's
|
|
648
|
+
// box delta — the chrome between the element edge and the canvas
|
|
649
|
+
// is constant across a resize.
|
|
650
|
+
const hostRect = this.getBoundingClientRect();
|
|
651
|
+
const glRect = this._glCanvas.getBoundingClientRect();
|
|
652
|
+
return await this._renderer.presize(
|
|
653
|
+
Math.max(0, glRect.width + (width - hostRect.width)),
|
|
654
|
+
Math.max(0, glRect.height + (height - hostRect.height)),
|
|
655
|
+
);
|
|
597
656
|
}
|
|
598
657
|
|
|
599
658
|
restyle() {
|
|
600
659
|
this._renderer?.invalidateTheme();
|
|
601
|
-
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Clear any active selection state (pinned tooltip) WITHOUT emitting
|
|
664
|
+
* selection events — the host calls this when a global filter
|
|
665
|
+
* contributed by this panel's selection is removed from the global
|
|
666
|
+
* filter bar, so the pin can't outlive the filter it produced. The
|
|
667
|
+
* transport's `_lastInsertConfig` (remove-set memory) is deliberately
|
|
668
|
+
* RETAINED so a subsequent selection still replaces any leftover
|
|
669
|
+
* clauses rather than accumulating.
|
|
670
|
+
*/
|
|
671
|
+
async deselect(): Promise<void> {
|
|
672
|
+
this._renderer?.deselect();
|
|
602
673
|
}
|
|
603
674
|
|
|
604
675
|
async render(view: View): Promise<Blob> {
|
|
@@ -627,7 +698,6 @@ export class HTMLPerspectiveViewerWebGLPluginElement
|
|
|
627
698
|
}
|
|
628
699
|
|
|
629
700
|
delete() {
|
|
630
|
-
this._generation++;
|
|
631
701
|
if (this._rawEventForwarder) {
|
|
632
702
|
this._rawEventForwarder.detach();
|
|
633
703
|
this._rawEventForwarder = null;
|
|
@@ -16,8 +16,8 @@ import type { WebGLContextManager } from "../webgl/context-manager";
|
|
|
16
16
|
* Module-level render scheduler. The single entry point for driving a
|
|
17
17
|
* chart frame. Every render-triggering caller — upload chunks, zoom /
|
|
18
18
|
* pan, resize, theme invalidation, host-driven redraws — calls
|
|
19
|
-
* `requestRender(glManager, fullRender)` and awaits the
|
|
20
|
-
* promise.
|
|
19
|
+
* `requestRender(glManager, fullRender, render2D)` and awaits the
|
|
20
|
+
* returned promise.
|
|
21
21
|
*
|
|
22
22
|
* ## Guarantees
|
|
23
23
|
*
|
|
@@ -51,9 +51,12 @@ import type { WebGLContextManager } from "../webgl/context-manager";
|
|
|
51
51
|
* begins, letting per-context GPU work overlap.
|
|
52
52
|
*
|
|
53
53
|
* - **Phase 2 (parallel):** `Promise.all(snapshot.map(present))`
|
|
54
|
-
* where `present` does `await awaitGpuFence();
|
|
55
|
-
* resolve waiters`.
|
|
56
|
-
*
|
|
54
|
+
* where `present` does `await awaitGpuFence(); render2D();
|
|
55
|
+
* endFrame(); resolve waiters`. The `render2D()` step draws the 2D
|
|
56
|
+
* layers (gridlines + chrome) only after the GPU fence, so the 2D
|
|
57
|
+
* placeholder canvases don't present a frame ahead of the GL frame
|
|
58
|
+
* on resize. Each entry's waiters resolve as soon as its own
|
|
59
|
+
* present completes — independent of other entries.
|
|
57
60
|
*
|
|
58
61
|
* ## Failure modes
|
|
59
62
|
*
|
|
@@ -78,6 +81,15 @@ import type { WebGLContextManager } from "../webgl/context-manager";
|
|
|
78
81
|
interface Entry {
|
|
79
82
|
glManager: WebGLContextManager;
|
|
80
83
|
fullRender: () => void;
|
|
84
|
+
/**
|
|
85
|
+
* The chart's 2D-canvas draws (gridlines + chrome), split out of
|
|
86
|
+
* `fullRender` so they run *after* `awaitGpuFence` — see
|
|
87
|
+
* {@link present}. Deferring the 2D work past the GPU fence keeps
|
|
88
|
+
* the 2D placeholder canvases from pushing a resized frame to the
|
|
89
|
+
* compositor a frame ahead of the GL present (the GL/2D "distortion"
|
|
90
|
+
* on resize). Populated as captured closures during `fullRender`.
|
|
91
|
+
*/
|
|
92
|
+
render2D: () => void;
|
|
81
93
|
waiters: PromiseWithResolvers<void>[];
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -105,25 +117,30 @@ const inFlight = new Set<WebGLContextManager>();
|
|
|
105
117
|
const deferred = new Map<WebGLContextManager, (() => void)[]>();
|
|
106
118
|
|
|
107
119
|
/**
|
|
108
|
-
* Request a coalesced render of `glManager`
|
|
109
|
-
*
|
|
110
|
-
* Phase 2
|
|
120
|
+
* Request a coalesced render of `glManager`. `fullRender` submits the
|
|
121
|
+
* GL commands (Phase 1, synchronous); `render2D` draws the 2D layers
|
|
122
|
+
* (gridlines + chrome) and runs in Phase 2 *after* `awaitGpuFence`, so
|
|
123
|
+
* the 2D canvases don't present ahead of the GL frame. Returns a
|
|
124
|
+
* promise that resolves when this entry's Phase 2 (`awaitGpuFence` +
|
|
125
|
+
* `render2D` + `endFrame`) completes.
|
|
111
126
|
*
|
|
112
127
|
* If a request is already pending for the same glManager, the new
|
|
113
|
-
* call's `fullRender`
|
|
114
|
-
* wins; closures read chart state lazily so this is
|
|
115
|
-
* no-op, but keeps the
|
|
116
|
-
* resolves alongside the existing waiters.
|
|
128
|
+
* call's `fullRender` / `render2D` closures replace the prior ones
|
|
129
|
+
* (latest call wins; closures read chart state lazily so this is
|
|
130
|
+
* functionally a no-op, but keeps the closures fresh) and the returned
|
|
131
|
+
* promise resolves alongside the existing waiters.
|
|
117
132
|
*/
|
|
118
133
|
export function requestRender(
|
|
119
134
|
glManager: WebGLContextManager,
|
|
120
135
|
fullRender: () => void,
|
|
136
|
+
render2D: () => void,
|
|
121
137
|
): Promise<void> {
|
|
122
138
|
let entry = pending.get(glManager);
|
|
123
139
|
if (entry) {
|
|
124
140
|
entry.fullRender = fullRender;
|
|
141
|
+
entry.render2D = render2D;
|
|
125
142
|
} else {
|
|
126
|
-
entry = { glManager, fullRender, waiters: [] };
|
|
143
|
+
entry = { glManager, fullRender, render2D, waiters: [] };
|
|
127
144
|
pending.set(glManager, entry);
|
|
128
145
|
}
|
|
129
146
|
|
|
@@ -354,6 +371,15 @@ async function present(entry: Entry): Promise<void> {
|
|
|
354
371
|
|
|
355
372
|
try {
|
|
356
373
|
await entry.glManager.awaitGpuFence();
|
|
374
|
+
// Draw the 2D layers (gridlines + chrome) only now that the GL
|
|
375
|
+
// frame's GPU work has completed. Issuing the 2D-canvas commands
|
|
376
|
+
// here — rather than inline in `fullRender` (Phase 1) — keeps the
|
|
377
|
+
// 2D placeholder canvases from pushing their resized frame to the
|
|
378
|
+
// compositor during the fence-wait yields, a frame ahead of the
|
|
379
|
+
// GL present. That early push is the visible GL/2D misalignment
|
|
380
|
+
// on resize. Failures fall through to the same present-failed
|
|
381
|
+
// path as the fence.
|
|
382
|
+
entry.render2D();
|
|
357
383
|
entry.glManager.endFrame();
|
|
358
384
|
for (const w of entry.waiters) {
|
|
359
385
|
w.resolve();
|
|
@@ -31,6 +31,7 @@ export type ControlMsg =
|
|
|
31
31
|
| SetBufferMaxCapacityMsg
|
|
32
32
|
| LoadAndRenderMsg
|
|
33
33
|
| RedrawMsg
|
|
34
|
+
| DeselectMsg
|
|
34
35
|
| ResizeMsg
|
|
35
36
|
| ClearMsg
|
|
36
37
|
| InvalidateThemeMsg
|
|
@@ -53,6 +54,7 @@ export type WorkerMsg =
|
|
|
53
54
|
| UserClickMsg
|
|
54
55
|
| UserSelectMsg
|
|
55
56
|
| LoadAndRenderAckMsg
|
|
57
|
+
| ResizeAckMsg
|
|
56
58
|
| FrameBitmapMsg
|
|
57
59
|
| ErrorMsg;
|
|
58
60
|
|
|
@@ -300,13 +302,44 @@ export interface RedrawMsg {
|
|
|
300
302
|
kind: "redraw";
|
|
301
303
|
}
|
|
302
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Host → worker: silently clear the chart's selection state (pinned
|
|
307
|
+
* tooltip) — no `userSelect` reply, so the host filter-bar removal that
|
|
308
|
+
* triggered it can't echo back into the host's filter set. The worker's
|
|
309
|
+
* `TooltipController.dismiss()` posts `dismissTooltip` to tear down the
|
|
310
|
+
* host-side pinned artifact.
|
|
311
|
+
*/
|
|
312
|
+
export interface DeselectMsg {
|
|
313
|
+
kind: "deselect";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Host → worker: resize the chart to a new CSS box. Replied with
|
|
318
|
+
* {@link ResizeAckMsg} AFTER the resized frame PRESENTS — the host's
|
|
319
|
+
* `plugin.resize()` promise is the viewer's presize contract ("the
|
|
320
|
+
* plugin has repainted at the target box when this resolves"), so an
|
|
321
|
+
* ack at message-receipt would let the settings-pane layout commit
|
|
322
|
+
* against the old-dimensions bitmap (the aspect-ratio warp).
|
|
323
|
+
*/
|
|
303
324
|
export interface ResizeMsg {
|
|
304
325
|
kind: "resize";
|
|
326
|
+
msgId: number;
|
|
305
327
|
cssWidth: number;
|
|
306
328
|
cssHeight: number;
|
|
307
329
|
dpr: number;
|
|
308
330
|
}
|
|
309
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Worker → host reply to a `ResizeMsg`, posted after the resized
|
|
334
|
+
* frame's present completes. Always sent — present failures and
|
|
335
|
+
* torn-down charts included — so the host's awaited promise resolves
|
|
336
|
+
* deterministically.
|
|
337
|
+
*/
|
|
338
|
+
export interface ResizeAckMsg {
|
|
339
|
+
kind: "resizeAck";
|
|
340
|
+
msgId: number;
|
|
341
|
+
}
|
|
342
|
+
|
|
310
343
|
export interface ClearMsg {
|
|
311
344
|
kind: "clear";
|
|
312
345
|
}
|
|
@@ -84,7 +84,11 @@ interface RendererHandle {
|
|
|
84
84
|
terminate(): void;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
type PendingRenderType =
|
|
87
|
+
type PendingRenderType =
|
|
88
|
+
| "saveZoom"
|
|
89
|
+
| "loadAndRender"
|
|
90
|
+
| "snapshotPng"
|
|
91
|
+
| "resize";
|
|
88
92
|
interface PendingRenderRequest {
|
|
89
93
|
kind: PendingRenderType;
|
|
90
94
|
resolve: (v: any) => void;
|
|
@@ -110,11 +114,22 @@ interface PendingRenderRequest {
|
|
|
110
114
|
*/
|
|
111
115
|
export class RendererTransport {
|
|
112
116
|
private _handle: RendererHandle | null = null;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Set by {@link destroy}. Requests allocated after teardown settle
|
|
120
|
+
* immediately in `_allocPending` — `_post` is already a no-op once
|
|
121
|
+
* `_handle` is null, so a late request would otherwise never
|
|
122
|
+
* receive its reply and pend forever.
|
|
123
|
+
*/
|
|
124
|
+
private _destroyed = false;
|
|
113
125
|
private _proxyChannel: MessageChannel | null = null;
|
|
114
126
|
private _proxySession: any = null;
|
|
115
127
|
private _client: Client;
|
|
116
128
|
private _view: View;
|
|
117
129
|
private _tableName: string | undefined;
|
|
130
|
+
/** Source panel `slot`, tagged onto dispatched interaction events so the
|
|
131
|
+
* host can route them (master/detail, global filter) to the right panel. */
|
|
132
|
+
private _panel: string | undefined;
|
|
118
133
|
private _clientWorkerURL: URL;
|
|
119
134
|
private _clientWasm: WebAssembly.Module;
|
|
120
135
|
private _chartTag: string;
|
|
@@ -158,6 +173,37 @@ export class RendererTransport {
|
|
|
158
173
|
*/
|
|
159
174
|
private _displayCtx: CanvasRenderingContext2D | null = null;
|
|
160
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Present-hold state for the staged-present presize protocol
|
|
178
|
+
* ({@link presize}). While holding, inbound `frameBitmap`s are
|
|
179
|
+
* STAGED (latest wins) instead of blitted, so the on-screen frame
|
|
180
|
+
* — old dimensions in the old, unchanged box — stays put until the
|
|
181
|
+
* host invokes the present closure `presize` resolved to, in the
|
|
182
|
+
* same task as its layout commit. Entered only by `presize`;
|
|
183
|
+
* exited by the present closure, by `presize`'s rejection path, or
|
|
184
|
+
* by `destroy` — every settle path releases the hold or hands the
|
|
185
|
+
* caller the release.
|
|
186
|
+
*/
|
|
187
|
+
/**
|
|
188
|
+
* The last `(cssWidth, cssHeight, dpr)` posted to the worker — at init
|
|
189
|
+
* or any resize — i.e. the dimensions the worker has already rendered
|
|
190
|
+
* (or has an in-flight render for). {@link resize} short-circuits when
|
|
191
|
+
* its measurement matches (±0.5px, the presize protocol's own
|
|
192
|
+
* threshold): reactive resizes fan in from several host paths per
|
|
193
|
+
* layout transition (the post-commit fan-out, activation nudges, the
|
|
194
|
+
* fresh-restore tail), and every worker resize is an unconditional
|
|
195
|
+
* full re-render.
|
|
196
|
+
*/
|
|
197
|
+
private _lastPostedSize: {
|
|
198
|
+
cssWidth: number;
|
|
199
|
+
cssHeight: number;
|
|
200
|
+
dpr: number;
|
|
201
|
+
} | null = null;
|
|
202
|
+
|
|
203
|
+
private _holdPresent = false;
|
|
204
|
+
|
|
205
|
+
private _stagedFrame: ImageBitmap | null = null;
|
|
206
|
+
|
|
161
207
|
/**
|
|
162
208
|
* Host-side sink for tooltip + cursor side-effects. The chart
|
|
163
209
|
* inside the renderer calls into a `MessageHostSink` that posts
|
|
@@ -182,6 +228,7 @@ export class RendererTransport {
|
|
|
182
228
|
client: Client;
|
|
183
229
|
view: View;
|
|
184
230
|
tableName?: string;
|
|
231
|
+
panel?: string;
|
|
185
232
|
clientWasm: WebAssembly.Module;
|
|
186
233
|
clientWorkerURL: URL;
|
|
187
234
|
chartTag: string;
|
|
@@ -192,6 +239,7 @@ export class RendererTransport {
|
|
|
192
239
|
this._client = opts.client;
|
|
193
240
|
this._view = opts.view;
|
|
194
241
|
this._tableName = opts.tableName;
|
|
242
|
+
this._panel = opts.panel;
|
|
195
243
|
this._clientWorkerURL = opts.clientWorkerURL;
|
|
196
244
|
this._clientWasm = opts.clientWasm;
|
|
197
245
|
this._chartTag = opts.chartTag;
|
|
@@ -301,6 +349,11 @@ export class RendererTransport {
|
|
|
301
349
|
precompileShaders: this._precompileShaders,
|
|
302
350
|
};
|
|
303
351
|
|
|
352
|
+
this._lastPostedSize = {
|
|
353
|
+
cssWidth: rect.width,
|
|
354
|
+
cssHeight: rect.height,
|
|
355
|
+
dpr,
|
|
356
|
+
};
|
|
304
357
|
this._handle = await this._createHandle(workerURL, initMsg);
|
|
305
358
|
this._handle.addMessageListener((msg) =>
|
|
306
359
|
this._handleRendererMsg(msg as WorkerMsg),
|
|
@@ -438,7 +491,8 @@ export class RendererTransport {
|
|
|
438
491
|
* `loadAndRenderAck`. Per the worker's "resolve on stale"
|
|
439
492
|
* contract, a mid-flight cancellation (a newer `loadAndRender`
|
|
440
493
|
* superseding this one) still acks — the host's awaiter just
|
|
441
|
-
* resolves quietly.
|
|
494
|
+
* resolves quietly. Calls issued after `destroy()` resolve
|
|
495
|
+
* immediately (see `_allocPending`).
|
|
442
496
|
*/
|
|
443
497
|
loadAndRender(opts: {
|
|
444
498
|
viewerConfig: {
|
|
@@ -464,19 +518,96 @@ export class RendererTransport {
|
|
|
464
518
|
this._post({ kind: "redraw" });
|
|
465
519
|
}
|
|
466
520
|
|
|
467
|
-
|
|
521
|
+
/**
|
|
522
|
+
* Silently clear the worker chart's selection state (pinned tooltip) —
|
|
523
|
+
* no selection events are emitted; the worker echoes `dismissTooltip`
|
|
524
|
+
* to drop the host-side pinned artifact. See `DeselectMsg`.
|
|
525
|
+
*/
|
|
526
|
+
deselect(): void {
|
|
527
|
+
this._post({ kind: "deselect" });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Resize the chart to the GL canvas's CURRENT CSS box and resolve
|
|
532
|
+
* once the resized frame has PRESENTED (the worker's `resizeAck`) —
|
|
533
|
+
* not at message-post, which would let callers proceed while the
|
|
534
|
+
* old-dimensions bitmap is still on screen. Reactive geometry
|
|
535
|
+
* changes only; anticipated ones go through {@link presize}.
|
|
536
|
+
*
|
|
537
|
+
* A measurement matching {@link _lastPostedSize} resolves immediately
|
|
538
|
+
* WITHOUT a worker round trip: the worker has already rendered (or is
|
|
539
|
+
* presenting) that exact frame, and re-posting would be a full
|
|
540
|
+
* re-render of an identical bitmap.
|
|
541
|
+
*/
|
|
542
|
+
resize(): Promise<void> {
|
|
468
543
|
if (!this._hostGlCanvas) {
|
|
469
|
-
return;
|
|
544
|
+
return Promise.resolve();
|
|
470
545
|
}
|
|
471
546
|
|
|
472
547
|
const rect = this._hostGlCanvas.getBoundingClientRect();
|
|
473
548
|
const dpr = window.devicePixelRatio || 1;
|
|
549
|
+
const last = this._lastPostedSize;
|
|
550
|
+
if (
|
|
551
|
+
last &&
|
|
552
|
+
Math.abs(last.cssWidth - rect.width) <= 0.5 &&
|
|
553
|
+
Math.abs(last.cssHeight - rect.height) <= 0.5 &&
|
|
554
|
+
last.dpr === dpr
|
|
555
|
+
) {
|
|
556
|
+
return Promise.resolve();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return this._postResize(rect.width, rect.height);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Staged-present variant of {@link resize}: render at the given
|
|
564
|
+
* TARGET canvas CSS box (no measurement — the caller supplies the
|
|
565
|
+
* box the pending layout commit will produce), HOLDING the
|
|
566
|
+
* resulting frame (and any others that arrive meanwhile) offscreen
|
|
567
|
+
* instead of blitting, so nothing on screen changes. Resolves,
|
|
568
|
+
* once the resized frame is staged, to a present closure: calling
|
|
569
|
+
* it blits the latest staged frame and exits the hold — the host
|
|
570
|
+
* runs it in the same task as its layout commit so geometry and
|
|
571
|
+
* pixels land in one paint. On rejection (teardown) the hold is
|
|
572
|
+
* released here, so every settle path either frees the display or
|
|
573
|
+
* hands the caller the release.
|
|
574
|
+
*/
|
|
575
|
+
presize(cssWidth: number, cssHeight: number): Promise<(() => void) | void> {
|
|
576
|
+
if (!this._hostGlCanvas) {
|
|
577
|
+
return Promise.resolve();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
this._holdPresent = true;
|
|
581
|
+
return this._postResize(cssWidth, cssHeight).then(
|
|
582
|
+
() => () => this._presentStaged(),
|
|
583
|
+
(err) => {
|
|
584
|
+
this._presentStaged();
|
|
585
|
+
throw err;
|
|
586
|
+
},
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private _postResize(cssWidth: number, cssHeight: number): Promise<void> {
|
|
591
|
+
const dpr = window.devicePixelRatio || 1;
|
|
592
|
+
const { id, promise } = this._allocPending<void>("resize");
|
|
593
|
+
this._lastPostedSize = { cssWidth, cssHeight, dpr };
|
|
474
594
|
this._post({
|
|
475
595
|
kind: "resize",
|
|
476
|
-
|
|
477
|
-
|
|
596
|
+
msgId: id,
|
|
597
|
+
cssWidth,
|
|
598
|
+
cssHeight,
|
|
478
599
|
dpr,
|
|
479
600
|
});
|
|
601
|
+
return promise;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private _presentStaged(): void {
|
|
605
|
+
this._holdPresent = false;
|
|
606
|
+
const staged = this._stagedFrame;
|
|
607
|
+
this._stagedFrame = null;
|
|
608
|
+
if (staged) {
|
|
609
|
+
this._drawFrameBitmap(staged);
|
|
610
|
+
}
|
|
480
611
|
}
|
|
481
612
|
|
|
482
613
|
clear() {
|
|
@@ -501,12 +632,39 @@ export class RendererTransport {
|
|
|
501
632
|
* Allocate a pending request slot of the given `kind`. Returns the
|
|
502
633
|
* id (encoded into the outgoing `ControlMsg`) and a promise that
|
|
503
634
|
* resolves / rejects when the matching reply arrives or
|
|
504
|
-
* `destroy()` drains the table.
|
|
635
|
+
* `destroy()` drains the table. Requests issued *after* `destroy()`
|
|
636
|
+
* settle immediately with the same per-kind semantics as the drain.
|
|
505
637
|
*/
|
|
506
638
|
private _allocPending<T>(kind: PendingRenderType): {
|
|
507
639
|
id: number;
|
|
508
640
|
promise: Promise<T>;
|
|
509
641
|
} {
|
|
642
|
+
// A request issued after `destroy()` would never settle: `_post`
|
|
643
|
+
// no-ops with no handle, so no reply ever lands — and the host
|
|
644
|
+
// awaits `loadAndRender` while holding its per-renderer draw
|
|
645
|
+
// lock, so a pending-forever promise wedges that panel's lock
|
|
646
|
+
// permanently. (Reachable when an external DOM disconnect
|
|
647
|
+
// `delete()`s the element mid-draw; the host's own teardown
|
|
648
|
+
// paths are serialized behind the same lock.) Mirror the
|
|
649
|
+
// `destroy()` drain: `loadAndRender` resolves silently,
|
|
650
|
+
// reply-bearing kinds reject. The pre-attached no-op `catch`
|
|
651
|
+
// marks the rejection handled for fire-and-forget callers
|
|
652
|
+
// (`saveZoom`) without affecting real awaiters.
|
|
653
|
+
if (this._destroyed) {
|
|
654
|
+
if (kind === "loadAndRender") {
|
|
655
|
+
return {
|
|
656
|
+
id: -1,
|
|
657
|
+
promise: Promise.resolve(undefined as unknown as T),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const promise = Promise.reject<T>(
|
|
662
|
+
new Error("RendererTransport destroyed"),
|
|
663
|
+
);
|
|
664
|
+
promise.catch(() => {});
|
|
665
|
+
return { id: -1, promise };
|
|
666
|
+
}
|
|
667
|
+
|
|
510
668
|
const id = ++this._pendingCounter;
|
|
511
669
|
const promise = new Promise<T>((resolve, reject) => {
|
|
512
670
|
this._pending.set(id, { kind, resolve, reject });
|
|
@@ -548,6 +706,7 @@ export class RendererTransport {
|
|
|
548
706
|
}
|
|
549
707
|
|
|
550
708
|
destroy(): void {
|
|
709
|
+
this._destroyed = true;
|
|
551
710
|
this._post({ kind: "destroy" });
|
|
552
711
|
if (this._proxySession) {
|
|
553
712
|
this._proxySession.close().catch(() => {});
|
|
@@ -572,6 +731,9 @@ export class RendererTransport {
|
|
|
572
731
|
// them, and so the GPU-backed 2D context can release earlier.
|
|
573
732
|
this._hostGlCanvas = null;
|
|
574
733
|
this._displayCtx = null;
|
|
734
|
+
this._holdPresent = false;
|
|
735
|
+
this._stagedFrame?.close();
|
|
736
|
+
this._stagedFrame = null;
|
|
575
737
|
|
|
576
738
|
// Drain pending request promises with kind-aware semantics:
|
|
577
739
|
// - `loadAndRender` resolves silently (the host's awaited draw
|
|
@@ -633,7 +795,9 @@ export class RendererTransport {
|
|
|
633
795
|
{
|
|
634
796
|
bubbles: true,
|
|
635
797
|
composed: true,
|
|
636
|
-
|
|
798
|
+
// Tag with the source panel so a multi-panel host
|
|
799
|
+
// can attribute the click to the right panel.
|
|
800
|
+
detail: { ...msg.detail, panel: this._panel },
|
|
637
801
|
},
|
|
638
802
|
),
|
|
639
803
|
);
|
|
@@ -658,6 +822,9 @@ export class RendererTransport {
|
|
|
658
822
|
removeConfigs as any,
|
|
659
823
|
insertConfigs as any,
|
|
660
824
|
);
|
|
825
|
+
// Tag with the source panel so the host's master/detail +
|
|
826
|
+
// global-filter routing can identify the originating panel.
|
|
827
|
+
detail.panel = this._panel;
|
|
661
828
|
this._dispatchOnViewer(
|
|
662
829
|
new CustomEvent<PerspectiveSelectDetail>(
|
|
663
830
|
"perspective-global-filter",
|
|
@@ -672,7 +839,13 @@ export class RendererTransport {
|
|
|
672
839
|
}
|
|
673
840
|
|
|
674
841
|
case "frameBitmap":
|
|
675
|
-
this.
|
|
842
|
+
if (this._holdPresent) {
|
|
843
|
+
this._stagedFrame?.close();
|
|
844
|
+
this._stagedFrame = msg.bitmap;
|
|
845
|
+
} else {
|
|
846
|
+
this._drawFrameBitmap(msg.bitmap);
|
|
847
|
+
}
|
|
848
|
+
|
|
676
849
|
break;
|
|
677
850
|
case "error":
|
|
678
851
|
this._rejectReady(new Error(msg.message));
|
|
@@ -680,6 +853,9 @@ export class RendererTransport {
|
|
|
680
853
|
case "loadAndRenderAck":
|
|
681
854
|
this._resolvePending(msg.msgId, "loadAndRender", undefined);
|
|
682
855
|
break;
|
|
856
|
+
case "resizeAck":
|
|
857
|
+
this._resolvePending(msg.msgId, "resize", undefined);
|
|
858
|
+
break;
|
|
683
859
|
case "snapshotPngReply":
|
|
684
860
|
this._resolvePending(msg.requestId, "snapshotPng", msg.blob);
|
|
685
861
|
break;
|
|
@@ -108,6 +108,12 @@ export function createQuadCornerBuffer(gl: GL): WebGLBuffer {
|
|
|
108
108
|
* paint zero data). No-op return `true` when `attr` is negative
|
|
109
109
|
* (optimized-out attribute — drawing the rest is still valid).
|
|
110
110
|
*
|
|
111
|
+
* `firstInstance` byte-offsets the attribute pointer so the draw's
|
|
112
|
+
* instance 0 reads element `firstInstance` of the buffer — the WebGL
|
|
113
|
+
* substitute for a `baseInstance` draw parameter. Faceted renderers
|
|
114
|
+
* use it to draw a contiguous per-facet sub-range of a shared
|
|
115
|
+
* instance buffer.
|
|
116
|
+
*
|
|
111
117
|
* Render-path uses `peek`, never `getOrCreate`: the latter recreates
|
|
112
118
|
* with zero-initialized contents when `_totalCapacity` has grown past
|
|
113
119
|
* the current buffer, which would wipe the previous draw's vertex
|
|
@@ -120,6 +126,7 @@ export function bindInstancedFloatAttr(
|
|
|
120
126
|
attr: number,
|
|
121
127
|
name: string,
|
|
122
128
|
components: number,
|
|
129
|
+
firstInstance = 0,
|
|
123
130
|
): boolean {
|
|
124
131
|
if (attr < 0) {
|
|
125
132
|
return true;
|
|
@@ -133,7 +140,14 @@ export function bindInstancedFloatAttr(
|
|
|
133
140
|
const gl: GL = glManager.gl;
|
|
134
141
|
gl.bindBuffer(gl.ARRAY_BUFFER, buf.buffer);
|
|
135
142
|
gl.enableVertexAttribArray(attr);
|
|
136
|
-
gl.vertexAttribPointer(
|
|
143
|
+
gl.vertexAttribPointer(
|
|
144
|
+
attr,
|
|
145
|
+
components,
|
|
146
|
+
gl.FLOAT,
|
|
147
|
+
false,
|
|
148
|
+
0,
|
|
149
|
+
firstInstance * components * Float32Array.BYTES_PER_ELEMENT,
|
|
150
|
+
);
|
|
137
151
|
instancing.setDivisor(attr, 1);
|
|
138
152
|
return true;
|
|
139
153
|
}
|