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

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 (71) hide show
  1. package/esm/RenderLifecycleMixin.d.ts +1 -0
  2. package/esm/RenderLifecycleMixin.js +8 -0
  3. package/esm/gpuContextLostError.d.ts +30 -0
  4. package/esm/gpuContextLostError.js +40 -0
  5. package/esm/gpuDevice.js +4 -1
  6. package/esm/hal/gpuHalBase.d.ts +3 -3
  7. package/esm/hal/index.d.ts +1 -1
  8. package/esm/hal/mockHal.d.ts +3 -3
  9. package/esm/hal/mockHal.js +3 -2
  10. package/esm/hal/oomReporter.d.ts +7 -0
  11. package/esm/hal/oomReporter.js +10 -0
  12. package/esm/hal/types.d.ts +8 -1
  13. package/esm/hal/webgl2Hal.d.ts +2 -2
  14. package/esm/hal/webgl2Hal.js +8 -1
  15. package/esm/hal/webgpuHal.d.ts +12 -2
  16. package/esm/hal/webgpuHal.js +49 -11
  17. package/esm/marks/barMark.d.ts +28 -0
  18. package/esm/marks/barMark.js +63 -0
  19. package/esm/marks/drawAgainstHit.d.ts +85 -14
  20. package/esm/marks/drawAgainstHit.js +260 -38
  21. package/esm/marks/glyphPaint.d.ts +10 -2
  22. package/esm/marks/glyphPaint.js +21 -0
  23. package/esm/marks/index.d.ts +9 -2
  24. package/esm/marks/index.js +4 -1
  25. package/esm/marks/markBackend.d.ts +21 -9
  26. package/esm/marks/markBackend.js +63 -29
  27. package/esm/marks/markHit.d.ts +32 -0
  28. package/esm/marks/markHit.js +61 -0
  29. package/esm/marks/markInk.d.ts +14 -0
  30. package/esm/marks/markInk.js +34 -0
  31. package/esm/marks/markPlan.d.ts +25 -0
  32. package/esm/marks/markPlan.js +20 -0
  33. package/esm/marks/markRamp.d.ts +43 -0
  34. package/esm/marks/markRamp.js +68 -0
  35. package/esm/marks/pointMark.d.ts +8 -4
  36. package/esm/marks/pointMark.js +39 -23
  37. package/esm/marks/spanMark.d.ts +8 -0
  38. package/esm/marks/spanMark.js +19 -26
  39. package/esm/marks/types.d.ts +101 -4
  40. package/esm/marks/types.js +78 -5
  41. package/esm/renderBlock.d.ts +14 -0
  42. package/esm/renderBlock.js +25 -0
  43. package/esm/shaders/barMark.generated.d.ts +4 -0
  44. package/esm/shaders/barMark.generated.js +6 -0
  45. package/esm/shaders/barMark.iface.generated.d.ts +64 -0
  46. package/esm/shaders/barMark.iface.generated.js +110 -0
  47. package/esm/shaders/markColor.generated.d.ts +3 -0
  48. package/esm/shaders/markColor.generated.js +5 -0
  49. package/esm/shaders/pointMark.generated.d.ts +3 -3
  50. package/esm/shaders/pointMark.generated.js +3 -3
  51. package/esm/shaders/pointMark.iface.generated.d.ts +17 -6
  52. package/esm/shaders/pointMark.iface.generated.js +27 -9
  53. package/esm/shaders/pointMark.js.generated.d.ts +2 -0
  54. package/esm/shaders/pointMark.js.generated.js +42 -0
  55. package/esm/shaders/scoreScale.generated.d.ts +2 -0
  56. package/esm/shaders/scoreScale.generated.js +4 -0
  57. package/esm/useRenderingBackend.d.ts +1 -16
  58. package/esm/useRenderingBackend.js +2 -24
  59. package/package.json +5 -2
  60. package/src/shaders/barMark.generated.ts +10 -0
  61. package/src/shaders/barMark.iface.generated.ts +160 -0
  62. package/src/shaders/barMark.slang +109 -0
  63. package/src/shaders/markColor.generated.ts +8 -0
  64. package/src/shaders/markColor.slang +47 -0
  65. package/src/shaders/pointMark.generated.ts +3 -3
  66. package/src/shaders/pointMark.iface.generated.ts +34 -10
  67. package/src/shaders/pointMark.js.generated.ts +45 -0
  68. package/src/shaders/pointMark.slang +35 -33
  69. package/src/shaders/scoreScale.generated.ts +6 -0
  70. package/src/shaders/scoreScale.slang +1 -0
  71. package/src/shaders/valueScale.slang +52 -0
