@perspective-dev/viewer-charts 4.3.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/LICENSE.md +193 -0
- package/dist/cdn/perspective-viewer-charts.js +3 -0
- package/dist/cdn/perspective-viewer-charts.js.map +7 -0
- package/dist/esm/axis/axis-primitives.d.ts +24 -0
- package/dist/esm/axis/bar-axis.d.ts +51 -0
- package/dist/esm/axis/canvas.d.ts +24 -0
- package/dist/esm/axis/categorical-axis-core.d.ts +42 -0
- package/dist/esm/axis/categorical-axis.d.ts +27 -0
- package/dist/esm/axis/facet-chrome.d.ts +13 -0
- package/dist/esm/axis/label-geometry.d.ts +41 -0
- package/dist/esm/axis/legend.d.ts +44 -0
- package/dist/esm/axis/numeric-axis.d.ts +20 -0
- package/dist/esm/charts/candlestick/candlestick-build.d.ts +129 -0
- package/dist/esm/charts/candlestick/candlestick-interact.d.ts +10 -0
- package/dist/esm/charts/candlestick/candlestick-render.d.ts +24 -0
- package/dist/esm/charts/candlestick/candlestick.d.ts +144 -0
- package/dist/esm/charts/candlestick/glyphs/draw-candlesticks.d.ts +36 -0
- package/dist/esm/charts/candlestick/glyphs/draw-ohlc.d.ts +33 -0
- package/dist/esm/charts/canvas-types.d.ts +15 -0
- package/dist/esm/charts/cartesian/cartesian-build.d.ts +14 -0
- package/dist/esm/charts/cartesian/cartesian-interact.d.ts +20 -0
- package/dist/esm/charts/cartesian/cartesian-render.d.ts +26 -0
- package/dist/esm/charts/cartesian/cartesian.d.ts +239 -0
- package/dist/esm/charts/cartesian/glyph.d.ts +53 -0
- package/dist/esm/charts/cartesian/glyphs/density.d.ts +142 -0
- package/dist/esm/charts/cartesian/glyphs/lines.d.ts +23 -0
- package/dist/esm/charts/cartesian/glyphs/points.d.ts +24 -0
- package/dist/esm/charts/cartesian/label-interner.d.ts +21 -0
- package/dist/esm/charts/cartesian/tooltip-lines.d.ts +11 -0
- package/dist/esm/charts/chart-base.d.ts +402 -0
- package/dist/esm/charts/chart.d.ts +338 -0
- package/dist/esm/charts/common/band-layout.d.ts +32 -0
- package/dist/esm/charts/common/categorical-y-chart.d.ts +53 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +90 -0
- package/dist/esm/charts/common/chrome-cache.d.ts +18 -0
- package/dist/esm/charts/common/draw-tooltip-box.d.ts +9 -0
- package/dist/esm/charts/common/leaf-color.d.ts +33 -0
- package/dist/esm/charts/common/node-store.d.ts +81 -0
- package/dist/esm/charts/common/tree-chart.d.ts +48 -0
- package/dist/esm/charts/common/tree-chrome.d.ts +31 -0
- package/dist/esm/charts/common/tree-data.d.ts +54 -0
- package/dist/esm/charts/common/visible-extent.d.ts +51 -0
- package/dist/esm/charts/heatmap/heatmap-build.d.ts +86 -0
- package/dist/esm/charts/heatmap/heatmap-interact.d.ts +19 -0
- package/dist/esm/charts/heatmap/heatmap-render.d.ts +19 -0
- package/dist/esm/charts/heatmap/heatmap-y-axis.d.ts +46 -0
- package/dist/esm/charts/heatmap/heatmap.d.ts +117 -0
- package/dist/esm/charts/map/map.d.ts +67 -0
- package/dist/esm/charts/registry.d.ts +14 -0
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +30 -0
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +15 -0
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +34 -0
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +33 -0
- package/dist/esm/charts/series/series-build.d.ts +228 -0
- package/dist/esm/charts/series/series-interact.d.ts +35 -0
- package/dist/esm/charts/series/series-render.d.ts +41 -0
- package/dist/esm/charts/series/series-type.d.ts +49 -0
- package/dist/esm/charts/series/series.d.ts +317 -0
- package/dist/esm/charts/sunburst/sunburst-interact.d.ts +7 -0
- package/dist/esm/charts/sunburst/sunburst-layout.d.ts +33 -0
- package/dist/esm/charts/sunburst/sunburst-render.d.ts +22 -0
- package/dist/esm/charts/sunburst/sunburst.d.ts +85 -0
- package/dist/esm/charts/treemap/treemap-interact.d.ts +12 -0
- package/dist/esm/charts/treemap/treemap-layout.d.ts +28 -0
- package/dist/esm/charts/treemap/treemap-render.d.ts +18 -0
- package/dist/esm/charts/treemap/treemap.d.ts +74 -0
- package/dist/esm/config.d.ts +27 -0
- package/dist/esm/data/lazy-row.d.ts +32 -0
- package/dist/esm/data/split-groups.d.ts +20 -0
- package/dist/esm/data/view-reader.d.ts +35 -0
- package/dist/esm/event-detail.d.ts +28 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/interaction/hit-test.d.ts +30 -0
- package/dist/esm/interaction/host-sink-dom.d.ts +19 -0
- package/dist/esm/interaction/host-sink-message.d.ts +46 -0
- package/dist/esm/interaction/lazy-tooltip.d.ts +61 -0
- package/dist/esm/interaction/raw-event-forwarder.d.ts +27 -0
- package/dist/esm/interaction/spatial-grid.d.ts +15 -0
- package/dist/esm/interaction/tooltip-controller.d.ts +193 -0
- package/dist/esm/interaction/zoom-controller.d.ts +106 -0
- package/dist/esm/interaction/zoom-router.d.ts +48 -0
- package/dist/esm/layout/facet-grid.d.ts +126 -0
- package/dist/esm/layout/plot-layout.d.ts +104 -0
- package/dist/esm/layout/ticks.d.ts +17 -0
- package/dist/esm/map/mercator.d.ts +102 -0
- package/dist/esm/map/tile-cache.d.ts +38 -0
- package/dist/esm/map/tile-layer.d.ts +66 -0
- package/dist/esm/map/tile-loader.d.ts +52 -0
- package/dist/esm/map/tile-source.d.ts +66 -0
- package/dist/esm/perspective-viewer-charts.js +3 -0
- package/dist/esm/perspective-viewer-charts.js.map +7 -0
- package/dist/esm/plugin/charts.d.ts +40 -0
- package/dist/esm/plugin/plugin.d.ts +95 -0
- package/dist/esm/render/scheduler.d.ts +41 -0
- package/dist/esm/theme/gradient.d.ts +48 -0
- package/dist/esm/theme/palette.d.ts +13 -0
- package/dist/esm/theme/theme-snapshot.d.ts +7 -0
- package/dist/esm/theme/theme.d.ts +53 -0
- package/dist/esm/transport/protocol.d.ts +430 -0
- package/dist/esm/transport/renderer-transport.d.ts +201 -0
- package/dist/esm/utils/css.d.ts +1 -0
- package/dist/esm/utils/font-snapshot.d.ts +50 -0
- package/dist/esm/webgl/buffer-pool.d.ts +62 -0
- package/dist/esm/webgl/context-manager.d.ts +184 -0
- package/dist/esm/webgl/gradient-texture.d.ts +17 -0
- package/dist/esm/webgl/instanced-attrs.d.ts +44 -0
- package/dist/esm/webgl/plot-frame.d.ts +39 -0
- package/dist/esm/webgl/program-cache.d.ts +13 -0
- package/dist/esm/webgl/shader-manifest.d.ts +53 -0
- package/dist/esm/webgl/shader-registry.d.ts +22 -0
- package/dist/esm/worker/boot.d.ts +0 -0
- package/dist/esm/worker/dispatch.d.ts +9 -0
- package/dist/esm/worker/font-loader.d.ts +2 -0
- package/dist/esm/worker/renderer.worker.d.ts +115 -0
- package/dist/esm/worker/session-host.d.ts +26 -0
- package/package.json +47 -0
- package/src/css/perspective-viewer-charts.css +95 -0
- package/src/ts/axis/axis-primitives.ts +125 -0
- package/src/ts/axis/bar-axis.ts +345 -0
- package/src/ts/axis/canvas.ts +64 -0
- package/src/ts/axis/categorical-axis-core.ts +125 -0
- package/src/ts/axis/categorical-axis.ts +716 -0
- package/src/ts/axis/facet-chrome.ts +42 -0
- package/src/ts/axis/label-geometry.ts +188 -0
- package/src/ts/axis/legend.ts +218 -0
- package/src/ts/axis/numeric-axis.ts +353 -0
- package/src/ts/charts/candlestick/candlestick-build.ts +516 -0
- package/src/ts/charts/candlestick/candlestick-interact.ts +256 -0
- package/src/ts/charts/candlestick/candlestick-render.ts +387 -0
- package/src/ts/charts/candlestick/candlestick.ts +367 -0
- package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +432 -0
- package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +317 -0
- package/src/ts/charts/canvas-types.ts +30 -0
- package/src/ts/charts/cartesian/cartesian-build.ts +616 -0
- package/src/ts/charts/cartesian/cartesian-interact.ts +355 -0
- package/src/ts/charts/cartesian/cartesian-render.ts +948 -0
- package/src/ts/charts/cartesian/cartesian.ts +469 -0
- package/src/ts/charts/cartesian/glyph.ts +81 -0
- package/src/ts/charts/cartesian/glyphs/density.ts +1263 -0
- package/src/ts/charts/cartesian/glyphs/lines.ts +320 -0
- package/src/ts/charts/cartesian/glyphs/points.ts +239 -0
- package/src/ts/charts/cartesian/label-interner.ts +56 -0
- package/src/ts/charts/cartesian/tooltip-lines.ts +80 -0
- package/src/ts/charts/chart-base.ts +840 -0
- package/src/ts/charts/chart.ts +427 -0
- package/src/ts/charts/common/band-layout.ts +63 -0
- package/src/ts/charts/common/categorical-y-chart.ts +81 -0
- package/src/ts/charts/common/category-axis-resolver.ts +314 -0
- package/src/ts/charts/common/chrome-cache.ts +79 -0
- package/src/ts/charts/common/draw-tooltip-box.ts +84 -0
- package/src/ts/charts/common/leaf-color.ts +92 -0
- package/src/ts/charts/common/node-store.ts +235 -0
- package/src/ts/charts/common/tree-chart.ts +76 -0
- package/src/ts/charts/common/tree-chrome.ts +123 -0
- package/src/ts/charts/common/tree-data.ts +623 -0
- package/src/ts/charts/common/visible-extent.ts +112 -0
- package/src/ts/charts/heatmap/heatmap-build.ts +426 -0
- package/src/ts/charts/heatmap/heatmap-interact.ts +274 -0
- package/src/ts/charts/heatmap/heatmap-render.ts +815 -0
- package/src/ts/charts/heatmap/heatmap-y-axis.ts +351 -0
- package/src/ts/charts/heatmap/heatmap.ts +368 -0
- package/src/ts/charts/map/map.ts +201 -0
- package/src/ts/charts/registry.ts +65 -0
- package/src/ts/charts/series/glyphs/draw-areas.ts +331 -0
- package/src/ts/charts/series/glyphs/draw-bars.ts +113 -0
- package/src/ts/charts/series/glyphs/draw-lines.ts +320 -0
- package/src/ts/charts/series/glyphs/draw-scatter.ts +328 -0
- package/src/ts/charts/series/series-build.ts +848 -0
- package/src/ts/charts/series/series-interact.ts +604 -0
- package/src/ts/charts/series/series-render.ts +1109 -0
- package/src/ts/charts/series/series-type.ts +99 -0
- package/src/ts/charts/series/series.ts +794 -0
- package/src/ts/charts/sunburst/sunburst-interact.ts +460 -0
- package/src/ts/charts/sunburst/sunburst-layout.ts +238 -0
- package/src/ts/charts/sunburst/sunburst-render.ts +887 -0
- package/src/ts/charts/sunburst/sunburst.ts +248 -0
- package/src/ts/charts/treemap/treemap-interact.ts +445 -0
- package/src/ts/charts/treemap/treemap-layout.ts +328 -0
- package/src/ts/charts/treemap/treemap-render.ts +886 -0
- package/src/ts/charts/treemap/treemap.ts +247 -0
- package/src/ts/config.ts +41 -0
- package/src/ts/data/lazy-row.ts +140 -0
- package/src/ts/data/split-groups.ts +97 -0
- package/src/ts/data/view-reader.ts +107 -0
- package/src/ts/event-detail.ts +44 -0
- package/src/ts/index.ts +53 -0
- package/src/ts/interaction/hit-test.ts +106 -0
- package/src/ts/interaction/host-sink-dom.ts +85 -0
- package/src/ts/interaction/host-sink-message.ts +75 -0
- package/src/ts/interaction/lazy-tooltip.ts +102 -0
- package/src/ts/interaction/raw-event-forwarder.ts +175 -0
- package/src/ts/interaction/spatial-grid.ts +100 -0
- package/src/ts/interaction/tooltip-controller.ts +407 -0
- package/src/ts/interaction/zoom-controller.ts +468 -0
- package/src/ts/interaction/zoom-router.ts +230 -0
- package/src/ts/layout/facet-grid.ts +346 -0
- package/src/ts/layout/plot-layout.ts +277 -0
- package/src/ts/layout/ticks.ts +168 -0
- package/src/ts/map/mercator.ts +204 -0
- package/src/ts/map/tile-cache.ts +96 -0
- package/src/ts/map/tile-layer.ts +382 -0
- package/src/ts/map/tile-loader.ts +143 -0
- package/src/ts/map/tile-source.ts +156 -0
- package/src/ts/plugin/charts.ts +286 -0
- package/src/ts/plugin/plugin.ts +668 -0
- package/src/ts/render/scheduler.ts +339 -0
- package/src/ts/shaders/area.frag.glsl +20 -0
- package/src/ts/shaders/area.vert.glsl +19 -0
- package/src/ts/shaders/bar.frag.glsl +25 -0
- package/src/ts/shaders/bar.vert.glsl +60 -0
- package/src/ts/shaders/candlestick-body.frag.glsl +19 -0
- package/src/ts/shaders/candlestick-body.vert.glsl +34 -0
- package/src/ts/shaders/density-extreme.frag.glsl +30 -0
- package/src/ts/shaders/density-mrt.frag.glsl +44 -0
- package/src/ts/shaders/density-mrt.vert.glsl +48 -0
- package/src/ts/shaders/density-resolve.frag.glsl +89 -0
- package/src/ts/shaders/density-resolve.vert.glsl +23 -0
- package/src/ts/shaders/density-splat.frag.glsl +34 -0
- package/src/ts/shaders/density-splat.vert.glsl +52 -0
- package/src/ts/shaders/gridline.frag.glsl +18 -0
- package/src/ts/shaders/gridline.vert.glsl +18 -0
- package/src/ts/shaders/heatmap.frag.glsl +23 -0
- package/src/ts/shaders/heatmap.vert.glsl +42 -0
- package/src/ts/shaders/line-uniform.frag.glsl +26 -0
- package/src/ts/shaders/line-uniform.vert.glsl +54 -0
- package/src/ts/shaders/line.frag.glsl +28 -0
- package/src/ts/shaders/line.vert.glsl +87 -0
- package/src/ts/shaders/scatter.frag.glsl +39 -0
- package/src/ts/shaders/scatter.vert.glsl +67 -0
- package/src/ts/shaders/sunburst-arc.frag.glsl +19 -0
- package/src/ts/shaders/sunburst-arc.vert.glsl +79 -0
- package/src/ts/shaders/tile.frag.glsl +27 -0
- package/src/ts/shaders/tile.vert.glsl +35 -0
- package/src/ts/shaders/treemap.frag.glsl +19 -0
- package/src/ts/shaders/treemap.vert.glsl +25 -0
- package/src/ts/shaders/y-scatter.frag.glsl +30 -0
- package/src/ts/shaders/y-scatter.vert.glsl +31 -0
- package/src/ts/theme/gradient.ts +312 -0
- package/src/ts/theme/palette.ts +64 -0
- package/src/ts/theme/theme-snapshot.ts +66 -0
- package/src/ts/theme/theme.ts +166 -0
- package/src/ts/transport/protocol.ts +497 -0
- package/src/ts/transport/renderer-transport.ts +788 -0
- package/src/ts/utils/css.ts +36 -0
- package/src/ts/utils/font-snapshot.ts +159 -0
- package/src/ts/webgl/buffer-pool.ts +163 -0
- package/src/ts/webgl/context-manager.ts +414 -0
- package/src/ts/webgl/gradient-texture.ts +84 -0
- package/src/ts/webgl/instanced-attrs.ts +139 -0
- package/src/ts/webgl/plot-frame.ts +91 -0
- package/src/ts/webgl/program-cache.ts +46 -0
- package/src/ts/webgl/shader-manifest.ts +148 -0
- package/src/ts/webgl/shader-registry.ts +97 -0
- package/src/ts/worker/boot.ts +22 -0
- package/src/ts/worker/dispatch.ts +99 -0
- package/src/ts/worker/font-loader.ts +89 -0
- package/src/ts/worker/renderer.worker.ts +734 -0
- package/src/ts/worker/session-host.ts +118 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import type { Client, View, ViewConfig } from "@perspective-dev/client";
|
|
14
|
+
import type { FacetConfig, PluginConfig } from "../charts/chart";
|
|
15
|
+
import type {
|
|
16
|
+
ControlMsg,
|
|
17
|
+
InitMsg,
|
|
18
|
+
InteractionEvent,
|
|
19
|
+
LoadAndRenderMsg,
|
|
20
|
+
WorkerEnvelope,
|
|
21
|
+
WorkerMsg,
|
|
22
|
+
} from "./protocol";
|
|
23
|
+
import {
|
|
24
|
+
PerspectiveSelectDetail,
|
|
25
|
+
type PerspectiveClickDetail,
|
|
26
|
+
} from "../event-detail";
|
|
27
|
+
import { snapshotThemeVars } from "../theme/theme-snapshot";
|
|
28
|
+
import { snapshotFontFaces } from "../utils/font-snapshot";
|
|
29
|
+
import { DomHostSink } from "../interaction/host-sink-dom";
|
|
30
|
+
import { RUNTIME_MODE } from "../config";
|
|
31
|
+
|
|
32
|
+
// @ts-ignore — resolved at build time by `@perspective-dev/esbuild-plugin/worker`
|
|
33
|
+
import getWorkerURL from "../worker/renderer.worker.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Module-level shared `Worker` for every `RendererTransport` running
|
|
37
|
+
* in worker mode. One worker process hosts one `WorkerRenderer` per
|
|
38
|
+
* active `sessionId` — N chart instances share startup costs (wasm
|
|
39
|
+
* `initSync`, font loads, JS module parse) instead of paying them N
|
|
40
|
+
* times.
|
|
41
|
+
*
|
|
42
|
+
* Lazy: created on first `transport.init()` in worker mode. Lives
|
|
43
|
+
* until page teardown — no refcount, no termination logic. Pages
|
|
44
|
+
* with no charts never spawn one. Per-session memory still scales
|
|
45
|
+
* with N (each session retains its own WebGL context, buffer pool,
|
|
46
|
+
* chart impl, view, client); the browser's ~16-context-per-worker
|
|
47
|
+
* cap is the new ceiling on simultaneous worker-mode charts.
|
|
48
|
+
*
|
|
49
|
+
* In-process mode bypasses this entirely — each transport gets its
|
|
50
|
+
* own `MessageChannel` + in-thread `WorkerRenderer`.
|
|
51
|
+
*/
|
|
52
|
+
let SHARED_WORKER: Promise<Worker> | null = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Per-session message handlers, keyed by the host-allocated
|
|
56
|
+
* `sessionId`. The shared worker's response listener demultiplexes
|
|
57
|
+
* incoming envelopes into here.
|
|
58
|
+
*/
|
|
59
|
+
const HOST_LISTENERS = new Map<number, (msg: WorkerMsg) => void>();
|
|
60
|
+
|
|
61
|
+
let NEXT_SESSION_ID = 0;
|
|
62
|
+
|
|
63
|
+
async function getSharedWorker(): Promise<Worker> {
|
|
64
|
+
if (SHARED_WORKER) {
|
|
65
|
+
return SHARED_WORKER;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
SHARED_WORKER = (async () => {
|
|
69
|
+
const url = await getWorkerURL();
|
|
70
|
+
const w = new Worker(url, { type: "module", name: "viewer-charts" });
|
|
71
|
+
w.addEventListener("message", (e: MessageEvent) => {
|
|
72
|
+
const env = e.data as WorkerEnvelope;
|
|
73
|
+
HOST_LISTENERS.get(env.sessionId)?.(env.msg);
|
|
74
|
+
});
|
|
75
|
+
return w;
|
|
76
|
+
})();
|
|
77
|
+
|
|
78
|
+
return SHARED_WORKER;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface RendererHandle {
|
|
82
|
+
post(msg: any, transfer: Transferable[]): void;
|
|
83
|
+
addMessageListener(cb: (msg: any) => void): void;
|
|
84
|
+
terminate(): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type PendingRenderType = "saveZoom" | "loadAndRender" | "snapshotPng";
|
|
88
|
+
interface PendingRenderRequest {
|
|
89
|
+
kind: PendingRenderType;
|
|
90
|
+
resolve: (v: any) => void;
|
|
91
|
+
reject: (e: Error) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Unified host-side driver for the chart renderer. Owns one of two
|
|
96
|
+
* handle shapes:
|
|
97
|
+
*
|
|
98
|
+
* - **Worker mode**: a real `Worker` running the same module. The
|
|
99
|
+
* handle posts `ControlMsg`s over `Worker.postMessage`.
|
|
100
|
+
* - **In-process mode**: a `MessageChannel` whose `port2` is owned
|
|
101
|
+
* by an in-thread `WorkerRenderer` instantiated via
|
|
102
|
+
* `await import(workerURL)`. Same module bytes, different host.
|
|
103
|
+
*
|
|
104
|
+
* Both modes go through the same control channel, the same
|
|
105
|
+
* `ProxySession` proxy port, and the same `OffscreenCanvas` transfer
|
|
106
|
+
* — `MessageChannel` and `transferControlToOffscreen` work in-realm
|
|
107
|
+
* just as well as cross-thread. The only branching is at construction
|
|
108
|
+
* (handle creation) and bootstrap (worker scope sets up its own
|
|
109
|
+
* `Client`; in-process reuses the host's).
|
|
110
|
+
*/
|
|
111
|
+
export class RendererTransport {
|
|
112
|
+
private _handle: RendererHandle | null = null;
|
|
113
|
+
private _proxyChannel: MessageChannel | null = null;
|
|
114
|
+
private _proxySession: any = null;
|
|
115
|
+
private _client: Client;
|
|
116
|
+
private _view: View;
|
|
117
|
+
private _tableName: string | undefined;
|
|
118
|
+
private _clientWorkerURL: URL;
|
|
119
|
+
private _clientWasm: WebAssembly.Module;
|
|
120
|
+
private _chartTag: string;
|
|
121
|
+
private _maxCells: number;
|
|
122
|
+
private _precompileShaders: boolean;
|
|
123
|
+
private _ready: Promise<void>;
|
|
124
|
+
private _resolveReady!: () => void;
|
|
125
|
+
private _rejectReady!: (err: Error) => void;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Pending request/reply promises across all worker round-trips —
|
|
129
|
+
* `saveZoom`, `uploadChunk` ACKs, and `snapshotPng`. Each entry
|
|
130
|
+
* carries its `kind` so `destroy()` can apply per-kind teardown
|
|
131
|
+
* semantics (uploadChunk resolves silently, the rest reject with
|
|
132
|
+
* a teardown error).
|
|
133
|
+
*
|
|
134
|
+
* Keyed by a single monotonic counter; the worker's reply messages
|
|
135
|
+
* carry that id back verbatim. One counter for all kinds is safe
|
|
136
|
+
* because the host's switch already keys on `msg.kind` before
|
|
137
|
+
* resolving.
|
|
138
|
+
*/
|
|
139
|
+
private _pending = new Map<number, PendingRenderRequest>();
|
|
140
|
+
|
|
141
|
+
private _pendingCounter = 0;
|
|
142
|
+
private _onZoomChanged: ((isDefault: boolean) => void) | null = null;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Cached zoom-default flag pushed by the renderer after each zoom
|
|
146
|
+
* mutation. Surfaced sync via `allZoomsDefault()`; updates between
|
|
147
|
+
* calls are best-effort.
|
|
148
|
+
*/
|
|
149
|
+
private _allZoomsDefault = true;
|
|
150
|
+
private _hostGlCanvas: HTMLCanvasElement | null = null;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Blit-mode only: the visible `.webgl-canvas`'s 2D context. The
|
|
154
|
+
* worker emits each completed GL frame as a `FrameBitmapMsg`; on
|
|
155
|
+
* receipt we `drawImage` the bitmap into this context and `close()`
|
|
156
|
+
* it to release the GPU surface. Null in direct mode (the visible
|
|
157
|
+
* canvas's drawing buffer is the worker's transferred GL canvas).
|
|
158
|
+
*/
|
|
159
|
+
private _displayCtx: CanvasRenderingContext2D | null = null;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Host-side sink for tooltip + cursor side-effects. The chart
|
|
163
|
+
* inside the renderer calls into a `MessageHostSink` that posts
|
|
164
|
+
* `pinTooltip` / `dismissTooltip` / `setCursor` over the control
|
|
165
|
+
* channel; this sink applies them to the DOM. Initialized lazily
|
|
166
|
+
* on first signal so we don't pay for the parent-style lookup
|
|
167
|
+
* unless a user interacts.
|
|
168
|
+
*/
|
|
169
|
+
private _hostSink: DomHostSink | null = null;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Last `insertConfig` accepted by a `userSelect { selected: true }`
|
|
173
|
+
* message. Used to populate `removeConfigs` on the next
|
|
174
|
+
* `selected: false` (unpin / drill-up / view-change) — mirrors
|
|
175
|
+
* datagrid's `model._last_insert_configs` so coordinated-filter
|
|
176
|
+
* consumers can roll back the previous select when a new one
|
|
177
|
+
* supplants it.
|
|
178
|
+
*/
|
|
179
|
+
private _lastInsertConfig: Partial<ViewConfig> | undefined = undefined;
|
|
180
|
+
|
|
181
|
+
constructor(opts: {
|
|
182
|
+
client: Client;
|
|
183
|
+
view: View;
|
|
184
|
+
tableName?: string;
|
|
185
|
+
clientWasm: WebAssembly.Module;
|
|
186
|
+
clientWorkerURL: URL;
|
|
187
|
+
chartTag: string;
|
|
188
|
+
maxCells: number;
|
|
189
|
+
precompileShaders?: boolean;
|
|
190
|
+
onZoomChanged?: (isDefault: boolean) => void;
|
|
191
|
+
}) {
|
|
192
|
+
this._client = opts.client;
|
|
193
|
+
this._view = opts.view;
|
|
194
|
+
this._tableName = opts.tableName;
|
|
195
|
+
this._clientWorkerURL = opts.clientWorkerURL;
|
|
196
|
+
this._clientWasm = opts.clientWasm;
|
|
197
|
+
this._chartTag = opts.chartTag;
|
|
198
|
+
this._maxCells = opts.maxCells;
|
|
199
|
+
this._precompileShaders = opts.precompileShaders ?? false;
|
|
200
|
+
this._onZoomChanged = opts.onZoomChanged ?? null;
|
|
201
|
+
this._ready = new Promise((resolve, reject) => {
|
|
202
|
+
this._resolveReady = resolve;
|
|
203
|
+
this._rejectReady = reject;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async init(opts: {
|
|
208
|
+
gl: HTMLCanvasElement;
|
|
209
|
+
gridlines: HTMLCanvasElement;
|
|
210
|
+
chrome: HTMLCanvasElement;
|
|
211
|
+
facetConfig: FacetConfig;
|
|
212
|
+
pluginConfig: PluginConfig;
|
|
213
|
+
defaultChartType?: string;
|
|
214
|
+
renderBlitMode: "blit" | "direct";
|
|
215
|
+
}): Promise<void> {
|
|
216
|
+
this._hostGlCanvas = opts.gl;
|
|
217
|
+
const workerURL: string = await getWorkerURL();
|
|
218
|
+
|
|
219
|
+
// Worker mode: bridge the worker's fresh `Client` (instantiated
|
|
220
|
+
// in `bootstrapWorker` from `clientWasm` + `clientWorkerURL`)
|
|
221
|
+
// back to the host's real `Client` via a `ProxySession` over a
|
|
222
|
+
// dedicated `MessageChannel`.
|
|
223
|
+
//
|
|
224
|
+
// In-process mode skips this entirely — `bootstrapInProcess`
|
|
225
|
+
// is handed the host's `Client` directly, so there's no
|
|
226
|
+
// worker-side `Client` to bridge. The proxy port would just
|
|
227
|
+
// dangle.
|
|
228
|
+
if (RUNTIME_MODE === "worker") {
|
|
229
|
+
this._proxyChannel = new MessageChannel();
|
|
230
|
+
this._proxySession = (this._client as any).new_proxy_session(
|
|
231
|
+
(bytes: Uint8Array) => {
|
|
232
|
+
const buf = bytes.slice().buffer;
|
|
233
|
+
this._proxyChannel!.port1.postMessage(buf, [buf]);
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
this._proxyChannel.port1.addEventListener(
|
|
238
|
+
"message",
|
|
239
|
+
(e: MessageEvent) => {
|
|
240
|
+
this._proxySession.handle_request(new Uint8Array(e.data));
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
this._proxyChannel.port1.start();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Blit mode keeps the visible `.webgl-canvas` main-thread with
|
|
248
|
+
// a 2D context — the renderer paints into its own internal
|
|
249
|
+
// `OffscreenCanvas` and ships each completed frame back as an
|
|
250
|
+
// `ImageBitmap`. Direct mode transfers the visible canvas's
|
|
251
|
+
// drawing buffer to the renderer so GL paints straight to
|
|
252
|
+
// screen.
|
|
253
|
+
let glOC: OffscreenCanvas | undefined;
|
|
254
|
+
if (opts.renderBlitMode === "blit") {
|
|
255
|
+
this._displayCtx = opts.gl.getContext("2d");
|
|
256
|
+
} else {
|
|
257
|
+
glOC = opts.gl.transferControlToOffscreen();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const gridlinesOC = opts.gridlines.transferControlToOffscreen();
|
|
261
|
+
const chromeOC = opts.chrome.transferControlToOffscreen();
|
|
262
|
+
const rect = opts.gl.getBoundingClientRect();
|
|
263
|
+
const dpr = window.devicePixelRatio || 1;
|
|
264
|
+
const themeVars = snapshotThemeVars(opts.gl);
|
|
265
|
+
|
|
266
|
+
// Worker mode forwards `@font-face` rules so the worker's
|
|
267
|
+
// separate `FontFaceSet` can resolve `ctx.font` family names.
|
|
268
|
+
// In-process mode shares `document.fonts` with the host —
|
|
269
|
+
// omit the descriptors entirely.
|
|
270
|
+
const fontFaces = RUNTIME_MODE === "worker" ? snapshotFontFaces() : [];
|
|
271
|
+
const clientWasm =
|
|
272
|
+
RUNTIME_MODE === "worker" ? this._clientWasm : undefined;
|
|
273
|
+
|
|
274
|
+
const clientWorkerURL =
|
|
275
|
+
RUNTIME_MODE === "worker" ? this._clientWorkerURL : undefined;
|
|
276
|
+
|
|
277
|
+
const proxyPort =
|
|
278
|
+
RUNTIME_MODE === "worker" ? this._proxyChannel!.port2 : undefined;
|
|
279
|
+
|
|
280
|
+
const initMsg: InitMsg = {
|
|
281
|
+
kind: "init",
|
|
282
|
+
renderMode: opts.renderBlitMode,
|
|
283
|
+
glCanvas: glOC,
|
|
284
|
+
gridlinesCanvas: gridlinesOC,
|
|
285
|
+
chromeCanvas: chromeOC,
|
|
286
|
+
proxyPort,
|
|
287
|
+
clientWorkerURL,
|
|
288
|
+
clientWasm,
|
|
289
|
+
chartTag: this._chartTag,
|
|
290
|
+
viewName: this._view.__unsafe_get_name(),
|
|
291
|
+
tableName: this._tableName,
|
|
292
|
+
facetConfig: opts.facetConfig,
|
|
293
|
+
pluginConfig: opts.pluginConfig,
|
|
294
|
+
defaultChartType: opts.defaultChartType,
|
|
295
|
+
themeVars,
|
|
296
|
+
fontFaces,
|
|
297
|
+
cssWidth: rect.width,
|
|
298
|
+
cssHeight: rect.height,
|
|
299
|
+
dpr,
|
|
300
|
+
bufferMaxCapacity: 0,
|
|
301
|
+
precompileShaders: this._precompileShaders,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
this._handle = await this._createHandle(workerURL, initMsg);
|
|
305
|
+
this._handle.addMessageListener((msg) =>
|
|
306
|
+
this._handleRendererMsg(msg as WorkerMsg),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (RUNTIME_MODE === "worker") {
|
|
310
|
+
// Worker mode: the bootstrap is triggered by posting the
|
|
311
|
+
// init message into the worker's scope (which the
|
|
312
|
+
// `if (IS_WORKER_SCOPE)` block in `renderer.worker.ts`
|
|
313
|
+
// listens for). `glOC` is omitted in blit mode (the
|
|
314
|
+
// renderer allocates its own offscreen) — only include the
|
|
315
|
+
// GL canvas in the transfer list when present.
|
|
316
|
+
const transfer: Transferable[] = [
|
|
317
|
+
gridlinesOC,
|
|
318
|
+
chromeOC,
|
|
319
|
+
this._proxyChannel!.port2,
|
|
320
|
+
];
|
|
321
|
+
if (glOC) {
|
|
322
|
+
transfer.unshift(glOC);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this._handle.post(initMsg, transfer);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// In-process mode: the handle's `_createHandle` already kicked
|
|
329
|
+
// off `bootstrapInProcess` with the init msg directly, no
|
|
330
|
+
// postMessage needed.
|
|
331
|
+
|
|
332
|
+
await this._ready;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Construct the underlying transport. Worker mode wraps the
|
|
337
|
+
* module-shared `Worker` (lazy, page-singleton) and tags every
|
|
338
|
+
* message with a unique `sessionId`. In-process mode pairs a
|
|
339
|
+
* `MessageChannel` with a dynamically-imported
|
|
340
|
+
* {@link bootstrapInProcess}.
|
|
341
|
+
*/
|
|
342
|
+
private async _createHandle(
|
|
343
|
+
workerURL: string,
|
|
344
|
+
initMsg: InitMsg,
|
|
345
|
+
): Promise<RendererHandle> {
|
|
346
|
+
if (RUNTIME_MODE === "worker") {
|
|
347
|
+
const w = await getSharedWorker();
|
|
348
|
+
const sessionId = ++NEXT_SESSION_ID;
|
|
349
|
+
return {
|
|
350
|
+
post: (msg, transfer) =>
|
|
351
|
+
w.postMessage({ sessionId, msg }, transfer),
|
|
352
|
+
addMessageListener: (cb) => {
|
|
353
|
+
HOST_LISTENERS.set(sessionId, cb);
|
|
354
|
+
},
|
|
355
|
+
terminate: () => {
|
|
356
|
+
HOST_LISTENERS.delete(sessionId);
|
|
357
|
+
// Don't terminate the underlying worker — other
|
|
358
|
+
// sessions may still be live. Worker-side
|
|
359
|
+
// `WorkerRenderer` cleanup is driven by the
|
|
360
|
+
// `destroy` ControlMsg posted by the transport
|
|
361
|
+
// before reaching here.
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// In-process: instantiate the renderer on this thread by
|
|
367
|
+
// dynamic-importing the same module the worker uses. The Blob
|
|
368
|
+
// URL (or file URL in debug builds) loads as ESM, so module
|
|
369
|
+
// dedup means only one copy of the chart code lives in
|
|
370
|
+
// memory regardless of how many host elements use this mode.
|
|
371
|
+
// `@vite-ignore` is harmless under esbuild (esbuild's parser
|
|
372
|
+
// ignores it); some downstream bundlers honor it to suppress
|
|
373
|
+
// a static-import warning on the dynamic URL.
|
|
374
|
+
//
|
|
375
|
+
// Hand the host's already-bound `Client` to the renderer via
|
|
376
|
+
// `bootstrapInProcess` — option B. The dynamically-imported
|
|
377
|
+
// module has its own copy of the perspective-viewer
|
|
378
|
+
// wasm-bindgen JS, but that copy stays unused: we never
|
|
379
|
+
// construct `new Client(...)` inside it; we only ever call
|
|
380
|
+
// methods on the host-supplied instance.
|
|
381
|
+
const mod: any = await import(/* @vite-ignore */ workerURL);
|
|
382
|
+
const channel = new MessageChannel();
|
|
383
|
+
await mod.bootstrapInProcess({
|
|
384
|
+
msg: initMsg,
|
|
385
|
+
client: this._client,
|
|
386
|
+
controlPort: channel.port2,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
post: (msg, transfer) => channel.port1.postMessage(msg, transfer),
|
|
391
|
+
addMessageListener: (cb) => {
|
|
392
|
+
// `addEventListener("message", …)` does NOT auto-start
|
|
393
|
+
// a `MessagePort` — only setting `onmessage` does.
|
|
394
|
+
// Without this explicit `start()` the renderer's
|
|
395
|
+
// `{ kind: "ready" }` would queue on `port1` forever
|
|
396
|
+
// and `init()` would hang on `await this._ready`.
|
|
397
|
+
channel.port1.addEventListener("message", (e: MessageEvent) =>
|
|
398
|
+
cb(e.data),
|
|
399
|
+
);
|
|
400
|
+
channel.port1.start();
|
|
401
|
+
},
|
|
402
|
+
terminate: () => {
|
|
403
|
+
channel.port1.close();
|
|
404
|
+
channel.port2.close();
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
setView(view: View): void {
|
|
410
|
+
this._view = view;
|
|
411
|
+
this._post({
|
|
412
|
+
kind: "setViewByName",
|
|
413
|
+
name: this._view.__unsafe_get_name(),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
setColumnsConfig(cfg: Record<string, any>): void {
|
|
418
|
+
this._post({ kind: "setColumnsConfig", cfg });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
setPluginConfig(cfg: PluginConfig): void {
|
|
422
|
+
this._post({ kind: "setPluginConfig", cfg });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
setBufferMaxCapacity(n: number): void {
|
|
426
|
+
this._post({ kind: "setBufferMaxCapacity", n });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Trigger a worker-side data fetch + render cycle. The worker
|
|
431
|
+
* resolves all schema / row-count metadata against its own `View`
|
|
432
|
+
* and `Table`, runs `view.with_typed_arrays`, and pipes the
|
|
433
|
+
* resulting `ColumnDataMap` directly into `chartImpl.uploadAndRender`
|
|
434
|
+
* — no host-side `Client`/`Table`/`View` await, no `postMessage` of
|
|
435
|
+
* column buffers.
|
|
436
|
+
*
|
|
437
|
+
* The returned promise resolves when the worker replies with
|
|
438
|
+
* `loadAndRenderAck`. Per the worker's "resolve on stale"
|
|
439
|
+
* contract, a mid-flight cancellation (a newer `loadAndRender`
|
|
440
|
+
* superseding this one) still acks — the host's awaiter just
|
|
441
|
+
* resolves quietly.
|
|
442
|
+
*/
|
|
443
|
+
loadAndRender(opts: {
|
|
444
|
+
viewerConfig: {
|
|
445
|
+
group_by: string[];
|
|
446
|
+
split_by: string[];
|
|
447
|
+
columns: (string | null)[];
|
|
448
|
+
};
|
|
449
|
+
options?: { float32?: boolean };
|
|
450
|
+
}): Promise<void> {
|
|
451
|
+
const { id, promise } = this._allocPending<void>("loadAndRender");
|
|
452
|
+
const msg: LoadAndRenderMsg = {
|
|
453
|
+
kind: "loadAndRender",
|
|
454
|
+
msgId: id,
|
|
455
|
+
viewerConfig: opts.viewerConfig,
|
|
456
|
+
options: { float32: opts.options?.float32 ?? true },
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
this._post(msg);
|
|
460
|
+
return promise;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
redraw(): void {
|
|
464
|
+
this._post({ kind: "redraw" });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
resize(): void {
|
|
468
|
+
if (!this._hostGlCanvas) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const rect = this._hostGlCanvas.getBoundingClientRect();
|
|
473
|
+
const dpr = window.devicePixelRatio || 1;
|
|
474
|
+
this._post({
|
|
475
|
+
kind: "resize",
|
|
476
|
+
cssWidth: rect.width,
|
|
477
|
+
cssHeight: rect.height,
|
|
478
|
+
dpr,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
clear() {
|
|
483
|
+
this._post({ kind: "clear" });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
invalidateTheme() {
|
|
487
|
+
if (!this._hostGlCanvas) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const themeVars = snapshotThemeVars(this._hostGlCanvas);
|
|
492
|
+
this._post({ kind: "invalidateTheme", themeVars });
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async saveZoom() {
|
|
496
|
+
const { id } = this._allocPending<any>("saveZoom");
|
|
497
|
+
this._post({ kind: "saveZoom", requestId: id });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Allocate a pending request slot of the given `kind`. Returns the
|
|
502
|
+
* id (encoded into the outgoing `ControlMsg`) and a promise that
|
|
503
|
+
* resolves / rejects when the matching reply arrives or
|
|
504
|
+
* `destroy()` drains the table.
|
|
505
|
+
*/
|
|
506
|
+
private _allocPending<T>(kind: PendingRenderType): {
|
|
507
|
+
id: number;
|
|
508
|
+
promise: Promise<T>;
|
|
509
|
+
} {
|
|
510
|
+
const id = ++this._pendingCounter;
|
|
511
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
512
|
+
this._pending.set(id, { kind, resolve, reject });
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return { id, promise };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
restoreZoom(state: any): void {
|
|
519
|
+
this._post({ kind: "restoreZoom", state });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
allZoomsDefault(): boolean {
|
|
523
|
+
return this._allZoomsDefault;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
resetAllZooms(): void {
|
|
527
|
+
this._post({ kind: "resetAllZooms" });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
resetExpandedDomain(): void {
|
|
531
|
+
this._post({ kind: "resetExpandedDomain" });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Request a PNG snapshot of the current frame. The worker flushes a
|
|
536
|
+
* synchronous render across the GL + gridlines + chrome layers,
|
|
537
|
+
* composites them into a single `OffscreenCanvas`, fills the theme
|
|
538
|
+
* background, and replies with the `convertToBlob` result.
|
|
539
|
+
*/
|
|
540
|
+
snapshotPng(): Promise<Blob> {
|
|
541
|
+
const { id, promise } = this._allocPending<Blob>("snapshotPng");
|
|
542
|
+
this._post({ kind: "snapshotPng", requestId: id });
|
|
543
|
+
return promise;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
forwardInteraction(event: InteractionEvent): void {
|
|
547
|
+
this._post({ kind: "interaction", event });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
destroy(): void {
|
|
551
|
+
this._post({ kind: "destroy" });
|
|
552
|
+
if (this._proxySession) {
|
|
553
|
+
this._proxySession.close().catch(() => {});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (this._proxyChannel) {
|
|
557
|
+
this._proxyChannel.port1.close();
|
|
558
|
+
this._proxyChannel = null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (this._handle) {
|
|
562
|
+
this._handle.terminate();
|
|
563
|
+
this._handle = null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
this._hostSink?.dismiss();
|
|
567
|
+
this._hostSink = null;
|
|
568
|
+
|
|
569
|
+
// The host's `<canvas>` elements are torn down by the plugin
|
|
570
|
+
// element's `disconnectedCallback` after `destroy()` returns —
|
|
571
|
+
// null these refs now so any post-destroy code can't dereference
|
|
572
|
+
// them, and so the GPU-backed 2D context can release earlier.
|
|
573
|
+
this._hostGlCanvas = null;
|
|
574
|
+
this._displayCtx = null;
|
|
575
|
+
|
|
576
|
+
// Drain pending request promises with kind-aware semantics:
|
|
577
|
+
// - `loadAndRender` resolves silently (the host's awaited draw
|
|
578
|
+
// observes a clean "no more work" rather than a teardown
|
|
579
|
+
// rejection it would otherwise have to suppress).
|
|
580
|
+
// - `saveZoom` / `snapshotPng` reject so the upstream promise
|
|
581
|
+
// chain doesn't hang. Any unanswered messages still in the
|
|
582
|
+
// worker's queue are abandoned along with the renderer when
|
|
583
|
+
// the `destroy` ControlMsg fires worker-side.
|
|
584
|
+
const teardownErr = new Error("RendererTransport destroyed");
|
|
585
|
+
for (const entry of this._pending.values()) {
|
|
586
|
+
if (entry.kind === "loadAndRender") {
|
|
587
|
+
entry.resolve(undefined);
|
|
588
|
+
} else {
|
|
589
|
+
entry.reject(teardownErr);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this._pending.clear();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private _post(msg: ControlMsg): void {
|
|
597
|
+
this._postRaw(msg, []);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private _postRaw(msg: ControlMsg, transfer: Transferable[]): void {
|
|
601
|
+
if (!this._handle) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
this._handle.post(msg, transfer);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private _handleRendererMsg(msg: WorkerMsg): void {
|
|
609
|
+
switch (msg.kind) {
|
|
610
|
+
case "ready":
|
|
611
|
+
this._resolveReady();
|
|
612
|
+
break;
|
|
613
|
+
case "zoomChanged":
|
|
614
|
+
this._allZoomsDefault = msg.isDefault;
|
|
615
|
+
this._onZoomChanged?.(msg.isDefault);
|
|
616
|
+
break;
|
|
617
|
+
case "saveZoomReply":
|
|
618
|
+
this._resolvePending(msg.requestId, "saveZoom", msg.state);
|
|
619
|
+
break;
|
|
620
|
+
case "pinTooltip":
|
|
621
|
+
this._ensureHostSink()?.pin(msg.lines, msg.pos, msg.bounds);
|
|
622
|
+
break;
|
|
623
|
+
case "dismissTooltip":
|
|
624
|
+
this._hostSink?.dismiss();
|
|
625
|
+
break;
|
|
626
|
+
case "setCursor":
|
|
627
|
+
this._ensureHostSink()?.setCursor(msg.cursor);
|
|
628
|
+
break;
|
|
629
|
+
case "userClick":
|
|
630
|
+
this._dispatchOnViewer(
|
|
631
|
+
new CustomEvent<PerspectiveClickDetail>(
|
|
632
|
+
"perspective-click",
|
|
633
|
+
{
|
|
634
|
+
bubbles: true,
|
|
635
|
+
composed: true,
|
|
636
|
+
detail: msg.detail,
|
|
637
|
+
},
|
|
638
|
+
),
|
|
639
|
+
);
|
|
640
|
+
break;
|
|
641
|
+
case "userSelect": {
|
|
642
|
+
const removeConfigs = this._lastInsertConfig
|
|
643
|
+
? [this._lastInsertConfig]
|
|
644
|
+
: [];
|
|
645
|
+
const insertConfigs = msg.selected ? [msg.insertConfig] : [];
|
|
646
|
+
this._lastInsertConfig = msg.selected
|
|
647
|
+
? msg.insertConfig
|
|
648
|
+
: undefined;
|
|
649
|
+
const detail = new PerspectiveSelectDetail(
|
|
650
|
+
msg.selected,
|
|
651
|
+
msg.row,
|
|
652
|
+
msg.column_names,
|
|
653
|
+
// `Partial<ViewConfig>` (what the chart emits) is
|
|
654
|
+
// structurally a `ViewConfigUpdate` for the
|
|
655
|
+
// `filter`-only patches we ship; the only
|
|
656
|
+
// incompatible field (`group_by_depth: number |
|
|
657
|
+
// null`) is never set by our emitters.
|
|
658
|
+
removeConfigs as any,
|
|
659
|
+
insertConfigs as any,
|
|
660
|
+
);
|
|
661
|
+
this._dispatchOnViewer(
|
|
662
|
+
new CustomEvent<PerspectiveSelectDetail>(
|
|
663
|
+
"perspective-global-filter",
|
|
664
|
+
{
|
|
665
|
+
bubbles: true,
|
|
666
|
+
composed: true,
|
|
667
|
+
detail,
|
|
668
|
+
},
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
case "frameBitmap":
|
|
675
|
+
this._drawFrameBitmap(msg.bitmap);
|
|
676
|
+
break;
|
|
677
|
+
case "error":
|
|
678
|
+
this._rejectReady(new Error(msg.message));
|
|
679
|
+
break;
|
|
680
|
+
case "loadAndRenderAck":
|
|
681
|
+
this._resolvePending(msg.msgId, "loadAndRender", undefined);
|
|
682
|
+
break;
|
|
683
|
+
case "snapshotPngReply":
|
|
684
|
+
this._resolvePending(msg.requestId, "snapshotPng", msg.blob);
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Look up a pending request by id, verify the recorded kind
|
|
691
|
+
* matches the inbound reply, resolve, and remove. Mismatches are
|
|
692
|
+
* silently dropped — they would only fire if the worker echoed
|
|
693
|
+
* the wrong kind for a given id, which would itself be a bug
|
|
694
|
+
* worth catching at the worker side.
|
|
695
|
+
*/
|
|
696
|
+
private _resolvePending(
|
|
697
|
+
id: number,
|
|
698
|
+
kind: PendingRenderType,
|
|
699
|
+
value: unknown,
|
|
700
|
+
): void {
|
|
701
|
+
const entry = this._pending.get(id);
|
|
702
|
+
if (!entry || entry.kind !== kind) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
this._pending.delete(id);
|
|
707
|
+
entry.resolve(value);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Blit-mode handler: draw a renderer-emitted frame into the
|
|
712
|
+
* visible 2D-context display canvas, then close the bitmap so its
|
|
713
|
+
* GPU-backed surface is released. Resizes the visible canvas's
|
|
714
|
+
* drawing buffer to the bitmap dimensions on first frame and
|
|
715
|
+
* after any worker-side resize — the host doesn't directly
|
|
716
|
+
* control GL canvas size in blit mode, so we follow whatever the
|
|
717
|
+
* renderer emits.
|
|
718
|
+
*/
|
|
719
|
+
private _drawFrameBitmap(bitmap: ImageBitmap): void {
|
|
720
|
+
if (this._displayCtx && this._hostGlCanvas) {
|
|
721
|
+
const w = bitmap.width;
|
|
722
|
+
const h = bitmap.height;
|
|
723
|
+
if (this._hostGlCanvas.width !== w) {
|
|
724
|
+
this._hostGlCanvas.width = w;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (this._hostGlCanvas.height !== h) {
|
|
728
|
+
this._hostGlCanvas.height = h;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
this._displayCtx.globalCompositeOperation = "copy";
|
|
732
|
+
this._displayCtx.drawImage(bitmap, 0, 0);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
bitmap.close();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Dispatch a `CustomEvent` on the `<perspective-viewer>` ancestor
|
|
740
|
+
* of this transport's GL canvas. Walks the parent chain so the
|
|
741
|
+
* event bubbles from the viewer (matching where datagrid
|
|
742
|
+
* dispatches its `perspective-click` / `perspective-global-filter`
|
|
743
|
+
* events). No-op when the canvas is detached or no viewer ancestor
|
|
744
|
+
* exists (test harnesses, snapshot mode).
|
|
745
|
+
*/
|
|
746
|
+
private _dispatchOnViewer(ev: CustomEvent): void {
|
|
747
|
+
if (!this._hostGlCanvas) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
let node: Node | null = this._hostGlCanvas;
|
|
752
|
+
while (node) {
|
|
753
|
+
if (
|
|
754
|
+
node instanceof HTMLElement &&
|
|
755
|
+
node.tagName === "PERSPECTIVE-VIEWER"
|
|
756
|
+
) {
|
|
757
|
+
node.dispatchEvent(ev);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Cross shadow-root boundaries — `parentNode` returns `null`
|
|
762
|
+
// at a ShadowRoot, so use `host` when present.
|
|
763
|
+
node =
|
|
764
|
+
(node as ShadowRoot).host ??
|
|
765
|
+
(node as Element).parentNode ??
|
|
766
|
+
null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Lazily construct a `DomHostSink` rooted at the host GL canvas
|
|
772
|
+
* (cursor mutations) and its parent (pinned-tooltip `<div>`).
|
|
773
|
+
* Returns `null` if the canvas has been detached.
|
|
774
|
+
*/
|
|
775
|
+
private _ensureHostSink(): DomHostSink | null {
|
|
776
|
+
if (this._hostSink) {
|
|
777
|
+
return this._hostSink;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const parent = this._hostGlCanvas?.parentElement;
|
|
781
|
+
if (!parent || !this._hostGlCanvas) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
this._hostSink = new DomHostSink(this._hostGlCanvas, parent);
|
|
786
|
+
return this._hostSink;
|
|
787
|
+
}
|
|
788
|
+
}
|