@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.
- package/LICENSE +201 -0
- package/esm/OverlayCanvas.d.ts +29 -0
- package/esm/OverlayCanvas.js +43 -0
- package/esm/RenderCanvas.d.ts +76 -0
- package/esm/RenderCanvas.js +38 -0
- package/esm/RenderLifecycleMixin.d.ts +180 -0
- package/esm/RenderLifecycleMixin.js +279 -0
- package/esm/ScrollLockedOverlay.d.ts +28 -0
- package/esm/ScrollLockedOverlay.js +42 -0
- package/esm/blockClipUtils.d.ts +27 -0
- package/esm/blockClipUtils.js +101 -0
- package/esm/canvas2dUtils.d.ts +372 -0
- package/esm/canvas2dUtils.js +564 -0
- package/esm/canvasContext.d.ts +68 -0
- package/esm/canvasContext.js +74 -0
- package/esm/colorRampLut.d.ts +17 -0
- package/esm/colorRampLut.js +25 -0
- package/esm/coverageBand.d.ts +107 -0
- package/esm/coverageBand.js +109 -0
- package/esm/coverageBandBuffers.d.ts +30 -0
- package/esm/coverageBandBuffers.js +13 -0
- package/esm/createRenderingBackend.d.ts +52 -0
- package/esm/createRenderingBackend.js +58 -0
- package/esm/displayPhase.d.ts +155 -0
- package/esm/displayPhase.js +97 -0
- package/esm/globalRenderingBackend.d.ts +90 -0
- package/esm/globalRenderingBackend.js +53 -0
- package/esm/gpuDevice.d.ts +91 -0
- package/esm/gpuDevice.js +277 -0
- package/esm/graphicsCapabilities.d.ts +98 -0
- package/esm/graphicsCapabilities.js +165 -0
- package/esm/hal/createHal.d.ts +22 -0
- package/esm/hal/createHal.js +91 -0
- package/esm/hal/deviceGpuCache.d.ts +36 -0
- package/esm/hal/deviceGpuCache.js +120 -0
- package/esm/hal/gpuHalBase.d.ts +55 -0
- package/esm/hal/gpuHalBase.js +81 -0
- package/esm/hal/index.d.ts +7 -0
- package/esm/hal/index.js +5 -0
- package/esm/hal/mockHal.d.ts +153 -0
- package/esm/hal/mockHal.js +297 -0
- package/esm/hal/oomReporter.d.ts +15 -0
- package/esm/hal/oomReporter.js +22 -0
- package/esm/hal/passIds.d.ts +22 -0
- package/esm/hal/passIds.js +36 -0
- package/esm/hal/regionRegistry.d.ts +11 -0
- package/esm/hal/regionRegistry.js +66 -0
- package/esm/hal/types.d.ts +116 -0
- package/esm/hal/types.js +1 -0
- package/esm/hal/webgl2Hal.d.ts +47 -0
- package/esm/hal/webgl2Hal.js +490 -0
- package/esm/hal/webgpuHal.d.ts +169 -0
- package/esm/hal/webgpuHal.js +722 -0
- package/esm/installUpload.d.ts +91 -0
- package/esm/installUpload.js +94 -0
- package/esm/instanceCache.d.ts +43 -0
- package/esm/instanceCache.js +53 -0
- package/esm/instancePass.d.ts +50 -0
- package/esm/instancePass.js +34 -0
- package/esm/keyedRenderingBackend.d.ts +41 -0
- package/esm/keyedRenderingBackend.js +19 -0
- package/esm/mapUploadSync.d.ts +31 -0
- package/esm/mapUploadSync.js +45 -0
- package/esm/namedReactions.d.ts +29 -0
- package/esm/namedReactions.js +62 -0
- package/esm/perRegionRenderingBackend.d.ts +114 -0
- package/esm/perRegionRenderingBackend.js +93 -0
- package/esm/recoveryBudget.d.ts +40 -0
- package/esm/recoveryBudget.js +56 -0
- package/esm/regionDataMap.d.ts +60 -0
- package/esm/regionDataMap.js +98 -0
- package/esm/renderBlock.d.ts +25 -0
- package/esm/renderBlock.js +10 -0
- package/esm/renderingBackendBase.d.ts +71 -0
- package/esm/renderingBackendBase.js +50 -0
- package/esm/shaders/antialias.js.generated.d.ts +4 -0
- package/esm/shaders/antialias.js.generated.js +25 -0
- package/esm/shaders/capsule.generated.d.ts +1 -0
- package/esm/shaders/capsule.generated.js +3 -0
- package/esm/shaders/colorRampLut.generated.d.ts +1 -0
- package/esm/shaders/colorRampLut.generated.js +3 -0
- package/esm/shaders/coverageBand.generated.d.ts +2 -0
- package/esm/shaders/coverageBand.generated.js +4 -0
- package/esm/shaders/coverageBar.generated.d.ts +4 -0
- package/esm/shaders/coverageBar.generated.js +6 -0
- package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageBar.iface.generated.js +114 -0
- package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
- package/esm/shaders/coverageIndicator.generated.js +6 -0
- package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
- package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
- package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
- package/esm/shaders/coverageInterbase.generated.js +6 -0
- package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
- package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
- package/esm/shaders/coverageMod.generated.d.ts +4 -0
- package/esm/shaders/coverageMod.generated.js +6 -0
- package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
- package/esm/shaders/coverageMod.iface.generated.js +144 -0
- package/esm/shaders/coverageSnp.generated.d.ts +4 -0
- package/esm/shaders/coverageSnp.generated.js +6 -0
- package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
- package/esm/shaders/coverageSnp.iface.generated.js +144 -0
- package/esm/shaders/hpmath.js.generated.d.ts +3 -0
- package/esm/shaders/hpmath.js.generated.js +48 -0
- package/esm/shaders/pointGlyph.generated.d.ts +1 -0
- package/esm/shaders/pointGlyph.generated.js +3 -0
- package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
- package/esm/shaders/pointGlyph.js.generated.js +7 -0
- package/esm/shaders/rowRect.generated.d.ts +3 -0
- package/esm/shaders/rowRect.generated.js +5 -0
- package/esm/shaders/rowRect.js.generated.d.ts +2 -0
- package/esm/shaders/rowRect.js.generated.js +13 -0
- package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
- package/esm/shaders/scoreScale.js.generated.js +46 -0
- package/esm/slangPass.d.ts +49 -0
- package/esm/slangPass.js +22 -0
- package/esm/useEventCallback.d.ts +1 -0
- package/esm/useEventCallback.js +19 -0
- package/esm/useGraphicsCapabilities.d.ts +17 -0
- package/esm/useGraphicsCapabilities.js +38 -0
- package/esm/useRenderingBackend.d.ts +83 -0
- package/esm/useRenderingBackend.js +422 -0
- package/esm/useTabVisibilityRerender.d.ts +13 -0
- package/esm/useTabVisibilityRerender.js +39 -0
- package/esm/webgpuUtils.d.ts +23 -0
- package/esm/webgpuUtils.js +76 -0
- package/package.json +91 -0
- package/src/shaders/antialias.js.generated.ts +33 -0
- package/src/shaders/antialias.slang +112 -0
- package/src/shaders/capsule.generated.ts +4 -0
- package/src/shaders/capsule.slang +62 -0
- package/src/shaders/colorPack.slang +26 -0
- package/src/shaders/colorRampLut.generated.ts +4 -0
- package/src/shaders/colorRampLut.slang +51 -0
- package/src/shaders/coverageBand.generated.ts +6 -0
- package/src/shaders/coverageBand.slang +276 -0
- package/src/shaders/coverageBar.generated.ts +10 -0
- package/src/shaders/coverageBar.iface.generated.ts +172 -0
- package/src/shaders/coverageBar.slang +37 -0
- package/src/shaders/coverageIndicator.generated.ts +10 -0
- package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
- package/src/shaders/coverageIndicator.slang +61 -0
- package/src/shaders/coverageInterbase.generated.ts +10 -0
- package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
- package/src/shaders/coverageInterbase.slang +63 -0
- package/src/shaders/coverageMod.generated.ts +10 -0
- package/src/shaders/coverageMod.iface.generated.ts +211 -0
- package/src/shaders/coverageMod.slang +36 -0
- package/src/shaders/coverageSnp.generated.ts +10 -0
- package/src/shaders/coverageSnp.iface.generated.ts +211 -0
- package/src/shaders/coverageSnp.slang +52 -0
- package/src/shaders/diagonalGrid.slang +25 -0
- package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
- package/src/shaders/hpmath.js.generated.ts +47 -0
- package/src/shaders/hpmath.slang +226 -0
- package/src/shaders/pointGlyph.generated.ts +4 -0
- package/src/shaders/pointGlyph.js.generated.ts +9 -0
- package/src/shaders/pointGlyph.slang +93 -0
- package/src/shaders/rowRect.generated.ts +8 -0
- package/src/shaders/rowRect.js.generated.ts +17 -0
- package/src/shaders/rowRect.slang +160 -0
- package/src/shaders/scoreScale.js.generated.ts +51 -0
- 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
|
+
}
|