@aethervtt/shard-dice 0.0.0-stage → 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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/attachments.d.ts +79 -0
  4. package/dist/attachments.js +43 -0
  5. package/dist/builtins.d.ts +25 -0
  6. package/dist/builtins.js +335 -0
  7. package/dist/cells.d.ts +33 -0
  8. package/dist/cells.js +53 -0
  9. package/dist/components.d.ts +14 -0
  10. package/dist/components.js +27 -0
  11. package/dist/definition-asset.d.ts +19 -0
  12. package/dist/definition-asset.js +60 -0
  13. package/dist/definition.d.ts +98 -0
  14. package/dist/definition.js +320 -0
  15. package/dist/dice-worker.d.ts +1 -0
  16. package/dist/dice-worker.js +3 -0
  17. package/dist/effects.d.ts +97 -0
  18. package/dist/effects.js +196 -0
  19. package/dist/entrances.d.ts +48 -0
  20. package/dist/entrances.js +54 -0
  21. package/dist/families.d.ts +17 -0
  22. package/dist/families.js +131 -0
  23. package/dist/glyphs.d.ts +29 -0
  24. package/dist/glyphs.js +270 -0
  25. package/dist/golden.json +17 -0
  26. package/dist/hull.d.ts +32 -0
  27. package/dist/hull.js +154 -0
  28. package/dist/index.d.ts +31 -0
  29. package/dist/index.js +322 -0
  30. package/dist/landing.d.ts +29 -0
  31. package/dist/landing.js +93 -0
  32. package/dist/layout.d.ts +75 -0
  33. package/dist/layout.js +137 -0
  34. package/dist/marks.d.ts +54 -0
  35. package/dist/marks.js +543 -0
  36. package/dist/material.d.ts +80 -0
  37. package/dist/material.js +343 -0
  38. package/dist/math.d.ts +39 -0
  39. package/dist/math.js +136 -0
  40. package/dist/mesh.d.ts +9 -0
  41. package/dist/mesh.js +105 -0
  42. package/dist/methods.d.ts +2 -0
  43. package/dist/methods.js +42 -0
  44. package/dist/numerals.d.ts +9 -0
  45. package/dist/numerals.js +47 -0
  46. package/dist/plugin.d.ts +41 -0
  47. package/dist/plugin.js +174 -0
  48. package/dist/preview.d.ts +32 -0
  49. package/dist/preview.js +123 -0
  50. package/dist/quality.d.ts +12 -0
  51. package/dist/quality.js +18 -0
  52. package/dist/resources.d.ts +85 -0
  53. package/dist/resources.js +191 -0
  54. package/dist/roll.d.ts +68 -0
  55. package/dist/roll.js +97 -0
  56. package/dist/settle.d.ts +53 -0
  57. package/dist/settle.js +96 -0
  58. package/dist/skin.d.ts +61 -0
  59. package/dist/skin.js +303 -0
  60. package/dist/sound.d.ts +33 -0
  61. package/dist/sound.js +296 -0
  62. package/dist/studio.d.ts +3 -0
  63. package/dist/studio.js +48 -0
  64. package/dist/table.d.ts +225 -0
  65. package/dist/table.js +1757 -0
  66. package/dist/test-worker.d.ts +1 -0
  67. package/dist/test-worker.js +12 -0
  68. package/dist/track.d.ts +70 -0
  69. package/dist/track.js +304 -0
  70. package/dist/tray.d.ts +2 -0
  71. package/dist/tray.js +43 -0
  72. package/dist/variants.d.ts +9 -0
  73. package/dist/variants.js +46 -0
  74. package/dist/windows.d.ts +25 -0
  75. package/dist/windows.js +77 -0
  76. package/dist/worker.d.ts +6 -0
  77. package/dist/worker.js +9 -0
  78. package/package.json +47 -4
