@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,372 @@
1
+ import type { BpRegionBounds } from './renderBlock.ts';
2
+ export interface BlockClip {
3
+ scissorX: number;
4
+ scissorW: number;
5
+ fullBlockWidth: number;
6
+ bpLength: number;
7
+ }
8
+ export declare const MAX_DPR = 2;
9
+ /**
10
+ * The ratio every canvas in the app renders at, capped at {@link MAX_DPR}.
11
+ *
12
+ * Use this, never a bare `devicePixelRatio` read — the value has to agree
13
+ * across the backing-store sizing (`syncCanvasSize` / `prepareCanvas`), the
14
+ * scissor/viewport rects derived from it (`clipBlock`, alignments'
15
+ * `computeBlockGeom`), and any shader uniform that reconstructs backing-store
16
+ * dimensions from CSS ones. A call site that reads the global directly renders
17
+ * geometry at a different scale than the canvas it lands on.
18
+ *
19
+ * Two places legitimately want something other than this and should keep saying
20
+ * so explicitly: `createSvgRasterCanvas` pins 2x because export goes to a file
21
+ * rather than a screen, and the analytics / error-report paths read the raw
22
+ * global because they are reporting the device, not drawing on it.
23
+ */
24
+ export declare function getDpr(): number;
25
+ export declare const MAX_CANVAS_DIM_PX = 8192;
26
+ /**
27
+ * The largest CSS-px extent a canvas may take on one axis before its backing
28
+ * store (`× dpr`) crosses the ceiling above.
29
+ *
30
+ * For displays that size a canvas to their **content** rather than to a
31
+ * viewport — a row stack that grows with the row count, so nothing downstream
32
+ * bounds it. Past this the backing store stops tracking the CSS size
33
+ * (`syncCanvasSize` clamps it) while scissor/viewport rects are still derived
34
+ * as `cssPx * dpr`, so WebGPU rejects an out-of-bounds rect and blanks the
35
+ * frame, WebGL clamps and paints at the wrong scale, and the Canvas2D fallback
36
+ * stretches its top slice over the whole track.
37
+ *
38
+ * A function, not a constant: `getDpr()` follows the window, so a display
39
+ * reading this into a computed re-derives it when the window moves to another
40
+ * screen. Where the cap lands is the caller's — MAF scrolls its overflow into a
41
+ * viewport, the multi-row painting divides the cap across its rows so they thin
42
+ * out and all stay on screen.
43
+ */
44
+ export declare function maxCanvasCssPx(): number;
45
+ /**
46
+ * The ratio a canvas's backing store ACTUALLY has to its CSS box, per axis.
47
+ *
48
+ * Equal to `getDpr()` on both axes until `backingPx` clamps, and below it after
49
+ * — which is the whole point: every device-px rect a renderer hands the GPU has
50
+ * to be derived from this rather than from the true dpr, or a clamped canvas
51
+ * gets a viewport taller than the target it draws into and the frame is
52
+ * rejected outright. See ARCHITECTURAL_LIMITS.md §"A canvas past
53
+ * MAX_CANVAS_DIM_PX renders wrong, not smaller".
54
+ */
55
+ export interface CanvasScale {
56
+ x: number;
57
+ y: number;
58
+ }
59
+ export declare function syncCanvasSize(canvas: HTMLCanvasElement, width: number, height: number): {
60
+ changed: boolean;
61
+ scale: CanvasScale;
62
+ };
63
+ export declare function prepareCanvas(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, canvasWidth: number, canvasHeight: number): void;
64
+ export declare function getPreparedCanvas2D(canvas: HTMLCanvasElement | null, width: number, height: number): CanvasRenderingContext2D | null;
65
+ export declare function devicePxSpan(cssStart: number, cssEnd: number, dpr: number): {
66
+ start: number;
67
+ width: number;
68
+ };
69
+ /**
70
+ * One horizontal band of a stacked canvas as a device-px scissor rect, clamped
71
+ * to the backing store: `top`/`height` in CSS px, `bufH` the backing store's
72
+ * height in device px.
73
+ *
74
+ * The clamp is the part that matters. A band scrolled past either edge — a
75
+ * grouped alignments section, a MAF rows viewport under a resized band — yields
76
+ * a rect the backing store does not contain, and WebGPU REJECTS an
77
+ * out-of-bounds scissor and blanks the whole frame rather than clipping it. A
78
+ * band clamped to zero height is the caller's cue to skip its passes.
79
+ *
80
+ * `dpr` is the scale the canvas ACTUALLY got (`hal.resize`'s return), not
81
+ * `getDpr()`: the two part company once the backing store clamps at
82
+ * `MAX_CANVAS_DIM_PX`, and past that a band built from the true ratio is taller
83
+ * than the target.
84
+ */
85
+ export declare function devicePxBand(top: number, height: number, dpr: number, bufH: number): {
86
+ top: number;
87
+ height: number;
88
+ };
89
+ export declare function clampBlockScissor(screenStartPx: number, screenEndPx: number, canvasWidth: number): {
90
+ scissorX: number;
91
+ scissorEnd: number;
92
+ scissorW: number;
93
+ } | null;
94
+ export declare function clipBlockForCanvas(block: BpRegionBounds, canvasWidth: number): BlockClip | null;
95
+ /**
96
+ * The 2D-context subset the block scaffold below needs. Structural on purpose:
97
+ * render-core must not depend on `@jbrowse/core`, where the real `Ctx2D =
98
+ * CanvasRenderingContext2D | SvgCanvas` union lives. Both members satisfy it,
99
+ * so a plugin passes its `Ctx2D` straight in.
100
+ */
101
+ export interface ClipContext2D {
102
+ save(): void;
103
+ restore(): void;
104
+ beginPath(): void;
105
+ rect(x: number, y: number, w: number, h: number): void;
106
+ clip(): void;
107
+ }
108
+ /**
109
+ * Clip to one rect, paint, unclip — the `save`/`clip`/`restore` triple with the
110
+ * `restore` in a `finally`.
111
+ *
112
+ * The `finally` is the whole point, and the cost of missing it is not local: an
113
+ * on-screen ctx outlives the frame and `prepareCanvas`'s setTransform/clearRect
114
+ * do **not** reset clip state, so one painter throwing once leaves every later
115
+ * frame drawing through a stale clip. A transient error becomes permanent
116
+ * corruption, and the visible symptom (content missing from a band that has no
117
+ * reason to be clipped) points nowhere near the painter that threw.
118
+ *
119
+ * `forEachClippedBlock` is the per-block form and covers the common case on its
120
+ * own. Reach for this one for the clips *nested inside* a block — a display
121
+ * whose bands each own a horizontal strip of the canvas (alignments clips its
122
+ * coverage, pileup and arc bands separately within one block clip), where the
123
+ * nesting is what makes a hand-rolled pairing easy to get wrong: an early
124
+ * `return` inside the band body unbalances the block's `save` too.
125
+ */
126
+ export declare function withClip(ctx: ClipContext2D, x: number, y: number, w: number, h: number, paint: () => void): void;
127
+ /**
128
+ * The Canvas2D twin of `GpuPerRegionRenderingBackend.renderBlocks`' per-block
129
+ * scissor: skip blocks with nothing to draw or no on-screen span, clip to the
130
+ * block's columns, paint, unclip. Owning it here means a painter can't pair
131
+ * `save()` with a missed `restore()` (leaking the clip onto every later block),
132
+ * clip to a span other than `clipBlockForCanvas`', or forget the null check.
133
+ *
134
+ * `select` resolves a block to what it paints, or `undefined` to skip it — the
135
+ * one gate covering all three reasons a painter skips: no region in the map, a
136
+ * region with zero features, and a region whose relevant sub-field is absent
137
+ * (MAF's `?.coverage`). It runs *before* the clip so an empty block costs
138
+ * neither the path-and-clip round trip on a real context nor the queued group
139
+ * on `SvgCanvas` — that class now drops a clip nothing was drawn inside, so a
140
+ * skipped-late block no longer leaves dead `<clipPath>` markup in the export,
141
+ * but relying on it to clean up after the gate is backwards. Resolving here
142
+ * also lets `paint` take a non-optional value instead of re-narrowing inside
143
+ * the callback.
144
+ *
145
+ * `clipHeight` is a parameter rather than read off a render state because
146
+ * painters that own a sub-band of the canvas clip to that band, not the full
147
+ * height (MAF's coverage row).
148
+ *
149
+ * `paint` receives the block, so callers needing bp→px build their own mapper
150
+ * (`makeBpMapper` / `makeCellLeftMapper` — which one is a per-painter choice).
151
+ * Two closures per draw call, not per feature: per-feature loops live inside the
152
+ * `paint` body and stay allocation-free.
153
+ */
154
+ export declare function forEachClippedBlock<T, B extends BpRegionBounds>(ctx: ClipContext2D, blocks: readonly B[], canvasWidth: number, clipHeight: number, select: (block: B) => T | undefined, paint: (data: T, block: B, clip: BlockClip) => void): void;
155
+ /**
156
+ * Stroke the outline of `[x, y, w, h]` so the line lands **inside** the rect
157
+ * rather than straddling its edge.
158
+ *
159
+ * Canvas2D centres a stroke on its path, so `strokeRect(x, y, w, h)` puts half
160
+ * the line width outside the rectangle you named. Every GPU pass in this repo
161
+ * draws an outline as an inner band — a fragment test on distance-to-edge, which
162
+ * has no way to paint outside the glyph — so the straddling spelling is a
163
+ * guaranteed half-pixel disagreement with the shader, and it eats whatever gap
164
+ * separates the glyph from its neighbour.
165
+ *
166
+ * **It matters more in the SVG export than on screen**, which is the reason this
167
+ * is a helper and not a house style. On a canvas the difference is half a pixel;
168
+ * in SVG a centred stroke means the emitted element's *declared geometry* and
169
+ * its painted extent differ by half the stroke width, so a 10px-tall read
170
+ * exports as an 11px-tall mark. Anything that measures the file, or opens it in
171
+ * a vector editor and snaps to the shape's bounds, is then reading a rectangle
172
+ * whose coordinates do not describe what is drawn.
173
+ *
174
+ * Degenerate rects are left to the caller: below `lineWidth` on an axis the
175
+ * inset would invert it, and what to do instead (skip the outline, or fill
176
+ * solid) is a per-glyph decision. The alignments read painter and the canvas
177
+ * feature painter both gate on a minimum size first.
178
+ */
179
+ export declare function strokeRectInside(ctx: {
180
+ strokeRect: (x: number, y: number, w: number, h: number) => void;
181
+ }, x: number, y: number, w: number, h: number, lineWidth?: number): void;
182
+ /**
183
+ * `fillRect` x for a mark that spans `x1`→`x2` but is painted at `width` — the
184
+ * Canvas2D pivot for every painter that widens a too-narrow span to a floor (a
185
+ * 1bp feature, a zoomed-out coverage bin), and the twin of the pivot baked into
186
+ * the shaders' `extendToMinWidthX`.
187
+ *
188
+ * The mark is anchored at `x1` and grows *away* from it. That's the whole point:
189
+ * on a reversed block `makeBpMapper` flips, so `x2 < x1`, `x1` is the feature's
190
+ * **start** — its right edge — and the mark must grow leftward from it. Spelling
191
+ * the fill as `min(x1, x2)` + `max(floor, |dx|)` instead anchors whichever edge
192
+ * happens to be leftmost, which reversed is the feature's *end*, sliding the
193
+ * mark by up to the floor. Forward blocks are identical either way, so the error
194
+ * only ever shows on flipped regions and survives review. The canvas rect
195
+ * painter (2px floor), the multi-row painter (1px), and wiggle (1.5px) each had
196
+ * it independently.
197
+ *
198
+ * Callers own the *width*, like `makeCellLeftMapper` below — it's a per-plugin
199
+ * rule (canvas/multi-row take `max(floor, |dx|)`, matching `extendToMinWidthX`
200
+ * exactly; wiggle adds a seam-closing fudge the shader deliberately omits).
201
+ * Only the pivot is shared, and it's the part that keeps being got wrong.
202
+ *
203
+ * Scalar in, scalar out: runs per feature per frame, so it must not allocate.
204
+ */
205
+ export declare function spanLeft(x1: number, x2: number, width: number): number;
206
+ export declare function makeRampFillStyleLut(ramp: Uint8Array): (t: number) => string;
207
+ export declare function bpToScreenPx(absBp: number, regionStart: number, regionEnd: number, screenStartPx: number, screenEndPx: number, reversed?: boolean): number;
208
+ export declare function makeBpMapper(bounds: BpRegionBounds): (bp: number) => number;
209
+ /**
210
+ * Left edge of the **1bp cell** covering `bp` — for painters that fill a rect
211
+ * per base (MAF alignment cells, the alignments pileup's mismatch /
212
+ * modification / per-base quality+letter / soft-clip base layers).
213
+ *
214
+ * Use this, not `makeBpMapper`, whenever the mark is a cell rather than a point
215
+ * or a two-edge span. `makeBpMapper(bp)` is the cell's left edge only on a
216
+ * forward block: a reversed block runs bp leftward, so it lands on the cell's
217
+ * *right* edge, and `fillRect(x, y, +w, h)` from there covers bp-1 instead. The
218
+ * error is one base wide — invisible zoomed out (cells floor to ~1px), a whole
219
+ * base off zoomed in, and it only reproduces on flipped regions, so it survives
220
+ * casual review. Every plugin that hand-rolled the pivot had to rediscover it;
221
+ * the alignments pileup got it wrong across all five of its cell layers.
222
+ *
223
+ * Two-edge spans (`min(toX(start), toX(end))`) and boundary marks (insertions,
224
+ * clip bars, centered on a bp edge) are orientation-safe already and don't need
225
+ * this. The GPU side has no equivalent bug — shaders span
226
+ * `bpToClipX(pos)`→`bpToClipX(pos+1)` unflipped and mirror via `flipX` — and
227
+ * gets its own pivot from `writeBpRangeUniforms`.
228
+ *
229
+ * Callers own the cell *width*: it's a per-plugin rule (MAF uses the raw
230
+ * bp-scale; the pileup floors at 1px and adds a seam fudge). Only the pivot is
231
+ * shared.
232
+ */
233
+ /**
234
+ * Screen px one base occupies in this region — the scale factor, unsigned, with
235
+ * the block's direction deliberately left out (that lives in `makeBpMapper` and
236
+ * `spanLeft`, and folding it in here would make a *scale* carry an orientation).
237
+ *
238
+ * Shared because it is usually an input to something a second painter has to
239
+ * reproduce exactly: a mark sized against it in a draw pass and hit-tested
240
+ * against it in a component is two spellings of one number, and the hit target
241
+ * silently stops covering the mark if they ever drift. Trivial arithmetic, but
242
+ * the drift is not.
243
+ */
244
+ export declare function pxPerBpOf(bounds: BpRegionBounds): number;
245
+ export declare function makeCellLeftMapper(bounds: BpRegionBounds): (bp: number) => number;
246
+ /**
247
+ * Fill the rect covering genomic `[startBp, endBp)`, given a bare
248
+ * `makeBpMapper`. Maps **both** edges and takes the leftmost, which is
249
+ * orientation-safe by construction: on a reversed block bp runs leftward, so
250
+ * the span's left edge is its *end*, and there is no pivot to get backwards.
251
+ *
252
+ * Prefer this to `makeCellLeftMapper` + a caller-computed width for anything
253
+ * wider than one base. The pivot in `makeCellLeftMapper` is exactly one base,
254
+ * which is right for a 1bp cell and silently wrong for an N-bp span — it
255
+ * anchors the rect one base from the correct edge and then grows it the wrong
256
+ * way. `fillBpSpan(bp, bp + 1)` is *identical* to the cell-mapper spelling, so
257
+ * a per-base painter can use this too and never think about orientation again.
258
+ *
259
+ * `seam` is the caller's sub-pixel overlap (MAF's `GAP_STROKE_OFFSET`), added
260
+ * to the width so adjacent cells don't show hairlines at ~1px/bp. Widening a
261
+ * sub-pixel *feature* to a visible floor is a different job — that's
262
+ * `spanLeft`, which must anchor the feature's start edge.
263
+ */
264
+ export declare function fillBpSpan(ctx: {
265
+ fillRect: (x: number, y: number, w: number, h: number) => void;
266
+ }, toX: (bp: number) => number, startBp: number, endBp: number, top: number, height: number, seam?: number): void;
267
+ /**
268
+ * The **integer base** whose 1bp cell covers screen pixel `px` — the inverse of
269
+ * `makeCellLeftMapper`, for hit-testing (which base did the cursor land on) as
270
+ * opposed to painting.
271
+ *
272
+ * Rounding is not `Math.floor` on both orientations, which is the trap. The
273
+ * un-rounded inverse of `makeBpMapper` lands in `[b, b+1)` on a forward block
274
+ * but in `(b, b+1]` on a reversed one, because bp runs leftward there — so
275
+ * flooring reversed names `b+1` on base `b`'s leftmost pixel column, and names
276
+ * `end` (outside the block) on the block's first column. It is the same
277
+ * one-base pivot `makeCellLeftMapper` exists to get right, seen from the other
278
+ * direction, and it fails the same way: invisible zoomed out, a whole base off
279
+ * at base zoom, and only on flipped regions.
280
+ *
281
+ * Callers own the *range* check: a `px` outside `[screenStartPx, screenEndPx)`
282
+ * extrapolates rather than clamping, since which block owns a pixel is the
283
+ * caller's decision (adjacent blocks share an edge pixel).
284
+ *
285
+ * **Multiply before dividing, and never form the fraction.** That is the whole
286
+ * of the arithmetic below and it is load-bearing: `(px - screenStartPx) * span`
287
+ * is *exact* — both operands are dyadic and the product tops out around 3e12
288
+ * against a 2^53 budget — so the one division that follows is the only rounding
289
+ * in the expression, and its true quotient is either an exact integer (the
290
+ * division is then exact too) or at least `1 / blockWidth` from one, which is
291
+ * ~1e-3 against a relative error of 2^-53. So the floor cannot land on the
292
+ * wrong base. Spelling it `frac = (px - s) / w` and then `frac * span` rounds
293
+ * twice, and the second product can land arbitrarily close to an integer from
294
+ * either side.
295
+ *
296
+ * Not a micro-optimization — a correctness one, and measured. Against an exact
297
+ * rational oracle over 11.6M realistic samples (integer through eighth-pixel
298
+ * cursor positions, chr1-scale starts, spans from 1bp to 3Mb, both
299
+ * orientations, fractional block offsets) this form is exact on every one,
300
+ * where the `frac` spelling it replaces named the wrong base 4202 times and
301
+ * wiggle's independent `baseAtFraction` — which this now backs — 10992 times.
302
+ * Those are all boundary pixels, which is exactly where a per-base tooltip is
303
+ * read.
304
+ *
305
+ * **The anchor splits, because `bounds.start` is usually not a whole base.** A
306
+ * block's edges are laid out in PIXELS, so its bp edges land wherever that
307
+ * projection puts them — `visibleRegions` hands every hit test a start like
308
+ * `14468.9936` — and rounding the *offset* and then adding such an anchor
309
+ * returns the anchor's fraction untouched. The result is then not a base at
310
+ * all, and every caller that indexes one by equality (a mismatch column, a
311
+ * coverage bin, the sort-at-this-column anchor) misses silently: the pileup
312
+ * offered no SNP submenu over a mismatch it was drawing. Rounding
313
+ * `whole + fraction + offset` in one step instead fixes that and hands back the
314
+ * genome-scale addend the paragraph above exists to keep out, so the whole base
315
+ * comes off first and only the sub-base remainder enters the rounded
316
+ * expression. On a whole-base anchor that remainder is zero and the arithmetic
317
+ * is identical to what an integer start has always done.
318
+ *
319
+ * The same oracle, re-run over 3.7M samples on fractional anchors: this spelling
320
+ * misses 696 and the shipped one missed every single fractional-anchor sample,
321
+ * 3,076,480 of them. `Math.floor(start + offset)` misses 2,524. The residue is
322
+ * the boundary pixel itself, which no double-precision spelling resolves both
323
+ * ways once base boundaries stop landing on whole pixels.
324
+ */
325
+ export declare function bpAtPx(px: number, bounds: BpRegionBounds): number;
326
+ /**
327
+ * The **fractional bp** at screen pixel `px` — the un-rounded inverse of
328
+ * `makeBpMapper`, for hit tests that measure a distance or open a search window
329
+ * rather than index a base (the alignments interbase triangles and modification
330
+ * centers, the variant cell's bp padding, the Manhattan point's ±radius query).
331
+ *
332
+ * Ask `bpAtPx` instead the moment the answer indexes per-base data, and **do
333
+ * not floor this** — flooring an already-divided fraction is the double
334
+ * rounding `bpAtPx`'s JSDoc argues against at length, and reversed it names the
335
+ * neighbouring base as well. `end - offset` reversed, not `start + offset`: bp
336
+ * runs leftward there, and each of the three plugins that spelled this out
337
+ * locally had to rediscover that.
338
+ *
339
+ * **Neither function is the other's implementation.** This multiplies before
340
+ * dividing for the same reason `bpAtPx` does — `(px - screenStartPx) * span` is
341
+ * exact, so the division is the only rounding — but it does not split the
342
+ * anchor into whole and remainder the way `bpAtPx` does. That split keeps the
343
+ * genome-scale addend out of a *rounded* expression, and there is no rounding
344
+ * here to protect; the caller wants the anchor's fraction kept, not floored
345
+ * away.
346
+ *
347
+ * Adopting it moved no consumer's answer, which is the point rather than a
348
+ * caveat. Over 300000 fractional-anchor samples against the exact-rational
349
+ * oracle `bpAtPx` was measured on, it and all three spellings it replaced land
350
+ * within 1 ulp of the exactly-rounded value; it returns a different double from
351
+ * the `frac` spelling on 114 and from gwas' `bpPerPx` spelling on 289, by at
352
+ * most 7.5e-9 bp against hit radii measured in whole pixels.
353
+ */
354
+ export declare function bpAtPxExact(px: number, bounds: BpRegionBounds): number;
355
+ /**
356
+ * The region owning screen pixel `px`, or undefined past the last one — the
357
+ * range check `bpAtPx` and `bpAtPxExact` leave to their callers, answered once.
358
+ *
359
+ * **The exclusive upper bound is the whole content of this.** Adjacent regions
360
+ * share a pixel (`regionA.screenEndPx === regionB.screenStartPx`), so an
361
+ * inclusive bound matches both and `find` hands back the earlier one — which
362
+ * then steals every click meant for the later region's first column. Half-open,
363
+ * exactly one region can match, so this answers with a region rather than a
364
+ * list.
365
+ *
366
+ * Generic so a caller gets its own region type back, since what it wants from
367
+ * the match is a `displayedRegionIndex` or the bp/px edges for a mapper.
368
+ */
369
+ export declare function regionAtPixel<R extends {
370
+ screenStartPx: number;
371
+ screenEndPx: number;
372
+ }>(regions: readonly R[], px: number): R | undefined;