pollyroll 0.1.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,2515 @@
1
+ 'use strict';
2
+
3
+ var chunkOEMS7CZP_cjs = require('./chunk-OEMS7CZP.cjs');
4
+
5
+ // src/geometry/vec.ts
6
+ var IDENTITY = [0, 0, 0, 1];
7
+ function add(a, b) {
8
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
9
+ }
10
+ function sub(a, b) {
11
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
12
+ }
13
+ function scale(a, s) {
14
+ return [a[0] * s, a[1] * s, a[2] * s];
15
+ }
16
+ function dot(a, b) {
17
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
18
+ }
19
+ function cross(a, b) {
20
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
21
+ }
22
+ function length(a) {
23
+ return Math.sqrt(dot(a, a));
24
+ }
25
+ function normalize(a) {
26
+ const len = length(a);
27
+ if (len <= 1e-12) return [0, 0, 0];
28
+ return [a[0] / len, a[1] / len, a[2] / len];
29
+ }
30
+ function quatMul(a, b) {
31
+ const [ax, ay, az, aw] = a;
32
+ const [bx, by, bz, bw] = b;
33
+ return [
34
+ aw * bx + ax * bw + ay * bz - az * by,
35
+ aw * by - ax * bz + ay * bw + az * bx,
36
+ aw * bz + ax * by - ay * bx + az * bw,
37
+ aw * bw - ax * bx - ay * by - az * bz
38
+ ];
39
+ }
40
+ function quatRotate(q, v) {
41
+ const u = [q[0], q[1], q[2]];
42
+ const t = scale(cross(u, v), 2);
43
+ return add(add(v, scale(t, q[3])), cross(u, t));
44
+ }
45
+ function quatFromMatrix(m) {
46
+ const [m00 = 0, m01 = 0, m02 = 0, m10 = 0, m11 = 0, m12 = 0, m20 = 0, m21 = 0, m22 = 0] = m;
47
+ const trace = m00 + m11 + m22;
48
+ let x;
49
+ let y;
50
+ let z;
51
+ let w;
52
+ if (trace > 0) {
53
+ const s = Math.sqrt(trace + 1) * 2;
54
+ w = s / 4;
55
+ x = (m21 - m12) / s;
56
+ y = (m02 - m20) / s;
57
+ z = (m10 - m01) / s;
58
+ } else if (m00 > m11 && m00 > m22) {
59
+ const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
60
+ w = (m21 - m12) / s;
61
+ x = s / 4;
62
+ y = (m01 + m10) / s;
63
+ z = (m02 + m20) / s;
64
+ } else if (m11 > m22) {
65
+ const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
66
+ w = (m02 - m20) / s;
67
+ x = (m01 + m10) / s;
68
+ y = s / 4;
69
+ z = (m12 + m21) / s;
70
+ } else {
71
+ const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
72
+ w = (m10 - m01) / s;
73
+ x = (m02 + m20) / s;
74
+ y = (m12 + m21) / s;
75
+ z = s / 4;
76
+ }
77
+ const len = Math.sqrt(x * x + y * y + z * z + w * w);
78
+ x /= len;
79
+ y /= len;
80
+ z /= len;
81
+ w /= len;
82
+ let flip;
83
+ if (Math.abs(w) >= 1e-12) flip = w < 0;
84
+ else if (Math.abs(x) >= 1e-12) flip = x < 0;
85
+ else if (Math.abs(y) >= 1e-12) flip = y < 0;
86
+ else flip = z < 0;
87
+ return flip ? [-x, -y, -z, -w] : [x, y, z, w];
88
+ }
89
+
90
+ // src/geometry/polyhedra.ts
91
+ var EPS = 1e-9;
92
+ var PHI = (1 + Math.sqrt(5)) / 2;
93
+ var INV_PHI = 1 / PHI;
94
+ function signs(base) {
95
+ let out = [[]];
96
+ for (const c of base) {
97
+ const next = [];
98
+ for (const prefix of out) {
99
+ next.push([...prefix, c]);
100
+ if (c !== 0) next.push([...prefix, -c]);
101
+ }
102
+ out = next;
103
+ }
104
+ return out.map(([x = 0, y = 0, z = 0]) => [x, y, z]);
105
+ }
106
+ function d10Vertices() {
107
+ const r5 = Math.sqrt(5);
108
+ const c36 = (1 + r5) / 4;
109
+ const s36 = Math.sqrt(10 - 2 * r5) / 4;
110
+ const c72 = (r5 - 1) / 4;
111
+ const s72 = Math.sqrt(10 + 2 * r5) / 4;
112
+ const h = 1;
113
+ const r = 1;
114
+ const z = h * (1 - c36) / (1 + c36);
115
+ const upper = [
116
+ [1, 0],
117
+ [c72, s72],
118
+ [-c36, s36],
119
+ [-c36, -s36],
120
+ [c72, -s72]
121
+ ];
122
+ const lower = [
123
+ [c36, s36],
124
+ [-c72, s72],
125
+ [-1, 0],
126
+ [-c72, -s72],
127
+ [c36, -s36]
128
+ ];
129
+ return [
130
+ [0, h, 0],
131
+ [0, -h, 0],
132
+ ...upper.map(([c, s]) => [r * c, z, r * s]),
133
+ ...lower.map(([c, s]) => [r * c, -z, r * s])
134
+ ];
135
+ }
136
+ function rawVertices(type) {
137
+ switch (type) {
138
+ case "d4":
139
+ return [
140
+ [1, 1, 1],
141
+ [1, -1, -1],
142
+ [-1, 1, -1],
143
+ [-1, -1, 1]
144
+ ];
145
+ case "d6":
146
+ return signs([1, 1, 1]);
147
+ case "d8":
148
+ return [...signs([1, 0, 0]), ...signs([0, 1, 0]), ...signs([0, 0, 1])];
149
+ case "d10":
150
+ return d10Vertices();
151
+ case "d12":
152
+ return [
153
+ ...signs([1, 1, 1]),
154
+ ...signs([0, INV_PHI, PHI]),
155
+ ...signs([INV_PHI, PHI, 0]),
156
+ ...signs([PHI, 0, INV_PHI])
157
+ ];
158
+ case "d20":
159
+ return [...signs([0, 1, PHI]), ...signs([1, PHI, 0]), ...signs([PHI, 0, 1])];
160
+ }
161
+ }
162
+ var TARGET_RADIUS = {
163
+ d4: 0.75,
164
+ d6: Math.sqrt(3) / 2,
165
+ d8: 0.75,
166
+ d10: 0.75,
167
+ d12: 0.72,
168
+ d20: 0.8
169
+ };
170
+ function vertexAt(vertices, i) {
171
+ const v = vertices[i];
172
+ if (v === void 0) throw new RangeError(`vertex ${i} out of range`);
173
+ return v;
174
+ }
175
+ function orderFace(vertices, ids, n) {
176
+ let c = [0, 0, 0];
177
+ for (const i of ids) {
178
+ const v = vertexAt(vertices, i);
179
+ c = [c[0] + v[0], c[1] + v[1], c[2] + v[2]];
180
+ }
181
+ c = scale(c, 1 / ids.length);
182
+ const [start = 0, ...rest] = ids;
183
+ const r0 = sub(vertexAt(vertices, start), c);
184
+ const half = (d) => {
185
+ const side = dot(n, cross(r0, d));
186
+ return side > 1e-12 || side >= -1e-12 && dot(r0, d) > 0 ? 0 : 1;
187
+ };
188
+ const keyed = rest.map((i) => {
189
+ const d = sub(vertexAt(vertices, i), c);
190
+ return { i, d, h: half(d) };
191
+ });
192
+ keyed.sort((a, b) => a.h - b.h || -dot(n, cross(a.d, b.d)));
193
+ return [start, ...keyed.map((k) => k.i)];
194
+ }
195
+ function findFaces(vertices) {
196
+ let c = [0, 0, 0];
197
+ for (const v of vertices) c = [c[0] + v[0], c[1] + v[1], c[2] + v[2]];
198
+ c = scale(c, 1 / vertices.length);
199
+ const faces = [];
200
+ const normals = [];
201
+ const count = vertices.length;
202
+ for (let i = 0; i < count; i++) {
203
+ const vi = vertexAt(vertices, i);
204
+ for (let j = i + 1; j < count; j++) {
205
+ const vj = vertexAt(vertices, j);
206
+ for (let k = j + 1; k < count; k++) {
207
+ const vk = vertexAt(vertices, k);
208
+ let n = normalize(cross(sub(vj, vi), sub(vk, vi)));
209
+ if (n[0] === 0 && n[1] === 0 && n[2] === 0) continue;
210
+ if (dot(n, sub(c, vi)) > 0) n = scale(n, -1);
211
+ if (!vertices.every((v) => dot(n, sub(v, vi)) <= EPS)) continue;
212
+ const duplicate = normals.some(
213
+ (m) => Math.abs(m[0] - n[0]) < EPS && Math.abs(m[1] - n[1]) < EPS && Math.abs(m[2] - n[2]) < EPS
214
+ );
215
+ if (duplicate) continue;
216
+ const ids = [];
217
+ vertices.forEach((v, idx) => {
218
+ if (Math.abs(dot(n, sub(v, vi))) <= EPS) ids.push(idx);
219
+ });
220
+ faces.push(orderFace(vertices, ids, n));
221
+ normals.push(n);
222
+ }
223
+ }
224
+ }
225
+ return { faces, normals };
226
+ }
227
+ function build(type) {
228
+ const raw = rawVertices(type);
229
+ let rawRadius = 0;
230
+ for (const v of raw) rawRadius = Math.max(rawRadius, length(v));
231
+ const s = TARGET_RADIUS[type] / rawRadius;
232
+ const vertices = raw.map((v) => scale(v, s));
233
+ let radius = 0;
234
+ for (const v of vertices) radius = Math.max(radius, length(v));
235
+ const { faces, normals } = findFaces(vertices);
236
+ const readouts = type === "d4" ? vertices.map(normalize) : normals;
237
+ return { type, vertices, faces, normals, radius, readouts };
238
+ }
239
+ var cache = {};
240
+ function getPolyhedron(type) {
241
+ let p = cache[type];
242
+ if (p === void 0) {
243
+ p = build(type);
244
+ cache[type] = p;
245
+ }
246
+ return p;
247
+ }
248
+
249
+ // src/geometry/labels.ts
250
+ var EPS2 = 1e-9;
251
+ function near(a, b) {
252
+ return Math.abs(a[0] - b[0]) < EPS2 && Math.abs(a[1] - b[1]) < EPS2 && Math.abs(a[2] - b[2]) < EPS2;
253
+ }
254
+ function shapeOf(set) {
255
+ if (set === "d100tens" || set === "d100ones") return "d10";
256
+ if (set === "dF") return "d6";
257
+ return set;
258
+ }
259
+ var D6_AXES = [
260
+ [[0, 1, 0], 1],
261
+ [[0, 0, 1], 2],
262
+ [[1, 0, 0], 3],
263
+ [[-1, 0, 0], 4],
264
+ [[0, 0, -1], 5],
265
+ [[0, -1, 0], 6]
266
+ ];
267
+ function numberShape(shape) {
268
+ const readouts = getPolyhedron(shape).readouts;
269
+ if (shape === "d4") return readouts.map((_, i) => i + 1);
270
+ if (shape === "d6") {
271
+ return readouts.map((r) => {
272
+ const hit = D6_AXES.find(([axis]) => near(axis, r));
273
+ if (hit === void 0) throw new Error("d6 face normal is not axis-aligned");
274
+ return hit[1];
275
+ });
276
+ }
277
+ const high = shape === "d10" ? 9 : readouts.length + 1;
278
+ let next = shape === "d10" ? 0 : 1;
279
+ const numbers = readouts.map(() => -1);
280
+ readouts.forEach((r, i) => {
281
+ if (numbers[i] !== -1) return;
282
+ const j = readouts.findIndex((o) => near(o, [-r[0], -r[1], -r[2]]));
283
+ if (j < 0) throw new Error(`${shape} face ${i} has no opposite face`);
284
+ numbers[i] = next;
285
+ numbers[j] = high - next;
286
+ next++;
287
+ });
288
+ return numbers;
289
+ }
290
+ var numberCache = {};
291
+ function readoutNumbers(shape) {
292
+ let numbers = numberCache[shape];
293
+ if (numbers === void 0) {
294
+ numbers = numberShape(shape);
295
+ numberCache[shape] = numbers;
296
+ }
297
+ return numbers;
298
+ }
299
+ function numberAt(set, readout) {
300
+ const n = readoutNumbers(shapeOf(set))[readout];
301
+ if (n === void 0) throw new RangeError(`${set} has no readout ${readout}`);
302
+ return n;
303
+ }
304
+ function labelText(set, readout) {
305
+ const n = numberAt(set, readout);
306
+ if (set === "d100tens") return n === 0 ? "00" : `${n}0`;
307
+ if (set === "dF") return n <= 2 ? "\u2212" : n <= 4 ? "" : "+";
308
+ return String(n);
309
+ }
310
+ function labelIndex(set, readout) {
311
+ const n = numberAt(set, readout);
312
+ if (set === "dF") return n - 1 >> 1;
313
+ return shapeOf(set) === "d10" ? n : n - 1;
314
+ }
315
+ function labelUnderline(set, readout) {
316
+ const text = labelText(set, readout);
317
+ const marked = set === "d10" || set === "d12" || set === "d20" || set === "d100ones";
318
+ return marked && (text === "6" || text === "9");
319
+ }
320
+ function readoutForValue(set, value) {
321
+ let target = value;
322
+ if (set === "d10") {
323
+ target = Number.isInteger(value) && value >= 1 && value <= 10 ? value % 10 : Number.NaN;
324
+ } else if (set === "dF") {
325
+ target = value === -1 ? 1 : value === 0 ? 3 : value === 1 ? 5 : Number.NaN;
326
+ }
327
+ const i = readoutNumbers(shapeOf(set)).indexOf(target);
328
+ if (i < 0) throw new RangeError(`${set} cannot show value ${value}`);
329
+ return i;
330
+ }
331
+
332
+ // src/geometry/mesh.ts
333
+ var BEVEL = 0.08;
334
+ var MESH_STRIDE = 9;
335
+ var EPS3 = 1e-9;
336
+ function at(list, i) {
337
+ const v = list[(i + list.length) % list.length];
338
+ if (v === void 0) throw new RangeError(`index ${i} out of range`);
339
+ return v;
340
+ }
341
+ function midpoint(a, b) {
342
+ return scale(add(a, b), 0.5);
343
+ }
344
+ function centroid(points) {
345
+ let c = [0, 0, 0];
346
+ for (const p of points) c = add(c, p);
347
+ return scale(c, 1 / points.length);
348
+ }
349
+ function insetFace(poly, f) {
350
+ const face = at(poly.faces, f);
351
+ const n = at(poly.normals, f);
352
+ const pts = face.map((i) => at(poly.vertices, i));
353
+ return pts.map((v, k) => {
354
+ const m1 = cross(n, normalize(sub(v, at(pts, k - 1))));
355
+ const m2 = cross(n, normalize(sub(at(pts, k + 1), v)));
356
+ return add(v, scale(add(m1, m2), BEVEL / (1 + dot(m1, m2))));
357
+ });
358
+ }
359
+ function inradius(c, pts) {
360
+ let r = Infinity;
361
+ pts.forEach((a, k) => {
362
+ const e = normalize(sub(at(pts, k + 1), a));
363
+ const d = sub(c, a);
364
+ r = Math.min(r, length(sub(d, scale(e, dot(d, e)))));
365
+ });
366
+ return r;
367
+ }
368
+ function labelFrame(center, side, upDir, n, cell) {
369
+ const up = normalize(upDir);
370
+ return { center, side, up, right: cross(up, n), cell };
371
+ }
372
+ function faceUp(c, pts) {
373
+ const dist = pts.map((p) => length(sub(p, c)));
374
+ const max = Math.max(...dist);
375
+ const min = Math.min(...dist);
376
+ if (pts.length === 4 && max - min <= EPS3) return sub(midpoint(at(pts, 0), at(pts, 1)), c);
377
+ const k = dist.findIndex((d) => d >= max - EPS3);
378
+ return sub(at(pts, k), c);
379
+ }
380
+ function build2(shape) {
381
+ const poly = getPolyhedron(shape);
382
+ const out = [];
383
+ const emit = (p, n, label) => {
384
+ out.push(p[0], p[1], p[2], n[0], n[1], n[2]);
385
+ if (label === null) {
386
+ out.push(-1, -1, -1);
387
+ return;
388
+ }
389
+ const d = sub(p, label.center);
390
+ out.push(
391
+ dot(d, label.right) / label.side + 0.5,
392
+ dot(d, label.up) / label.side + 0.5,
393
+ label.cell
394
+ );
395
+ };
396
+ const tri = (a, b, c, n, label) => {
397
+ emit(a, n, label);
398
+ emit(b, n, label);
399
+ emit(c, n, label);
400
+ };
401
+ const insets = poly.faces.map((_, f) => insetFace(poly, f));
402
+ poly.faces.forEach((face, f) => {
403
+ const n = at(poly.normals, f);
404
+ const pts = at(insets, f);
405
+ const c = centroid(pts);
406
+ const r = inradius(c, pts);
407
+ if (shape === "d4") {
408
+ pts.forEach((v, k) => {
409
+ const next = midpoint(v, at(pts, k + 1));
410
+ const prev = midpoint(at(pts, k - 1), v);
411
+ const label2 = labelFrame(
412
+ add(c, scale(sub(v, c), 0.36)),
413
+ 1.1 * r,
414
+ sub(v, c),
415
+ n,
416
+ at(face, k)
417
+ );
418
+ tri(v, next, c, n, label2);
419
+ tri(v, c, prev, n, label2);
420
+ });
421
+ return;
422
+ }
423
+ const label = labelFrame(c, 1.45 * r, faceUp(c, pts), n, f);
424
+ pts.forEach((v, k) => tri(c, v, at(pts, k + 1), n, label));
425
+ });
426
+ const findEdge = (a, b) => {
427
+ for (let g = 0; g < poly.faces.length; g++) {
428
+ const face = at(poly.faces, g);
429
+ const k = face.indexOf(a);
430
+ if (k >= 0 && at(face, k + 1) === b) return [g, k];
431
+ }
432
+ throw new Error(`${shape} has no face with edge ${a} \u2192 ${b}`);
433
+ };
434
+ poly.faces.forEach((face, f) => {
435
+ const nf = at(poly.normals, f);
436
+ const pf = at(insets, f);
437
+ face.forEach((a, k) => {
438
+ const b = at(face, k + 1);
439
+ if (a > b) return;
440
+ const [g, j] = findEdge(b, a);
441
+ const ng = at(poly.normals, g);
442
+ const pg = at(insets, g);
443
+ const aF = at(pf, k);
444
+ const bF = at(pf, k + 1);
445
+ const bG = at(pg, j);
446
+ const aG = at(pg, j + 1);
447
+ emit(aF, nf, null);
448
+ emit(aG, ng, null);
449
+ emit(bG, ng, null);
450
+ emit(aF, nf, null);
451
+ emit(bG, ng, null);
452
+ emit(bF, nf, null);
453
+ });
454
+ });
455
+ poly.vertices.forEach((_, v) => {
456
+ const start = poly.faces.findIndex((face) => face.includes(v));
457
+ const ring = [];
458
+ let f = start;
459
+ do {
460
+ const face = at(poly.faces, f);
461
+ const k = face.indexOf(v);
462
+ ring.push([at(at(insets, f), k), at(poly.normals, f)]);
463
+ f = findEdge(v, at(face, k - 1))[0];
464
+ } while (f !== start && ring.length <= poly.faces.length);
465
+ const [p0, n0] = at(ring, 0);
466
+ for (let i = 1; i + 1 < ring.length; i++) {
467
+ const [p1, n1] = at(ring, i);
468
+ const [p2, n2] = at(ring, i + 1);
469
+ emit(p0, n0, null);
470
+ emit(p1, n1, null);
471
+ emit(p2, n2, null);
472
+ }
473
+ });
474
+ return { shape, data: new Float32Array(out), vertexCount: out.length / MESH_STRIDE };
475
+ }
476
+ var cache2 = {};
477
+ function getDieMesh(shape) {
478
+ let mesh = cache2[shape];
479
+ if (mesh === void 0) {
480
+ mesh = build2(shape);
481
+ cache2[shape] = mesh;
482
+ }
483
+ return mesh;
484
+ }
485
+
486
+ // src/geometry/symmetry.ts
487
+ var MATCH = 1e-6;
488
+ function near2(a, b) {
489
+ for (let i = 0; i < a.length; i++) {
490
+ const x = a[i];
491
+ const y = b[i];
492
+ if (x === void 0 || y === void 0 || Math.abs(x - y) > MATCH) return false;
493
+ }
494
+ return a.length === b.length;
495
+ }
496
+ function frame(a, b) {
497
+ const e1 = normalize(a);
498
+ const e2 = normalize(sub(b, scale(e1, dot(b, e1))));
499
+ return [e1, e2, cross(e1, e2)];
500
+ }
501
+ function generate(type) {
502
+ const vertices = getPolyhedron(type).vertices;
503
+ const [v0] = vertices;
504
+ if (v0 === void 0) return [];
505
+ const v1 = vertices.find((v) => length(cross(v0, v)) > MATCH);
506
+ if (v1 === void 0) return [];
507
+ const len0 = length(v0);
508
+ const len1 = length(v1);
509
+ const d01 = dot(v0, v1);
510
+ const [a1, a2, a3] = frame(v0, v1);
511
+ const group = [];
512
+ vertices.forEach((vi, i) => {
513
+ if (Math.abs(length(vi) - len0) >= 1e-9) return;
514
+ vertices.forEach((vj, j) => {
515
+ if (i === j || Math.abs(length(vj) - len1) >= 1e-9) return;
516
+ if (Math.abs(dot(vi, vj) - d01) >= 1e-9) return;
517
+ const [b1, b2, b3] = frame(vi, vj);
518
+ const row = (r) => [
519
+ b1[r] * a1[0] + b2[r] * a2[0] + b3[r] * a3[0],
520
+ b1[r] * a1[1] + b2[r] * a2[1] + b3[r] * a3[1],
521
+ b1[r] * a1[2] + b2[r] * a2[2] + b3[r] * a3[2]
522
+ ];
523
+ const r0 = row(0);
524
+ const r1 = row(1);
525
+ const r2 = row(2);
526
+ const apply = (v) => [dot(r0, v), dot(r1, v), dot(r2, v)];
527
+ if (!vertices.every((v) => vertices.some((w) => near2(apply(v), w)))) return;
528
+ const q = quatFromMatrix([...r0, ...r1, ...r2]);
529
+ if (group.some((g) => near2(g, q))) return;
530
+ group.push(q);
531
+ });
532
+ });
533
+ return group;
534
+ }
535
+ var groupCache = {};
536
+ function getRotationGroup(type) {
537
+ let group = groupCache[type];
538
+ if (group === void 0) {
539
+ group = generate(type);
540
+ groupCache[type] = group;
541
+ }
542
+ return group;
543
+ }
544
+ function findRemap(type, from, to) {
545
+ const readouts = getPolyhedron(type).readouts;
546
+ const a = readouts[from];
547
+ const b = readouts[to];
548
+ if (a !== void 0 && b !== void 0) {
549
+ const g = getRotationGroup(type).find((q) => near2(quatRotate(q, a), b));
550
+ if (g !== void 0) return g;
551
+ }
552
+ throw new Error(`${type}: no rotation maps readout ${from} to ${to}`);
553
+ }
554
+
555
+ // src/physics/body.ts
556
+ function inertiaTensor(shape) {
557
+ const { vertices, faces } = getPolyhedron(shape);
558
+ const point = (i) => {
559
+ const v = vertices[i];
560
+ if (v === void 0) throw new RangeError(`vertex ${i} out of range`);
561
+ return v;
562
+ };
563
+ let cxx = 0;
564
+ let cyy = 0;
565
+ let czz = 0;
566
+ let cxy = 0;
567
+ let cxz = 0;
568
+ let cyz = 0;
569
+ let volume = 0;
570
+ for (const [first, ...rest] of faces) {
571
+ const a = point(first ?? 0);
572
+ for (let k = 0; k + 1 < rest.length; k++) {
573
+ const b = point(rest[k] ?? 0);
574
+ const c = point(rest[k + 1] ?? 0);
575
+ const det = dot(a, cross(b, c));
576
+ volume += det / 6;
577
+ const s = add(add(a, b), c);
578
+ const w = det / 120;
579
+ const pair = (i, j) => w * (a[i] * a[j] + b[i] * b[j] + c[i] * c[j] + s[i] * s[j]);
580
+ cxx += pair(0, 0);
581
+ cyy += pair(1, 1);
582
+ czz += pair(2, 2);
583
+ cxy += pair(0, 1);
584
+ cxz += pair(0, 2);
585
+ cyz += pair(1, 2);
586
+ }
587
+ }
588
+ const inv = volume > 1e-12 ? 1 / volume : 0;
589
+ cxx *= inv;
590
+ cyy *= inv;
591
+ czz *= inv;
592
+ cxy *= inv;
593
+ cxz *= inv;
594
+ cyz *= inv;
595
+ const trace = cxx + cyy + czz;
596
+ return new Float64Array([
597
+ trace - cxx,
598
+ -cxy,
599
+ -cxz,
600
+ -cxy,
601
+ trace - cyy,
602
+ -cyz,
603
+ -cxz,
604
+ -cyz,
605
+ trace - czz
606
+ ]);
607
+ }
608
+ var cache3 = {};
609
+ function getBodyShape(shape) {
610
+ let body = cache3[shape];
611
+ if (body === void 0) {
612
+ const poly = getPolyhedron(shape);
613
+ const vertices = new Float64Array(poly.vertices.length * 3);
614
+ poly.vertices.forEach((v, i) => vertices.set(v, i * 3));
615
+ const [ixx = 0, , , , iyy = 0, , , , izz = 0] = inertiaTensor(shape);
616
+ const inverse = (m) => m > 1e-12 ? 1 / m : 0;
617
+ const invInertia = new Float64Array([inverse(ixx), inverse(iyy), inverse(izz)]);
618
+ body = { shape, vertices, radius: poly.radius, invInertia };
619
+ cache3[shape] = body;
620
+ }
621
+ return body;
622
+ }
623
+
624
+ // src/physics/flatten.ts
625
+ var FLATTEN_STEPS = 24;
626
+ var COS_HALF_DEGREE = 0.9999619230641713;
627
+ function upReadout(shape, pose) {
628
+ const [, , , x = 0, y = 0, z = 0, w = 1] = pose;
629
+ const r0 = 2 * (x * y + z * w);
630
+ const r1 = 1 - 2 * (x * x + z * z);
631
+ const r2 = 2 * (y * z - x * w);
632
+ let best = 0;
633
+ let bestY = -Infinity;
634
+ getPolyhedron(shape).readouts.forEach((r, k) => {
635
+ const ry = r0 * r[0] + r1 * r[1] + r2 * r[2];
636
+ if (ry > bestY) {
637
+ bestY = ry;
638
+ best = k;
639
+ }
640
+ });
641
+ return best;
642
+ }
643
+ var SPACING = 0.8;
644
+ var PASSES = 8;
645
+ function separate(xz, radii, bounds) {
646
+ const clamp = (k) => {
647
+ const m = SPACING * (radii[k] ?? 0);
648
+ xz[2 * k] = Math.min(Math.max(xz[2 * k] ?? 0, bounds.minX + m), bounds.maxX - m);
649
+ xz[2 * k + 1] = Math.min(Math.max(xz[2 * k + 1] ?? 0, bounds.minZ + m), bounds.maxZ - m);
650
+ };
651
+ for (let pass = 0; pass < PASSES; pass++) {
652
+ for (let i = 0; i < radii.length; i++) {
653
+ for (let j = i + 1; j < radii.length; j++) {
654
+ const dx = (xz[2 * j] ?? 0) - (xz[2 * i] ?? 0);
655
+ const dz = (xz[2 * j + 1] ?? 0) - (xz[2 * i + 1] ?? 0);
656
+ const want = SPACING * ((radii[i] ?? 0) + (radii[j] ?? 0));
657
+ const d = Math.sqrt(dx * dx + dz * dz);
658
+ if (d >= want) continue;
659
+ const nx = d < 1e-9 ? 1 : dx / d;
660
+ const nz = d < 1e-9 ? 0 : dz / d;
661
+ const push = (want - d) / 2;
662
+ xz[2 * i] = (xz[2 * i] ?? 0) - nx * push;
663
+ xz[2 * i + 1] = (xz[2 * i + 1] ?? 0) - nz * push;
664
+ xz[2 * j] = (xz[2 * j] ?? 0) + nx * push;
665
+ xz[2 * j + 1] = (xz[2 * j + 1] ?? 0) + nz * push;
666
+ clamp(i);
667
+ clamp(j);
668
+ }
669
+ }
670
+ }
671
+ }
672
+ function flattenTail(shape, pose, x, z) {
673
+ const [px = 0, py = 0, pz = 0, qx = 0, qy = 0, qz = 0, qw = 1] = pose;
674
+ const q = [qx, qy, qz, qw];
675
+ const poly = getPolyhedron(shape);
676
+ const r = poly.readouts[upReadout(shape, pose)] ?? [0, 1, 0];
677
+ const [ux, uy, uz] = quatRotate(q, r);
678
+ if (uy >= COS_HALF_DEGREE && x === px && z === pz) return null;
679
+ const aw = 1 + uy;
680
+ const an = Math.sqrt(uz * uz + ux * ux + aw * aw);
681
+ const [fx, fy, fz, fw] = quatMul([-uz / an, 0, ux / an, aw / an], q);
682
+ const low = lowestY(poly.vertices, fx, fy, fz, fw);
683
+ const sign = qx * fx + qy * fy + qz * fz + qw * fw < 0 ? -1 : 1;
684
+ const out = new Float64Array(FLATTEN_STEPS * 7);
685
+ for (let k = 1; k <= FLATTEN_STEPS; k++) {
686
+ const t = k / FLATTEN_STEPS;
687
+ const s = 1 - t;
688
+ const cx = s * qx + t * sign * fx;
689
+ const cy = s * qy + t * sign * fy;
690
+ const cz = s * qz + t * sign * fz;
691
+ const cw = s * qw + t * sign * fw;
692
+ const n = Math.sqrt(cx * cx + cy * cy + cz * cz + cw * cw);
693
+ const i = (k - 1) * 7;
694
+ out[i] = s * px + t * x;
695
+ out[i + 1] = Math.max(
696
+ s * py - t * low,
697
+ -lowestY(poly.vertices, cx / n, cy / n, cz / n, cw / n)
698
+ );
699
+ out[i + 2] = s * pz + t * z;
700
+ out[i + 3] = cx / n;
701
+ out[i + 4] = cy / n;
702
+ out[i + 5] = cz / n;
703
+ out[i + 6] = cw / n;
704
+ }
705
+ return out;
706
+ }
707
+ function lowestY(vertices, x, y, z, w) {
708
+ const r0 = 2 * (x * y + z * w);
709
+ const r1 = 1 - 2 * (x * x + z * z);
710
+ const r2 = 2 * (y * z - x * w);
711
+ let low = Infinity;
712
+ for (const v of vertices) low = Math.min(low, r0 * v[0] + r1 * v[1] + r2 * v[2]);
713
+ return low;
714
+ }
715
+
716
+ // src/physics/throw.ts
717
+ function clampInto(value, lo, hi) {
718
+ if (lo > hi) return (lo + hi) / 2;
719
+ return value < lo ? lo : value > hi ? hi : value;
720
+ }
721
+ function throwWave(rng, radii, bounds) {
722
+ let hx = 0;
723
+ let hz = 0;
724
+ let lenSq = 0;
725
+ do {
726
+ hx = 2 * rng.float() - 1;
727
+ hz = 2 * rng.float() - 1;
728
+ lenSq = hx * hx + hz * hz;
729
+ } while (!(lenSq > 0.04 && lenSq <= 1));
730
+ const hLen = Math.sqrt(lenSq);
731
+ hx /= hLen;
732
+ hz /= hLen;
733
+ const speed = 9 + 5 * rng.float();
734
+ const px = -hz;
735
+ const pz = hx;
736
+ const count = radii.length;
737
+ let cols = 0;
738
+ while (cols * cols < count) cols++;
739
+ const centerX = (bounds.minX + bounds.maxX) / 2;
740
+ const centerZ = (bounds.minZ + bounds.maxZ) / 2;
741
+ const back = 0.35 * Math.min((bounds.maxX - bounds.minX) / 2, (bounds.maxZ - bounds.minZ) / 2);
742
+ return radii.map((r, i) => {
743
+ const col = i % cols;
744
+ const row = Math.floor(i / cols);
745
+ const across = (col - (cols - 1) / 2) * 1.3;
746
+ const x = centerX - hx * back + px * across - hx * (row * 1.3) + (2 * rng.float() - 1) * 0.15;
747
+ const z = centerZ - hz * back + pz * across - hz * (row * 1.3) + (2 * rng.float() - 1) * 0.15;
748
+ const y = 2.2 + 0.6 * rng.float();
749
+ let ux = 0;
750
+ let uy = 0;
751
+ let uz = 0;
752
+ let uw = 0;
753
+ let uSq = 0;
754
+ do {
755
+ ux = 2 * rng.float() - 1;
756
+ uy = 2 * rng.float() - 1;
757
+ uz = 2 * rng.float() - 1;
758
+ uw = 2 * rng.float() - 1;
759
+ uSq = ux * ux + uy * uy + uz * uz + uw * uw;
760
+ } while (!(uSq > 1e-6 && uSq <= 1));
761
+ const uLen = Math.sqrt(uSq);
762
+ const along = speed * (0.85 + 0.3 * rng.float());
763
+ const lateral = (2 * rng.float() - 1) * 1.5;
764
+ const vy = -2 * rng.float();
765
+ const wx = (2 * rng.float() - 1) * 25;
766
+ const wy = (2 * rng.float() - 1) * 25;
767
+ const wz = (2 * rng.float() - 1) * 25;
768
+ return {
769
+ position: [
770
+ clampInto(x, bounds.minX + r, bounds.maxX - r),
771
+ y,
772
+ clampInto(z, bounds.minZ + r, bounds.maxZ - r)
773
+ ],
774
+ orientation: [ux / uLen, uy / uLen, uz / uLen, uw / uLen],
775
+ velocity: [hx * along + px * lateral, vy, hz * along + pz * lateral],
776
+ angularVelocity: [wx, wy, wz]
777
+ };
778
+ });
779
+ }
780
+
781
+ // src/physics/world.ts
782
+ var DT = 1 / 120;
783
+ var DEFAULT_CONFIG = {
784
+ gravity: -30,
785
+ restitution: 0.35,
786
+ friction: 0.6,
787
+ // Tuned on the settle-acceptance throws (10 mixed dice, seeds 0-49 and 50-249): the best settle
788
+ // rate in the allowed ranges with a median of about 250 steps.
789
+ linearDamping: 0.6,
790
+ angularDamping: 2,
791
+ iterations: 10
792
+ };
793
+ var MAX_BODIES = 30;
794
+ var MAX_VERTICES = 20;
795
+ var PLANES = 5;
796
+ var MAX_CONTACTS = MAX_BODIES * PLANES * MAX_VERTICES + MAX_BODIES * (MAX_BODIES - 1) / 2;
797
+ var BAUMGARTE = 0.2;
798
+ var SLOP = 5e-3;
799
+ var EPSILON = 1e-12;
800
+ var C_N = 0;
801
+ var C_T1 = 3;
802
+ var C_T2 = 6;
803
+ var C_TARGET = 9;
804
+ var C_LAMBDA = 10;
805
+ var C_MASS = 13;
806
+ var C_ROWS = 16;
807
+ var ROW = 12;
808
+ var STRIDE = C_ROWS + 3 * ROW;
809
+ var PLANE_SLOTS = MAX_BODIES * PLANES * MAX_VERTICES;
810
+ var SLOTS = PLANE_SLOTS + MAX_BODIES * MAX_BODIES;
811
+ function at2(a, i) {
812
+ const v = a[i];
813
+ return v === void 0 ? 0 : v;
814
+ }
815
+ function atInt(a, i) {
816
+ const v = a[i];
817
+ return v === void 0 ? 0 : v;
818
+ }
819
+ function checkHull(vertices) {
820
+ if (vertices.length > MAX_VERTICES * 3) {
821
+ throw new RangeError(`at most ${MAX_VERTICES} hull vertices per body`);
822
+ }
823
+ }
824
+ function clampNumber(value, min, max) {
825
+ if (!(value >= min)) return min;
826
+ return value > max ? max : value;
827
+ }
828
+ function createWorld(bounds, config) {
829
+ const cfg = { ...DEFAULT_CONFIG, ...config };
830
+ const { gravity, iterations } = cfg;
831
+ const restitution = clampNumber(cfg.restitution, 0, 1);
832
+ const friction = clampNumber(cfg.friction, 0, Infinity);
833
+ const linearFactor = 1 / (1 + clampNumber(cfg.linearDamping, 0, Infinity) * DT);
834
+ const angularFactor = 1 / (1 + clampNumber(cfg.angularDamping, 0, Infinity) * DT);
835
+ const biasRate = BAUMGARTE / DT;
836
+ const planeNormal = new Float64Array([0, 1, 0, 1, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, -1]);
837
+ const planeOffset = new Float64Array([0, bounds.minX, -bounds.maxX, bounds.minZ, -bounds.maxZ]);
838
+ const shapes = [];
839
+ const bodyInvInertia = new Float64Array(MAX_BODIES * 3);
840
+ const bodyRadius = new Float64Array(MAX_BODIES);
841
+ const pos = new Float64Array(MAX_BODIES * 3);
842
+ const vel = new Float64Array(MAX_BODIES * 3);
843
+ const quat = new Float64Array(MAX_BODIES * 4);
844
+ const omega = new Float64Array(MAX_BODIES * 3);
845
+ const rot = new Float64Array(MAX_BODIES * 9);
846
+ const invI = new Float64Array(MAX_BODIES * 9);
847
+ const worldVerts = new Float64Array(MAX_VERTICES * 3);
848
+ const contacts = new Float64Array(MAX_CONTACTS * STRIDE);
849
+ const contactA = new Int32Array(MAX_CONTACTS);
850
+ const contactB = new Int32Array(MAX_CONTACTS);
851
+ const contactSlot = new Int32Array(MAX_CONTACTS);
852
+ const slotLambda = new Float64Array(SLOTS * 3);
853
+ const slotStep = new Int32Array(SLOTS);
854
+ let stepNumber = 1;
855
+ let contactCount = 0;
856
+ let count = 0;
857
+ function checkIndex(i) {
858
+ if (!(i >= 0 && i < count)) throw new RangeError(`body ${i} out of range`);
859
+ }
860
+ function updateInertia(i) {
861
+ const x = at2(quat, i * 4);
862
+ const y = at2(quat, i * 4 + 1);
863
+ const z = at2(quat, i * 4 + 2);
864
+ const w = at2(quat, i * 4 + 3);
865
+ const r = i * 9;
866
+ rot[r] = 1 - 2 * (y * y + z * z);
867
+ rot[r + 1] = 2 * (x * y - z * w);
868
+ rot[r + 2] = 2 * (x * z + y * w);
869
+ rot[r + 3] = 2 * (x * y + z * w);
870
+ rot[r + 4] = 1 - 2 * (x * x + z * z);
871
+ rot[r + 5] = 2 * (y * z - x * w);
872
+ rot[r + 6] = 2 * (x * z - y * w);
873
+ rot[r + 7] = 2 * (y * z + x * w);
874
+ rot[r + 8] = 1 - 2 * (x * x + y * y);
875
+ const d0 = at2(bodyInvInertia, i * 3);
876
+ const d1 = at2(bodyInvInertia, i * 3 + 1);
877
+ const d2 = at2(bodyInvInertia, i * 3 + 2);
878
+ for (let row = 0; row < 3; row++) {
879
+ const a0 = at2(rot, r + row * 3);
880
+ const a1 = at2(rot, r + row * 3 + 1);
881
+ const a2 = at2(rot, r + row * 3 + 2);
882
+ for (let col = 0; col < 3; col++) {
883
+ invI[r + row * 3 + col] = a0 * d0 * at2(rot, r + col * 3) + a1 * d1 * at2(rot, r + col * 3 + 1) + a2 * d2 * at2(rot, r + col * 3 + 2);
884
+ }
885
+ }
886
+ }
887
+ function angularRow(body, rx, ry, rz, dx, dy, dz, offset) {
888
+ if (body < 0) {
889
+ for (let k = 0; k < 6; k++) contacts[offset + k] = 0;
890
+ return 0;
891
+ }
892
+ const cx = ry * dz - rz * dy;
893
+ const cy = rz * dx - rx * dz;
894
+ const cz = rx * dy - ry * dx;
895
+ const m = body * 9;
896
+ const ix = at2(invI, m) * cx + at2(invI, m + 1) * cy + at2(invI, m + 2) * cz;
897
+ const iy = at2(invI, m + 3) * cx + at2(invI, m + 4) * cy + at2(invI, m + 5) * cz;
898
+ const iz = at2(invI, m + 6) * cx + at2(invI, m + 7) * cy + at2(invI, m + 8) * cz;
899
+ contacts[offset] = cx;
900
+ contacts[offset + 1] = cy;
901
+ contacts[offset + 2] = cz;
902
+ contacts[offset + 3] = ix;
903
+ contacts[offset + 4] = iy;
904
+ contacts[offset + 5] = iz;
905
+ return cx * ix + cy * iy + cz * iz;
906
+ }
907
+ function rowVelocity(c, row) {
908
+ const base = c * STRIDE;
909
+ const dir = base + row * 3;
910
+ const r = base + C_ROWS + row * ROW;
911
+ const dx = at2(contacts, dir);
912
+ const dy = at2(contacts, dir + 1);
913
+ const dz = at2(contacts, dir + 2);
914
+ const b = atInt(contactB, c) * 3;
915
+ let v = dx * at2(vel, b) + dy * at2(vel, b + 1) + dz * at2(vel, b + 2) + at2(omega, b) * at2(contacts, r + 6) + at2(omega, b + 1) * at2(contacts, r + 7) + at2(omega, b + 2) * at2(contacts, r + 8);
916
+ const ai = atInt(contactA, c);
917
+ if (ai >= 0) {
918
+ const a = ai * 3;
919
+ v -= dx * at2(vel, a) + dy * at2(vel, a + 1) + dz * at2(vel, a + 2) + at2(omega, a) * at2(contacts, r) + at2(omega, a + 1) * at2(contacts, r + 1) + at2(omega, a + 2) * at2(contacts, r + 2);
920
+ }
921
+ return v;
922
+ }
923
+ function addContact(a, b, nx, ny, nz, rax, ray, raz, rbx, rby, rbz, depth, slot) {
924
+ const c = contactCount++;
925
+ contactA[c] = a;
926
+ contactB[c] = b;
927
+ contactSlot[c] = slot;
928
+ const warm = atInt(slotStep, slot) === stepNumber;
929
+ const base = c * STRIDE;
930
+ let t1x;
931
+ let t1y;
932
+ let t1z;
933
+ if (Math.abs(nx) >= 0.57735) {
934
+ const len = Math.sqrt(nx * nx + ny * ny);
935
+ t1x = ny / len;
936
+ t1y = -nx / len;
937
+ t1z = 0;
938
+ } else {
939
+ const len = Math.sqrt(ny * ny + nz * nz);
940
+ t1x = 0;
941
+ t1y = nz / len;
942
+ t1z = -ny / len;
943
+ }
944
+ const t2x = ny * t1z - nz * t1y;
945
+ const t2y = nz * t1x - nx * t1z;
946
+ const t2z = nx * t1y - ny * t1x;
947
+ contacts[base + C_N] = nx;
948
+ contacts[base + C_N + 1] = ny;
949
+ contacts[base + C_N + 2] = nz;
950
+ contacts[base + C_T1] = t1x;
951
+ contacts[base + C_T1 + 1] = t1y;
952
+ contacts[base + C_T1 + 2] = t1z;
953
+ contacts[base + C_T2] = t2x;
954
+ contacts[base + C_T2 + 1] = t2y;
955
+ contacts[base + C_T2 + 2] = t2z;
956
+ const linear = a >= 0 ? 2 : 1;
957
+ for (let row = 0; row < 3; row++) {
958
+ const dx = at2(contacts, base + row * 3);
959
+ const dy = at2(contacts, base + row * 3 + 1);
960
+ const dz = at2(contacts, base + row * 3 + 2);
961
+ const rowAt = base + C_ROWS + row * ROW;
962
+ const k = linear + angularRow(a, rax, ray, raz, dx, dy, dz, rowAt) + angularRow(b, rbx, rby, rbz, dx, dy, dz, rowAt + 6);
963
+ contacts[base + C_MASS + row] = k > EPSILON ? 1 / k : 0;
964
+ contacts[base + C_LAMBDA + row] = warm ? at2(slotLambda, slot * 3 + row) : 0;
965
+ }
966
+ const approach = rowVelocity(c, 0);
967
+ const bias = depth > SLOP ? biasRate * (depth - SLOP) : 0;
968
+ const bounce = approach < -1 ? -restitution * approach : 0;
969
+ contacts[base + C_TARGET] = bias > bounce ? bias : bounce;
970
+ }
971
+ function collide() {
972
+ contactCount = 0;
973
+ for (let i = 0; i < count; i++) {
974
+ const shape = shapes[i];
975
+ if (shape === void 0) continue;
976
+ const verts = shape.vertices;
977
+ const nv = verts.length / 3;
978
+ const m = i * 9;
979
+ const px = at2(pos, i * 3);
980
+ const py = at2(pos, i * 3 + 1);
981
+ const pz = at2(pos, i * 3 + 2);
982
+ for (let k = 0; k < nv; k++) {
983
+ const vx = at2(verts, k * 3);
984
+ const vy = at2(verts, k * 3 + 1);
985
+ const vz = at2(verts, k * 3 + 2);
986
+ worldVerts[k * 3] = at2(rot, m) * vx + at2(rot, m + 1) * vy + at2(rot, m + 2) * vz;
987
+ worldVerts[k * 3 + 1] = at2(rot, m + 3) * vx + at2(rot, m + 4) * vy + at2(rot, m + 5) * vz;
988
+ worldVerts[k * 3 + 2] = at2(rot, m + 6) * vx + at2(rot, m + 7) * vy + at2(rot, m + 8) * vz;
989
+ }
990
+ for (let p = 0; p < PLANES; p++) {
991
+ const nx = at2(planeNormal, p * 3);
992
+ const ny = at2(planeNormal, p * 3 + 1);
993
+ const nz = at2(planeNormal, p * 3 + 2);
994
+ const offset = at2(planeOffset, p);
995
+ for (let k = 0; k < nv; k++) {
996
+ const rx = at2(worldVerts, k * 3);
997
+ const ry = at2(worldVerts, k * 3 + 1);
998
+ const rz = at2(worldVerts, k * 3 + 2);
999
+ const separation = nx * (px + rx) + ny * (py + ry) + nz * (pz + rz) - offset;
1000
+ if (separation < 0) {
1001
+ const slot = (i * PLANES + p) * MAX_VERTICES + k;
1002
+ addContact(-1, i, nx, ny, nz, 0, 0, 0, rx, ry, rz, -separation, slot);
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+ for (let i = 0; i < count; i++) {
1008
+ const ri = at2(bodyRadius, i);
1009
+ for (let j = i + 1; j < count; j++) {
1010
+ const rj = at2(bodyRadius, j);
1011
+ const dx = at2(pos, j * 3) - at2(pos, i * 3);
1012
+ const dy = at2(pos, j * 3 + 1) - at2(pos, i * 3 + 1);
1013
+ const dz = at2(pos, j * 3 + 2) - at2(pos, i * 3 + 2);
1014
+ const distSq = dx * dx + dy * dy + dz * dz;
1015
+ const reach = ri + rj;
1016
+ if (distSq >= reach * reach) continue;
1017
+ const dist = Math.sqrt(distSq);
1018
+ let nx = 0;
1019
+ let ny = 1;
1020
+ let nz = 0;
1021
+ if (dist >= 1e-9) {
1022
+ nx = dx / dist;
1023
+ ny = dy / dist;
1024
+ nz = dz / dist;
1025
+ }
1026
+ addContact(
1027
+ i,
1028
+ j,
1029
+ nx,
1030
+ ny,
1031
+ nz,
1032
+ nx * ri,
1033
+ ny * ri,
1034
+ nz * ri,
1035
+ -nx * rj,
1036
+ -ny * rj,
1037
+ -nz * rj,
1038
+ reach - dist,
1039
+ PLANE_SLOTS + i * MAX_BODIES + j
1040
+ );
1041
+ }
1042
+ }
1043
+ }
1044
+ function solve() {
1045
+ for (let c = 0; c < contactCount; c++) {
1046
+ const base = c * STRIDE;
1047
+ const b = atInt(contactB, c) * 3;
1048
+ const ai = atInt(contactA, c);
1049
+ for (let row = 0; row < 3; row++) {
1050
+ const lambda = at2(contacts, base + C_LAMBDA + row);
1051
+ if (lambda === 0) continue;
1052
+ const dir = base + row * 3;
1053
+ const r = base + C_ROWS + row * ROW;
1054
+ for (let k = 0; k < 3; k++) {
1055
+ vel[b + k] = at2(vel, b + k) + at2(contacts, dir + k) * lambda;
1056
+ omega[b + k] = at2(omega, b + k) + at2(contacts, r + 9 + k) * lambda;
1057
+ }
1058
+ if (ai >= 0) {
1059
+ const a = ai * 3;
1060
+ for (let k = 0; k < 3; k++) {
1061
+ vel[a + k] = at2(vel, a + k) - at2(contacts, dir + k) * lambda;
1062
+ omega[a + k] = at2(omega, a + k) - at2(contacts, r + 3 + k) * lambda;
1063
+ }
1064
+ }
1065
+ }
1066
+ }
1067
+ for (let it = 0; it < iterations; it++) {
1068
+ for (let c = 0; c < contactCount; c++) {
1069
+ const base = c * STRIDE;
1070
+ const b = atInt(contactB, c) * 3;
1071
+ const ai = atInt(contactA, c);
1072
+ const a = ai < 0 ? 0 : ai * 3;
1073
+ let vbx = at2(vel, b);
1074
+ let vby = at2(vel, b + 1);
1075
+ let vbz = at2(vel, b + 2);
1076
+ let wbx = at2(omega, b);
1077
+ let wby = at2(omega, b + 1);
1078
+ let wbz = at2(omega, b + 2);
1079
+ let vax = 0;
1080
+ let vay = 0;
1081
+ let vaz = 0;
1082
+ let wax = 0;
1083
+ let way = 0;
1084
+ let waz = 0;
1085
+ if (ai >= 0) {
1086
+ vax = at2(vel, a);
1087
+ vay = at2(vel, a + 1);
1088
+ vaz = at2(vel, a + 2);
1089
+ wax = at2(omega, a);
1090
+ way = at2(omega, a + 1);
1091
+ waz = at2(omega, a + 2);
1092
+ }
1093
+ let limit = 0;
1094
+ for (let row = 0; row < 3; row++) {
1095
+ const dir = base + row * 3;
1096
+ const r = base + C_ROWS + row * ROW;
1097
+ const dx = at2(contacts, dir);
1098
+ const dy = at2(contacts, dir + 1);
1099
+ const dz = at2(contacts, dir + 2);
1100
+ let v = dx * vbx + dy * vby + dz * vbz + wbx * at2(contacts, r + 6) + wby * at2(contacts, r + 7) + wbz * at2(contacts, r + 8);
1101
+ if (ai >= 0) {
1102
+ v -= dx * vax + dy * vay + dz * vaz + wax * at2(contacts, r) + way * at2(contacts, r + 1) + waz * at2(contacts, r + 2);
1103
+ }
1104
+ const old = at2(contacts, base + C_LAMBDA + row);
1105
+ const mass = at2(contacts, base + C_MASS + row);
1106
+ let next;
1107
+ if (row === 0) {
1108
+ next = old + mass * (at2(contacts, base + C_TARGET) - v);
1109
+ if (next < 0) next = 0;
1110
+ limit = friction * next;
1111
+ } else {
1112
+ next = old - mass * v;
1113
+ if (next > limit) next = limit;
1114
+ else if (next < -limit) next = -limit;
1115
+ }
1116
+ contacts[base + C_LAMBDA + row] = next;
1117
+ const lambda = next - old;
1118
+ vbx += dx * lambda;
1119
+ vby += dy * lambda;
1120
+ vbz += dz * lambda;
1121
+ wbx += at2(contacts, r + 9) * lambda;
1122
+ wby += at2(contacts, r + 10) * lambda;
1123
+ wbz += at2(contacts, r + 11) * lambda;
1124
+ if (ai >= 0) {
1125
+ vax -= dx * lambda;
1126
+ vay -= dy * lambda;
1127
+ vaz -= dz * lambda;
1128
+ wax -= at2(contacts, r + 3) * lambda;
1129
+ way -= at2(contacts, r + 4) * lambda;
1130
+ waz -= at2(contacts, r + 5) * lambda;
1131
+ }
1132
+ }
1133
+ vel[b] = vbx;
1134
+ vel[b + 1] = vby;
1135
+ vel[b + 2] = vbz;
1136
+ omega[b] = wbx;
1137
+ omega[b + 1] = wby;
1138
+ omega[b + 2] = wbz;
1139
+ if (ai >= 0) {
1140
+ vel[a] = vax;
1141
+ vel[a + 1] = vay;
1142
+ vel[a + 2] = vaz;
1143
+ omega[a] = wax;
1144
+ omega[a + 1] = way;
1145
+ omega[a + 2] = waz;
1146
+ }
1147
+ }
1148
+ }
1149
+ for (let c = 0; c < contactCount; c++) {
1150
+ const slot = atInt(contactSlot, c);
1151
+ const base = c * STRIDE + C_LAMBDA;
1152
+ slotLambda[slot * 3] = at2(contacts, base);
1153
+ slotLambda[slot * 3 + 1] = at2(contacts, base + 1);
1154
+ slotLambda[slot * 3 + 2] = at2(contacts, base + 2);
1155
+ slotStep[slot] = stepNumber + 1;
1156
+ }
1157
+ }
1158
+ return {
1159
+ get count() {
1160
+ return count;
1161
+ },
1162
+ add(shape, position, orientation, velocity, angularVelocity) {
1163
+ if (count >= MAX_BODIES) throw new RangeError(`at most ${MAX_BODIES} bodies per world`);
1164
+ const i = count;
1165
+ const body = getBodyShape(shape);
1166
+ checkHull(body.vertices);
1167
+ shapes.push(body);
1168
+ bodyInvInertia.set(body.invInertia, i * 3);
1169
+ bodyRadius[i] = body.radius;
1170
+ pos.set(position, i * 3);
1171
+ vel.set(velocity, i * 3);
1172
+ omega.set(angularVelocity, i * 3);
1173
+ const [x, y, z, w] = orientation;
1174
+ const len = Math.sqrt(x * x + y * y + z * z + w * w);
1175
+ if (len > EPSILON) quat.set([x / len, y / len, z / len, w / len], i * 4);
1176
+ else quat.set([0, 0, 0, 1], i * 4);
1177
+ count++;
1178
+ return i;
1179
+ },
1180
+ /** One fixed step of DT: semi-implicit Euler velocity update (gravity, damping), contacts,
1181
+ * sequential impulses, then position and first-order quaternion integration
1182
+ * q += ½·DT·(ω, 0) ⊗ q with renormalization. */
1183
+ step() {
1184
+ const gdt = gravity * DT;
1185
+ for (let i = 0; i < count; i++) {
1186
+ const v = i * 3;
1187
+ vel[v + 1] = at2(vel, v + 1) + gdt;
1188
+ for (let k = 0; k < 3; k++) {
1189
+ vel[v + k] = at2(vel, v + k) * linearFactor;
1190
+ omega[v + k] = at2(omega, v + k) * angularFactor;
1191
+ }
1192
+ updateInertia(i);
1193
+ }
1194
+ collide();
1195
+ solve();
1196
+ stepNumber++;
1197
+ const half = 0.5 * DT;
1198
+ for (let i = 0; i < count; i++) {
1199
+ const v = i * 3;
1200
+ pos[v] = at2(pos, v) + at2(vel, v) * DT;
1201
+ pos[v + 1] = at2(pos, v + 1) + at2(vel, v + 1) * DT;
1202
+ pos[v + 2] = at2(pos, v + 2) + at2(vel, v + 2) * DT;
1203
+ const wx = at2(omega, v);
1204
+ const wy = at2(omega, v + 1);
1205
+ const wz = at2(omega, v + 2);
1206
+ const q = i * 4;
1207
+ const qx = at2(quat, q);
1208
+ const qy = at2(quat, q + 1);
1209
+ const qz = at2(quat, q + 2);
1210
+ const qw = at2(quat, q + 3);
1211
+ const nx = qx + half * (wx * qw + wy * qz - wz * qy);
1212
+ const ny = qy + half * (-wx * qz + wy * qw + wz * qx);
1213
+ const nz = qz + half * (wx * qy - wy * qx + wz * qw);
1214
+ const nw = qw + half * (-wx * qx - wy * qy - wz * qz);
1215
+ const len = Math.sqrt(nx * nx + ny * ny + nz * nz + nw * nw);
1216
+ if (len > EPSILON) {
1217
+ quat[q] = nx / len;
1218
+ quat[q + 1] = ny / len;
1219
+ quat[q + 2] = nz / len;
1220
+ quat[q + 3] = nw / len;
1221
+ } else {
1222
+ quat[q] = 0;
1223
+ quat[q + 1] = 0;
1224
+ quat[q + 2] = 0;
1225
+ quat[q + 3] = 1;
1226
+ }
1227
+ }
1228
+ },
1229
+ read(i, out, offset) {
1230
+ checkIndex(i);
1231
+ for (let k = 0; k < 3; k++) out[offset + k] = at2(pos, i * 3 + k);
1232
+ for (let k = 0; k < 4; k++) out[offset + 3 + k] = at2(quat, i * 4 + k);
1233
+ },
1234
+ linearSpeedSq(i) {
1235
+ checkIndex(i);
1236
+ const x = at2(vel, i * 3);
1237
+ const y = at2(vel, i * 3 + 1);
1238
+ const z = at2(vel, i * 3 + 2);
1239
+ return x * x + y * y + z * z;
1240
+ },
1241
+ angularSpeedSq(i) {
1242
+ checkIndex(i);
1243
+ const x = at2(omega, i * 3);
1244
+ const y = at2(omega, i * 3 + 1);
1245
+ const z = at2(omega, i * 3 + 2);
1246
+ return x * x + y * y + z * z;
1247
+ }
1248
+ };
1249
+ }
1250
+
1251
+ // src/physics/sim.ts
1252
+ var MAX_STEPS_PER_WAVE = 720;
1253
+ var SETTLE_LINEAR = 0.05;
1254
+ var SETTLE_ANGULAR = 0.1;
1255
+ var SETTLE_STEPS = 24;
1256
+ var FRAME = 7;
1257
+ var LINEAR_SQ = SETTLE_LINEAR * SETTLE_LINEAR;
1258
+ var ANGULAR_SQ = SETTLE_ANGULAR * SETTLE_ANGULAR;
1259
+ function simulate(input) {
1260
+ const { shapes, waves, bounds } = input;
1261
+ if (shapes.length !== waves.length) {
1262
+ throw new RangeError("shapes and waves must have the same length");
1263
+ }
1264
+ const rng = chunkOEMS7CZP_cjs.createSeedRng(input.seed);
1265
+ const world = createWorld(bounds);
1266
+ const bodies = shapes.map((shape, i) => ({
1267
+ shape,
1268
+ wave: waves[i] ?? 0,
1269
+ startStep: 0,
1270
+ frames: new Float32Array(FRAME * 256),
1271
+ length: 0,
1272
+ index: -1
1273
+ }));
1274
+ const values = [];
1275
+ for (const w of waves) if (!values.includes(w)) values.push(w);
1276
+ values.sort((a, b) => a - b);
1277
+ const present = [];
1278
+ let totalSteps = 0;
1279
+ let settled = true;
1280
+ const record = (body) => {
1281
+ if (body.length + FRAME > body.frames.length) {
1282
+ const grown = new Float32Array(body.frames.length * 2);
1283
+ grown.set(body.frames);
1284
+ body.frames = grown;
1285
+ }
1286
+ world.read(body.index, body.frames, body.length);
1287
+ body.length += FRAME;
1288
+ };
1289
+ for (const wave of values) {
1290
+ const members = bodies.filter((b) => b.wave === wave);
1291
+ const states = throwWave(
1292
+ rng,
1293
+ members.map((b) => getBodyShape(b.shape).radius),
1294
+ bounds
1295
+ );
1296
+ states.forEach((s, k) => {
1297
+ const body = members[k];
1298
+ if (body === void 0) return;
1299
+ body.index = world.add(body.shape, s.position, s.orientation, s.velocity, s.angularVelocity);
1300
+ body.startStep = totalSteps;
1301
+ present.push(body);
1302
+ record(body);
1303
+ });
1304
+ let calm = 0;
1305
+ let steps = 0;
1306
+ while (calm < SETTLE_STEPS && steps < MAX_STEPS_PER_WAVE) {
1307
+ world.step();
1308
+ steps++;
1309
+ totalSteps++;
1310
+ let still = true;
1311
+ for (const body of present) {
1312
+ record(body);
1313
+ if (world.linearSpeedSq(body.index) >= LINEAR_SQ || world.angularSpeedSq(body.index) >= ANGULAR_SQ) {
1314
+ still = false;
1315
+ }
1316
+ }
1317
+ calm = still ? calm + 1 : 0;
1318
+ }
1319
+ if (calm < SETTLE_STEPS) settled = false;
1320
+ }
1321
+ const thrown = bodies.filter((b) => b.index >= 0);
1322
+ const poses = thrown.map((body) => {
1323
+ const pose = new Float64Array(FRAME);
1324
+ world.read(body.index, pose, 0);
1325
+ return pose;
1326
+ });
1327
+ const xz = new Float64Array(poses.flatMap((p) => [p[0] ?? 0, p[2] ?? 0]));
1328
+ separate(
1329
+ xz,
1330
+ thrown.map((b) => getBodyShape(b.shape).radius),
1331
+ bounds
1332
+ );
1333
+ const ends = bodies.map((body) => {
1334
+ const k = thrown.indexOf(body);
1335
+ const pose = poses[k];
1336
+ if (pose === void 0) return { up: 0, tail: null };
1337
+ const tail = flattenTail(body.shape, pose, xz[2 * k] ?? 0, xz[2 * k + 1] ?? 0);
1338
+ return { up: upReadout(body.shape, pose), tail };
1339
+ });
1340
+ const extra = ends.some((e) => e.tail !== null) ? FLATTEN_STEPS : 0;
1341
+ const tracks = bodies.map((body, i) => {
1342
+ const frames = new Float32Array(body.length + extra * FRAME);
1343
+ frames.set(body.frames.subarray(0, body.length));
1344
+ const tail = ends[i]?.tail;
1345
+ for (let k = 0; k < extra; k++) {
1346
+ const at4 = body.length + k * FRAME;
1347
+ if (tail) frames.set(tail.subarray(k * FRAME, (k + 1) * FRAME), at4);
1348
+ else frames.copyWithin(at4, body.length - FRAME, body.length);
1349
+ }
1350
+ return {
1351
+ shape: body.shape,
1352
+ wave: body.wave,
1353
+ startStep: body.startStep,
1354
+ frames,
1355
+ upReadout: ends[i]?.up ?? 0
1356
+ };
1357
+ });
1358
+ return { totalSteps: totalSteps + extra, tracks, settled };
1359
+ }
1360
+ var FNV_OFFSET = 2166136261;
1361
+ var FNV_PRIME = 16777619;
1362
+ function fnvByte(hash, byte) {
1363
+ return Math.imul(hash ^ byte, FNV_PRIME) >>> 0;
1364
+ }
1365
+ function trackHash(result) {
1366
+ let hash = FNV_OFFSET;
1367
+ for (const track of result.tracks) {
1368
+ const f = track.frames;
1369
+ const words = new Uint32Array(f.buffer, f.byteOffset, f.length);
1370
+ for (const word of words) {
1371
+ hash = fnvByte(hash, word & 255);
1372
+ hash = fnvByte(hash, word >>> 8 & 255);
1373
+ hash = fnvByte(hash, word >>> 16 & 255);
1374
+ hash = fnvByte(hash, word >>> 24);
1375
+ }
1376
+ }
1377
+ return hash.toString(16).padStart(8, "0");
1378
+ }
1379
+
1380
+ // src/physics/plan.ts
1381
+ var MAX_BODIES2 = 30;
1382
+ function slots(event) {
1383
+ const out = [];
1384
+ for (let i = 0; i < event.dice.length; i++) {
1385
+ const d = event.dice[i];
1386
+ if (d === void 0) break;
1387
+ const { type, value, wave } = d;
1388
+ if (type === "d100") {
1389
+ if (out.length + 2 > MAX_BODIES2) break;
1390
+ let tens = null;
1391
+ let ones = null;
1392
+ if (value !== null) {
1393
+ ones = value % 10;
1394
+ tens = value === 100 ? 0 : (value - ones) / 10;
1395
+ }
1396
+ out.push({ die: i, labelSet: "d100tens", wave, value: tens });
1397
+ out.push({ die: i, labelSet: "d100ones", wave, value: ones });
1398
+ } else {
1399
+ if (out.length + 1 > MAX_BODIES2) break;
1400
+ out.push({ die: i, labelSet: type, wave, value });
1401
+ }
1402
+ }
1403
+ return out;
1404
+ }
1405
+ function planRoll(event, bounds) {
1406
+ const list = slots(event);
1407
+ const shapes = list.map((s) => shapeOf(s.labelSet));
1408
+ const result = simulate({ seed: event.seed, shapes, waves: list.map((s) => s.wave), bounds });
1409
+ const bodies = list.map((slot, k) => {
1410
+ const track = result.tracks[k];
1411
+ if (track === void 0) throw new Error(`missing track ${k}`);
1412
+ const remap = slot.value === null ? IDENTITY : findRemap(track.shape, readoutForValue(slot.labelSet, slot.value), track.upReadout);
1413
+ return {
1414
+ die: slot.die,
1415
+ labelSet: slot.labelSet,
1416
+ shape: track.shape,
1417
+ wave: track.wave,
1418
+ startStep: track.startStep,
1419
+ frames: track.frames,
1420
+ remap,
1421
+ upReadout: track.upReadout,
1422
+ value: slot.value
1423
+ };
1424
+ });
1425
+ return {
1426
+ totalSteps: result.totalSteps,
1427
+ settled: result.settled,
1428
+ hash: trackHash(result),
1429
+ bodies
1430
+ };
1431
+ }
1432
+
1433
+ // src/skins/patterns.ts
1434
+ var NOISE = `uvec3 pcg3(uvec3 v) {
1435
+ v = v * 1664525u + 1013904223u;
1436
+ v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
1437
+ v ^= v >> 16u;
1438
+ v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
1439
+ return v;
1440
+ }
1441
+ vec3 hash3(vec3 c) {
1442
+ return vec3(pcg3(uvec3(c + 4096.0))) / 4294967296.0;
1443
+ }
1444
+ float vnoise(vec3 p) {
1445
+ vec3 i = floor(p);
1446
+ vec3 f = fract(p);
1447
+ f = f * f * (3.0 - 2.0 * f);
1448
+ vec2 e = vec2(1.0, 0.0);
1449
+ return mix(
1450
+ mix(mix(hash3(i).x, hash3(i + e.xyy).x, f.x), mix(hash3(i + e.yxy).x, hash3(i + e.xxy).x, f.x), f.y),
1451
+ mix(mix(hash3(i + e.yyx).x, hash3(i + e.xyx).x, f.x), mix(hash3(i + e.yxx).x, hash3(i + e.xxx).x, f.x), f.y),
1452
+ f.z);
1453
+ }
1454
+ float fbm(vec3 p) {
1455
+ float s = 0.0;
1456
+ float w = 0.5;
1457
+ for (int k = 0; k < 4; k++) {
1458
+ s += w * vnoise(p);
1459
+ p = p * 2.03 + 1.7;
1460
+ w *= 0.5;
1461
+ }
1462
+ return s;
1463
+ }
1464
+ `;
1465
+ var SIGNATURE = "vec3 pattern(vec3 p, vec3 n, vec3 a, vec3 b) {";
1466
+ var PATTERNS = {
1467
+ none: `${SIGNATURE} return a; }`,
1468
+ gradient: `${SIGNATURE} return mix(a, b, smoothstep(-0.8, 0.8, p.y)); }`,
1469
+ speckle: `${SIGNATURE}
1470
+ vec3 q = p * 14.0;
1471
+ vec3 h = hash3(floor(q));
1472
+ float d = length(fract(q) - 0.25 - 0.5 * h);
1473
+ return mix(a, b, step(h.x, 0.5) * (1.0 - smoothstep(0.12, 0.2, d)));
1474
+ }`,
1475
+ marble: `${SIGNATURE}
1476
+ float t = fbm(p * 2.5);
1477
+ float v = abs(fract(p.x * 0.9 + p.y * 0.6 + 2.5 * t) - 0.5) * 2.0;
1478
+ return mix(mix(a, b, 0.5 * t), b, 1.0 - smoothstep(0.0, 0.12, v));
1479
+ }`,
1480
+ wood: `${SIGNATURE}
1481
+ float ring = fract(length(p.xz) * 9.0 + 2.0 * fbm(p * vec3(2.0, 0.4, 2.0)));
1482
+ float grain = vnoise(p * vec3(40.0, 2.0, 40.0));
1483
+ return mix(a, b, clamp(ring * ring + 0.2 * grain, 0.0, 1.0));
1484
+ }`,
1485
+ swirl: `${SIGNATURE}
1486
+ float t = atan(p.z, p.x) * 0.4774648 + length(p.xz) * 2.5 + p.y * 0.6;
1487
+ return mix(a, b, smoothstep(0.3, 0.7, abs(fract(t) - 0.5) * 2.0));
1488
+ }`
1489
+ };
1490
+
1491
+ // src/skins/presets.ts
1492
+ var classic = {
1493
+ material: "plastic",
1494
+ color: "#f4f1ea",
1495
+ labelColor: "#1a1a1a",
1496
+ labelStyle: "printed"
1497
+ };
1498
+ var obsidian = {
1499
+ material: "stone",
1500
+ color: ["#0b0b0f", "#2c2c36"],
1501
+ labelColor: "#d9b45a",
1502
+ pattern: "marble",
1503
+ labelStyle: "engraved"
1504
+ };
1505
+ var brass = {
1506
+ material: "metal",
1507
+ color: ["#b8893a", "#8a6526"],
1508
+ labelColor: "#1b1209",
1509
+ pattern: "gradient",
1510
+ labelStyle: "engraved"
1511
+ };
1512
+ var oak = {
1513
+ material: "wood",
1514
+ color: ["#a87444", "#6b4424"],
1515
+ labelColor: "#f6ead6",
1516
+ pattern: "wood",
1517
+ labelStyle: "engraved"
1518
+ };
1519
+ var sapphire = {
1520
+ material: "glass",
1521
+ color: "#1d4fd6",
1522
+ labelColor: "#ffffff",
1523
+ labelStyle: "printed"
1524
+ };
1525
+ var ruby = {
1526
+ material: "gem",
1527
+ color: ["#8c0a22", "#b5142f"],
1528
+ labelColor: "#ffe7a3",
1529
+ pattern: "swirl",
1530
+ labelStyle: "printed"
1531
+ };
1532
+ var emerald = {
1533
+ material: { metalness: 0, roughness: 0.05, clearcoat: 1, transmission: 0.75, tint: 0.95 },
1534
+ color: ["#003d1a", "#00873f"],
1535
+ labelColor: "#fff4c8",
1536
+ pattern: "gradient",
1537
+ labelStyle: "printed"
1538
+ };
1539
+ var amethyst = {
1540
+ material: "gem",
1541
+ color: ["#5b1a8c", "#8a3fc0"],
1542
+ labelColor: "#f3e8ff",
1543
+ pattern: "swirl",
1544
+ labelStyle: "printed"
1545
+ };
1546
+ var topaz = {
1547
+ material: "gem",
1548
+ color: ["#b9770e", "#e6b04a"],
1549
+ labelColor: "#3a1e05",
1550
+ pattern: "gradient",
1551
+ labelStyle: "printed"
1552
+ };
1553
+ var aquamarine = {
1554
+ material: { metalness: 0, roughness: 0.05, clearcoat: 1, transmission: 1, tint: 0.6 },
1555
+ color: "#5fc4d8",
1556
+ labelColor: "#0b2f6b",
1557
+ labelStyle: "printed"
1558
+ };
1559
+ var smoke = {
1560
+ material: { metalness: 0, roughness: 0.05, clearcoat: 1, transmission: 0.6, tint: 0.35 },
1561
+ color: ["#1e150f", "#3a2c20"],
1562
+ labelColor: "#ffffff",
1563
+ pattern: "gradient",
1564
+ labelStyle: "printed"
1565
+ };
1566
+ var skinRegistry = /* @__PURE__ */ new Map([
1567
+ ["classic", classic],
1568
+ ["obsidian", obsidian],
1569
+ ["brass", brass],
1570
+ ["oak", oak],
1571
+ ["sapphire", sapphire],
1572
+ ["ruby", ruby],
1573
+ ["emerald", emerald],
1574
+ ["amethyst", amethyst],
1575
+ ["topaz", topaz],
1576
+ ["aquamarine", aquamarine],
1577
+ ["smoke", smoke]
1578
+ ]);
1579
+ function resolveSkin(ref) {
1580
+ if (typeof ref === "object") return ref;
1581
+ return (ref === void 0 ? void 0 : skinRegistry.get(ref)) ?? classic;
1582
+ }
1583
+ function defineSkin(base, overrides) {
1584
+ return { ...resolveSkin(base), ...overrides };
1585
+ }
1586
+ function registerSkin(name, skin) {
1587
+ if (name === "") throw new TypeError("skin name must not be empty");
1588
+ skinRegistry.set(name, skin);
1589
+ }
1590
+
1591
+ // src/render/shaders.ts
1592
+ var ATLAS_COLUMNS = 20;
1593
+ var ATLAS_ROWS = 9;
1594
+ var LIGHT_DIR = [-0.5, 0.7071, -0.5];
1595
+ var DIE_VERTEX = `#version 300 es
1596
+ layout(location=0) in vec3 aPos;
1597
+ layout(location=1) in vec3 aNormal;
1598
+ layout(location=2) in vec3 aLabel;
1599
+ layout(location=3) in vec4 aInst;
1600
+ layout(location=4) in vec4 aQuat;
1601
+ uniform mat4 uVP;
1602
+ uniform float uScale;
1603
+ out vec3 vObj;
1604
+ out vec3 vObjN;
1605
+ out vec3 vN;
1606
+ out vec3 vW;
1607
+ out vec2 vUV;
1608
+ flat out vec2 vCell;
1609
+ vec3 rot(vec4 q, vec3 v) {
1610
+ vec3 t = 2.0 * cross(q.xyz, v);
1611
+ return v + q.w * t + cross(q.xyz, t);
1612
+ }
1613
+ void main() {
1614
+ vObj = aPos;
1615
+ vObjN = aNormal;
1616
+ vN = rot(aQuat, aNormal);
1617
+ vW = (rot(aQuat, aPos) + aInst.xyz) * uScale;
1618
+ vUV = aLabel.xy;
1619
+ vCell = aLabel.z < 0.0 || aInst.w < 0.0 ? vec2(-1.0) : vec2(aLabel.z, aInst.w);
1620
+ gl_Position = uVP * vec4(vW, 1.0);
1621
+ }
1622
+ `;
1623
+ var bump = (h, shade) => `vec3 r1 = cross(dpy, N);
1624
+ vec3 r2 = cross(N, dpx);
1625
+ float det = dot(dpx, r1);
1626
+ N = normalize(abs(det) * N - sign(det) * ${h} * (da.x * r1 + da.y * r2));
1627
+ base = mix(base, uLabel, a) * (1.0 ${shade} 0.15 * a);`;
1628
+ var LABEL = {
1629
+ printed: "base = mix(base, uLabel, a);",
1630
+ engraved: bump("-0.02", "-"),
1631
+ embossed: bump("0.02", "+")
1632
+ };
1633
+ var GLASS = `float x = min((0.8 - uGlass.x) * 2.5, 1.5);
1634
+ float fr = 0.04 + 0.96 * pow(1.0 - nv, 5.0);
1635
+ float path = 1.0 - nv;
1636
+ vec3 tint = pow(max(base, vec3(1e-4)), vec3(uGlass.y + (1.0 + uGlass.y / 1.5) * path));
1637
+ vec3 g = env(refract(-V, N, 1.0 / 1.5), 0.0) * tint * mix(4.0, 3.0, x);
1638
+ if (gl_FrontFacing) {
1639
+ vec3 hl = (spec(N, V, LIGHT_DIR, vec3(0.04), 0.18 - 0.06 * uGlass.z) * 2.0
1640
+ + spec(N, V, LIGHT_DIR, vec3(0.04), 0.4)) * LIGHT * nl
1641
+ + fr * env(R, 0.0) * 2.0 + vec3(pow(path, 3.0) * mix(1.6, 0.8, x));
1642
+ vec3 h = hash3(floor(vObj * 10.0));
1643
+ vec3 Rj = reflect(-V, normalize(N + (h - 0.5) * 0.8));
1644
+ hl += uGlass.z * step(0.8, h.x) * pow(max(dot(Rj, LIGHT_DIR), 0.0), 12.0) * 6.0;
1645
+ g += hl;
1646
+ al = max(mix(mix(0.12, 0.35, x), mix(0.85, 0.9, x), path), clamp(dot(hl, vec3(0.5)), 0.0, 1.0));
1647
+ } else {
1648
+ al = mix(mix(0.6, 0.8, x), mix(0.85, 0.95, x), path);
1649
+ }
1650
+ c = mix(g, c, lab);
1651
+ al = mix(al, 1.0, lab);`;
1652
+ function dieFragment(label, transparent, pattern) {
1653
+ const flat = label === "printed";
1654
+ return `#version 300 es
1655
+ precision highp float;
1656
+ in vec3 vObj;
1657
+ in vec3 vObjN;
1658
+ in vec3 vN;
1659
+ in vec3 vW;
1660
+ in vec2 vUV;
1661
+ flat in vec2 vCell;
1662
+ uniform vec3 uCam;
1663
+ uniform vec3 uBase;
1664
+ uniform vec3 uBase2;
1665
+ uniform vec3 uLabel;
1666
+ uniform vec3 uMat;
1667
+ uniform vec3 uGlass;
1668
+ uniform float uAlpha;
1669
+ uniform sampler2D uAtlas;
1670
+ out vec4 oColor;
1671
+ const float PI = 3.14159265;
1672
+ const vec3 LIGHT_DIR = vec3(${LIGHT_DIR.join(", ")});
1673
+ const vec3 LIGHT = vec3(3.2);
1674
+ const vec3 SKY = vec3(0.42, 0.44, 0.48);
1675
+ const vec3 GROUND = vec3(0.26, 0.24, 0.22);
1676
+ const float EXPOSURE = 0.55;
1677
+ ${NOISE}
1678
+ ${pattern}
1679
+ vec3 hemi(vec3 d) {
1680
+ return mix(GROUND, SKY, d.y * 0.5 + 0.5);
1681
+ }
1682
+ vec3 env(vec3 d, float r) {
1683
+ return mix(mix(GROUND, SKY, smoothstep(-0.2, 0.2, d.y)), hemi(d), r);
1684
+ }
1685
+ vec3 fresnel(vec3 f0, float c) {
1686
+ return f0 + (1.0 - f0) * pow(1.0 - c, 5.0);
1687
+ }
1688
+ vec3 spec(vec3 N, vec3 V, vec3 L, vec3 f0, float r) {
1689
+ vec3 H = normalize(V + L);
1690
+ float nl = max(dot(N, L), 0.001);
1691
+ float nv = max(dot(N, V), 0.001);
1692
+ float nh = max(dot(N, H), 0.0);
1693
+ float a = r * r;
1694
+ float a2 = a * a;
1695
+ float d = nh * nh * (a2 - 1.0) + 1.0;
1696
+ float D = a2 / (PI * d * d);
1697
+ float k = (r + 1.0) * (r + 1.0) / 8.0;
1698
+ float G = nv / (nv * (1.0 - k) + k) * nl / (nl * (1.0 - k) + k);
1699
+ return D * G * fresnel(f0, max(dot(V, H), 0.0)) / (4.0 * nv * nl);
1700
+ }
1701
+ void main() {
1702
+ vec3 base = pattern(vObj, normalize(vObjN), uBase, uBase2);
1703
+ vec3 N = normalize(vN);
1704
+ if (!gl_FrontFacing) N = -N;
1705
+ // Sample and differentiate in uniform control flow; apply inside the branch.
1706
+ vec2 uv = clamp(vUV, 0.0, 1.0);
1707
+ vec2 cell = max(vCell, 0.0);
1708
+ float a = texture(uAtlas, vec2((cell.x + uv.x) / ${ATLAS_COLUMNS}.0, (cell.y + 1.0 - uv.y) / ${ATLAS_ROWS}.0)).r;
1709
+ ${flat ? "" : "vec3 dpx = dFdx(vW);\n vec3 dpy = dFdy(vW);\n vec2 da = vec2(dFdx(a), dFdy(a));"}
1710
+ float lab = 0.0;
1711
+ if (vCell.x >= 0.0 && uv == vUV && gl_FrontFacing) {
1712
+ lab = a;
1713
+ ${LABEL[label]}
1714
+ }
1715
+ // Label fill is dielectric paint: on metal it stays dark instead of mirroring the environment.
1716
+ float m = uMat.x * (1.0 - min(2.0 * lab, 1.0));
1717
+ float r = uMat.y;
1718
+ float cc = uMat.z;
1719
+ vec3 V = normalize(uCam); // orthographic: one view direction everywhere
1720
+ vec3 R = reflect(-V, N);
1721
+ float nl = max(dot(N, LIGHT_DIR), 0.0);
1722
+ float nv = max(dot(N, V), 0.001);
1723
+ vec3 f0 = mix(vec3(0.04), base, m);
1724
+ vec3 F = fresnel(f0, max(dot(V, normalize(V + LIGHT_DIR)), 0.0));
1725
+ vec3 c = ((1.0 - F) * (1.0 - m) * base / PI + spec(N, V, LIGHT_DIR, f0, r)) * LIGHT * nl;
1726
+ vec3 Fa = fresnel(f0, nv) * (1.0 - r * 0.5);
1727
+ c += (1.0 - Fa) * (1.0 - m) * base * hemi(N) + Fa * env(R, r);
1728
+ if (cc > 0.0) {
1729
+ float fc = 0.04 + 0.96 * pow(1.0 - nv, 5.0);
1730
+ c = c * (1.0 - cc * fc) + cc * (spec(N, V, LIGHT_DIR, vec3(0.04), 0.15) * LIGHT * nl + fc * env(R, 0.15));
1731
+ }
1732
+ c *= 1.0 - 0.7 * lab * uMat.x;
1733
+ float al = 1.0;
1734
+ ${transparent ? GLASS : ""}
1735
+ c *= EXPOSURE;
1736
+ c = clamp(c * (2.51 * c + 0.03) / (c * (2.43 * c + 0.59) + 0.14), 0.0, 1.0);
1737
+ c = pow(c, vec3(1.0 / 2.2));
1738
+ al *= uAlpha;
1739
+ oColor = vec4(c * al, al);
1740
+ }
1741
+ `;
1742
+ }
1743
+ var SHADOW_VERTEX = `#version 300 es
1744
+ layout(location=0) in vec2 aCorner;
1745
+ layout(location=1) in vec4 aShadow;
1746
+ uniform mat4 uVP;
1747
+ out vec2 vC;
1748
+ out float vA;
1749
+ void main() {
1750
+ vC = aCorner;
1751
+ vA = aShadow.w;
1752
+ gl_Position = uVP * vec4(aShadow.x + aCorner.x * aShadow.z, 0.001, aShadow.y + aCorner.y * aShadow.z, 1.0);
1753
+ }
1754
+ `;
1755
+ var SHADOW_FRAGMENT = `#version 300 es
1756
+ precision highp float;
1757
+ in vec2 vC;
1758
+ in float vA;
1759
+ uniform float uAlpha;
1760
+ out vec4 oColor;
1761
+ void main() {
1762
+ float a = vA * uAlpha * (1.0 - smoothstep(0.0, 1.0, length(vC)));
1763
+ oColor = vec4(0.0, 0.0, 0.0, a);
1764
+ }
1765
+ `;
1766
+
1767
+ // src/render/atlas.ts
1768
+ var CELL = 128;
1769
+ var PAD = 8;
1770
+ var ATLAS_MAX_LEVEL = Math.log2(PAD);
1771
+ var INK_HEIGHT = 0.8 * CELL;
1772
+ var INK_WIDTH = CELL - 2 * PAD;
1773
+ var PROBE = 100;
1774
+ var LABEL_SETS = [
1775
+ "d4",
1776
+ "d6",
1777
+ "d8",
1778
+ "d10",
1779
+ "d12",
1780
+ "d20",
1781
+ "d100tens",
1782
+ "d100ones",
1783
+ "dF"
1784
+ ];
1785
+ var SENTINEL = "10px serif";
1786
+ function pickFont(ctx, fonts) {
1787
+ for (const font of fonts) {
1788
+ ctx.font = SENTINEL;
1789
+ const sentinel = ctx.font;
1790
+ ctx.font = `bold ${PROBE}px ${font}`;
1791
+ if (ctx.font !== sentinel) return font;
1792
+ }
1793
+ return "system-ui";
1794
+ }
1795
+ function buildAtlas(requested, labels = {}, fallback = "system-ui") {
1796
+ const canvas = document.createElement("canvas");
1797
+ canvas.width = ATLAS_COLUMNS * CELL;
1798
+ canvas.height = ATLAS_ROWS * CELL;
1799
+ const ctx = canvas.getContext("2d");
1800
+ if (ctx === null) return null;
1801
+ const font = pickFont(ctx, [requested, fallback, "system-ui"]);
1802
+ ctx.fillStyle = "#fff";
1803
+ ctx.textAlign = "center";
1804
+ ctx.textBaseline = "alphabetic";
1805
+ LABEL_SETS.forEach((set, row) => {
1806
+ const count = getPolyhedron(shapeOf(set)).readouts.length;
1807
+ for (let i = 0; i < count; i++) {
1808
+ const custom = labels[set]?.[labelIndex(set, i)];
1809
+ const text = custom ?? labelText(set, i);
1810
+ if (text === "") continue;
1811
+ const underline = custom === void 0 && labelUnderline(set, i);
1812
+ ctx.font = `bold ${PROBE}px ${font}`;
1813
+ const probe = ctx.measureText(text);
1814
+ const blockH = probe.actualBoundingBoxAscent + probe.actualBoundingBoxDescent + (underline ? 0.16 * PROBE : 0);
1815
+ const inkW = probe.actualBoundingBoxLeft + probe.actualBoundingBoxRight;
1816
+ if (blockH <= 0 || inkW <= 0) continue;
1817
+ const size = PROBE * Math.min(INK_HEIGHT / blockH, INK_WIDTH / inkW);
1818
+ ctx.font = `bold ${size}px ${font}`;
1819
+ const m = ctx.measureText(text);
1820
+ const ascent = m.actualBoundingBoxAscent;
1821
+ const descent = m.actualBoundingBoxDescent;
1822
+ const height = ascent + descent + (underline ? 0.16 * size : 0);
1823
+ const cx = (i + 0.5) * CELL + (m.actualBoundingBoxLeft - m.actualBoundingBoxRight) / 2;
1824
+ const baseline = (row + 0.5) * CELL - height / 2 + ascent;
1825
+ ctx.fillText(text, cx, baseline);
1826
+ if (underline) {
1827
+ const half = Math.max(m.actualBoundingBoxLeft + m.actualBoundingBoxRight, 0.4 * size) / 2;
1828
+ ctx.fillRect(cx - half, baseline + descent + 0.08 * size, 2 * half, 0.08 * size);
1829
+ }
1830
+ }
1831
+ });
1832
+ return canvas;
1833
+ }
1834
+
1835
+ // src/render/camera.ts
1836
+ var CAMERA_DISTANCE = 14;
1837
+ var INSET_X = 0.6;
1838
+ var INSET_Z = 0.6;
1839
+ var HALF_Z = 5.1;
1840
+ var AREA_PER_DIE = 5;
1841
+ var POSITION = Object.freeze([
1842
+ 0,
1843
+ CAMERA_DISTANCE,
1844
+ 0
1845
+ ]);
1846
+ var TOP = 20;
1847
+ var BOTTOM = -1;
1848
+ function viewProjection(aspect, out) {
1849
+ out.fill(0);
1850
+ out[0] = 1 / (aspect * HALF_Z);
1851
+ out[6] = -2 / (TOP - BOTTOM);
1852
+ out[9] = -1 / HALF_Z;
1853
+ out[14] = (TOP + BOTTOM) / (TOP - BOTTOM);
1854
+ out[15] = 1;
1855
+ return out;
1856
+ }
1857
+ function cameraPosition() {
1858
+ return POSITION;
1859
+ }
1860
+ function positive(name, value) {
1861
+ if (!Number.isFinite(value) || value <= 0) {
1862
+ throw new RangeError(`${name} must be a positive finite number, got ${value}`);
1863
+ }
1864
+ }
1865
+ function trayBounds(aspect, dieScale) {
1866
+ positive("aspect", aspect);
1867
+ positive("dieScale", dieScale);
1868
+ const halfX = aspect * HALF_Z;
1869
+ const ceil = (v) => Math.ceil(v * 4) / 4 + 0;
1870
+ const floor = (v) => Math.floor(v * 4) / 4 + 0;
1871
+ return {
1872
+ minX: Math.min(ceil(-halfX / dieScale + INSET_X), -2),
1873
+ maxX: Math.max(floor(halfX / dieScale - INSET_X), 2),
1874
+ minZ: ceil(-HALF_Z / dieScale + INSET_Z),
1875
+ maxZ: floor(HALF_Z / dieScale - INSET_Z)
1876
+ };
1877
+ }
1878
+ function fitScale(requested, bodyCount, aspect) {
1879
+ let s = requested;
1880
+ for (let k = 1; s > 0.5; k++) {
1881
+ const b = trayBounds(aspect, s);
1882
+ if ((b.maxX - b.minX) * (b.maxZ - b.minZ) >= bodyCount * AREA_PER_DIE) break;
1883
+ s = Math.max((requested * 20 - k) / 20, 0.5);
1884
+ }
1885
+ return s;
1886
+ }
1887
+
1888
+ // src/render/gl.ts
1889
+ var PollyrollShaderError = class extends Error {
1890
+ log;
1891
+ constructor(message, log) {
1892
+ super(message);
1893
+ this.name = "PollyrollShaderError";
1894
+ this.log = log;
1895
+ }
1896
+ };
1897
+ function compile(gl, type, source) {
1898
+ const shader = gl.createShader(type);
1899
+ if (shader === null) throw new PollyrollShaderError("cannot create shader", "");
1900
+ gl.shaderSource(shader, source);
1901
+ gl.compileShader(shader);
1902
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
1903
+ const log = gl.getShaderInfoLog(shader) ?? "";
1904
+ gl.deleteShader(shader);
1905
+ throw new PollyrollShaderError("shader compile failed", log);
1906
+ }
1907
+ return shader;
1908
+ }
1909
+ function createProgram(gl, vertex, fragment) {
1910
+ const vs = compile(gl, gl.VERTEX_SHADER, vertex);
1911
+ let fs;
1912
+ try {
1913
+ fs = compile(gl, gl.FRAGMENT_SHADER, fragment);
1914
+ } catch (error) {
1915
+ gl.deleteShader(vs);
1916
+ throw error;
1917
+ }
1918
+ const program = gl.createProgram();
1919
+ gl.attachShader(program, vs);
1920
+ gl.attachShader(program, fs);
1921
+ gl.linkProgram(program);
1922
+ gl.detachShader(program, vs);
1923
+ gl.detachShader(program, fs);
1924
+ gl.deleteShader(vs);
1925
+ gl.deleteShader(fs);
1926
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1927
+ const log = gl.getProgramInfoLog(program) ?? "";
1928
+ gl.deleteProgram(program);
1929
+ throw new PollyrollShaderError("shader link failed", log);
1930
+ }
1931
+ return program;
1932
+ }
1933
+ function createBuffer(gl, data) {
1934
+ const buffer = gl.createBuffer();
1935
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1936
+ if (typeof data === "number") gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
1937
+ else gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
1938
+ return buffer;
1939
+ }
1940
+ function attrib(gl, location, size, stride, offset, divisor) {
1941
+ gl.enableVertexAttribArray(location);
1942
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, stride * 4, offset * 4);
1943
+ gl.vertexAttribDivisor(location, divisor);
1944
+ }
1945
+
1946
+ // src/render/tray.ts
1947
+ var FADE_MS = 300;
1948
+ var INST = 8;
1949
+ var FRAME2 = 7;
1950
+ var SHADOW_X = 0.5 / LIGHT_DIR[1];
1951
+ var SHADOW_Z = 0.5 / LIGHT_DIR[1];
1952
+ var materialPresets = {
1953
+ plastic: { metalness: 0, roughness: 0.35, clearcoat: 0.3, transmission: 0, tint: 0, sparkle: 0 },
1954
+ metal: { metalness: 1, roughness: 0.3, clearcoat: 0, transmission: 0, tint: 0, sparkle: 0 },
1955
+ wood: { metalness: 0, roughness: 0.7, clearcoat: 0.1, transmission: 0, tint: 0, sparkle: 0 },
1956
+ stone: { metalness: 0, roughness: 0.45, clearcoat: 0.6, transmission: 0, tint: 0, sparkle: 0 },
1957
+ glass: { metalness: 0, roughness: 0.05, clearcoat: 1, transmission: 0.8, tint: 0.6, sparkle: 0 },
1958
+ gem: { metalness: 0, roughness: 0.08, clearcoat: 1, transmission: 0.4, tint: 0.9, sparkle: 1 }
1959
+ };
1960
+ for (const m of Object.values(materialPresets)) Object.freeze(m);
1961
+ Object.freeze(materialPresets);
1962
+ var DIE_UNIFORMS = [
1963
+ "uVP",
1964
+ "uScale",
1965
+ "uCam",
1966
+ "uBase",
1967
+ "uBase2",
1968
+ "uLabel",
1969
+ "uMat",
1970
+ "uGlass",
1971
+ "uAlpha",
1972
+ "uAtlas"
1973
+ ];
1974
+ var MAX_ANISOTROPY = 4;
1975
+ var colorCtx;
1976
+ var colors = /* @__PURE__ */ new Map();
1977
+ function linearColor(css) {
1978
+ let c = colors.get(css);
1979
+ if (c === void 0) {
1980
+ colorCtx ??= document.createElement("canvas").getContext("2d");
1981
+ let text = "#000000";
1982
+ if (colorCtx !== null) {
1983
+ colorCtx.fillStyle = "#000";
1984
+ colorCtx.fillStyle = css;
1985
+ text = String(colorCtx.fillStyle);
1986
+ }
1987
+ const srgb = text.startsWith("#") ? [1, 3, 5].map((i) => parseInt(text.slice(i, i + 2), 16)) : (text.match(/[\d.]+/g) ?? []).map(Number);
1988
+ c = [0, 1, 2].map((i) => Math.pow((srgb[i] ?? 0) / 255, 2.2));
1989
+ colors.set(css, c);
1990
+ }
1991
+ return c;
1992
+ }
1993
+ function skinGpu(skin) {
1994
+ const m = typeof skin.material === "object" ? skin.material : materialPresets[skin.material];
1995
+ const transparent = (m.transmission ?? 0) > 0;
1996
+ const [a, b] = typeof skin.color === "string" ? [skin.color, skin.color] : skin.color;
1997
+ const p = skin.pattern ?? "none";
1998
+ const pattern = typeof p === "object" ? p.glsl : PATTERNS[p];
1999
+ const label = skin.labelStyle ?? "engraved";
2000
+ return {
2001
+ key: `${label}|${transparent}|${pattern}`,
2002
+ fragment: dieFragment(label, transparent, pattern),
2003
+ transparent,
2004
+ font: skin.font,
2005
+ u: new Float32Array([
2006
+ ...linearColor(a),
2007
+ ...linearColor(b),
2008
+ ...linearColor(skin.labelColor),
2009
+ m.metalness,
2010
+ m.roughness,
2011
+ m.clearcoat ?? 0,
2012
+ m.transmission ?? 0,
2013
+ m.tint ?? 0,
2014
+ m.sparkle ?? 0
2015
+ ])
2016
+ };
2017
+ }
2018
+ function at3(f, i) {
2019
+ return f[i] ?? 0;
2020
+ }
2021
+ function writePose(b, s, row, out, o) {
2022
+ const f = b.frames;
2023
+ const last = f.length / FRAME2 - 1;
2024
+ const fl = Math.floor(s);
2025
+ let i0 = fl - b.startStep;
2026
+ let t = s - fl;
2027
+ if (i0 >= last) {
2028
+ i0 = last;
2029
+ t = 0;
2030
+ }
2031
+ const a = i0 * FRAME2;
2032
+ const c = (i0 < last ? i0 + 1 : i0) * FRAME2;
2033
+ for (let k = 0; k < 3; k++) out[o + k] = at3(f, a + k) + (at3(f, c + k) - at3(f, a + k)) * t;
2034
+ out[o + 3] = row;
2035
+ const sign = at3(f, a + 3) * at3(f, c + 3) + at3(f, a + 4) * at3(f, c + 4) + at3(f, a + 5) * at3(f, c + 5) + at3(f, a + 6) * at3(f, c + 6) < 0 ? -t : t;
2036
+ const u = 1 - t;
2037
+ let x = at3(f, a + 3) * u + at3(f, c + 3) * sign;
2038
+ let y = at3(f, a + 4) * u + at3(f, c + 4) * sign;
2039
+ let z = at3(f, a + 5) * u + at3(f, c + 5) * sign;
2040
+ let w = at3(f, a + 6) * u + at3(f, c + 6) * sign;
2041
+ const len = Math.sqrt(x * x + y * y + z * z + w * w) || 1;
2042
+ x /= len;
2043
+ y /= len;
2044
+ z /= len;
2045
+ w /= len;
2046
+ const r = b.remap;
2047
+ out[o + 4] = w * r[0] + x * r[3] + y * r[2] - z * r[1];
2048
+ out[o + 5] = w * r[1] - x * r[2] + y * r[3] + z * r[0];
2049
+ out[o + 6] = w * r[2] + x * r[1] - y * r[0] + z * r[3];
2050
+ out[o + 7] = w * r[3] - x * r[0] - y * r[1] - z * r[2];
2051
+ }
2052
+ function createDiceTray(target, opts = {}) {
2053
+ const own = !(target instanceof HTMLCanvasElement);
2054
+ const canvas = own ? document.createElement("canvas") : target;
2055
+ if (own) {
2056
+ canvas.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none";
2057
+ target.appendChild(canvas);
2058
+ }
2059
+ let dieScale = opts.dieScale ?? 2;
2060
+ const shadows = opts.shadows ?? true;
2061
+ const maxDpr = opts.maxDpr ?? 2;
2062
+ const fadeAfterMs = opts.fadeAfterMs ?? null;
2063
+ const gl = canvas.getContext("webgl2", {
2064
+ alpha: true,
2065
+ premultipliedAlpha: true,
2066
+ antialias: true
2067
+ });
2068
+ const labelFont = opts.labelFont ?? "system-ui";
2069
+ let atlasFont = resolveSkin(opts.skin).font ?? labelFont;
2070
+ let atlas = gl === null ? null : buildAtlas(atlasFont, opts.labels, labelFont);
2071
+ let traySkin = opts.skin;
2072
+ let lostSkin = null;
2073
+ let roll = null;
2074
+ let pending = null;
2075
+ let disposed = false;
2076
+ let lost = false;
2077
+ let frame2 = 0;
2078
+ let fadeTimer;
2079
+ let fadeStart = -1;
2080
+ let alpha = 1;
2081
+ let aspect = 1;
2082
+ const vp = new Float32Array(16);
2083
+ const shadowData = new Float32Array(MAX_BODIES2 * 4);
2084
+ const instances = /* @__PURE__ */ new Map();
2085
+ const programs = /* @__PURE__ */ new Map();
2086
+ const meshes = /* @__PURE__ */ new Map();
2087
+ const sets = /* @__PURE__ */ new Map();
2088
+ let shadowGpu = null;
2089
+ let atlasTex = null;
2090
+ function program(g, key, fragment) {
2091
+ let p = programs.get(key);
2092
+ if (p === void 0) {
2093
+ const shadow = key === "shadow";
2094
+ const prog = shadow ? createProgram(g, SHADOW_VERTEX, SHADOW_FRAGMENT) : createProgram(g, DIE_VERTEX, fragment);
2095
+ const u = /* @__PURE__ */ new Map();
2096
+ for (const name of shadow ? ["uVP", "uAlpha"] : DIE_UNIFORMS) {
2097
+ u.set(name, g.getUniformLocation(prog, name));
2098
+ }
2099
+ p = { program: prog, u };
2100
+ programs.set(key, p);
2101
+ }
2102
+ return p;
2103
+ }
2104
+ function setGpu(g, set) {
2105
+ let s = sets.get(set);
2106
+ if (s === void 0) {
2107
+ const shape = shapeOf(set);
2108
+ let mesh = meshes.get(shape);
2109
+ if (mesh === void 0) {
2110
+ mesh = createBuffer(g, getDieMesh(shape).data);
2111
+ meshes.set(shape, mesh);
2112
+ }
2113
+ const vao = g.createVertexArray();
2114
+ g.bindVertexArray(vao);
2115
+ g.bindBuffer(g.ARRAY_BUFFER, mesh);
2116
+ attrib(g, 0, 3, MESH_STRIDE, 0, 0);
2117
+ attrib(g, 1, 3, MESH_STRIDE, 3, 0);
2118
+ attrib(g, 2, 3, MESH_STRIDE, 6, 0);
2119
+ const buf = createBuffer(g, MAX_BODIES2 * INST * 4);
2120
+ attrib(g, 3, 4, INST, 0, 1);
2121
+ attrib(g, 4, 4, INST, 4, 1);
2122
+ g.bindVertexArray(null);
2123
+ s = { vao, buf };
2124
+ sets.set(set, s);
2125
+ }
2126
+ return s;
2127
+ }
2128
+ function shadowRes(g) {
2129
+ if (shadowGpu === null) {
2130
+ const vao = g.createVertexArray();
2131
+ g.bindVertexArray(vao);
2132
+ const quad = createBuffer(g, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]));
2133
+ attrib(g, 0, 2, 2, 0, 0);
2134
+ const buf = createBuffer(g, MAX_BODIES2 * 4 * 4);
2135
+ attrib(g, 1, 4, 4, 0, 1);
2136
+ g.bindVertexArray(null);
2137
+ shadowGpu = { vao, quad, buf };
2138
+ }
2139
+ return shadowGpu;
2140
+ }
2141
+ function atlasTexture(g) {
2142
+ if (atlasTex === null) {
2143
+ atlasTex = g.createTexture();
2144
+ g.bindTexture(g.TEXTURE_2D, atlasTex);
2145
+ g.pixelStorei(g.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
2146
+ if (atlas === null) {
2147
+ g.texImage2D(g.TEXTURE_2D, 0, g.R8, 1, 1, 0, g.RED, g.UNSIGNED_BYTE, new Uint8Array(1));
2148
+ } else {
2149
+ g.texImage2D(g.TEXTURE_2D, 0, g.R8, g.RED, g.UNSIGNED_BYTE, atlas);
2150
+ }
2151
+ g.pixelStorei(g.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
2152
+ g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MAX_LEVEL, ATLAS_MAX_LEVEL);
2153
+ g.generateMipmap(g.TEXTURE_2D);
2154
+ g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MIN_FILTER, g.LINEAR_MIPMAP_LINEAR);
2155
+ const aniso = g.getExtension("EXT_texture_filter_anisotropic");
2156
+ if (aniso !== null) {
2157
+ const max = Number(g.getParameter(aniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT)) || 1;
2158
+ g.texParameterf(
2159
+ g.TEXTURE_2D,
2160
+ aniso.TEXTURE_MAX_ANISOTROPY_EXT,
2161
+ Math.min(max, MAX_ANISOTROPY)
2162
+ );
2163
+ }
2164
+ g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MAG_FILTER, g.LINEAR);
2165
+ g.texParameteri(g.TEXTURE_2D, g.TEXTURE_WRAP_S, g.CLAMP_TO_EDGE);
2166
+ g.texParameteri(g.TEXTURE_2D, g.TEXTURE_WRAP_T, g.CLAMP_TO_EDGE);
2167
+ }
2168
+ return atlasTex;
2169
+ }
2170
+ function atlasFor(g, font) {
2171
+ if (font === atlasFont) return;
2172
+ atlasFont = font;
2173
+ atlas = buildAtlas(font, opts.labels, labelFont);
2174
+ if (atlasTex !== null) g.deleteTexture(atlasTex);
2175
+ atlasTex = null;
2176
+ }
2177
+ function compiles(g, s) {
2178
+ try {
2179
+ program(g, s.key, s.fragment);
2180
+ return true;
2181
+ } catch {
2182
+ return false;
2183
+ }
2184
+ }
2185
+ function dropGpu(g, remove) {
2186
+ if (remove) {
2187
+ for (const p of programs.values()) g.deleteProgram(p.program);
2188
+ for (const b of meshes.values()) g.deleteBuffer(b);
2189
+ for (const s of sets.values()) {
2190
+ g.deleteVertexArray(s.vao);
2191
+ g.deleteBuffer(s.buf);
2192
+ }
2193
+ if (shadowGpu !== null) {
2194
+ g.deleteVertexArray(shadowGpu.vao);
2195
+ g.deleteBuffer(shadowGpu.quad);
2196
+ g.deleteBuffer(shadowGpu.buf);
2197
+ }
2198
+ if (atlasTex !== null) g.deleteTexture(atlasTex);
2199
+ }
2200
+ programs.clear();
2201
+ meshes.clear();
2202
+ sets.clear();
2203
+ shadowGpu = null;
2204
+ atlasTex = null;
2205
+ }
2206
+ function render() {
2207
+ const g = gl;
2208
+ if (g === null || lost || disposed) return;
2209
+ g.viewport(0, 0, canvas.width, canvas.height);
2210
+ g.clearColor(0, 0, 0, 0);
2211
+ g.depthMask(true);
2212
+ g.clear(g.COLOR_BUFFER_BIT | g.DEPTH_BUFFER_BIT);
2213
+ const r = roll;
2214
+ if (r === null) return;
2215
+ viewProjection(aspect, vp);
2216
+ const s = r.step;
2217
+ const scale2 = r.scale;
2218
+ let ns = 0;
2219
+ for (let i = 0; i < r.groups.length; i++) {
2220
+ const grp = r.groups[i];
2221
+ if (grp === void 0) continue;
2222
+ let n = 0;
2223
+ for (let k2 = 0; k2 < grp.bodies.length; k2++) {
2224
+ const b = grp.bodies[k2];
2225
+ if (b === void 0 || b.startStep > s) continue;
2226
+ const o = n * INST;
2227
+ writePose(b, s, b.value === null ? -1 : grp.row, grp.data, o);
2228
+ const h = at3(grp.data, o + 1) * scale2;
2229
+ const fade = 1 - h / 4;
2230
+ shadowData[ns * 4] = at3(grp.data, o) * scale2 + h * SHADOW_X;
2231
+ shadowData[ns * 4 + 1] = at3(grp.data, o + 2) * scale2 + h * SHADOW_Z;
2232
+ shadowData[ns * 4 + 2] = 1.3 * grp.radius * scale2;
2233
+ shadowData[ns * 4 + 3] = 0.45 * (fade < 0 ? 0 : fade > 1 ? 1 : fade);
2234
+ n++;
2235
+ ns++;
2236
+ }
2237
+ grp.count = n;
2238
+ }
2239
+ g.enable(g.BLEND);
2240
+ g.blendFunc(g.ONE, g.ONE_MINUS_SRC_ALPHA);
2241
+ if (shadows && ns > 0) {
2242
+ const p2 = program(g, "shadow", SHADOW_FRAGMENT);
2243
+ const res = shadowRes(g);
2244
+ g.disable(g.DEPTH_TEST);
2245
+ g.disable(g.CULL_FACE);
2246
+ g.useProgram(p2.program);
2247
+ g.uniformMatrix4fv(p2.u.get("uVP") ?? null, false, vp);
2248
+ g.uniform1f(p2.u.get("uAlpha") ?? null, alpha);
2249
+ g.bindBuffer(g.ARRAY_BUFFER, res.buf);
2250
+ g.bufferSubData(g.ARRAY_BUFFER, 0, shadowData, 0, ns * 4);
2251
+ g.bindVertexArray(res.vao);
2252
+ g.drawArraysInstanced(g.TRIANGLE_STRIP, 0, 4, ns);
2253
+ }
2254
+ const k = r.skin;
2255
+ const p = program(g, k.key, k.fragment);
2256
+ const u = p.u;
2257
+ const f = k.u;
2258
+ const cam = cameraPosition();
2259
+ g.enable(g.DEPTH_TEST);
2260
+ g.enable(g.CULL_FACE);
2261
+ g.cullFace(g.BACK);
2262
+ atlasFor(g, k.font ?? labelFont);
2263
+ g.useProgram(p.program);
2264
+ g.activeTexture(g.TEXTURE0);
2265
+ g.bindTexture(g.TEXTURE_2D, atlasTexture(g));
2266
+ g.uniform1i(u.get("uAtlas") ?? null, 0);
2267
+ g.uniformMatrix4fv(u.get("uVP") ?? null, false, vp);
2268
+ g.uniform1f(u.get("uScale") ?? null, scale2);
2269
+ g.uniform3f(u.get("uCam") ?? null, cam[0], cam[1], cam[2]);
2270
+ g.uniform3f(u.get("uBase") ?? null, at3(f, 0), at3(f, 1), at3(f, 2));
2271
+ g.uniform3f(u.get("uBase2") ?? null, at3(f, 3), at3(f, 4), at3(f, 5));
2272
+ g.uniform3f(u.get("uLabel") ?? null, at3(f, 6), at3(f, 7), at3(f, 8));
2273
+ g.uniform3f(u.get("uMat") ?? null, at3(f, 9), at3(f, 10), at3(f, 11));
2274
+ g.uniform3f(u.get("uGlass") ?? null, at3(f, 12), at3(f, 13), at3(f, 14));
2275
+ g.uniform1f(u.get("uAlpha") ?? null, alpha);
2276
+ if (k.transparent) {
2277
+ g.depthMask(false);
2278
+ g.cullFace(g.FRONT);
2279
+ drawDice(g, r, true);
2280
+ g.depthMask(true);
2281
+ g.cullFace(g.BACK);
2282
+ drawDice(g, r, false);
2283
+ } else {
2284
+ drawDice(g, r, true);
2285
+ }
2286
+ g.bindVertexArray(null);
2287
+ }
2288
+ function drawDice(g, r, upload) {
2289
+ for (let i = 0; i < r.groups.length; i++) {
2290
+ const grp = r.groups[i];
2291
+ if (grp === void 0 || grp.count === 0) continue;
2292
+ const res = setGpu(g, grp.set);
2293
+ if (upload) {
2294
+ g.bindBuffer(g.ARRAY_BUFFER, res.buf);
2295
+ g.bufferSubData(g.ARRAY_BUFFER, 0, grp.data, 0, grp.count * INST);
2296
+ }
2297
+ g.bindVertexArray(res.vao);
2298
+ g.drawArraysInstanced(g.TRIANGLES, 0, getDieMesh(shapeOf(grp.set)).vertexCount, grp.count);
2299
+ }
2300
+ }
2301
+ function resolvePending() {
2302
+ const p = pending;
2303
+ pending = null;
2304
+ if (p !== null) p.resolve(p.summary);
2305
+ }
2306
+ function loop() {
2307
+ if (frame2 === 0 && !lost && !disposed) frame2 = requestAnimationFrame(tick);
2308
+ }
2309
+ function stop() {
2310
+ if (frame2 !== 0) cancelAnimationFrame(frame2);
2311
+ frame2 = 0;
2312
+ clearTimeout(fadeTimer);
2313
+ fadeTimer = void 0;
2314
+ fadeStart = -1;
2315
+ alpha = 1;
2316
+ }
2317
+ function settle(r) {
2318
+ r.step = r.plan.totalSteps;
2319
+ r.done = true;
2320
+ render();
2321
+ resolvePending();
2322
+ if (fadeAfterMs === null) return;
2323
+ fadeTimer = setTimeout(() => {
2324
+ fadeTimer = void 0;
2325
+ fadeStart = performance.now();
2326
+ loop();
2327
+ }, fadeAfterMs);
2328
+ }
2329
+ function tick() {
2330
+ frame2 = 0;
2331
+ const r = roll;
2332
+ if (r === null) return;
2333
+ const now = performance.now();
2334
+ if (!r.done) {
2335
+ const s = (now - r.t0) / 1e3 / DT;
2336
+ if (s >= r.plan.totalSteps) {
2337
+ settle(r);
2338
+ return;
2339
+ }
2340
+ r.step = s < 0 ? 0 : s;
2341
+ } else if (fadeStart >= 0) {
2342
+ alpha = 1 - (now - fadeStart) / FADE_MS;
2343
+ if (alpha <= 0) {
2344
+ tray.clear();
2345
+ return;
2346
+ }
2347
+ } else {
2348
+ return;
2349
+ }
2350
+ render();
2351
+ frame2 = requestAnimationFrame(tick);
2352
+ }
2353
+ function fit() {
2354
+ const cw = canvas.clientWidth;
2355
+ const ch = canvas.clientHeight;
2356
+ if (cw <= 0 || ch <= 0) {
2357
+ aspect = 1;
2358
+ return;
2359
+ }
2360
+ aspect = cw / ch;
2361
+ const dpr = Math.min(devicePixelRatio, maxDpr);
2362
+ const w = Math.max(1, Math.round(cw * dpr));
2363
+ const h = Math.max(1, Math.round(ch * dpr));
2364
+ if (canvas.width !== w || canvas.height !== h) {
2365
+ canvas.width = w;
2366
+ canvas.height = h;
2367
+ }
2368
+ }
2369
+ function reduced() {
2370
+ const mode = opts.reducedMotion ?? "auto";
2371
+ return mode === "always" || mode === "auto" && matchMedia("(prefers-reduced-motion: reduce)").matches;
2372
+ }
2373
+ const onLost = (e) => {
2374
+ e.preventDefault();
2375
+ lost = true;
2376
+ if (frame2 !== 0) cancelAnimationFrame(frame2);
2377
+ frame2 = 0;
2378
+ if (gl !== null) dropGpu(gl, false);
2379
+ resolvePending();
2380
+ };
2381
+ const onRestored = () => {
2382
+ lost = false;
2383
+ const g = gl;
2384
+ const r = roll;
2385
+ if (g !== null) {
2386
+ const want = lostSkin;
2387
+ lostSkin = null;
2388
+ if (want !== null && compiles(g, want.gpu)) {
2389
+ traySkin = want.ref;
2390
+ if (r !== null && r.eventSkin === void 0) r.skin = want.gpu;
2391
+ }
2392
+ if (r !== null && !compiles(g, r.skin)) {
2393
+ r.skin = skinGpu(resolveSkin(traySkin));
2394
+ if (!compiles(g, r.skin)) r.skin = skinGpu(resolveSkin(void 0));
2395
+ }
2396
+ }
2397
+ if (r !== null && (!r.done || fadeStart >= 0)) loop();
2398
+ else render();
2399
+ };
2400
+ let observer = null;
2401
+ const tray = {
2402
+ supported: gl !== null,
2403
+ async playRoll(event) {
2404
+ const summary = chunkOEMS7CZP_cjs.evaluate(event);
2405
+ if (disposed || gl === null) return summary;
2406
+ const skin = skinGpu(resolveSkin(event.skin ?? traySkin));
2407
+ if (!lost) program(gl, skin.key, skin.fragment);
2408
+ stop();
2409
+ resolvePending();
2410
+ fit();
2411
+ const scale2 = fitScale(dieScale, slots(event).length, aspect);
2412
+ const plan = planRoll(event, trayBounds(aspect, scale2));
2413
+ const groups = [];
2414
+ LABEL_SETS.forEach((set, row) => {
2415
+ const bodies = plan.bodies.filter((b) => b.labelSet === set);
2416
+ if (bodies.length === 0) return;
2417
+ const radius = getPolyhedron(shapeOf(set)).radius;
2418
+ let data = instances.get(set);
2419
+ if (data === void 0) {
2420
+ data = new Float32Array(MAX_BODIES2 * INST);
2421
+ instances.set(set, data);
2422
+ }
2423
+ groups.push({ set, row, bodies, radius, data, count: 0 });
2424
+ });
2425
+ const r = {
2426
+ plan,
2427
+ groups,
2428
+ eventSkin: event.skin,
2429
+ skin,
2430
+ scale: scale2,
2431
+ t0: performance.now(),
2432
+ step: 0,
2433
+ done: false
2434
+ };
2435
+ roll = r;
2436
+ const done = new Promise((resolve) => {
2437
+ pending = { resolve, summary };
2438
+ });
2439
+ if (reduced() || lost) settle(r);
2440
+ else loop();
2441
+ return done;
2442
+ },
2443
+ setSkin(skin) {
2444
+ if (gl !== null && !disposed) {
2445
+ const s = skinGpu(resolveSkin(skin));
2446
+ if (lost) {
2447
+ lostSkin = { ref: skin, gpu: s };
2448
+ return;
2449
+ }
2450
+ program(gl, s.key, s.fragment);
2451
+ const r = roll;
2452
+ if (r !== null && r.eventSkin === void 0) {
2453
+ r.skin = s;
2454
+ if (frame2 === 0) render();
2455
+ }
2456
+ }
2457
+ traySkin = skin;
2458
+ },
2459
+ setDieScale(scale2) {
2460
+ if (!Number.isFinite(scale2) || scale2 <= 0) {
2461
+ throw new RangeError(`dieScale must be a positive finite number, got ${scale2}`);
2462
+ }
2463
+ dieScale = scale2;
2464
+ },
2465
+ clear() {
2466
+ stop();
2467
+ roll = null;
2468
+ resolvePending();
2469
+ render();
2470
+ },
2471
+ resize() {
2472
+ fit();
2473
+ render();
2474
+ },
2475
+ dispose() {
2476
+ if (disposed) return;
2477
+ stop();
2478
+ roll = null;
2479
+ resolvePending();
2480
+ disposed = true;
2481
+ canvas.removeEventListener("webglcontextlost", onLost);
2482
+ canvas.removeEventListener("webglcontextrestored", onRestored);
2483
+ observer?.disconnect();
2484
+ observer = null;
2485
+ if (gl !== null) dropGpu(gl, !lost && !gl.isContextLost());
2486
+ if (own) canvas.remove();
2487
+ }
2488
+ };
2489
+ if (gl !== null) {
2490
+ canvas.addEventListener("webglcontextlost", onLost);
2491
+ canvas.addEventListener("webglcontextrestored", onRestored);
2492
+ observer = new ResizeObserver(() => tray.resize());
2493
+ observer.observe(canvas);
2494
+ fit();
2495
+ render();
2496
+ }
2497
+ return tray;
2498
+ }
2499
+
2500
+ exports.PollyrollShaderError = PollyrollShaderError;
2501
+ exports.amethyst = amethyst;
2502
+ exports.aquamarine = aquamarine;
2503
+ exports.brass = brass;
2504
+ exports.classic = classic;
2505
+ exports.createDiceTray = createDiceTray;
2506
+ exports.defineSkin = defineSkin;
2507
+ exports.emerald = emerald;
2508
+ exports.materialPresets = materialPresets;
2509
+ exports.oak = oak;
2510
+ exports.obsidian = obsidian;
2511
+ exports.registerSkin = registerSkin;
2512
+ exports.ruby = ruby;
2513
+ exports.sapphire = sapphire;
2514
+ exports.smoke = smoke;
2515
+ exports.topaz = topaz;