@jbrowse/render-core 5.0.0-beta.8 → 5.0.0-beta.9

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 (174) hide show
  1. package/esm/RenderCanvas.d.ts +2 -2
  2. package/esm/RenderLifecycleMixin.d.ts +7 -6
  3. package/esm/RenderLifecycleMixin.js +7 -6
  4. package/esm/canvas2dUtils.d.ts +58 -17
  5. package/esm/canvas2dUtils.js +82 -30
  6. package/esm/computePipeline.d.ts +4 -3
  7. package/esm/computePipeline.js +21 -14
  8. package/esm/coverageBand.d.ts +3 -3
  9. package/esm/coverageBand.js +2 -2
  10. package/esm/coverageBandBuffers.d.ts +2 -3
  11. package/esm/createRenderingBackend.d.ts +1 -1
  12. package/esm/displayPhase.d.ts +79 -109
  13. package/esm/displayPhase.js +42 -83
  14. package/esm/gpuDevice.d.ts +11 -10
  15. package/esm/gpuDevice.js +10 -11
  16. package/esm/graphicsCapabilities.d.ts +6 -7
  17. package/esm/graphicsCapabilities.js +3 -4
  18. package/esm/hal/createHal.js +1 -1
  19. package/esm/hal/deviceGpuCache.d.ts +25 -30
  20. package/esm/hal/deviceGpuCache.js +85 -97
  21. package/esm/hal/index.d.ts +2 -2
  22. package/esm/hal/index.js +1 -1
  23. package/esm/hal/mockHal.d.ts +8 -6
  24. package/esm/hal/mockHal.js +7 -2
  25. package/esm/hal/passIds.d.ts +8 -7
  26. package/esm/hal/passIds.js +11 -10
  27. package/esm/hal/types.d.ts +28 -12
  28. package/esm/hal/webgl2Hal.d.ts +11 -3
  29. package/esm/hal/webgl2Hal.js +116 -145
  30. package/esm/hal/webgpuHal.d.ts +8 -8
  31. package/esm/hal/webgpuHal.js +97 -124
  32. package/esm/instancePass.d.ts +2 -1
  33. package/esm/mapUploadSync.d.ts +2 -2
  34. package/esm/mapUploadSync.js +2 -2
  35. package/esm/marks/barMark.d.ts +6 -3
  36. package/esm/marks/barMark.js +19 -13
  37. package/esm/marks/drawAgainstHit.d.ts +58 -39
  38. package/esm/marks/drawAgainstHit.js +136 -96
  39. package/esm/marks/glyphPaint.d.ts +14 -2
  40. package/esm/marks/glyphPaint.js +16 -2
  41. package/esm/marks/index.d.ts +4 -2
  42. package/esm/marks/index.js +3 -2
  43. package/esm/marks/markBackend.d.ts +2 -2
  44. package/esm/marks/markBackend.js +2 -2
  45. package/esm/marks/markHit.d.ts +2 -2
  46. package/esm/marks/markHit.js +2 -2
  47. package/esm/marks/markPlan.d.ts +2 -2
  48. package/esm/marks/markPlan.js +2 -2
  49. package/esm/marks/markRamp.d.ts +1 -3
  50. package/esm/marks/markRamp.js +1 -5
  51. package/esm/marks/nearestMarkHit.d.ts +49 -0
  52. package/esm/marks/nearestMarkHit.js +126 -0
  53. package/esm/marks/pointMark.d.ts +9 -2
  54. package/esm/marks/pointMark.js +36 -31
  55. package/esm/marks/rowLane.d.ts +17 -0
  56. package/esm/marks/rowLane.js +11 -0
  57. package/esm/marks/spanMark.d.ts +6 -26
  58. package/esm/marks/spanMark.js +34 -31
  59. package/esm/marks/types.d.ts +51 -181
  60. package/esm/marks/types.js +91 -131
  61. package/esm/namedReactions.d.ts +3 -4
  62. package/esm/namedReactions.js +3 -4
  63. package/esm/scoreScale.d.ts +20 -0
  64. package/esm/scoreScale.js +77 -0
  65. package/esm/shaders/barMark.generated.d.ts +2 -3
  66. package/esm/shaders/barMark.generated.js +4 -3
  67. package/esm/shaders/barMark.glsl.generated.d.ts +2 -0
  68. package/esm/shaders/barMark.glsl.generated.js +4 -0
  69. package/esm/shaders/barMark.iface.generated.d.ts +12 -6
  70. package/esm/shaders/barMark.iface.generated.js +27 -15
  71. package/esm/shaders/barMark.wgsl.generated.d.ts +1 -0
  72. package/esm/shaders/barMark.wgsl.generated.js +3 -0
  73. package/esm/shaders/coverageBar.generated.d.ts +2 -3
  74. package/esm/shaders/coverageBar.generated.js +4 -3
  75. package/esm/shaders/coverageBar.glsl.generated.d.ts +2 -0
  76. package/esm/shaders/coverageBar.glsl.generated.js +4 -0
  77. package/esm/shaders/coverageBar.iface.generated.js +1 -1
  78. package/esm/shaders/coverageBar.wgsl.generated.d.ts +1 -0
  79. package/esm/shaders/coverageBar.wgsl.generated.js +3 -0
  80. package/esm/shaders/coverageIndicator.generated.d.ts +2 -3
  81. package/esm/shaders/coverageIndicator.generated.js +4 -3
  82. package/esm/shaders/coverageIndicator.glsl.generated.d.ts +2 -0
  83. package/esm/shaders/coverageIndicator.glsl.generated.js +4 -0
  84. package/esm/shaders/coverageIndicator.iface.generated.js +1 -1
  85. package/esm/shaders/coverageIndicator.wgsl.generated.d.ts +1 -0
  86. package/esm/shaders/coverageIndicator.wgsl.generated.js +3 -0
  87. package/esm/shaders/coverageInterbase.generated.d.ts +2 -3
  88. package/esm/shaders/coverageInterbase.generated.js +4 -3
  89. package/esm/shaders/coverageInterbase.glsl.generated.d.ts +2 -0
  90. package/esm/shaders/coverageInterbase.glsl.generated.js +4 -0
  91. package/esm/shaders/coverageInterbase.iface.generated.js +1 -1
  92. package/esm/shaders/coverageInterbase.wgsl.generated.d.ts +1 -0
  93. package/esm/shaders/coverageInterbase.wgsl.generated.js +3 -0
  94. package/esm/shaders/coverageMod.generated.d.ts +2 -3
  95. package/esm/shaders/coverageMod.generated.js +4 -3
  96. package/esm/shaders/coverageMod.glsl.generated.d.ts +2 -0
  97. package/esm/shaders/coverageMod.glsl.generated.js +4 -0
  98. package/esm/shaders/coverageMod.iface.generated.js +1 -1
  99. package/esm/shaders/coverageMod.wgsl.generated.d.ts +1 -0
  100. package/esm/shaders/coverageMod.wgsl.generated.js +3 -0
  101. package/esm/shaders/coverageSnp.generated.d.ts +2 -3
  102. package/esm/shaders/coverageSnp.generated.js +4 -3
  103. package/esm/shaders/coverageSnp.glsl.generated.d.ts +2 -0
  104. package/esm/shaders/coverageSnp.glsl.generated.js +4 -0
  105. package/esm/shaders/coverageSnp.iface.generated.js +1 -1
  106. package/esm/shaders/coverageSnp.wgsl.generated.d.ts +1 -0
  107. package/esm/shaders/coverageSnp.wgsl.generated.js +3 -0
  108. package/esm/shaders/pointMark.generated.d.ts +2 -3
  109. package/esm/shaders/pointMark.generated.js +4 -3
  110. package/esm/shaders/pointMark.glsl.generated.d.ts +2 -0
  111. package/esm/shaders/pointMark.glsl.generated.js +4 -0
  112. package/esm/shaders/pointMark.iface.generated.d.ts +11 -3
  113. package/esm/shaders/pointMark.iface.generated.js +23 -9
  114. package/esm/shaders/pointMark.js.generated.d.ts +1 -1
  115. package/esm/shaders/pointMark.js.generated.js +51 -29
  116. package/esm/shaders/pointMark.wgsl.generated.d.ts +1 -0
  117. package/esm/shaders/pointMark.wgsl.generated.js +3 -0
  118. package/esm/shaders/scoreScale.js.generated.js +46 -22
  119. package/esm/shaders/spanMark.generated.d.ts +2 -3
  120. package/esm/shaders/spanMark.generated.js +4 -3
  121. package/esm/shaders/spanMark.glsl.generated.d.ts +2 -0
  122. package/esm/shaders/spanMark.glsl.generated.js +4 -0
  123. package/esm/shaders/spanMark.iface.generated.js +1 -1
  124. package/esm/shaders/spanMark.wgsl.generated.d.ts +1 -0
  125. package/esm/shaders/spanMark.wgsl.generated.js +3 -0
  126. package/esm/slangPass.d.ts +9 -10
  127. package/esm/slangPass.js +9 -3
  128. package/esm/useGraphicsCapabilities.d.ts +1 -1
  129. package/esm/useGraphicsCapabilities.js +1 -1
  130. package/esm/webgpuUtils.d.ts +3 -11
  131. package/esm/webgpuUtils.js +12 -33
  132. package/package.json +3 -2
  133. package/src/shaders/barMark.generated.ts +6 -5
  134. package/src/shaders/barMark.glsl.generated.ts +6 -0
  135. package/src/shaders/barMark.iface.generated.ts +31 -15
  136. package/src/shaders/barMark.slang +11 -3
  137. package/src/shaders/barMark.wgsl.generated.ts +4 -0
  138. package/src/shaders/colorPack.slang +3 -2
  139. package/src/shaders/coverageBand.slang +5 -11
  140. package/src/shaders/coverageBar.generated.ts +6 -5
  141. package/src/shaders/coverageBar.glsl.generated.ts +6 -0
  142. package/src/shaders/coverageBar.iface.generated.ts +1 -1
  143. package/src/shaders/coverageBar.slang +1 -1
  144. package/src/shaders/coverageBar.wgsl.generated.ts +4 -0
  145. package/src/shaders/coverageIndicator.generated.ts +6 -5
  146. package/src/shaders/coverageIndicator.glsl.generated.ts +6 -0
  147. package/src/shaders/coverageIndicator.iface.generated.ts +1 -1
  148. package/src/shaders/coverageIndicator.wgsl.generated.ts +4 -0
  149. package/src/shaders/coverageInterbase.generated.ts +6 -5
  150. package/src/shaders/coverageInterbase.glsl.generated.ts +6 -0
  151. package/src/shaders/coverageInterbase.iface.generated.ts +1 -1
  152. package/src/shaders/coverageInterbase.wgsl.generated.ts +4 -0
  153. package/src/shaders/coverageMod.generated.ts +6 -5
  154. package/src/shaders/coverageMod.glsl.generated.ts +6 -0
  155. package/src/shaders/coverageMod.iface.generated.ts +1 -1
  156. package/src/shaders/coverageMod.wgsl.generated.ts +4 -0
  157. package/src/shaders/coverageSnp.generated.ts +6 -5
  158. package/src/shaders/coverageSnp.glsl.generated.ts +6 -0
  159. package/src/shaders/coverageSnp.iface.generated.ts +1 -1
  160. package/src/shaders/coverageSnp.wgsl.generated.ts +4 -0
  161. package/src/shaders/hpmath.slang +18 -6
  162. package/src/shaders/pointMark.generated.ts +6 -6
  163. package/src/shaders/pointMark.glsl.generated.ts +6 -0
  164. package/src/shaders/pointMark.iface.generated.ts +28 -9
  165. package/src/shaders/pointMark.js.generated.ts +48 -29
  166. package/src/shaders/pointMark.slang +18 -5
  167. package/src/shaders/pointMark.wgsl.generated.ts +4 -0
  168. package/src/shaders/scoreScale.js.generated.ts +43 -22
  169. package/src/shaders/scoreScale.slang +37 -53
  170. package/src/shaders/spanMark.generated.ts +6 -5
  171. package/src/shaders/spanMark.glsl.generated.ts +6 -0
  172. package/src/shaders/spanMark.iface.generated.ts +1 -1
  173. package/src/shaders/spanMark.wgsl.generated.ts +4 -0
  174. package/src/shaders/valueScale.slang +8 -45
