@jbrowse/render-core 5.0.0-beta.2 → 5.0.0-beta.3

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 (110) hide show
  1. package/esm/RenderLifecycleMixin.d.ts +20 -1
  2. package/esm/RenderLifecycleMixin.js +25 -2
  3. package/esm/canvas2dUtils.d.ts +25 -0
  4. package/esm/canvas2dUtils.js +27 -0
  5. package/esm/colorRampLut.d.ts +4 -4
  6. package/esm/colorRampLut.js +4 -4
  7. package/esm/computePipeline.d.ts +13 -0
  8. package/esm/computePipeline.js +55 -0
  9. package/esm/coverageBand.d.ts +0 -1
  10. package/esm/coverageBand.js +0 -1
  11. package/esm/coverageBandBuffers.d.ts +0 -6
  12. package/esm/coverageBandBuffers.js +1 -13
  13. package/esm/createRenderingBackend.d.ts +14 -18
  14. package/esm/createRenderingBackend.js +4 -2
  15. package/esm/encodeMemo.d.ts +14 -0
  16. package/esm/encodeMemo.js +48 -0
  17. package/esm/gpuDevice.d.ts +9 -0
  18. package/esm/gpuDevice.js +12 -3
  19. package/esm/graphicsCapabilities.d.ts +18 -15
  20. package/esm/graphicsCapabilities.js +23 -16
  21. package/esm/hal/createHal.d.ts +1 -2
  22. package/esm/hal/createHal.js +14 -4
  23. package/esm/hal/deviceGpuCache.js +2 -2
  24. package/esm/hal/gpuHalBase.d.ts +1 -1
  25. package/esm/hal/gpuHalBase.js +2 -3
  26. package/esm/hal/mockHal.d.ts +0 -1
  27. package/esm/hal/mockHal.js +0 -10
  28. package/esm/hal/regionRegistry.d.ts +0 -1
  29. package/esm/hal/regionRegistry.js +3 -15
  30. package/esm/hal/types.d.ts +5 -1
  31. package/esm/hal/webgl2Hal.d.ts +1 -1
  32. package/esm/hal/webgl2Hal.js +2 -2
  33. package/esm/hal/webgpuHal.d.ts +2 -2
  34. package/esm/hal/webgpuHal.js +13 -10
  35. package/esm/installUpload.d.ts +4 -1
  36. package/esm/installUpload.js +7 -39
  37. package/esm/instanceCache.d.ts +10 -7
  38. package/esm/instanceCache.js +14 -15
  39. package/esm/mapUploadSync.d.ts +0 -8
  40. package/esm/mapUploadSync.js +0 -1
  41. package/esm/marks/colorFill.d.ts +17 -0
  42. package/esm/marks/colorFill.js +26 -0
  43. package/esm/marks/drawAgainstHit.d.ts +47 -0
  44. package/esm/marks/drawAgainstHit.js +140 -0
  45. package/esm/marks/glyphPaint.d.ts +16 -0
  46. package/esm/marks/glyphPaint.js +41 -0
  47. package/esm/marks/index.d.ts +9 -0
  48. package/esm/marks/index.js +6 -0
  49. package/esm/marks/markBackend.d.ts +87 -0
  50. package/esm/marks/markBackend.js +133 -0
  51. package/esm/marks/markPaint.d.ts +13 -0
  52. package/esm/marks/markPaint.js +18 -0
  53. package/esm/marks/pointMark.d.ts +24 -0
  54. package/esm/marks/pointMark.js +98 -0
  55. package/esm/marks/spanMark.d.ts +45 -0
  56. package/esm/marks/spanMark.js +71 -0
  57. package/esm/marks/types.d.ts +206 -0
  58. package/esm/marks/types.js +125 -0
  59. package/esm/perRegionRenderingBackend.d.ts +15 -3
  60. package/esm/perRegionRenderingBackend.js +26 -3
  61. package/esm/regionDataMap.d.ts +2 -2
  62. package/esm/regionDataMap.js +2 -2
  63. package/esm/renderingBackendBase.d.ts +11 -14
  64. package/esm/renderingBackendBase.js +7 -7
  65. package/esm/shaders/capsule.js.generated.d.ts +1 -0
  66. package/esm/shaders/capsule.js.generated.js +11 -0
  67. package/esm/shaders/coverageBar.generated.d.ts +2 -2
  68. package/esm/shaders/coverageBar.generated.js +2 -2
  69. package/esm/shaders/coverageMod.generated.d.ts +2 -2
  70. package/esm/shaders/coverageMod.generated.js +2 -2
  71. package/esm/shaders/coverageSnp.generated.d.ts +2 -2
  72. package/esm/shaders/coverageSnp.generated.js +2 -2
  73. package/esm/shaders/pointMark.consts.generated.d.ts +4 -0
  74. package/esm/shaders/pointMark.consts.generated.js +6 -0
  75. package/esm/shaders/pointMark.generated.d.ts +5 -0
  76. package/esm/shaders/pointMark.generated.js +7 -0
  77. package/esm/shaders/pointMark.iface.generated.d.ts +55 -0
  78. package/esm/shaders/pointMark.iface.generated.js +100 -0
  79. package/esm/shaders/pointMark.js.generated.d.ts +1 -0
  80. package/esm/shaders/pointMark.js.generated.js +16 -0
  81. package/esm/shaders/spanMark.generated.d.ts +4 -0
  82. package/esm/shaders/spanMark.generated.js +6 -0
  83. package/esm/shaders/spanMark.iface.generated.d.ts +58 -0
  84. package/esm/shaders/spanMark.iface.generated.js +122 -0
  85. package/esm/sharedBackendKey.d.ts +18 -0
  86. package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
  87. package/esm/slangPass.d.ts +2 -0
  88. package/esm/slangPass.js +2 -0
  89. package/package.json +13 -4
  90. package/src/shaders/capsule.js.generated.ts +14 -0
  91. package/src/shaders/capsule.slang +12 -4
  92. package/src/shaders/colorPack.slang +5 -1
  93. package/src/shaders/coverageBand.slang +38 -15
  94. package/src/shaders/coverageBar.generated.ts +2 -2
  95. package/src/shaders/coverageBar.slang +9 -0
  96. package/src/shaders/coverageMod.generated.ts +2 -2
  97. package/src/shaders/coverageSnp.generated.ts +2 -2
  98. package/src/shaders/hpmath.slang +7 -1
  99. package/src/shaders/pointMark.consts.generated.ts +10 -0
  100. package/src/shaders/pointMark.generated.ts +12 -0
  101. package/src/shaders/pointMark.iface.generated.ts +146 -0
  102. package/src/shaders/pointMark.js.generated.ts +21 -0
  103. package/src/shaders/pointMark.slang +192 -0
  104. package/src/shaders/rowRect.slang +7 -8
  105. package/src/shaders/spanMark.generated.ts +10 -0
  106. package/src/shaders/spanMark.iface.generated.ts +168 -0
  107. package/src/shaders/spanMark.slang +22 -0
  108. package/esm/globalRenderingBackend.d.ts +0 -90
  109. package/esm/globalRenderingBackend.js +0 -53
  110. package/esm/keyedRenderingBackend.d.ts +0 -41