@@ -0,0 +1,343 @@
1
+ import { ShardError, t } from "@aethervtt/shard-core";
2
+ import {
3
+ defineMaterial,
4
+ findMaterialType,
5
+ materialModulePath
6
+ } from "@aethervtt/shard-render";
7
+ import { MARK_RANGE } from "./cells.js";
8
+ const DICE_FIELDS = {
9
+ marks: t.handle("Texture", { description: "The mark atlas (MSDF), baked by the package." }),
10
+ markColor: t.color({ default: [0.95, 0.94, 0.9, 1], description: "Mark ink (linear)." }),
11
+ markEmissive: t.f32({
12
+ min: 0,
13
+ unit: "cd/m\xB2",
14
+ description: "Glowing marks: luminance of the ink."
15
+ }),
16
+ markRoughness: t.f32({ default: 0.55, min: 0, max: 1, description: "Roughness of the ink." }),
17
+ markMetallic: t.f32({ min: 0, max: 1, description: "Metallic ink (gilded marks)." }),
18
+ markDepth: t.f32({
19
+ default: 0.6,
20
+ min: 0,
21
+ max: 1,
22
+ description: "How deep the marks are engraved: a bump taken from the distance field. 0 prints them."
23
+ }),
24
+ edgeColor: t.color({
25
+ default: [0.5, 0.5, 0.5, 1],
26
+ description: "Color the chamfer blends toward."
27
+ }),
28
+ edgeMix: t.f32({
29
+ default: 0.3,
30
+ min: 0,
31
+ max: 1,
32
+ description: "How much of edgeColor the chamfer takes."
33
+ }),
34
+ droppedColor: t.color({
35
+ default: [0.34, 0.38, 0.43, 1],
36
+ description: "The dropped look's tone: dropped dice fade toward it."
37
+ }),
38
+ dropped: t.f32({
39
+ min: 0,
40
+ max: 1,
41
+ description: "Blend toward the dropped look: desaturated, 0.62 opacity, rougher, no transmission."
42
+ }),
43
+ fade: t.f32({ default: 1, min: 0, max: 1, description: "Opacity for blended dice fading in." }),
44
+ result: t.f32({
45
+ min: 0,
46
+ max: 1,
47
+ description: "0 while tumbling, 1 once landed: result-reactive families light the face on top."
48
+ }),
49
+ resultTime: t.f32({
50
+ unit: "s",
51
+ description: "The shader clock (globals.time) when the dice landed: reactions animate from globals.time \u2212 resultTime once result is above 0."
52
+ })
53
+ };
54
+ const DICE_FIELD_NAMES = Object.keys(DICE_FIELDS);
55
+ const DICE_SHADERS = {
56
+ "shard::dice::surface": `
57
+ import shard::pbr::types::{ VertexOutput, PbrInput };
58
+ import shard::pbr::standard::material;
59
+ import shard::mesh::vertex_instance_data;
60
+ import shard::view::view;
61
+
62
+ /** The atlas's distance range, in atlas pixels (the bake's MARK_RANGE). */
63
+ const DICE_MARK_RANGE: f32 = ${MARK_RANGE.toFixed(1)};
64
+
65
+ /** What every family's dice fields say, gathered by its shader. */
66
+ struct DiceLook {
67
+ mark_color: vec4f,
68
+ edge_color: vec4f,
69
+ dropped_color: vec4f,
70
+ mark_emissive: f32,
71
+ mark_roughness: f32,
72
+ mark_metallic: f32,
73
+ mark_depth: f32,
74
+ edge_mix: f32,
75
+ dropped: f32,
76
+ fade: f32,
77
+ result: f32,
78
+ /** globals.time when the dice landed (see result). */
79
+ result_time: f32,
80
+ }
81
+
82
+ /** The mark at a fragment. */
83
+ struct DiceMark {
84
+ /** 1 on faces, 0 on the chamfer. */
85
+ face: f32,
86
+ /** Ink coverage, antialiased in screen space. */
87
+ coverage: f32,
88
+ /** Signed distance to the mark's outline in atlas pixels, positive inside. */
89
+ inside: f32,
90
+ /** The distance's change per atlas pixel along u and v. */
91
+ slope: vec2f,
92
+ /** How squarely this face points up: 1 on the face on top. */
93
+ top: f32,
94
+ }
95
+
96
+ /** For vertex_extra: the object-space position (for body patterns that stick to the die), and the die's value. */
97
+ fn dice_extra(position: vec3f) -> vec4f {
98
+ return vec4f(position, vertex_instance_data().x);
99
+ }
100
+
101
+ fn dice_median(c: vec3f) -> f32 {
102
+ return max(min(c.r, c.g), min(max(c.r, c.g), c.b));
103
+ }
104
+
105
+ /** Samples the mark atlas. Call it first in pbr_input: it takes derivatives. */
106
+ fn dice_mark(in: VertexOutput, atlas: texture_2d<f32>, samp: sampler) -> DiceMark {
107
+ let size = vec2f(textureDimensions(atlas));
108
+ let texel = 1.0 / size;
109
+ let fw = fwidth(in.uv * size);
110
+ let c = textureSample(atlas, samp, in.uv).rgb;
111
+ let cx = textureSample(atlas, samp, in.uv + vec2f(texel.x, 0.0)).rgb;
112
+ let cy = textureSample(atlas, samp, in.uv + vec2f(0.0, texel.y)).rgb;
113
+ let d = (dice_median(c) - 0.5) * DICE_MARK_RANGE;
114
+ var m: DiceMark;
115
+ m.face = step(0.5, in.uv1.x);
116
+ let per_pixel = max(0.5 * (fw.x + fw.y), 1e-3);
117
+ m.coverage = clamp(d / per_pixel + 0.5, 0.0, 1.0) * m.face;
118
+ m.inside = d;
119
+ m.slope = vec2f((dice_median(cx) - 0.5) * DICE_MARK_RANGE - d, (dice_median(cy) - 0.5) * DICE_MARK_RANGE - d);
120
+ m.top = smoothstep(0.9, 0.995, normalize(in.world_normal).y) * m.face;
121
+ return m;
122
+ }
123
+
124
+ /** The surface from the standard fields: baseColor, metallic, roughness, emissive. */
125
+ fn dice_base(in: VertexOutput) -> PbrInput {
126
+ var p: PbrInput;
127
+ p.base_color = material.baseColor.rgb;
128
+ p.alpha = material.baseColor.a;
129
+ p.normal = normalize(in.world_normal);
130
+ p.metallic = material.metallic;
131
+ p.roughness = clamp(material.roughness, 0.045, 1.0);
132
+ p.emissive = material.emissive.rgb * material.emissiveLuminance;
133
+ p.occlusion = 1.0;
134
+ return p;
135
+ }
136
+
137
+ /** From the surface toward the camera. */
138
+ fn dice_view(in: VertexOutput) -> vec3f {
139
+ return normalize(view.cameraPosition - in.world_position);
140
+ }
141
+
142
+ /**
143
+ * The ray from the camera through this fragment in the die's own space (unit length), for what's
144
+ * seen inside a die: a vortex, a star field at depth. It's recovered from how the world and object
145
+ * positions change across the pixel (one rigid map for the whole die), so call it where
146
+ * derivatives are valid: in the surface's top level, not under a branch.
147
+ */
148
+ fn dice_local_ray(in: VertexOutput, local: vec3f) -> vec3f {
149
+ let wx = dpdx(in.world_position);
150
+ let wy = dpdy(in.world_position);
151
+ let lx = dpdx(local);
152
+ let ly = dpdy(local);
153
+ let wn = cross(wx, wy);
154
+ let ln = cross(lx, ly);
155
+ let d = normalize(in.world_position - view.cameraPosition);
156
+ let det = max(dot(wn, wn), 1e-30);
157
+ // d = a\xB7wx + b\xB7wy + c\xB7wn, and the die's space maps wx, wy, wn to s\xB7lx, s\xB7ly, s\xB2\xB7ln.
158
+ let a = dot(cross(wy, wn), d) / det;
159
+ let b = dot(cross(wn, wx), d) / det;
160
+ let c = dot(wn, d) / det;
161
+ let s = sqrt(length(wn) / max(length(ln), 1e-30));
162
+ let r = a * lx + b * ly + c * s * ln;
163
+ return r / max(length(r), 1e-20);
164
+ }
165
+
166
+ fn dice_hash(p: vec3f) -> f32 {
167
+ var q = fract(p * 0.1031);
168
+ q += dot(q, q.yzx + 33.33);
169
+ return fract((q.x + q.y) * q.z);
170
+ }
171
+
172
+ /** Smooth value noise in 0..1, for body patterns in object space. */
173
+ fn dice_noise(p: vec3f) -> f32 {
174
+ let i = floor(p);
175
+ let f = fract(p);
176
+ let u = f * f * (3.0 - 2.0 * f);
177
+ return mix(
178
+ mix(mix(dice_hash(i), dice_hash(i + vec3f(1.0, 0.0, 0.0)), u.x),
179
+ mix(dice_hash(i + vec3f(0.0, 1.0, 0.0)), dice_hash(i + vec3f(1.0, 1.0, 0.0)), u.x), u.y),
180
+ mix(mix(dice_hash(i + vec3f(0.0, 0.0, 1.0)), dice_hash(i + vec3f(1.0, 0.0, 1.0)), u.x),
181
+ mix(dice_hash(i + vec3f(0.0, 1.0, 1.0)), dice_hash(i + vec3f(1.0, 1.0, 1.0)), u.x), u.y),
182
+ u.z);
183
+ }
184
+
185
+ /** Two octaves of dice_noise. */
186
+ fn dice_fbm(p: vec3f) -> f32 {
187
+ return dice_noise(p) * 0.65 + dice_noise(p * 2.13 + vec3f(5.2, 1.3, 7.1)) * 0.35;
188
+ }
189
+
190
+ fn dice_luma(c: vec3f) -> f32 {
191
+ return dot(c, vec3f(0.2126, 0.7152, 0.0722));
192
+ }
193
+
194
+ /**
195
+ * Finishes a family's body: the chamfer toward edge_color, the marks (ink, engraving, glow), the
196
+ * dropped look, and the fade. Families shade the body into p, then call this.
197
+ */
198
+ fn dice_finish(p: ptr<function, PbrInput>, m: DiceMark, look: DiceLook, in: VertexOutput) {
199
+ let n = normalize(in.world_normal);
200
+ let chamfer = 1.0 - m.face;
201
+ (*p).base_color = mix((*p).base_color, look.edge_color.rgb, look.edge_mix * chamfer);
202
+
203
+ // Engraving: the rim of each mark slopes into it, and the recess is a little shadowed.
204
+ let t = in.world_tangent.xyz;
205
+ if (look.mark_depth > 0.0 && m.face > 0.5 && dot(t, t) > 1e-8) {
206
+ let tangent = normalize(t - n * dot(n, t));
207
+ let bitangent = cross(n, tangent) * in.world_tangent.w;
208
+ let rim = clamp((m.inside + 1.0) / 2.5, 0.0, 1.0);
209
+ let wall = 6.0 * rim * (1.0 - rim) * look.mark_depth;
210
+ (*p).normal = normalize(n + (tangent * m.slope.x - bitangent * m.slope.y) * wall * 0.9);
211
+ (*p).occlusion *= 1.0 - 0.35 * look.mark_depth * clamp(m.inside / 3.0, 0.0, 1.0);
212
+ }
213
+
214
+ let ink = m.coverage * look.mark_color.a;
215
+ (*p).base_color = mix((*p).base_color, look.mark_color.rgb, ink);
216
+ (*p).roughness = mix((*p).roughness, look.mark_roughness, ink);
217
+ (*p).metallic = mix((*p).metallic, look.mark_metallic, ink);
218
+ (*p).emissive += look.mark_color.rgb * look.mark_emissive * ink;
219
+ (*p).alpha = max((*p).alpha, ink);
220
+
221
+ // Dropped: toward the dropped tone, keeping some of the body's lightness; rougher, 0.62 opaque.
222
+ let d = look.dropped;
223
+ let tone = look.dropped_color.rgb * (0.55 + 0.45 * dice_luma((*p).base_color) / max(dice_luma(look.dropped_color.rgb), 0.05));
224
+ (*p).base_color = mix((*p).base_color, tone, d * 0.85);
225
+ (*p).roughness = min(1.0, (*p).roughness + 0.24 * d);
226
+ (*p).metallic *= 1.0 - d;
227
+ (*p).emissive *= 1.0 - 0.8 * d;
228
+ (*p).alpha = mix((*p).alpha, 0.62, d) * look.fade;
229
+ }
230
+ `
231
+ };
232
+ const families = /* @__PURE__ */ new Map();
233
+ function snake(name) {
234
+ return materialModulePath(name).slice("material::".length);
235
+ }
236
+ function familyModule(name, type, surface) {
237
+ const u = type.varName;
238
+ const look = [
239
+ "markColor",
240
+ "edgeColor",
241
+ "droppedColor",
242
+ "markEmissive",
243
+ "markRoughness",
244
+ "markMetallic",
245
+ "markDepth",
246
+ "edgeMix",
247
+ "dropped",
248
+ "fade",
249
+ "result",
250
+ "resultTime"
251
+ ].map((f) => `${u}.${f}`).join(", ");
252
+ const path = `dice::family::${snake(name)}`;
253
+ const source = `
254
+ import shard::pbr::types::{ VertexOutput, PbrInput };
255
+ import shard::globals::globals;
256
+ import shard::dice::surface::{ DiceLook, DiceMark, dice_base, dice_mark, dice_finish, dice_extra, dice_view, dice_local_ray, dice_hash, dice_noise, dice_fbm, dice_luma };
257
+ import ${type.modulePath}::{ ${u}, ${u}_marks, ${u}_marks_sampler };
258
+
259
+ override fn vertex_extra(position: vec3f, normal: vec3f, uv: vec2f) -> vec4f {
260
+ return dice_extra(position);
261
+ }
262
+
263
+ override fn pbr_input(in: VertexOutput) -> PbrInput {
264
+ let m = dice_mark(in, ${u}_marks, ${u}_marks_sampler);
265
+ let look = DiceLook(${look});
266
+ var p = dice_base(in);
267
+ let F = ${u};
268
+ let local = in.extra.xyz;
269
+ let value = floor(in.extra.w + 0.5);
270
+ let v = dice_view(in);
271
+ ${surface}
272
+ dice_finish(&p, m, look, in);
273
+ return p;
274
+ }
275
+ `;
276
+ return { path, source };
277
+ }
278
+ function defineDiceFamily(name, options) {
279
+ for (const key of Object.keys(options.fields ?? {})) {
280
+ if (DICE_FIELD_NAMES.includes(key)) {
281
+ throw new ShardError(
282
+ "dice/family-field-clash",
283
+ `Dice family ${name} redefines the dice field "${key}"`,
284
+ {
285
+ hint: "Give the field another name: the dice fields are shared by every family."
286
+ }
287
+ );
288
+ }
289
+ }
290
+ if (!options.surface && !options.shader) {
291
+ throw new ShardError(
292
+ "dice/invalid-family",
293
+ `Dice family ${name} has no surface and no shader`,
294
+ {
295
+ hint: "Give a WGSL `surface` snippet, or a `shader` module overriding pbr_input."
296
+ }
297
+ );
298
+ }
299
+ const shaderPath = options.shader ?? `dice::family::${snake(name)}`;
300
+ const type = defineMaterial(name, {
301
+ extends: "standard",
302
+ standardTextures: false,
303
+ fields: { ...DICE_FIELDS, ...options.fields },
304
+ blend: options.blend,
305
+ shader: shaderPath,
306
+ description: options.description ?? `Dice family ${name}.`
307
+ });
308
+ const animated = options.animated;
309
+ if (typeof animated === "object" && !(animated.fps > 0)) {
310
+ throw new ShardError(
311
+ "dice/invalid-family",
312
+ `Dice family ${name} animates at ${animated.fps} fps`,
313
+ { hint: "Give animated: true (the display rate) or { fps } above 0.", path: "animated.fps" }
314
+ );
315
+ }
316
+ const family = {
317
+ name,
318
+ type,
319
+ module: options.surface ? familyModule(name, type, options.surface) : void 0,
320
+ translucent: options.translucent,
321
+ fps: animated === true ? Number.POSITIVE_INFINITY : animated ? animated.fps : 0
322
+ };
323
+ families.set(name, family);
324
+ return family;
325
+ }
326
+ function findDiceFamily(name) {
327
+ return families.get(name);
328
+ }
329
+ function allDiceFamilies() {
330
+ return [...families.values()];
331
+ }
332
+ function isDiceMaterial(type) {
333
+ return DICE_FIELD_NAMES.every((f) => f in type.schema.fields);
334
+ }
335
+ export {
336
+ DICE_FIELDS,
337
+ DICE_SHADERS,
338
+ allDiceFamilies,
339
+ defineDiceFamily,
340
+ findDiceFamily,
341
+ findMaterialType,
342
+ isDiceMaterial
343
+ };
package/dist/math.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ export type V3 = [number, number, number];
2
+ /** x y z w. */
3
+ export type Q4 = [number, number, number, number];
4
+ export declare const EPSILON = 0.00001;
5
+ export declare const v3: (x?: number, y?: number, z?: number) => V3;
6
+ export declare const add: (a: V3, b: V3) => V3;
7
+ export declare const sub: (a: V3, b: V3) => V3;
8
+ export declare const scale: (a: V3, s: number) => V3;
9
+ export declare const dot: (a: V3, b: V3) => number;
10
+ export declare const cross: (a: V3, b: V3) => V3;
11
+ export declare const length: (a: V3) => number;
12
+ export declare function normalize(a: V3): V3;
13
+ export declare const distance: (a: V3, b: V3) => number;
14
+ /** The part of `a` perpendicular to unit `n`, normalized. */
15
+ export declare function tangentOf(a: V3, n: V3): V3;
16
+ /** A unit tangent of unit `n` that doesn't depend on anything else (Aether's convention). */
17
+ export declare function stableTangent(n: V3): V3;
18
+ export declare const qIdentity: () => Q4;
19
+ export declare function qMul(a: Q4, b: Q4): Q4;
20
+ export declare const qConj: (q: Q4) => Q4;
21
+ export declare function qNormalize(q: Q4): Q4;
22
+ export declare function qRotate(q: Q4, v: V3): V3;
23
+ export declare function qAxisAngle(axis: V3, radians: number): Q4;
24
+ /** The rotation whose matrix has these columns (an orthonormal, right-handed basis). */
25
+ export declare function qFromBasis(x: V3, y: V3, z: V3): Q4;
26
+ /**
27
+ * The rotation taking the frame (n, b) to (n2, b2): n and n2 unit normals, b ⟂ n and b2 ⟂ n2 unit
28
+ * "up" directions. Exact when the angle between n and b equals the one between n2 and b2.
29
+ */
30
+ export declare function qFromFrames(n: V3, b: V3, n2: V3, b2: V3): Q4;
31
+ /** Canonical sign (first non-zero of w, x, y, z positive), so equal rotations compare equal. */
32
+ export declare function qCanonical(q: Q4): Q4;
33
+ export declare const qKey: (q: Q4) => string;
34
+ /** Rounds away float noise, so hull output and frames are stable across platforms. */
35
+ export declare function round(v: number): number;
36
+ /** The angle between two unit vectors, in radians. */
37
+ export declare function angleBetween(a: V3, b: V3): number;
38
+ /** FNV-1a over a string, finished with a 32-bit mix: a stable seed from a roll id. */
39
+ export declare function hashString(s: string): number;
package/dist/math.js ADDED
@@ -0,0 +1,136 @@
1
+ const EPSILON = 1e-5;
2
+ const v3 = (x = 0, y = 0, z = 0) => [x, y, z];
3
+ const add = (a, b) => [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
4
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
5
+ const scale = (a, s) => [a[0] * s, a[1] * s, a[2] * s];
6
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
7
+ const cross = (a, b) => [
8
+ a[1] * b[2] - a[2] * b[1],
9
+ a[2] * b[0] - a[0] * b[2],
10
+ a[0] * b[1] - a[1] * b[0]
11
+ ];
12
+ const length = (a) => Math.sqrt(dot(a, a));
13
+ function normalize(a) {
14
+ const l = length(a);
15
+ return l > 0 ? [a[0] / l, a[1] / l, a[2] / l] : [0, 0, 0];
16
+ }
17
+ const distance = (a, b) => length(sub(a, b));
18
+ function tangentOf(a, n) {
19
+ return normalize(sub(a, scale(n, dot(a, n))));
20
+ }
21
+ function stableTangent(n) {
22
+ const axis = Math.abs(n[1]) < 0.82 ? [0, 1, 0] : [1, 0, 0];
23
+ return tangentOf(axis, n);
24
+ }
25
+ const qIdentity = () => [0, 0, 0, 1];
26
+ function qMul(a, b) {
27
+ const [ax, ay, az, aw] = a;
28
+ const [bx, by, bz, bw] = b;
29
+ return [
30
+ aw * bx + ax * bw + ay * bz - az * by,
31
+ aw * by - ax * bz + ay * bw + az * bx,
32
+ aw * bz + ax * by - ay * bx + az * bw,
33
+ aw * bw - ax * bx - ay * by - az * bz
34
+ ];
35
+ }
36
+ const qConj = (q) => [-q[0], -q[1], -q[2], q[3]];
37
+ function qNormalize(q) {
38
+ const l = Math.sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]) || 1;
39
+ return [q[0] / l, q[1] / l, q[2] / l, q[3] / l];
40
+ }
41
+ function qRotate(q, v) {
42
+ const [qx, qy, qz, qw] = q;
43
+ const tx = 2 * (qy * v[2] - qz * v[1]);
44
+ const ty = 2 * (qz * v[0] - qx * v[2]);
45
+ const tz = 2 * (qx * v[1] - qy * v[0]);
46
+ return [
47
+ v[0] + qw * tx + (qy * tz - qz * ty),
48
+ v[1] + qw * ty + (qz * tx - qx * tz),
49
+ v[2] + qw * tz + (qx * ty - qy * tx)
50
+ ];
51
+ }
52
+ function qAxisAngle(axis, radians) {
53
+ const n = normalize(axis);
54
+ const s = Math.sin(radians / 2);
55
+ return [n[0] * s, n[1] * s, n[2] * s, Math.cos(radians / 2)];
56
+ }
57
+ function qFromBasis(x, y, z) {
58
+ const m00 = x[0];
59
+ const m11 = y[1];
60
+ const m22 = z[2];
61
+ const trace = m00 + m11 + m22;
62
+ let q;
63
+ if (trace > 0) {
64
+ const s = Math.sqrt(trace + 1) * 2;
65
+ q = [(y[2] - z[1]) / s, (z[0] - x[2]) / s, (x[1] - y[0]) / s, s / 4];
66
+ } else if (m00 > m11 && m00 > m22) {
67
+ const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
68
+ q = [s / 4, (y[0] + x[1]) / s, (z[0] + x[2]) / s, (y[2] - z[1]) / s];
69
+ } else if (m11 > m22) {
70
+ const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
71
+ q = [(y[0] + x[1]) / s, s / 4, (z[1] + y[2]) / s, (z[0] - x[2]) / s];
72
+ } else {
73
+ const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
74
+ q = [(z[0] + x[2]) / s, (z[1] + y[2]) / s, s / 4, (x[1] - y[0]) / s];
75
+ }
76
+ return qNormalize(q);
77
+ }
78
+ function qFromFrames(n, b, n2, b2) {
79
+ const t = cross(b, n);
80
+ const t2 = cross(b2, n2);
81
+ const from = qFromBasis(t, b, n);
82
+ const to = qFromBasis(t2, b2, n2);
83
+ return qNormalize(qMul(to, qConj(from)));
84
+ }
85
+ function qCanonical(q) {
86
+ const pivot = [q[3], q[0], q[1], q[2]].find((v) => Math.abs(v) > 1e-8) ?? 1;
87
+ const s = pivot < 0 ? -1 : 1;
88
+ return [round(q[0] * s), round(q[1] * s), round(q[2] * s), round(q[3] * s)];
89
+ }
90
+ const qKey = (q) => qCanonical(q).map((v) => v.toFixed(6)).join(":");
91
+ function round(v) {
92
+ return Math.abs(v) < 1e-9 ? 0 : Number(v.toFixed(9));
93
+ }
94
+ function angleBetween(a, b) {
95
+ return Math.acos(Math.max(-1, Math.min(1, dot(a, b))));
96
+ }
97
+ function hashString(s) {
98
+ let h = 2166136261;
99
+ for (let i = 0; i < s.length; i++) {
100
+ h ^= s.charCodeAt(i);
101
+ h = Math.imul(h, 16777619);
102
+ }
103
+ h ^= h >>> 16;
104
+ h = Math.imul(h, 2246822507);
105
+ h ^= h >>> 13;
106
+ h = Math.imul(h, 3266489909);
107
+ h ^= h >>> 16;
108
+ return h >>> 0;
109
+ }
110
+ export {
111
+ EPSILON,
112
+ add,
113
+ angleBetween,
114
+ cross,
115
+ distance,
116
+ dot,
117
+ hashString,
118
+ length,
119
+ normalize,
120
+ qAxisAngle,
121
+ qCanonical,
122
+ qConj,
123
+ qFromBasis,
124
+ qFromFrames,
125
+ qIdentity,
126
+ qKey,
127
+ qMul,
128
+ qNormalize,
129
+ qRotate,
130
+ round,
131
+ scale,
132
+ stableTangent,
133
+ sub,
134
+ tangentOf,
135
+ v3
136
+ };
package/dist/mesh.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { Mesh } from '@aethervtt/shard-mesh';
2
+ import type { DieGeometry } from './definition.js';
3
+ /**
4
+ * A chamfered, flat-shaded die: `bevel` (default the definition's) moves each face corner toward
5
+ * its face's center. Unit radius; the entity's scale sizes it.
6
+ */
7
+ export declare function dieMesh(g: DieGeometry, bevel?: number): Mesh;
8
+ /** A unit quad on the floor (y up), for the tray and its contact blobs. */
9
+ export declare function floorQuad(): Mesh;
package/dist/mesh.js ADDED
@@ -0,0 +1,105 @@
1
+ import { Mesh } from "@aethervtt/shard-mesh";
2
+ import { cellLayout, cellUv } from "./cells.js";
3
+ import { add, cross, dot, normalize, scale, stableTangent, sub } from "./math.js";
4
+ function vertex(b, p, n, t, u, v, face) {
5
+ const i = b.positions.length / 3;
6
+ b.positions.push(p[0], p[1], p[2]);
7
+ b.normals.push(n[0], n[1], n[2]);
8
+ b.tangents.push(t[0], t[1], t[2], 1);
9
+ b.uvs.push(u, v);
10
+ b.uvs1.push(face >= 0 ? 1 : 0, face);
11
+ return i;
12
+ }
13
+ function fan(b, ids) {
14
+ for (let k = 1; k + 1 < ids.length; k++) b.indices.push(ids[0], ids[k], ids[k + 1]);
15
+ }
16
+ function dieMesh(g, bevel = g.definition.bevel) {
17
+ const { points, faces } = g.polytope;
18
+ const layout = cellLayout(g);
19
+ const b = { positions: [], normals: [], uvs: [], uvs1: [], tangents: [], indices: [] };
20
+ const inset = faces.map((face) => {
21
+ const map = /* @__PURE__ */ new Map();
22
+ for (const v of face.vertices) {
23
+ map.set(v, add(points[v], scale(sub(face.center, points[v]), bevel)));
24
+ }
25
+ return map;
26
+ });
27
+ const uv = [0, 0];
28
+ faces.forEach((face, f) => {
29
+ const cell = layout.faces[f];
30
+ const ids = face.vertices.map((v) => {
31
+ const p = inset[f].get(v);
32
+ cellUv(layout, cell, p, uv, 0);
33
+ return vertex(b, p, face.normal, cell.tangent, uv[0], uv[1], f);
34
+ });
35
+ fan(b, ids);
36
+ });
37
+ if (bevel <= 0) return build(b);
38
+ const radial = (p) => normalize(p);
39
+ const chamferVertex = (p) => {
40
+ const n = radial(p);
41
+ return vertex(b, p, n, stableTangent(n), 0, 0, -1);
42
+ };
43
+ const edges = /* @__PURE__ */ new Map();
44
+ faces.forEach((face, f) => {
45
+ const vs = face.vertices;
46
+ for (let k = 0; k < vs.length; k++) {
47
+ const a = vs[k];
48
+ const c = vs[(k + 1) % vs.length];
49
+ const key = a < c ? `${a}:${c}` : `${c}:${a}`;
50
+ const list = edges.get(key) ?? [];
51
+ list.push({ face: f, a, b: c });
52
+ edges.set(key, list);
53
+ }
54
+ });
55
+ for (const pair of edges.values()) {
56
+ if (pair.length !== 2) continue;
57
+ const [first, second] = pair;
58
+ const quad = [
59
+ inset[first.face].get(first.a),
60
+ inset[first.face].get(first.b),
61
+ inset[second.face].get(first.b),
62
+ inset[second.face].get(first.a)
63
+ ];
64
+ const center = scale(add(add(quad[0], quad[1]), add(quad[2], quad[3])), 0.25);
65
+ const n = cross(sub(quad[1], quad[0]), sub(quad[2], quad[0]));
66
+ if (dot(n, center) < 0) quad.reverse();
67
+ fan(b, quad.map(chamferVertex));
68
+ }
69
+ points.forEach((p, v) => {
70
+ const around = [];
71
+ faces.forEach((face, f) => {
72
+ if (face.vertices.includes(v)) around.push(inset[f].get(v));
73
+ });
74
+ if (around.length < 3) return;
75
+ const out = normalize(p);
76
+ const t = stableTangent(out);
77
+ const bt = cross(out, t);
78
+ around.sort((l, r) => Math.atan2(dot(l, bt), dot(l, t)) - Math.atan2(dot(r, bt), dot(r, t)));
79
+ fan(b, around.map(chamferVertex));
80
+ });
81
+ return build(b);
82
+ }
83
+ function build(b) {
84
+ const n = b.positions.length / 3;
85
+ return Mesh.create({
86
+ positions: Float32Array.from(b.positions),
87
+ normals: Float32Array.from(b.normals),
88
+ uvs: Float32Array.from(b.uvs),
89
+ uvs1: Float32Array.from(b.uvs1),
90
+ tangents: Float32Array.from(b.tangents),
91
+ indices: n < 65536 ? Uint16Array.from(b.indices) : Uint32Array.from(b.indices)
92
+ });
93
+ }
94
+ function floorQuad() {
95
+ return Mesh.create({
96
+ positions: Float32Array.from([-1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1]),
97
+ normals: Float32Array.from([0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]),
98
+ uvs: Float32Array.from([0, 0, 1, 0, 1, 1, 0, 1]),
99
+ indices: Uint16Array.from([0, 2, 1, 0, 3, 2])
100
+ });
101
+ }
102
+ export {
103
+ dieMesh,
104
+ floorQuad
105
+ };
@@ -0,0 +1,2 @@
1
+ import type { AppMethod } from '@aethervtt/shard-runtime';
2
+ export declare const diceMethods: AppMethod[];
@@ -0,0 +1,42 @@
1
+ import { defineSchema, t } from "@aethervtt/shard-core";
2
+ import { DiceTable } from "./table.js";
3
+ const diceMethods = [
4
+ {
5
+ name: "dice.describe",
6
+ description: "The dice table: phase, quality tier and its reason, each die (kind, value, label, natural value, correction, placed), the track (hash, steps, settled, maxStepsHit, simulation ms, contacts), placed-fallback dice, matched recipes, draw calls, live attachments, the dice's lens fields, resources and GPU objects.",
7
+ params: defineSchema("dice/DescribeParams", {}),
8
+ handler: ({ world }) => world.resource(DiceTable).describe()
9
+ },
10
+ {
11
+ name: "dice.play",
12
+ description: "Plays a roll the host decided: { roll: { id, dice: [{ kind: 'd20', value: 17, skin: { guid: 'dice:skin/ivory' } }], seed, tray, motion, quality, effects }, replace, wait }. With wait, resolves with the outcome once it's over (a 60 s rest unless dismissed); otherwise at once.",
13
+ params: defineSchema("dice/PlayParams", {
14
+ roll: t.json({ description: "A DiceRoll." }),
15
+ replace: t.bool({ description: "Dismiss a roll that is playing." }),
16
+ wait: t.bool({ description: "Resolve with the outcome instead of at once." })
17
+ }),
18
+ handler: async ({ world }, p) => {
19
+ const table = world.resource(DiceTable);
20
+ const outcome = table.play(p.roll, { replace: p.replace });
21
+ if (p.wait) return { outcome: await outcome, lastError: table.describe().lastError };
22
+ outcome.catch(() => {
23
+ });
24
+ return { started: true, phase: table.phase };
25
+ }
26
+ },
27
+ {
28
+ name: "dice.dismiss",
29
+ description: "Dismisses the roll on the table, if any.",
30
+ params: defineSchema("dice/DismissParams", {}),
31
+ handler: ({ world }) => ({ dismissed: world.resource(DiceTable).dismiss() })
32
+ },
33
+ {
34
+ name: "dice.skip",
35
+ description: "Lands every entrance still to come or playing, now (0065): each die snaps to its rest pose, target up; scenes wind down within 400 ms.",
36
+ params: defineSchema("dice/SkipParams", {}),
37
+ handler: ({ world }) => ({ skipped: world.resource(DiceTable).skip() })
38
+ }
39
+ ];
40
+ export {
41
+ diceMethods
42
+ };
@@ -0,0 +1,9 @@
1
+ export interface NumeralGlyph {
2
+ /** Advance width, thousandths of an em. */
3
+ advance: number;
4
+ /** Outline: M, L, Q, C and Z commands, absolute, y up. */
5
+ path: string;
6
+ }
7
+ /** Cap height of the digits, thousandths of an em. */
8
+ export declare const NUMERAL_HEIGHT = 727.5;
9
+ export declare const DICE_NUMERALS: Readonly<Record<string, NumeralGlyph>>;