@jbrowse/render-core 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 (164) hide show
  1. package/LICENSE +201 -0
  2. package/esm/OverlayCanvas.d.ts +29 -0
  3. package/esm/OverlayCanvas.js +43 -0
  4. package/esm/RenderCanvas.d.ts +76 -0
  5. package/esm/RenderCanvas.js +38 -0
  6. package/esm/RenderLifecycleMixin.d.ts +180 -0
  7. package/esm/RenderLifecycleMixin.js +279 -0
  8. package/esm/ScrollLockedOverlay.d.ts +28 -0
  9. package/esm/ScrollLockedOverlay.js +42 -0
  10. package/esm/blockClipUtils.d.ts +27 -0
  11. package/esm/blockClipUtils.js +101 -0
  12. package/esm/canvas2dUtils.d.ts +372 -0
  13. package/esm/canvas2dUtils.js +564 -0
  14. package/esm/canvasContext.d.ts +68 -0
  15. package/esm/canvasContext.js +74 -0
  16. package/esm/colorRampLut.d.ts +17 -0
  17. package/esm/colorRampLut.js +25 -0
  18. package/esm/coverageBand.d.ts +107 -0
  19. package/esm/coverageBand.js +109 -0
  20. package/esm/coverageBandBuffers.d.ts +30 -0
  21. package/esm/coverageBandBuffers.js +13 -0
  22. package/esm/createRenderingBackend.d.ts +52 -0
  23. package/esm/createRenderingBackend.js +58 -0
  24. package/esm/displayPhase.d.ts +155 -0
  25. package/esm/displayPhase.js +97 -0
  26. package/esm/globalRenderingBackend.d.ts +90 -0
  27. package/esm/globalRenderingBackend.js +53 -0
  28. package/esm/gpuDevice.d.ts +91 -0
  29. package/esm/gpuDevice.js +277 -0
  30. package/esm/graphicsCapabilities.d.ts +98 -0
  31. package/esm/graphicsCapabilities.js +165 -0
  32. package/esm/hal/createHal.d.ts +22 -0
  33. package/esm/hal/createHal.js +91 -0
  34. package/esm/hal/deviceGpuCache.d.ts +36 -0
  35. package/esm/hal/deviceGpuCache.js +120 -0
  36. package/esm/hal/gpuHalBase.d.ts +55 -0
  37. package/esm/hal/gpuHalBase.js +81 -0
  38. package/esm/hal/index.d.ts +7 -0
  39. package/esm/hal/index.js +5 -0
  40. package/esm/hal/mockHal.d.ts +153 -0
  41. package/esm/hal/mockHal.js +297 -0
  42. package/esm/hal/oomReporter.d.ts +15 -0
  43. package/esm/hal/oomReporter.js +22 -0
  44. package/esm/hal/passIds.d.ts +22 -0
  45. package/esm/hal/passIds.js +36 -0
  46. package/esm/hal/regionRegistry.d.ts +11 -0
  47. package/esm/hal/regionRegistry.js +66 -0
  48. package/esm/hal/types.d.ts +116 -0
  49. package/esm/hal/types.js +1 -0
  50. package/esm/hal/webgl2Hal.d.ts +47 -0
  51. package/esm/hal/webgl2Hal.js +490 -0
  52. package/esm/hal/webgpuHal.d.ts +169 -0
  53. package/esm/hal/webgpuHal.js +722 -0
  54. package/esm/installUpload.d.ts +91 -0
  55. package/esm/installUpload.js +94 -0
  56. package/esm/instanceCache.d.ts +43 -0
  57. package/esm/instanceCache.js +53 -0
  58. package/esm/instancePass.d.ts +50 -0
  59. package/esm/instancePass.js +34 -0
  60. package/esm/keyedRenderingBackend.d.ts +41 -0
  61. package/esm/keyedRenderingBackend.js +19 -0
  62. package/esm/mapUploadSync.d.ts +31 -0
  63. package/esm/mapUploadSync.js +45 -0
  64. package/esm/namedReactions.d.ts +29 -0
  65. package/esm/namedReactions.js +62 -0
  66. package/esm/perRegionRenderingBackend.d.ts +114 -0
  67. package/esm/perRegionRenderingBackend.js +93 -0
  68. package/esm/recoveryBudget.d.ts +40 -0
  69. package/esm/recoveryBudget.js +56 -0
  70. package/esm/regionDataMap.d.ts +60 -0
  71. package/esm/regionDataMap.js +98 -0
  72. package/esm/renderBlock.d.ts +25 -0
  73. package/esm/renderBlock.js +10 -0
  74. package/esm/renderingBackendBase.d.ts +71 -0
  75. package/esm/renderingBackendBase.js +50 -0
  76. package/esm/shaders/antialias.js.generated.d.ts +4 -0
  77. package/esm/shaders/antialias.js.generated.js +25 -0
  78. package/esm/shaders/capsule.generated.d.ts +1 -0
  79. package/esm/shaders/capsule.generated.js +3 -0
  80. package/esm/shaders/colorRampLut.generated.d.ts +1 -0
  81. package/esm/shaders/colorRampLut.generated.js +3 -0
  82. package/esm/shaders/coverageBand.generated.d.ts +2 -0
  83. package/esm/shaders/coverageBand.generated.js +4 -0
  84. package/esm/shaders/coverageBar.generated.d.ts +4 -0
  85. package/esm/shaders/coverageBar.generated.js +6 -0
  86. package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
  87. package/esm/shaders/coverageBar.iface.generated.js +114 -0
  88. package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
  89. package/esm/shaders/coverageIndicator.generated.js +6 -0
  90. package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
  91. package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
  92. package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
  93. package/esm/shaders/coverageInterbase.generated.js +6 -0
  94. package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
  95. package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
  96. package/esm/shaders/coverageMod.generated.d.ts +4 -0
  97. package/esm/shaders/coverageMod.generated.js +6 -0
  98. package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
  99. package/esm/shaders/coverageMod.iface.generated.js +144 -0
  100. package/esm/shaders/coverageSnp.generated.d.ts +4 -0
  101. package/esm/shaders/coverageSnp.generated.js +6 -0
  102. package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
  103. package/esm/shaders/coverageSnp.iface.generated.js +144 -0
  104. package/esm/shaders/hpmath.js.generated.d.ts +3 -0
  105. package/esm/shaders/hpmath.js.generated.js +48 -0
  106. package/esm/shaders/pointGlyph.generated.d.ts +1 -0
  107. package/esm/shaders/pointGlyph.generated.js +3 -0
  108. package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
  109. package/esm/shaders/pointGlyph.js.generated.js +7 -0
  110. package/esm/shaders/rowRect.generated.d.ts +3 -0
  111. package/esm/shaders/rowRect.generated.js +5 -0
  112. package/esm/shaders/rowRect.js.generated.d.ts +2 -0
  113. package/esm/shaders/rowRect.js.generated.js +13 -0
  114. package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
  115. package/esm/shaders/scoreScale.js.generated.js +46 -0
  116. package/esm/slangPass.d.ts +49 -0
  117. package/esm/slangPass.js +22 -0
  118. package/esm/useEventCallback.d.ts +1 -0
  119. package/esm/useEventCallback.js +19 -0
  120. package/esm/useGraphicsCapabilities.d.ts +17 -0
  121. package/esm/useGraphicsCapabilities.js +38 -0
  122. package/esm/useRenderingBackend.d.ts +83 -0
  123. package/esm/useRenderingBackend.js +422 -0
  124. package/esm/useTabVisibilityRerender.d.ts +13 -0
  125. package/esm/useTabVisibilityRerender.js +39 -0
  126. package/esm/webgpuUtils.d.ts +23 -0
  127. package/esm/webgpuUtils.js +76 -0
  128. package/package.json +91 -0
  129. package/src/shaders/antialias.js.generated.ts +33 -0
  130. package/src/shaders/antialias.slang +112 -0
  131. package/src/shaders/capsule.generated.ts +4 -0
  132. package/src/shaders/capsule.slang +62 -0
  133. package/src/shaders/colorPack.slang +26 -0
  134. package/src/shaders/colorRampLut.generated.ts +4 -0
  135. package/src/shaders/colorRampLut.slang +51 -0
  136. package/src/shaders/coverageBand.generated.ts +6 -0
  137. package/src/shaders/coverageBand.slang +276 -0
  138. package/src/shaders/coverageBar.generated.ts +10 -0
  139. package/src/shaders/coverageBar.iface.generated.ts +172 -0
  140. package/src/shaders/coverageBar.slang +37 -0
  141. package/src/shaders/coverageIndicator.generated.ts +10 -0
  142. package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
  143. package/src/shaders/coverageIndicator.slang +61 -0
  144. package/src/shaders/coverageInterbase.generated.ts +10 -0
  145. package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
  146. package/src/shaders/coverageInterbase.slang +63 -0
  147. package/src/shaders/coverageMod.generated.ts +10 -0
  148. package/src/shaders/coverageMod.iface.generated.ts +211 -0
  149. package/src/shaders/coverageMod.slang +36 -0
  150. package/src/shaders/coverageSnp.generated.ts +10 -0
  151. package/src/shaders/coverageSnp.iface.generated.ts +211 -0
  152. package/src/shaders/coverageSnp.slang +52 -0
  153. package/src/shaders/diagonalGrid.slang +25 -0
  154. package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
  155. package/src/shaders/hpmath.js.generated.ts +47 -0
  156. package/src/shaders/hpmath.slang +226 -0
  157. package/src/shaders/pointGlyph.generated.ts +4 -0
  158. package/src/shaders/pointGlyph.js.generated.ts +9 -0
  159. package/src/shaders/pointGlyph.slang +93 -0
  160. package/src/shaders/rowRect.generated.ts +8 -0
  161. package/src/shaders/rowRect.js.generated.ts +17 -0
  162. package/src/shaders/rowRect.slang +160 -0
  163. package/src/shaders/scoreScale.js.generated.ts +51 -0
  164. package/src/shaders/scoreScale.slang +68 -0
