@jbrowse/render-core 4.3.0

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 (164) hide show
  1. package/LICENSE +201 -0
  2. package/esm/OverlayCanvas.d.ts +29 -0
  3. package/esm/OverlayCanvas.js +43 -0
  4. package/esm/RenderCanvas.d.ts +76 -0
  5. package/esm/RenderCanvas.js +38 -0
  6. package/esm/RenderLifecycleMixin.d.ts +180 -0
  7. package/esm/RenderLifecycleMixin.js +279 -0
  8. package/esm/ScrollLockedOverlay.d.ts +28 -0
  9. package/esm/ScrollLockedOverlay.js +42 -0
  10. package/esm/blockClipUtils.d.ts +27 -0
  11. package/esm/blockClipUtils.js +101 -0
  12. package/esm/canvas2dUtils.d.ts +372 -0
  13. package/esm/canvas2dUtils.js +564 -0
  14. package/esm/canvasContext.d.ts +68 -0
  15. package/esm/canvasContext.js +74 -0
  16. package/esm/colorRampLut.d.ts +17 -0
  17. package/esm/colorRampLut.js +25 -0
  18. package/esm/coverageBand.d.ts +107 -0
  19. package/esm/coverageBand.js +109 -0
  20. package/esm/coverageBandBuffers.d.ts +30 -0
  21. package/esm/coverageBandBuffers.js +13 -0
  22. package/esm/createRenderingBackend.d.ts +52 -0
  23. package/esm/createRenderingBackend.js +58 -0
  24. package/esm/displayPhase.d.ts +155 -0
  25. package/esm/displayPhase.js +97 -0
  26. package/esm/globalRenderingBackend.d.ts +90 -0
  27. package/esm/globalRenderingBackend.js +53 -0
  28. package/esm/gpuDevice.d.ts +91 -0
  29. package/esm/gpuDevice.js +277 -0
  30. package/esm/graphicsCapabilities.d.ts +98 -0
  31. package/esm/graphicsCapabilities.js +165 -0
  32. package/esm/hal/createHal.d.ts +22 -0
  33. package/esm/hal/createHal.js +91 -0
  34. package/esm/hal/deviceGpuCache.d.ts +36 -0
  35. package/esm/hal/deviceGpuCache.js +120 -0
  36. package/esm/hal/gpuHalBase.d.ts +55 -0
  37. package/esm/hal/gpuHalBase.js +81 -0
  38. package/esm/hal/index.d.ts +7 -0
  39. package/esm/hal/index.js +5 -0
  40. package/esm/hal/mockHal.d.ts +153 -0
  41. package/esm/hal/mockHal.js +297 -0
  42. package/esm/hal/oomReporter.d.ts +15 -0
  43. package/esm/hal/oomReporter.js +22 -0
  44. package/esm/hal/passIds.d.ts +22 -0
  45. package/esm/hal/passIds.js +36 -0
  46. package/esm/hal/regionRegistry.d.ts +11 -0
  47. package/esm/hal/regionRegistry.js +66 -0
  48. package/esm/hal/types.d.ts +116 -0
  49. package/esm/hal/types.js +1 -0
  50. package/esm/hal/webgl2Hal.d.ts +47 -0
  51. package/esm/hal/webgl2Hal.js +490 -0
  52. package/esm/hal/webgpuHal.d.ts +169 -0
  53. package/esm/hal/webgpuHal.js +722 -0
  54. package/esm/installUpload.d.ts +91 -0
  55. package/esm/installUpload.js +94 -0
  56. package/esm/instanceCache.d.ts +43 -0
  57. package/esm/instanceCache.js +53 -0
  58. package/esm/instancePass.d.ts +50 -0
  59. package/esm/instancePass.js +34 -0
  60. package/esm/keyedRenderingBackend.d.ts +41 -0
  61. package/esm/keyedRenderingBackend.js +19 -0
  62. package/esm/mapUploadSync.d.ts +31 -0
  63. package/esm/mapUploadSync.js +45 -0
  64. package/esm/namedReactions.d.ts +29 -0
  65. package/esm/namedReactions.js +62 -0
  66. package/esm/perRegionRenderingBackend.d.ts +114 -0
  67. package/esm/perRegionRenderingBackend.js +93 -0
  68. package/esm/recoveryBudget.d.ts +40 -0
  69. package/esm/recoveryBudget.js +56 -0
  70. package/esm/regionDataMap.d.ts +60 -0
  71. package/esm/regionDataMap.js +98 -0
  72. package/esm/renderBlock.d.ts +25 -0
  73. package/esm/renderBlock.js +10 -0
  74. package/esm/renderingBackendBase.d.ts +71 -0
  75. package/esm/renderingBackendBase.js +50 -0
  76. package/esm/shaders/antialias.js.generated.d.ts +4 -0
  77. package/esm/shaders/antialias.js.generated.js +25 -0
  78. package/esm/shaders/capsule.generated.d.ts +1 -0
  79. package/esm/shaders/capsule.generated.js +3 -0
  80. package/esm/shaders/colorRampLut.generated.d.ts +1 -0
  81. package/esm/shaders/colorRampLut.generated.js +3 -0
  82. package/esm/shaders/coverageBand.generated.d.ts +2 -0
  83. package/esm/shaders/coverageBand.generated.js +4 -0
  84. package/esm/shaders/coverageBar.generated.d.ts +4 -0
  85. package/esm/shaders/coverageBar.generated.js +6 -0
  86. package/esm/shaders/coverageBar.iface.generated.d.ts +85 -0
  87. package/esm/shaders/coverageBar.iface.generated.js +114 -0
  88. package/esm/shaders/coverageIndicator.generated.d.ts +4 -0
  89. package/esm/shaders/coverageIndicator.generated.js +6 -0
  90. package/esm/shaders/coverageIndicator.iface.generated.d.ts +85 -0
  91. package/esm/shaders/coverageIndicator.iface.generated.js +114 -0
  92. package/esm/shaders/coverageInterbase.generated.d.ts +4 -0
  93. package/esm/shaders/coverageInterbase.generated.js +6 -0
  94. package/esm/shaders/coverageInterbase.iface.generated.d.ts +93 -0
  95. package/esm/shaders/coverageInterbase.iface.generated.js +134 -0
  96. package/esm/shaders/coverageMod.generated.d.ts +4 -0
  97. package/esm/shaders/coverageMod.generated.js +6 -0
  98. package/esm/shaders/coverageMod.iface.generated.d.ts +97 -0
  99. package/esm/shaders/coverageMod.iface.generated.js +144 -0
  100. package/esm/shaders/coverageSnp.generated.d.ts +4 -0
  101. package/esm/shaders/coverageSnp.generated.js +6 -0
  102. package/esm/shaders/coverageSnp.iface.generated.d.ts +97 -0
  103. package/esm/shaders/coverageSnp.iface.generated.js +144 -0
  104. package/esm/shaders/hpmath.js.generated.d.ts +3 -0
  105. package/esm/shaders/hpmath.js.generated.js +48 -0
  106. package/esm/shaders/pointGlyph.generated.d.ts +1 -0
  107. package/esm/shaders/pointGlyph.generated.js +3 -0
  108. package/esm/shaders/pointGlyph.js.generated.d.ts +1 -0
  109. package/esm/shaders/pointGlyph.js.generated.js +7 -0
  110. package/esm/shaders/rowRect.generated.d.ts +3 -0
  111. package/esm/shaders/rowRect.generated.js +5 -0
  112. package/esm/shaders/rowRect.js.generated.d.ts +2 -0
  113. package/esm/shaders/rowRect.js.generated.js +13 -0
  114. package/esm/shaders/scoreScale.js.generated.d.ts +1 -0
  115. package/esm/shaders/scoreScale.js.generated.js +46 -0
  116. package/esm/slangPass.d.ts +49 -0
  117. package/esm/slangPass.js +22 -0
  118. package/esm/useEventCallback.d.ts +1 -0
  119. package/esm/useEventCallback.js +19 -0
  120. package/esm/useGraphicsCapabilities.d.ts +17 -0
  121. package/esm/useGraphicsCapabilities.js +38 -0
  122. package/esm/useRenderingBackend.d.ts +83 -0
  123. package/esm/useRenderingBackend.js +422 -0
  124. package/esm/useTabVisibilityRerender.d.ts +13 -0
  125. package/esm/useTabVisibilityRerender.js +39 -0
  126. package/esm/webgpuUtils.d.ts +23 -0
  127. package/esm/webgpuUtils.js +76 -0
  128. package/package.json +91 -0
  129. package/src/shaders/antialias.js.generated.ts +33 -0
  130. package/src/shaders/antialias.slang +112 -0
  131. package/src/shaders/capsule.generated.ts +4 -0
  132. package/src/shaders/capsule.slang +62 -0
  133. package/src/shaders/colorPack.slang +26 -0
  134. package/src/shaders/colorRampLut.generated.ts +4 -0
  135. package/src/shaders/colorRampLut.slang +51 -0
  136. package/src/shaders/coverageBand.generated.ts +6 -0
  137. package/src/shaders/coverageBand.slang +276 -0
  138. package/src/shaders/coverageBar.generated.ts +10 -0
  139. package/src/shaders/coverageBar.iface.generated.ts +172 -0
  140. package/src/shaders/coverageBar.slang +37 -0
  141. package/src/shaders/coverageIndicator.generated.ts +10 -0
  142. package/src/shaders/coverageIndicator.iface.generated.ts +172 -0
  143. package/src/shaders/coverageIndicator.slang +61 -0
  144. package/src/shaders/coverageInterbase.generated.ts +10 -0
  145. package/src/shaders/coverageInterbase.iface.generated.ts +198 -0
  146. package/src/shaders/coverageInterbase.slang +63 -0
  147. package/src/shaders/coverageMod.generated.ts +10 -0
  148. package/src/shaders/coverageMod.iface.generated.ts +211 -0
  149. package/src/shaders/coverageMod.slang +36 -0
  150. package/src/shaders/coverageSnp.generated.ts +10 -0
  151. package/src/shaders/coverageSnp.iface.generated.ts +211 -0
  152. package/src/shaders/coverageSnp.slang +52 -0
  153. package/src/shaders/diagonalGrid.slang +25 -0
  154. package/src/shaders/glyphEdgeAlpha.test.ts +241 -0
  155. package/src/shaders/hpmath.js.generated.ts +47 -0
  156. package/src/shaders/hpmath.slang +226 -0
  157. package/src/shaders/pointGlyph.generated.ts +4 -0
  158. package/src/shaders/pointGlyph.js.generated.ts +9 -0
  159. package/src/shaders/pointGlyph.slang +93 -0
  160. package/src/shaders/rowRect.generated.ts +8 -0
  161. package/src/shaders/rowRect.js.generated.ts +17 -0
  162. package/src/shaders/rowRect.slang +160 -0
  163. package/src/shaders/scoreScale.js.generated.ts +51 -0
  164. package/src/shaders/scoreScale.slang +68 -0