@@ -0,0 +1,32 @@
1
+ import type { MarkHit, MarkShape } from './types.ts';
2
+ /** One instance's ink measured against a cursor — a `MarkHit` before it is one. */
3
+ export type InkHit = Omit<MarkHit, 'index'>;
4
+ /**
5
+ * Where a rect's ink sits nearest `(xPx, yPx)`, and how far: the point itself
6
+ * where the cursor is inside, else the nearest point on the edge.
7
+ */
8
+ export declare function inkOnRect(xPx: number, yPx: number, left: number, top: number, width: number, height: number): InkHit;
9
+ /** The same for a mark with no extent to clamp into. */
10
+ export declare function inkAtPoint(xPx: number, yPx: number, x: number, y: number): InkHit;
11
+ /**
12
+ * The nearest of `candidates` by where each one's ink is, or undefined when
13
+ * none beats `maxDistSq`.
14
+ *
15
+ * This is the whole of every shape's `hitNearest` but the geometry: `inkAt`
16
+ * answers where instance `i` put ink and how far that is, and this keeps the
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.
20
+ *
21
+ * `inkAt` may answer `undefined` for an instance that drew nothing at all, as
22
+ * against one that drew somewhere far away: a bar clipped out of its band has
23
+ * no ink to be near.
24
+ */
25
+ export declare function nearestInk(candidates: Iterable<number>, maxDistSq: number, inkAt: (index: number) => InkHit | undefined): MarkHit | undefined;
26
+ /**
27
+ * The hit test a shape's `ink` implies: the nearest of its boxes, edges
28
+ * included, so a point inside one answers it at distance 0.
29
+ */
30
+ export declare function inkHitNearest<C, P>(ink: NonNullable<MarkShape<C, P>['ink']>): NonNullable<MarkShape<C, P>['hitNearest']>;
31
+ /** A shape's own `hitNearest`, else the one its `ink` implies, else none. */
32
+ export declare function shapeHitNearest<C, P>(shape: MarkShape<C, P>): MarkShape<C, P>['hitNearest'];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Where a rect's ink sits nearest `(xPx, yPx)`, and how far: the point itself
3
+ * where the cursor is inside, else the nearest point on the edge.
4
+ */
5
+ export function inkOnRect(xPx, yPx, left, top, width, height) {
6
+ const x = Math.min(Math.max(xPx, left), left + width);
7
+ const y = Math.min(Math.max(yPx, top), top + height);
8
+ const dx = xPx - x;
9
+ const dy = yPx - y;
10
+ return { x, y, distSq: dx * dx + dy * dy };
11
+ }
12
+ /** The same for a mark with no extent to clamp into. */
13
+ export function inkAtPoint(xPx, yPx, x, y) {
14
+ const dx = xPx - x;
15
+ const dy = yPx - y;
16
+ return { x, y, distSq: dx * dx + dy * dy };
17
+ }
18
+ /**
19
+ * The nearest of `candidates` by where each one's ink is, or undefined when
20
+ * none beats `maxDistSq`.
21
+ *
22
+ * This is the whole of every shape's `hitNearest` but the geometry: `inkAt`
23
+ * answers where instance `i` put ink and how far that is, and this keeps the
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.
27
+ *
28
+ * `inkAt` may answer `undefined` for an instance that drew nothing at all, as
29
+ * against one that drew somewhere far away: a bar clipped out of its band has
30
+ * no ink to be near.
31
+ */
32
+ export function nearestInk(candidates, maxDistSq, inkAt) {
33
+ let best;
34
+ let bestDistSq = maxDistSq;
35
+ for (const index of candidates) {
36
+ const ink = inkAt(index);
37
+ if (ink && ink.distSq < bestDistSq) {
38
+ bestDistSq = ink.distSq;
39
+ // Field by field, not `{ index, ...ink }`, which is both slower and one
40
+ // shape away from minting a hidden class per ink source and making every
41
+ // downstream `hit.x` read polymorphic.
42
+ best = { index, x: ink.x, y: ink.y, distSq: ink.distSq };
43
+ }
44
+ }
45
+ return best;
46
+ }
47
+ /**
48
+ * The hit test a shape's `ink` implies: the nearest of its boxes, edges
49
+ * included, so a point inside one answers it at distance 0.
50
+ */
51
+ export function inkHitNearest(ink) {
52
+ return (channels, block, frame, params, xPx, yPx, candidates, maxDistSq) => nearestInk(candidates, maxDistSq, i => {
53
+ const r = ink(channels, block, frame, params, i);
54
+ return r && inkOnRect(xPx, yPx, r.left, r.top, r.width, r.height);
55
+ });
56
+ }
57
+ /** A shape's own `hitNearest`, else the one its `ink` implies, else none. */
58
+ export function shapeHitNearest(shape) {
59
+ return (shape.hitNearest ??
60
+ (shape.ink ? inkHitNearest(shape.ink.bind(shape)) : undefined));
61
+ }
@@ -0,0 +1,14 @@
1
+ import type { RenderBlock } from '../renderBlock.ts';
2
+ import type { InkRect, Mark, MarkFrame } from './types.ts';
3
+ /** One instance of one mark in a display's list, as a highlight names it. */
4
+ export interface MarkInstance {
5
+ mark: number;
6
+ index: number;
7
+ }
8
+ /**
9
+ * The boxes a set of instances painted this frame: each mark's `ink` for the
10
+ * instances a display names in each region, clipped to the block's column the
11
+ * way the painter was. What a highlight guide draws, so a display says WHICH
12
+ * instances are lit and nothing about where they are.
13
+ */
14
+ export declare function inkOfInstances<TRegion, TState extends MarkFrame>(marks: readonly Mark<TRegion, TState>[], blocks: readonly RenderBlock[], regionOf: (displayedRegionIndex: number) => TRegion | undefined, state: TState, instancesOf: (displayedRegionIndex: number) => Iterable<MarkInstance> | undefined): InkRect[];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The boxes a set of instances painted this frame: each mark's `ink` for the
3
+ * instances a display names in each region, clipped to the block's column the
4
+ * way the painter was. What a highlight guide draws, so a display says WHICH
5
+ * instances are lit and nothing about where they are.
6
+ */
7
+ export function inkOfInstances(marks, blocks, regionOf, state, instancesOf) {
8
+ const rects = [];
9
+ for (const block of blocks) {
10
+ const region = regionOf(block.displayedRegionIndex);
11
+ const instances = region && instancesOf(block.displayedRegionIndex);
12
+ if (!instances) {
13
+ continue;
14
+ }
15
+ const x0 = Math.min(block.screenStartPx, block.screenEndPx);
16
+ const x1 = Math.max(block.screenStartPx, block.screenEndPx);
17
+ for (const { mark, index } of instances) {
18
+ const r = marks[mark]?.ink?.(region, block, state, index);
19
+ if (r) {
20
+ const left = Math.max(r.left, x0);
21
+ const right = Math.min(r.left + r.width, x1);
22
+ if (right >= left) {
23
+ rects.push({
24
+ left,
25
+ top: r.top,
26
+ width: right - left,
27
+ height: r.height,
28
+ });
29
+ }
30
+ }
31
+ }
32
+ }
33
+ return rects;
34
+ }
@@ -0,0 +1,25 @@
1
+ import type { Mark, MarkFrame, PlannedPass } from './types.ts';
2
+ /**
3
+ * A mark list resolved against one frame's state: the marks whose `enabled`
4
+ * said yes, in list order, and the passes they draw.
5
+ *
6
+ * `drawMarks` asks every mark's gate, lens and staged compare per block, which
7
+ * is the right shape for a display that stages its uniforms through the marks
8
+ * — and at 120 section blocks a frame it is the whole cost the pileup measured
9
+ * and declined (`benches/pileupUniformWrite.bench.ts` has the table). A
10
+ * display that writes the uniforms itself, once per section, has nothing left
11
+ * for the per-block walk to decide: `planMarks` answers the frame question
12
+ * once, `drawPlannedPasses` issues a `drawPass` per planned mark against
13
+ * whatever the caller staged, and `marks` is the painter's list.
14
+ */
15
+ export interface MarkPlan<TRegion, TState extends MarkFrame> {
16
+ readonly marks: readonly Mark<TRegion, TState>[];
17
+ readonly passes: readonly PlannedPass[];
18
+ }
19
+ /**
20
+ * Resolve which marks draw this frame. Throws for an enabled mark the plan
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.
24
+ */
25
+ export declare function planMarks<TRegion, TState extends MarkFrame>(marks: readonly Mark<TRegion, TState>[], state: TState): MarkPlan<TRegion, TState>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Resolve which marks draw this frame. Throws for an enabled mark the plan
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.
6
+ */
7
+ export function planMarks(marks, state) {
8
+ const enabled = [];
9
+ const passes = [];
10
+ for (const mark of marks) {
11
+ if (!mark.enabled || mark.enabled(state)) {
12
+ if (!mark.planned) {
13
+ throw new Error(`mark ${mark.pass.id} declares a band or a block gate, which a frame plan does not carry; draw it through drawMarks`);
14
+ }
15
+ enabled.push(mark);
16
+ passes.push(mark.planned);
17
+ }
18
+ }
19
+ return { marks: enabled, passes };
20
+ }
@@ -0,0 +1,43 @@
1
+ import type { MarkRamp, MarkValueScaleType } from './types.ts';
2
+ /**
3
+ * A colour channel as a shape reads it: `color`, the packed ABGR the worker
4
+ * resolved, or `colorValue`, the raw values a frame's {@link MarkRamp} turns
5
+ * into colours here. A shape reading a ramp declares both optional and asks
6
+ * {@link colorBits} for the GPU and {@link paintColors} for Canvas2D.
7
+ */
8
+ export interface ColorChannel {
9
+ color?: Uint32Array;
10
+ colorValue?: Float32Array;
11
+ /** The Canvas2D bake, memoized on the payload; nothing else writes it. */
12
+ rampBake?: RampBake;
13
+ }
14
+ interface RampBake {
15
+ min: number;
16
+ max: number;
17
+ log: boolean;
18
+ lut: Uint8Array;
19
+ colors: Uint32Array;
20
+ }
21
+ /** `scoreScale`'s scale-type code for a declared value scale. */
22
+ export declare function valueScaleTypeCode(scale: MarkValueScaleType | undefined): 0 | 1;
23
+ /** `markColor.slang`'s three uniforms for a frame's ramp, or for none. */
24
+ export declare function rampUniforms(ramp: MarkRamp | undefined): {
25
+ rampMode: number;
26
+ rampMin: number;
27
+ rampMax: number;
28
+ };
29
+ /**
30
+ * The 4-byte instance lane, for the packer. Under a ramp it is the value's
31
+ * float32 bits viewed as the `uint` the attribute declares — the reinterpret
32
+ * `markColor.slang`'s `asfloat` undoes, and the reason a ramp adds no lane.
33
+ */
34
+ export declare function colorBits(c: ColorChannel): ArrayLike<number>;
35
+ /**
36
+ * The packed colours a painter fills with. Under a ramp the values are baked
37
+ * against the domain once and kept on the payload, so a repaint at an
38
+ * unchanged domain — every pan and every hover — walks no values and the
39
+ * painters' fill batching still sees runs of one colour. The bake reruns when
40
+ * the domain, the scale type or the ramp's bytes move.
41
+ */
42
+ export declare function paintColors(c: ColorChannel, count: number, ramp: MarkRamp | undefined): ArrayLike<number>;
43
+ export {};
@@ -0,0 +1,68 @@
1
+ import { RAMP_LINEAR, RAMP_LOG, RAMP_NONE, } from '../shaders/markColor.generated.js';
2
+ import { SCALE_TYPE_LOG } from '../shaders/scoreScale.generated.js';
3
+ import { normalizeScore } from '../shaders/scoreScale.js.generated.js';
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
+ /** `markColor.slang`'s three uniforms for a frame's ramp, or for none. */
10
+ export function rampUniforms(ramp) {
11
+ return ramp
12
+ ? {
13
+ rampMode: ramp.scale === 'log' ? RAMP_LOG : RAMP_LINEAR,
14
+ rampMin: ramp.domain[0],
15
+ rampMax: ramp.domain[1],
16
+ }
17
+ : { rampMode: RAMP_NONE, rampMin: 0, rampMax: 1 };
18
+ }
19
+ /**
20
+ * The 4-byte instance lane, for the packer. Under a ramp it is the value's
21
+ * float32 bits viewed as the `uint` the attribute declares — the reinterpret
22
+ * `markColor.slang`'s `asfloat` undoes, and the reason a ramp adds no lane.
23
+ */
24
+ export function colorBits(c) {
25
+ const { colorValue } = c;
26
+ return colorValue
27
+ ? new Uint32Array(colorValue.buffer, colorValue.byteOffset, colorValue.length)
28
+ : (c.color ?? NO_COLORS);
29
+ }
30
+ /**
31
+ * The packed colours a painter fills with. Under a ramp the values are baked
32
+ * against the domain once and kept on the payload, so a repaint at an
33
+ * unchanged domain — every pan and every hover — walks no values and the
34
+ * painters' fill batching still sees runs of one colour. The bake reruns when
35
+ * the domain, the scale type or the ramp's bytes move.
36
+ */
37
+ export function paintColors(c, count, ramp) {
38
+ const { colorValue } = c;
39
+ if (!ramp || !colorValue) {
40
+ return c.color ?? NO_COLORS;
41
+ }
42
+ const [min, max] = ramp.domain;
43
+ const log = ramp.scale === 'log';
44
+ const { lut } = ramp;
45
+ const bake = c.rampBake;
46
+ if (bake &&
47
+ bake.min === min &&
48
+ bake.max === max &&
49
+ bake.log === log &&
50
+ bake.lut === lut &&
51
+ bake.colors.length >= count) {
52
+ return bake.colors;
53
+ }
54
+ const entries = lut.length / 4;
55
+ const colors = new Uint32Array(count);
56
+ for (let i = 0; i < count; i++) {
57
+ const t = normalizeScore(colorValue[i], min, max, log ? SCALE_TYPE_LOG : 0, 1);
58
+ const o = Math.round(t * (entries - 1)) * 4;
59
+ colors[i] =
60
+ ((lut[o + 3] << 24) |
61
+ (lut[o + 2] << 16) |
62
+ (lut[o + 1] << 8) |
63
+ lut[o]) >>>
64
+ 0;
65
+ }
66
+ c.rampBake = { min, max, log, lut, colors };
67
+ return colors;
68
+ }
@@ -1,4 +1,5 @@
1
- import type { MarkShape } from './types.ts';
1
+ import type { ColorChannel } from './markRamp.ts';
2
+ import type { MarkRamp, MarkShape, MarkValueScaleType } from './types.ts';
2
3
  /**
3
4
  * The `point` shape's channels: a glyph per instance at `x`, on the `y` scale,
4
5
  * widening to a bar where `x2 - x` is wider than the glyph.
@@ -7,17 +8,20 @@ import type { MarkShape } from './types.ts';
7
8
  * one array holds SNPs (`x2 === x + 1`) and structural variants together, and
8
9
  * the shader, the painter and the hit test all take the same branch off it.
9
10
  */