@@ -1,12 +1,12 @@
1
- import { inkOnRect, shapeHitNearest } from './markHit.js';
1
+ import { inkOnRect } from './markHit.js';
2
2
  const IDENTITY = [1, 0, 0, 1, 0, 0];
3
3
  /**
4
4
  * A `MarkContext2D` that records every rect a painter fills, in paint order and
5
5
  * in CANVAS coordinates — `fillRect` and path `rect` alike, so a shape that
6
6
  * batches by colour records the same list as one that fills per instance. A
7
7
  * path traced with `moveTo`/`lineTo`/`bezierCurveTo` and filled records its
8
- * bounding box, which is what a shape whose hit test answers a polygon's box
9
- * (the variant inversion triangle) is held to; a curve contributes its control
8
+ * bounding box, the extent a shape whose hit test answers a polygon's box
9
+ * (the variant inversion triangle) is checked against; a curve contributes its control
10
10
  * points, whose hull contains it.
11
11
  *
12
12
  * A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
@@ -209,64 +209,125 @@ function unionBox(rs) {
209
209
  // arithmetic over the same projection, so a real disagreement is orders of
210
210
  // magnitude above this and a rounding one is below it.
211
211
  const EPS = 1e-9;
212
+ const MAX_VIOLATIONS = 10;
213
+ /**
214
+ * The ink clause of {@link sweepMarkAgainstHit} alone, for a mark with no
215
+ * `hitNearest`: `boxes[i]` is what the painter recorded for instance `i`.
216
+ */
217
+ export function inkViolations(mark, channels, block, state, boxes, inkSlackPx) {
218
+ const violations = [];
219
+ if (!mark.ink) {
220
+ return violations;
221
+ }
222
+ for (let i = 0; i < boxes.length; i++) {
223
+ const r = mark.ink(channels, block, state, i);
224
+ const b = boxes[i];
225
+ if (r && !b) {
226
+ violations.push(`${i} painted nothing but has ink`);
227
+ }
228
+ else if (b && !r) {
229
+ violations.push(`${i} painted but has no ink`);
230
+ }
231
+ else if (b && r) {
232
+ const gaps = [
233
+ b.x - r.left,
234
+ r.left + r.width - (b.x + b.w),
235
+ b.y - r.top,
236
+ r.top + r.height - (b.y + b.h),
237
+ ];
238
+ if (gaps.some(g => g < -EPS)) {
239
+ violations.push(`${i} painted (${b.x}, ${b.y}, ${b.w}, ${b.h}) outside its ink (${r.left}, ${r.top}, ${r.width}, ${r.height})`);
240
+ }
241
+ else if (gaps.some(g => g > inkSlackPx + EPS)) {
242
+ violations.push(`${i}'s ink (${r.left}, ${r.top}, ${r.width}, ${r.height}) is more than ${inkSlackPx}px outside its painting (${b.x}, ${b.y}, ${b.w}, ${b.h})`);
243
+ }
244
+ }
245
+ if (violations.length >= MAX_VIOLATIONS) {
246
+ return violations;
247
+ }
248
+ }
249
+ return violations;
250
+ }
251
+ function hitViolations(hit, boxes, x, y) {
252
+ const violations = [];
253
+ const box = hit && boxes[hit.index];
254
+ if (hit && !box) {
255
+ violations.push(`(${x}, ${y}) answered ${hit.index}, which painted nothing`);
256
+ }
257
+ else if (hit && box) {
258
+ if (!contains(box, hit.x, hit.y, EPS)) {
259
+ violations.push(`(${x}, ${y}) answered ${hit.index} with ink at (${hit.x}, ${hit.y}), off its painting`);
260
+ }
261
+ const toBox = inkOnRect(x, y, box.x, box.y, box.w, box.h).distSq;
262
+ if (hit.distSq < toBox - EPS) {
263
+ violations.push(`(${x}, ${y}) answered ${hit.index} at ${hit.distSq}, nearer than its painting (${toBox})`);
264
+ }
265
+ const toPoint = (x - hit.x) ** 2 + (y - hit.y) ** 2;
266
+ if (Math.abs(hit.distSq - toPoint) > EPS * Math.max(1, toPoint)) {
267
+ violations.push(`(${x}, ${y}) answered ${hit.index} at ${hit.distSq}, which is not the distance to the ink it named (${toPoint})`);
268
+ }
269
+ }
270
+ return violations;
271
+ }
212
272
  /**
213
273
  * The draw-against-hit gate: paint the block, then walk it in `step` px and
214
274
  * hold `hitNearest` to what the painter actually put on the canvas.
215
275
  *
216
276
  * **What the painter drew for instance `i` has to be attributed to `i`,** and
217
- * there are two ways to get that. A shape whose every instance is one
218
- * `fillRect` gets it positionally, which is the default and needs nothing. A
219
- * shape that batches a colour run into one path (`point`'s glyphs), or that
220
- * skips an instance the view has scrolled past (`cell`'s off-canvas rows),
221
- * cannot be read that way at all — the batch has fewer rects than instances, or
222
- * the wrong ones. `sliceOne` is the way in for those: hand back one instance's
223
- * channels and the sweep paints it alone, so the attribution holds by
224
- * construction and a culled instance is visibly an empty painting.
277
+ * there are two ways to get that. A mark whose every instance is one `fillRect`
278
+ * gets it positionally, which is the default and needs nothing. A mark that
279
+ * batches a colour run into one path (`point`'s glyphs), or that skips an
280
+ * instance the view has scrolled past (`cell`'s off-canvas rows), cannot be read
281
+ * that way at all — the batch has fewer rects than instances, or the wrong ones.
282
+ * `sliceOne` is the way in for those: hand back one instance's channels and the
283
+ * sweep paints it alone, so the attribution holds by construction and a culled
284
+ * instance is visibly an empty painting.
225
285
  *
226
- * Four claims, all of them one-directional and true of every shape:
286
+ * Four claims, all of them one-directional and true of every mark:
227
287
  *
228
288
  * - an instance that painted nothing is never the answer,
229
- * - `hit.x`/`hit.y` — where the shape says its ink is — lies inside the box the
289
+ * - `hit.x`/`hit.y` — where the mark says its ink is — lies inside the box the
230
290
  * painter drew for the instance the hit names,
231
291
  * - `hit.distSq` is no smaller than the distance to that box, since the box
232
292
  * contains the ink and cannot be farther than it,
233
293
  * - `hit.distSq` IS the distance to `hit.x`/`hit.y`, which is `MarkHit`'s own
234
- * contract rather than a fact about the painting: a shape that answers a
294
+ * contract rather than a fact about the painting: a mark that answers a
235
295
  * distance its own reported point does not support has two spellings of where
236
296
  * its ink is, and the nearest-wins walk is resolved by the one the caller
237
297
  * cannot see.
238
298
  *
239
- * The fourth claim is the strongest and is not universal: **a point on the box
240
- * has to answer that box**, the last-painted one where boxes overlap, since the
241
- * sweep hands candidates back to front. It holds only where the painted box IS
242
- * the hit target — every `fillRect` shape, and `cell`'s inversion triangle,
243
- * which answers as its bounding box on purpose — and not for a glyph, whose box
244
- * is a superset of its ink. So it runs exactly when the batch painted one rect
245
- * per instance, which is that set.
299
+ * The fifth is the containment rule: **a point the rule contains answers the
300
+ * nearest painted instance it contains**, the last-painted where tied, since the
301
+ * sweep hands candidates back to front. The default rule is the painted box, so
302
+ * a point on a box answers that box, and off every box a hit target wider than
303
+ * its ink may still answer. The default holds only where the box IS the hit
304
+ * target — every `fillRect` mark, and `cell`'s inversion triangle, which answers
305
+ * as its bounding box on purpose — and not for a glyph, whose box is a superset
306
+ * of its ink, so it runs exactly when the batch painted one rect per instance. A
307
+ * mark whose hit rule is not its box (the pileup's bp containment) passes
308
+ * `contains`, which is then the whole rule: the sweep covers the whole canvas,
309
+ * and a point the rule contains nowhere answers nothing.
246
310
  *
247
- * A shape declaring `ink` is held to a second clause, per instance: the box
248
- * the painter recorded for `i` lies inside `ink(i)`, and each edge of `ink(i)`
249
- * lies within `inkSlackPx` (default 1) of the painting's — so the rect is
250
- * tight to the ink within a pixel on every side, which admits a seam overdraw
251
- * or a pixel snap and rejects a rect that is the wrong row, the wrong width or
252
- * the whole canvas. An instance that painted nothing must answer no ink, and
253
- * one with ink must have painted.
311
+ * A mark declaring `ink` is held to one more clause, per instance: the box the
312
+ * painter recorded for `i` lies inside `ink(i)`, and each edge of `ink(i)` lies
313
+ * within `inkSlackPx` (default 1) of the painting's — so the rect is tight to
314
+ * the ink within a pixel on every side, which admits a seam overdraw or a pixel
315
+ * snap and rejects a rect that is the wrong row, the wrong width or the whole
316
+ * canvas. An instance that painted nothing must answer no ink, and one with ink
317
+ * must have painted.
254
318
  *
255
319
  * Returns the violations rather than asserting, so a test reads as
256
- * `expect(sweep(...)).toEqual([])`. `maxDistSq` is the caller's, because a
257
- * shape whose hit target is wider than its ink (`point`) needs a bound that
258
- * admits the whole rect.
320
+ * `expect(sweep(...)).toEqual([])`. `maxDistSq` is the bound the hit test is
321
+ * asked with, `Infinity` unless the caller's differs.
259
322
  */