@@ -0,0 +1,241 @@
1
+ // `glyphEdgeAlpha` (antialias.slang) sizes the antialiasing ramp on a glyph's
2
+ // edge from the SDF's own gradient, so that one ramp covers one output pixel for
3
+ // every shape the Manhattan and wiggle point modes draw. Two things have to hold
4
+ // for that, and neither is checked by the compiler:
5
+ //
6
+ // 1. The gradient really is what the reasoning assumes. Unit for the disc and
7
+ // for each of the triangle's three edges — but NOT for the diamond, whose
8
+ // `1 - (|x| + |y|)` is the L1 norm and therefore sqrt(2) times the true
9
+ // perpendicular distance to its edge. That sqrt(2) is exactly why the ramp
10
+ // is measured rather than assumed, and a shape added later with its own
11
+ // scaling would be silently mis-sized by any constant.
12
+ //
13
+ // 2. The ramp fits in the pad the quad leaves it. `discExpand` grows the quad
14
+ // by aaHalfPx(dpr) CSS px past the glyph edge; a ramp reaching further is
15
+ // clipped by the rasterizer and the fade ends on a hard edge. This is what
16
+ // the previous `fwidth`-as-half-width spelling got wrong, and it got it
17
+ // wrong worst on the diagonals, where fwidth overshoots most.
18
+ //
19
+ // The shader isn't unit-testable, so this models it. Same approach, and same
20
+ // reason, as LinearSyntenyDisplay/shaders/syntenyFillPad.test.ts and
21
+ // DotplotDisplay/shaders/dotplotCapsulePad.test.ts.
22
+ // SYNC: keep in step with antialias.slang's glyphEdgeAlpha and with the SDFs
23
+ // in manhattan.slang.
24
+ import { aaHalfPx } from './antialias.js.generated.ts'
25
+
26
+ const INV_SQRT5 = 1 / Math.sqrt(5)
27
+
28
+ // The four SDFs manhattan.slang selects between, over quad-local `p`. Positive
29
+ // inside, zero on the edge. SHAPE_BAR's constant 1 is the degenerate case the
30
+ // ramp's floor exists for.
31
+ const SDFS = {
32
+ disc: (x: number, y: number) => 1 - Math.hypot(x, y),
33
+ diamond: (x: number, y: number) => 1 - (Math.abs(x) + Math.abs(y)),
34
+ triangle: (x: number, y: number) =>
35
+ Math.min(1 - y, (2 * x + y + 1) * INV_SQRT5, (-2 * x + y + 1) * INV_SQRT5),
36
+ bar: () => 1,
37
+ } as const
38
+
39
+ // |grad sdf| per unit of p, centrally differenced. Away from creases this is the
40
+ // exact analytic gradient.
41
+ function gradMag(sdf: (x: number, y: number) => number, x: number, y: number) {
42
+ const h = 1e-6
43
+ return Math.hypot(
44
+ (sdf(x + h, y) - sdf(x - h, y)) / (2 * h),
45
+ (sdf(x, y + h) - sdf(x, y - h)) / (2 * h),
46
+ )
47
+ }
48
+
49
+ // Points ON each shape's edge, parameterised so the sweep covers every facing —
50
+ // including the diagonals, which is where fwidth overshot and where the disc's
51
+ // quad has least room to spare.
52
+ function edgePoints(shape: keyof typeof SDFS, n = 64): [number, number][] {
53
+ const out: [number, number][] = []
54
+ for (let i = 0; i < n; i++) {
55
+ // Half-offset so no sample lands on an axis, i.e. on one of the diamond's
56
+ // four corners — `|y|` has a kink at y = 0, so a central difference there
57
+ // reports a gradient of 1 rather than the facet's sqrt(2). The corners are a
58
+ // property of the shape, not of the ramp, and the shader meets them on 1-2
59
+ // pixels per glyph.
60
+ const a = ((i + 0.5) / n) * Math.PI * 2
61
+ const c = Math.cos(a)
62
+ const s = Math.sin(a)
63
+ if (shape === 'disc') {
64
+ out.push([c, s])
65
+ } else if (shape === 'diamond') {
66
+ // scale the ray to the L1 unit ball
67
+ const k = 1 / (Math.abs(c) + Math.abs(s))
68
+ out.push([c * k, s * k])
69
+ } else if (shape === 'triangle') {
70
+ // walk the three edges rather than a ray, so corners are approached but
71
+ // the samples themselves stay on a single facet
72
+ const t = (i / n) * 3
73
+ const seg = Math.floor(t)
74
+ const f = 0.08 + (t - seg) * 0.84 // keep clear of the creases
75
+ const verts: [number, number][] = [
76
+ [-1, 1],
77
+ [1, 1],
78
+ [0, -1],
79
+ ]
80
+ const [ax, ay] = verts[seg]!
81
+ const [bx, by] = verts[(seg + 1) % 3]!
82
+ out.push([ax + (bx - ax) * f, ay + (by - ay) * f])
83
+ }
84
+ }
85
+ return out
86
+ }
87
+
88
+ const RADII_PX = [1.5, 2, 4, 10]
89
+ const DPRS = [1, 2]
90
+
91
+ describe('glyph SDF gradients', () => {
92
+ test('the disc and the triangle carry unit gradients', () => {
93
+ for (const [x, y] of edgePoints('disc')) {
94
+ expect(gradMag(SDFS.disc, x, y)).toBeCloseTo(1, 5)
95
+ }
96
+ for (const [x, y] of edgePoints('triangle')) {
97
+ expect(gradMag(SDFS.triangle, x, y)).toBeCloseTo(1, 5)
98
+ }
99
+ })
100
+
101
+ test('the diamond does not — its L1 norm carries sqrt(2)', () => {
102
+ // The trap a hard-coded ramp width would fall into: this SDF reports a
103
+ // distance sqrt(2) times the true perpendicular one, so a constant sized for
104
+ // the disc would make the diamond's edge sqrt(2) too soft.
105
+ for (const [x, y] of edgePoints('diamond')) {
106
+ expect(gradMag(SDFS.diamond, x, y)).toBeCloseTo(Math.SQRT2, 5)
107
+ }
108
+ })
109
+
110
+ test('the bar is constant, which is what the ramp floor is for', () => {
111
+ expect(gradMag(SDFS.bar, 0.3, -0.4)).toBe(0)
112
+ // max(0, 1e-5) is a positive ramp width, and a coverage of 1 over a ramp
113
+ // that narrow clamps to 1 — the solid fill SHAPE_BAR wants.
114
+ expect(Math.max(0, 1e-5)).toBeGreaterThan(0)
115
+ })
116
+ })
117
+
118
+ // The screen-space width of the ramp glyphEdgeAlpha produces, in DEVICE px, and
119
+ // how far it reaches outside the glyph edge in CSS px.
120
+ //
121
+ // `p` units relate to pixels through the glyph's radius: |p| = 1 sits at
122
+ // radiusPx CSS px from the center, so one device px is 1/(radiusPx*dpr) of p.
123
+ function rampGeometry(
124
+ sdf: (x: number, y: number) => number,
125
+ x: number,
126
+ y: number,
127
+ radiusPx: number,
128
+ dpr: number,
129
+ // how the shader turns its measured derivative into the ramp's half-width.
130
+ // `aaRamp` takes the measurement as the FULL ramp width, so the half is half
131
+ // of it; the retired spelling used fwidth's overshooting value as the half.
132
+ halfRampOf = (measuredPerDevicePx: number) => 0.5 * measuredPerDevicePx,
133
+ // what the shader differentiates WITH. These are two separate quantities and
134
+ // conflating them is what hides the bug: the ramp's width in coverage units
135
+ // comes from whatever the shader measured, but converting that back to a
136
+ // screen distance is the TRUE gradient's job. Divide by the measured value and
137
+ // fwidth's overshoot cancels itself out and looks correct.
138
+ measure = gradMag,
139
+ ) {
140
+ const trueG = gradMag(sdf, x, y)
141
+ const pPerDevicePx = 1 / (radiusPx * dpr)
142
+ const halfRamp = halfRampOf(measure(sdf, x, y) * pPerDevicePx)
143
+ // Back to screen distance: coverage changes by trueG per unit of p, so the
144
+ // half-ramp spans halfRamp/trueG of p, i.e. that times radiusPx CSS px.
145
+ const halfRampCssPx = (halfRamp / trueG) * radiusPx
146
+ return { widthDevicePx: 2 * halfRampCssPx * dpr, reachCssPx: halfRampCssPx }
147
+ }
148
+
149
+ // fwidth = |ddx| + |ddy|, which is what the retired spelling measured with.
150
+ function fwidthMag(
151
+ sdf: (x: number, y: number) => number,
152
+ x: number,
153
+ y: number,
154
+ ) {
155
+ const h = 1e-6
156
+ return (
157
+ Math.abs((sdf(x + h, y) - sdf(x - h, y)) / (2 * h)) +
158
+ Math.abs((sdf(x, y + h) - sdf(x, y - h)) / (2 * h))
159
+ )
160
+ }
161
+
162
+ describe('glyphEdgeAlpha ramp', () => {
163
+ test.each(['disc', 'diamond', 'triangle'] as const)(
164
+ 'is exactly one output pixel wide all the way round: %s',
165
+ shape => {
166
+ for (const [x, y] of edgePoints(shape)) {
167
+ for (const radiusPx of RADII_PX) {
168
+ for (const dpr of DPRS) {
169
+ const { widthDevicePx } = rampGeometry(
170
+ SDFS[shape],
171
+ x,
172
+ y,
173
+ radiusPx,
174
+ dpr,
175
+ )
176
+ expect(widthDevicePx).toBeCloseTo(1, 6)
177
+ }
178
+ }
179
+ }
180
+ },
181
+ )
182
+
183
+ // EQUALS, not "fits inside". The measured ramp reaches exactly half an output
184
+ // pixel past the edge whatever the shape — that is what taking the gradient
185
+ // buys — so the pad `discExpand` leaves is knowable exactly, and a pad merely
186
+ // big enough is one nobody can tell from a pad four times too big. `<=` held
187
+ // for any constant at or above 0.5 and is how a flat 1.0 CSS px survived.
188
+ test.each(['disc', 'diamond', 'triangle'] as const)(
189
+ 'the pad discExpand leaves is exactly the ramp reach: %s',
190
+ shape => {
191
+ for (const [x, y] of edgePoints(shape)) {
192
+ for (const radiusPx of RADII_PX) {
193
+ for (const dpr of DPRS) {
194
+ const { reachCssPx } = rampGeometry(
195
+ SDFS[shape],
196
+ x,
197
+ y,
198
+ radiusPx,
199
+ dpr,
200
+ )
201
+ expect(reachCssPx).toBeCloseTo(aaHalfPx(dpr), 9)
202
+ }
203
+ }
204
+ }
205
+ },
206
+ )
207
+
208
+ test('fwidth as a half-width was both too wide and clipped (do not reintroduce)', () => {
209
+ // The retired spelling, on the worst facing it has: a disc's 45-degree
210
+ // point, at dpr 1. fwidth reads sqrt(2) there instead of 1, and was then
211
+ // used whole as the half-width.
212
+ const diag = Math.SQRT1_2
213
+ const retired = rampGeometry(
214
+ SDFS.disc,
215
+ diag,
216
+ diag,
217
+ 4,
218
+ 1,
219
+ g => g, // used whole, not halved
220
+ fwidthMag,
221
+ )
222
+ expect(retired.widthDevicePx).toBeCloseTo(2 * Math.SQRT2, 6)
223
+ expect(retired.reachCssPx).toBeGreaterThan(aaHalfPx(1))
224
+
225
+ // and on an axis-aligned facing it was merely 2x too wide, which is what
226
+ // made the fade vary with position around one circle.
227
+ const axis = rampGeometry(SDFS.disc, 1, 0, 4, 1, g => g, fwidthMag)
228
+ expect(axis.widthDevicePx).toBeCloseTo(2, 6)
229
+ expect(retired.widthDevicePx / axis.widthDevicePx).toBeCloseTo(
230
+ Math.SQRT2,
231
+ 6,
232
+ )
233
+
234
+ // both are one pixel now, at every facing
235
+ expect(rampGeometry(SDFS.disc, diag, diag, 4, 1).widthDevicePx).toBeCloseTo(
236
+ 1,
237
+ 6,
238
+ )
239
+ expect(rampGeometry(SDFS.disc, 1, 0, 4, 1).widthDevicePx).toBeCloseTo(1, 6)
240
+ })
241
+ })
@@ -0,0 +1,47 @@
1
+ // AUTO-GENERATED by packages/shader-tools/src/shader-codegen from hpmath.slang.
2
+ // Do not edit. Run `pnpm gen:shaders` to regenerate.
3
+
4
+ // Scalar twins of hpmath.slang, transliterated from slangc's WGSL so
5
+ // the Canvas2D and SVG paths run the shader's own math. See adr-051.
6
+
7
+ export function snapBoxHeightPx(heightPx: number): number {
8
+ let hPx = Math.floor((heightPx + 0.5))
9
+ let _t0: boolean
10
+ if (((hPx - (2.0 * Math.floor((hPx * 0.5)))) == 0.0)) {
11
+ _t0 = (hPx >= 2.0)
12
+ } else {
13
+ _t0 = false
14
+ }
15
+ if (_t0) {
16
+ _t0 = (hPx <= 4.0)
17
+ } else {
18
+ _t0 = false
19
+ }
20
+ let _t1: number
21
+ if (_t0) {
22
+ _t1 = (hPx + 1.0)
23
+ } else {
24
+ _t1 = hPx
25
+ }
26
+ return _t1
27
+ }
28
+
29
+ export function snapBoxCenterYPx(centerY: number, heightPx: number, scrollY: number): number {
30
+ return ((Math.floor((((centerY - (heightPx * 0.5)) - scrollY) + 0.5)) + Math.floor((snapBoxHeightPx(heightPx) * 0.5))) + 0.5)
31
+ }
32
+
33
+ export function extendToMinWidthPx(x1: number, x2: number, minWidth: number): number {
34
+ let dx = (x2 - x1)
35
+ let _t0: number
36
+ if ((Math.abs(dx) < minWidth)) {
37
+ if ((dx < 0.0)) {
38
+ _t0 = -minWidth
39
+ } else {
40
+ _t0 = minWidth
41
+ }
42
+ _t0 = (x1 + _t0)
43
+ } else {
44
+ _t0 = x2
45
+ }
46
+ return _t0
47
+ }
@@ -0,0 +1,226 @@
1
+ // Shared high-precision genome-coordinate math. HP technique from genome-spy
2
+ // (MIT): https://github.com/genome-spy/genome-spy
3
+ //
4
+ // Imported via `import hpmath;` from shaders that need sub-pixel-accurate
5
+ // conversion of base-pair positions to clip space when zoomed far in.
6
+ //
7
+ // The `js-export` set is the px-space *decisions* the Canvas2D and SVG paths
8
+ // must make identically to the GPU — which pixel row a glyph centers on, how a
9
+ // sub-pixel span is widened. `pnpm gen:shaders` transliterates them into
10
+ // hpmath.js.generated.ts; nothing downstream re-derives them by hand.
11
+ //! js-export: snapBoxHeightPx, snapBoxCenterYPx, extendToMinWidthPx
12
+ //! js-skip: hpSplitUint — the hi/lo float32 precision split exists because a GPU has no float64; the Canvas2D path just uses a number
13
+ //! js-skip: quadLocal — maps a vertex id to a quad corner — Canvas2D has no vertices, it calls fillRect
14
+ //! js-skip: yPxToClipY — clip space is the GPU coordinate system; the px-space decision it wraps is what gets exported (snapBoxCenterYPx and friends)
15
+ //! js-skip: clipXToPx — the x half of the same clip-space conversion, and the reason a px decision can be written once — nothing outside a shader is in clip space
16
+ //! js-skip: pxToClipX — the inverse of clipXToPx, same reason
17
+ //! js-skip: pxToClipLen — the length half of the same conversion; a px dimension is already in px on the Canvas2D side, so there is nothing to convert
18
+ //! js-skip: clipLenToPx — the inverse of pxToClipLen, same reason
19
+ //! js-skip: extendToMinWidthX — clip-space wrapper over the exported extendToMinWidthPx, which is the decision
20
+ //! js-skip: expandToMinWidthPx — the midpoint rule has no Canvas2D counterpart: the pileup floors a cell width one-sidedly and adds a seam fudge the shader deliberately omits (rendererTypes.ts pileupCellWidth), and the variant matrix floors in physical px against a canvas the Canvas2D twin sizes itself. A different rule, not a twin
21
+ //! js-skip: expandToMinWidthX — clip-space wrapper over expandToMinWidthPx, same reason as extendToMinWidthX
22
+ //! js-skip: snapToPixelX — clip in, clip out; its px core is `floor(x + 0.5)`, which is Math.round and needs no twin
23
+ //! js-skip: snapBoxCenterY — clip-space wrapper over the exported snapBoxCenterYPx
24
+ module hpmath;
25
+
26
+ public static const uint HP_LOW_MASK = 0xFFFu;
27
+
28
+ public float2 hpSplitUint(uint value) {
29
+ uint lo = value & HP_LOW_MASK;
30
+ uint hi = value - lo;
31
+ return float2(float(hi), float(lo));
32
+ }
33
+
34
+ // `max(-inf)` and `dot()` exist to thwart the compiler — without them it
35
+ // collapses (hi_a+lo_a)-(hi_b+lo_b) into one large subtraction and the
36
+ // float32 precision the hi/lo split exists to preserve is gone. `hpZero` is
37
+ // a tiny CPU-supplied number; `-1/hpZero` is smaller than any real diff.
38
+ public float hpToClipX(float2 splitPos, float3 bpRange, float hpZero) {
39
+ float inf = 1.0 / hpZero;
40
+ float step = 2.0 / bpRange.z;
41
+ float hi = max(splitPos.x - bpRange.x, -inf);
42
+ float lo = max(splitPos.y - bpRange.y, -inf);
43
+ return dot(float3(-1.0, hi, lo), float3(1.0, step, step));
44
+ }
45
+
46
+ public float hpScaleLinear(float2 splitPos, float3 bpRange, float hpZero) {
47
+ float inf = 1.0 / hpZero;
48
+ float step = 1.0 / bpRange.z;
49
+ float hi = max(splitPos.x - bpRange.x, -inf);
50
+ float lo = max(splitPos.y - bpRange.y, -inf);
51
+ return dot(float2(hi, lo), float2(step, step));
52
+ }
53
+
54
+ // Exact-as-possible bp offset from a split position to the viewport start
55
+ // stored in bpRange.xy. Subtracts the hi/lo components separately (same
56
+ // ordering as hpToClipX) so the result stays precise at high genomic
57
+ // coordinates — recombining bpStartHi+bpStartLo first would lose ~256bp near
58
+ // 3e9. Unlike hpToClipX this returns bp, not clip space.
59
+ public float hpViewportOffsetBp(float2 splitPos, float3 bpRange) {
60
+ return (bpRange.x - splitPos.x) + (bpRange.y - splitPos.y);
61
+ }
62
+
63
+ public float snapToPixelX(float clipX, float canvasWidth) {
64
+ float px = (clipX + 1.0) * 0.5 * canvasWidth;
65
+ return floor(px + 0.5) / canvasWidth * 2.0 - 1.0;
66
+ }
67
+
68
+ public float yPxToClipY(float yPx, float canvasHeight) {
69
+ return 1.0 - (yPx / canvasHeight) * 2.0;
70
+ }
71
+
72
+ // Tallest box that gets the odd-height nudge below. Above this a half-pixel of
73
+ // asymmetry is a small fraction of the box and invisible; at 2px it is half of
74
+ // it, so the exons appear to float above their intron line.
75
+ public static const float THIN_BOX_PX = 4.0;
76
+
77
+ // The pixel height a box is drawn at: its rounded height, except that a THIN box
78
+ // with an EVEN height is nudged up to the next odd one. A box spanning an even
79
+ // number of rows has no middle ROW — its center falls on the seam between two —
80
+ // so the 1px glyphs that ride on it (intron line, chevrons, strand arrow) can
81
+ // only ever sit half a pixel off center, which at the 2px bodies fit mode
82
+ // squeezes down to reads as the exons sitting above the intron line. An odd
83
+ // height gives the box a true center row for them to land on. The extra pixel
84
+ // can't reach the row below: the packer quantizes row tops to a pitch of at
85
+ // least twice the body height at these sizes (see layout.ts pitchY).
86
+ // A height that rounds to 0 is left at 0: it draws nothing, and nothing is what
87
+ // a caller asking for a zero-height box wants.
88
+ public float snapBoxHeightPx(float heightPx) {
89
+ float hPx = floor(heightPx + 0.5);
90
+ bool even = hPx - 2.0 * floor(hPx * 0.5) == 0.0;
91
+ return even && hPx >= 2.0 && hPx <= THIN_BOX_PX ? hPx + 1.0 : hPx;
92
+ }
93
+
94
+ // Center a thin glyph on the *drawn* middle row of the box it annotates, given
95
+ // the box's real center `centerY` and height `heightPx`. The box is rasterized
96
+ // with the rect shader's edge snapping (top = floor(top+0.5), height =
97
+ // snapBoxHeightPx(h)); reproducing that here lands the line/chevron exactly on
98
+ // the box's center row instead of the ~1px bias a plain pixel-center snap
99
+ // (`floor(centerY - scrollY + 0.5) + 0.5`) gives. Thin boxes are drawn at an odd
100
+ // height precisely so that center row exists; a taller even box still has none,
101
+ // and stays consistent (just below the seam).
102
+ //
103
+ // Split in two on purpose. The decision — which pixel row is the center — is
104
+ // resolution math the Canvas2D and SVG paths need in *screen px*; only the
105
+ // clip-space conversion is GPU-specific. `//! js-export` lifts the px half into
106
+ // TypeScript, so the two backends share the rule instead of the Canvas2D twin
107
+ // re-deriving it (see adr-051). Keep new shared decisions in this shape: pure
108
+ // px-in/px-out below, the `yPxToClipY` wrapper above it.
109
+ public float snapBoxCenterYPx(float centerY, float heightPx, float scrollY) {
110
+ float topPx = floor(centerY - heightPx * 0.5 - scrollY + 0.5);
111
+ return topPx + floor(snapBoxHeightPx(heightPx) * 0.5) + 0.5;
112
+ }
113
+
114
+ public float snapBoxCenterY(
115
+ float centerY, float heightPx, float scrollY, float canvasHeight
116
+ ) {
117
+ return yPxToClipY(snapBoxCenterYPx(centerY, heightPx, scrollY), canvasHeight);
118
+ }
119
+
120
+ // Extend the free edge `x2` away from the anchor `x1` so the span is at least
121
+ // `minWidthPx` CSS px wide, returning the new x2. Sub-pixel-narrow features (a
122
+ // 1bp rect, a zoomed-out coverage bin) still paint instead of vanishing.
123
+ //
124
+ // Orientation-preserving on purpose: reversal is baked into bpRangeX (negated
125
+ // length), so x2 < x1 on reversed blocks and the span must grow in the sign
126
+ // direction of dx. Widening toward +x would flip the quad on reversed regions.
127
+ // Contrast `expandToMinWidthPx` below, which moves both edges about the
128
+ // midpoint — that one is for spans with no anchored edge.
129
+ // Unit-agnostic core: `minWidth` is in whatever space x1/x2 are. Canvas2D calls
130
+ // this directly in screen px (paired with `spanLeft`, which owns the fill-x
131
+ // pivot); the clip-space wrapper below converts the px floor first. Same split
132
+ // as snapBoxCenterYPx — the rule is shared, the coordinate space is not.
133
+ public float extendToMinWidthPx(float x1, float x2, float minWidth) {
134
+ float dx = x2 - x1;
135
+ return abs(dx) < minWidth ? x1 + (dx < 0.0 ? -minWidth : minWidth) : x2;
136
+ }
137
+
138
+ public float extendToMinWidthX(
139
+ float x1, float x2, float minWidthPx, float canvasWidth
140
+ ) {
141
+ return extendToMinWidthPx(x1, x2, minWidthPx * 2.0 / canvasWidth);
142
+ }
143
+
144
+ // The other min-width rule: widen a [x1, x2] span about its MIDPOINT, so both
145
+ // edges move. For a span with no anchored edge — a coverage bin, a gap, an
146
+ // insertion bar, a variant-matrix column — where growing off one side would
147
+ // slide the mark away from the coordinate it reports.
148
+ //
149
+ // Not orientation-preserving, and deliberately not: the callers here hand it an
150
+ // already-ordered span. `x2 - x1 < minWidth` rather than `abs(...)` means a
151
+ // span given backwards is always widened and comes back forwards.
152
+ //
153
+ // Same px-core/clip-wrapper split as `extendToMinWidthPx` above, and here it is
154
+ // what lets the alignments passes (clip space, a 1 CSS px floor) and the variant
155
+ // matrix (physical px, its own MIN_COL_PX) run one rule instead of two spellings
156
+ // of it.
157
+ public float2 expandToMinWidthPx(float x1, float x2, float minWidth) {
158
+ if (x2 - x1 < minWidth) {
159
+ float mid = (x1 + x2) * 0.5;
160
+ return float2(mid - minWidth * 0.5, mid + minWidth * 0.5);
161
+ }
162
+ return float2(x1, x2);
163
+ }
164
+
165
+ public float2 expandToMinWidthX(
166
+ float x1, float x2, float minWidthPx, float canvasWidth
167
+ ) {
168
+ return expandToMinWidthPx(x1, x2, minWidthPx * 2.0 / canvasWidth);
169
+ }
170
+
171
+ // vid%6: 0→(0,0) 1→(1,0) 2→(0,1) 3→(0,1) 4→(1,0) 5→(1,1)
172
+ public float2 quadLocal(uint vid) {
173
+ uint v = vid % 6u;
174
+ return float2(
175
+ (v == 0u || v == 2u || v == 3u) ? 0.0 : 1.0,
176
+ (v == 0u || v == 1u || v == 4u) ? 0.0 : 1.0
177
+ );
178
+ }
179
+
180
+ // Screen-space (CSS px, top-left origin) → clip space, with the Y flip. Shared
181
+ // by the synteny/dotplot/hic renderers, whose 2D marks all map a screen-px
182
+ // point onto the viewport this way; keeps the flip sign from drifting between
183
+ // the plugins that previously open-coded it. (Appended after quadLocal on
184
+ // purpose: inserting earlier reshuffles every importer's debug #line numbers.)
185
+ public float4 screenToClip(float2 screen, float2 resolution) {
186
+ float2 clip = screen / resolution * 2.0 - 1.0;
187
+ return float4(clip.x, -clip.y, 0.0, 1.0);
188
+ }
189
+
190
+ // The x pair of `yPxToClipY`/its inverse: clip x ↔ screen px across the span
191
+ // clip [-1,1] covers. `canvasWidth` is whatever width that span means for the
192
+ // caller — for the LGV glyph passes it is the block's scissored width, not the
193
+ // canvas (see FeatureGlyphUniforms.canvasWidth).
194
+ //
195
+ // These exist so a horizontal decision can be written once in px and used from
196
+ // clip space, the way `snapBoxCenterYPx` already is vertically:
197
+ // `pxToClipX(decide(clipXToPx(a), clipXToPx(b)), w)`. `snapToPixelX` above is
198
+ // that sandwich with `floor(x + 0.5)` in the middle — the round trip is
199
+ // arithmetic it was already doing internally, so spelling a decision this way
200
+ // costs no precision. (Appended, per the note on screenToClip.)
201
+ public float clipXToPx(float clipX, float canvasWidth) {
202
+ return (clipX + 1.0) * 0.5 * canvasWidth;
203
+ }
204
+
205
+ public float pxToClipX(float xPx, float canvasWidth) {
206
+ return xPx / canvasWidth * 2.0 - 1.0;
207
+ }
208
+
209
+ // The LENGTH pair, as against the POSITION pair above: clip spans 2 units
210
+ // across `size`, so a px span is `px * 2 / size` with no origin term. `size` is
211
+ // the canvas width or height, or whatever span clip [-1,1] covers for the
212
+ // caller — the same argument `clipXToPx` and `yPxToClipY` take.
213
+ //
214
+ // Spell every px dimension through these. Writing `HALF_W_PX / u.canvasWidth`
215
+ // gives the same number but folds the /2 into the conversion, so the constant
216
+ // reads as a full width while meaning a half one; and `TRIANGLE_HW * 2.0 * 2.0
217
+ // / u.canvasW` (indicator.slang, before this existed) leaves a reader to work
218
+ // out which 2.0 doubles the half-width and which one is the clip span.
219
+ // (Appended, per the note on screenToClip.)
220
+ public float pxToClipLen(float px, float size) {
221
+ return px * 2.0 / size;
222
+ }
223
+
224
+ public float clipLenToPx(float clipLen, float size) {
225
+ return clipLen * 0.5 * size;
226
+ }
@@ -0,0 +1,4 @@
1
+ // AUTO-GENERATED by packages/shader-tools/src/shader-codegen from pointGlyph.slang.
2
+ // Do not edit. Run `pnpm gen:shaders` to regenerate.
3
+
4
+ export const SMALL_POINT_MAX_DIAMETER = 3
@@ -0,0 +1,9 @@
1
+ // AUTO-GENERATED by packages/shader-tools/src/shader-codegen from pointGlyph.slang.
2
+ // Do not edit. Run `pnpm gen:shaders` to regenerate.
3
+
4
+ // Scalar twins of pointGlyph.slang, transliterated from slangc's WGSL so
5
+ // the Canvas2D and SVG paths run the shader's own math. See adr-051.
6
+
7
+ export function crispSquareTopLeftPx(centerPx: number, diameterPx: number): number {
8
+ return Math.floor(((centerPx - (diameterPx * 0.5)) + 0.5))
9
+ }
@@ -0,0 +1,93 @@
1
+ // Shared scatter-point glyph math for the wiggle and Manhattan renderers, whose
2
+ // point mode draws the same antialiased disc. Factored here so the disc SDF and
3
+ // the "small point → crisp square" / AA-pad thresholds can't drift between the
4
+ // two shaders (they previously redeclared identical consts with "must match"
5
+ // comments pointing at each other).
6
+ //
7
+ // The CPU side keeps no copy of either the threshold or the snap:
8
+ // `export-consts` emits the constant and `js-export` emits the snap, both
9
+ // consumed by `pointMarker.ts` (adr-051).
10
+ //! export-consts: SMALL_POINT_MAX_DIAMETER
11
+ //! js-export: crispSquareTopLeftPx
12
+ //! js-skip: discExpand — expands a quad so the fragment AA ramp is not clipped; Canvas2D draws ctx.arc and has no quad to expand
13
+ module pointGlyph;
14
+
15
+ import antialias;
16
+ import hpmath;
17
+
18
+ // A disc's edge is ramped by `glyphEdgeAlpha`, whose width is one OUTPUT pixel
19
+ // measured in the SDF's own units — so in CSS px the ramp reaches `aaHalfPx`
20
+ // past the boundary and the quad has to grow by exactly that.
21
+ //
22
+ // This was a flat 1.0 under a comment calling it device px while both consumers
23
+ // added it in CSS px: 2x the reach at dpr 1 and 4x at dpr 2, every surplus
24
+ // fragment shading to alpha 0. A pad is the one place the two units cannot be
25
+ // swapped without anyone seeing.
26
+
27
+ // At/below this diameter an AA disc reads as a muddy blob, so callers draw a
28
+ // crisp filled square instead.
29
+ public static const float SMALL_POINT_MAX_DIAMETER = 3.0;
30
+
31
+ // Quad scale factor that grows the disc's bounding quad by the ramp's reach so
32
+ // its faded edge isn't clipped. localPos is scaled by the same factor so the
33
+ // disc boundary stays at distance 1 (see discAlpha). `max(radiusPx, 0.5)` keeps
34
+ // sub-pixel points from exploding the expansion.
35
+ public float discExpand(float radiusPx, float dpr) {
36
+ return (radiusPx + aaHalfPx(dpr)) / max(radiusPx, 0.5);
37
+ }
38
+
39
+ // Pixel-grid-snapped top-left of the crisp small-point square, on one axis. The
40
+ // snap (round-half-up) is what makes the square's fill land on whole pixels
41
+ // instead of AA-blurring across columns/rows — it is SHAPE_SOLID with no
42
+ // fragment-side AA, so an unsnapped center reads as a fuzzy blob.
43
+ //
44
+ // Per-axis and scalar so `//! js-export` can lift it: Canvas2D's
45
+ // `appendPointMarker` calls this for x and y rather than re-deriving the snap
46
+ // (adr-051). The float2 wrapper below keeps the shader's call site unchanged.
47
+ public float crispSquareTopLeftPx(float centerPx, float diameterPx) {
48
+ return floor(centerPx - diameterPx * 0.5 + 0.5);
49
+ }
50
+
51
+ // Screen-px position of quad corner `q` (each component 0 or 1) for the crisp
52
+ // small-point square, given the marker center and diameter in px.
53
+ public float2 crispSquareCornerPx(float2 centerPx, float diameterPx, float2 q) {
54
+ float2 topLeft = float2(
55
+ crispSquareTopLeftPx(centerPx.x, diameterPx),
56
+ crispSquareTopLeftPx(centerPx.y, diameterPx)
57
+ );
58
+ return topLeft + q * diameterPx;
59
+ }
60
+
61
+ // The same corner in clip space, for a marker whose centre arrives as a clip x
62
+ // and a screen y — which is how both callers hold it, since x comes off the hp
63
+ // bp conversion and y off a score scale.
64
+ //
65
+ // The round trip is the point: the snap has to happen in px, so the caller
66
+ // converts out and back, and doing that in each of the two shaders is two
67
+ // chances to convert one of the four edges differently. `viewportWidth` and
68
+ // `canvasHeight` are whatever spans clip [-1,1] covers for the caller.
69
+ public float4 crispSquareCornerClip(
70
+ float centerClipX, float centerYPx, float diameterPx, float2 q,
71
+ float viewportWidth, float canvasHeight
72
+ ) {
73
+ float2 centerPx = float2(clipXToPx(centerClipX, viewportWidth), centerYPx);
74
+ float2 vPx = crispSquareCornerPx(centerPx, diameterPx, q);
75
+ return float4(pxToClipX(vPx.x, viewportWidth),
76
+ yPxToClipY(vPx.y, canvasHeight), 0.0, 1.0);
77
+ }
78
+
79
+ // Disc SDF, positive inside. localPos is the expanded quad-local coord
80
+ // (-expand..expand); the disc boundary sits at distance 1, so the SDF is
81
+ // `1 - length`. Exposed separately for shaders that select between several
82
+ // glyph SDFs and take the AA derivative once afterwards — WGSL only allows
83
+ // derivatives in uniform control flow, so they can't call discAlpha inside a
84
+ // branch on a varying.
85
+ public float discCoverage(float2 localPos) {
86
+ return 1.0 - length(localPos);
87
+ }
88
+
89
+ // Antialiased disc coverage → alpha, for callers that draw only a disc and can
90
+ // take the derivative inline. Only call from uniform control flow.
91
+ public float discAlpha(float2 localPos) {
92
+ return glyphEdgeAlpha(discCoverage(localPos));
93
+ }
@@ -0,0 +1,8 @@
1
+ // AUTO-GENERATED by packages/shader-tools/src/shader-codegen from rowRect.slang.
2
+ // Do not edit. Run `pnpm gen:shaders` to regenerate.
3
+
4
+ export const MIN_DRAWN_ROW_PX = 1
5
+
6
+ export const MIN_DRAWN_CELL_PX = 1
7
+
8
+ export const MULTI_ROW_MIN_CELL_PX = 2
@@ -0,0 +1,17 @@
1
+ // AUTO-GENERATED by packages/shader-tools/src/shader-codegen from rowRect.slang.
2
+ // Do not edit. Run `pnpm gen:shaders` to regenerate.
3
+
4
+ // Scalar twins of rowRect.slang, transliterated from slangc's WGSL so
5
+ // the Canvas2D and SVG paths run the shader's own math. See adr-051.
6
+
7
+ function _max(a: number, b: number) {
8
+ return b > a || Number.isNaN(a) ? b : a
9
+ }
10
+
11
+ export function drawnRowHeightPx(rowHeight: number, rowProportion: number): number {
12
+ return _max((rowHeight * rowProportion), 1.0)
13
+ }
14
+
15
+ export function rowBandOffsetPx(rowHeight: number, rowProportion: number): number {
16
+ return ((rowHeight - drawnRowHeightPx(rowHeight, rowProportion)) * 0.5)
17
+ }