10
- export interface PointChannels {
11
+ export interface PointChannels extends ColorChannel {
11
12
  x: Uint32Array;
12
13
  x2: Uint32Array;
13
14
  y: Float32Array;
14
- color: Uint32Array;
15
15
  glyph: Uint8Array;
16
16
  count: number;
17
17
  }
18
18
  export interface PointParams {
19
- /** `[min, max]` of the linear scale `y` is read through. */
19
+ /** `[min, max]` `y` is read through. */
20
20
  domain: [number, number];
21
+ /** How that domain is read; linear when absent. */
22
+ scaleType?: MarkValueScaleType;
23
+ /** The quantitative colour scale, for a point whose colour is a ramp. */
24
+ ramp?: MarkRamp;
21
25
  /** Glyph diameter in CSS px. */
22
26
  diameterPx: number;
23
27
  }
@@ -1,15 +1,18 @@
1
1
  import { bpRangeXTuple } from '../blockClipUtils.js';
2
2
  import { getDpr, makeBpMapper } from '../canvas2dUtils.js';
3
3
  import * as shader from '../shaders/pointMark.generated.js';
4
- import { valueToYPx } from '../shaders/pointMark.js.generated.js';
4
+ import { pointDrawsBar, valueToYPxScaled, } from '../shaders/pointMark.js.generated.js';
5
5
  import { slangPass } from '../slangPass.js';
6
6
  import { abgrToCssRgba } from './colorFill.js';
7
- import { appendGlyph } from './glyphPaint.js';
7
+ import { appendGlyph, glyphBox } from './glyphPaint.js';
8
+ import { inkAtPoint, inkOnRect, nearestInk } from './markHit.js';
9
+ import { colorBits, paintColors, rampUniforms, valueScaleTypeCode, } from './markRamp.js';
10
+ import { blockPx } from './spanMark.js';
8
11
  export const pointMark = {
9
12
  id: 'point',
10
13
  pass: {
11
14
  ...slangPass({ id: 'point', mod: shader }),
12
- pack: c => shader.packInstances(c, c.count),
15
+ pack: c => shader.packInstances({ ...c, color: colorBits(c) }, c.count),
13
16
  },
14
17
  writeUniforms(scratch, clip, block, frame, params) {
15
18
  shader.writeUniforms(scratch, {
@@ -17,6 +20,8 @@ export const pointMark = {
17
20
  canvasHeight: frame.canvasHeight,
18
21
  domainMin: params.domain[0],
19
22
  domainMax: params.domain[1],
23
+ valueScaleType: valueScaleTypeCode(params.scaleType),
24
+ ...rampUniforms(params.ramp),
20
25
  zero: 0,
21
26
  // viewportWidth and radiusPx stay in CSS units to match canvasHeight:
22
27
  // mixing a DPR-scaled radius with a CSS-scaled height draws vertically
@@ -27,15 +32,17 @@ export const pointMark = {
27
32
  });
28
33
  },
29
34
  paintBlock(ctx, channels, block, frame, params) {
30
- const { x, x2, y, color, glyph, count } = channels;
35
+ const { x, x2, y, glyph, count } = channels;
31
36
  if (count === 0) {
32
37
  return;
33
38
  }
39
+ const color = paintColors(channels, count, params.ramp);
34
40
  const { diameterPx, domain } = params;
35
41
  const r = diameterPx / 2;
36
42
  const canvasHeight = frame.canvasHeight;
37
43
  const domainMin = domain[0];
38
44
  const domainMax = domain[1];
45
+ const st = valueScaleTypeCode(params.scaleType);
39
46
  // The per-block closure, not the six-argument `bpToScreenPx`: that spelling
40
47
  // re-derives the region span and the block width at every call, and this
41
48
  // loop makes two calls per instance. Measured at 1.67x on 100K points.
@@ -55,9 +62,9 @@ export const pointMark = {
55
62
  }
56
63
  const xStart = bpToPx(x[i]);
57
64
  const xEnd = bpToPx(x2[i]);
58
- const yPx = valueToYPx(y[i], domainMin, domainMax, canvasHeight);
65
+ const yPx = valueToYPxScaled(y[i], domainMin, domainMax, canvasHeight, st);
59
66
  const widthPx = Math.abs(xEnd - xStart);
60
- if (widthPx > diameterPx) {
67
+ if (pointDrawsBar(widthPx, r)) {
61
68
  ctx.rect(Math.min(xStart, xEnd), yPx - r, widthPx, diameterPx);
62
69
  }
63
70
  else {
@@ -66,6 +73,25 @@ export const pointMark = {
66
73
  }
67
74
  ctx.fill();
68
75
  },
76
+ // A bar is the rect the painter fills, unpadded on every side; a glyph is
77
+ // the box `appendGlyph` paints inside.
78
+ ink(channels, block, frame, params, i) {
79
+ const { x, x2, y, glyph } = channels;
80
+ const { diameterPx, domain } = params;
81
+ const xStart = blockPx(block, x[i]);
82
+ const xEnd = blockPx(block, x2[i]);
83
+ const cy = valueToYPxScaled(y[i], domain[0], domain[1], frame.canvasHeight, valueScaleTypeCode(params.scaleType));
84
+ const lo = Math.min(xStart, xEnd);
85
+ const hi = Math.max(xStart, xEnd);
86
+ const r = diameterPx / 2;
87
+ return pointDrawsBar(hi - lo, r)
88
+ ? { left: lo, top: cy - r, width: hi - lo, height: diameterPx }
89
+ : glyphBox(glyph[i], xStart, cy, diameterPx);
90
+ },
91
+ // Its own rather than the one `ink` implies, for the glyph: a dense plot
92
+ // stacks glyphs whose boxes all contain the cursor at distance 0, and the
93
+ // one under the cursor is the nearest CENTRE, which no box can say. A bar
94
+ // is grabbed anywhere inside the rect it fills, as the derived test would.
69
95
  hitNearest(channels, block, frame, params, xPx, yPx, candidates, maxDistSq) {
70
96
  const { x, x2, y } = channels;
71
97
  const bpToPx = makeBpMapper(block);
@@ -73,26 +99,16 @@ export const pointMark = {
73
99
  const domainMin = domain[0];
74
100
  const domainMax = domain[1];
75
101
  const canvasHeight = frame.canvasHeight;
76
- let best;
77
- let bestDistSq = maxDistSq;
78
- for (const i of candidates) {
102
+ const st = valueScaleTypeCode(params.scaleType);
103
+ return nearestInk(candidates, maxDistSq, i => {
79
104
  const xStart = bpToPx(x[i]);
80
105
  const xEnd = bpToPx(x2[i]);
81
- // The bar branch, in the same words the shader and the painter take it:
82
- // a mark with extent is grabbed at the nearest point along its span, a
83
- // glyph at its centre.
106
+ const cy = valueToYPxScaled(y[i], domainMin, domainMax, canvasHeight, st);
84
107
  const lo = Math.min(xStart, xEnd);
85
108
  const hi = Math.max(xStart, xEnd);
86
- const ptX = hi - lo > diameterPx ? Math.max(lo, Math.min(xPx, hi)) : xStart;
87
- const ptY = valueToYPx(y[i], domainMin, domainMax, canvasHeight);
88
- const dx = xPx - ptX;
89
- const dy = yPx - ptY;
90
- const distSq = dx * dx + dy * dy;
91
- if (distSq < bestDistSq) {
92
- bestDistSq = distSq;
93
- best = { index: i, x: ptX, y: ptY, distSq };
94
- }
95
- }
96
- return best;
109
+ return pointDrawsBar(hi - lo, diameterPx / 2)
110
+ ? inkOnRect(xPx, yPx, lo, cy - diameterPx / 2, hi - lo, diameterPx)
111
+ : inkAtPoint(xPx, yPx, xStart, cy);
112
+ });
97
113
  },
98
114
  };
@@ -1,3 +1,4 @@
1
+ import type { RenderBlock } from '../renderBlock.ts';
1
2
  import type { MarkShape } from './types.ts';
2
3
  /**
3
4
  * The `span` shape's channels: a coloured rectangle from `x` to `x2` on the
@@ -7,6 +8,12 @@ export interface SpanChannels {
7
8
  x: Uint32Array;
8
9
  x2: Uint32Array;
9
10
  row: Uint32Array;
11
+ /**
12
+ * Packed ABGR, resolved in the worker — a span's colour has no ramp arm.
13
+ * `rowRect`'s uniform struct is shared with four other consumers, and a
14
+ * sampler on it would move all of them for a channel no config asks for
15
+ * on a span (ADR-113).
16
+ */
10
17
  color: Uint32Array;
11
18
  count: number;
12
19
  }
@@ -42,4 +49,5 @@ export interface SpanParams {
42
49
  /** Rows-area scroll offset in CSS px; 0 for a canvas sized to its content. */
43
50
  scrollTop: number;
44
51
  }
52
+ export declare function blockPx(block: RenderBlock, bp: number): number;
45
53
  export declare const spanMark: MarkShape<SpanChannels, SpanParams>;
@@ -1,9 +1,15 @@
1
1
  import { bpRangeXTuple } from '../blockClipUtils.js';
2
- import { makeBpMapper, spanLeft } from '../canvas2dUtils.js';
2
+ import { bpToScreenPx, makeBpMapper, spanLeft } from '../canvas2dUtils.js';
3
3
  import { drawnRowHeightPx, rowBandOffsetPx, } from '../shaders/rowRect.js.generated.js';
4
4
  import * as shader from '../shaders/spanMark.generated.js';
5
5
  import { slangPass } from '../slangPass.js';
6
6
  import { makeAbgrFill } from './colorFill.js';
7
+ // Per instance rather than through `makeBpMapper`: `ink` is asked one
8
+ // instance at a time, and a closure per ask is the allocation the mapper
9
+ // exists to hoist out of a loop.
10
+ export function blockPx(block, bp) {
11
+ return bpToScreenPx(bp, block.start, block.end, block.screenStartPx, block.screenEndPx, block.reversed);
12
+ }
7
13
  export const spanMark = {
8
14
  id: 'span',
9
15
  pass: {
@@ -40,32 +46,19 @@ export const spanMark = {
40
46
  ctx.fillRect(spanLeft(xa, xb, width), offset + rowHeight * row[i] - scrollTop, width + seamPx, h);
41
47
  }
42
48
  },
43
- hitNearest(channels, block, _frame, params, xPx, yPx, candidates, maxDistSq) {
49
+ ink(channels, block, _frame, params, i) {
44
50
  const { x, x2, row } = channels;
45
51
  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;
52
+ const xa = blockPx(block, x[i]);
53
+ const xb = blockPx(block, x2[i]);
54
+ const width = Math.max(minWidthPx, Math.abs(xb - xa));
55
+ return {
56
+ left: spanLeft(xa, xb, width),
57
+ top: rowBandOffsetPx(rowHeight, rowProportion) +
58
+ rowHeight * row[i] -
59
+ scrollTop,
60
+ width,
61
+ height: drawnRowHeightPx(rowHeight, rowProportion),
62
+ };
70
63
  },
71
64
  };