@pieai/swimmer-avatar-kit 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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +23 -0
  3. package/README.md +125 -0
  4. package/UPSTREAM.md +47 -0
  5. package/dist/avatar.d.ts +2 -0
  6. package/dist/avatar.js +69 -0
  7. package/dist/avatar.js.map +1 -0
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/materials.d.ts +20 -0
  12. package/dist/materials.js +61 -0
  13. package/dist/materials.js.map +1 -0
  14. package/dist/react-three-fiber.d.ts +16 -0
  15. package/dist/react-three-fiber.js +52 -0
  16. package/dist/react-three-fiber.js.map +1 -0
  17. package/dist/recipe.d.ts +18 -0
  18. package/dist/recipe.js +118 -0
  19. package/dist/recipe.js.map +1 -0
  20. package/dist/types.d.ts +62 -0
  21. package/dist/types.js +2 -0
  22. package/dist/types.js.map +1 -0
  23. package/package.json +102 -0
  24. package/upstream-lock.json +38 -0
  25. package/vendor/kindergrimm/LICENSE +24 -0
  26. package/vendor/kindergrimm/src/gloss/catmullClark.js +129 -0
  27. package/vendor/kindergrimm/src/gloss/gface.d.ts +25 -0
  28. package/vendor/kindergrimm/src/gloss/gface.js +226 -0
  29. package/vendor/kindergrimm/src/gloss/gform.js +201 -0
  30. package/vendor/kindergrimm/src/gloss/ghair.js +534 -0
  31. package/vendor/kindergrimm/src/gloss/glayout.js +365 -0
  32. package/vendor/kindergrimm/src/gloss/gmedia.d.ts +28 -0
  33. package/vendor/kindergrimm/src/gloss/gmedia.js +470 -0
  34. package/vendor/kindergrimm/src/gloss/gpalette.d.ts +10 -0
  35. package/vendor/kindergrimm/src/gloss/gpalette.js +205 -0
  36. package/vendor/kindergrimm/src/gloss/gparts/blush.js +57 -0
  37. package/vendor/kindergrimm/src/gloss/gparts/body.js +47 -0
  38. package/vendor/kindergrimm/src/gloss/gparts/brows.js +55 -0
  39. package/vendor/kindergrimm/src/gloss/gparts/crest.js +88 -0
  40. package/vendor/kindergrimm/src/gloss/gparts/eyes.js +357 -0
  41. package/vendor/kindergrimm/src/gloss/gparts/frame.js +225 -0
  42. package/vendor/kindergrimm/src/gloss/gparts/hair.js +100 -0
  43. package/vendor/kindergrimm/src/gloss/gparts/hat.js +163 -0
  44. package/vendor/kindergrimm/src/gloss/gparts/index.js +33 -0
  45. package/vendor/kindergrimm/src/gloss/gparts/mark.js +139 -0
  46. package/vendor/kindergrimm/src/gloss/gparts/mouth.js +170 -0
  47. package/vendor/kindergrimm/src/gloss/gparts/nose.js +63 -0
  48. package/vendor/kindergrimm/src/gloss/gparts/specs.js +124 -0
  49. package/vendor/kindergrimm/src/gloss/grig.d.ts +41 -0
  50. package/vendor/kindergrimm/src/gloss/grig.js +286 -0
  51. package/vendor/kindergrimm/src/gloss/gshape.js +421 -0
  52. package/vendor/kindergrimm/src/gloss/gspecies.d.ts +10 -0
  53. package/vendor/kindergrimm/src/gloss/gspecies.js +338 -0
  54. package/vendor/kindergrimm/src/gloss/gtexture.js +406 -0
  55. package/vendor/kindergrimm/src/rng.d.ts +11 -0
  56. package/vendor/kindergrimm/src/rng.js +27 -0
