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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/esm/RenderLifecycleMixin.d.ts +25 -6
  2. package/esm/RenderLifecycleMixin.js +30 -7
  3. package/esm/canvas2dUtils.d.ts +25 -0
  4. package/esm/canvas2dUtils.js +27 -0
  5. package/esm/colorRampLut.d.ts +4 -4
  6. package/esm/colorRampLut.js +4 -4
  7. package/esm/computePipeline.d.ts +13 -0
  8. package/esm/computePipeline.js +55 -0
  9. package/esm/contractReports.d.ts +54 -0
  10. package/esm/contractReports.js +112 -0
  11. package/esm/coverageBand.d.ts +0 -1
  12. package/esm/coverageBand.js +0 -1
  13. package/esm/coverageBandBuffers.d.ts +0 -6
  14. package/esm/coverageBandBuffers.js +1 -13
  15. package/esm/createRenderingBackend.d.ts +14 -18
  16. package/esm/createRenderingBackend.js +4 -2
  17. package/esm/displayPhase.d.ts +10 -1
  18. package/esm/displayPhase.js +2 -1
  19. package/esm/encodeMemo.d.ts +14 -0
  20. package/esm/encodeMemo.js +48 -0
  21. package/esm/gpuDevice.d.ts +9 -0
  22. package/esm/gpuDevice.js +12 -3
  23. package/esm/graphicsCapabilities.d.ts +18 -15
  24. package/esm/graphicsCapabilities.js +23 -16
  25. package/esm/hal/createHal.d.ts +1 -2
  26. package/esm/hal/createHal.js +14 -4
  27. package/esm/hal/deviceGpuCache.js +2 -2
  28. package/esm/hal/gpuHalBase.d.ts +1 -1
  29. package/esm/hal/gpuHalBase.js +2 -3
  30. package/esm/hal/mockHal.d.ts +0 -1
  31. package/esm/hal/mockHal.js +0 -10
  32. package/esm/hal/regionRegistry.d.ts +0 -1
  33. package/esm/hal/regionRegistry.js +3 -15
  34. package/esm/hal/types.d.ts +5 -1
  35. package/esm/hal/webgl2Hal.d.ts +1 -1
  36. package/esm/hal/webgl2Hal.js +2 -2
  37. package/esm/hal/webgpuHal.d.ts +2 -2
  38. package/esm/hal/webgpuHal.js +13 -10
  39. package/esm/installUpload.d.ts +4 -1
  40. package/esm/installUpload.js +10 -41
  41. package/esm/instanceCache.d.ts +10 -7
  42. package/esm/instanceCache.js +14 -15
  43. package/esm/mapUploadSync.d.ts +0 -8
  44. package/esm/mapUploadSync.js +0 -1
  45. package/esm/marks/colorFill.d.ts +17 -0
  46. package/esm/marks/colorFill.js +26 -0
  47. package/esm/marks/drawAgainstHit.d.ts +47 -0
  48. package/esm/marks/drawAgainstHit.js +140 -0
  49. package/esm/marks/glyphPaint.d.ts +16 -0
  50. package/esm/marks/glyphPaint.js +41 -0
  51. package/esm/marks/index.d.ts +9 -0
  52. package/esm/marks/index.js +6 -0
  53. package/esm/marks/markBackend.d.ts +87 -0
  54. package/esm/marks/markBackend.js +133 -0
  55. package/esm/marks/markPaint.d.ts +13 -0
  56. package/esm/marks/markPaint.js +18 -0
  57. package/esm/marks/pointMark.d.ts +24 -0
  58. package/esm/marks/pointMark.js +98 -0
  59. package/esm/marks/spanMark.d.ts +45 -0
  60. package/esm/marks/spanMark.js +71 -0
  61. package/esm/marks/types.d.ts +206 -0
  62. package/esm/marks/types.js +125 -0
  63. package/esm/perRegionRenderingBackend.d.ts +15 -3
  64. package/esm/perRegionRenderingBackend.js +26 -3
  65. package/esm/regionDataMap.d.ts +2 -2
  66. package/esm/regionDataMap.js +25 -23
  67. package/esm/renderingBackendBase.d.ts +11 -14
  68. package/esm/renderingBackendBase.js +7 -7
  69. package/esm/shaders/capsule.js.generated.d.ts +1 -0
  70. package/esm/shaders/capsule.js.generated.js +11 -0
  71. package/esm/shaders/coverageBar.generated.d.ts +2 -2
  72. package/esm/shaders/coverageBar.generated.js +2 -2
  73. package/esm/shaders/coverageMod.generated.d.ts +2 -2
  74. package/esm/shaders/coverageMod.generated.js +2 -2
  75. package/esm/shaders/coverageSnp.generated.d.ts +2 -2
  76. package/esm/shaders/coverageSnp.generated.js +2 -2
  77. package/esm/shaders/hpmath.js.generated.d.ts +1 -0
  78. package/esm/shaders/hpmath.js.generated.js +4 -1
  79. package/esm/shaders/pointMark.consts.generated.d.ts +4 -0
  80. package/esm/shaders/pointMark.consts.generated.js +6 -0
  81. package/esm/shaders/pointMark.generated.d.ts +5 -0
  82. package/esm/shaders/pointMark.generated.js +7 -0
  83. package/esm/shaders/pointMark.iface.generated.d.ts +55 -0
  84. package/esm/shaders/pointMark.iface.generated.js +100 -0
  85. package/esm/shaders/pointMark.js.generated.d.ts +1 -0
  86. package/esm/shaders/pointMark.js.generated.js +16 -0
  87. package/esm/shaders/rowRect.generated.d.ts +0 -1
  88. package/esm/shaders/rowRect.generated.js +0 -1
  89. package/esm/shaders/spanMark.generated.d.ts +4 -0
  90. package/esm/shaders/spanMark.generated.js +6 -0
  91. package/esm/shaders/spanMark.iface.generated.d.ts +58 -0
  92. package/esm/shaders/spanMark.iface.generated.js +122 -0
  93. package/esm/sharedBackendKey.d.ts +18 -0
  94. package/esm/{keyedRenderingBackend.js → sharedBackendKey.js} +5 -0
  95. package/esm/slangPass.d.ts +2 -0
  96. package/esm/slangPass.js +2 -0
  97. package/package.json +14 -4
  98. package/src/shaders/capsule.js.generated.ts +14 -0
  99. package/src/shaders/capsule.slang +12 -4
  100. package/src/shaders/colorPack.slang +5 -1
  101. package/src/shaders/coverageBand.slang +38 -15
  102. package/src/shaders/coverageBar.generated.ts +2 -2
  103. package/src/shaders/coverageBar.slang +9 -0
  104. package/src/shaders/coverageMod.generated.ts +2 -2
  105. package/src/shaders/coverageSnp.generated.ts +2 -2
  106. package/src/shaders/hpmath.js.generated.ts +5 -1
  107. package/src/shaders/hpmath.slang +29 -10
  108. package/src/shaders/pointMark.consts.generated.ts +10 -0
  109. package/src/shaders/pointMark.generated.ts +12 -0
  110. package/src/shaders/pointMark.iface.generated.ts +146 -0
  111. package/src/shaders/pointMark.js.generated.ts +21 -0
  112. package/src/shaders/pointMark.slang +192 -0
  113. package/src/shaders/rowRect.generated.ts +0 -2
  114. package/src/shaders/rowRect.slang +33 -44
  115. package/src/shaders/spanMark.generated.ts +10 -0
  116. package/src/shaders/spanMark.iface.generated.ts +168 -0
  117. package/src/shaders/spanMark.slang +22 -0
  118. package/esm/globalRenderingBackend.d.ts +0 -90
  119. package/esm/globalRenderingBackend.js +0 -53
  120. package/esm/keyedRenderingBackend.d.ts +0 -41
