@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.
Files changed (258) hide show
  1. package/LICENSE.md +193 -0
  2. package/dist/cdn/perspective-viewer-charts.js +3 -0
  3. package/dist/cdn/perspective-viewer-charts.js.map +7 -0
  4. package/dist/esm/axis/axis-primitives.d.ts +24 -0
  5. package/dist/esm/axis/bar-axis.d.ts +51 -0
  6. package/dist/esm/axis/canvas.d.ts +24 -0
  7. package/dist/esm/axis/categorical-axis-core.d.ts +42 -0
  8. package/dist/esm/axis/categorical-axis.d.ts +27 -0
  9. package/dist/esm/axis/facet-chrome.d.ts +13 -0
  10. package/dist/esm/axis/label-geometry.d.ts +41 -0
  11. package/dist/esm/axis/legend.d.ts +44 -0
  12. package/dist/esm/axis/numeric-axis.d.ts +20 -0
  13. package/dist/esm/charts/candlestick/candlestick-build.d.ts +129 -0
  14. package/dist/esm/charts/candlestick/candlestick-interact.d.ts +10 -0
  15. package/dist/esm/charts/candlestick/candlestick-render.d.ts +24 -0
  16. package/dist/esm/charts/candlestick/candlestick.d.ts +144 -0
  17. package/dist/esm/charts/candlestick/glyphs/draw-candlesticks.d.ts +36 -0
  18. package/dist/esm/charts/candlestick/glyphs/draw-ohlc.d.ts +33 -0
  19. package/dist/esm/charts/canvas-types.d.ts +15 -0
  20. package/dist/esm/charts/cartesian/cartesian-build.d.ts +14 -0
  21. package/dist/esm/charts/cartesian/cartesian-interact.d.ts +20 -0
  22. package/dist/esm/charts/cartesian/cartesian-render.d.ts +26 -0
  23. package/dist/esm/charts/cartesian/cartesian.d.ts +239 -0
  24. package/dist/esm/charts/cartesian/glyph.d.ts +53 -0
  25. package/dist/esm/charts/cartesian/glyphs/density.d.ts +142 -0
  26. package/dist/esm/charts/cartesian/glyphs/lines.d.ts +23 -0
  27. package/dist/esm/charts/cartesian/glyphs/points.d.ts +24 -0
  28. package/dist/esm/charts/cartesian/label-interner.d.ts +21 -0
  29. package/dist/esm/charts/cartesian/tooltip-lines.d.ts +11 -0
  30. package/dist/esm/charts/chart-base.d.ts +402 -0
  31. package/dist/esm/charts/chart.d.ts +338 -0
  32. package/dist/esm/charts/common/band-layout.d.ts +32 -0
  33. package/dist/esm/charts/common/categorical-y-chart.d.ts +53 -0
  34. package/dist/esm/charts/common/category-axis-resolver.d.ts +90 -0
  35. package/dist/esm/charts/common/chrome-cache.d.ts +18 -0
  36. package/dist/esm/charts/common/draw-tooltip-box.d.ts +9 -0
  37. package/dist/esm/charts/common/leaf-color.d.ts +33 -0
  38. package/dist/esm/charts/common/node-store.d.ts +81 -0
  39. package/dist/esm/charts/common/tree-chart.d.ts +48 -0
  40. package/dist/esm/charts/common/tree-chrome.d.ts +31 -0
  41. package/dist/esm/charts/common/tree-data.d.ts +54 -0
  42. package/dist/esm/charts/common/visible-extent.d.ts +51 -0
  43. package/dist/esm/charts/heatmap/heatmap-build.d.ts +86 -0
  44. package/dist/esm/charts/heatmap/heatmap-interact.d.ts +19 -0
  45. package/dist/esm/charts/heatmap/heatmap-render.d.ts +19 -0
  46. package/dist/esm/charts/heatmap/heatmap-y-axis.d.ts +46 -0
  47. package/dist/esm/charts/heatmap/heatmap.d.ts +117 -0
  48. package/dist/esm/charts/map/map.d.ts +67 -0
  49. package/dist/esm/charts/registry.d.ts +14 -0
  50. package/dist/esm/charts/series/glyphs/draw-areas.d.ts +30 -0
  51. package/dist/esm/charts/series/glyphs/draw-bars.d.ts +15 -0
  52. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +34 -0
  53. package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +33 -0
  54. package/dist/esm/charts/series/series-build.d.ts +228 -0
  55. package/dist/esm/charts/series/series-interact.d.ts +35 -0
  56. package/dist/esm/charts/series/series-render.d.ts +41 -0
  57. package/dist/esm/charts/series/series-type.d.ts +49 -0
  58. package/dist/esm/charts/series/series.d.ts +317 -0
  59. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +7 -0
  60. package/dist/esm/charts/sunburst/sunburst-layout.d.ts +33 -0
  61. package/dist/esm/charts/sunburst/sunburst-render.d.ts +22 -0
  62. package/dist/esm/charts/sunburst/sunburst.d.ts +85 -0
  63. package/dist/esm/charts/treemap/treemap-interact.d.ts +12 -0
  64. package/dist/esm/charts/treemap/treemap-layout.d.ts +28 -0
  65. package/dist/esm/charts/treemap/treemap-render.d.ts +18 -0
  66. package/dist/esm/charts/treemap/treemap.d.ts +74 -0
  67. package/dist/esm/config.d.ts +27 -0
  68. package/dist/esm/data/lazy-row.d.ts +32 -0
  69. package/dist/esm/data/split-groups.d.ts +20 -0
  70. package/dist/esm/data/view-reader.d.ts +35 -0
  71. package/dist/esm/event-detail.d.ts +28 -0
  72. package/dist/esm/index.d.ts +3 -0
  73. package/dist/esm/interaction/hit-test.d.ts +30 -0
  74. package/dist/esm/interaction/host-sink-dom.d.ts +19 -0
  75. package/dist/esm/interaction/host-sink-message.d.ts +46 -0
  76. package/dist/esm/interaction/lazy-tooltip.d.ts +61 -0
  77. package/dist/esm/interaction/raw-event-forwarder.d.ts +27 -0
  78. package/dist/esm/interaction/spatial-grid.d.ts +15 -0
  79. package/dist/esm/interaction/tooltip-controller.d.ts +193 -0
  80. package/dist/esm/interaction/zoom-controller.d.ts +106 -0
  81. package/dist/esm/interaction/zoom-router.d.ts +48 -0
  82. package/dist/esm/layout/facet-grid.d.ts +126 -0
  83. package/dist/esm/layout/plot-layout.d.ts +104 -0
  84. package/dist/esm/layout/ticks.d.ts +17 -0
  85. package/dist/esm/map/mercator.d.ts +102 -0
  86. package/dist/esm/map/tile-cache.d.ts +38 -0
  87. package/dist/esm/map/tile-layer.d.ts +66 -0
  88. package/dist/esm/map/tile-loader.d.ts +52 -0
  89. package/dist/esm/map/tile-source.d.ts +66 -0
  90. package/dist/esm/perspective-viewer-charts.js +3 -0
  91. package/dist/esm/perspective-viewer-charts.js.map +7 -0
  92. package/dist/esm/plugin/charts.d.ts +40 -0
  93. package/dist/esm/plugin/plugin.d.ts +95 -0
  94. package/dist/esm/render/scheduler.d.ts +41 -0
  95. package/dist/esm/theme/gradient.d.ts +48 -0
  96. package/dist/esm/theme/palette.d.ts +13 -0
  97. package/dist/esm/theme/theme-snapshot.d.ts +7 -0
  98. package/dist/esm/theme/theme.d.ts +53 -0
  99. package/dist/esm/transport/protocol.d.ts +430 -0
  100. package/dist/esm/transport/renderer-transport.d.ts +201 -0
  101. package/dist/esm/utils/css.d.ts +1 -0
  102. package/dist/esm/utils/font-snapshot.d.ts +50 -0
  103. package/dist/esm/webgl/buffer-pool.d.ts +62 -0
  104. package/dist/esm/webgl/context-manager.d.ts +184 -0
  105. package/dist/esm/webgl/gradient-texture.d.ts +17 -0
  106. package/dist/esm/webgl/instanced-attrs.d.ts +44 -0
  107. package/dist/esm/webgl/plot-frame.d.ts +39 -0
  108. package/dist/esm/webgl/program-cache.d.ts +13 -0
  109. package/dist/esm/webgl/shader-manifest.d.ts +53 -0
  110. package/dist/esm/webgl/shader-registry.d.ts +22 -0
  111. package/dist/esm/worker/boot.d.ts +0 -0
  112. package/dist/esm/worker/dispatch.d.ts +9 -0
  113. package/dist/esm/worker/font-loader.d.ts +2 -0
  114. package/dist/esm/worker/renderer.worker.d.ts +115 -0
  115. package/dist/esm/worker/session-host.d.ts +26 -0
  116. package/package.json +47 -0
  117. package/src/css/perspective-viewer-charts.css +95 -0
  118. package/src/ts/axis/axis-primitives.ts +125 -0
  119. package/src/ts/axis/bar-axis.ts +345 -0
  120. package/src/ts/axis/canvas.ts +64 -0
  121. package/src/ts/axis/categorical-axis-core.ts +125 -0
  122. package/src/ts/axis/categorical-axis.ts +716 -0
  123. package/src/ts/axis/facet-chrome.ts +42 -0
  124. package/src/ts/axis/label-geometry.ts +188 -0
  125. package/src/ts/axis/legend.ts +218 -0
  126. package/src/ts/axis/numeric-axis.ts +353 -0
  127. package/src/ts/charts/candlestick/candlestick-build.ts +516 -0
  128. package/src/ts/charts/candlestick/candlestick-interact.ts +256 -0
  129. package/src/ts/charts/candlestick/candlestick-render.ts +387 -0
  130. package/src/ts/charts/candlestick/candlestick.ts +367 -0
  131. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +432 -0
  132. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +317 -0
  133. package/src/ts/charts/canvas-types.ts +30 -0
  134. package/src/ts/charts/cartesian/cartesian-build.ts +616 -0
  135. package/src/ts/charts/cartesian/cartesian-interact.ts +355 -0
  136. package/src/ts/charts/cartesian/cartesian-render.ts +948 -0
  137. package/src/ts/charts/cartesian/cartesian.ts +469 -0
  138. package/src/ts/charts/cartesian/glyph.ts +81 -0
  139. package/src/ts/charts/cartesian/glyphs/density.ts +1263 -0
  140. package/src/ts/charts/cartesian/glyphs/lines.ts +320 -0
  141. package/src/ts/charts/cartesian/glyphs/points.ts +239 -0
  142. package/src/ts/charts/cartesian/label-interner.ts +56 -0
  143. package/src/ts/charts/cartesian/tooltip-lines.ts +80 -0
  144. package/src/ts/charts/chart-base.ts +840 -0
  145. package/src/ts/charts/chart.ts +427 -0
  146. package/src/ts/charts/common/band-layout.ts +63 -0
  147. package/src/ts/charts/common/categorical-y-chart.ts +81 -0
  148. package/src/ts/charts/common/category-axis-resolver.ts +314 -0
  149. package/src/ts/charts/common/chrome-cache.ts +79 -0
  150. package/src/ts/charts/common/draw-tooltip-box.ts +84 -0
  151. package/src/ts/charts/common/leaf-color.ts +92 -0
  152. package/src/ts/charts/common/node-store.ts +235 -0
  153. package/src/ts/charts/common/tree-chart.ts +76 -0
  154. package/src/ts/charts/common/tree-chrome.ts +123 -0
  155. package/src/ts/charts/common/tree-data.ts +623 -0
  156. package/src/ts/charts/common/visible-extent.ts +112 -0
  157. package/src/ts/charts/heatmap/heatmap-build.ts +426 -0
  158. package/src/ts/charts/heatmap/heatmap-interact.ts +274 -0
  159. package/src/ts/charts/heatmap/heatmap-render.ts +815 -0
  160. package/src/ts/charts/heatmap/heatmap-y-axis.ts +351 -0
  161. package/src/ts/charts/heatmap/heatmap.ts +368 -0
  162. package/src/ts/charts/map/map.ts +201 -0
  163. package/src/ts/charts/registry.ts +65 -0
  164. package/src/ts/charts/series/glyphs/draw-areas.ts +331 -0
  165. package/src/ts/charts/series/glyphs/draw-bars.ts +113 -0
  166. package/src/ts/charts/series/glyphs/draw-lines.ts +320 -0
  167. package/src/ts/charts/series/glyphs/draw-scatter.ts +328 -0
  168. package/src/ts/charts/series/series-build.ts +848 -0
  169. package/src/ts/charts/series/series-interact.ts +604 -0
  170. package/src/ts/charts/series/series-render.ts +1109 -0
  171. package/src/ts/charts/series/series-type.ts +99 -0
  172. package/src/ts/charts/series/series.ts +794 -0
  173. package/src/ts/charts/sunburst/sunburst-interact.ts +460 -0
  174. package/src/ts/charts/sunburst/sunburst-layout.ts +238 -0
  175. package/src/ts/charts/sunburst/sunburst-render.ts +887 -0
  176. package/src/ts/charts/sunburst/sunburst.ts +248 -0
  177. package/src/ts/charts/treemap/treemap-interact.ts +445 -0
  178. package/src/ts/charts/treemap/treemap-layout.ts +328 -0
  179. package/src/ts/charts/treemap/treemap-render.ts +886 -0
  180. package/src/ts/charts/treemap/treemap.ts +247 -0
  181. package/src/ts/config.ts +41 -0
  182. package/src/ts/data/lazy-row.ts +140 -0
  183. package/src/ts/data/split-groups.ts +97 -0
  184. package/src/ts/data/view-reader.ts +107 -0
  185. package/src/ts/event-detail.ts +44 -0
  186. package/src/ts/index.ts +53 -0
  187. package/src/ts/interaction/hit-test.ts +106 -0
  188. package/src/ts/interaction/host-sink-dom.ts +85 -0
  189. package/src/ts/interaction/host-sink-message.ts +75 -0
  190. package/src/ts/interaction/lazy-tooltip.ts +102 -0
  191. package/src/ts/interaction/raw-event-forwarder.ts +175 -0
  192. package/src/ts/interaction/spatial-grid.ts +100 -0
  193. package/src/ts/interaction/tooltip-controller.ts +407 -0
  194. package/src/ts/interaction/zoom-controller.ts +468 -0
  195. package/src/ts/interaction/zoom-router.ts +230 -0
  196. package/src/ts/layout/facet-grid.ts +346 -0
  197. package/src/ts/layout/plot-layout.ts +277 -0
  198. package/src/ts/layout/ticks.ts +168 -0
  199. package/src/ts/map/mercator.ts +204 -0
  200. package/src/ts/map/tile-cache.ts +96 -0
  201. package/src/ts/map/tile-layer.ts +382 -0
  202. package/src/ts/map/tile-loader.ts +143 -0
  203. package/src/ts/map/tile-source.ts +156 -0
  204. package/src/ts/plugin/charts.ts +286 -0
  205. package/src/ts/plugin/plugin.ts +668 -0
  206. package/src/ts/render/scheduler.ts +339 -0
  207. package/src/ts/shaders/area.frag.glsl +20 -0
  208. package/src/ts/shaders/area.vert.glsl +19 -0
  209. package/src/ts/shaders/bar.frag.glsl +25 -0
  210. package/src/ts/shaders/bar.vert.glsl +60 -0
  211. package/src/ts/shaders/candlestick-body.frag.glsl +19 -0
  212. package/src/ts/shaders/candlestick-body.vert.glsl +34 -0
  213. package/src/ts/shaders/density-extreme.frag.glsl +30 -0
  214. package/src/ts/shaders/density-mrt.frag.glsl +44 -0
  215. package/src/ts/shaders/density-mrt.vert.glsl +48 -0
  216. package/src/ts/shaders/density-resolve.frag.glsl +89 -0
  217. package/src/ts/shaders/density-resolve.vert.glsl +23 -0
  218. package/src/ts/shaders/density-splat.frag.glsl +34 -0
  219. package/src/ts/shaders/density-splat.vert.glsl +52 -0
  220. package/src/ts/shaders/gridline.frag.glsl +18 -0
  221. package/src/ts/shaders/gridline.vert.glsl +18 -0
  222. package/src/ts/shaders/heatmap.frag.glsl +23 -0
  223. package/src/ts/shaders/heatmap.vert.glsl +42 -0
  224. package/src/ts/shaders/line-uniform.frag.glsl +26 -0
  225. package/src/ts/shaders/line-uniform.vert.glsl +54 -0
  226. package/src/ts/shaders/line.frag.glsl +28 -0
  227. package/src/ts/shaders/line.vert.glsl +87 -0
  228. package/src/ts/shaders/scatter.frag.glsl +39 -0
  229. package/src/ts/shaders/scatter.vert.glsl +67 -0
  230. package/src/ts/shaders/sunburst-arc.frag.glsl +19 -0
  231. package/src/ts/shaders/sunburst-arc.vert.glsl +79 -0
  232. package/src/ts/shaders/tile.frag.glsl +27 -0
  233. package/src/ts/shaders/tile.vert.glsl +35 -0
  234. package/src/ts/shaders/treemap.frag.glsl +19 -0
  235. package/src/ts/shaders/treemap.vert.glsl +25 -0
  236. package/src/ts/shaders/y-scatter.frag.glsl +30 -0
  237. package/src/ts/shaders/y-scatter.vert.glsl +31 -0
  238. package/src/ts/theme/gradient.ts +312 -0
  239. package/src/ts/theme/palette.ts +64 -0
  240. package/src/ts/theme/theme-snapshot.ts +66 -0
  241. package/src/ts/theme/theme.ts +166 -0
  242. package/src/ts/transport/protocol.ts +497 -0
  243. package/src/ts/transport/renderer-transport.ts +788 -0
  244. package/src/ts/utils/css.ts +36 -0
  245. package/src/ts/utils/font-snapshot.ts +159 -0
  246. package/src/ts/webgl/buffer-pool.ts +163 -0
  247. package/src/ts/webgl/context-manager.ts +414 -0
  248. package/src/ts/webgl/gradient-texture.ts +84 -0
  249. package/src/ts/webgl/instanced-attrs.ts +139 -0
  250. package/src/ts/webgl/plot-frame.ts +91 -0
  251. package/src/ts/webgl/program-cache.ts +46 -0
  252. package/src/ts/webgl/shader-manifest.ts +148 -0
  253. package/src/ts/webgl/shader-registry.ts +97 -0
  254. package/src/ts/worker/boot.ts +22 -0
  255. package/src/ts/worker/dispatch.ts +99 -0
  256. package/src/ts/worker/font-loader.ts +89 -0
  257. package/src/ts/worker/renderer.worker.ts +734 -0
  258. 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
+ }