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