@@ -0,0 +1,71 @@
1
+ import { bpRangeXTuple } from '../blockClipUtils.js';
2
+ import { makeBpMapper, spanLeft } from '../canvas2dUtils.js';
3
+ import { drawnRowHeightPx, rowBandOffsetPx, } from '../shaders/rowRect.js.generated.js';
4
+ import * as shader from '../shaders/spanMark.generated.js';
5
+ import { slangPass } from '../slangPass.js';
6
+ import { makeAbgrFill } from './colorFill.js';
7
+ export const spanMark = {
8
+ id: 'span',
9
+ pass: {
10
+ ...slangPass({ id: 'span', mod: shader }),
11
+ pack: c => shader.packInstances(c, c.count),
12
+ },
13
+ writeUniforms(scratch, clip, block, frame, params) {
14
+ shader.writeUniforms(scratch, {
15
+ bpRangeX: bpRangeXTuple(clip, block.reversed),
16
+ canvasHeight: frame.canvasHeight,
17
+ // CSS px, not physical: `extendToMinWidthX` divides `minCellPx` by this
18
+ // to reach clip space, so a CSS width is what makes the floor a CSS-pixel
19
+ // one and matches the painter's `Math.max` below.
20
+ minCellDenomPx: clip.scissorW,
21
+ minCellPx: params.minWidthPx,
22
+ zero: 0,
23
+ rowHeight: params.rowHeight,
24
+ rowProportion: params.rowProportion,
25
+ scrollTop: params.scrollTop,
26
+ });
27
+ },
28
+ paintBlock(ctx, channels, block, _frame, params) {
29
+ const { x, x2, row, color, count } = channels;
30
+ const { rowHeight, rowProportion, minWidthPx, seamPx, scrollTop } = params;
31
+ const h = drawnRowHeightPx(rowHeight, rowProportion);
32
+ const offset = rowBandOffsetPx(rowHeight, rowProportion);
33
+ const bpToPx = makeBpMapper(block);
34
+ const setFill = makeAbgrFill(ctx);
35
+ for (let i = 0; i < count; i++) {
36
+ const xa = bpToPx(x[i]);
37
+ const xb = bpToPx(x2[i]);
38
+ const width = Math.max(minWidthPx, Math.abs(xb - xa));
39
+ setFill(color[i]);
40
+ ctx.fillRect(spanLeft(xa, xb, width), offset + rowHeight * row[i] - scrollTop, width + seamPx, h);
41
+ }
42
+ },
43
+ hitNearest(channels, block, _frame, params, xPx, yPx, candidates, maxDistSq) {
44
+ const { x, x2, row } = channels;
45
+ const { rowHeight, rowProportion, minWidthPx, scrollTop } = params;
46
+ const h = drawnRowHeightPx(rowHeight, rowProportion);
47
+ const offset = rowBandOffsetPx(rowHeight, rowProportion);
48
+ const bpToPx = makeBpMapper(block);
49
+ let best;
50
+ let bestDistSq = maxDistSq;
51
+ for (const i of candidates) {
52
+ const xa = bpToPx(x[i]);
53
+ const xb = bpToPx(x2[i]);
54
+ const width = Math.max(minWidthPx, Math.abs(xb - xa));
55
+ const left = spanLeft(xa, xb, width);
56
+ const top = offset + rowHeight * row[i] - scrollTop;
57
+ // the rect `paintBlock` fills, less `seamPx`, which is painter-only
58
+ // overdraw; distance 0 inside it, else to its nearest edge
59
+ const nx = Math.min(Math.max(xPx, left), left + width);
60
+ const ny = Math.min(Math.max(yPx, top), top + h);
61
+ const dx = xPx - nx;
62
+ const dy = yPx - ny;
63
+ const distSq = dx * dx + dy * dy;
64
+ if (distSq < bestDistSq) {
65
+ bestDistSq = distSq;
66
+ best = { index: i, x: nx, y: ny, distSq };
67
+ }
68
+ }
69
+ return best;
70
+ },
71
+ };
@@ -0,0 +1,206 @@
1
+ import type { BlockClipResult } from '../blockClipUtils.ts';
2
+ import type { ClipContext2D } from '../canvas2dUtils.ts';
3
+ import type { GpuHal } from '../hal/index.ts';
4
+ import type { InstancePass } from '../instancePass.ts';
5
+ import type { RenderBlock } from '../renderBlock.ts';
6
+ import type { FrameDimensions } from '../renderingBackendBase.ts';
7
+ /**
8
+ * The 2D-context subset a shape's painter needs, structural for the same reason
9
+ * `ClipContext2D` is: render-core must not depend on `@jbrowse/core`, where the
10
+ * real `Ctx2D = CanvasRenderingContext2D | SvgCanvas` union lives. Both members
11
+ * satisfy it, so a display passes its `Ctx2D` straight in — which is also what
12
+ * makes one painter the on-screen fallback and the SVG export path.
13
+ */
14
+ export interface MarkContext2D extends ClipContext2D {
15
+ fillStyle: string | CanvasGradient | CanvasPattern;
16
+ strokeStyle: string | CanvasGradient | CanvasPattern;
17
+ lineWidth: number;
18
+ lineCap?: CanvasLineCap;
19
+ lineJoin?: CanvasLineJoin;
20
+ fillRect(x: number, y: number, w: number, h: number): void;
21
+ strokeRect(x: number, y: number, w: number, h: number): void;
22
+ translate(x: number, y: number): void;
23
+ scale(x: number, y: number): void;
24
+ rotate(angle: number): void;
25
+ moveTo(x: number, y: number): void;
26
+ lineTo(x: number, y: number): void;
27
+ bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void;
28
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number): void;
29
+ ellipse(x: number, y: number, radiusX: number, radiusY: number, rotation: number, startAngle: number, endAngle: number): void;
30
+ setLineDash(segments: number[]): void;
31
+ closePath(): void;
32
+ fill(): void;
33
+ stroke(): void;
34
+ }
35
+ export type MarkFrame = FrameDimensions;
36
+ /** A horizontal strip of the canvas, in CSS px down from its top edge. */
37
+ export interface MarkBand {
38
+ top: number;
39
+ height: number;
40
+ }
41
+ /** Where a shape's ink sits nearest a cursor, and how far that is. */
42
+ export interface MarkHit {
43
+ index: number;
44
+ x: number;
45
+ y: number;
46
+ distSq: number;
47
+ }
48
+ /**
49
+ * A shape: one hand-written `.slang`, one packer, one Canvas2D painter and one
50
+ * hit test, all reading the same channel arrays.
51
+ *
52
+ * `TChannels` is the shape's own vocabulary — parallel typed arrays plus a
53
+ * count. `TParams` is everything else the drawing needs, which reaches the GPU
54
+ * as uniforms and the painter as arguments; the two must be the same values or
55
+ * the backends diverge, which is what having one object per shape prevents.
56
+ *
57
+ * There is no `MarkShape` constructor. A shape is written out once and
58
+ * admitted by a consumer rather than by completeness — the ADR-040 bar. It
59
+ * lives here when two displays share it (`span`) or its geometry is generic
60
+ * (`point`); a display whose shader's generated twins feed its own hit test
61
+ * keeps the shape beside them (variants' `cellMark`) and still declares it
62
+ * through `defineMark`.
63
+ */
64
+ export interface MarkShape<TChannels, TParams> {
65
+ readonly id: string;
66
+ readonly pass: InstancePass<TChannels>;
67
+ writeUniforms(scratch: ArrayBuffer, clip: BlockClipResult, block: RenderBlock, frame: MarkFrame, params: TParams): void;
68
+ paintBlock(ctx: MarkContext2D, channels: TChannels, block: RenderBlock, frame: MarkFrame, params: TParams): void;
69
+ /**
70
+ * The shape's draw predicate over (block, frame, params) — not over its
71
+ * instances, which it is never handed. Both backends skip the block when it
72
+ * says no. Optional: a shape whose every block can carry ink leaves it off.
73
+ *
74
+ * Placement is one use: the canvas continuation marker exists only where a
75
+ * block meets a canvas edge, and an interior block would otherwise shade a
76
+ * whole pileup's worth of vertices to draw nothing. Settings are the other,
77
+ * and the general one: the coverage band's layers read `hasDomain` off
78
+ * `params` so an unresolved autoscale draws no depth-scaled layer rather
79
+ * than bars of arbitrary height, and `showInterbase` off it so the histogram
80
+ * and its triangles turn off together.
81
+ */
82
+ paintsBlock?(block: RenderBlock, frame: MarkFrame, params: TParams): boolean;
83
+ /**
84
+ * The nearest drawn ink to `(xPx, yPx)` among `candidates`, or undefined if
85
+ * nothing beats `maxDistSq`.
86
+ *
87
+ * The candidate set is the caller's, not the shape's: a display with a
88
+ * worker-built index (GWAS's Flatbush over (bp, value)) hands in what the
89
+ * index answered, and one without hands in every instance. What the shape
90
+ * owns is where its ink actually is — the half that drifts from the painter.
91
+ *
92
+ * Distance 0 means the point is on the ink `paintBlock` fills, edges
93
+ * included, and a caller wanting containment alone passes a bound that
94
+ * admits nothing else (`Number.MIN_VALUE`). Only a strictly nearer candidate
95
+ * replaces the best, so on a tie the first candidate wins — a caller that
96
+ * iterates back to front gets the last-painted mark, which is the one on
97
+ * top. `drawAgainstHit.ts` is the sweep that holds a shape to this.
98
+ *
99
+ * Optional, because a shape earns it from a consumer like any other member.
100
+ * MAF answers a hit from row/column arithmetic over the alignment rather than
101
+ * from `span`'s channels, so it reads none.
102
+ */
103
+ hitNearest?(channels: TChannels, block: RenderBlock, frame: MarkFrame, params: TParams, xPx: number, yPx: number, candidates: Iterable<number>, maxDistSq: number): MarkHit | undefined;
104
+ }
105
+ /**
106
+ * Which uniform struct is standing in the HAL's current slot, so the next mark
107
+ * of the SAME block can draw off it instead of packing and staging the same
108
+ * bytes again. Compared by reference and never called or read for its values:
109
+ * one writer plus one `params` lens is one struct, because `writeUniforms` sees
110
+ * nothing but the block, the clip, the frame and what that lens returned.
111
+ *
112
+ * A display declaring several marks over one writer and one lens is the common
113
+ * shape, not an edge case — the coverage band is five, the feature glyphs are
114
+ * five — and every mark after the first was restating the previous one's write.
115
+ *
116
+ * **The caller owns one of these per block and hands the same one to every mark
117
+ * of that block.** A stale one drawn against the next block's clip is silently
118
+ * the wrong geometry, so it is a fresh object per block rather than a field
119
+ * that has to be remembered to reset. A caller that writes uniforms of its own
120
+ * between marks (alignments' per-section pileup write) starts a new one after
121
+ * it, or hands none at all.
122
+ */
123
+ export interface StagedUniforms {
124
+ writer: unknown;
125
+ params: unknown;
126
+ }
127
+ /**
128
+ * A shape bound to one display's region payload and render state — what
129
+ * `defineMark` returns and what a display actually holds.
130
+ *
131
+ * The shape's own generics are erased here on purpose: a display draws a list
132
+ * of marks over heterogeneous shapes, and only the binding knows how to reach
133
+ * either one's channels.
134
+ */
135
+ export interface Mark<TRegion, TState extends MarkFrame> {
136
+ readonly pass: InstancePass<TRegion>;
137
+ /**
138
+ * The pass id whose instance buffer this mark draws from, for a mark that
139
+ * is registered but never uploaded to: the canvas chevrons ride the line
140
+ * buffer and the continuation markers ride rect's. `defineMark` refuses the
141
+ * pairing unless the two passes declare one instance struct.
142
+ */
143
+ readonly bufferOf?: string;
144
+ /**
145
+ * The 256-entry RGBA colour ramp this mark's pass samples this frame, or
146
+ * undefined when it samples none. `createMarkBackend` uploads it per pass and
147
+ * only when the bytes' identity moves, and binds an inert table for the
148
+ * undefined case — a shader owns its sampler unconditionally, and a textured
149
+ * pass with no texture never draws on the WebGPU HAL.
150
+ */
151
+ readonly texture?: (state: TState, region: TRegion) => Uint8Array | undefined;
152
+ drawRegion(hal: GpuHal, scratch: ArrayBuffer, block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState, regionKey: number, staged?: StagedUniforms): void;
153
+ paintBlock(ctx: MarkContext2D, region: TRegion, block: RenderBlock, state: TState): void;
154
+ hitNearest?(region: TRegion, block: RenderBlock, state: TState, xPx: number, yPx: number, candidates: Iterable<number>, maxDistSq: number): MarkHit | undefined;
155
+ }
156
+ /**
157
+ * `{ shape, channels }` over typed arrays — the declaration a display writes in
158
+ * place of a `GpuXxxRenderer` class, a `Canvas2DXxxRenderer` class, a factory
159
+ * and a pass list.
160
+ *
161
+ * `channels` and `params` are projections, not copies: `channels` names which
162
+ * of the region's arrays feed the shape's lanes, and `params` names which of
163
+ * the render state's values reach the uniforms — and which of the region's,
164
+ * for a value the payload carries rather than the frame (the variant matrix's
165
+ * column count). Both run once per block per frame, never per instance.
166
+ *
167
+ * **`channels` answers `undefined` for a region this mark has nothing in.**
168
+ * That is how one mark list serves a display whose cells are a union — synteny
169
+ * draws ribbon cells, outline cells and glyph-lane cells off one list, and a
170
+ * region key holds exactly one of the three. The mark then packs an empty
171
+ * buffer (which every HAL reads as the release), draws nothing and paints
172
+ * nothing, so the kind test is stated once per mark instead of once per
173
+ * backend.
174
+ *
175
+ * **`params` is handed the block**, because the key a display's per-track
176
+ * state is filed under is the block's `displayedRegionIndex` and nothing else
177
+ * in the lens's reach says which track this is. Every method the lens feeds
178
+ * already receives the block; the lens receiving it is what lets it stay a
179
+ * pick rather than pushing the lookup into the shape.
180
+ *
181
+ * `bufferOf` names the mark whose uploaded buffer this one draws from, so the
182
+ * declaration says what the HAL's `drawPass(id, region, bufferPassId)` says:
183
+ * the chevron mark's channels are the line mark's, packed once under the line
184
+ * pass.
185
+ *
186
+ * `texture` is the colour ramp the mark's pass samples, for a shape that
187
+ * resolves a per-instance scalar through a LUT (wiggle density, and the same
188
+ * 256-entry table HiC and LD bind). It is state, not a channel: the backend
189
+ * uploads it once per pass and re-uploads only when the bytes' identity moves,
190
+ * so a ramp change costs one texture and no instance byte.
191
+ *
192
+ * `band` is the strip of the canvas the mark is clipped to, for a display that
193
+ * stacks bands on one canvas (MAF's coverage strip over its rows viewport).
194
+ * The GPU scissors to it and Canvas2D clips to it, a zero-height band draws
195
+ * nothing, and the shape still places Y against the whole canvas — the band is
196
+ * a clip, never an offset, which is what lets one `scrollTop` serve both
197
+ * backends. A mark without one paints wherever its shape puts ink.
198
+ */
199
+ export declare function defineMark<TRegion, TState extends MarkFrame, TChannels, TParams>(spec: {
200
+ shape: MarkShape<TChannels, TParams>;
201
+ channels: (region: TRegion) => TChannels | undefined;
202
+ params: (state: TState, region: TRegion, block: RenderBlock) => TParams;
203
+ bufferOf?: Mark<TRegion, TState>;
204
+ band?: (state: TState) => MarkBand;
205
+ texture?: (state: TState, region: TRegion) => Uint8Array | undefined;
206
+ }): Mark<TRegion, TState>;
@@ -0,0 +1,125 @@
1
+ import { devicePxBand, withClip } from '../canvas2dUtils.js';
2
+ /**
3
+ * `{ shape, channels }` over typed arrays — the declaration a display writes in
4
+ * place of a `GpuXxxRenderer` class, a `Canvas2DXxxRenderer` class, a factory
5
+ * and a pass list.
6
+ *
7
+ * `channels` and `params` are projections, not copies: `channels` names which
8
+ * of the region's arrays feed the shape's lanes, and `params` names which of
9
+ * the render state's values reach the uniforms — and which of the region's,
10
+ * for a value the payload carries rather than the frame (the variant matrix's
11
+ * column count). Both run once per block per frame, never per instance.
12
+ *
13
+ * **`channels` answers `undefined` for a region this mark has nothing in.**
14
+ * That is how one mark list serves a display whose cells are a union — synteny
15
+ * draws ribbon cells, outline cells and glyph-lane cells off one list, and a
16
+ * region key holds exactly one of the three. The mark then packs an empty
17
+ * buffer (which every HAL reads as the release), draws nothing and paints
18
+ * nothing, so the kind test is stated once per mark instead of once per
19
+ * backend.
20
+ *
21
+ * **`params` is handed the block**, because the key a display's per-track
22
+ * state is filed under is the block's `displayedRegionIndex` and nothing else
23
+ * in the lens's reach says which track this is. Every method the lens feeds
24
+ * already receives the block; the lens receiving it is what lets it stay a
25
+ * pick rather than pushing the lookup into the shape.
26
+ *
27
+ * `bufferOf` names the mark whose uploaded buffer this one draws from, so the
28
+ * declaration says what the HAL's `drawPass(id, region, bufferPassId)` says:
29
+ * the chevron mark's channels are the line mark's, packed once under the line
30
+ * pass.
31
+ *
32
+ * `texture` is the colour ramp the mark's pass samples, for a shape that
33
+ * resolves a per-instance scalar through a LUT (wiggle density, and the same
34
+ * 256-entry table HiC and LD bind). It is state, not a channel: the backend
35
+ * uploads it once per pass and re-uploads only when the bytes' identity moves,
36
+ * so a ramp change costs one texture and no instance byte.
37
+ *
38
+ * `band` is the strip of the canvas the mark is clipped to, for a display that
39
+ * stacks bands on one canvas (MAF's coverage strip over its rows viewport).
40
+ * The GPU scissors to it and Canvas2D clips to it, a zero-height band draws
41
+ * nothing, and the shape still places Y against the whole canvas — the band is
42
+ * a clip, never an offset, which is what lets one `scrollTop` serve both
43
+ * backends. A mark without one paints wherever its shape puts ink.
44
+ */
45
+ export function defineMark(spec) {
46
+ const { shape, channels, params, band, texture } = spec;
47
+ const lender = spec.bufferOf?.pass;
48
+ if (lender &&
49
+ (lender.instanceStride !== shape.pass.instanceStride ||
50
+ JSON.stringify(lender.vertexAttributes) !==
51
+ JSON.stringify(shape.pass.vertexAttributes))) {
52
+ throw new Error(`mark ${shape.id} draws off ${lender.id}'s buffer but declares a different instance struct`);
53
+ }
54
+ const bufferOf = lender?.id;
55
+ return {
56
+ pass: {
57
+ ...shape.pass,
58
+ pack: region => {
59
+ const c = channels(region);
60
+ return c === undefined ? NOTHING : shape.pass.pack(c);
61
+ },
62
+ },
63
+ bufferOf,
64
+ texture,
65
+ drawRegion(hal, scratch, block, clip, region, state, regionKey, staged) {
66
+ const strip = band?.(state);
67
+ const scissor = strip
68
+ ? devicePxBand(strip.top, strip.height, clip.scaleY, clip.pxH)
69
+ : undefined;
70
+ if (scissor?.height !== 0 && channels(region) !== undefined) {
71
+ const p = params(state, region, block);
72
+ if (!shape.paintsBlock || shape.paintsBlock(block, state, p)) {
73
+ if (scissor) {
74
+ hal.setScissor(clip.pxX, scissor.top, clip.pxW, scissor.height);
75
+ }
76
+ if (staged?.writer !== shape.writeUniforms ||
77
+ staged.params !== params) {
78
+ shape.writeUniforms(scratch, clip, block, state, p);
79
+ hal.writeUniforms(scratch);
80
+ if (staged) {
81
+ staged.writer = shape.writeUniforms;
82
+ staged.params = params;
83
+ }
84
+ }
85
+ hal.drawPass(shape.pass.id, regionKey, bufferOf);
86
+ if (scissor) {
87
+ hal.setScissor(clip.pxX, 0, clip.pxW, clip.pxH);
88
+ }
89
+ }
90
+ }
91
+ },
92
+ paintBlock(ctx, region, block, state) {
93
+ const strip = band?.(state);
94
+ const c = channels(region);
95
+ if ((!strip || strip.height > 0) && c !== undefined) {
96
+ const p = params(state, region, block);
97
+ if (!shape.paintsBlock || shape.paintsBlock(block, state, p)) {
98
+ const paint = () => {
99
+ shape.paintBlock(ctx, c, block, state, p);
100
+ };
101
+ if (strip) {
102
+ withClip(ctx, 0, strip.top, state.canvasWidth, strip.height, paint);
103
+ }
104
+ else {
105
+ paint();
106
+ }
107
+ }
108
+ }
109
+ },
110
+ hitNearest: shape.hitNearest
111
+ ? (region, block, state, xPx, yPx, candidates, maxDistSq) => {
112
+ const c = channels(region);
113
+ return c === undefined || bandExcludes(band?.(state), yPx)
114
+ ? undefined
115
+ : shape.hitNearest(c, block, state, params(state, region, block), xPx, yPx, candidates, maxDistSq);
116
+ }
117
+ : undefined,
118
+ };
119
+ }
120
+ // What a mark with nothing in this region packs. An empty pack IS the release
121
+ // — every HAL deletes the pass's prior buffer before it looks at the count.
122
+ const NOTHING = new ArrayBuffer(0);
123
+ function bandExcludes(strip, yPx) {
124
+ return (strip !== undefined && (yPx < strip.top || yPx > strip.top + strip.height));
125
+ }
@@ -4,6 +4,8 @@ import type { InstancePass } from './instancePass.ts';
4
4
  import type { RenderBlock } from './renderBlock.ts';
