insomni 0.2.0-alpha.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.
@@ -0,0 +1,1574 @@
1
+ import { A as DEPTH_FORMAT, _ as TAU, i as DEPTH_COMPARE, t as ALPHA_BLEND } from "./pipeline-BWCAZTKx.mjs";
2
+ import { d } from "typegpu";
3
+ import "earcut";
4
+ //#region src/math/color.ts
5
+ function rgba(r, g, b, a = 1) {
6
+ return {
7
+ r,
8
+ g,
9
+ b,
10
+ a
11
+ };
12
+ }
13
+ /** Parse a 24-bit hex color, e.g. hex(0xff6600). Alpha is always 1. */
14
+ function hex(value) {
15
+ return {
16
+ r: (value >> 16 & 255) / 255,
17
+ g: (value >> 8 & 255) / 255,
18
+ b: (value & 255) / 255,
19
+ a: 1
20
+ };
21
+ }
22
+ /** Parse a CSS hex string, e.g. cssHex("#ff6600") or cssHex("#ff660080"). */
23
+ function cssHex(value) {
24
+ const s = value.replace("#", "");
25
+ const n = parseInt(s, 16);
26
+ if (s.length === 6) return hex(n);
27
+ return {
28
+ r: (n >> 24 & 255) / 255,
29
+ g: (n >> 16 & 255) / 255,
30
+ b: (n >> 8 & 255) / 255,
31
+ a: (n & 255) / 255
32
+ };
33
+ }
34
+ /**
35
+ * HSL → RGB. All inputs in `[0, 1]`. Hue wraps automatically.
36
+ * `hsv` is the cousin of this for value-based color picking.
37
+ */
38
+ function hsl(h, s, l, a = 1) {
39
+ const hh = (h % 1 + 1) % 1;
40
+ const k = (n) => (n + hh * 12) % 12;
41
+ const chroma = s * Math.min(l, 1 - l);
42
+ const f = (n) => l - chroma * Math.max(-1, Math.min(k(n) - 3, 9 - k(n), 1));
43
+ return {
44
+ r: f(0),
45
+ g: f(8),
46
+ b: f(4),
47
+ a
48
+ };
49
+ }
50
+ /** HSV → RGB. All inputs in `[0, 1]`. Hue wraps automatically. */
51
+ function hsv(h, s, v, a = 1) {
52
+ const hh = (h % 1 + 1) % 1;
53
+ const k = (n) => (n + hh * 6) % 6;
54
+ const f = (n) => v - v * s * Math.max(0, Math.min(k(n), 4 - k(n), 1));
55
+ return {
56
+ r: f(5),
57
+ g: f(3),
58
+ b: f(1),
59
+ a
60
+ };
61
+ }
62
+ /** Alias of `hsl` for parity with code that explicitly calls out the conversion. */
63
+ const hslToRgb = hsl;
64
+ /** Alias of `hsl` — accepts the same `(h, s, l, a)` signature. */
65
+ const hslToRgba = hsl;
66
+ /** Linearly interpolate two colors channel-wise. */
67
+ function lerpColor(a, b, t) {
68
+ return {
69
+ r: a.r + (b.r - a.r) * t,
70
+ g: a.g + (b.g - a.g) * t,
71
+ b: a.b + (b.b - a.b) * t,
72
+ a: a.a + (b.a - a.a) * t
73
+ };
74
+ }
75
+ /** Alias of `lerpColor` matching the `mix()` naming used in shaders. */
76
+ const mixColor = lerpColor;
77
+ /** Return a copy of `color` with the given alpha. */
78
+ function withAlpha(color, alpha) {
79
+ return {
80
+ r: color.r,
81
+ g: color.g,
82
+ b: color.b,
83
+ a: alpha
84
+ };
85
+ }
86
+ /** Multiply alpha — useful for fading layered colors without losing the base value. */
87
+ function fade(color, factor) {
88
+ return {
89
+ r: color.r,
90
+ g: color.g,
91
+ b: color.b,
92
+ a: color.a * factor
93
+ };
94
+ }
95
+ /** Mix `color` with white by `t` ∈ [0, 1]. */
96
+ function lighten(color, t) {
97
+ return lerpColor(color, WHITE, t);
98
+ }
99
+ /** Mix `color` with black by `t` ∈ [0, 1]. */
100
+ function darken(color, t) {
101
+ return lerpColor(color, BLACK, t);
102
+ }
103
+ const TRANSPARENT = {
104
+ r: 0,
105
+ g: 0,
106
+ b: 0,
107
+ a: 0
108
+ };
109
+ const BLACK = {
110
+ r: 0,
111
+ g: 0,
112
+ b: 0,
113
+ a: 1
114
+ };
115
+ const WHITE = {
116
+ r: 1,
117
+ g: 1,
118
+ b: 1,
119
+ a: 1
120
+ };
121
+ //#endregion
122
+ //#region src/schema/instance.ts
123
+ /**
124
+ * Universal per-instance record: 16 floats / 64 bytes. Field order here IS the
125
+ * GPU memory order; do not reorder without re-deriving every consumer.
126
+ *
127
+ * Depth is carried by the explicit `order` field (float-offset 10), never
128
+ * derived from the instance index. Index-derived Z couples depth precision to
129
+ * draw count and requires a per-draw uniform; the explicit field removes both.
130
+ *
131
+ * Layout (float offset / byte offset):
132
+ * geom0 @ 0 / 0 vec4f — geometry slot 0 (kind-specific)
133
+ * geom1 @ 4 / 16 vec4f — geometry slot 1 (kind-specific)
134
+ * colorBits @ 8 / 32 u32 — packed unorm8x4 color
135
+ * typeFlag @ 9 / 36 u32 — shape-kind discriminant
136
+ * order @ 10 / 40 f32 — EXPLICIT depth; never index-derived
137
+ * lane0 @ 11 / 44 u32 — kind-specific payload
138
+ * lane1 @ 12 / 48 u32
139
+ * lane2 @ 13 / 52 u32
140
+ * lane3 @ 14 / 56 u32
141
+ * lane4 @ 15 / 60 u32 — see emphasis note below
142
+ *
143
+ * `lane4` is dual-purpose, kind-by-kind. No *shape* kind (rect/circle/ellipse/
144
+ * segment/curve/arc/triangle/polyline) reads it, so for shapes it carries the
145
+ * **emphasis key** (Phase 4): the uber `transparent` / OIT-build fragments
146
+ * compare it against the `Emphasis.focusedKey` uniform to dim non-focused
147
+ * PARTICIPATING instances (`shader/assemble.ts`). EXEMPT-ZERO: `lane4 == 0u` —
148
+ * the deterministic default `UberPack.append` writes — is the opt-out sentinel;
149
+ * a key-0 shape is NEVER dimmed (instances opt INTO emphasis with a key ≥ 1), so
150
+ * untagged shapes stay full-alpha during a mark emphasis. A shape's `lane4` is
151
+ * never stale. The glyph pack (`glyph/glyph-pack.ts`) independently uses `lane4`
152
+ * for the anchored-text world anchor Y — that is fine because glyphs render
153
+ * through the dedicated glyph pipeline, which never reads the emphasis uniform.
154
+ */
155
+ const INSTANCE_FIELDS = [
156
+ {
157
+ name: "geom0",
158
+ kind: "vec4f",
159
+ floats: 4
160
+ },
161
+ {
162
+ name: "geom1",
163
+ kind: "vec4f",
164
+ floats: 4
165
+ },
166
+ {
167
+ name: "colorBits",
168
+ kind: "u32",
169
+ floats: 1
170
+ },
171
+ {
172
+ name: "typeFlag",
173
+ kind: "u32",
174
+ floats: 1
175
+ },
176
+ {
177
+ name: "order",
178
+ kind: "f32",
179
+ floats: 1
180
+ },
181
+ {
182
+ name: "lane0",
183
+ kind: "u32",
184
+ floats: 1
185
+ },
186
+ {
187
+ name: "lane1",
188
+ kind: "u32",
189
+ floats: 1
190
+ },
191
+ {
192
+ name: "lane2",
193
+ kind: "u32",
194
+ floats: 1
195
+ },
196
+ {
197
+ name: "lane3",
198
+ kind: "u32",
199
+ floats: 1
200
+ },
201
+ {
202
+ name: "lane4",
203
+ kind: "u32",
204
+ floats: 1
205
+ }
206
+ ];
207
+ /** Total floats per instance — derived from {@link INSTANCE_FIELDS}. */
208
+ const INSTANCE_FLOATS = INSTANCE_FIELDS.reduce((sum, f) => sum + f.floats, 0);
209
+ /** Total bytes per instance — derived from {@link INSTANCE_FLOATS}. */
210
+ const INSTANCE_BYTES = INSTANCE_FLOATS * 4;
211
+ if (INSTANCE_FLOATS !== 16) throw new Error(`v3 INSTANCE_FLOATS must be 16, got ${INSTANCE_FLOATS}`);
212
+ if (INSTANCE_BYTES !== 64) throw new Error(`v3 INSTANCE_BYTES must be 64, got ${INSTANCE_BYTES}`);
213
+ //#endregion
214
+ //#region src/kinds/kind.ts
215
+ /** Shape-kind discriminant written into `InstanceRecord.typeFlag`. */
216
+ const TYPE_RECT = 0;
217
+ const TYPE_CIRCLE = 1;
218
+ const TYPE_ELLIPSE = 2;
219
+ const TYPE_SEGMENT = 3;
220
+ const TYPE_CURVE = 4;
221
+ /**
222
+ * Reserved for a future Phase-4 in-uber sprite kind (texture binding in the
223
+ * uber-shader + `Layer.pushSprite`). In Phase 3, sprites render via the
224
+ * {@link BufferLayer} + sibling pipeline path — see
225
+ * `pipelines/sprite-pipelines.ts` (`createSpritePipelines`) and
226
+ * `layers/buffer-layer.ts` (`BufferLayerSpritesOptions`). `TYPE_SPRITE` is
227
+ * intentionally absent from `ORDERED_KINDS` in `kinds/index.ts`: the shader
228
+ * assembler (`assembleShader`) does NOT emit a dispatch branch for it, and
229
+ * there is no concrete sprite kind module under `kinds/`.
230
+ */
231
+ const TYPE_SPRITE = 5;
232
+ const TYPE_TRIANGLE = 6;
233
+ const TYPE_ARC = 7;
234
+ /**
235
+ * Glyph quad packed by `GlyphPack`. One instance per visible glyph;
236
+ * atlas UV, atlas glyph id, and per-string flags live in the lane slots.
237
+ * The MSDF glyph pipeline (RTT bake) selects this kind at fragment time.
238
+ */
239
+ const TYPE_GLYPH = 8;
240
+ /**
241
+ * Analytic SDF stroke-path (`kinds/stroke.ts`). Up to 4 points per instance; an
242
+ * N-point stroke chains `ceil((M-1)/3)` instances. The min-of-segment SDF in
243
+ * the fragment computes coverage analytically (no CPU triangulation) — the
244
+ * opt-in alternative to the tessellated `pushPolyline` path for dense strokes.
245
+ */
246
+ const TYPE_STROKE_PATH = 9;
247
+ //#endregion
248
+ //#region src/core/f16.ts
249
+ /**
250
+ * IEEE-754 half-precision (f16) packing helpers. Used to pack per-shape
251
+ * scalars into the instance vertex buffer — see `pack/uber-pack.ts`.
252
+ *
253
+ * Not suitable for large-magnitude world coordinates: f16 has ±65504 range
254
+ * and sub-pixel precision only up to ~1024.
255
+ */
256
+ const _f32 = new Float32Array(1);
257
+ const _u32 = new Uint32Array(_f32.buffer);
258
+ const _f32_2 = new Float32Array(2);
259
+ const _u32_2 = new Uint32Array(_f32_2.buffer);
260
+ /**
261
+ * Pack a 32-bit float into its 16-bit half-precision bit pattern.
262
+ * Rounds ties to even. NaN → canonical quiet NaN. Out-of-range → ±Inf.
263
+ */
264
+ function packF16(value) {
265
+ if (value === 0) return 1 / value === -Infinity ? 32768 : 0;
266
+ _f32[0] = value;
267
+ const bits = _u32[0];
268
+ const sign = bits >>> 16 & 32768;
269
+ const exp = bits >>> 23 & 255;
270
+ const mant = bits & 8388607;
271
+ if (exp > 112 && exp < 143) {
272
+ let m16 = mant >>> 13;
273
+ if ((mant & 4096) !== 0) {
274
+ m16 += 1;
275
+ if ((m16 & 1024) !== 0) return sign | exp - 111 << 10;
276
+ }
277
+ return sign | exp - 112 << 10 | m16;
278
+ }
279
+ if (exp === 255) return sign | 31744 | (mant !== 0 ? 512 : 0);
280
+ if (exp <= 112) {
281
+ const e = exp - 112;
282
+ if (e < -10) return sign;
283
+ let m = (mant | 8388608) >>> 1 - e;
284
+ if ((m & 4096) !== 0) m += 4096 + (m >>> 13 & 1);
285
+ return sign | m >>> 13;
286
+ }
287
+ return sign | 31744;
288
+ }
289
+ /** Pack (lo, hi) as a single little-endian u32 — `pack2x16float` equivalent. */
290
+ function pack2xF16(lo, hi) {
291
+ if (lo === 0 && hi === 0) return ((1 / lo === -Infinity ? 32768 : 0) | (1 / hi === -Infinity ? 32768 : 0) << 16) >>> 0;
292
+ _f32_2[0] = lo;
293
+ _f32_2[1] = hi;
294
+ const bitsLo = _u32_2[0];
295
+ const bitsHi = _u32_2[1];
296
+ let f0 = 0;
297
+ if (lo === 0) f0 = 1 / lo === -Infinity ? 32768 : 0;
298
+ else {
299
+ const sign = bitsLo >>> 16 & 32768;
300
+ const exp = bitsLo >>> 23 & 255;
301
+ const mant = bitsLo & 8388607;
302
+ if (exp > 112 && exp < 143) {
303
+ let m16 = mant >>> 13;
304
+ if ((mant & 4096) !== 0) {
305
+ m16 += 1;
306
+ if ((m16 & 1024) !== 0) f0 = sign | exp - 111 << 10;
307
+ else f0 = sign | exp - 112 << 10 | m16;
308
+ } else f0 = sign | exp - 112 << 10 | m16;
309
+ } else if (exp === 255) f0 = sign | 31744 | (mant !== 0 ? 512 : 0);
310
+ else if (exp <= 112) {
311
+ const e = exp - 112;
312
+ if (e < -10) f0 = sign;
313
+ else {
314
+ let m = (mant | 8388608) >>> 1 - e;
315
+ if ((m & 4096) !== 0) m += 4096 + (m >>> 13 & 1);
316
+ f0 = sign | m >>> 13;
317
+ }
318
+ } else f0 = sign | 31744;
319
+ }
320
+ let f1 = 0;
321
+ if (hi === 0) f1 = 1 / hi === -Infinity ? 32768 : 0;
322
+ else {
323
+ const sign = bitsHi >>> 16 & 32768;
324
+ const exp = bitsHi >>> 23 & 255;
325
+ const mant = bitsHi & 8388607;
326
+ if (exp > 112 && exp < 143) {
327
+ let m16 = mant >>> 13;
328
+ if ((mant & 4096) !== 0) {
329
+ m16 += 1;
330
+ if ((m16 & 1024) !== 0) f1 = sign | exp - 111 << 10;
331
+ else f1 = sign | exp - 112 << 10 | m16;
332
+ } else f1 = sign | exp - 112 << 10 | m16;
333
+ } else if (exp === 255) f1 = sign | 31744 | (mant !== 0 ? 512 : 0);
334
+ else if (exp <= 112) {
335
+ const e = exp - 112;
336
+ if (e < -10) f1 = sign;
337
+ else {
338
+ let m = (mant | 8388608) >>> 1 - e;
339
+ if ((m & 4096) !== 0) m += 4096 + (m >>> 13 & 1);
340
+ f1 = sign | m >>> 13;
341
+ }
342
+ } else f1 = sign | 31744;
343
+ }
344
+ return (f0 | f1 << 16) >>> 0;
345
+ }
346
+ const _bcF32 = new Float32Array(1);
347
+ const _bcU32 = new Uint32Array(_bcF32.buffer);
348
+ /**
349
+ * Reinterpret the bits of a 32-bit float as a u32 (IEEE-754 bitcast).
350
+ * Equivalent to WGSL `bitcast<u32>(value)`. Used to store f32 scalars in
351
+ * u32-typed instance slots (e.g. segment width, arc geometry params).
352
+ */
353
+ function bitcastF32(value) {
354
+ _bcF32[0] = value;
355
+ return _bcU32[0];
356
+ }
357
+ /**
358
+ * Pack four components clamped to [0,1] as a little-endian unorm8x4 u32.
359
+ * Match to WGSL `unpack4x8unorm(value)` which yields `vec4f(r, g, b, a)` with
360
+ * component 0 in the low byte.
361
+ */
362
+ function packUnorm8x4(r, g, b, a) {
363
+ const r8 = r * 255 + .5 | 0;
364
+ const g8 = g * 255 + .5 | 0;
365
+ const b8 = b * 255 + .5 | 0;
366
+ const a8 = a * 255 + .5 | 0;
367
+ return ((r8 < 0 ? 0 : r8 > 255 ? 255 : r8) | (g8 < 0 ? 0 : g8 > 255 ? 255 : g8) << 8 | (b8 < 0 ? 0 : b8 > 255 ? 255 : b8) << 16 | (a8 < 0 ? 0 : a8 > 255 ? 255 : a8) << 24) >>> 0;
368
+ }
369
+ /** Unpack an IEEE-754 f16 bit pattern into a JS number. Inverse of `packF16`. */
370
+ function unpackF16(bits) {
371
+ const sign = (bits & 32768) !== 0 ? -1 : 1;
372
+ const exp = bits >>> 10 & 31;
373
+ const mant = bits & 1023;
374
+ if (exp === 0) return sign * 2 ** -14 * (mant / 1024);
375
+ if (exp === 31) return mant === 0 ? sign * Infinity : NaN;
376
+ return sign * 2 ** (exp - 15) * (1 + mant / 1024);
377
+ }
378
+ //#endregion
379
+ //#region src/schema/codegen.ts
380
+ /** Map a {@link InstanceField} kind to its TypeGPU primitive schema. */
381
+ function tgpuPrimitive(kind) {
382
+ switch (kind) {
383
+ case "vec4f": return d.vec4f;
384
+ case "u32": return d.u32;
385
+ case "f32": return d.f32;
386
+ }
387
+ }
388
+ /**
389
+ * Derive the full instance layout from {@link INSTANCE_FIELDS}. Pure and
390
+ * deterministic — calling it twice yields structurally identical layouts.
391
+ */
392
+ function deriveLayout() {
393
+ let cursor = 0;
394
+ const offsets = {};
395
+ const shape = {};
396
+ for (const field of INSTANCE_FIELDS) {
397
+ offsets[field.name] = {
398
+ floatOffset: cursor,
399
+ byteOffset: cursor * 4
400
+ };
401
+ shape[field.name] = tgpuPrimitive(field.kind);
402
+ cursor += field.floats;
403
+ }
404
+ return {
405
+ byteStride: INSTANCE_BYTES,
406
+ floatStride: INSTANCE_FLOATS,
407
+ offsets,
408
+ tgpuStruct: d.struct(shape),
409
+ wgslAccessor: ""
410
+ };
411
+ }
412
+ /** Eagerly-derived canonical instance layout — the shared v3 contract. */
413
+ const INSTANCE_LAYOUT = deriveLayout();
414
+ //#endregion
415
+ //#region src/kinds/polyline.ts
416
+ function defaultEllipseSegments(rx, ry) {
417
+ return Math.max(16, Math.min(128, 16 + Math.ceil(Math.max(Math.abs(rx), Math.abs(ry)) * .5)));
418
+ }
419
+ /**
420
+ * Generate a closed-loop polyline approximating an ellipse. Vertices are
421
+ * spaced evenly in angle (not arc length) — for small markers this matches
422
+ * SDF rendering to within a sub-pixel. Returns points in CCW math order (which
423
+ * is CW in screen coords where y grows downward). The returned array does NOT
424
+ * include a duplicate closing point — pass `closed: true` to `tessellatePolyline`.
425
+ */
426
+ function polylineEllipseRing(opts) {
427
+ const { cx, cy, rx, ry, rotation = 0 } = opts;
428
+ const segments = opts.segments ?? defaultEllipseSegments(rx, ry);
429
+ if (segments < 3) throw new Error("polylineEllipseRing requires at least 3 segments.");
430
+ const cos = Math.cos(rotation);
431
+ const sin = Math.sin(rotation);
432
+ const out = Array.from({ length: segments });
433
+ for (let i = 0; i < segments; i++) {
434
+ const a = i / segments * TAU;
435
+ const px = Math.cos(a) * rx;
436
+ const py = Math.sin(a) * ry;
437
+ out[i] = {
438
+ x: cx + cos * px - sin * py,
439
+ y: cy + sin * px + cos * py
440
+ };
441
+ }
442
+ return out;
443
+ }
444
+ /**
445
+ * Generate a closed-loop polyline for a rectangle outline. Sharp by default;
446
+ * supply `cornerRadius` to get rounded corners via small arcs at each corner.
447
+ * Like {@link polylineEllipseRing}, the returned array is unclosed — feed it
448
+ * to `tessellatePolyline` with `closed: true`.
449
+ */
450
+ function polylineRectRing(opts) {
451
+ const { x, y, width, height } = opts;
452
+ const cornerRadius = opts.cornerRadius ?? 0;
453
+ if (cornerRadius <= 0) return [
454
+ {
455
+ x,
456
+ y
457
+ },
458
+ {
459
+ x: x + width,
460
+ y
461
+ },
462
+ {
463
+ x: x + width,
464
+ y: y + height
465
+ },
466
+ {
467
+ x,
468
+ y: y + height
469
+ }
470
+ ];
471
+ const r = Math.min(cornerRadius, width * .5, height * .5);
472
+ const cornerSegments = Math.max(2, opts.cornerSegments ?? 4);
473
+ const out = [];
474
+ const corners = [
475
+ {
476
+ cx: x + r,
477
+ cy: y + r,
478
+ a0: Math.PI,
479
+ a1: 1.5 * Math.PI
480
+ },
481
+ {
482
+ cx: x + width - r,
483
+ cy: y + r,
484
+ a0: 1.5 * Math.PI,
485
+ a1: 2 * Math.PI
486
+ },
487
+ {
488
+ cx: x + width - r,
489
+ cy: y + height - r,
490
+ a0: 0,
491
+ a1: .5 * Math.PI
492
+ },
493
+ {
494
+ cx: x + r,
495
+ cy: y + height - r,
496
+ a0: .5 * Math.PI,
497
+ a1: Math.PI
498
+ }
499
+ ];
500
+ for (const c of corners) for (let i = 0; i <= cornerSegments; i++) {
501
+ const t = i / cornerSegments;
502
+ const a = c.a0 + (c.a1 - c.a0) * t;
503
+ out.push({
504
+ x: c.cx + Math.cos(a) * r,
505
+ y: c.cy + Math.sin(a) * r
506
+ });
507
+ }
508
+ return out;
509
+ }
510
+ /**
511
+ * Tessellate a polyline into triangles for GPU rendering.
512
+ * Returns an array of triangles representing the stroke geometry.
513
+ */
514
+ function tessellatePolyline(shape) {
515
+ const width = shape.width ?? 1;
516
+ if (width <= 0 || shape.color.a <= 0) return [];
517
+ const points = shape.points;
518
+ if (points.length < 2) return [];
519
+ assertFinitePoints(points, "Polyline points");
520
+ const closed = shape.closed ?? false;
521
+ const cap = shape.cap ?? "butt";
522
+ const join = shape.join ?? "miter";
523
+ const miterLimit = shape.miterLimit ?? 4;
524
+ const halfWidth = width * .5;
525
+ const dashPattern = shape.dashPattern;
526
+ if (dashPattern && dashPattern.length > 0) return tessellateDashed(points, closed, cap, join, miterLimit, halfWidth, dashPattern, shape.dashOffset ?? 0);
527
+ return tessellateStroke(points, closed, cap, join, miterLimit, halfWidth);
528
+ }
529
+ function tessellateStroke(points, closed, cap, join, miterLimit, halfWidth) {
530
+ const out = [];
531
+ const pts = dedup(points);
532
+ if (pts.length < 2) return out;
533
+ const segCount = closed ? pts.length : pts.length - 1;
534
+ const normals = [];
535
+ for (let i = 0; i < segCount; i++) {
536
+ const next = (i + 1) % pts.length;
537
+ normals.push(segmentNormal(pts[i], pts[next]));
538
+ }
539
+ const entryLeft = [];
540
+ const entryRight = [];
541
+ const exitLeft = [];
542
+ const exitRight = [];
543
+ if (closed) {
544
+ for (let i = 0; i < pts.length; i++) {
545
+ const prevSeg = (i - 1 + segCount) % segCount;
546
+ const nextSeg = i % segCount;
547
+ const joinPts = computeJoin(pts[i], normals[prevSeg], normals[nextSeg], halfWidth, join, miterLimit);
548
+ entryLeft.push(joinPts.entryLeft);
549
+ entryRight.push(joinPts.entryRight);
550
+ exitLeft.push(joinPts.exitLeft);
551
+ exitRight.push(joinPts.exitRight);
552
+ if (joinPts.extraTriangles) for (const tri of joinPts.extraTriangles) out.push(tri);
553
+ }
554
+ for (let i = 0; i < segCount; i++) {
555
+ const next = (i + 1) % pts.length;
556
+ emitQuad(exitLeft[i], exitRight[i], entryLeft[next], entryRight[next], out);
557
+ }
558
+ } else {
559
+ const startCapTris = emitCap(pts[0], normals[0], halfWidth, cap, true);
560
+ for (const tri of startCapTris) out.push(tri);
561
+ const firstL = offset(pts[0], normals[0], halfWidth);
562
+ const firstR = offset(pts[0], normals[0], -halfWidth);
563
+ entryLeft.push(firstL);
564
+ entryRight.push(firstR);
565
+ exitLeft.push(firstL);
566
+ exitRight.push(firstR);
567
+ for (let i = 1; i < pts.length - 1; i++) {
568
+ const joinPts = computeJoin(pts[i], normals[i - 1], normals[i], halfWidth, join, miterLimit);
569
+ entryLeft.push(joinPts.entryLeft);
570
+ entryRight.push(joinPts.entryRight);
571
+ exitLeft.push(joinPts.exitLeft);
572
+ exitRight.push(joinPts.exitRight);
573
+ if (joinPts.extraTriangles) for (const tri of joinPts.extraTriangles) out.push(tri);
574
+ }
575
+ const last = pts.length - 1;
576
+ const lastL = offset(pts[last], normals[segCount - 1], halfWidth);
577
+ const lastR = offset(pts[last], normals[segCount - 1], -halfWidth);
578
+ entryLeft.push(lastL);
579
+ entryRight.push(lastR);
580
+ exitLeft.push(lastL);
581
+ exitRight.push(lastR);
582
+ const endCapTris = emitCap(pts[last], normals[segCount - 1], halfWidth, cap, false);
583
+ for (const tri of endCapTris) out.push(tri);
584
+ for (let i = 0; i < segCount; i++) emitQuad(exitLeft[i], exitRight[i], entryLeft[i + 1], entryRight[i + 1], out);
585
+ }
586
+ return out;
587
+ }
588
+ function tessellateDashed(points, closed, cap, join, miterLimit, halfWidth, dashPattern, dashOffset) {
589
+ const pts = dedup(points);
590
+ if (pts.length < 2) return [];
591
+ const segCount = closed ? pts.length : pts.length - 1;
592
+ const distances = [0];
593
+ for (let i = 0; i < segCount; i++) {
594
+ const next = (i + 1) % pts.length;
595
+ distances.push(distances[i] + dist(pts[i], pts[next]));
596
+ }
597
+ const totalLength = distances[distances.length - 1];
598
+ if (totalLength <= 0) return [];
599
+ let patternLength = 0;
600
+ for (const d of dashPattern) patternLength += Math.abs(d);
601
+ if (patternLength <= 0) return tessellateStroke(pts, closed, cap, join, miterLimit, halfWidth);
602
+ const out = [];
603
+ const patPos = (dashOffset % patternLength + patternLength) % patternLength;
604
+ let dashIdx = 0;
605
+ let consumed = 0;
606
+ for (let i = 0; i < dashPattern.length; i++) {
607
+ const dl = Math.abs(dashPattern[i]);
608
+ if (consumed + dl > patPos) {
609
+ dashIdx = i;
610
+ break;
611
+ }
612
+ consumed += dl;
613
+ if (i === dashPattern.length - 1) {
614
+ dashIdx = 0;
615
+ consumed = 0;
616
+ }
617
+ }
618
+ let remainInDash = Math.abs(dashPattern[dashIdx]) - (patPos - consumed);
619
+ let isDraw = dashIdx % 2 === 0;
620
+ let currentDash = [];
621
+ if (isDraw) currentDash.push(sampleAt(pts, distances, segCount, 0));
622
+ let walkDist = 0;
623
+ for (let seg = 0; seg < segCount; seg++) {
624
+ let segRemain = distances[seg + 1] - distances[seg];
625
+ while (segRemain > 0) {
626
+ const step = Math.min(remainInDash, segRemain);
627
+ walkDist += step;
628
+ segRemain -= step;
629
+ remainInDash -= step;
630
+ if (remainInDash <= 1e-6) {
631
+ if (isDraw) {
632
+ const t = Math.min(walkDist, totalLength);
633
+ currentDash.push(sampleAt(pts, distances, segCount, t));
634
+ if (currentDash.length >= 2) {
635
+ const tris = tessellateStroke(currentDash, false, cap, join, miterLimit, halfWidth);
636
+ for (const tri of tris) out.push(tri);
637
+ }
638
+ currentDash = [];
639
+ }
640
+ dashIdx = (dashIdx + 1) % dashPattern.length;
641
+ isDraw = dashIdx % 2 === 0;
642
+ remainInDash = Math.abs(dashPattern[dashIdx]);
643
+ if (isDraw) currentDash.push(sampleAt(pts, distances, segCount, Math.min(walkDist, totalLength)));
644
+ }
645
+ }
646
+ }
647
+ if (isDraw && currentDash.length >= 1) {
648
+ const endPt = closed ? pts[0] : pts[pts.length - 1];
649
+ if (currentDash.length === 1 && (currentDash[0].x !== endPt.x || currentDash[0].y !== endPt.y)) currentDash.push(endPt);
650
+ if (currentDash.length >= 2) {
651
+ const tris = tessellateStroke(currentDash, false, cap, join, miterLimit, halfWidth);
652
+ for (const tri of tris) out.push(tri);
653
+ }
654
+ }
655
+ return out;
656
+ }
657
+ function computeJoin(point, prevNormal, nextNormal, halfWidth, join, miterLimit) {
658
+ const mx = prevNormal.x + nextNormal.x;
659
+ const my = prevNormal.y + nextNormal.y;
660
+ const mLen = Math.sqrt(mx * mx + my * my);
661
+ if (mLen < 1e-6) {
662
+ const l = offset(point, prevNormal, halfWidth);
663
+ const r = offset(point, prevNormal, -halfWidth);
664
+ return {
665
+ entryLeft: l,
666
+ entryRight: r,
667
+ exitLeft: l,
668
+ exitRight: r
669
+ };
670
+ }
671
+ const miterX = mx / mLen;
672
+ const miterY = my / mLen;
673
+ const dotMN = miterX * prevNormal.x + miterY * prevNormal.y;
674
+ if (Math.abs(dotMN) < 1e-6) {
675
+ const l = offset(point, prevNormal, halfWidth);
676
+ const r = offset(point, prevNormal, -halfWidth);
677
+ return {
678
+ entryLeft: l,
679
+ entryRight: r,
680
+ exitLeft: l,
681
+ exitRight: r
682
+ };
683
+ }
684
+ const miterLen = halfWidth / dotMN;
685
+ const absMiterLen = Math.abs(miterLen);
686
+ const crossVal = prevNormal.x * nextNormal.y - prevNormal.y * nextNormal.x;
687
+ if (join === "miter" && absMiterLen <= halfWidth * miterLimit) {
688
+ const l = {
689
+ x: point.x + miterX * miterLen,
690
+ y: point.y + miterY * miterLen
691
+ };
692
+ const r = {
693
+ x: point.x - miterX * miterLen,
694
+ y: point.y - miterY * miterLen
695
+ };
696
+ return {
697
+ entryLeft: l,
698
+ entryRight: r,
699
+ exitLeft: l,
700
+ exitRight: r
701
+ };
702
+ }
703
+ const prevLeft = offset(point, prevNormal, halfWidth);
704
+ const prevRight = offset(point, prevNormal, -halfWidth);
705
+ const nextLeft = offset(point, nextNormal, halfWidth);
706
+ const nextRight = offset(point, nextNormal, -halfWidth);
707
+ const extraTriangles = [];
708
+ if (crossVal > 0) {
709
+ const innerLeft = {
710
+ x: point.x + miterX * miterLen,
711
+ y: point.y + miterY * miterLen
712
+ };
713
+ if (join === "round") emitArc(innerLeft, point, prevRight, nextRight, halfWidth, extraTriangles);
714
+ else extraTriangles.push(tri(innerLeft, prevRight, nextRight));
715
+ return {
716
+ entryLeft: innerLeft,
717
+ entryRight: prevRight,
718
+ exitLeft: innerLeft,
719
+ exitRight: nextRight,
720
+ extraTriangles
721
+ };
722
+ } else {
723
+ const innerRight = {
724
+ x: point.x - miterX * miterLen,
725
+ y: point.y - miterY * miterLen
726
+ };
727
+ if (join === "round") emitArc(innerRight, point, prevLeft, nextLeft, halfWidth, extraTriangles);
728
+ else extraTriangles.push(tri(innerRight, prevLeft, nextLeft));
729
+ return {
730
+ entryLeft: prevLeft,
731
+ entryRight: innerRight,
732
+ exitLeft: nextLeft,
733
+ exitRight: innerRight,
734
+ extraTriangles
735
+ };
736
+ }
737
+ }
738
+ function emitCap(point, normal, halfWidth, cap, isStart) {
739
+ if (cap === "butt") return [];
740
+ const left = offset(point, normal, halfWidth);
741
+ const right = offset(point, normal, -halfWidth);
742
+ const dir = isStart ? {
743
+ x: -normal.y,
744
+ y: normal.x
745
+ } : {
746
+ x: normal.y,
747
+ y: -normal.x
748
+ };
749
+ if (cap === "square") {
750
+ const extLeft = {
751
+ x: left.x + dir.x * halfWidth,
752
+ y: left.y + dir.y * halfWidth
753
+ };
754
+ const extRight = {
755
+ x: right.x + dir.x * halfWidth,
756
+ y: right.y + dir.y * halfWidth
757
+ };
758
+ return [tri(left, extLeft, extRight), tri(left, extRight, right)];
759
+ }
760
+ const tris = [];
761
+ const segments = Math.max(8, Math.ceil(halfWidth));
762
+ const startAngle = Math.atan2(isStart ? left.y - point.y : right.y - point.y, isStart ? left.x - point.x : right.x - point.x);
763
+ for (let i = 0; i < segments; i++) {
764
+ const a1 = startAngle + Math.PI * i / segments;
765
+ const a2 = startAngle + Math.PI * (i + 1) / segments;
766
+ const p1 = {
767
+ x: point.x + Math.cos(a1) * halfWidth,
768
+ y: point.y + Math.sin(a1) * halfWidth
769
+ };
770
+ const p2 = {
771
+ x: point.x + Math.cos(a2) * halfWidth,
772
+ y: point.y + Math.sin(a2) * halfWidth
773
+ };
774
+ tris.push(tri(point, p1, p2));
775
+ }
776
+ return tris;
777
+ }
778
+ function emitArc(anchor, center, from, to, radius, out) {
779
+ const a1 = Math.atan2(from.y - center.y, from.x - center.x);
780
+ let sweep = Math.atan2(to.y - center.y, to.x - center.x) - a1;
781
+ if (sweep > Math.PI) sweep -= TAU;
782
+ if (sweep < -Math.PI) sweep += TAU;
783
+ const segments = Math.max(Math.ceil(Math.abs(sweep) / (Math.PI / 8)), 2);
784
+ for (let i = 0; i < segments; i++) {
785
+ const t1 = a1 + sweep * i / segments;
786
+ const t2 = a1 + sweep * (i + 1) / segments;
787
+ const p1 = {
788
+ x: center.x + Math.cos(t1) * radius,
789
+ y: center.y + Math.sin(t1) * radius
790
+ };
791
+ const p2 = {
792
+ x: center.x + Math.cos(t2) * radius,
793
+ y: center.y + Math.sin(t2) * radius
794
+ };
795
+ out.push(tri(anchor, p1, p2));
796
+ }
797
+ }
798
+ /**
799
+ * Emit the two triangles for a single stroke segment given its start (left0,
800
+ * right0) and end (left1, right1) offset corners.
801
+ */
802
+ function emitQuad(left0, right0, left1, right1, out) {
803
+ out.push(tri(left0, right0, left1));
804
+ out.push(tri(left1, right0, right1));
805
+ }
806
+ function segmentNormal(a, b) {
807
+ const dx = b.x - a.x;
808
+ const dy = b.y - a.y;
809
+ const len = Math.sqrt(dx * dx + dy * dy);
810
+ if (len === 0) return {
811
+ x: 0,
812
+ y: -1
813
+ };
814
+ return {
815
+ x: -dy / len,
816
+ y: dx / len
817
+ };
818
+ }
819
+ function offset(point, normal, amount) {
820
+ return {
821
+ x: point.x + normal.x * amount,
822
+ y: point.y + normal.y * amount
823
+ };
824
+ }
825
+ function tri(a, b, c) {
826
+ return {
827
+ x1: a.x,
828
+ y1: a.y,
829
+ x2: b.x,
830
+ y2: b.y,
831
+ x3: c.x,
832
+ y3: c.y
833
+ };
834
+ }
835
+ function dist(a, b) {
836
+ const dx = b.x - a.x;
837
+ const dy = b.y - a.y;
838
+ return Math.sqrt(dx * dx + dy * dy);
839
+ }
840
+ function dedup(points) {
841
+ if (points.length === 0) return [];
842
+ const out = [points[0]];
843
+ for (let i = 1; i < points.length; i++) {
844
+ const prev = out[out.length - 1];
845
+ if (Math.abs(points[i].x - prev.x) > 1e-6 || Math.abs(points[i].y - prev.y) > 1e-6) out.push(points[i]);
846
+ }
847
+ return out;
848
+ }
849
+ function assertFinitePoints(points, label) {
850
+ for (const point of points) if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) throw new Error(`${label} must contain only finite coordinates.`);
851
+ }
852
+ function sampleAt(pts, distances, segCount, t) {
853
+ if (t <= 0) return pts[0];
854
+ if (t >= distances[distances.length - 1]) return pts[segCount % pts.length === 0 && segCount > 0 ? 0 : pts.length - 1];
855
+ let lo = 0;
856
+ let hi = segCount;
857
+ while (lo < hi) {
858
+ const mid = lo + hi >>> 1;
859
+ if (distances[mid + 1] < t) lo = mid + 1;
860
+ else hi = mid;
861
+ }
862
+ const segStart = distances[lo];
863
+ const segEnd = distances[lo + 1];
864
+ const frac = segEnd > segStart ? (t - segStart) / (segEnd - segStart) : 0;
865
+ const next = (lo + 1) % pts.length;
866
+ return {
867
+ x: pts[lo].x + (pts[next].x - pts[lo].x) * frac,
868
+ y: pts[lo].y + (pts[next].y - pts[lo].y) * frac
869
+ };
870
+ }
871
+ //#endregion
872
+ //#region src/shared/pack.ts
873
+ /**
874
+ * Bytes per shape instance in the packed vertex buffer. Layout, little-endian:
875
+ * 0: float32 pos.x
876
+ * 4: float32 pos.y
877
+ * 8: float16 size.x
878
+ * 10: float16 size.y
879
+ * 12: u32 fill (unorm8x4; R in low byte)
880
+ * 16: u32 stroke (unorm8x4)
881
+ * 20: float16 rotation
882
+ * 22: float16 strokeWidth
883
+ * 24: float16 cornerRadius
884
+ * 26: u16 _pad (zero)
885
+ * 28: u32 shapeType
886
+ */
887
+ const SHAPE_BYTES = 32;
888
+ const TRIANGLE_FLOATS = 12;
889
+ const SPRITE_FLOATS = 16;
890
+ /**
891
+ * Floats per segment instance in the packed segment buffer (24 B total).
892
+ * Layout:
893
+ * float 0: start.x (f32)
894
+ * float 1: start.y (f32)
895
+ * float 2: end.x (f32)
896
+ * float 3: end.y (f32)
897
+ * float 4: colorBits (u32 reinterpret; straight RGBA unorm8x4)
898
+ * float 5: width (f32; layer-local units)
899
+ *
900
+ * Color is stored unpremultiplied (matching the shape pipeline); the shader
901
+ * multiplies by alpha at output to produce premultiplied blended results.
902
+ */
903
+ const SEGMENT_FLOATS = 6;
904
+ /**
905
+ * Floats per curve instance in the packed curve buffer (48 B total — a
906
+ * multiple of 16 B for storage-buffer struct-array alignment). Layout:
907
+ * float 0..1: p0.xy (f32)
908
+ * float 2..3: p1.xy (f32)
909
+ * float 4..5: p2.xy (f32)
910
+ * float 6..7: p3.xy (f32)
911
+ * float 8: colorBits (u32 reinterpret; unorm8x4 RGBA, unpremultiplied)
912
+ * float 9: widthPacked (u32 reinterpret; pack2x16float(widthStart, widthEnd))
913
+ * float 10: flagsBits (u32 reinterpret; cap style in low 2 bits, rest reserved)
914
+ * float 11: _pad (zero)
915
+ *
916
+ * widthPacked carries two f16 widths from day one so curve taper can land
917
+ * later without a schema migration. v1 callers pass the same width for both.
918
+ * flagsBits reserves the remaining 30 bits for dash / phase / outline.
919
+ */
920
+ const CURVE_FLOATS = 12;
921
+ const CURVE_CAP_ROUND = 0;
922
+ const CURVE_CAP_BUTT = 1;
923
+ const CURVE_CAP_SQUARE = 2;
924
+ /**
925
+ * Floats per arc instance in the packed arc buffer (32 B total — multiple
926
+ * of 16 B for storage-buffer struct-array alignment). Layout:
927
+ * float 0..1: center.xy (f32)
928
+ * float 2: radius (f32)
929
+ * float 3: theta0 (f32; radians)
930
+ * float 4: theta1 (f32; radians)
931
+ * float 5: width (f32; layer-local units)
932
+ * float 6: colorBits (u32 reinterpret; unorm8x4 RGBA, unpremultiplied)
933
+ * float 7: _pad (zero)
934
+ *
935
+ * Arc is the short path from `theta0` to `theta1`; rendering depends on the
936
+ * absolute span only. Use the {@link ArcShape} interface — the layer/pack
937
+ * normalises into this form. Butt caps; no joins.
938
+ */
939
+ const ARC_FLOATS = 8;
940
+ const SHAPE_RECT = 0;
941
+ const SHAPE_CIRCLE = 1;
942
+ const SHAPE_ELLIPSE = 2;
943
+ //#endregion
944
+ //#region src/pipelines/pipelines.ts
945
+ const CLEAR_QUAD_WGSL = `
946
+ struct ClearOut {
947
+ @builtin(position) pos: vec4f,
948
+ };
949
+
950
+ @vertex
951
+ fn vsMain(@builtin(vertex_index) vi: u32) -> ClearOut {
952
+ var p = vec2f(-1.0, -1.0);
953
+ if (vi == 1u) { p = vec2f(3.0, -1.0); }
954
+ else if (vi == 2u) { p = vec2f(-1.0, 3.0); }
955
+ return ClearOut(vec4f(p, 1.0, 1.0));
956
+ }
957
+
958
+ @group(0) @binding(0) var<uniform> clearColor: vec4f;
959
+
960
+ @fragment
961
+ fn fsMain() -> @location(0) vec4f {
962
+ return clearColor;
963
+ }
964
+ `;
965
+ /**
966
+ * Construct the three v3 render pipeline variants.
967
+ *
968
+ * @param root TypeGPU root (provides `root.device`).
969
+ * @param format Swapchain texture format.
970
+ * @param shader Assembled uber-shader from {@link assembleShader}.
971
+ */
972
+ function createPipelines(root, format, shader) {
973
+ const device = root.device;
974
+ const vertexModule = device.createShaderModule({ code: shader.vertex });
975
+ const opaqueModule = device.createShaderModule({ code: shader.opaque });
976
+ const transparentModule = device.createShaderModule({ code: shader.transparent });
977
+ const clearModule = device.createShaderModule({ code: CLEAR_QUAD_WGSL });
978
+ const depthBase = {
979
+ format: DEPTH_FORMAT,
980
+ depthWriteEnabled: true,
981
+ depthCompare: DEPTH_COMPARE
982
+ };
983
+ const cameraLayout = device.createBindGroupLayout({
984
+ label: "insomni-v3-camera",
985
+ entries: [{
986
+ binding: 0,
987
+ visibility: 1,
988
+ buffer: { type: "uniform" }
989
+ }, {
990
+ binding: 1,
991
+ visibility: 2,
992
+ buffer: { type: "uniform" }
993
+ }]
994
+ });
995
+ const instanceLayout = device.createBindGroupLayout({
996
+ label: "insomni-v3-instances",
997
+ entries: [{
998
+ binding: 0,
999
+ visibility: 3,
1000
+ buffer: { type: "read-only-storage" }
1001
+ }]
1002
+ });
1003
+ const sharedLayout = device.createPipelineLayout({
1004
+ label: "insomni-v3-shared",
1005
+ bindGroupLayouts: [cameraLayout, instanceLayout]
1006
+ });
1007
+ return {
1008
+ opaque: device.createRenderPipeline({
1009
+ layout: sharedLayout,
1010
+ primitive: { topology: "triangle-strip" },
1011
+ vertex: {
1012
+ module: vertexModule,
1013
+ entryPoint: "main"
1014
+ },
1015
+ fragment: {
1016
+ module: opaqueModule,
1017
+ entryPoint: "main",
1018
+ targets: [{ format }]
1019
+ },
1020
+ depthStencil: depthBase,
1021
+ multisample: { count: 1 }
1022
+ }),
1023
+ transparent: device.createRenderPipeline({
1024
+ layout: sharedLayout,
1025
+ primitive: { topology: "triangle-strip" },
1026
+ vertex: {
1027
+ module: vertexModule,
1028
+ entryPoint: "main"
1029
+ },
1030
+ fragment: {
1031
+ module: transparentModule,
1032
+ entryPoint: "main",
1033
+ targets: [{
1034
+ format,
1035
+ blend: ALPHA_BLEND
1036
+ }]
1037
+ },
1038
+ depthStencil: {
1039
+ ...depthBase,
1040
+ depthWriteEnabled: false
1041
+ },
1042
+ multisample: { count: 1 }
1043
+ }),
1044
+ clearQuad: device.createRenderPipeline({
1045
+ layout: "auto",
1046
+ primitive: { topology: "triangle-list" },
1047
+ vertex: {
1048
+ module: clearModule,
1049
+ entryPoint: "vsMain"
1050
+ },
1051
+ fragment: {
1052
+ module: clearModule,
1053
+ entryPoint: "fsMain",
1054
+ targets: [{ format }]
1055
+ },
1056
+ depthStencil: {
1057
+ format: DEPTH_FORMAT,
1058
+ depthWriteEnabled: true,
1059
+ depthCompare: "always"
1060
+ },
1061
+ multisample: { count: 1 }
1062
+ }),
1063
+ cameraLayout,
1064
+ instanceLayout
1065
+ };
1066
+ }
1067
+ //#endregion
1068
+ //#region src/shader/assemble.ts
1069
+ /**
1070
+ * Pure-math WGSL helpers (`rot2`, the `sdf*` SDF primitives, `cubicAABB1D`,
1071
+ * `arcLocalAabb`, `sdSegment`, `joinOvershoot`, …) referenced by the kinds'
1072
+ * `wgslGeom()` / `wgslSdf()` contributions. Exported so the EXTERNAL-INSTANCE
1073
+ * pipeline (`pipelines/instance-pipelines.ts`) re-emits the SAME helpers around
1074
+ * the SAME kind contributions — sharing this one string keeps an external buffer
1075
+ * decoding byte-identically to the live uber path with no duplicate to drift.
1076
+ */
1077
+ const WGSL_HELPERS = `fn rot2(v: vec2f, a: f32) -> vec2f {
1078
+ let c = cos(a);
1079
+ let s = sin(a);
1080
+ return vec2f(c * v.x - s * v.y, s * v.x + c * v.y);
1081
+ }
1082
+
1083
+ fn sdfRoundedBox(p: vec2f, b: vec2f, r: f32) -> f32 {
1084
+ let q = abs(p) - b + r;
1085
+ return length(max(q, vec2f(0.0))) + min(max(q.x, q.y), 0.0) - r;
1086
+ }
1087
+
1088
+ fn sdfCircle(p: vec2f, r: f32) -> f32 {
1089
+ return length(p) - r;
1090
+ }
1091
+
1092
+ fn sdfEllipse(p: vec2f, ab: vec2f) -> f32 {
1093
+ let k1 = length(p / ab);
1094
+ let k2 = length(p / (ab * ab));
1095
+ return k1 * (k1 - 1.0) / k2;
1096
+ }
1097
+
1098
+ fn cubicAABB1D(c0: f32, c1: f32, c2: f32, c3: f32) -> vec2f {
1099
+ var mn = min(c0, c3);
1100
+ var mx = max(c0, c3);
1101
+ let alpha = -c0 + 3.0 * c1 - 3.0 * c2 + c3;
1102
+ let beta = c0 - 2.0 * c1 + c2;
1103
+ let gamma = -c0 + c1;
1104
+ if (abs(alpha) < 1e-6) {
1105
+ if (abs(beta) > 1e-6) {
1106
+ let t = -gamma / (2.0 * beta);
1107
+ if (t > 0.0 && t < 1.0) {
1108
+ let u = 1.0 - t;
1109
+ let v = u * u * u * c0 + 3.0 * u * u * t * c1 + 3.0 * u * t * t * c2 + t * t * t * c3;
1110
+ mn = min(mn, v);
1111
+ mx = max(mx, v);
1112
+ }
1113
+ }
1114
+ } else {
1115
+ let disc = beta * beta - alpha * gamma;
1116
+ if (disc >= 0.0) {
1117
+ let s = sqrt(disc);
1118
+ let t1 = (-beta + s) / alpha;
1119
+ if (t1 > 0.0 && t1 < 1.0) {
1120
+ let u = 1.0 - t1;
1121
+ let v = u * u * u * c0 + 3.0 * u * u * t1 * c1 + 3.0 * u * t1 * t1 * c2 + t1 * t1 * t1 * c3;
1122
+ mn = min(mn, v);
1123
+ mx = max(mx, v);
1124
+ }
1125
+ let t2 = (-beta - s) / alpha;
1126
+ if (t2 > 0.0 && t2 < 1.0) {
1127
+ let u = 1.0 - t2;
1128
+ let v = u * u * u * c0 + 3.0 * u * u * t2 * c1 + 3.0 * u * t2 * t2 * c2 + t2 * t2 * t2 * c3;
1129
+ mn = min(mn, v);
1130
+ mx = max(mx, v);
1131
+ }
1132
+ }
1133
+ }
1134
+ return vec2f(mn, mx);
1135
+ }
1136
+
1137
+ fn arcLocalAabb(r: f32, halfAng: f32, halfW: f32, pad: f32) -> vec4f {
1138
+ let outer = r + halfW + pad;
1139
+ let innerRaw = r - halfW - pad;
1140
+ let inner = select(0.0, innerRaw, innerRaw > 0.0);
1141
+ let ch = cos(halfAng);
1142
+ let sh = sin(halfAng);
1143
+ let xMax = outer;
1144
+ var xMin: f32;
1145
+ var yMax: f32;
1146
+ var yMin: f32;
1147
+ let PI = 3.141592653589793;
1148
+ let HALF_PI = 1.5707963267948966;
1149
+ if (halfAng >= PI) {
1150
+ xMin = -outer;
1151
+ yMax = outer;
1152
+ yMin = -outer;
1153
+ } else if (halfAng >= HALF_PI) {
1154
+ yMax = outer;
1155
+ yMin = -outer;
1156
+ xMin = outer * ch;
1157
+ } else {
1158
+ yMax = outer * sh;
1159
+ yMin = -yMax;
1160
+ xMin = inner * ch;
1161
+ }
1162
+ return vec4f(xMin, yMin, xMax, yMax);
1163
+ }
1164
+
1165
+ fn sdfArcCurve(p: vec2f, r: f32, mid: f32, halfAng: f32) -> f32 {
1166
+ let c = cos(mid);
1167
+ let s = sin(mid);
1168
+ let q = vec2f(c * p.x + s * p.y, -s * p.x + c * p.y);
1169
+ let qa = vec2f(q.x, abs(q.y));
1170
+ let ch = cos(halfAng);
1171
+ let sh = sin(halfAng);
1172
+ let cross = ch * qa.y - sh * qa.x;
1173
+ if (cross <= 0.0) {
1174
+ return abs(length(qa) - r);
1175
+ }
1176
+ let endpoint = vec2f(r * ch, r * sh);
1177
+ return length(qa - endpoint);
1178
+ }
1179
+
1180
+ // Unsigned distance from point \`p\` to the segment [a,b]. Used by the
1181
+ // stroke-path kind (min over a polyline's sub-segments). Classic IQ form.
1182
+ fn sdSegment(p: vec2f, a: vec2f, b: vec2f) -> f32 {
1183
+ let pa = p - a;
1184
+ let ba = b - a;
1185
+ let h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-12), 0.0, 1.0);
1186
+ return length(pa - ba * h);
1187
+ }
1188
+
1189
+ // Decide whether a fragment in the round overshoot of an interior vertex \`b\`
1190
+ // (neighbors \`a\`,\`c\`) should be DISCARDED to reshape the FREE round join into a
1191
+ // bevel or miter. The min-of-segment SDF already yields a round join (a halfW
1192
+ // disc at \`b\`); this trims that disc on the OUTER side of the corner. Returns
1193
+ // \`true\` when the caller should drop the fragment. (The drop itself stays in
1194
+ // the kind's fragment snippet — helpers must remain side-effect-free pure math.)
1195
+ //
1196
+ // join: 1 = bevel, 2 = miter (0 = round → caller skips this call entirely).
1197
+ //
1198
+ // Geometry: \`din\`/\`dout\` are the incoming/outgoing unit tangents; \`nin\`/\`nout\`
1199
+ // the corresponding left normals. The outward bisector \`m\` points along the
1200
+ // outer side. The bevel chord (line through the two outer offset corners) sits
1201
+ // at signed distance \`halfW * cosHalf\` from \`b\` along \`m\`; the miter apex sits
1202
+ // at \`halfW / cosHalf\`. A fragment is only in the overshoot region when it is
1203
+ // on the outer side of BOTH segment offset edges (i.e. the min-distance closest
1204
+ // point is the shared vertex \`b\`, not a segment interior). Clipping outside the
1205
+ // chord/apex there reshapes the disc into a bevel/miter without touching the
1206
+ // segment bodies.
1207
+ fn joinOvershoot(p: vec2f, a: vec2f, b: vec2f, c: vec2f, halfW: f32, join: u32, miterLimit: f32) -> bool {
1208
+ // Zero-length guard: a degenerate incoming or outgoing segment would feed
1209
+ // \`normalize(0)\` → NaN below. Coincident points have no join to clip, so bail.
1210
+ let ein = b - a;
1211
+ let eout = c - b;
1212
+ if (dot(ein, ein) < 1e-12 || dot(eout, eout) < 1e-12) { return false; }
1213
+ let din = normalize(b - a);
1214
+ let dout = normalize(c - b);
1215
+ // Degenerate (collinear / zero-length) → nothing to clip.
1216
+ let cosTurn = dot(din, dout);
1217
+ if (cosTurn > 0.99999) { return false; }
1218
+ // Left normals.
1219
+ let nin = vec2f(-din.y, din.x);
1220
+ let nout = vec2f(-dout.y, dout.x);
1221
+ // Outward side: the turn direction. cross > 0 = left turn ⇒ outer side is the
1222
+ // RIGHT (-normal) side; cross < 0 ⇒ outer side is the LEFT (+normal) side.
1223
+ let crossv = din.x * dout.y - din.y * dout.x;
1224
+ let sgn = select(1.0, -1.0, crossv > 0.0);
1225
+ // Outer offset normals (point toward the outer side).
1226
+ let onin = nin * sgn;
1227
+ let onout = nout * sgn;
1228
+ // The outward bisector (sum of the two outer normals, renormalized).
1229
+ let bis = onin + onout;
1230
+ let bisLen = length(bis);
1231
+ if (bisLen < 1e-6) { return false; }
1232
+ let m = bis / bisLen;
1233
+ // Only clip fragments in the join wedge: on the outer side past BOTH offset
1234
+ // edges. \`pb = p - b\`. Past the incoming offset edge: dot(pb, onin) > halfW;
1235
+ // past the outgoing: dot(pb, onout) > halfW. Inside the wedge both hold.
1236
+ let pb = p - b;
1237
+ let dIn = dot(pb, onin);
1238
+ let dOut = dot(pb, onout);
1239
+ if (dIn <= halfW || dOut <= halfW) { return false; }
1240
+ // cosHalf = cos of half the exterior angle = dot(m, onin).
1241
+ let cosHalf = max(dot(m, onin), 1e-4);
1242
+ let along = dot(pb, m); // distance from b along the outward bisector
1243
+ if (join == 1u) {
1244
+ // Bevel: clip past the chord through the two outer corners.
1245
+ return along > halfW * cosHalf;
1246
+ }
1247
+ // Miter: extend to the apex unless it exceeds the limit (then bevel).
1248
+ let miterDist = halfW / cosHalf; // apex distance along m
1249
+ let limitDist = halfW * miterLimit; // miterLimit is a ratio of halfW
1250
+ let cutoff = select(halfW * cosHalf, miterDist, miterDist <= limitDist);
1251
+ return along > cutoff;
1252
+ }`;
1253
+ /**
1254
+ * The emphasis uniform + its `@group(0) @binding(1)` declaration, shared by the
1255
+ * `transparent` and OIT-build fragments (Phase 4, decisions.md D2). It rides on
1256
+ * the camera group — `@group(0)` is already in both pipeline layouts and the
1257
+ * camera bind group binds it per slot — so no new bind group is introduced. The
1258
+ * VERTEX-only camera lives at `@binding(0)`; this is FRAGMENT-visible at
1259
+ * `@binding(1)`.
1260
+ *
1261
+ * focusedKey — instances whose `lane4` equals this render at FULL alpha.
1262
+ * dimAlpha — the alpha multiplier applied to NON-focused instances.
1263
+ * t — emphasis amount in [0,1]; `t == 0` is an exact no-op
1264
+ * (`mix(1, dimAlpha, 0) == 1`), so emphasis-inactive frames are
1265
+ * byte-identical to today and cost one multiply by 1.0.
1266
+ */
1267
+ const EMPHASIS_BINDING_WGSL = `struct Emphasis {
1268
+ focusedKey: u32,
1269
+ dimAlpha: f32,
1270
+ t: f32,
1271
+ _pad: u32,
1272
+ };
1273
+
1274
+ @group(0) @binding(1) var<uniform> emphasis: Emphasis;`;
1275
+ /**
1276
+ * Apply the global-dim emphasis to `outColor`. `outColor` is premultiplied
1277
+ * (every kind emits premultiplied RGBA), and scaling all four channels by one
1278
+ * factor preserves premultiplication — so this is correct for both the hardware
1279
+ * alpha-blend (transparent) and the A-buffer over-composite (OIT build). The
1280
+ * focused key keeps full alpha; everything else is multiplied by
1281
+ * `mix(1, dimAlpha, t)`. Reads `inst.lane4` (the per-instance emphasis key).
1282
+ *
1283
+ * EXEMPT-ZERO semantics: `lane4 == 0u` (the default `UberPack.append` writes) is
1284
+ * NON-participating — those instances are NEVER dimmed, regardless of `t`. An
1285
+ * instance OPTS IN to emphasis by tagging a key ≥ 1; this keeps axis/grid/overlay
1286
+ * shapes (which never set a key) at full alpha during chart-mark emphasis.
1287
+ * Consequently `focusedKey: 0, t: 1` means "dim ALL tagged instances, focus none"
1288
+ * — key 0 cannot be a focus target, it is the opt-out sentinel.
1289
+ */
1290
+ const EMPHASIS_APPLY_WGSL = ` let _emFocused = inst.lane4 == emphasis.focusedKey || inst.lane4 == 0u;
1291
+ let _emDim = select(mix(1.0, emphasis.dimAlpha, emphasis.t), 1.0, _emFocused);
1292
+ outColor = outColor * _emDim;`;
1293
+ /** Map an {@link INSTANCE_FIELDS} kind to its WGSL scalar/vector type. */
1294
+ function wgslType(kind) {
1295
+ switch (kind) {
1296
+ case "vec4f": return "vec4f";
1297
+ case "u32": return "u32";
1298
+ case "f32": return "f32";
1299
+ }
1300
+ }
1301
+ /**
1302
+ * `struct InstanceStruct { ... }` emitted from the SAME {@link INSTANCE_FIELDS}
1303
+ * array the CPU offsets and the TypeGPU struct derive from. The kinds read
1304
+ * instance fields by these exact names (`inst.geom0`, `inst.lane3`, …);
1305
+ * declaring the struct here from the schema array — never a hand-typed copy —
1306
+ * keeps the WGSL field names locked to the schema. The accessor snippet
1307
+ * (caller-supplied, from `codegen.wgslAccessor()`) references this struct.
1308
+ *
1309
+ * Exported so the glyph bake pipeline (`pipelines/glyph-pipelines.ts`) can
1310
+ * interpolate the SAME struct text into its vertex stage instead of carrying a
1311
+ * hand-copied duplicate that could silently drift from the schema layout.
1312
+ */
1313
+ const INSTANCE_STRUCT_WGSL = [
1314
+ "struct InstanceStruct {",
1315
+ ...INSTANCE_FIELDS.map((f) => ` ${f.name}: ${wgslType(f.kind)},`),
1316
+ "}"
1317
+ ].join("\n");
1318
+ /** The dispatch chain over `typeFlag`: one `if` branch per kind, bodies joined
1319
+ * by `else`. Each kind's snippet is an already-braced block, so the chain reads
1320
+ * `if (typeFlag == 0u) { … } else if (typeFlag == 1u) { … }`. */
1321
+ function dispatch(kinds, body) {
1322
+ return kinds.map((k) => `if (typeFlag == ${k.typeFlag}u) ${body(k)}`).join(" else ");
1323
+ }
1324
+ /**
1325
+ * Vertex-stage WGSL: schema struct + accessor + math helpers, then a
1326
+ * `@vertex fn main` that reads `typeFlag`, sets up the unit quad `q`, runs the
1327
+ * per-kind geometry switch (which writes `world` + `v0` + `v1`), applies the
1328
+ * camera transform, and returns clip-space `pos` plus the varyings. Mirrors v2's
1329
+ * varyings layout (`varying0: vec4f`, `varying1: vec4f`, flat `instanceIdx`).
1330
+ */
1331
+ function buildVertex(geomCases) {
1332
+ return `${INSTANCE_STRUCT_WGSL}
1333
+
1334
+ struct Camera {
1335
+ vpCol0: vec2f,
1336
+ vpCol1: vec2f,
1337
+ vpCol2: vec2f,
1338
+ };
1339
+
1340
+ @group(0) @binding(0) var<uniform> camera: Camera;
1341
+ @group(1) @binding(0) var<storage, read> instances: array<InstanceStruct>;
1342
+
1343
+ struct VertexOut {
1344
+ @builtin(position) pos: vec4f,
1345
+ @location(0) varying0: vec4f,
1346
+ @location(1) varying1: vec4f,
1347
+ @location(2) @interpolate(flat) instanceIdx: u32,
1348
+ };
1349
+
1350
+ // The geom snippets are authored to read \`in.vertexIndex\` (the same name they
1351
+ // use in the v2-OIT framing). \`VertexIn\` lets us alias the entry-point builtins
1352
+ // behind that one field without rewriting every kind.
1353
+ struct VertexIn {
1354
+ vertexIndex: u32,
1355
+ instanceIndex: u32,
1356
+ };
1357
+
1358
+ ${WGSL_HELPERS}
1359
+
1360
+ @vertex
1361
+ fn main(@builtin(vertex_index) vertexIndex: u32, @builtin(instance_index) instanceIndex: u32) -> VertexOut {
1362
+ let inst = instances[instanceIndex];
1363
+ let typeFlag = inst.typeFlag;
1364
+
1365
+ // Per-vertex quad corner. Shape kinds scale it to half-extents; the segment,
1366
+ // curve, arc, and triangle kinds re-derive their own per-vertex layout from
1367
+ // \`in.vertexIndex\` and ignore \`q\`.
1368
+ let quad = array<vec2f, 4>(
1369
+ vec2f(-1.0, -1.0),
1370
+ vec2f( 1.0, -1.0),
1371
+ vec2f(-1.0, 1.0),
1372
+ vec2f( 1.0, 1.0),
1373
+ );
1374
+ let q = quad[vertexIndex];
1375
+
1376
+ // \`in\` aliases the entry-point builtins so the kind snippets (authored to read
1377
+ // \`in.vertexIndex\`) compile unchanged in both the v3 and v2-OIT framings.
1378
+ let in = VertexIn(vertexIndex, instanceIndex);
1379
+
1380
+ var world: vec2f = vec2f(0.0);
1381
+ var v0: vec4f = vec4f(0.0);
1382
+ var v1: vec4f = vec4f(0.0);
1383
+
1384
+ ${geomCases}
1385
+
1386
+ let ndc = camera.vpCol0 * world.x + camera.vpCol1 * world.y + camera.vpCol2;
1387
+ // Explicit depth from the instance \`order\` field — never index-derived (v2
1388
+ // used \`draw.baseZ + zStep * index\`, which coupled depth to draw count).
1389
+ var out: VertexOut;
1390
+ out.pos = vec4f(ndc, inst.order, 1.0);
1391
+ out.varying0 = v0;
1392
+ out.varying1 = v1;
1393
+ out.instanceIdx = instanceIndex;
1394
+ return out;
1395
+ }`;
1396
+ }
1397
+ /**
1398
+ * Fragment-stage WGSL up to (but NOT including) the terminal `return outColor;`
1399
+ * — the opaque/transparent suffix is appended by {@link assembleShader}.
1400
+ *
1401
+ * The `fwidth` derivatives are computed at the absolute top of `main`, BEFORE
1402
+ * the non-uniform `typeFlag` switch, so analytic SDF AA (`pixelLocal` /
1403
+ * `pixelLocalX`) is well-defined under WGSL's uniformity rules — exactly the
1404
+ * hoist both the opaque and OIT variants rely on.
1405
+ */
1406
+ function buildFragBody(sdfCases, withEmphasis) {
1407
+ return `${INSTANCE_STRUCT_WGSL}
1408
+
1409
+ @group(1) @binding(0) var<storage, read> instances: array<InstanceStruct>;
1410
+ ${withEmphasis ? `\n${EMPHASIS_BINDING_WGSL}\n` : ""}
1411
+ struct FragmentIn {
1412
+ @location(0) varying0: vec4f,
1413
+ @location(1) varying1: vec4f,
1414
+ @location(2) @interpolate(flat) instanceIdx: u32,
1415
+ };
1416
+
1417
+ ${WGSL_HELPERS}
1418
+
1419
+ @fragment
1420
+ fn main(in: FragmentIn) -> @location(0) vec4f {
1421
+ // Hoist derivatives to the top of \`main\` — guaranteed-uniform control flow —
1422
+ // so the analytic-AA pixel sizes are valid before the non-uniform switch.
1423
+ let dV0 = fwidth(in.varying0.xy);
1424
+ let pixelLocal = max(length(dV0), 1e-6);
1425
+ let pixelLocalX = max(abs(dV0.x), 1e-6);
1426
+
1427
+ let inst = instances[in.instanceIdx];
1428
+ let typeFlag = inst.typeFlag;
1429
+ var outColor: vec4f = vec4f(0.0);
1430
+
1431
+ ${sdfCases}`;
1432
+ }
1433
+ /**
1434
+ * Fragment-stage WGSL for the OIT (A-buffer) build pass.
1435
+ *
1436
+ * The geometry/SDF body is IDENTICAL to {@link buildFragBody} — it reuses the
1437
+ * same `INSTANCE_STRUCT_WGSL`, the same `accessorWgsl`, the same `WGSL_HELPERS`,
1438
+ * and the same per-kind `sdfCases` switch — so the `outColor` math cannot drift
1439
+ * from the transparent variant (the exact v2 OIT bug this rework fixes). The
1440
+ * differences are I/O only:
1441
+ *
1442
+ * - `FragmentIn` gains `@builtin(position) pos: vec4f` (to compute the integer
1443
+ * pixel index). The opaque/transparent input drops it because hardware
1444
+ * blending never needs the pixel coordinate.
1445
+ * - Three extra `@group(2)` bindings declare the A-buffer (heads / slots /
1446
+ * oitViewport). Groups 0 (camera, vertex-only) and 1 (instances) are
1447
+ * unchanged, so the shared `vertex` source feeds this pass verbatim.
1448
+ * - The trailing suffix does the atomic per-pixel slot reserve + write-once
1449
+ * store instead of `return outColor;`. The slot index is
1450
+ * `pixelIdx * K + slotIdx`; fragments past the per-pixel budget K are
1451
+ * dropped (the resolve pass reads `min(count, K)` slots). Color is masked
1452
+ * off by the pipeline `writeMask:0`, so the returned `vec4f(0.0)` is inert.
1453
+ */
1454
+ function buildOitFragment(sdfCases) {
1455
+ return `${INSTANCE_STRUCT_WGSL}
1456
+
1457
+ struct OITNode {
1458
+ rgba: u32,
1459
+ depth: f32,
1460
+ };
1461
+
1462
+ struct OITViewport {
1463
+ width: u32,
1464
+ height: u32,
1465
+ K: u32,
1466
+ _pad: u32,
1467
+ };
1468
+
1469
+ @group(1) @binding(0) var<storage, read> instances: array<InstanceStruct>;
1470
+ ${EMPHASIS_BINDING_WGSL}
1471
+ @group(2) @binding(0) var<storage, read_write> heads: array<atomic<u32>>;
1472
+ @group(2) @binding(1) var<storage, read_write> slots: array<OITNode>;
1473
+ @group(2) @binding(2) var<uniform> oitViewport: OITViewport;
1474
+ @group(2) @binding(3) var opaqueDepth: texture_depth_2d;
1475
+
1476
+ struct FragmentIn {
1477
+ @builtin(position) pos: vec4f,
1478
+ @location(0) varying0: vec4f,
1479
+ @location(1) varying1: vec4f,
1480
+ @location(2) @interpolate(flat) instanceIdx: u32,
1481
+ };
1482
+
1483
+ ${WGSL_HELPERS}
1484
+
1485
+ @fragment
1486
+ fn main(in: FragmentIn) -> @location(0) vec4f {
1487
+ // Identical derivative hoist + SDF switch as the transparent variant.
1488
+ let dV0 = fwidth(in.varying0.xy);
1489
+ let pixelLocal = max(length(dV0), 1e-6);
1490
+ let pixelLocalX = max(abs(dV0.x), 1e-6);
1491
+
1492
+ let inst = instances[in.instanceIdx];
1493
+ let typeFlag = inst.typeFlag;
1494
+ var outColor: vec4f = vec4f(0.0);
1495
+
1496
+ ${sdfCases}
1497
+
1498
+ ${EMPHASIS_APPLY_WGSL}
1499
+
1500
+ // ---- A-buffer insert (this is the only divergence from \`transparent\`) ----
1501
+ // \`outColor\` is already premultiplied (every kind emits premultiplied RGBA),
1502
+ // matching the resolve pass's premultiplied over-composite.
1503
+ if (outColor.a >= 0.005) {
1504
+ let px = u32(in.pos.x);
1505
+ let py = u32(in.pos.y);
1506
+ if (px < oitViewport.width && py < oitViewport.height) {
1507
+ // Opaque-occlusion gate (byte-identical to oitAppend in src/oit/append-snippet.ts).
1508
+ // The late depth test gates only attachment writes, not this storage append,
1509
+ // so gate it in software: skip the append when this fragment is behind the
1510
+ // opaque depth. depth24plus → texture_depth_2d; textureLoad returns scalar f32
1511
+ // (no .r). Cleared depth = 1.0 → fragZ < 1.0 appends. Skip when fragZ >= opaqueZ.
1512
+ let opaqueZ = textureLoad(opaqueDepth, vec2i(i32(px), i32(py)), 0);
1513
+ if (in.pos.z >= opaqueZ) { return vec4f(0.0); }
1514
+ let pixelIdx = py * oitViewport.width + px;
1515
+ // Per-pixel slot allocation: atomicAdd returns this fragment's slot index.
1516
+ // Fragments past K are dropped — saturation is local to a pixel, so a
1517
+ // heavy pixel loses detail without blanking neighboring rasterizer tiles.
1518
+ // No global counter / no freelist; slot \`pixelIdx*K + slotIdx\` is written
1519
+ // exactly once (slotIdx is unique per fragment), so slot stores never race.
1520
+ //
1521
+ // BOUNDED-K DROP RULE: slots are claimed in RASTERIZATION (allocation)
1522
+ // order, NOT depth order, and the K-cap drops everything past slot K. So
1523
+ // when >K transparent fragments overlap ONE pixel (e.g. a hover-dimmed
1524
+ // joyplot whose forced-transparent rows stack deeper than K — see
1525
+ // classifyOpaque), a NEAR fragment can be dropped while a FAR one survives:
1526
+ // a far row bleeds through. The fragments that DO survive still composite in
1527
+ // correct depth order (the resolve pass sorts the kept slots). Relief valve:
1528
+ // raise \`RendererConfig.oitFragmentsPerPixel\` (this K).
1529
+ let slotIdx = atomicAdd(&heads[pixelIdx], 1u);
1530
+ if (slotIdx < oitViewport.K) {
1531
+ let r = u32(clamp(outColor.r, 0.0, 1.0) * 255.0 + 0.5);
1532
+ let g = u32(clamp(outColor.g, 0.0, 1.0) * 255.0 + 0.5);
1533
+ let b = u32(clamp(outColor.b, 0.0, 1.0) * 255.0 + 0.5);
1534
+ let a = u32(clamp(outColor.a, 0.0, 1.0) * 255.0 + 0.5);
1535
+ let packed = r | (g << 8u) | (b << 16u) | (a << 24u);
1536
+ let base = pixelIdx * oitViewport.K + slotIdx;
1537
+ slots[base].rgba = packed;
1538
+ slots[base].depth = in.pos.z;
1539
+ }
1540
+ }
1541
+ }
1542
+ // Color writeMask is 0 in the build pipeline target; this is discarded.
1543
+ return vec4f(0.0);
1544
+ }`;
1545
+ }
1546
+ /**
1547
+ * Assemble the full uber-shader from the kind contributions. Produces the
1548
+ * vertex source plus the opaque and transparent fragment sources from a SINGLE
1549
+ * fragment body — the two variants differ only by the trailing `discard`, so
1550
+ * they cannot drift.
1551
+ *
1552
+ * @param kinds The primitive kinds to dispatch over. Every kind's `typeFlag`
1553
+ * gets a geom branch in the vertex and an sdf branch in the fragment.
1554
+ * @param _accessorWgsl Deprecated no-op parameter kept for call-site
1555
+ * compatibility. Kinds read `inst.*` directly from the `InstanceStruct`
1556
+ * binding; the generated `readInstance` accessor was dead code and has been
1557
+ * removed. Pass any string (e.g. `INSTANCE_LAYOUT.wgslAccessor`) — it is
1558
+ * ignored.
1559
+ */
1560
+ function assembleShader(kinds, _accessorWgsl) {
1561
+ const geomCases = dispatch(kinds, (k) => k.wgslGeom());
1562
+ const sdfCases = dispatch(kinds, (k) => k.wgslSdf());
1563
+ const vertex = buildVertex(geomCases);
1564
+ const opaqueBody = buildFragBody(sdfCases, false);
1565
+ const transparentBody = buildFragBody(sdfCases, true);
1566
+ return {
1567
+ vertex,
1568
+ opaque: `${opaqueBody}\n\n if (outColor.a < 0.5) { discard; }\n return outColor;\n}`,
1569
+ transparent: `${transparentBody}\n\n${EMPHASIS_APPLY_WGSL}\n return outColor;\n}`,
1570
+ oitBuild: buildOitFragment(sdfCases)
1571
+ };
1572
+ }
1573
+ //#endregion
1574
+ export { mixColor as $, TYPE_ELLIPSE as A, BLACK as B, pack2xF16 as C, TYPE_ARC as D, unpackF16 as E, TYPE_STROKE_PATH as F, fade as G, WHITE as H, TYPE_TRIANGLE as I, hslToRgb as J, hex as K, INSTANCE_BYTES as L, TYPE_RECT as M, TYPE_SEGMENT as N, TYPE_CIRCLE as O, TYPE_SPRITE as P, lighten as Q, INSTANCE_FIELDS as R, bitcastF32 as S, packUnorm8x4 as T, cssHex as U, TRANSPARENT as V, darken as W, hsv as X, hslToRgba as Y, lerpColor as Z, polylineEllipseRing as _, ARC_FLOATS as a, INSTANCE_LAYOUT as b, CURVE_CAP_SQUARE as c, SHAPE_BYTES as d, rgba as et, SHAPE_CIRCLE as f, TRIANGLE_FLOATS as g, SPRITE_FLOATS as h, createPipelines as i, TYPE_GLYPH as j, TYPE_CURVE as k, CURVE_FLOATS as l, SHAPE_RECT as m, WGSL_HELPERS as n, CURVE_CAP_BUTT as o, SHAPE_ELLIPSE as p, hsl as q, assembleShader as r, CURVE_CAP_ROUND as s, INSTANCE_STRUCT_WGSL as t, withAlpha as tt, SEGMENT_FLOATS as u, polylineRectRing as v, packF16 as w, deriveLayout as x, tessellatePolyline as y, INSTANCE_FLOATS as z };