260
- export function sweepDrawAgainstHit(shape, channels, block, frame, params, { maxDistSq, step = 0.5, sliceOne, inkSlackPx = 1, }) {
323
+ export function sweepMarkAgainstHit(mark, channels, block, state, { maxDistSq = Infinity, step = 0.5, sliceOne, inkSlackPx = 1, contains: rule, } = {}) {
261
324
  const { count } = channels;
262
- const violations = [];
263
325
  const paint = (c) => {
264
326
  const { ctx, calls } = recordingContext();
265
- shape.paintBlock(ctx, c, block, frame, params);
327
+ mark.paintBlock(ctx, c, block, state);
266
328
  return calls;
267
329
  };
268
330
  const rects = paint(channels);
269
- // Positional attribution, and the licence for the containment claim below.
270
331
  const perRect = rects.length === count;
271
332
  if (!perRect && !sliceOne) {
272
333
  return [`painted ${rects.length} rects for ${count} instances`];
@@ -274,40 +335,6 @@ export function sweepDrawAgainstHit(shape, channels, block, frame, params, { max
274
335
  const boxes = sliceOne
275
336
  ? Array.from({ length: count }, (_, i) => unionBox(paint(sliceOne(channels, i))))
276
337
  : rects.map(r => ({ x: r.x, y: r.y, w: r.w, h: r.h }));
277
- if (shape.ink) {
278
- for (let i = 0; i < count; i++) {
279
- const r = shape.ink(channels, block, frame, params, i);
280
- const b = boxes[i];
281
- if (r && !b) {
282
- violations.push(`${i} painted nothing but has ink`);
283
- }
284
- else if (b && !r) {
285
- violations.push(`${i} painted but has no ink`);
286
- }
287
- else if (b && r) {
288
- const gaps = [
289
- b.x - r.left,
290
- r.left + r.width - (b.x + b.w),
291
- b.y - r.top,
292
- r.top + r.height - (b.y + b.h),
293
- ];
294
- if (gaps.some(g => g < -EPS)) {
295
- violations.push(`${i} painted (${b.x}, ${b.y}, ${b.w}, ${b.h}) outside its ink (${r.left}, ${r.top}, ${r.width}, ${r.height})`);
296
- }
297
- else if (gaps.some(g => g > inkSlackPx + EPS)) {
298
- violations.push(`${i}'s ink (${r.left}, ${r.top}, ${r.width}, ${r.height}) is more than ${inkSlackPx}px outside its painting (${b.x}, ${b.y}, ${b.w}, ${b.h})`);
299
- }
300
- }
301
- if (violations.length >= 10) {
302
- return violations;
303
- }
304
- }
305
- }
306
- const hitNearest = shapeHitNearest(shape);
307
- if (!hitNearest) {
308
- return [`${shape.id} has no hit test`];
309
- }
310
- const candidates = Array.from({ length: count }, (_, k) => count - 1 - k);
311
338
  let yMin = Infinity;
312
339
  let yMax = -Infinity;
313
340
  for (const b of boxes) {
@@ -319,44 +346,57 @@ export function sweepDrawAgainstHit(shape, channels, block, frame, params, { max
319
346
  if (yMin === Infinity) {
320
347
  return [`painted nothing for ${count} instances`];
321
348
  }
349
+ const violations = inkViolations(mark, channels, block, state, boxes, inkSlackPx);
350
+ if (violations.length >= MAX_VIOLATIONS) {
351
+ return violations;
352
+ }
353
+ if (!mark.hitNearest) {
354
+ return ['no hit test'];
355
+ }
356
+ const claims = rule ??
357
+ (perRect
358
+ ? (k, x, y) => contains(boxes[k], x, y)
359
+ : undefined);
360
+ const candidates = Array.from({ length: count }, (_, k) => count - 1 - k);
322
361
  const x0 = Math.min(block.screenStartPx, block.screenEndPx) - 1;
323
362
  const x1 = Math.max(block.screenStartPx, block.screenEndPx) + 1;
324
- for (let y = Math.floor(yMin) - 1; y <= yMax + 1; y += step) {
363
+ const y0 = rule ? -1 : Math.floor(yMin) - 1;
364
+ const y1 = rule ? state.canvasHeight + 1 : yMax + 1;
365
+ let answered = 0;
366
+ for (let y = y0; y <= y1; y += step) {
325
367
  for (let x = x0; x <= x1; x += step) {
326
368
  let expected = -1;
327
- if (perRect) {
328
- for (let k = count - 1; k >= 0; k--) {
329
- if (contains(boxes[k], x, y)) {
330
- expected = k;
331
- break;
369
+ let expectedDistSq = maxDistSq;
370
+ if (claims) {
371
+ for (const k of candidates) {
372
+ const b = boxes[k];
373
+ if (b && claims(k, x, y)) {
374
+ const distSq = inkOnRect(x, y, b.x, b.y, b.w, b.h).distSq;
375
+ if (expected === -1
376
+ ? distSq < maxDistSq
377
+ : distSq < expectedDistSq - EPS) {
378
+ expected = k;
379
+ expectedDistSq = distSq;
380
+ }
332
381
  }
333
382
  }
334
383
  }
335
- const hit = hitNearest(channels, block, frame, params, x, y, candidates, maxDistSq);
336
- const box = hit && boxes[hit.index];
337
- if (hit && !box) {
338
- violations.push(`(${x}, ${y}) answered ${hit.index}, which painted nothing`);
384
+ const hit = mark.hitNearest(channels, block, state, x, y, candidates, maxDistSq);
385
+ if (hit) {
386
+ answered++;
339
387
  }
340
- else if (hit && box) {
341
- if (!contains(box, hit.x, hit.y, EPS)) {
342
- violations.push(`(${x}, ${y}) answered ${hit.index} with ink at (${hit.x}, ${hit.y}), off its painting`);
343
- }
344
- const toBox = inkOnRect(x, y, box.x, box.y, box.w, box.h).distSq;
345
- if (hit.distSq < toBox - EPS) {
346
- violations.push(`(${x}, ${y}) answered ${hit.index} at ${hit.distSq}, nearer than its painting (${toBox})`);
347
- }
348
- const toPoint = (x - hit.x) ** 2 + (y - hit.y) ** 2;
349
- if (Math.abs(hit.distSq - toPoint) > EPS * Math.max(1, toPoint)) {
350
- violations.push(`(${x}, ${y}) answered ${hit.index} at ${hit.distSq}, which is not the distance to the ink it named (${toPoint})`);
351
- }
388
+ violations.push(...hitViolations(hit, boxes, x, y));
389
+ if (expected === -1
390
+ ? rule !== undefined && hit !== undefined
391
+ : hit?.index !== expected) {
392
+ violations.push(`(${x}, ${y}) should answer ${expected === -1 ? 'nothing' : expected}, answered ${hit?.index ?? 'nothing'}`);
352
393
  }
353
- if (expected !== -1 && hit?.index !== expected) {
354
- violations.push(`(${x}, ${y}) is on rect ${expected}, answered ${hit?.index ?? 'nothing'}`);
355
- }
356
- if (violations.length >= 10) {
394
+ if (violations.length >= MAX_VIOLATIONS) {
357
395
  return violations;
358
396
  }
359
397
  }
360
398
  }
361
- return violations;
399
+ return answered === 0 && violations.length === 0
400
+ ? [`no swept point answered any of ${count} instances`]
401
+ : violations;
362
402
  }
@@ -7,8 +7,8 @@ export type GlyphPath = Pick<MarkContext2D, 'moveTo' | 'lineTo' | 'closePath' |
7
7
  *
8
8
  * The disc/crisp-square split and the square's snap are pointGlyph.slang's
9
9
  * (adr-051), so the painter cannot drift from the shader about where the split
10
- * is. The caller owns `beginPath`/`fill`, which is what lets a run of one
11
- * colour batch into a single fill.
10
+ * is. The caller owns `beginPath`/`fill`, so a run of one colour can batch into
11
+ * a single fill.
12
12
  *
13
13
  * Its own module, not `pointMark.ts`'s: this is the half of the shape a
14
14
  * consumer can want without the pass, and importing it through the shape would
@@ -16,6 +16,18 @@ export type GlyphPath = Pick<MarkContext2D, 'moveTo' | 'lineTo' | 'closePath' |
16
16
  * is that consumer.
17
17
  */
18
18
  export declare function appendGlyph(ctx: GlyphPath, glyph: number, cx: number, y: number, diameter: number): void;
19
+ /**
20
+ * Room a glyph needs at each end of a plot whose display wants its topmost and
21
+ * bottommost points drawn whole: a bar's datum is its top edge and never
22
+ * overshoots, a glyph's is its centre, so one at domain max loses its top half
23
+ * off the canvas. The diamond's scale, since that is the largest thing the
24
+ * shape draws and `glyphBox` measures it the same way.
25
+ *
26
+ * A display passes this as the point shape's `insetPx` and gives its axis the
27
+ * same number as an offset, so domain max goes on labelling the row the
28
+ * topmost glyph centres on.
29
+ */
30
+ export declare function pointInsetPx(diameter: number): number;
19
31
  /**
20
32
  * The box `appendGlyph` paints inside, by the same split and snap: the
21
33
  * diamond is scaled by `DIAMOND_GLYPH_SCALE` and the small square snaps to
@@ -7,8 +7,8 @@ import { DIAMOND_GLYPH_SCALE, GLYPH_DIAMOND, GLYPH_TRIANGLE, } from '../shaders/
7
7
  *
8
8
  * The disc/crisp-square split and the square's snap are pointGlyph.slang's
9
9
  * (adr-051), so the painter cannot drift from the shader about where the split
10
- * is. The caller owns `beginPath`/`fill`, which is what lets a run of one
11
- * colour batch into a single fill.
10
+ * is. The caller owns `beginPath`/`fill`, so a run of one colour can batch into
11
+ * a single fill.
12
12
  *
13
13
  * Its own module, not `pointMark.ts`'s: this is the half of the shape a
14
14
  * consumer can want without the pass, and importing it through the shape would
@@ -39,6 +39,20 @@ export function appendGlyph(ctx, glyph, cx, y, diameter) {
39
39
  ctx.arc(cx, y, r, 0, Math.PI * 2);
40
40
  }
41
41
  }
42
+ /**
43
+ * Room a glyph needs at each end of a plot whose display wants its topmost and
44
+ * bottommost points drawn whole: a bar's datum is its top edge and never
45
+ * overshoots, a glyph's is its centre, so one at domain max loses its top half
46
+ * off the canvas. The diamond's scale, since that is the largest thing the
47
+ * shape draws and `glyphBox` measures it the same way.
48
+ *
49
+ * A display passes this as the point shape's `insetPx` and gives its axis the
50
+ * same number as an offset, so domain max goes on labelling the row the
51
+ * topmost glyph centres on.
52
+ */
53
+ export function pointInsetPx(diameter) {
54
+ return (diameter / 2) * DIAMOND_GLYPH_SCALE;
55
+ }
42
56
  /**
43
57
  * The box `appendGlyph` paints inside, by the same split and snap: the
44
58
  * diamond is scaled by `DIAMOND_GLYPH_SCALE` and the small square snaps to
@@ -2,13 +2,15 @@ export { defineMark } from './types.ts';
2
2
  export { paintMarkBlocks } from './markPaint.ts';
3
3
  export { planMarks } from './markPlan.ts';
4
4
  export { inkOfInstances } from './markInk.ts';
5
+ export { backToFront, nearestMarkHit, valueWindow } from './nearestMarkHit.ts';
5
6
  export { barMark } from './barMark.ts';
6
7
  export { pointMark } from './pointMark.ts';
7
- export { blockPx, spanMark } from './spanMark.ts';
8
- export { appendGlyph } from './glyphPaint.ts';
8
+ export { spanMark } from './spanMark.ts';
9
+ export { appendGlyph, pointInsetPx } from './glyphPaint.ts';
9
10
  export { abgrToCssRgba, makeAbgrFill } from './colorFill.ts';
10
11
  export type { MarkPlan } from './markPlan.ts';
11
12
  export type { MarkInstance } from './markInk.ts';
13
+ export type { HitWindow, NearestMarkHit } from './nearestMarkHit.ts';
12
14
  export type { Mark, MarkBand, MarkContext2D, MarkFrame, MarkImage, MarkRamp, MarkTexture, MarkValueScaleType, InkRect, MarkHit, MarkShape, PlannedPass, StagedUniforms, } from './types.ts';
13
15
  export type { BarChannels, BarParams } from './barMark.ts';
14
16
  export type { PointChannels, PointParams } from './pointMark.ts';
@@ -2,8 +2,9 @@ export { defineMark } from './types.js';
2
2
  export { paintMarkBlocks } from './markPaint.js';
3
3
  export { planMarks } from './markPlan.js';
4
4
  export { inkOfInstances } from './markInk.js';
5
+ export { backToFront, nearestMarkHit, valueWindow } from './nearestMarkHit.js';
5
6
  export { barMark } from './barMark.js';
6
7
  export { pointMark } from './pointMark.js';
7
- export { blockPx, spanMark } from './spanMark.js';
8
- export { appendGlyph } from './glyphPaint.js';
8
+ export { spanMark } from './spanMark.js';
9
+ export { appendGlyph, pointInsetPx } from './glyphPaint.js';
9
10
  export { abgrToCssRgba, makeAbgrFill } from './colorFill.js';
@@ -81,8 +81,8 @@ export declare class Canvas2DMarkBackend<TRegion, TState extends FrameDimensions
81
81
  * are the marks' own and each backend walks the same list.
82
82
  *
83
83
  * One uniform buffer serves every mark because each writes its own layout into
84
- * the scratch immediately before its own `drawPass`, which is what the HAL's
85
- * write-then-draw ordering already guarantees for a multi-pass renderer — or
84
+ * the scratch immediately before its `drawPass`, the ordering the HAL's
85
+ * write-then-draw contract already guarantees for a multi-pass renderer — or
86
86
  * draws off the struct the previous mark of the same block already staged, per
87
87
  * `StagedUniforms`.
88
88
  */
@@ -151,8 +151,8 @@ export class Canvas2DMarkBackend extends Canvas2DPerRegionRenderingBackend {
151
151
  * are the marks' own and each backend walks the same list.
152
152
  *
153
153
  * One uniform buffer serves every mark because each writes its own layout into
154
- * the scratch immediately before its own `drawPass`, which is what the HAL's
155
- * write-then-draw ordering already guarantees for a multi-pass renderer — or
154
+ * the scratch immediately before its `drawPass`, the ordering the HAL's
155
+ * write-then-draw contract already guarantees for a multi-pass renderer — or
156
156
  * draws off the struct the previous mark of the same block already staged, per
157
157
  * `StagedUniforms`.
158
158
  */
@@ -15,8 +15,8 @@ export declare function inkAtPoint(xPx: number, yPx: number, x: number, y: numbe
15
15
  * This is the whole of every shape's `hitNearest` but the geometry: `inkAt`
16
16
  * answers where instance `i` put ink and how far that is, and this keeps the
17
17
  * closest. Only a STRICTLY nearer candidate replaces the best, so on a tie the
18
- * first wins — which is what makes a caller that iterates back to front get the
19
- * mark on top, and it is a rule that used to be spelled out five times over.
18
+ * first wins, so a caller that iterates back to front gets the mark on top.
19
+ * Five shapes used to spell that rule out separately.
20
20
  *
21
21
  * `inkAt` may answer `undefined` for an instance that drew nothing at all, as
22
22
  * against one that drew somewhere far away: a bar clipped out of its band has
@@ -22,8 +22,8 @@ export function inkAtPoint(xPx, yPx, x, y) {
22
22
  * This is the whole of every shape's `hitNearest` but the geometry: `inkAt`
23
23
  * answers where instance `i` put ink and how far that is, and this keeps the
24
24
  * closest. Only a STRICTLY nearer candidate replaces the best, so on a tie the
25
- * first wins — which is what makes a caller that iterates back to front get the
26
- * mark on top, and it is a rule that used to be spelled out five times over.
25
+ * first wins, so a caller that iterates back to front gets the mark on top.
26
+ * Five shapes used to spell that rule out separately.
27
27
  *
28
28
  * `inkAt` may answer `undefined` for an instance that drew nothing at all, as
29
29
  * against one that drew somewhere far away: a bar clipped out of its band has
@@ -19,7 +19,7 @@ export interface MarkPlan<TRegion, TState extends MarkFrame> {
19
19
  /**
20
20
  * Resolve which marks draw this frame. Throws for an enabled mark the plan
21
21
  * form cannot carry — one with a `band` or a `paintsBlock` — since a plan
22
- * neither scissors nor asks the block anything, and dropping the gate silently
23
- * would draw on the GPU what Canvas2D declines.
22
+ * neither scissors nor asks the block anything, and without the throw the GPU
23
+ * would draw what Canvas2D declines.
24
24
  */
25
25
  export declare function planMarks<TRegion, TState extends MarkFrame>(marks: readonly Mark<TRegion, TState>[], state: TState): MarkPlan<TRegion, TState>;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Resolve which marks draw this frame. Throws for an enabled mark the plan
3
3
  * form cannot carry — one with a `band` or a `paintsBlock` — since a plan
4
- * neither scissors nor asks the block anything, and dropping the gate silently
5
- * would draw on the GPU what Canvas2D declines.
4
+ * neither scissors nor asks the block anything, and without the throw the GPU
5
+ * would draw what Canvas2D declines.
6
6
  */
7
7
  export function planMarks(marks, state) {
8
8
  const enabled = [];
@@ -1,4 +1,4 @@
1
- import type { MarkRamp, MarkValueScaleType } from './types.ts';
1
+ import type { MarkRamp } from './types.ts';
2
2
  /**
3
3
  * A colour channel as a shape reads it: `color`, the packed ABGR the worker
4
4
  * resolved, or `colorValue`, the raw values a frame's {@link MarkRamp} turns
@@ -18,8 +18,6 @@ interface RampBake {
18
18
  lut: Uint8Array;
19
19
  colors: Uint32Array;
20
20
  }
21
- /** `scoreScale`'s scale-type code for a declared value scale. */
22
- export declare function valueScaleTypeCode(scale: MarkValueScaleType | undefined): 0 | 1;
23
21
  /** `markColor.slang`'s three uniforms for a frame's ramp, or for none. */
24
22
  export declare function rampUniforms(ramp: MarkRamp | undefined): {
25
23
  rampMode: number;
@@ -1,11 +1,7 @@
1
+ import { SCALE_TYPE_LOG } from '../scoreScale.js';
1
2
  import { RAMP_LINEAR, RAMP_LOG, RAMP_NONE, } from '../shaders/markColor.generated.js';
2
- import { SCALE_TYPE_LOG } from '../shaders/scoreScale.generated.js';
3
3
  import { normalizeScore } from '../shaders/scoreScale.js.generated.js';
4
4
  const NO_COLORS = new Uint32Array(0);
5
- /** `scoreScale`'s scale-type code for a declared value scale. */
6
- export function valueScaleTypeCode(scale) {
7
- return scale === 'log' ? SCALE_TYPE_LOG : 0;
8
- }
9
5
  /** `markColor.slang`'s three uniforms for a frame's ramp, or for none. */
10
6
  export function rampUniforms(ramp) {
11
7
  return ramp
@@ -0,0 +1,49 @@
1
+ import type { RenderBlock } from '../renderBlock.ts';
2
+ import type { RowParams } from './rowLane.ts';
3
+ import type { Mark, MarkFrame, MarkHit, MarkValueScaleType } from './types.ts';
4
+ /**
5
+ * What a cursor's grab radius spans in one block for one mark: the bp, and
6
+ * the values the mark's shape can put ink at, every value for a shape with no
7
+ * `valueWindow`.
8
+ */
9
+ export interface HitWindow {
10
+ block: RenderBlock;
11
+ bpMin: number;
12
+ bpMax: number;
13
+ valueMin: number;
14
+ valueMax: number;
15
+ }
16
+ /** A {@link MarkHit}, with the mark, block and region it was found in. */
17
+ export interface NearestMarkHit<TRegion> extends MarkHit {
18
+ mark: number;
19
+ block: RenderBlock;
20
+ region: TRegion;
21
+ }
22
+ /**
23
+ * The instance nearest `(xPx, yPx)` within `radiusPx`, over the blocks the
24
+ * radius reaches. Marks are asked last to first and only a strictly nearer hit
25
+ * replaces the best, so a tie goes to the mark on top. `candidates` names what
26
+ * a mark is asked about in a block: what an index finds in the reach, or
27
+ * `backToFront` over every instance. `undefined` leaves the mark out, as a
28
+ * closed gate does.
29
+ */
30
+ export declare function nearestMarkHit<TRegion, TState extends MarkFrame>(marks: readonly Mark<TRegion, TState>[], blocks: readonly RenderBlock[], regionOf: (displayedRegionIndex: number) => TRegion | undefined, state: TState, xPx: number, yPx: number, { radiusPx, candidates, }: {
31
+ radiusPx: number;
32
+ candidates: (region: TRegion, mark: number, reach: HitWindow) => Iterable<number> | undefined;
33
+ }): NearestMarkHit<TRegion> | undefined;
34
+ /** `[end - 1 .. start]`: every instance as candidates, back to front. */
35
+ export declare function backToFront(start: number, end: number): Iterable<number>;
36
+ /**
37
+ * A value-scaled shape's `valueWindow`, read back through `denormalizeScore`
38
+ * inside each `rowHeight` band the radius touches — the whole canvas for a
39
+ * shape with no rows. An end within reach of a band edge opens to infinity,
40
+ * where an out-of-domain value clamps, so a cursor near the seam reaches the
41
+ * neighbouring band too. A point passes its `insetPx`; a bar passes its
42
+ * `origin`, and its window opens away from the origin on the cursor's side.
43
+ */
44
+ export declare function valueWindow(yPx: number, radiusPx: number, { canvasHeight }: MarkFrame, scale: RowParams & {
45
+ domain: [number, number];
46
+ scaleType?: MarkValueScaleType;
47
+ insetPx?: number;
48
+ origin?: number;
49
+ }): [number, number];