5
5
  import type { FrameDimensions, RenderingBackend } from './renderingBackendBase.ts';
6
6
  export type { FrameDimensions } from './renderingBackendBase.ts';
7
+ /** Straight RGBA, each channel 0-1 — what `beginFrame` takes. */
8
+ export type ClearColor = readonly [number, number, number, number];
7
9
  /**
8
10
  * Shared contract for per-region streamed GPU backends.
9
11
  *
@@ -12,9 +14,8 @@ export type { FrameDimensions } from './renderingBackendBase.ts';
12
14
  * against a data map passed back through `renderBlocks` each frame.
13
15
  *
14
16
  * `UploadData` is what the upload-side autorun pushes per region;
15
- * `RenderData` is what the render-side reads. They default to the same
16
- * type — MAF is the lone case where they diverge (upload payload also
17
- * carries a pre-encoded GPU buffer that the render side doesn't need).
17
+ * `RenderData` is what the render-side reads. They default to the same type,
18
+ * and every per-region display keeps the default today.
18
19
  *
19
20
  * Renderers are stateless: they keep no per-region cache. Canvas2D backends
20
21
  * stub `upload` / `release` as no-ops since they read everything from the
@@ -58,6 +59,15 @@ export declare abstract class Canvas2DPerRegionRenderingBackend<UploadData, Rend
58
59
  upload(): void;
59
60
  release(): void;
60
61
  renderBlocks(blocks: Block[], regions: ReadonlyMap<number, RenderData>, state: RenderState): boolean;
62
+ /**
63
+ * What the frame is painted onto before the blocks are drawn — the Canvas2D
64
+ * half of the GPU base's `beginFrame` clear. Undefined, the default, leaves
65
+ * `prepareCanvas`'s `clearRect` as the whole of it, which is transparent.
66
+ * Synteny is the one display that overrides: its indel wedges pre-blend
67
+ * against a KNOWN ground and only agree with the base ribbon beside them over
68
+ * a destination that really is that colour.
69
+ */
70
+ protected clearColor(_state: RenderState): ClearColor | undefined;
61
71
  /**
62
72
  * Paint the visible blocks into `this.ctx` (already DPR-prepared). Delegates
63
73
  * to the plugin's pure `drawXxxBlocks(ctx, regions, blocks, state)` function.
@@ -111,4 +121,6 @@ export declare abstract class GpuPerRegionRenderingBackend<UploadData, RenderSta
111
121
  * those whose draw state is fully in the uploaded buffer).
112
122
  */
