@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.
- 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 +49 -11
- 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 +21 -9
- package/esm/marks/markBackend.js +63 -29
- package/esm/marks/markHit.d.ts +32 -0
- package/esm/marks/markHit.js +61 -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 +39 -23
- package/esm/marks/spanMark.d.ts +8 -0
- package/esm/marks/spanMark.js +19 -26
- package/esm/marks/types.d.ts +101 -4
- package/esm/marks/types.js +78 -5
- package/esm/renderBlock.d.ts +14 -0
- package/esm/renderBlock.js +25 -0
- 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 +2 -0
- package/esm/shaders/pointMark.js.generated.js +42 -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 +5 -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 +45 -0
- package/src/shaders/pointMark.slang +35 -33
- package/src/shaders/scoreScale.generated.ts +6 -0
- package/src/shaders/scoreScale.slang +1 -0
- package/src/shaders/valueScale.slang +52 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { inkOnRect, shapeHitNearest } from './markHit.js';
|
|
2
|
+
const IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
1
3
|
/**
|
|
2
|
-
* A `MarkContext2D` that records every rect a painter fills, in paint order
|
|
3
|
-
* `fillRect` and path `rect` alike, so a shape that
|
|
4
|
-
* the same list as one that fills per instance. A
|
|
5
|
-
* `moveTo`/`lineTo` and filled records its
|
|
6
|
-
* shape whose hit test answers a polygon's box
|
|
7
|
-
* triangle) is held to
|
|
4
|
+
* A `MarkContext2D` that records every rect a painter fills, in paint order and
|
|
5
|
+
* in CANVAS coordinates — `fillRect` and path `rect` alike, so a shape that
|
|
6
|
+
* batches by colour records the same list as one that fills per instance. A
|
|
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
|
|
10
|
+
* points, whose hull contains it.
|
|
8
11
|
*
|
|
9
12
|
* A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
|
|
10
13
|
* edge's bounding box widened by half the current `lineWidth` — the extent a
|
|
@@ -13,34 +16,121 @@
|
|
|
13
16
|
* otherwise record the run's hull as a single instance. `fillStyle` on such a
|
|
14
17
|
* record is the `strokeStyle` it went down in; the field is the colour of the
|
|
15
18
|
* ink either way.
|
|
19
|
+
*
|
|
20
|
+
* `save`/`restore` bracket the transform and the styles the way a real context
|
|
21
|
+
* does, and every recorded point goes through the CTM. The half-widths — a
|
|
22
|
+
* stroke's and a `strokeRect`'s — are measured AFTER it, so they are exact under
|
|
23
|
+
* a translation and approximate under a scale; no shape in tree strokes under
|
|
24
|
+
* one, and a rotated CTM records the ink's axis-aligned box rather than its
|
|
25
|
+
* silhouette.
|
|
16
26
|
*/
|
|
17
27
|
export function recordingContext() {
|
|
18
28
|
const calls = [];
|
|
19
29
|
const points = [];
|
|
20
30
|
const edges = [];
|
|
31
|
+
let m = [...IDENTITY];
|
|
32
|
+
const stack = [];
|
|
33
|
+
const px = (x, y) => m[0] * x + m[2] * y + m[4];
|
|
34
|
+
const py = (x, y) => m[1] * x + m[3] * y + m[5];
|
|
35
|
+
const pushPoint = (x, y) => {
|
|
36
|
+
points.push([px(x, y), py(x, y)]);
|
|
37
|
+
};
|
|
38
|
+
// The transformed box of an axis-aligned rect. An axis-aligned CTM scales the
|
|
39
|
+
// extents directly rather than differencing two mapped corners, so an
|
|
40
|
+
// untranslated rect records the width the painter asked for and not a value
|
|
41
|
+
// one ulp off it; only a rotation falls back to the four-corner hull.
|
|
42
|
+
const boxOf = (x, y, w, h) => {
|
|
43
|
+
if (m[1] === 0 && m[2] === 0) {
|
|
44
|
+
const x0 = px(x, y);
|
|
45
|
+
const y0 = py(x, y);
|
|
46
|
+
const dx = m[0] * w;
|
|
47
|
+
const dy = m[3] * h;
|
|
48
|
+
return {
|
|
49
|
+
x: Math.min(x0, x0 + dx),
|
|
50
|
+
y: Math.min(y0, y0 + dy),
|
|
51
|
+
w: Math.abs(dx),
|
|
52
|
+
h: Math.abs(dy),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const xs = [px(x, y), px(x + w, y), px(x, y + h), px(x + w, y + h)];
|
|
56
|
+
const ys = [py(x, y), py(x + w, y), py(x, y + h), py(x + w, y + h)];
|
|
57
|
+
const left = Math.min(...xs);
|
|
58
|
+
const top = Math.min(...ys);
|
|
59
|
+
return {
|
|
60
|
+
x: left,
|
|
61
|
+
y: top,
|
|
62
|
+
w: Math.max(...xs) - left,
|
|
63
|
+
h: Math.max(...ys) - top,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
21
66
|
const ctx = {
|
|
22
67
|
fillStyle: '',
|
|
23
68
|
strokeStyle: '',
|
|
24
69
|
lineWidth: 1,
|
|
25
70
|
lineCap: 'butt',
|
|
26
|
-
|
|
27
|
-
|
|
71
|
+
lineJoin: 'miter',
|
|
72
|
+
save() {
|
|
73
|
+
const { fillStyle, strokeStyle, lineWidth, lineCap } = this;
|
|
74
|
+
stack.push({ m: [...m], fillStyle, strokeStyle, lineWidth, lineCap });
|
|
75
|
+
},
|
|
76
|
+
restore() {
|
|
77
|
+
const saved = stack.pop();
|
|
78
|
+
if (saved) {
|
|
79
|
+
m = saved.m;
|
|
80
|
+
this.fillStyle = saved.fillStyle;
|
|
81
|
+
this.strokeStyle = saved.strokeStyle;
|
|
82
|
+
this.lineWidth = saved.lineWidth;
|
|
83
|
+
this.lineCap = saved.lineCap;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
translate(x, y) {
|
|
87
|
+
m = [m[0], m[1], m[2], m[3], px(x, y), py(x, y)];
|
|
88
|
+
},
|
|
89
|
+
scale(x, y) {
|
|
90
|
+
m = [m[0] * x, m[1] * x, m[2] * y, m[3] * y, m[4], m[5]];
|
|
91
|
+
},
|
|
92
|
+
rotate(angle) {
|
|
93
|
+
const c = Math.cos(angle);
|
|
94
|
+
const s = Math.sin(angle);
|
|
95
|
+
m = [
|
|
96
|
+
m[0] * c + m[2] * s,
|
|
97
|
+
m[1] * c + m[3] * s,
|
|
98
|
+
m[2] * c - m[0] * s,
|
|
99
|
+
m[3] * c - m[1] * s,
|
|
100
|
+
m[4],
|
|
101
|
+
m[5],
|
|
102
|
+
];
|
|
103
|
+
},
|
|
28
104
|
beginPath() {
|
|
29
105
|
points.length = 0;
|
|
30
106
|
edges.length = 0;
|
|
31
107
|
},
|
|
32
108
|
clip() { },
|
|
109
|
+
setLineDash() { },
|
|
33
110
|
moveTo(x, y) {
|
|
34
|
-
|
|
111
|
+
pushPoint(x, y);
|
|
35
112
|
},
|
|
36
113
|
lineTo(x, y) {
|
|
37
114
|
const from = points.at(-1);
|
|
115
|
+
pushPoint(x, y);
|
|
116
|
+
const to = points.at(-1);
|
|
38
117
|
if (from) {
|
|
39
|
-
edges.push([from[0], from[1],
|
|
118
|
+
edges.push([from[0], from[1], to[0], to[1]]);
|
|
40
119
|
}
|
|
41
|
-
points.push([x, y]);
|
|
42
120
|
},
|
|
43
|
-
|
|
121
|
+
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
122
|
+
pushPoint(cp1x, cp1y);
|
|
123
|
+
pushPoint(cp2x, cp2y);
|
|
124
|
+
pushPoint(x, y);
|
|
125
|
+
},
|
|
126
|
+
arc(x, y, radius) {
|
|
127
|
+
pushPoint(x - radius, y - radius);
|
|
128
|
+
pushPoint(x + radius, y + radius);
|
|
129
|
+
},
|
|
130
|
+
ellipse(x, y, radiusX, radiusY) {
|
|
131
|
+
pushPoint(x - radiusX, y - radiusY);
|
|
132
|
+
pushPoint(x + radiusX, y + radiusY);
|
|
133
|
+
},
|
|
44
134
|
closePath() { },
|
|
45
135
|
fill() {
|
|
46
136
|
if (points.length > 0) {
|
|
@@ -76,57 +166,189 @@ export function recordingContext() {
|
|
|
76
166
|
edges.length = 0;
|
|
77
167
|
},
|
|
78
168
|
rect(x, y, w, h) {
|
|
79
|
-
calls.push({ x, y, w, h, fillStyle: this.fillStyle });
|
|
169
|
+
calls.push({ ...boxOf(x, y, w, h), fillStyle: this.fillStyle });
|
|
80
170
|
},
|
|
81
171
|
fillRect(x, y, w, h) {
|
|
82
|
-
calls.push({ x, y, w, h, fillStyle: this.fillStyle });
|
|
172
|
+
calls.push({ ...boxOf(x, y, w, h), fillStyle: this.fillStyle });
|
|
173
|
+
},
|
|
174
|
+
strokeRect(x, y, w, h) {
|
|
175
|
+
const half = this.lineWidth / 2;
|
|
176
|
+
const box = boxOf(x, y, w, h);
|
|
177
|
+
calls.push({
|
|
178
|
+
x: box.x - half,
|
|
179
|
+
y: box.y - half,
|
|
180
|
+
w: box.w + this.lineWidth,
|
|
181
|
+
h: box.h + this.lineWidth,
|
|
182
|
+
fillStyle: this.strokeStyle,
|
|
183
|
+
});
|
|
83
184
|
},
|
|
84
185
|
};
|
|
85
|
-
return { ctx
|
|
186
|
+
return { ctx, calls };
|
|
187
|
+
}
|
|
188
|
+
function contains(r, x, y, slack = 0) {
|
|
189
|
+
return (x >= r.x - slack &&
|
|
190
|
+
x <= r.x + r.w + slack &&
|
|
191
|
+
y >= r.y - slack &&
|
|
192
|
+
y <= r.y + r.h + slack);
|
|
86
193
|
}
|
|
87
|
-
function
|
|
88
|
-
|
|
194
|
+
function unionBox(rs) {
|
|
195
|
+
let box;
|
|
196
|
+
for (const r of rs) {
|
|
197
|
+
box = box
|
|
198
|
+
? {
|
|
199
|
+
x: Math.min(box.x, r.x),
|
|
200
|
+
y: Math.min(box.y, r.y),
|
|
201
|
+
w: Math.max(box.x + box.w, r.x + r.w) - Math.min(box.x, r.x),
|
|
202
|
+
h: Math.max(box.y + box.h, r.y + r.h) - Math.min(box.y, r.y),
|
|
203
|
+
}
|
|
204
|
+
: { x: r.x, y: r.y, w: r.w, h: r.h };
|
|
205
|
+
}
|
|
206
|
+
return box;
|
|
89
207
|
}
|
|
208
|
+
// The float slack the two distance comparisons take. Both sides are px
|
|
209
|
+
// arithmetic over the same projection, so a real disagreement is orders of
|
|
210
|
+
// magnitude above this and a rounding one is below it.
|
|
211
|
+
const EPS = 1e-9;
|
|
90
212
|
/**
|
|
91
|
-
* The draw-against-hit gate
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
213
|
+
* The draw-against-hit gate: paint the block, then walk it in `step` px and
|
|
214
|
+
* hold `hitNearest` to what the painter actually put on the canvas.
|
|
215
|
+
*
|
|
216
|
+
* **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.
|
|
225
|
+
*
|
|
226
|
+
* Four claims, all of them one-directional and true of every shape:
|
|
227
|
+
*
|
|
228
|
+
* - 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
|
|
230
|
+
* painter drew for the instance the hit names,
|
|
231
|
+
* - `hit.distSq` is no smaller than the distance to that box, since the box
|
|
232
|
+
* contains the ink and cannot be farther than it,
|
|
233
|
+
* - `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
|
|
235
|
+
* distance its own reported point does not support has two spellings of where
|
|
236
|
+
* its ink is, and the nearest-wins walk is resolved by the one the caller
|
|
237
|
+
* cannot see.
|
|
238
|
+
*
|
|
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.
|
|
246
|
+
*
|
|
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.
|
|
96
254
|
*
|
|
97
255
|
* Returns the violations rather than asserting, so a test reads as
|
|
98
256
|
* `expect(sweep(...)).toEqual([])`. `maxDistSq` is the caller's, because a
|
|
99
257
|
* shape whose hit target is wider than its ink (`point`) needs a bound that
|
|
100
258
|
* admits the whole rect.
|
|
101
259
|
*/
|
|
102
|
-
export function sweepDrawAgainstHit(shape, channels, block, frame, params, { maxDistSq, step = 0.5 }) {
|
|
260
|
+
export function sweepDrawAgainstHit(shape, channels, block, frame, params, { maxDistSq, step = 0.5, sliceOne, inkSlackPx = 1, }) {
|
|
261
|
+
const { count } = channels;
|
|
103
262
|
const violations = [];
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return
|
|
263
|
+
const paint = (c) => {
|
|
264
|
+
const { ctx, calls } = recordingContext();
|
|
265
|
+
shape.paintBlock(ctx, c, block, frame, params);
|
|
266
|
+
return calls;
|
|
267
|
+
};
|
|
268
|
+
const rects = paint(channels);
|
|
269
|
+
// Positional attribution, and the licence for the containment claim below.
|
|
270
|
+
const perRect = rects.length === count;
|
|
271
|
+
if (!perRect && !sliceOne) {
|
|
272
|
+
return [`painted ${rects.length} rects for ${count} instances`];
|
|
273
|
+
}
|
|
274
|
+
const boxes = sliceOne
|
|
275
|
+
? Array.from({ length: count }, (_, i) => unionBox(paint(sliceOne(channels, i))))
|
|
276
|
+
: 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
|
+
}
|
|
108
305
|
}
|
|
109
|
-
const
|
|
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);
|
|
110
311
|
let yMin = Infinity;
|
|
111
312
|
let yMax = -Infinity;
|
|
112
|
-
for (const
|
|
113
|
-
|
|
114
|
-
|
|
313
|
+
for (const b of boxes) {
|
|
314
|
+
if (b) {
|
|
315
|
+
yMin = Math.min(yMin, b.y);
|
|
316
|
+
yMax = Math.max(yMax, b.y + b.h);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (yMin === Infinity) {
|
|
320
|
+
return [`painted nothing for ${count} instances`];
|
|
115
321
|
}
|
|
116
322
|
const x0 = Math.min(block.screenStartPx, block.screenEndPx) - 1;
|
|
117
323
|
const x1 = Math.max(block.screenStartPx, block.screenEndPx) + 1;
|
|
118
324
|
for (let y = Math.floor(yMin) - 1; y <= yMax + 1; y += step) {
|
|
119
325
|
for (let x = x0; x <= x1; x += step) {
|
|
120
326
|
let expected = -1;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
327
|
+
if (perRect) {
|
|
328
|
+
for (let k = count - 1; k >= 0; k--) {
|
|
329
|
+
if (contains(boxes[k], x, y)) {
|
|
330
|
+
expected = k;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
125
333
|
}
|
|
126
334
|
}
|
|
127
|
-
const hit =
|
|
128
|
-
|
|
129
|
-
|
|
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`);
|
|
339
|
+
}
|
|
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
|
+
}
|
|
130
352
|
}
|
|
131
353
|
if (expected !== -1 && hit?.index !== expected) {
|
|
132
354
|
violations.push(`(${x}, ${y}) is on rect ${expected}, answered ${hit?.index ?? 'nothing'}`);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { MarkContext2D } from './types.ts';
|
|
1
|
+
import type { InkRect, MarkContext2D } from './types.ts';
|
|
2
|
+
/** The path calls a glyph is built from — what a recorder has to answer. */
|
|
3
|
+
export type GlyphPath = Pick<MarkContext2D, 'moveTo' | 'lineTo' | 'closePath' | 'rect' | 'arc'>;
|
|
2
4
|
/**
|
|
3
5
|
* Append one of the `point` shape's glyphs to the current path, centred on
|
|
4
6
|
* `(cx, y)`.
|
|
@@ -13,4 +15,10 @@ import type { MarkContext2D } from './types.ts';
|
|
|
13
15
|
* hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
|
|
14
16
|
* is that consumer.
|
|
15
17
|
*/
|
|
16
|
-
export declare function appendGlyph(ctx:
|
|
18
|
+
export declare function appendGlyph(ctx: GlyphPath, glyph: number, cx: number, y: number, diameter: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* The box `appendGlyph` paints inside, by the same split and snap: the
|
|
21
|
+
* diamond is scaled by `DIAMOND_GLYPH_SCALE` and the small square snaps to
|
|
22
|
+
* the pixel grid, so neither is the diameter about the centre.
|
|
23
|
+
*/
|
|
24
|
+
export declare function glyphBox(glyph: number, cx: number, y: number, diameter: number): InkRect;
|
package/esm/marks/glyphPaint.js
CHANGED
|
@@ -39,3 +39,24 @@ 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
|
+
* The box `appendGlyph` paints inside, by the same split and snap: the
|
|
44
|
+
* diamond is scaled by `DIAMOND_GLYPH_SCALE` and the small square snaps to
|
|
45
|
+
* the pixel grid, so neither is the diameter about the centre.
|
|
46
|
+
*/
|
|
47
|
+
export function glyphBox(glyph, cx, y, diameter) {
|
|
48
|
+
const r = diameter / 2;
|
|
49
|
+
if (glyph === GLYPH_DIAMOND) {
|
|
50
|
+
const ri = r * DIAMOND_GLYPH_SCALE;
|
|
51
|
+
return { left: cx - ri, top: y - ri, width: 2 * ri, height: 2 * ri };
|
|
52
|
+
}
|
|
53
|
+
if (glyph !== GLYPH_TRIANGLE && diameter <= SMALL_POINT_MAX_DIAMETER) {
|
|
54
|
+
return {
|
|
55
|
+
left: crispSquareTopLeftPx(cx, diameter),
|
|
56
|
+
top: crispSquareTopLeftPx(y, diameter),
|
|
57
|
+
width: diameter,
|
|
58
|
+
height: diameter,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { left: cx - r, top: y - r, width: diameter, height: diameter };
|
|
62
|
+
}
|
package/esm/marks/index.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
export { defineMark } from './types.ts';
|
|
2
2
|
export { paintMarkBlocks } from './markPaint.ts';
|
|
3
|
+
export { planMarks } from './markPlan.ts';
|
|
4
|
+
export { inkOfInstances } from './markInk.ts';
|
|
5
|
+
export { barMark } from './barMark.ts';
|
|
3
6
|
export { pointMark } from './pointMark.ts';
|
|
4
|
-
export { spanMark } from './spanMark.ts';
|
|
7
|
+
export { blockPx, spanMark } from './spanMark.ts';
|
|
5
8
|
export { appendGlyph } from './glyphPaint.ts';
|
|
6
9
|
export { abgrToCssRgba, makeAbgrFill } from './colorFill.ts';
|
|
7
|
-
export type {
|
|
10
|
+
export type { MarkPlan } from './markPlan.ts';
|
|
11
|
+
export type { MarkInstance } from './markInk.ts';
|
|
12
|
+
export type { Mark, MarkBand, MarkContext2D, MarkFrame, MarkImage, MarkRamp, MarkTexture, MarkValueScaleType, InkRect, MarkHit, MarkShape, PlannedPass, StagedUniforms, } from './types.ts';
|
|
13
|
+
export type { BarChannels, BarParams } from './barMark.ts';
|
|
8
14
|
export type { PointChannels, PointParams } from './pointMark.ts';
|
|
9
15
|
export type { SpanChannels, SpanParams } from './spanMark.ts';
|
|
16
|
+
export type { ColorChannel } from './markRamp.ts';
|
package/esm/marks/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { defineMark } from './types.js';
|
|
2
2
|
export { paintMarkBlocks } from './markPaint.js';
|
|
3
|
+
export { planMarks } from './markPlan.js';
|
|
4
|
+
export { inkOfInstances } from './markInk.js';
|
|
5
|
+
export { barMark } from './barMark.js';
|
|
3
6
|
export { pointMark } from './pointMark.js';
|
|
4
|
-
export { spanMark } from './spanMark.js';
|
|
7
|
+
export { blockPx, spanMark } from './spanMark.js';
|
|
5
8
|
export { appendGlyph } from './glyphPaint.js';
|
|
6
9
|
export { abgrToCssRgba, makeAbgrFill } from './colorFill.js';
|
|
@@ -6,13 +6,9 @@ import type { InstancePass } from '../instancePass.ts';
|
|
|
6
6
|
import type { ClearColor, PerRegionRenderingBackend } from '../perRegionRenderingBackend.ts';
|
|
7
7
|
import type { RenderBlock } from '../renderBlock.ts';
|
|
8
8
|
import type { FrameDimensions } from '../renderingBackendBase.ts';
|
|
9
|
+
import type { MarkPlan } from './markPlan.ts';
|
|
9
10
|
import type { Mark } from './types.ts';
|
|
10
|
-
/**
|
|
11
|
-
* Pack and upload one region's buffers for a mark list — every mark that owns
|
|
12
|
-
* one, which is the whole list minus those drawing off another's (`bufferOf`).
|
|
13
|
-
* That skip is the reason a display's upload is this call rather than a loop:
|
|
14
|
-
* uploading to a borrowed pass is silent, and its buffer is the lender's.
|
|
15
|
-
*/
|
|
11
|
+
/** Pack and upload one region's buffers for a mark list. */
|
|
16
12
|
export declare function uploadMarks<TRegion, TState extends FrameDimensions>(hal: GpuHal, regionKey: number, marks: readonly Mark<TRegion, TState>[], data: TRegion): void;
|
|
17
13
|
/**
|
|
18
14
|
* Draw one already-clipped block through a mark list.
|
|
@@ -23,8 +19,25 @@ export declare function uploadMarks<TRegion, TState extends FrameDimensions>(hal
|
|
|
23
19
|
* coverage strip inside the column it hands over. `GpuPerRegionRenderingBackend`
|
|
24
20
|
* sets both per block already, so for a backend on that scaffold the viewport
|
|
25
21
|
* set here is the same rect twice.
|
|
22
|
+
*
|
|
23
|
+
* A caller that narrows it must hand over marks declaring no `band`, and that
|
|
24
|
+
* is the one way the two clips do not compose: a banded mark scissors to its
|
|
25
|
+
* strip and hands the BLOCK COLUMN back rather than what the caller had. The
|
|
26
|
+
* split is deliberate — `render-core/CLAUDE.md` §Drawing has which displays
|
|
27
|
+
* take which side — but nothing enforces it, so it is stated at both.
|
|
26
28
|
*/
|
|
27
29
|
export declare function drawMarks<TRegion, TState extends FrameDimensions>(hal: GpuHal, scratch: ArrayBuffer, marks: readonly Mark<TRegion, TState>[], block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState, regionKey: number): void;
|
|
30
|
+
/**
|
|
31
|
+
* Draw one block's planned passes off the uniforms the caller has already
|
|
32
|
+
* staged for it. No gate, no lens, no write: `planMarks` settled the frame
|
|
33
|
+
* question and the caller's own `hal.writeUniforms` settled the block's, so
|
|
34
|
+
* what is left per mark is the `drawPass` — the loop a display with its own
|
|
35
|
+
* frame scaffold was already running over a hand-kept pass list.
|
|
36
|
+
*
|
|
37
|
+
* Viewport and scissor are the caller's, as with `drawMarks`' scissor: a
|
|
38
|
+
* plan's marks declare no `band`, so nothing here narrows or restores either.
|
|
39
|
+
*/
|
|
40
|
+
export declare function drawPlannedPasses(hal: GpuHal, plan: Pick<MarkPlan<never, never>, 'passes'>, regionKey: number): void;
|
|
28
41
|
/**
|
|
29
42
|
* The HAL-side half of `createMarkBackend`, exported so a display's mark list
|
|
30
43
|
* can be driven against `MockHal`: which marks upload, which draw off another's
|
|
@@ -36,9 +49,8 @@ export declare class GpuMarkBackend<TRegion, TState extends FrameDimensions> ext
|
|
|
36
49
|
protected regionPasses: InstancePass<TRegion>[];
|
|
37
50
|
constructor(hal: GpuHal, marks: readonly Mark<TRegion, TState>[], clear?: ((state: TState) => ClearColor) | undefined);
|
|
38
51
|
protected clearColor(state: TState): ClearColor;
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
private bindRamp;
|
|
52
|
+
private boundTextures;
|
|
53
|
+
private bindTexture;
|
|
42
54
|
protected drawRegion(block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState): void;
|
|
43
55
|
}
|
|
44
56
|
/**
|
package/esm/marks/markBackend.js
CHANGED
|
@@ -3,17 +3,17 @@ import { createRenderingBackend } from '../createRenderingBackend.js';
|
|
|
3
3
|
import { uploadPass } from '../instancePass.js';
|
|
4
4
|
import { Canvas2DPerRegionRenderingBackend, GpuPerRegionRenderingBackend, } from '../perRegionRenderingBackend.js';
|
|
5
5
|
import { paintMarkBlocks } from './markPaint.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
// The passes a mark list owns a buffer for: the whole list minus those drawing
|
|
7
|
+
// off another's (`bufferOf`). Uploading to a borrowed pass is silent and its
|
|
8
|
+
// buffer is the lender's, so the skip is stated once, here — module-local,
|
|
9
|
+
// because `marks/backend` is a published subpath and nothing outside wants it.
|
|
10
|
+
function ownedPasses(marks) {
|
|
11
|
+
return marks.filter(m => !m.bufferOf).map(m => m.pass);
|
|
12
|
+
}
|
|
13
|
+
/** Pack and upload one region's buffers for a mark list. */
|
|
12
14
|
export function uploadMarks(hal, regionKey, marks, data) {
|
|
13
|
-
for (const
|
|
14
|
-
|
|
15
|
-
uploadPass(hal, regionKey, mark.pass, data);
|
|
16
|
-
}
|
|
15
|
+
for (const pass of ownedPasses(marks)) {
|
|
16
|
+
uploadPass(hal, regionKey, pass, data);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
@@ -25,6 +25,12 @@ export function uploadMarks(hal, regionKey, marks, data) {
|
|
|
25
25
|
* coverage strip inside the column it hands over. `GpuPerRegionRenderingBackend`
|
|
26
26
|
* sets both per block already, so for a backend on that scaffold the viewport
|
|
27
27
|
* set here is the same rect twice.
|
|
28
|
+
*
|
|
29
|
+
* A caller that narrows it must hand over marks declaring no `band`, and that
|
|
30
|
+
* is the one way the two clips do not compose: a banded mark scissors to its
|
|
31
|
+
* strip and hands the BLOCK COLUMN back rather than what the caller had. The
|
|
32
|
+
* split is deliberate — `render-core/CLAUDE.md` §Drawing has which displays
|
|
33
|
+
* take which side — but nothing enforces it, so it is stated at both.
|
|
28
34
|
*/
|
|
29
35
|
export function drawMarks(hal, scratch, marks, block, clip, region, state, regionKey) {
|
|
30
36
|
hal.setViewport(clip.pxX, 0, clip.pxW, clip.pxH);
|
|
@@ -34,9 +40,28 @@ export function drawMarks(hal, scratch, marks, block, clip, region, state, regio
|
|
|
34
40
|
mark.drawRegion(hal, scratch, block, clip, region, state, regionKey, staged);
|
|
35
41
|
}
|
|
36
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Draw one block's planned passes off the uniforms the caller has already
|
|
45
|
+
* staged for it. No gate, no lens, no write: `planMarks` settled the frame
|
|
46
|
+
* question and the caller's own `hal.writeUniforms` settled the block's, so
|
|
47
|
+
* what is left per mark is the `drawPass` — the loop a display with its own
|
|
48
|
+
* frame scaffold was already running over a hand-kept pass list.
|
|
49
|
+
*
|
|
50
|
+
* Viewport and scissor are the caller's, as with `drawMarks`' scissor: a
|
|
51
|
+
* plan's marks declare no `band`, so nothing here narrows or restores either.
|
|
52
|
+
*/
|
|
53
|
+
export function drawPlannedPasses(hal, plan, regionKey) {
|
|
54
|
+
const { passes } = plan;
|
|
55
|
+
for (let i = 0; i < passes.length; i++) {
|
|
56
|
+
const pass = passes[i];
|
|
57
|
+
hal.drawPass(pass.id, regionKey, pass.bufferOf);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
37
60
|
// What a textured pass binds while its mark names no ramp: a shader declares
|
|
38
61
|
// its sampler unconditionally, and a textured pass with no texture never draws
|
|
39
|
-
// on the WebGPU HAL.
|
|
62
|
+
// on the WebGPU HAL. A mark declaring no `texture` at all is the same case —
|
|
63
|
+
// `pointMark` carries a sampler for the shapes that ramp, and Manhattan draws
|
|
64
|
+
// it without one.
|
|
40
65
|
const INERT_RAMP = new Uint8Array(COLOR_RAMP_LUT_ENTRIES * 4);
|
|
41
66
|
/**
|
|
42
67
|
* The HAL-side half of `createMarkBackend`, exported so a display's mark list
|
|
@@ -46,37 +71,46 @@ const INERT_RAMP = new Uint8Array(COLOR_RAMP_LUT_ENTRIES * 4);
|
|
|
46
71
|
export class GpuMarkBackend extends GpuPerRegionRenderingBackend {
|
|
47
72
|
marks;
|
|
48
73
|
clear;
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
regionPasses
|
|
74
|
+
// The base's upload loop already IS the mark walk, so this backend names its
|
|
75
|
+
// passes instead of overriding `upload` with a second spelling of it.
|
|
76
|
+
regionPasses;
|
|
52
77
|
constructor(hal, marks, clear) {
|
|
53
78
|
super(hal);
|
|
54
79
|
this.marks = marks;
|
|
55
80
|
this.clear = clear;
|
|
81
|
+
this.regionPasses = ownedPasses(marks);
|
|
56
82
|
}
|
|
57
83
|
clearColor(state) {
|
|
58
84
|
return this.clear ? this.clear(state) : super.clearColor(state);
|
|
59
85
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
// the
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
// The texture each textured pass holds, by identity — the mirror of the one
|
|
87
|
+
// texture the HAL keeps per pass, so an unchanged ramp or strip costs a
|
|
88
|
+
// frame nothing and a backend rebuilt after context loss re-uploads. Per
|
|
89
|
+
// pass and per backend, never per region. A lens answering nothing for a
|
|
90
|
+
// region leaves the pass's texture where it is, and binds the inert table
|
|
91
|
+
// only while the pass has none at all: a ring canvas draws one region per
|
|
92
|
+
// pass, and a lens with no opinion on the other regions must not swap the
|
|
93
|
+
// strip out and back on every frame.
|
|
94
|
+
boundTextures = new Map();
|
|
95
|
+
bindTexture(mark, region, state) {
|
|
96
|
+
if (mark.pass.textures) {
|
|
97
|
+
const bound = this.boundTextures.get(mark.pass.id);
|
|
98
|
+
const texture = mark.texture?.(state, region) ?? bound ?? INERT_RAMP;
|
|
99
|
+
if (texture !== bound) {
|
|
100
|
+
if (texture instanceof Uint8Array) {
|
|
101
|
+
uploadColorRampLut(this.hal, texture, [mark.pass.id]);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const { image, width, height } = texture;
|
|
105
|
+
this.hal.uploadTexture(mark.pass.id, image, width, height);
|
|
106
|
+
}
|
|
107
|
+
this.boundTextures.set(mark.pass.id, texture);
|
|
74
108
|
}
|
|
75
109
|
}
|
|
76
110
|
}
|
|
77
111
|
drawRegion(block, clip, region, state) {
|
|
78
112
|
for (const mark of this.marks) {
|
|
79
|
-
this.
|
|
113
|
+
this.bindTexture(mark, region, state);
|
|
80
114
|
}
|
|
81
115
|
drawMarks(this.hal, this.uniformData, this.marks, block, clip, region, state, block.displayedRegionIndex);
|
|
82
116
|
}
|