@@ -0,0 +1,279 @@
1
+ import { types } from '@jbrowse/mobx-state-tree';
2
+ import { namedAutorun } from './namedReactions.js';
3
+ /**
4
+ * Owns the GPU draw lifecycle for any display that paints to a canvas.
5
+ *
6
+ * Plugins compose this mixin (directly or via `MultiRegionDisplayMixin` /
7
+ * `GlobalFetchMixin`) and call
8
+ * `self.attachRenderingBackend(backend, () => ({ upload, render }))` from their
9
+ * own `startRenderingBackend(backend)` action. **The second argument is a thunk
10
+ * because it runs exactly once**, on the first attach: `startRenderingBackend`
11
+ * fires again on every context-loss recovery, and the autoruns keep the
12
+ * callbacks they were given first. Anything the callbacks close over — an
13
+ * upload sync's memo of what it last sent — is built inside it, so it lives as
14
+ * long as the callbacks that read it rather than being allocated per recovery
15
+ * and discarded. The mixin owns:
16
+ *
17
+ * - `canvasDrawn` — observable flag read by test-selector `data-testid` attributes to detect first paint.
18
+ * - `currentRenderingBackend` — the backend reference, updated on context-loss
19
+ * recovery. Autoruns read it each tick so they re-fire against the new
20
+ * one without being reinstalled.
21
+ * - `renderTick` — counter the render autorun observes; bumped by
22
+ * `renderNow()` (tab-visibility restore) and after every upload
23
+ * (ensures render re-fires when an upload happens but renderState
24
+ * identity stays stable).
25
+ * - `autorunsInstalled` — guards `attachRenderingBackend` so the autorun
26
+ * pair is spawned once per model instance, not once per backend
27
+ * assignment.
28
+ *
29
+ * The `upload` callback runs in one autorun, `render` in another. Inside
30
+ * each, every observable read is auto-tracked by MobX — no getter-layer
31
+ * indirection, no multi-entry config. `render` returns `true` when the
32
+ * backend actually painted content (flips `canvasDrawn`), `false` to skip
33
+ * this tick (e.g. `renderState` not yet computed or no regions loaded).
34
+ *
35
+ * #stateModel RenderLifecycleMixin
36
+ * #category display
37
+ * @see RenderLifecycleMixin.test.ts for the upload/render autorun behavior
38
+ */
39
+ export function RenderLifecycleMixin() {
40
+ return types
41
+ .model('RenderLifecycle', {})
42
+ .volatile(() => ({
43
+ /**
44
+ * #volatile
45
+ * flips true on first paint; read by test selectors to detect render
46
+ */
47
+ canvasDrawn: false,
48
+ /**
49
+ * #volatile
50
+ * current backend reference, updated on context-loss recovery.
51
+ * Typed `unknown` (not generic `B`) on purpose: this mixin is composed
52
+ * by every display via a non-generic factory, so the per-display backend
53
+ * type `B` isn't known here — it's supplied at `attachRenderingBackend<B>`
54
+ * and narrowed with `as B` inside the autoruns. Don't "fix" the cast.
55
+ */
56
+ currentRenderingBackend: undefined,
57
+ /**
58
+ * #volatile
59
+ * counter the render autorun observes; bumped to force a re-render
60
+ */
61
+ renderTick: 0,
62
+ /**
63
+ * #volatile
64
+ * guards attachRenderingBackend so the autorun pair spawns once per instance
65
+ */
66
+ autorunsInstalled: false,
67
+ /**
68
+ * #volatile
69
+ * the render-backend (GPU/Canvas2D init or context-loss) error, or
70
+ * undefined. Single source of truth for the render-error terminal state:
71
+ * `useRenderingBackend` writes it from the canvas-init mechanism so the
72
+ * model — not React-local hook state — owns every terminal state. Read by
73
+ * `displayPhase` (whose `renderError` term outranks `loading`, suppressing
74
+ * the scrim) and by `DisplayChrome` (shows the retry overlay).
75
+ */
76
+ renderError: undefined,
77
+ }))
78
+ .views(() => ({
79
+ /**
80
+ * #getter
81
+ * Overridable precondition (default true): both lifecycle autoruns skip
82
+ * their callback entirely while this is false, so a display never has to
83
+ * open its own `upload`/`render` with a readiness check.
84
+ *
85
+ * The LGV mixins override it with `view.initialized`, because before the
86
+ * view is measured its geometry throws by design (`view.width`, and so
87
+ * `visibleRegions` / `trackWidthPx` with it) and a throw in either callback
88
+ * is routed to `renderError` — surfacing "not measured yet" as the GPU
89
+ * render-error banner. Because it's an observable read inside the autoruns,
90
+ * the pair re-fires the moment it flips. This is the *precondition* axis
91
+ * only: whether any data has arrived stays the render callback's own gate
92
+ * (return `false` to skip a tick), which is why `renderState` getters can
93
+ * be plain resolved values.
94
+ */
95
+ get canRender() {
96
+ return true;
97
+ },
98
+ /**
99
+ * #getter
100
+ * Overridable hook (default true): whether this display paints a canvas
101
+ * in its **current** configuration, as opposed to a deliberate static
102
+ * placeholder (LD with the triangle off, sequence past base resolution —
103
+ * both render a message where the `<canvas>` would go, so `canvasRef` is
104
+ * never called and `canvasDrawn` can never flip).
105
+ *
106
+ * Lives here, beside `canvasDrawn`, because every consumer of "has this
107
+ * display painted" needs the pair — and until 2026-08 each family
108
+ * declared its own copy (per-region hard-coded `true`, global carried the
109
+ * hook for LD), so a display could express the state only to whichever
110
+ * family it happened to compose. See `painted` below for the reader that
111
+ * was missed.
112
+ */
113
+ get rendersCanvas() {
114
+ return true;
115
+ },
116
+ /**
117
+ * #getter
118
+ * Overridable hook (default false): the display has reached a state it
119
+ * will not paint its way out of, so `painted` below should answer
120
+ * *finished* rather than *pending*. Both LGV fetch families fill it with
121
+ * `!!error` — a fetch that failed before first paint keeps its canvas
122
+ * mounted, since the error bar is an overlay rather than a subtree
123
+ * replacement, so nothing ever draws into it. Named for `fetchInert` on
124
+ * the comparative side.
125
+ *
126
+ * A hook rather than a read of `error` here, for two reasons that both
127
+ * bite: this package is a leaf and `error` belongs to the fetch mixins,
128
+ * and declaring that name here would *collide* with `FetchMixin`'s
129
+ * volatile — `types.compose` gives the collision to its later argument,
130
+ * and the two families compose the two mixins in opposite orders.
131
+ */
132
+ get paintInert() {
133
+ return false;
134
+ },
135
+ }))
136
+ .views(self => ({
137
+ /**
138
+ * #getter
139
+ * **The first-paint answer every consumer outside the display should
140
+ * read**, `canvasDrawn` being only the raw flag: a display that is
141
+ * deliberately not painting a canvas has finished, and saying otherwise
142
+ * is a lie that never resolves.
143
+ *
144
+ * The two `rendersCanvas: false` states each had three of their four
145
+ * consumers wired by hand — the loading scrim (`rendersCanvas` /
146
+ * `fetchInert`) and the SVG export (`fetchInert`) —
147
+ * while the fourth, `data-display-drawn`, went on publishing `"false"`
148
+ * forever off the raw flag. That attribute is what `PENDING_DISPLAYS`
149
+ * (`@jbrowse/browser-test-utils`) selects on, so a zoomed-out reference
150
+ * sequence track made every `waitForDisplaysDone` on the page burn its
151
+ * full timeout — silently, since that wait swallows its own. Same shape
152
+ * as `fetchInert` on the comparative side: the reader you forget is the
153
+ * one outside the display, so the display has to publish one name for it.
154
+ *
155
+ * `paintInert` is the third term and the same argument once more, for the
156
+ * state where a display *would* paint a canvas and never gets to — a fetch
157
+ * that failed before first paint. See that hook.
158
+ */
159
+ get painted() {
160
+ return self.canvasDrawn || !self.rendersCanvas || self.paintInert;
161
+ },
162
+ }))
163
+ .actions(self => ({
164
+ /**
165
+ * #action
166
+ */
167
+ markCanvasDrawn() {
168
+ if (!self.canvasDrawn) {
169
+ self.canvasDrawn = true;
170
+ }
171
+ },
172
+ /**
173
+ * #action
174
+ */
175
+ resetCanvasDrawn() {
176
+ if (self.canvasDrawn) {
177
+ self.canvasDrawn = false;
178
+ }
179
+ },
180
+ /**
181
+ * #action
182
+ */
183
+ stopRenderingBackend() {
184
+ self.currentRenderingBackend = undefined;
185
+ self.canvasDrawn = false;
186
+ },
187
+ /**
188
+ * #action
189
+ */
190
+ renderNow() {
191
+ self.renderTick += 1;
192
+ },
193
+ /**
194
+ * #action
195
+ * set/clear the render-backend error. Called by `useRenderingBackend`:
196
+ * with the error when the canvas factory rejects (or context-loss
197
+ * re-init fails), and with `undefined` on successful (re)init and on
198
+ * retry.
199
+ */
200
+ setRenderError(error) {
201
+ // Reference-identity guard, not value-equality: the render catch passes
202
+ // a fresh Error object per throw, so a repeatedly-failing render still
203
+ // reassigns each time (cosmetic MST churn). Harmless — nothing reads
204
+ // renderError inside a hot autorun — but the guard does dedupe the
205
+ // common set-then-clear-with-same-value (undefined) transitions.
206
+ if (self.renderError !== error) {
207
+ self.renderError = error;
208
+ }
209
+ },
210
+ }))
211
+ .actions(self => ({
212
+ /**
213
+ * #action
214
+ * attach a GPU/Canvas2D backend and install the upload + render autorun
215
+ * pair. Idempotent: re-calling swaps the backend and does not run `setup`
216
+ * again, so the callbacks and everything they close over are the first
217
+ * call's.
218
+ */
219
+ attachRenderingBackend(backend, setup) {
220
+ self.currentRenderingBackend = backend;
221
+ if (self.autorunsInstalled) {
222
+ return;
223
+ }
224
+ // Before the flag, so a throw in `setup` leaves the display
225
+ // un-installed and the next backend can try again.
226
+ const cbs = setup();
227
+ self.autorunsInstalled = true;
228
+ namedAutorun(self, () => {
229
+ const b = self.currentRenderingBackend;
230
+ if (b === undefined || !self.canRender) {
231
+ return;
232
+ }
233
+ // A throw in the upload callback (malformed worker data, a bad
234
+ // buffer-packing edge case) is routed to `renderError` for the
235
+ // same reason as the render autorun below: an uncaught reaction
236
+ // error would skip `renderNow()`, never flip `canvasDrawn`, and
237
+ // strand the display on 'loading' forever with no retry. For
238
+ // DisplayChrome consumers setting `renderError` unmounts the
239
+ // canvas and disposes the backend, so this can't re-fire into a
240
+ // loop; a shared-canvas consumer (dotplot, synteny) keeps the
241
+ // canvas mounted through an error (ADR-025), so there the only
242
+ // pacing is that autoruns re-fire on dep changes — a
243
+ // deterministically-throwing callback re-throws once per change,
244
+ // each landing on `setRenderError`'s identity guard.
245
+ try {
246
+ if (cbs.upload(b)) {
247
+ self.renderNow();
248
+ }
249
+ }
250
+ catch (e) {
251
+ self.setRenderError(e);
252
+ }
253
+ }, { name: 'RenderLifecycle:upload' });
254
+ namedAutorun(self, () => {
255
+ const b = self.currentRenderingBackend;
256
+ void self.renderTick;
257
+ if (b === undefined || !self.canRender) {
258
+ return;
259
+ }
260
+ // A throw in the render callback (e.g. an invalid render-state
261
+ // input like an empty rendering type) would otherwise escape as
262
+ // an uncaught reaction error and leave the display stuck on
263
+ // "loading" forever. Route it to `renderError` so it surfaces as
264
+ // the render-error overlay (with the message + retry) instead.
265
+ // Same loop caveat as the upload autorun above: the
266
+ // unmount-and-dispose that prevents re-fire is DisplayChrome's,
267
+ // not a shared canvas's.
268
+ try {
269
+ if (cbs.render(b)) {
270
+ self.markCanvasDrawn();
271
+ }
272
+ }
273
+ catch (e) {
274
+ self.setRenderError(e);
275
+ }
276
+ }, { name: 'RenderLifecycle:render' });
277
+ },
278
+ }));
279
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ /**
3
+ * DOM overlay layer for a display that scrolls a fixed GPU canvas virtually (a
4
+ * `VerticalScrollbar` overlay + `useVirtualScrollWheel`, everything positioned
5
+ * from `model.scrollTop`).
6
+ *
7
+ * The GPU canvas paints glyphs at `y - model.scrollTop`. Any DOM overlay
8
+ * (labels, highlights, hover boxes) that annotates those glyphs must derive its
9
+ * screen Y from the SAME `model.scrollTop`, or it drifts away from its glyph on
10
+ * scroll. This component clips to the viewport and translates its children by
11
+ * `-scrollTop` so they track the canvas exactly — one scroll source, no
12
+ * tearing.
13
+ *
14
+ * Render it as an absolute sibling of the canvas inside the display's
15
+ * position:relative container. Children position in raw content coordinates (y
16
+ * from 0..contentHeight); this applies the `-scrollTop` shift. `pointerEvents`
17
+ * is none so mouse events fall through to the canvas; interactive children
18
+ * re-enable `pointerEvents:auto`.
19
+ */
20
+ export declare function ScrollLockedOverlay({ scrollTop, viewportHeight, contentHeight, children, }: {
21
+ /** the model's scroll offset — the same value the GPU shader paints from */
22
+ scrollTop: number;
23
+ /** the visible viewport height (clips overflow) */
24
+ viewportHeight: number;
25
+ /** the full scrollable content height (the children's coordinate space) */
26
+ contentHeight: number;
27
+ children: React.ReactNode;
28
+ }): React.JSX.Element;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ /**
4
+ * DOM overlay layer for a display that scrolls a fixed GPU canvas virtually (a
5
+ * `VerticalScrollbar` overlay + `useVirtualScrollWheel`, everything positioned
6
+ * from `model.scrollTop`).
7
+ *
8
+ * The GPU canvas paints glyphs at `y - model.scrollTop`. Any DOM overlay
9
+ * (labels, highlights, hover boxes) that annotates those glyphs must derive its
10
+ * screen Y from the SAME `model.scrollTop`, or it drifts away from its glyph on
11
+ * scroll. This component clips to the viewport and translates its children by
12
+ * `-scrollTop` so they track the canvas exactly — one scroll source, no
13
+ * tearing.
14
+ *
15
+ * Render it as an absolute sibling of the canvas inside the display's
16
+ * position:relative container. Children position in raw content coordinates (y
17
+ * from 0..contentHeight); this applies the `-scrollTop` shift. `pointerEvents`
18
+ * is none so mouse events fall through to the canvas; interactive children
19
+ * re-enable `pointerEvents:auto`.
20
+ */
21
+ export function ScrollLockedOverlay({ scrollTop, viewportHeight, contentHeight, children, }) {
22
+ return (_jsx("div", { style: {
23
+ position: 'absolute',
24
+ top: 0,
25
+ left: 0,
26
+ width: '100%',
27
+ height: viewportHeight,
28
+ overflow: 'hidden',
29
+ pointerEvents: 'none',
30
+ }, children: _jsx("div", {
31
+ // Sized to the full content height so absolutely-positioned children
32
+ // keep a stable coordinate space; the transform shifts them by
33
+ // -scrollTop (and, being a transform, makes this their containing block).
34
+ style: {
35
+ position: 'absolute',
36
+ top: 0,
37
+ left: 0,
38
+ width: '100%',
39
+ height: contentHeight,
40
+ transform: `translateY(${-scrollTop}px)`,
41
+ }, children: children }) }));
42
+ }
@@ -0,0 +1,27 @@
1
+ import type { CanvasScale } from './canvas2dUtils.ts';
2
+ import type { BpRegionBounds } from './renderBlock.ts';
3
+ export declare function splitPositionWithFrac(value: number): [number, number];
4
+ export interface BlockClipResult {
5
+ scissorX: number;
6
+ scissorW: number;
7
+ pxX: number;
8
+ pxW: number;
9
+ pxH: number;
10
+ /**
11
+ * The vertical scale the backing store ACTUALLY got — `hal.resize`'s answer,
12
+ * not `getDpr()`. Carried through so a renderer scissoring bands out of one
13
+ * canvas (`devicePxBand`) has the ratio the clip was built with rather than
14
+ * reconstructing it as `pxH / canvasHeight`, which is a division by a height
15
+ * that can be 0.
16
+ */
17
+ scaleY: number;
18
+ bpStartHi: number;
19
+ bpStartLo: number;
20
+ bpEndHi: number;
21
+ bpEndLo: number;
22
+ clippedLengthBp: number;
23
+ bpPerPx: number;
24
+ }
25
+ export declare function bpRangeXTuple(clip: BlockClipResult, reversed: boolean): [number, number, number];
26
+ export declare function writeBpRangeUniforms(uniformF32: Float32Array, offsetF32: number, clip: BlockClipResult, reversed: boolean): void;
27
+ export declare function clipBlock(block: BpRegionBounds, canvasWidth: number, canvasHeight: number, scale: CanvasScale): BlockClipResult | null;
@@ -0,0 +1,101 @@
1
+ import { clampBlockScissor, devicePxSpan } from './canvas2dUtils.js';
2
+ // hp-math split: factor a bp position into a (hi, lo) pair safe to feed
3
+ // shaders as float32 (cumulative-bp coordinates can exceed 2^31, so a plain
4
+ // `intValue & 0xfff` would wrap). Float64 modulo handles the full 2^53
5
+ // safe range. The CPU counterpart of the shader-side `hpSplitUint` in
6
+ // shaders/hpmath.slang — but not a byte-for-byte mirror: `hpSplitUint` masks a
7
+ // `uint`, whereas this also carries the fractional part and tolerates
8
+ // cumulative-bp beyond 2^31. Renderer-side uniform writes use this to mirror
9
+ // the shader's hi/lo coordinate space.
10
+ export function splitPositionWithFrac(value) {
11
+ const intValue = Math.floor(value);
12
+ const frac = value - intValue;
13
+ // Genomic positions are non-negative, so % matches Math.floor(intValue/4096)*4096
14
+ // pattern while making the modulo intent explicit.
15
+ const loInt = intValue % 4096;
16
+ const hi = intValue - loInt;
17
+ const lo = loInt + frac;
18
+ return [hi, lo];
19
+ }
20
+ // [hi, lo, ±clippedLengthBp] — the first three floats of the HP uniform buffer
21
+ // used by hpmath.slang's hpToClipX. Reversed blocks pivot on bpEnd with a
22
+ // negated length.
23
+ export function bpRangeXTuple(clip, reversed) {
24
+ return reversed
25
+ ? [clip.bpEndHi, clip.bpEndLo, -clip.clippedLengthBp]
26
+ : [clip.bpStartHi, clip.bpStartLo, clip.clippedLengthBp];
27
+ }
28
+ // Poke the hp-math bpRangeX tuple (hi, lo, ±clippedLengthBp) into the uniform
29
+ // buffer at `offsetF32` — pass the shader's `UNIFORM_OFFSET_F32.bpRangeX`.
30
+ //
31
+ // **The offset-poke form, for a renderer that writes its uniforms
32
+ // incrementally.** A renderer that sets the whole block in one place says
33
+ // `bpRangeX: bpRangeXTuple(clip, reversed)` inside the generated
34
+ // `writeUniforms` instead, which is every genome-mapped renderer in tree today.
35
+ // Either way the point is the same and is the reason neither is spelled out at
36
+ // the call site: the reversed-block pivot and the hi/lo split live in one place,
37
+ // and a hand-rolled `uniformF32[U.bpRangeX + n] = …` triple is easy to get
38
+ // subtly wrong for reversed blocks.
39
+ export function writeBpRangeUniforms(uniformF32, offsetF32, clip, reversed) {
40
+ const [hi, lo, len] = bpRangeXTuple(clip, reversed);
41
+ uniformF32[offsetF32] = hi;
42
+ uniformF32[offsetF32 + 1] = lo;
43
+ uniformF32[offsetF32 + 2] = len;
44
+ }
45
+ export function clipBlock(block, canvasWidth, canvasHeight,
46
+ // The scale the backing store actually got, per axis (`hal.resize`), NOT
47
+ // `getDpr()`: only one axis clamps at a time, so past `MAX_CANVAS_DIM_PX` a
48
+ // canvas is 2x on one axis and less on the other.
49
+ scale) {
50
+ const clamp = clampBlockScissor(block.screenStartPx, block.screenEndPx, canvasWidth);
51
+ if (!clamp) {
52
+ return null;
53
+ }
54
+ const { scissorX, scissorEnd, scissorW } = clamp;
55
+ const fullBlockWidth = block.screenEndPx - block.screenStartPx;
56
+ const regionLengthBp = block.end - block.start;
57
+ // A degenerate block (no pixel span, or no bp span) still survives
58
+ // clampBlockScissor — floor/ceil widen a zero-width span straddling a pixel
59
+ // boundary to scissorW === 1. Dividing by it yields Infinity/NaN, which
60
+ // propagates through splitPositionWithFrac into every bpRangeX uniform and
61
+ // silently poisons the draw. Nothing is visible in a zero-width block, so
62
+ // skip it the same way a fully off-screen one is skipped.
63
+ if (fullBlockWidth <= 0 || regionLengthBp <= 0) {
64
+ return null;
65
+ }
66
+ const { start: pxX, width: pxW } = devicePxSpan(scissorX, scissorEnd, scale.x);
67
+ const pxH = Math.round(canvasHeight * scale.y);
68
+ const bpPerPx = regionLengthBp / fullBlockWidth;
69
+ // How much the clip cut off each SCREEN edge of the block.
70
+ const trimLeftPx = scissorX - block.screenStartPx;
71
+ const trimRightPx = block.screenEndPx - scissorEnd;
72
+ // `bpStart` is the LOW bp on both orientations and `bpEnd` the high one
73
+ // (`bpRangeXTuple` pivots on bpEnd when reversed), so each is pulled in by the
74
+ // trim on ITS OWN end — and which screen edge that is flips with the
75
+ // orientation, since a reversed block runs bp leftward from `end`. The two
76
+ // orientations agree exactly when nothing is trimmed, which is why the
77
+ // forward-only spelling survived — but `canvasWidth` is `trackWidthPx`, i.e.
78
+ // `view.width - 2` whenever track outlines are on (the default), so the
79
+ // rightmost block is clipped on every view and a reversed one had its whole
80
+ // painting shifted 2px against `makeBpMapper` (the Canvas2D painter and every
81
+ // hit-test). Another member of the reversed-block family in this package's
82
+ // CLAUDE.md: invisible forward, only ever wrong on a flipped view.
83
+ const clippedBpStart = block.start + (block.reversed ? trimRightPx : trimLeftPx) * bpPerPx;
84
+ const clippedBpEnd = block.end - (block.reversed ? trimLeftPx : trimRightPx) * bpPerPx;
85
+ const [bpStartHi, bpStartLo] = splitPositionWithFrac(clippedBpStart);
86
+ const [bpEndHi, bpEndLo] = splitPositionWithFrac(clippedBpEnd);
87
+ return {
88
+ scissorX,
89
+ scissorW,
90
+ pxX,
91
+ pxW,
92
+ pxH,
93
+ scaleY: scale.y,
94
+ bpStartHi,
95
+ bpStartLo,
96
+ bpEndHi,
97
+ bpEndLo,
98
+ clippedLengthBp: clippedBpEnd - clippedBpStart,
99
+ bpPerPx,
100
+ };
101
+ }