113
123
  protected abstract drawRegion(block: Block, clip: BlockClipResult, region: RenderData, state: RenderState): void;
124
+ /** @see Canvas2DPerRegionRenderingBackend.clearColor */
125
+ protected clearColor(_state: RenderState): ClearColor;
114
126
  }
@@ -2,6 +2,7 @@ import { clipBlock } from './blockClipUtils.js';
2
2
  import { prepareCanvas } from './canvas2dUtils.js';
3
3
  import { uploadPass } from './instancePass.js';
4
4
  import { Canvas2DRenderingBackendBase, GpuRenderingBackendBase, } from './renderingBackendBase.js';
5
+ const TRANSPARENT = [0, 0, 0, 0];
5
6
  /**
6
7
  * Canvas2D-side base for `PerRegionRenderingBackend` implementations. Inherits
7
8
  * the `canvas` + 2D context (and no-op `dispose`) from
@@ -21,9 +22,26 @@ export class Canvas2DPerRegionRenderingBackend extends Canvas2DRenderingBackendB
21
22
  release() { }
22
23
  renderBlocks(blocks, regions, state) {
23
24
  prepareCanvas(this.canvas, this.ctx, state.canvasWidth, state.canvasHeight);
25
+ const clear = this.clearColor(state);
26
+ if (clear) {
27
+ const [r, g, b, a] = clear;
28
+ this.ctx.fillStyle = `rgba(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)},${a})`;
29
+ this.ctx.fillRect(0, 0, state.canvasWidth, state.canvasHeight);
30
+ }
24
31
  this.draw(blocks, regions, state);
25
32
  return blocks.some(block => regions.has(block.displayedRegionIndex));
26
33
  }
34
+ /**
35
+ * What the frame is painted onto before the blocks are drawn — the Canvas2D
36
+ * half of the GPU base's `beginFrame` clear. Undefined, the default, leaves
37
+ * `prepareCanvas`'s `clearRect` as the whole of it, which is transparent.
38
+ * Synteny is the one display that overrides: its indel wedges pre-blend
39
+ * against a KNOWN ground and only agree with the base ribbon beside them over
40
+ * a destination that really is that colour.
41
+ */
42
+ clearColor(_state) {
43
+ return undefined;
44
+ }
27
45
  }