@@ -14,9 +14,8 @@ export interface InstanceCacheOpts<TData> {
14
14
  colorOffsetWords: number;
15
15
  }
16
16
  /**
17
- * A per-key cache of packed instance bytes with a geometry/color split: a
18
- * recolor patches the one color lane in place instead of re-packing every
19
- * other one.
17
+ * A cache of packed instance bytes with a geometry/color split: a recolor
18
+ * patches the one color lane in place instead of re-packing every other one.
20
19
  *
21
20
  * The split is worth having because the two inputs move on completely
22
21
  * different schedules — geometry on an RPC refetch, colors on a `colorBy`
@@ -32,12 +31,16 @@ export interface InstanceCacheOpts<TData> {
32
31
  * problem. `installUpload` exists for the same reason one
33
32
  * layer up — an upload memo is not a hand-rolled `let`.
34
33
  *
34
+ * **The geometry token is the key**, so nothing has to evict: a payload whose
35
+ * coordinates were replaced takes its bytes with it, and a cache shared by
36
+ * several displays cannot hand one display another's buffer — two displays
37
+ * never hold one coordinate array. That is also what lets a mark's `pack`
38
+ * hold one of these, a mark list being module-level and shared.
39
+ *
35
40
  * The GPU re-upload still happens on a recolor (no HAL does partial buffer
36
41
  * updates); what is skipped is the dominant CPU interleave.
37
42
  */
38
43
  export declare function createInstanceCache<TData>(opts: InstanceCacheOpts<TData>): {
39
- /** Packed bytes for `key`, re-packing or re-coloring only as needed. */
40
- get(key: number, data: TData): ArrayBuffer;
41
- delete(key: number): void;
42
- clear(): void;
44
+ /** Packed bytes for `data`, re-packing or re-coloring only as needed. */
45
+ get(data: TData): ArrayBuffer;
43
46
  };
@@ -1,7 +1,6 @@
1
1
  /**
2
- * A per-key cache of packed instance bytes with a geometry/color split: a
3
- * recolor patches the one color lane in place instead of re-packing every
4
- * other one.
2
+ * A cache of packed instance bytes with a geometry/color split: a recolor
3
+ * patches the one color lane in place instead of re-packing every other one.
5
4
  *
6
5
  * The split is worth having because the two inputs move on completely
7
6
  * different schedules — geometry on an RPC refetch, colors on a `colorBy`
@@ -17,19 +16,25 @@
17
16
  * problem. `installUpload` exists for the same reason one
18
17
  * layer up — an upload memo is not a hand-rolled `let`.
19
18
  *
19
+ * **The geometry token is the key**, so nothing has to evict: a payload whose
20
+ * coordinates were replaced takes its bytes with it, and a cache shared by
21
+ * several displays cannot hand one display another's buffer — two displays
22
+ * never hold one coordinate array. That is also what lets a mark's `pack`
23
+ * hold one of these, a mark list being module-level and shared.
24
+ *
20
25
  * The GPU re-upload still happens on a recolor (no HAL does partial buffer
21
26
  * updates); what is skipped is the dominant CPU interleave.
22
27
  */
