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.
- package/LICENSE.md +674 -0
- package/README.md +234 -0
- package/dist/advanced.d.mts +76 -0
- package/dist/advanced.mjs +81 -0
- package/dist/assemble-BT3CXbSx.mjs +1574 -0
- package/dist/camera-view-DHmMiKvP.d.mts +326 -0
- package/dist/frame-mHNdKRpF.mjs +135 -0
- package/dist/index-CmMZCMJT.d.mts +39 -0
- package/dist/index-DkJfpntS.d.mts +2417 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +6612 -0
- package/dist/internal.d.mts +892 -0
- package/dist/internal.mjs +566 -0
- package/dist/logger-DSyBF3Y_.mjs +15 -0
- package/dist/particles.d.mts +816 -0
- package/dist/particles.mjs +4804 -0
- package/dist/pipeline-BWCAZTKx.mjs +470 -0
- package/dist/pipeline-DE3a1Pnk.d.mts +115 -0
- package/dist/reactivity-B7I0pvzm.mjs +191 -0
- package/dist/reactivity.d.mts +2 -0
- package/dist/reactivity.mjs +2 -0
- package/dist/renderer-DzZqd1bY.d.mts +4566 -0
- package/dist/root-CHradZKM.mjs +30 -0
- package/dist/shape-DfZP9Jdk.mjs +349 -0
- package/dist/space-CeDnj6eu.mjs +11240 -0
- package/dist/spatial-Bd3Ay8I2.d.mts +85 -0
- package/dist/spatial-hash-C1crBjTo.mjs +77 -0
- package/dist/spatial.d.mts +2 -0
- package/dist/spatial.mjs +121 -0
- package/dist/text-font-D7GGDtTK.d.mts +185 -0
- package/dist/text-ttf.d.mts +91 -0
- package/dist/text-ttf.mjs +298 -0
- package/dist/texture-dABoqFoP.mjs +131 -0
- package/dist/viewport.d.mts +2 -0
- package/dist/viewport.mjs +274 -0
- package/package.json +69 -0
|
@@ -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 };
|