28
46
  /**
29
47
  * GPU-side base for `PerRegionRenderingBackend` implementations. Inherits the
@@ -70,9 +88,10 @@ export class GpuPerRegionRenderingBackend extends GpuRenderingBackendBase {
70
88
  // window, so a track dragged past the clamp draws at reduced resolution
71
89
  // instead of asking for a viewport its target cannot hold.
72
90
  const scale = this.hal.resize(canvasWidth, canvasHeight);
73
- // Always pair beginFrame/endFrame so the canvas clears to transparent even
74
- // when every block is skipped (e.g. all regions pruned by a density gate).
75
- this.hal.beginFrame(0, 0, 0, 0);
91
+ // Always pair beginFrame/endFrame so the canvas clears even when every
92
+ // block is skipped (e.g. all regions pruned by a density gate).
93
+ const [clearR, clearG, clearB, clearA] = this.clearColor(state);
94
+ this.hal.beginFrame(clearR, clearG, clearB, clearA);
76
95
  for (const block of blocks) {
77
96
  const region = regions.get(block.displayedRegionIndex);
78
97
  if (region !== undefined) {
@@ -90,4 +109,8 @@ export class GpuPerRegionRenderingBackend extends GpuRenderingBackendBase {
90
109
  this.hal.endFrame();
91
110
  return painted;
92
111
  }
112
+ /** @see Canvas2DPerRegionRenderingBackend.clearColor */
113
+ clearColor(_state) {
114
+ return TRANSPARENT;
115
+ }
93
116
  }