23
28
  export function createInstanceCache(opts) {
24
29
  const { geomToken, colors, interleave, strideWords, colorOffsetWords } = opts;
25
- const cache = new Map();
30
+ const cache = new WeakMap();
26
31
  return {
27
- /** Packed bytes for `key`, re-packing or re-coloring only as needed. */
28
- get(key, data) {
32
+ /** Packed bytes for `data`, re-packing or re-coloring only as needed. */
33
+ get(data) {
29
34
  const geom = geomToken(data);
30
35
  const cols = colors(data);
31
- const hit = cache.get(key);
32
- if (hit?.geom === geom) {
36
+ const hit = cache.get(geom);
37
+ if (hit) {
33
38
  if (hit.colors !== cols) {
34
39
  const u32 = new Uint32Array(hit.buf);
35
40
  for (let i = 0, n = cols.length; i < n; i++) {
@@ -40,14 +45,8 @@ export function createInstanceCache(opts) {
40
45
  return hit.buf;
41
46
  }
42
47
  const buf = interleave(data);
43
- cache.set(key, { geom, colors: cols, buf });
48
+ cache.set(geom, { colors: cols, buf });
44
49
  return buf;
45
50
  },
46
- delete(key) {
47
- cache.delete(key);
48
- },
49
- clear() {
50
- cache.clear();
51
- },
52
51
  };
53
52
  }
@@ -1,14 +1,6 @@
1
1
  export interface MapUploadTarget<K, T, B> {
2
2
  upload: (backend: B, key: K, data: T) => void;
3
- /**
4
- * How a key that left the map is released. `remove` deletes it by itself;
5
- * `prune` is handed the keys still present and releases everything else.
6
- * `installUpload` takes the first, which is the only correct shape on a
7
- * canvas several displays share and does the same job on a display's own
8
- * map.
9
- */
10
3
  remove?: (backend: B, key: K) => void;
11
- prune?: (backend: B, active: ReadonlySet<K>) => void;
12
4
  }
13
5
  /**
14
6
  * The one reference diff every keyed GPU upload in the tree runs on. Uploads
@@ -39,7 +39,6 @@ export function createMapUploadSync(target) {
39
39
  changed = true;
40
40
  }
41
41
  }
42
- target.prune?.(backend, new Set(entries.keys()));
43
42
  return changed;
44
43
  };
45
44
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Packed ABGR → `rgba(...)`, and the run-tracking fill a painter wraps it in.
3
+ *
4
+ * `@jbrowse/core/util/colorBits` re-exports `abgrToCssRgba` from here: core
5
+ * depends on render-core, never the reverse. The direction that genuinely
6
+ * needs core is resolving a CSS colour to a packed number, which is why a
7
+ * shape takes packed colours and the display resolves them.
8
+ */
9
+ export declare function abgrToCssRgba(c: number): string;
10
+ /**
11
+ * A fill-style setter that only touches `ctx.fillStyle` when the colour
12
+ * changes. Most of a painting is runs of one colour, and both the string
13
+ * allocation and the context write cost more than the comparison.
14
+ */
15
+ export declare function makeAbgrFill(ctx: {
16
+ fillStyle: string | CanvasGradient | CanvasPattern;
17
+ }): (abgr: number) => void;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Packed ABGR → `rgba(...)`, and the run-tracking fill a painter wraps it in.
3
+ *
4
+ * `@jbrowse/core/util/colorBits` re-exports `abgrToCssRgba` from here: core
5
+ * depends on render-core, never the reverse. The direction that genuinely
6
+ * needs core is resolving a CSS colour to a packed number, which is why a
7
+ * shape takes packed colours and the display resolves them.
8
+ */
9
+ export function abgrToCssRgba(c) {
10
+ const a = ((c >>> 24) & 255) / 255;
11
+ return `rgba(${c & 255},${(c >>> 8) & 255},${(c >>> 16) & 255},${a})`;
12
+ }
13
+ /**
14
+ * A fill-style setter that only touches `ctx.fillStyle` when the colour
15
+ * changes. Most of a painting is runs of one colour, and both the string
16
+ * allocation and the context write cost more than the comparison.
17
+ */
18
+ export function makeAbgrFill(ctx) {
19
+ let last;
20
+ return (abgr) => {
21
+ if (abgr !== last) {
22
+ last = abgr;
23
+ ctx.fillStyle = abgrToCssRgba(abgr);
24
+ }
25
+ };
26
+ }
@@ -0,0 +1,47 @@
1
+ import type { RenderBlock } from '../renderBlock.ts';
2
+ import type { MarkContext2D, MarkFrame, MarkShape } from './types.ts';
3
+ export interface RecordedRect {
4
+ x: number;
5
+ y: number;
6
+ w: number;
7
+ h: number;
8
+ fillStyle: string;
9
+ }
10
+ /**
11
+ * A `MarkContext2D` that records every rect a painter fills, in paint order —
12
+ * `fillRect` and path `rect` alike, so a shape that batches by colour records
13
+ * the same list as one that fills per instance. A path traced with
14
+ * `moveTo`/`lineTo` and filled records its bounding box, which is what a
15
+ * shape whose hit test answers a polygon's box (the variant inversion
16
+ * triangle) is held to.
17
+ *
18
+ * A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
19
+ * edge's bounding box widened by half the current `lineWidth` — the extent a
20
+ * round cap reaches on all four sides. Per edge rather than per `stroke()`,
21
+ * because a painter that batches a colour run into one path (dotplot's) would
22
+ * otherwise record the run's hull as a single instance. `fillStyle` on such a
23
+ * record is the `strokeStyle` it went down in; the field is the colour of the
24
+ * ink either way.
25
+ */
26
+ export declare function recordingContext(): {
27
+ ctx: MarkContext2D;
28
+ calls: RecordedRect[];
29
+ };
30
+ /**
31
+ * The draw-against-hit gate for a shape whose instances paint as rects: paint
32
+ * the block, then walk it in `step` px and hold `hitNearest` to what the
33
+ * painter recorded. A hit at distance 0 has to sit on the rect of the index it
34
+ * names, and a point on a rect has to answer that rect — the last-painted one
35
+ * where rects overlap, since the sweep hands candidates in back to front.
36
+ *
37
+ * Returns the violations rather than asserting, so a test reads as
38
+ * `expect(sweep(...)).toEqual([])`. `maxDistSq` is the caller's, because a
39
+ * shape whose hit target is wider than its ink (`point`) needs a bound that
40
+ * admits the whole rect.
41
+ */
42
+ export declare function sweepDrawAgainstHit<C extends {
43
+ count: number;
44
+ }, P>(shape: MarkShape<C, P>, channels: C, block: RenderBlock, frame: MarkFrame, params: P, { maxDistSq, step }: {
45
+ maxDistSq: number;
46
+ step?: number;
47
+ }): string[];
@@ -0,0 +1,140 @@
1
+ /**
2
+ * A `MarkContext2D` that records every rect a painter fills, in paint order —
3
+ * `fillRect` and path `rect` alike, so a shape that batches by colour records
4
+ * the same list as one that fills per instance. A path traced with
5
+ * `moveTo`/`lineTo` and filled records its bounding box, which is what a
6
+ * shape whose hit test answers a polygon's box (the variant inversion
7
+ * triangle) is held to.
8
+ *
9
+ * A **stroked** path records one rect per `moveTo`/`lineTo` edge, each the
10
+ * edge's bounding box widened by half the current `lineWidth` — the extent a
11
+ * round cap reaches on all four sides. Per edge rather than per `stroke()`,
12
+ * because a painter that batches a colour run into one path (dotplot's) would
13
+ * otherwise record the run's hull as a single instance. `fillStyle` on such a
14
+ * record is the `strokeStyle` it went down in; the field is the colour of the
15
+ * ink either way.
16
+ */
17
+ export function recordingContext() {
18
+ const calls = [];
19
+ const points = [];
20
+ const edges = [];
21
+ const ctx = {
22
+ fillStyle: '',
23
+ strokeStyle: '',
24
+ lineWidth: 1,
25
+ lineCap: 'butt',
26
+ save() { },
27
+ restore() { },
28
+ beginPath() {
29
+ points.length = 0;
30
+ edges.length = 0;
31
+ },
32
+ clip() { },
33
+ moveTo(x, y) {
34
+ points.push([x, y]);
35
+ },
36
+ lineTo(x, y) {
37
+ const from = points.at(-1);
38
+ if (from) {
39
+ edges.push([from[0], from[1], x, y]);
40
+ }
41
+ points.push([x, y]);
42
+ },
43
+ arc() { },
44
+ closePath() { },
45
+ fill() {
46
+ if (points.length > 0) {
47
+ const xs = points.map(p => p[0]);
48
+ const ys = points.map(p => p[1]);
49
+ const x = Math.min(...xs);
50
+ const y = Math.min(...ys);
51
+ calls.push({
52
+ x,
53
+ y,
54
+ w: Math.max(...xs) - x,
55
+ h: Math.max(...ys) - y,
56
+ fillStyle: this.fillStyle,
57
+ });
58
+ points.length = 0;
59
+ edges.length = 0;
60
+ }
61
+ },
62
+ stroke() {
63
+ const half = this.lineWidth / 2;
64
+ for (const [ax, ay, bx, by] of edges) {
65
+ const x = Math.min(ax, bx) - half;
66
+ const y = Math.min(ay, by) - half;
67
+ calls.push({
68
+ x,
69
+ y,
70
+ w: Math.max(ax, bx) + half - x,
71
+ h: Math.max(ay, by) + half - y,
72
+ fillStyle: this.strokeStyle,
73
+ });
74
+ }
75
+ points.length = 0;
76
+ edges.length = 0;
77
+ },
78
+ rect(x, y, w, h) {
79
+ calls.push({ x, y, w, h, fillStyle: this.fillStyle });
80
+ },
81
+ fillRect(x, y, w, h) {
82
+ calls.push({ x, y, w, h, fillStyle: this.fillStyle });
83
+ },
84
+ };
85
+ return { ctx: ctx, calls };
86
+ }
87
+ function contains(r, x, y) {
88
+ return x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h;
89
+ }
90
+ /**
91
+ * The draw-against-hit gate for a shape whose instances paint as rects: paint
92
+ * the block, then walk it in `step` px and hold `hitNearest` to what the
93
+ * painter recorded. A hit at distance 0 has to sit on the rect of the index it
94
+ * names, and a point on a rect has to answer that rect — the last-painted one
95
+ * where rects overlap, since the sweep hands candidates in back to front.
96
+ *
97
+ * Returns the violations rather than asserting, so a test reads as
98
+ * `expect(sweep(...)).toEqual([])`. `maxDistSq` is the caller's, because a
99
+ * shape whose hit target is wider than its ink (`point`) needs a bound that
100
+ * admits the whole rect.
101
+ */
102
+ export function sweepDrawAgainstHit(shape, channels, block, frame, params, { maxDistSq, step = 0.5 }) {
103
+ const violations = [];
104
+ const { ctx, calls: rects } = recordingContext();
105
+ shape.paintBlock(ctx, channels, block, frame, params);
106
+ if (rects.length !== channels.count) {
107
+ return [`painted ${rects.length} rects for ${channels.count} instances`];
108
+ }
109
+ const candidates = Array.from({ length: channels.count }, (_, k) => channels.count - 1 - k);
110
+ let yMin = Infinity;
111
+ let yMax = -Infinity;
112
+ for (const r of rects) {
113
+ yMin = Math.min(yMin, r.y);
114
+ yMax = Math.max(yMax, r.y + r.h);
115
+ }
116
+ const x0 = Math.min(block.screenStartPx, block.screenEndPx) - 1;
117
+ const x1 = Math.max(block.screenStartPx, block.screenEndPx) + 1;
118
+ for (let y = Math.floor(yMin) - 1; y <= yMax + 1; y += step) {
119
+ for (let x = x0; x <= x1; x += step) {
120
+ let expected = -1;
121
+ for (let k = rects.length - 1; k >= 0; k--) {
122
+ if (contains(rects[k], x, y)) {
123
+ expected = k;
124
+ break;
125
+ }
126
+ }
127
+ const hit = shape.hitNearest(channels, block, frame, params, x, y, candidates, maxDistSq);
128
+ if (hit && hit.distSq === 0 && !contains(rects[hit.index], x, y)) {
129
+ violations.push(`(${x}, ${y}) answered ${hit.index} at distance 0, off its rect`);
130
+ }
131
+ if (expected !== -1 && hit?.index !== expected) {
132
+ violations.push(`(${x}, ${y}) is on rect ${expected}, answered ${hit?.index ?? 'nothing'}`);
133
+ }
134
+ if (violations.length >= 10) {
135
+ return violations;
136
+ }
137
+ }
138
+ }
139
+ return violations;
140
+ }
@@ -0,0 +1,16 @@
1
+ import type { MarkContext2D } from './types.ts';
2
+ /**
3
+ * Append one of the `point` shape's glyphs to the current path, centred on
4
+ * `(cx, y)`.
5
+ *
6
+ * The disc/crisp-square split and the square's snap are pointGlyph.slang's
7
+ * (adr-051), so the painter cannot drift from the shader about where the split
8
+ * is. The caller owns `beginPath`/`fill`, which is what lets a run of one
9
+ * colour batch into a single fill.
10
+ *
11
+ * Its own module, not `pointMark.ts`'s: this is the half of the shape a
12
+ * consumer can want without the pass, and importing it through the shape would
13
+ * hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
14
+ * is that consumer.
15
+ */
16
+ export declare function appendGlyph(ctx: MarkContext2D, glyph: number, cx: number, y: number, diameter: number): void;
@@ -0,0 +1,41 @@
1
+ import { SMALL_POINT_MAX_DIAMETER } from '../shaders/pointGlyph.generated.js';
2
+ import { crispSquareTopLeftPx } from '../shaders/pointGlyph.js.generated.js';
3
+ import { DIAMOND_GLYPH_SCALE, GLYPH_DIAMOND, GLYPH_TRIANGLE, } from '../shaders/pointMark.consts.generated.js';
4
+ /**
5
+ * Append one of the `point` shape's glyphs to the current path, centred on
6
+ * `(cx, y)`.
7
+ *
8
+ * The disc/crisp-square split and the square's snap are pointGlyph.slang's
9
+ * (adr-051), so the painter cannot drift from the shader about where the split
10
+ * is. The caller owns `beginPath`/`fill`, which is what lets a run of one
11
+ * colour batch into a single fill.
12
+ *
13
+ * Its own module, not `pointMark.ts`'s: this is the half of the shape a
14
+ * consumer can want without the pass, and importing it through the shape would
15
+ * hand that consumer the shader's WGSL and GLSL strings. Wiggle's scatter mode
16
+ * is that consumer.
17
+ */
18
+ export function appendGlyph(ctx, glyph, cx, y, diameter) {
19
+ const r = diameter / 2;
20
+ if (glyph === GLYPH_TRIANGLE) {
21
+ ctx.moveTo(cx - r, y - r);
22
+ ctx.lineTo(cx + r, y - r);
23
+ ctx.lineTo(cx, y + r);
24
+ ctx.closePath();
25
+ }
26
+ else if (glyph === GLYPH_DIAMOND) {
27
+ const ri = r * DIAMOND_GLYPH_SCALE;
28
+ ctx.moveTo(cx, y - ri);
29
+ ctx.lineTo(cx + ri, y);
30
+ ctx.lineTo(cx, y + ri);
31
+ ctx.lineTo(cx - ri, y);
32
+ ctx.closePath();
33
+ }
34
+ else if (diameter <= SMALL_POINT_MAX_DIAMETER) {
35
+ ctx.rect(crispSquareTopLeftPx(cx, diameter), crispSquareTopLeftPx(y, diameter), diameter, diameter);
36
+ }
37
+ else {
38
+ ctx.moveTo(cx + r, y);
39
+ ctx.arc(cx, y, r, 0, Math.PI * 2);
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ export { defineMark } from './types.ts';
2
+ export { paintMarkBlocks } from './markPaint.ts';
3
+ export { pointMark } from './pointMark.ts';
4
+ export { spanMark } from './spanMark.ts';
5
+ export { appendGlyph } from './glyphPaint.ts';
6
+ export { abgrToCssRgba, makeAbgrFill } from './colorFill.ts';
7
+ export type { Mark, MarkBand, MarkContext2D, MarkFrame, MarkHit, MarkShape, StagedUniforms, } from './types.ts';
8
+ export type { PointChannels, PointParams } from './pointMark.ts';
9
+ export type { SpanChannels, SpanParams } from './spanMark.ts';
@@ -0,0 +1,6 @@
1
+ export { defineMark } from './types.js';
2
+ export { paintMarkBlocks } from './markPaint.js';
3
+ export { pointMark } from './pointMark.js';
4
+ export { spanMark } from './spanMark.js';
5
+ export { appendGlyph } from './glyphPaint.js';
6
+ export { abgrToCssRgba, makeAbgrFill } from './colorFill.js';
@@ -0,0 +1,87 @@
1
+ import { Canvas2DPerRegionRenderingBackend, GpuPerRegionRenderingBackend } from '../perRegionRenderingBackend.ts';
2
+ import type { BlockClipResult } from '../blockClipUtils.ts';
3
+ import type { GpuHal } from '../hal/index.ts';
4
+ import type { SampleCount } from '../hal/types.ts';
5
+ import type { InstancePass } from '../instancePass.ts';
6
+ import type { ClearColor, PerRegionRenderingBackend } from '../perRegionRenderingBackend.ts';
7
+ import type { RenderBlock } from '../renderBlock.ts';
8
+ import type { FrameDimensions } from '../renderingBackendBase.ts';
9
+ 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
+ */
16
+ export declare function uploadMarks<TRegion, TState extends FrameDimensions>(hal: GpuHal, regionKey: number, marks: readonly Mark<TRegion, TState>[], data: TRegion): void;
17
+ /**
18
+ * Draw one already-clipped block through a mark list.
19
+ *
20
+ * **The viewport is this helper's, the scissor is the caller's.** Every shape
21
+ * maps bp across the block column, so nobody wants a viewport narrower than the
22
+ * clip; a caller does want a narrower scissor, which is how alignments clips a
23
+ * coverage strip inside the column it hands over. `GpuPerRegionRenderingBackend`
24
+ * sets both per block already, so for a backend on that scaffold the viewport
25
+ * set here is the same rect twice.
26
+ */
27
+ 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;
28
+ /**
29
+ * The HAL-side half of `createMarkBackend`, exported so a display's mark list
30
+ * can be driven against `MockHal`: which marks upload, which draw off another's
31
+ * buffer, and which blocks a shape declines.
32
+ */
33
+ export declare class GpuMarkBackend<TRegion, TState extends FrameDimensions> extends GpuPerRegionRenderingBackend<TRegion, TState> {
34
+ private marks;
35
+ private clear?;
36
+ protected regionPasses: InstancePass<TRegion>[];
37
+ constructor(hal: GpuHal, marks: readonly Mark<TRegion, TState>[], clear?: ((state: TState) => ClearColor) | undefined);
38
+ protected clearColor(state: TState): ClearColor;
39
+ upload(regionKey: number, data: TRegion): void;
40
+ private boundRamps;
41
+ private bindRamp;
42
+ protected drawRegion(block: RenderBlock, clip: BlockClipResult, region: TRegion, state: TState): void;
43
+ }
44
+ /**
45
+ * The Canvas2D half, exported for the same reason `GpuMarkBackend` is: a
46
+ * display's frame scaffold — the ground it clears to, the blocks it paints — is
47
+ * a property of the pair, and a suite pinning it on one backend has to pin it
48
+ * on the other.
49
+ */
50
+ export declare class Canvas2DMarkBackend<TRegion, TState extends FrameDimensions> extends Canvas2DPerRegionRenderingBackend<TRegion, TState> {
51
+ private marks;
52
+ private clear?;
53
+ constructor(canvas: HTMLCanvasElement, marks: readonly Mark<TRegion, TState>[], clear?: ((state: TState) => ClearColor) | undefined);
54
+ protected clearColor(state: TState): ClearColor | undefined;
55
+ protected draw(blocks: RenderBlock[], regions: ReadonlyMap<number, TRegion>, state: TState): void;
56
+ }
57
+ /**
58
+ * Build a display's rendering backend from its mark declarations.
59
+ *
60
+ * **This is the only module on the mark path that reaches the HAL**, which is
61
+ * why it is not on the `marks` subpath: a display's declaration, its painter
62
+ * and its hit test are all things a state model can legitimately reach, and a
63
+ * state model is eager (ADR-091). Reaching the backend costs the GPU stack at
64
+ * plugin-install time, so it is a separate import a display makes once, from
65
+ * the factory the component hands to `DisplayChrome`.
66
+ *
67
+ * This is the whole of what a display used to spell as a `GpuXxxRenderer`
68
+ * class, a `Canvas2DXxxRenderer` class, a pass list and a factory: the passes
69
+ * are the marks' own and each backend walks the same list.
70
+ *
71
+ * One uniform buffer serves every mark because each writes its own layout into
72
+ * the scratch immediately before its own `drawPass`, which is what the HAL's
73
+ * write-then-draw ordering already guarantees for a multi-pass renderer — or
74
+ * draws off the struct the previous mark of the same block already staged, per
75
+ * `StagedUniforms`.
76
+ */
77
+ export declare function createMarkBackend<TRegion, TState extends FrameDimensions>(canvas: HTMLCanvasElement, marks: readonly Mark<TRegion, TState>[], opts?: {
78
+ sampleCount?: SampleCount;
79
+ /**
80
+ * What the frame is cleared to, when transparent is the wrong answer. The
81
+ * GPU side hands it to `beginFrame` and the Canvas2D side fills it after
82
+ * `prepareCanvas`, so both backends composite over the same colour — which
83
+ * synteny needs, its indel wedges being pre-blended against the band's
84
+ * ground rather than composited over it.
85
+ */
86
+ clearColor?: (state: TState) => ClearColor;
87
+ }): Promise<PerRegionRenderingBackend<TRegion, TState>>;
@@ -0,0 +1,133 @@
1
+ import { COLOR_RAMP_LUT_ENTRIES, uploadColorRampLut } from '../colorRampLut.js';
2
+ import { createRenderingBackend } from '../createRenderingBackend.js';
3
+ import { uploadPass } from '../instancePass.js';
4
+ import { Canvas2DPerRegionRenderingBackend, GpuPerRegionRenderingBackend, } from '../perRegionRenderingBackend.js';
5
+ import { paintMarkBlocks } from './markPaint.js';
6
+ /**
7
+ * Pack and upload one region's buffers for a mark list — every mark that owns
8
+ * one, which is the whole list minus those drawing off another's (`bufferOf`).
9
+ * That skip is the reason a display's upload is this call rather than a loop:
10
+ * uploading to a borrowed pass is silent, and its buffer is the lender's.
11
+ */
12
+ export function uploadMarks(hal, regionKey, marks, data) {
13
+ for (const mark of marks) {
14
+ if (!mark.bufferOf) {
15
+ uploadPass(hal, regionKey, mark.pass, data);
16
+ }
17
+ }
18
+ }
19
+ /**
20
+ * Draw one already-clipped block through a mark list.
21
+ *
22
+ * **The viewport is this helper's, the scissor is the caller's.** Every shape
23
+ * maps bp across the block column, so nobody wants a viewport narrower than the
24
+ * clip; a caller does want a narrower scissor, which is how alignments clips a
25
+ * coverage strip inside the column it hands over. `GpuPerRegionRenderingBackend`
26
+ * sets both per block already, so for a backend on that scaffold the viewport
27
+ * set here is the same rect twice.
28
+ */
29
+ export function drawMarks(hal, scratch, marks, block, clip, region, state, regionKey) {
30
+ hal.setViewport(clip.pxX, 0, clip.pxW, clip.pxH);
31
+ // One call is one block, which is exactly the life of a `StagedUniforms`
32
+ const staged = { writer: undefined, params: undefined };
33
+ for (const mark of marks) {
34
+ mark.drawRegion(hal, scratch, block, clip, region, state, regionKey, staged);
35
+ }
36
+ }
37
+ // What a textured pass binds while its mark names no ramp: a shader declares
38
+ // its sampler unconditionally, and a textured pass with no texture never draws
39
+ // on the WebGPU HAL.
40
+ const INERT_RAMP = new Uint8Array(COLOR_RAMP_LUT_ENTRIES * 4);
41
+ /**
42
+ * The HAL-side half of `createMarkBackend`, exported so a display's mark list
43
+ * can be driven against `MockHal`: which marks upload, which draw off another's
44
+ * buffer, and which blocks a shape declines.
45
+ */
46
+ export class GpuMarkBackend extends GpuPerRegionRenderingBackend {
47
+ marks;
48
+ clear;
49
+ // `upload` runs `uploadMarks` in place of the base's loop, so nothing reads
50
+ // this — the `bufferOf` skip is stated once, there
51
+ regionPasses = [];
52
+ constructor(hal, marks, clear) {
53
+ super(hal);
54
+ this.marks = marks;
55
+ this.clear = clear;
56
+ }
57
+ clearColor(state) {
58
+ return this.clear ? this.clear(state) : super.clearColor(state);
59
+ }
60
+ upload(regionKey, data) {
61
+ uploadMarks(this.hal, regionKey, this.marks, data);
62
+ }
63
+ // The ramp each textured pass holds, by the table's identity — the mirror of
64
+ // the one texture the HAL keeps per pass, so an unchanged ramp costs a frame
65
+ // nothing and a backend rebuilt after context loss re-uploads. Per pass and
66
+ // per backend, never per region.
67
+ boundRamps = new Map();
68
+ bindRamp(mark, region, state) {
69
+ if (mark.texture) {
70
+ const ramp = mark.texture(state, region) ?? INERT_RAMP;
71
+ if (ramp !== this.boundRamps.get(mark.pass.id)) {
72
+ uploadColorRampLut(this.hal, ramp, [mark.pass.id]);
73
+ this.boundRamps.set(mark.pass.id, ramp);
74
+ }
75
+ }
76
+ }
77
+ drawRegion(block, clip, region, state) {
78
+ for (const mark of this.marks) {
79
+ this.bindRamp(mark, region, state);
80
+ }
81
+ drawMarks(this.hal, this.uniformData, this.marks, block, clip, region, state, block.displayedRegionIndex);
82
+ }
83
+ }
84
+ /**
85
+ * The Canvas2D half, exported for the same reason `GpuMarkBackend` is: a
86
+ * display's frame scaffold — the ground it clears to, the blocks it paints — is
87
+ * a property of the pair, and a suite pinning it on one backend has to pin it
88
+ * on the other.
89
+ */
90
+ export class Canvas2DMarkBackend extends Canvas2DPerRegionRenderingBackend {
91
+ marks;
92
+ clear;
93
+ constructor(canvas, marks, clear) {
94
+ super(canvas);
95
+ this.marks = marks;
96
+ this.clear = clear;
97
+ }
98
+ clearColor(state) {
99
+ return this.clear ? this.clear(state) : super.clearColor(state);
100
+ }
101
+ draw(blocks, regions, state) {
102
+ paintMarkBlocks(this.ctx, this.marks, regions, blocks, state);
103
+ }
104
+ }
105
+ /**
106
+ * Build a display's rendering backend from its mark declarations.
107
+ *
108
+ * **This is the only module on the mark path that reaches the HAL**, which is
109
+ * why it is not on the `marks` subpath: a display's declaration, its painter
110
+ * and its hit test are all things a state model can legitimately reach, and a
111
+ * state model is eager (ADR-091). Reaching the backend costs the GPU stack at
112
+ * plugin-install time, so it is a separate import a display makes once, from
113
+ * the factory the component hands to `DisplayChrome`.
114
+ *
115
+ * This is the whole of what a display used to spell as a `GpuXxxRenderer`
116
+ * class, a `Canvas2DXxxRenderer` class, a pass list and a factory: the passes
117
+ * are the marks' own and each backend walks the same list.
118
+ *
119
+ * One uniform buffer serves every mark because each writes its own layout into
120
+ * the scratch immediately before its own `drawPass`, which is what the HAL's
121
+ * write-then-draw ordering already guarantees for a multi-pass renderer — or
122
+ * draws off the struct the previous mark of the same block already staged, per
123
+ * `StagedUniforms`.
124
+ */
125
+ export function createMarkBackend(canvas, marks, opts = {}) {
126
+ const { clearColor } = opts;
127
+ return createRenderingBackend(canvas, {
128
+ passes: marks.map(m => m.pass),
129
+ sampleCount: opts.sampleCount,
130
+ createGpuBackend: hal => new GpuMarkBackend(hal, marks, clearColor),
131
+ createCanvas2DBackend: c => new Canvas2DMarkBackend(c, marks, clearColor),
132
+ });
133
+ }
@@ -0,0 +1,13 @@
1
+ import type { RenderBlock } from '../renderBlock.ts';
2
+ import type { Mark, MarkContext2D, MarkFrame } from './types.ts';
3
+ /**
4
+ * Paint one frame's blocks with a mark list — the Canvas2D half of a
5
+ * declaration, and the SVG export path, since `MarkContext2D` is satisfied by
6
+ * both a real 2D context and the SVG one.
7
+ *
8
+ * Its own module, away from the backend that calls it: an SVG export runs with
9
+ * no backend attached at all (and must, or a headless export would depend on a
10
+ * GPU device having been probed), so the path that draws must not pull the HAL
11
+ * in behind it.
12
+ */
13
+ export declare function paintMarkBlocks<TRegion, TState extends MarkFrame>(ctx: MarkContext2D, marks: readonly Mark<TRegion, TState>[], regions: ReadonlyMap<number, TRegion>, blocks: RenderBlock[], state: TState): void;