@jbrowse/render-core 5.0.0-beta.4 → 5.0.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/RenderLifecycleMixin.d.ts +1 -0
- package/esm/RenderLifecycleMixin.js +8 -0
- package/esm/gpuContextLostError.d.ts +30 -0
- package/esm/gpuContextLostError.js +40 -0
- package/esm/gpuDevice.js +4 -1
- package/esm/hal/gpuHalBase.d.ts +3 -3
- package/esm/hal/index.d.ts +1 -1
- package/esm/hal/mockHal.d.ts +3 -3
- package/esm/hal/mockHal.js +3 -2
- package/esm/hal/oomReporter.d.ts +7 -0
- package/esm/hal/oomReporter.js +10 -0
- package/esm/hal/types.d.ts +8 -1
- package/esm/hal/webgl2Hal.d.ts +2 -2
- package/esm/hal/webgl2Hal.js +8 -1
- package/esm/hal/webgpuHal.d.ts +12 -2
- package/esm/hal/webgpuHal.js +43 -5
- package/esm/marks/barMark.d.ts +28 -0
- package/esm/marks/barMark.js +63 -0
- package/esm/marks/drawAgainstHit.d.ts +85 -14
- package/esm/marks/drawAgainstHit.js +260 -38
- package/esm/marks/glyphPaint.d.ts +10 -2
- package/esm/marks/glyphPaint.js +21 -0
- package/esm/marks/index.d.ts +9 -2
- package/esm/marks/index.js +4 -1
- package/esm/marks/markBackend.d.ts +14 -2
- package/esm/marks/markBackend.js +43 -13
- package/esm/marks/markHit.d.ts +8 -1
- package/esm/marks/markHit.js +15 -0
- package/esm/marks/markInk.d.ts +14 -0
- package/esm/marks/markInk.js +34 -0
- package/esm/marks/markPlan.d.ts +25 -0
- package/esm/marks/markPlan.js +20 -0
- package/esm/marks/markRamp.d.ts +43 -0
- package/esm/marks/markRamp.js +68 -0
- package/esm/marks/pointMark.d.ts +8 -4
- package/esm/marks/pointMark.js +32 -9
- package/esm/marks/spanMark.d.ts +8 -0
- package/esm/marks/spanMark.js +19 -14
- package/esm/marks/types.d.ts +90 -3
- package/esm/marks/types.js +56 -4
- package/esm/shaders/barMark.generated.d.ts +4 -0
- package/esm/shaders/barMark.generated.js +6 -0
- package/esm/shaders/barMark.iface.generated.d.ts +64 -0
- package/esm/shaders/barMark.iface.generated.js +110 -0
- package/esm/shaders/markColor.generated.d.ts +3 -0
- package/esm/shaders/markColor.generated.js +5 -0
- package/esm/shaders/pointMark.generated.d.ts +3 -3
- package/esm/shaders/pointMark.generated.js +3 -3
- package/esm/shaders/pointMark.iface.generated.d.ts +17 -6
- package/esm/shaders/pointMark.iface.generated.js +27 -9
- package/esm/shaders/pointMark.js.generated.d.ts +1 -0
- package/esm/shaders/pointMark.js.generated.js +39 -0
- package/esm/shaders/scoreScale.generated.d.ts +2 -0
- package/esm/shaders/scoreScale.generated.js +4 -0
- package/esm/useRenderingBackend.d.ts +1 -16
- package/esm/useRenderingBackend.js +2 -24
- package/package.json +4 -2
- package/src/shaders/barMark.generated.ts +10 -0
- package/src/shaders/barMark.iface.generated.ts +160 -0
- package/src/shaders/barMark.slang +109 -0
- package/src/shaders/markColor.generated.ts +8 -0
- package/src/shaders/markColor.slang +47 -0
- package/src/shaders/pointMark.generated.ts +3 -3
- package/src/shaders/pointMark.iface.generated.ts +34 -10
- package/src/shaders/pointMark.js.generated.ts +41 -0
- package/src/shaders/pointMark.slang +15 -26
- package/src/shaders/scoreScale.generated.ts +6 -0
- package/src/shaders/scoreScale.slang +1 -0
- package/src/shaders/valueScale.slang +52 -0
|
@@ -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
|
+
}
|
package/esm/marks/pointMark.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
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]`
|
|
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
|
}
|
package/esm/marks/pointMark.js
CHANGED
|
@@ -1,16 +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 { pointDrawsBar,
|
|
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
8
|
import { inkAtPoint, inkOnRect, nearestInk } from './markHit.js';
|
|
9
|
+
import { colorBits, paintColors, rampUniforms, valueScaleTypeCode, } from './markRamp.js';
|
|
10
|
+
import { blockPx } from './spanMark.js';
|
|
9
11
|
export const pointMark = {
|
|
10
12
|
id: 'point',
|
|
11
13
|
pass: {
|
|
12
14
|
...slangPass({ id: 'point', mod: shader }),
|
|
13
|
-
pack: c => shader.packInstances(c, c.count),
|
|
15
|
+
pack: c => shader.packInstances({ ...c, color: colorBits(c) }, c.count),
|
|
14
16
|
},
|
|
15
17
|
writeUniforms(scratch, clip, block, frame, params) {
|
|
16
18
|
shader.writeUniforms(scratch, {
|
|
@@ -18,6 +20,8 @@ export const pointMark = {
|
|
|
18
20
|
canvasHeight: frame.canvasHeight,
|
|
19
21
|
domainMin: params.domain[0],
|
|
20
22
|
domainMax: params.domain[1],
|
|
23
|
+
valueScaleType: valueScaleTypeCode(params.scaleType),
|
|
24
|
+
...rampUniforms(params.ramp),
|
|
21
25
|
zero: 0,
|
|
22
26
|
// viewportWidth and radiusPx stay in CSS units to match canvasHeight:
|
|
23
27
|
// mixing a DPR-scaled radius with a CSS-scaled height draws vertically
|
|
@@ -28,15 +32,17 @@ export const pointMark = {
|
|
|
28
32
|
});
|
|
29
33
|
},
|
|
30
34
|
paintBlock(ctx, channels, block, frame, params) {
|
|
31
|
-
const { x, x2, y,
|
|
35
|
+
const { x, x2, y, glyph, count } = channels;
|
|
32
36
|
if (count === 0) {
|
|
33
37
|
return;
|
|
34
38
|
}
|
|
39
|
+
const color = paintColors(channels, count, params.ramp);
|
|
35
40
|
const { diameterPx, domain } = params;
|
|
36
41
|
const r = diameterPx / 2;
|
|
37
42
|
const canvasHeight = frame.canvasHeight;
|
|
38
43
|
const domainMin = domain[0];
|
|
39
44
|
const domainMax = domain[1];
|
|
45
|
+
const st = valueScaleTypeCode(params.scaleType);
|
|
40
46
|
// The per-block closure, not the six-argument `bpToScreenPx`: that spelling
|
|
41
47
|
// re-derives the region span and the block width at every call, and this
|
|
42
48
|
// loop makes two calls per instance. Measured at 1.67x on 100K points.
|
|
@@ -56,7 +62,7 @@ export const pointMark = {
|
|
|
56
62
|
}
|
|
57
63
|
const xStart = bpToPx(x[i]);
|
|
58
64
|
const xEnd = bpToPx(x2[i]);
|
|
59
|
-
const yPx =
|
|
65
|
+
const yPx = valueToYPxScaled(y[i], domainMin, domainMax, canvasHeight, st);
|
|
60
66
|
const widthPx = Math.abs(xEnd - xStart);
|
|
61
67
|
if (pointDrawsBar(widthPx, r)) {
|
|
62
68
|
ctx.rect(Math.min(xStart, xEnd), yPx - r, widthPx, diameterPx);
|
|
@@ -67,6 +73,25 @@ export const pointMark = {
|
|
|
67
73
|
}
|
|
68
74
|
ctx.fill();
|
|
69
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.
|
|
70
95
|
hitNearest(channels, block, frame, params, xPx, yPx, candidates, maxDistSq) {
|
|
71
96
|
const { x, x2, y } = channels;
|
|
72
97
|
const bpToPx = makeBpMapper(block);
|
|
@@ -74,13 +99,11 @@ export const pointMark = {
|
|
|
74
99
|
const domainMin = domain[0];
|
|
75
100
|
const domainMax = domain[1];
|
|
76
101
|
const canvasHeight = frame.canvasHeight;
|
|
102
|
+
const st = valueScaleTypeCode(params.scaleType);
|
|
77
103
|
return nearestInk(candidates, maxDistSq, i => {
|
|
78
104
|
const xStart = bpToPx(x[i]);
|
|
79
105
|
const xEnd = bpToPx(x2[i]);
|
|
80
|
-
const cy =
|
|
81
|
-
// A bar is the rect the painter fills, unpadded on every side, so the
|
|
82
|
-
// drawn bar and this box are one rectangle. A glyph has no rect to clamp
|
|
83
|
-
// into and offers 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
109
|
return pointDrawsBar(hi - lo, diameterPx / 2)
|
package/esm/marks/spanMark.d.ts
CHANGED
|
@@ -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>;
|
package/esm/marks/spanMark.js
CHANGED
|
@@ -1,10 +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
|
-
|
|
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
|
+
}
|
|
8
13
|
export const spanMark = {
|
|
9
14
|
id: 'span',
|
|
10
15
|
pass: {
|
|
@@ -41,19 +46,19 @@ export const spanMark = {
|
|
|
41
46
|
ctx.fillRect(spanLeft(xa, xb, width), offset + rowHeight * row[i] - scrollTop, width + seamPx, h);
|
|
42
47
|
}
|
|
43
48
|
},
|
|
44
|
-
|
|
49
|
+
ink(channels, block, _frame, params, i) {
|
|
45
50
|
const { x, x2, row } = channels;
|
|
46
51
|
const { rowHeight, rowProportion, minWidthPx, scrollTop } = params;
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
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
|
+
};
|
|
58
63
|
},
|
|
59
64
|
};
|
package/esm/marks/types.d.ts
CHANGED
|
@@ -31,8 +31,35 @@ export interface MarkContext2D extends ClipContext2D {
|
|
|
31
31
|
closePath(): void;
|
|
32
32
|
fill(): void;
|
|
33
33
|
stroke(): void;
|
|
34
|
+
drawImage?(image: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A canvas a mark's pass samples in place of a colour ramp, with the size the
|
|
38
|
+
* texture is allocated at. A fresh object per repaint of the canvas: the
|
|
39
|
+
* backend uploads on identity, and a canvas element's identity never moves.
|
|
40
|
+
*/
|
|
41
|
+
export interface MarkImage {
|
|
42
|
+
image: HTMLCanvasElement | OffscreenCanvas;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
}
|
|
46
|
+
/** What a mark's pass binds: a 256-entry RGBA ramp, or a canvas. */
|
|
47
|
+
export type MarkTexture = Uint8Array | MarkImage;
|
|
35
48
|
export type MarkFrame = FrameDimensions;
|
|
49
|
+
/** How a declared value scale reads its domain. */
|
|
50
|
+
export type MarkValueScaleType = 'linear' | 'log';
|
|
51
|
+
/**
|
|
52
|
+
* The quantitative colour scale a frame paints a ramp channel through: the
|
|
53
|
+
* domain, unioned over the loaded regions by the display; the declared scale
|
|
54
|
+
* type; and the 256-entry RGBA LUT the pass's sampler binds. A shape that
|
|
55
|
+
* reads it takes the raw values in `colorValue` and resolves them per frame,
|
|
56
|
+
* so a pan that widens the domain uploads no instance bytes.
|
|
57
|
+
*/
|
|
58
|
+
export interface MarkRamp {
|
|
59
|
+
domain: [number, number];
|
|
60
|
+
scale: MarkValueScaleType;
|
|
61
|
+
lut: Uint8Array;
|
|
62
|
+
}
|
|
36
63
|
/** A horizontal strip of the canvas, in CSS px down from its top edge. */
|
|
37
64
|
export interface MarkBand {
|
|
38
65
|
top: number;
|
|
@@ -45,6 +72,13 @@ export interface MarkHit {
|
|
|
45
72
|
y: number;
|
|
46
73
|
distSq: number;
|
|
47
74
|
}
|
|
75
|
+
/** An axis-aligned box of the canvas, in CSS px. */
|
|
76
|
+
export interface InkRect {
|
|
77
|
+
left: number;
|
|
78
|
+
top: number;
|
|
79
|
+
width: number;
|
|
80
|
+
height: number;
|
|
81
|
+
}
|
|
48
82
|
/**
|
|
49
83
|
* A shape: one hand-written `.slang`, one packer, one Canvas2D painter and one
|
|
50
84
|
* hit test, all reading the same channel arrays.
|
|
@@ -82,6 +116,19 @@ export interface MarkShape<TChannels, TParams> {
|
|
|
82
116
|
* and its triangles turn off together.
|
|
83
117
|
*/
|
|
84
118
|
paintsBlock?(block: RenderBlock, frame: MarkFrame, params: TParams): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* The rect `paintBlock` fills for instance `i`, or undefined for an instance
|
|
121
|
+
* it skips — a row the viewport has scrolled past, a bar with no height. A
|
|
122
|
+
* shape whose every instance is one box declares this and nothing about
|
|
123
|
+
* hovering: `defineMark` derives `hitNearest` from it where none is
|
|
124
|
+
* declared, and a display's highlight guide reads it for the instances it
|
|
125
|
+
* names. A shape whose ink is not a box (a ribbon, an arc, a capsule) leaves
|
|
126
|
+
* it off and keeps its own hit test.
|
|
127
|
+
*
|
|
128
|
+
* Painter-only overdraw — `span`'s seam — is not ink; `drawAgainstHit.ts`
|
|
129
|
+
* holds the two to each other within a pixel.
|
|
130
|
+
*/
|
|
131
|
+
ink?(channels: TChannels, block: RenderBlock, frame: MarkFrame, params: TParams, i: number): InkRect | undefined;
|
|
85
132
|
/**
|
|
86
133
|
* The nearest drawn ink to `(xPx, yPx)` among `candidates`, or undefined if
|
|
87
134
|
* nothing beats `maxDistSq`.
|
|
@@ -100,7 +147,8 @@ export interface MarkShape<TChannels, TParams> {
|
|
|
100
147
|
*
|
|
101
148
|
* Optional, because a shape earns it from a consumer like any other member.
|
|
102
149
|
* MAF answers a hit from row/column arithmetic over the alignment rather than
|
|
103
|
-
* from `span`'s channels, so it reads none.
|
|
150
|
+
* from `span`'s channels, so it reads none. A shape with `ink` and no hit test
|
|
151
|
+
* gets `inkHitNearest` — the nearest of its boxes — through `shapeHitNearest`.
|
|
104
152
|
*/
|
|
105
153
|
hitNearest?(channels: TChannels, block: RenderBlock, frame: MarkFrame, params: TParams, xPx: number, yPx: number, candidates: Iterable<number>, maxDistSq: number): MarkHit | undefined;
|
|
106
154
|
}
|
|
@@ -126,6 +174,15 @@ export interface StagedUniforms {
|
|
|
126
174
|
writer: unknown;
|
|
127
175
|
params: unknown;
|
|
128
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* A mark's pass as a frame plan draws it: what `hal.drawPass(id, regionKey,
|
|
179
|
+
* bufferOf)` takes and nothing else, resolved once per frame so the per-block
|
|
180
|
+
* loop reads two fields per mark.
|
|
181
|
+
*/
|
|
182
|
+
export interface PlannedPass {
|
|
183
|
+
readonly id: string;
|
|
184
|
+
readonly bufferOf: string | undefined;
|
|
185
|
+
}
|
|
129
186
|
/**
|
|
130
187
|
* A shape bound to one display's region payload and render state — what
|
|
131
188
|
* `defineMark` returns and what a display actually holds.
|
|
@@ -150,10 +207,33 @@ export interface Mark<TRegion, TState extends MarkFrame> {
|
|
|
150
207
|
* undefined case — a shader owns its sampler unconditionally, and a textured
|
|
151
208
|
* pass with no texture never draws on the WebGPU HAL.
|
|
152
209
|
*/
|
|
153
|
-
readonly texture?: (state: TState, region: TRegion) =>
|
|
210
|
+
readonly texture?: (state: TState, region: TRegion) => MarkTexture | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* The frame-level gate: whether the mark draws at all under `state`, read
|
|
213
|
+
* off the display-wide state and nothing else. `planMarks` asks it once per
|
|
214
|
+
* frame; `drawRegion`, `paintBlock` and `hitNearest` ask it per block, so a
|
|
215
|
+
* list drawn through `drawMarks` gates like one drawn through a plan — and
|
|
216
|
+
* so does the hit test, which is what stops a switched-off layer answering a
|
|
217
|
+
* hover over blank pixels. A question about the block is `paintsBlock`, on
|
|
218
|
+
* the shape.
|
|
219
|
+
*/
|
|
220
|
+
readonly enabled?: (state: TState) => boolean;
|
|
221
|
+
/**
|
|
222
|
+
* The pass a frame plan draws for this mark, or undefined for a mark the plan
|
|
223
|
+
* form cannot carry: one with a `band` (a plan's caller owns the scissor) or a
|
|
224
|
+
* `paintsBlock` (a plan asks the block nothing).
|
|
225
|
+
*/
|
|
226
|
+
readonly planned?: PlannedPass;
|
|
154
227
|
drawRegion(hal: GpuHal, scratch: ArrayBuffer, block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState, regionKey: number, staged?: StagedUniforms): void;
|
|
155
228
|
paintBlock(ctx: MarkContext2D, region: TRegion, block: RenderBlock, state: TState): void;
|
|
156
229
|
hitNearest?(region: TRegion, block: RenderBlock, state: TState, xPx: number, yPx: number, candidates: Iterable<number>, maxDistSq: number): MarkHit | undefined;
|
|
230
|
+
/**
|
|
231
|
+
* The box instance `i` painted under the mark's gates, clipped to its band
|
|
232
|
+
* where it has one: what a highlight guide draws for the instance a display
|
|
233
|
+
* names. Undefined off a shape with no `ink`, and for an instance nothing
|
|
234
|
+
* drew.
|
|
235
|
+
*/
|
|
236
|
+
ink?(region: TRegion, block: RenderBlock, state: TState, i: number): InkRect | undefined;
|
|
157
237
|
}
|
|
158
238
|
/**
|
|
159
239
|
* `{ shape, channels }` over typed arrays — the declaration a display writes in
|
|
@@ -205,6 +285,12 @@ export interface Mark<TRegion, TState extends MarkFrame> {
|
|
|
205
285
|
* nothing, and the shape still places Y against the whole canvas — the band is
|
|
206
286
|
* a clip, never an offset, which is what lets one `scrollTop` serve both
|
|
207
287
|
* backends. A mark without one paints wherever its shape puts ink.
|
|
288
|
+
*
|
|
289
|
+
* `enabled` is the setting that turns the mark off for a whole frame — the
|
|
290
|
+
* pileup's "show mismatches" — and it is one gate for the three consumers: a
|
|
291
|
+
* mark that does not draw does not answer a hover either. It reads the state
|
|
292
|
+
* alone so `planMarks` can resolve it once per frame; a gate that needs the
|
|
293
|
+
* block is the shape's `paintsBlock`.
|
|
208
294
|
*/
|
|
209
295
|
export declare function defineMark<TRegion, TState extends MarkFrame, TChannels, TParams>(spec: {
|
|
210
296
|
shape: MarkShape<TChannels, TParams>;
|
|
@@ -212,5 +298,6 @@ export declare function defineMark<TRegion, TState extends MarkFrame, TChannels,
|
|
|
212
298
|
params: (state: TState, region: TRegion, block: RenderBlock) => TParams;
|
|
213
299
|
bufferOf?: Mark<TRegion, TState>;
|
|
214
300
|
band?: (state: TState) => MarkBand;
|
|
215
|
-
texture?: (state: TState, region: TRegion) =>
|
|
301
|
+
texture?: (state: TState, region: TRegion) => MarkTexture | undefined;
|
|
302
|
+
enabled?: (state: TState) => boolean;
|
|
216
303
|
}): Mark<TRegion, TState>;
|
package/esm/marks/types.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { devicePxBand, withClip } from '../canvas2dUtils.js';
|
|
2
|
+
import { shapeHitNearest } from './markHit.js';
|
|
2
3
|
/**
|
|
3
4
|
* `{ shape, channels }` over typed arrays — the declaration a display writes in
|
|
4
5
|
* place of a `GpuXxxRenderer` class, a `Canvas2DXxxRenderer` class, a factory
|
|
@@ -49,9 +50,17 @@ import { devicePxBand, withClip } from '../canvas2dUtils.js';
|
|
|
49
50
|
* nothing, and the shape still places Y against the whole canvas — the band is
|
|
50
51
|
* a clip, never an offset, which is what lets one `scrollTop` serve both
|
|
51
52
|
* backends. A mark without one paints wherever its shape puts ink.
|
|
53
|
+
*
|
|
54
|
+
* `enabled` is the setting that turns the mark off for a whole frame — the
|
|
55
|
+
* pileup's "show mismatches" — and it is one gate for the three consumers: a
|
|
56
|
+
* mark that does not draw does not answer a hover either. It reads the state
|
|
57
|
+
* alone so `planMarks` can resolve it once per frame; a gate that needs the
|
|
58
|
+
* block is the shape's `paintsBlock`.
|
|
52
59
|
*/
|
|
53
60
|
export function defineMark(spec) {
|
|
54
|
-
const { shape, channels, params, band, texture } = spec;
|
|
61
|
+
const { shape, channels, params, band, texture, enabled } = spec;
|
|
62
|
+
const hitNearest = shapeHitNearest(shape);
|
|
63
|
+
const shapeInk = shape.ink?.bind(shape);
|
|
55
64
|
const lender = spec.bufferOf?.pass;
|
|
56
65
|
if (lender &&
|
|
57
66
|
(lender.instanceStride !== shape.pass.instanceStride ||
|
|
@@ -60,6 +69,7 @@ export function defineMark(spec) {
|
|
|
60
69
|
throw new Error(`mark ${shape.id} draws off ${lender.id}'s buffer but declares a different instance struct`);
|
|
61
70
|
}
|
|
62
71
|
const bufferOf = lender?.id;
|
|
72
|
+
const planned = band || shape.paintsBlock ? undefined : { id: shape.pass.id, bufferOf };
|
|
63
73
|
return {
|
|
64
74
|
pass: {
|
|
65
75
|
...shape.pass,
|
|
@@ -70,7 +80,12 @@ export function defineMark(spec) {
|
|
|
70
80
|
},
|
|
71
81
|
bufferOf,
|
|
72
82
|
texture,
|
|
83
|
+
enabled,
|
|
84
|
+
planned,
|
|
73
85
|
drawRegion(hal, scratch, block, clip, region, state, regionKey, staged) {
|
|
86
|
+
if (enabled && !enabled(state)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
74
89
|
const strip = band?.(state);
|
|
75
90
|
const scissor = strip
|
|
76
91
|
? devicePxBand(strip.top, strip.height, clip.scaleY, clip.pxH)
|
|
@@ -98,6 +113,9 @@ export function defineMark(spec) {
|
|
|
98
113
|
}
|
|
99
114
|
},
|
|
100
115
|
paintBlock(ctx, region, block, state) {
|
|
116
|
+
if (enabled && !enabled(state)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
101
119
|
const strip = band?.(state);
|
|
102
120
|
const c = channels(region);
|
|
103
121
|
if ((!strip || strip.height > 0) && c !== undefined) {
|
|
@@ -115,7 +133,7 @@ export function defineMark(spec) {
|
|
|
115
133
|
}
|
|
116
134
|
}
|
|
117
135
|
},
|
|
118
|
-
hitNearest:
|
|
136
|
+
hitNearest: hitNearest
|
|
119
137
|
? (region, block, state, xPx, yPx, candidates, maxDistSq) => {
|
|
120
138
|
const strip = band?.(state);
|
|
121
139
|
const c = channels(region);
|
|
@@ -125,19 +143,53 @@ export function defineMark(spec) {
|
|
|
125
143
|
// either. The cursor being inside the strip is a fourth, and the ink
|
|
126
144
|
// being inside it the fifth: a band is a CLIP, so ink outside one was
|
|
127
145
|
// never drawn and cannot be the nearest thing to anything.
|
|
128
|
-
|
|
146
|
+
//
|
|
147
|
+
// The fifth gate REJECTS rather than re-runs, and that is the one
|
|
148
|
+
// conservative edge here: the shape picks its winner without knowing
|
|
149
|
+
// about the band, so a clipped-away nearest masks a farther candidate
|
|
150
|
+
// whose ink IS inside the strip, and the mark answers nothing instead
|
|
151
|
+
// of that one. Safe in the direction that matters — it never claims a
|
|
152
|
+
// hover over pixels neither backend drew — and closing it would mean
|
|
153
|
+
// pushing the band down into every shape's own ink test, where the
|
|
154
|
+
// band is deliberately not.
|
|
155
|
+
if ((!enabled || enabled(state)) &&
|
|
156
|
+
(!strip || strip.height > 0) &&
|
|
129
157
|
c !== undefined &&
|
|
130
158
|
!bandExcludes(strip, yPx)) {
|
|
131
159
|
const p = params(state, region, block);
|
|
132
160
|
if (!shape.paintsBlock || shape.paintsBlock(block, state, p)) {
|
|
133
|
-
hit =
|
|
161
|
+
hit = hitNearest(c, block, state, p, xPx, yPx, candidates, maxDistSq);
|
|
134
162
|
}
|
|
135
163
|
}
|
|
136
164
|
return hit && !bandExcludes(strip, hit.y) ? hit : undefined;
|
|
137
165
|
}
|
|
138
166
|
: undefined,
|
|
167
|
+
ink: shapeInk
|
|
168
|
+
? (region, block, state, i) => {
|
|
169
|
+
const strip = band?.(state);
|
|
170
|
+
const c = channels(region);
|
|
171
|
+
if ((enabled && !enabled(state)) ||
|
|
172
|
+
(strip && strip.height <= 0) ||
|
|
173
|
+
c === undefined) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const p = params(state, region, block);
|
|
177
|
+
if (shape.paintsBlock && !shape.paintsBlock(block, state, p)) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const r = shapeInk(c, block, state, p, i);
|
|
181
|
+
return r && strip ? clipToBand(r, strip) : r;
|
|
182
|
+
}
|
|
183
|
+
: undefined,
|
|
139
184
|
};
|
|
140
185
|
}
|
|
186
|
+
function clipToBand(r, strip) {
|
|
187
|
+
const top = Math.max(r.top, strip.top);
|
|
188
|
+
const bottom = Math.min(r.top + r.height, strip.top + strip.height);
|
|
189
|
+
return bottom > top
|
|
190
|
+
? { left: r.left, top, width: r.width, height: bottom - top }
|
|
191
|
+
: undefined;
|
|
192
|
+
}
|
|
141
193
|
// What a mark with nothing in this region packs. An empty pack IS the release
|
|
142
194
|
// — every HAL deletes the pass's prior buffer before it looks at the count.
|
|
143
195
|
const NOTHING = new ArrayBuffer(0);
|