@@ -17,8 +17,8 @@ export declare function isCheckedAtTheStore(map: ReadonlyMap<unknown, unknown>):
17
17
  * observable object graph — the stored value is not the object the worker
18
18
  * produced. A multi-wiggle region is one atom per field per source, so a
19
19
  * thousand-sample track pays ~18k on every pan; MAF pays the whole set again
20
- * for every cached region on every row reorder, since `placeFetchedRows`
21
- * re-places them all.
20
+ * for every cached region on every row reorder, since a reorder re-places
21
+ * them all.
22
22
  * - **Read.** Each field access goes through `getObservablePropValue_`. Hot
23
23
  * loops that hoist their typed arrays are fine, but the ones that don't were
24
24
  * paying it per iteration.
@@ -27,8 +27,8 @@ export function isCheckedAtTheStore(map) {
27
27
  * observable object graph — the stored value is not the object the worker
28
28
  * produced. A multi-wiggle region is one atom per field per source, so a
29
29
  * thousand-sample track pays ~18k on every pan; MAF pays the whole set again
30
- * for every cached region on every row reorder, since `placeFetchedRows`
31
- * re-places them all.
30
+ * for every cached region on every row reorder, since a reorder re-places
31
+ * them all.
32
32
  * - **Read.** Each field access goes through `getObservablePropValue_`. Hot
33
33
  * loops that hoist their typed arrays are fine, but the ones that don't were
34
34
  * paying it per iteration.
@@ -1,22 +1,19 @@
1
1
  import type { GpuHal } from './hal/types.ts';
2
2
  /**
3
3
  * Minimum render-state shape a frame scaffold needs: the CSS-pixel canvas
4
- * dimensions used to size the backing store, and — on the per-region side —
5
- * to clip each block.
4
+ * dimensions used to size the backing store, and to clip each block.
6
5
  *
7
6
  * Here rather than beside either family because both bound their render state
8
- * by it: `GpuPerRegionRenderingBackend.renderBlocks` and
9
- * `GpuGlobalRenderingBackend.render` open with the same `hal.resize`, and the
10
- * two Canvas2D bases with the same `prepareCanvas`.
7
+ * by it: `GpuPerRegionRenderingBackend.renderBlocks` opens with `hal.resize`,
8
+ * and its Canvas2D twin with `prepareCanvas`.
11
9
  */
12
10
  export interface FrameDimensions {
13
11
  canvasWidth: number;
14
12
  canvasHeight: number;
15
13
  }
16
14
  /**
17
- * What every rendering backend has, whatever its upload shape — the three
18
- * shape contracts (`PerRegionRenderingBackend`, `GlobalRenderingBackend`,
19
- * `KeyedRenderingBackend`) all extend this, and `useRenderingBackend` is
15
+ * What every rendering backend has, whatever its upload shape —
16
+ * `PerRegionRenderingBackend` extends this, and `useRenderingBackend` is
20
17
  * bounded by it.
21
18
  *
22
19
  * `setErrorHandler` is here rather than optional at the hook because it was
@@ -40,8 +37,8 @@ export interface RenderingBackend {
40
37
  dispose(): void;
41
38
  }
42
39
  /**
43
- * Shared GPU-side state for every GPU rendering backend — both per-region
44
- * (`GpuPerRegionRenderingBackend`) and monolithic (`GpuGlobalRenderingBackend`).
40
+ * Shared GPU-side state for every GPU rendering backend
41
+ * (`GpuPerRegionRenderingBackend`, and the shared-canvas backends over it).
45
42
  * Owns the `hal` reference and a pre-allocated uniform scratch `ArrayBuffer`
46
43
  * reused across frames to avoid per-frame GC churn. `dispose()` delegates to
47
44
  * the HAL. Subclasses add only their upload/render shape.
@@ -49,14 +46,14 @@ export interface RenderingBackend {
49
46
  export declare abstract class GpuRenderingBackendBase {
50
47
  protected hal: GpuHal;
51
48
  protected uniformData: ArrayBuffer;
52
- constructor(hal: GpuHal, uniformByteSize: number);
49
+ constructor(hal: GpuHal);
53
50
  setErrorHandler(handler: (error: Error) => void): void;
54
51
  dispose(): void;
55
52
  }
56
53
  /**
57
- * Shared Canvas2D-side state for every Canvas2D rendering backend — both
58
- * per-region (`Canvas2DPerRegionRenderingBackend`) and monolithic
59
- * (`Canvas2DGlobalRenderingBackend`). Owns the `canvas` and its 2D context
54
+ * Shared Canvas2D-side state for every Canvas2D rendering backend
55
+ * (`Canvas2DPerRegionRenderingBackend`, and the shared-canvas backends over
56
+ * it). Owns the `canvas` and its 2D context
60
57
  * (constructor throws if unavailable). `dispose()` is a no-op since Canvas2D
61
58
  * holds no GPU resources. Subclasses add only their render shape and stub the
62
59
  * upload/prune hooks their interface requires (everything flows through the
@@ -1,7 +1,7 @@
1
1
  import { acquireCanvas2D } from './canvasContext.js';
2
2
  /**
3
- * Shared GPU-side state for every GPU rendering backend — both per-region
4
- * (`GpuPerRegionRenderingBackend`) and monolithic (`GpuGlobalRenderingBackend`).
3
+ * Shared GPU-side state for every GPU rendering backend
4
+ * (`GpuPerRegionRenderingBackend`, and the shared-canvas backends over it).
5
5
  * Owns the `hal` reference and a pre-allocated uniform scratch `ArrayBuffer`
6
6
  * reused across frames to avoid per-frame GC churn. `dispose()` delegates to
7
7
  * the HAL. Subclasses add only their upload/render shape.
@@ -9,9 +9,9 @@ import { acquireCanvas2D } from './canvasContext.js';
9
9
  export class GpuRenderingBackendBase {
10
10
  hal;
11
11
  uniformData;
12
- constructor(hal, uniformByteSize) {
12
+ constructor(hal) {
13
13
  this.hal = hal;
14
- this.uniformData = new ArrayBuffer(uniformByteSize);
14
+ this.uniformData = new ArrayBuffer(hal.uniformByteSize);
15
15
  }
16
16
  // Forward OOM / over-limit allocation failures from the HAL to the display's
17
17
  // renderError. Wired by useRenderingBackend once the backend is live.
@@ -23,9 +23,9 @@ export class GpuRenderingBackendBase {
23
23
  }
24
24
  }
25
25
  /**
26
- * Shared Canvas2D-side state for every Canvas2D rendering backend — both
27
- * per-region (`Canvas2DPerRegionRenderingBackend`) and monolithic
28
- * (`Canvas2DGlobalRenderingBackend`). Owns the `canvas` and its 2D context
26
+ * Shared Canvas2D-side state for every Canvas2D rendering backend
27
+ * (`Canvas2DPerRegionRenderingBackend`, and the shared-canvas backends over
28
+ * it). Owns the `canvas` and its 2D context
29
29
  * (constructor throws if unavailable). `dispose()` is a no-op since Canvas2D
30
30
  * holds no GPU resources. Subclasses add only their render shape and stub the
31
31
  * upload/prune hooks their interface requires (everything flows through the
@@ -0,0 +1 @@
1
+ export declare function capsuleDistPx(alongPx: number, acrossPx: number, halfLenPx: number): number;
@@ -0,0 +1,11 @@
1
+ // AUTO-GENERATED by packages/shader-tools/src/shader-codegen from capsule.slang.
2
+ // Do not edit. Run `pnpm gen:shaders` to regenerate.
3
+ // Scalar twins of capsule.slang, transliterated from slangc's WGSL so
4
+ // the Canvas2D and SVG paths run the shader's own math. See adr-051.
5
+ function _max(a, b) {
6
+ return b > a || Number.isNaN(a) ? b : a;
7
+ }
8
+ export function capsuleDistPx(alongPx, acrossPx, halfLenPx) {
9
+ let _t0 = _max((Math.abs(alongPx) - halfLenPx), 0.0);
10
+ return Math.sqrt(((_t0 * _t0) + (acrossPx * acrossPx)));
11
+ }