@@ -0,0 +1,421 @@
1
+ // ---------------------------------------------------------------
2
+ // THE HAND — `sketch.js` draws, `carve.js` carves, this one CUTS AND
3
+ // STAMPS. Everything in this lab is solid geometry: no fields, no
4
+ // sampling grid, nothing to resolve. Two things get made here.
5
+ //
6
+ // A SOLID — the body. A sphere, scaled.
7
+ // A PLATE — every face feature. An outline, extruded, bevelled.
8
+ //
9
+ // The plate is the whole face system, and it is one shape of thing on
10
+ // purpose: a flat front with a rounded rim. That rim is the cartoon
11
+ // read — it is what catches the studio highlight around the edge of an
12
+ // eye, and it is why a feature looks moulded into the character rather than
13
+ // printed on it.
14
+ //
15
+ // AUTHORING RULE: a plate's front crest sits at z = 0 and its body
16
+ // runs BACKWARD. Placed on the skin it is therefore flush, and `proud`
17
+ // is the single number that lifts it out. Because the body runs
18
+ // backward and the sphere curves away underneath, a plate can never
19
+ // float off the silhouette.
20
+ //
21
+ // Parts never import this file — they never touch three.js. They name
22
+ // an OUTLINE and hand over numbers, and the rig stamps them here.
23
+ // ---------------------------------------------------------------
24
+ import * as THREE from 'three';
25
+
26
+ const SEG = 44;
27
+
28
+ // THE DETAIL KNOB — one number, set once by the page, that every
29
+ // segment count here is scaled by.
30
+ //
31
+ // It exists because the lab and a GAME want opposite things and both
32
+ // are right. On `gloss.html` a character is the subject, there is exactly
33
+ // one of it, and it is four hundred pixels across: every segment is
34
+ // worth having. In `marbles.html` there are a dozen on a table at
35
+ // sixty pixels each, and at full detail one of them was a quarter of a
36
+ // million vertices and twenty-eight milliseconds to build — which is
37
+ // half a frame's budget, per marble, for a resolution that lands
38
+ // entirely between two pixels.
39
+ //
40
+ // It is a SCALE rather than a set of alternative numbers, so the lab's
41
+ // proportions survive it, and every count keeps a floor: a body below
42
+ // about fourteen segments stops being round, and a bevel below two
43
+ // stops being a bevel, which is the whole cartoon read.
44
+ let DETAIL = 1;
45
+
46
+ /** 1 is the lab. A game asks for less; nothing else should. */
47
+ export function setGlossDetail(k) { DETAIL = Math.max(.2, Math.min(1, k)); }
48
+ export const glossDetail = () => DETAIL;
49
+ const seg = (n, min) => Math.max(min, Math.round(n * DETAIL));
50
+
51
+ // ---- outlines ---------------------------------------------------------
52
+ // Every one is centred on the origin and takes HALF-extents.
53
+
54
+ function ellipse(w, h) {
55
+ const s = new THREE.Shape();
56
+ s.absellipse(0, 0, w, h, 0, Math.PI * 2, false, 0);
57
+ return s;
58
+ }
59
+
60
+ function ring(w, h, thick) {
61
+ const s = ellipse(w, h);
62
+ const hole = new THREE.Path();
63
+ hole.absellipse(0, 0, w * (1 - thick), h * (1 - thick), 0, Math.PI * 2, true, 0);
64
+ s.holes.push(hole);
65
+ return s;
66
+ }
67
+
68
+ function roundedRect(w, h, r) {
69
+ const s = new THREE.Shape();
70
+ r = Math.min(r, w, h);
71
+ s.moveTo(-w + r, -h);
72
+ s.lineTo(w - r, -h); s.absarc(w - r, -h + r, r, -Math.PI / 2, 0);
73
+ s.lineTo(w, h - r); s.absarc(w - r, h - r, r, 0, Math.PI / 2);
74
+ s.lineTo(-w + r, h); s.absarc(-w + r, h - r, r, Math.PI / 2, Math.PI);
75
+ s.lineTo(-w, -h + r); s.absarc(-w + r, -h + r, r, Math.PI, Math.PI * 1.5);
76
+ return s;
77
+ }
78
+
79
+ /** a rounded rectangle with a rounded rectangle punched out of it —
80
+ * square spectacles, and anything else that is a frame rather than a
81
+ * filled shape. `ring` is its elliptical twin. */
82
+ function rectRing(w, h, r, thick) {
83
+ const s = roundedRect(w, h, r);
84
+ const iw = w * (1 - thick), ih = h * (1 - thick);
85
+ const ir = Math.max(1e-4, Math.min(r * (1 - thick), iw, ih));
86
+ const hole = new THREE.Path();
87
+ // wound the OTHER way round from `roundedRect`: a hole has to run
88
+ // against its shape or three fills it in as solid
89
+ hole.moveTo(-iw + ir, -ih);
90
+ hole.absarc(-iw + ir, -ih + ir, ir, Math.PI * 1.5, Math.PI, true);
91
+ hole.lineTo(-iw, ih - ir);
92
+ hole.absarc(-iw + ir, ih - ir, ir, Math.PI, Math.PI / 2, true);
93
+ hole.lineTo(iw - ir, ih);
94
+ hole.absarc(iw - ir, ih - ir, ir, Math.PI / 2, 0, true);
95
+ hole.lineTo(iw, -ih + ir);
96
+ hole.absarc(iw - ir, -ih + ir, ir, 0, -Math.PI / 2, true);
97
+ s.holes.push(hole);
98
+ return s;
99
+ }
100
+
101
+ /** the four-point sparkle. The sides are pulled IN toward the centre —
102
+ * that concavity is the entire reason it reads as a star and not a
103
+ * diamond, and `pinch` is how deep the waist cuts. */
104
+ function sparkle(w, h, pinch) {
105
+ const s = new THREE.Shape();
106
+ s.moveTo(0, h);
107
+ s.quadraticCurveTo(w * pinch, h * pinch, w, 0);
108
+ s.quadraticCurveTo(w * pinch, -h * pinch, 0, -h);
109
+ s.quadraticCurveTo(-w * pinch, -h * pinch, -w, 0);
110
+ s.quadraticCurveTo(-w * pinch, h * pinch, 0, h);
111
+ return s;
112
+ }
113
+
114
+ function star5(w, h) {
115
+ const s = new THREE.Shape();
116
+ for (let i = 0; i < 10; i++) {
117
+ const a = -Math.PI / 2 + i * Math.PI / 5;
118
+ const r = i % 2 ? .46 : 1;
119
+ const x = Math.cos(a) * w * r, y = -Math.sin(a) * h * r;
120
+ i === 0 ? s.moveTo(x, y) : s.lineTo(x, y);
121
+ }
122
+ s.closePath();
123
+ return s;
124
+ }
125
+
126
+ function heart(w, h) {
127
+ const s = new THREE.Shape();
128
+ s.moveTo(0, -h);
129
+ s.bezierCurveTo(-w * 1.25, h * .18, -w * .62, h * 1.28, 0, h * .46);
130
+ s.bezierCurveTo(w * .62, h * 1.28, w * 1.25, h * .18, 0, -h);
131
+ return s;
132
+ }
133
+
134
+ /** an X: a plus polygon turned 45°. The bevel rounds its corners. */
135
+ function cross(w, arm) {
136
+ const c = Math.SQRT1_2;
137
+ const pts = [[arm, arm], [w, arm], [w, -arm], [arm, -arm], [arm, -w], [-arm, -w],
138
+ [-arm, -arm], [-w, -arm], [-w, arm], [-arm, arm], [-arm, w], [arm, w]];
139
+ const s = new THREE.Shape();
140
+ pts.forEach(([x, y], i) => {
141
+ const px = (x - y) * c, py = (x + y) * c;
142
+ i === 0 ? s.moveTo(px, py) : s.lineTo(px, py);
143
+ });
144
+ s.closePath();
145
+ return s;
146
+ }
147
+
148
+ /** a polar polygon: `r(θ)` sampled round once. Every lobed outline in
149
+ * the catalogue is one of these with a different harmonic. */
150
+ function polar(w, h, n, fn) {
151
+ const s = new THREE.Shape();
152
+ for (let i = 0; i <= n; i++) {
153
+ const a = i / n * Math.PI * 2, r = fn(a);
154
+ const x = Math.cos(a) * w * r, y = Math.sin(a) * h * r;
155
+ i === 0 ? s.moveTo(x, y) : s.lineTo(x, y);
156
+ }
157
+ s.closePath();
158
+ return s;
159
+ }
160
+
161
+ /** a daisy. `petals` lobes from one absolute cosine. */
162
+ function flower(w, h, petals, amp) {
163
+ return polar(w, h, 132, a => 1 - amp + amp * Math.abs(Math.cos(petals * a / 2)));
164
+ }
165
+
166
+ /** an organic lump — fixed harmonics, so it is the SAME lump every
167
+ * rebuild. A wobble rolled per frame is the drawn rig's line boil,
168
+ * and a solid has no business shimmering. */
169
+ function wobble(w, h) {
170
+ return polar(w, h, 108, a => 1 + .13 * Math.cos(3 * a + .7) + .07 * Math.sin(5 * a));
171
+ }
172
+
173
+ /** a moon: one bulging arc out and a shallower one back. `bite` is how
174
+ * deep the inner curve cuts, so 0 is a disc and 1 is a hair. */
175
+ function crescent(w, h, bite) {
176
+ const s = new THREE.Shape();
177
+ const o = w * 1.33, i = o * (1 - bite);
178
+ s.moveTo(0, h);
179
+ s.bezierCurveTo(-o, h * .55, -o, -h * .55, 0, -h);
180
+ s.bezierCurveTo(-i, -h * .5, -i, h * .5, 0, h);
181
+ return s;
182
+ }
183
+
184
+ function triangle(w, h, flip) {
185
+ const s = new THREE.Shape();
186
+ const d = flip ? -1 : 1;
187
+ s.moveTo(0, -h * d); s.lineTo(w, h * d); s.lineTo(-w, h * d);
188
+ s.closePath();
189
+ return s;
190
+ }
191
+
192
+ // ---- bands ------------------------------------------------------------
193
+ // A mouth, a brow and a closed lid are all the SAME object: a stroke of
194
+ // constant width along a centreline. One ribbon function and a table of
195
+ // centrelines gives the whole family, so adding a mouth shape is adding
196
+ // a curve, not a polygon.
197
+
198
+ function ribbon(pts, tube) {
199
+ const n = pts.length, nor = [];
200
+ for (let i = 0; i < n; i++) {
201
+ const a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
202
+ let dx = b[0] - a[0], dy = b[1] - a[1];
203
+ const l = Math.hypot(dx, dy) || 1;
204
+ nor.push([-dy / l, dx / l]);
205
+ }
206
+ const s = new THREE.Shape();
207
+ s.moveTo(pts[0][0] + nor[0][0] * tube, pts[0][1] + nor[0][1] * tube);
208
+ for (let i = 1; i < n; i++) s.lineTo(pts[i][0] + nor[i][0] * tube, pts[i][1] + nor[i][1] * tube);
209
+ for (let i = n - 1; i >= 0; i--) s.lineTo(pts[i][0] - nor[i][0] * tube, pts[i][1] - nor[i][1] * tube);
210
+ s.closePath();
211
+ return s;
212
+ }
213
+
214
+ const STEPS = 16;
215
+ const span = f => Array.from({ length: STEPS }, (_, i) => f(-1 + 2 * i / (STEPS - 1)));
216
+
217
+ // `sag > 0` curves the middle DOWN — which is a smile, because the
218
+ // ends are what lift. Negate it and the same curve is a frown.
219
+ const CURVE = {
220
+ line: (w, sag) => [[-w, 0], [w, 0]],
221
+ arc: (w, sag) => span(t => [t * w, sag * (t * t - 1)]),
222
+ // ω — two dips meeting at a peak in the middle. The cat mouth.
223
+ wave: (w, sag) => span(t => [t * w, sag * (Math.cos(t * Math.PI * 2) * .5 - .5)]),
224
+ zig: (w, sag) => Array.from({ length: 7 }, (_, i) =>
225
+ [-w + 2 * w * i / 6, (i % 2 ? -sag : sag) * .6]),
226
+ // dizzy. A spiral is not a shape to be cut, it is a STROKE — so it
227
+ // costs one centreline here rather than a new outline.
228
+ spiral: w => Array.from({ length: 40 }, (_, i) => {
229
+ const t = i / 39, a = t * Math.PI * 4.3, r = w * (1 - t * .84);
230
+ return [Math.cos(a) * r, Math.sin(a) * r];
231
+ }),
232
+ };
233
+
234
+ // ---- the dispatch -----------------------------------------------------
235
+ // A part names one of these and passes numbers. Nothing else.
236
+
237
+ const OUTLINE = {
238
+ ellipse: s => ellipse(s.w, s.h),
239
+ ring: s => ring(s.w, s.h, s.thick ?? .36),
240
+ rring: s => rectRing(s.w, s.h, s.r ?? s.h * .4, s.thick ?? .3),
241
+ rect: s => roundedRect(s.w, s.h, s.r ?? s.h * .92),
242
+ sparkle: s => sparkle(s.w, s.h, s.pinch ?? .22),
243
+ star: s => star5(s.w, s.h),
244
+ heart: s => heart(s.w, s.h),
245
+ cross: s => cross(Math.max(s.w, s.h), Math.min(s.w, s.h) * .38),
246
+ tri: s => triangle(s.w, s.h, s.flip),
247
+ flower: s => flower(s.w, s.h, s.petals ?? 5, s.amp ?? .3),
248
+ wobble: s => wobble(s.w, s.h),
249
+ crescent: s => crescent(s.w, s.h, s.bite ?? .55),
250
+ band: s => ribbon(CURVE[s.curve ?? 'arc'](s.w, s.sag ?? 0), s.tube ?? s.h),
251
+ };
252
+
253
+ export const OUTLINES = Object.keys(OUTLINE);
254
+
255
+ /**
256
+ * plate spec → BufferGeometry, front crest at z = 0, body running
257
+ * backward, centred in u/v.
258
+ * { outline, w, h, d, bevel?, + whatever that outline reads }
259
+ */
260
+ export function plateGeometry(spec) {
261
+ const shape = OUTLINE[spec.outline](spec);
262
+ const depth = Math.max(spec.d, 1e-4);
263
+ // the bevel cannot exceed what the thinnest part of the outline has
264
+ // to give up, or ExtrudeGeometry folds the shape inside out
265
+ const bevel = Math.max(1e-5, Math.min(spec.bevel ?? depth * .55, depth * .9));
266
+
267
+ // A POLYLINE NEEDS NO CURVE SEGMENTS. `ExtrudeGeometry` subdivides
268
+ // EVERY curve of an outline by `curveSegments`, and a `LineCurve`
269
+ // subdivided is the same straight line carrying forty times the
270
+ // vertices. The whole BAND family — every mouth, brow, closed lid and
271
+ // blush stripe — is built by `ribbon` out of straight segments, so it
272
+ // was paying that toll for nothing: one mouth came to seven thousand
273
+ // vertices, four times what the body cost. Detecting it here rather
274
+ // than flagging it per outline means a new polyline outline gets the
275
+ // saving without knowing this function exists.
276
+ const straight = shape.curves.every(c => c.isLineCurve)
277
+ && shape.holes.every(h => h.curves.every(c => c.isLineCurve));
278
+
279
+ const g = new THREE.ExtrudeGeometry(shape, {
280
+ depth, steps: 1, curveSegments: straight ? 1 : seg(SEG, 8),
281
+ bevelEnabled: true, bevelThickness: bevel, bevelSize: bevel, bevelSegments: seg(6, 2),
282
+ });
283
+ g.translate(0, 0, -(depth + bevel)); // front crest to z = 0
284
+ g.computeBoundingBox();
285
+ const b = g.boundingBox;
286
+ g.translate(-(b.min.x + b.max.x) / 2, -(b.min.y + b.max.y) / 2, 0);
287
+ g.computeVertexNormals();
288
+ return g;
289
+ }
290
+
291
+ // ---- THE SURFACE, shared with the layout ------------------------------
292
+ // A superellipsoid: |x/rx|ⁿ + |y/ry|ⁿ + |z/rz|ⁿ = 1.
293
+ //
294
+ // These two functions ARE the surface: `solidGeometry` builds with
295
+ // them and `L.at` places with them, imported from here, so the two can
296
+ // never disagree about where the skin is — which used to be guaranteed
297
+ // by careful duplication and is now guaranteed by there being one copy.
298
+ //
299
+ // A chin-tuck knob lived here for a day, trying to bend this formula
300
+ // into a chibi skull, and it could not: one implicit surface cannot
301
+ // say "cheeks full HERE, chin THIS wide, face flat in front". The
302
+ // humanoid's head is MODELED instead — a control cage + subdivision,
303
+ // in gskull.js — and this family stays what it is: a ball with a
304
+ // squareness knob.
305
+
306
+ // Two shapes the exponent cannot reach — a rock that sits FLAT and a
307
+ // slime that comes to a POINT — are not here at all. They were tried
308
+ // twice in this file, first as a radial displacement on the ball and
309
+ // then as a surface of revolution over a silhouette, and both are gone:
310
+ // see `gform.js`, which models them from a control cage. A radial
311
+ // scale moves a ball's surface but keeps its topology of extents, so
312
+ // nothing can sit flat; a profile is only an outline, and everything a
313
+ // shape does that is not its outline then has to be smuggled in on
314
+ // top. A shape you can name the parts of wants a cage.
315
+
316
+ /** how far along direction (dx,dy,dz) the surface sits. */
317
+ export function surfT(dx, dy, dz, rx, ry, rz, n) {
318
+ return 1 / Math.pow(Math.pow(Math.abs(dx / rx), n)
319
+ + Math.pow(Math.abs(dy / ry), n)
320
+ + Math.pow(Math.abs(dz / rz), n), 1 / n);
321
+ }
322
+
323
+ /** the outward normal at a surface point — the GRADIENT, never the
324
+ * direction from the centre. */
325
+ export function surfN(x, y, z, rx, ry, rz, n) {
326
+ const gx = Math.pow(Math.abs(x / rx), n - 1) * Math.sign(x) / rx;
327
+ const gy = Math.pow(Math.abs(y / ry), n - 1) * Math.sign(y) / ry;
328
+ const gz = Math.pow(Math.abs(z / rz), n - 1) * Math.sign(z) / rz;
329
+ const l = Math.hypot(gx, gy, gz) || 1;
330
+ return [gx / l, gy / l, gz / l];
331
+ }
332
+
333
+ /**
334
+ * The body, and any other solid lump (a button nose).
335
+ *
336
+ * A SUPERELLIPSOID: `|x/rx|ⁿ + |y/ry|ⁿ + |z/rz|ⁿ = 1`. One number is
337
+ * the whole shape family — n=2 is a ball, n≈4 a rounded cube, n=8 a
338
+ * box with the corners knocked off — which is the same lever the voxel
339
+ * hand uses for its discs and the same reason: a form should be a knob
340
+ * you can scrub, not a second primitive to maintain.
341
+ *
342
+ * It is built by pushing a sphere's vertices out to the surface, so the
343
+ * topology (and therefore the UV-free smooth shading) is unchanged. The
344
+ * normals are ANALYTIC — the gradient of the implicit surface, not
345
+ * averaged from triangles — because a rounded cube's flat faces meet
346
+ * its corners at exactly the place where averaged normals go lumpy, and
347
+ * a lumpy highlight on a clearcoat is the first thing you notice.
348
+ *
349
+ * `dome` cuts the solid short: a fraction of π swept down from the top
350
+ * pole, so `.5` is a hemisphere. It exists for exactly one thing — the
351
+ * ball eye's lid, a cap that sits OVER another solid — which is why the
352
+ * open rim is fine: whatever a dome caps is filling it from inside.
353
+ */
354
+ export function solidGeometry(rx, ry, rz, n = 2, dome = 0, domeFrom = 0) {
355
+ const g = dome
356
+ ? new THREE.SphereGeometry(1, seg(72, 16), seg(40, 10), 0, Math.PI * 2,
357
+ Math.PI * domeFrom, Math.PI * (dome - domeFrom))
358
+ : new THREE.SphereGeometry(1, seg(72, 16), seg(54, 12));
359
+ const pos = g.attributes.position, nor = g.attributes.normal;
360
+ for (let i = 0; i < pos.count; i++) {
361
+ // a sphere vertex is already a unit direction
362
+ const dx = pos.getX(i), dy = pos.getY(i), dz = pos.getZ(i);
363
+ const t = surfT(dx, dy, dz, rx, ry, rz, n);
364
+ const x = dx * t, y = dy * t, z = dz * t;
365
+ pos.setXYZ(i, x, y, z);
366
+ const nn = surfN(x, y, z, rx, ry, rz, n);
367
+ nor.setXYZ(i, nn[0], nn[1], nn[2]);
368
+ }
369
+ pos.needsUpdate = true; nor.needsUpdate = true;
370
+ g.computeBoundingSphere();
371
+ return g;
372
+ }
373
+
374
+ /**
375
+ * A modeled mesh (`gform.js`, `ghair.js`) → BufferGeometry. The arrays arrive
376
+ * ALREADY BUILT from the layout's surface — the same vertices the
377
+ * features were placed against — so this is packing, not modeling.
378
+ * All vertices are shared, so computeVertexNormals is seam-free.
379
+ */
380
+ export function skullGeometry({ verts, faces }) {
381
+ const position = new Float32Array(verts.length * 3);
382
+ for (let i = 0; i < verts.length; i++) {
383
+ position[i * 3] = verts[i][0];
384
+ position[i * 3 + 1] = verts[i][1];
385
+ position[i * 3 + 2] = verts[i][2];
386
+ }
387
+ const indices = [];
388
+ for (const f of faces)
389
+ for (let i = 2; i < f.length; i++) indices.push(f[0], f[i - 1], f[i]);
390
+
391
+ const g = new THREE.BufferGeometry();
392
+ g.setAttribute('position', new THREE.BufferAttribute(position, 3));
393
+ g.setIndex(indices);
394
+ g.computeVertexNormals();
395
+ g.computeBoundingSphere();
396
+ return g;
397
+ }
398
+
399
+ /**
400
+ * surface point + normal → plate transform. v is held to world-up so a
401
+ * feature never rolls with the surface's whim, and `proud` lifts it out
402
+ * along the normal. `roll` tilts it in its own plane — which is all a
403
+ * slanted eyebrow is.
404
+ */
405
+ export function basisAt(p, n, proud = 0, roll = 0) {
406
+ const N = new THREE.Vector3(n[0], n[1], n[2]).normalize();
407
+ const T = new THREE.Vector3(0, 1, 0).cross(N);
408
+ if (T.lengthSq() < 1e-6) T.set(1, 0, 0); else T.normalize();
409
+ const B = new THREE.Vector3().crossVectors(N, T);
410
+ const q = new THREE.Quaternion().setFromRotationMatrix(
411
+ new THREE.Matrix4().makeBasis(T, B, N));
412
+ if (roll) q.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), roll));
413
+ return {
414
+ position: new THREE.Vector3(p[0], p[1], p[2]).addScaledVector(N, proud),
415
+ quaternion: q,
416
+ };
417
+ }
418
+
419
+ // Animating the face is NOT here — `gface.js` owns every write to a
420
+ // feature mesh, so blink, gaze and expression can never fight each
421
+ // other over the same transform.
@@ -0,0 +1,10 @@
1
+ export interface GlossSpecies {
2
+ label: string;
3
+ material?: string;
4
+ palette?: string;
5
+ cast: unknown;
6
+ }
7
+
8
+ export const GSPECIES: Readonly<Record<string, GlossSpecies>>;
9
+ export const GSPECIES_IDS: readonly string[];
10
+ export const GSPECIES_WEIGHTS: readonly [string, number][];