loomlarge 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.
package/dist/index.js ADDED
@@ -0,0 +1,1166 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // src/engines/three/AnimationThree.ts
6
+ var easeInOutQuad = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
7
+ var AnimationThree = class {
8
+ constructor() {
9
+ __publicField(this, "transitions", /* @__PURE__ */ new Map());
10
+ }
11
+ /**
12
+ * Tick all active transitions by dt seconds.
13
+ * Applies eased interpolation and removes completed transitions.
14
+ * Respects individual transition pause state.
15
+ */
16
+ tick(dtSeconds) {
17
+ if (dtSeconds <= 0) return;
18
+ const completed = [];
19
+ this.transitions.forEach((t, key) => {
20
+ if (t.paused) return;
21
+ t.elapsed += dtSeconds;
22
+ const progress = Math.min(t.elapsed / t.duration, 1);
23
+ const easedProgress = t.easing(progress);
24
+ const value = t.from + (t.to - t.from) * easedProgress;
25
+ t.apply(value);
26
+ if (progress >= 1) {
27
+ completed.push(key);
28
+ t.resolve?.();
29
+ }
30
+ });
31
+ completed.forEach((key) => this.transitions.delete(key));
32
+ }
33
+ /**
34
+ * Add or replace a transition for the given key.
35
+ * If a transition with the same key exists, it is cancelled and replaced.
36
+ * @returns TransitionHandle with { promise, pause, resume, cancel }
37
+ */
38
+ addTransition(key, from, to, durationMs, apply, easing = easeInOutQuad) {
39
+ const durationSec = durationMs / 1e3;
40
+ const existing = this.transitions.get(key);
41
+ if (existing?.resolve) {
42
+ existing.resolve();
43
+ }
44
+ if (durationSec <= 0 || Math.abs(to - from) < 1e-6) {
45
+ apply(to);
46
+ return {
47
+ promise: Promise.resolve(),
48
+ pause: () => {
49
+ },
50
+ resume: () => {
51
+ },
52
+ cancel: () => {
53
+ }
54
+ };
55
+ }
56
+ const promise = new Promise((resolve) => {
57
+ const transitionObj = {
58
+ key,
59
+ from,
60
+ to,
61
+ duration: durationSec,
62
+ elapsed: 0,
63
+ apply,
64
+ easing,
65
+ resolve,
66
+ paused: false
67
+ };
68
+ this.transitions.set(key, transitionObj);
69
+ });
70
+ return {
71
+ promise,
72
+ pause: () => {
73
+ const t = this.transitions.get(key);
74
+ if (t) t.paused = true;
75
+ },
76
+ resume: () => {
77
+ const t = this.transitions.get(key);
78
+ if (t) t.paused = false;
79
+ },
80
+ cancel: () => {
81
+ const t = this.transitions.get(key);
82
+ if (t) {
83
+ t.resolve?.();
84
+ this.transitions.delete(key);
85
+ }
86
+ }
87
+ };
88
+ }
89
+ /** Clear all running transitions. */
90
+ clearTransitions() {
91
+ this.transitions.forEach((t) => t.resolve?.());
92
+ this.transitions.clear();
93
+ }
94
+ /** Get count of active transitions. */
95
+ getActiveTransitionCount() {
96
+ return this.transitions.size;
97
+ }
98
+ };
99
+
100
+ // src/presets/cc4.ts
101
+ var AU_TO_MORPHS = {
102
+ // Brows / Forehead
103
+ 1: ["Brow_Raise_Inner_L", "Brow_Raise_Inner_R"],
104
+ 2: ["Brow_Raise_Outer_L", "Brow_Raise_Outer_R"],
105
+ 4: ["Brow_Drop_L", "Brow_Drop_R"],
106
+ // Eyes / Lids
107
+ 5: ["Eye_Wide_L", "Eye_Wide_R"],
108
+ 6: ["Cheek_Raise_L", "Cheek_Raise_R"],
109
+ 7: ["Eye_Squint_L", "Eye_Squint_R"],
110
+ 43: ["Eye_Blink_L", "Eye_Blink_R"],
111
+ 45: ["Eye_Blink_L", "Eye_Blink_R"],
112
+ // Blink alias
113
+ // Nose / Midface
114
+ 9: ["Nose_Sneer_L", "Nose_Sneer_R"],
115
+ 34: ["Cheek_Puff_L", "Cheek_Puff_R"],
116
+ // Mouth / Lips
117
+ 8: ["Mouth_Press_L", "Mouth_Press_R", "Mouth_Close"],
118
+ 10: ["Nose_Sneer_L", "Nose_Sneer_R"],
119
+ // Upper Lip Raiser (levator labii superioris) - raises upper lip in disgust/sneer
120
+ 11: ["Mouth_Up_Upper_L", "Mouth_Up_Upper_R"],
121
+ // Nasolabial Deepener (zygomaticus minor) - no dedicated morph
122
+ 12: ["Mouth_Smile_L", "Mouth_Smile_R"],
123
+ 13: ["Mouth_Dimple_L", "Mouth_Dimple_R"],
124
+ // Sharp Lip Puller (levator anguli oris) - pulls lip corners up
125
+ 14: ["Mouth_Press_L", "Mouth_Press_R"],
126
+ 15: ["Mouth_Frown_L", "Mouth_Frown_R"],
127
+ 16: ["Mouth_Down_Lower_L", "Mouth_Down_Lower_R"],
128
+ 17: ["Mouth_Shrug_Lower"],
129
+ 18: ["Mouth_Pucker"],
130
+ 20: ["Mouth_Stretch_L", "Mouth_Stretch_R"],
131
+ 22: ["Mouth_Funnel"],
132
+ 23: ["Mouth_Press_L", "Mouth_Press_R"],
133
+ 24: ["Mouth_Press_L", "Mouth_Press_R"],
134
+ 25: ["Jaw_Open"],
135
+ // Lips Part - small jaw open with morph
136
+ 26: ["Jaw_Open"],
137
+ // Jaw Drop - mixed: bone rotation + Jaw_Open morph
138
+ 27: ["Jaw_Open"],
139
+ // Mouth Stretch - larger jaw open with morph
140
+ 28: ["Mouth_Roll_In_Upper", "Mouth_Roll_In_Lower"],
141
+ 32: ["Mouth_Roll_In_Lower"],
142
+ // Lip Bite - using roll in lower as approximation
143
+ // Tongue
144
+ 19: ["Tongue_Out"],
145
+ 36: ["Tongue_Bulge_L", "Tongue_Bulge_R"],
146
+ 37: ["Tongue_Up"],
147
+ // Tongue Up - morph + bone rotation
148
+ 38: ["Tongue_Down"],
149
+ // Tongue Down - morph + bone rotation
150
+ 39: ["Tongue_L"],
151
+ // Tongue Left - morph + bone rotation
152
+ 40: ["Tongue_R"],
153
+ // Tongue Right - morph + bone rotation
154
+ 41: [],
155
+ // Tongue Tilt Left - BONE ONLY
156
+ 42: [],
157
+ // Tongue Tilt Right - BONE ONLY
158
+ // Extended tongue morphs (CC4-specific)
159
+ 73: ["Tongue_Narrow"],
160
+ 74: ["Tongue_Wide"],
161
+ 75: ["Tongue_Roll"],
162
+ 76: ["Tongue_Tip_Up"],
163
+ 77: ["Tongue_Tip_Down"],
164
+ // Jaw
165
+ 29: ["Jaw_Forward"],
166
+ 30: ["Jaw_L"],
167
+ // Jaw Left - mixed: bone rotation + Jaw_L morph
168
+ 31: [],
169
+ // Jaw Clencher (masseter/temporalis) - no dedicated CC4 morph
170
+ 35: ["Jaw_R"],
171
+ // Jaw Right - mixed: bone rotation + Jaw_R morph
172
+ // Head position (M51-M56 in FACS notation)
173
+ 51: ["Head_Turn_L"],
174
+ // Head turn left
175
+ 52: ["Head_Turn_R"],
176
+ // Head turn right
177
+ 53: ["Head_Turn_Up"],
178
+ // Head up
179
+ 54: ["Head_Turn_Down"],
180
+ 55: ["Head_Tilt_L"],
181
+ 56: ["Head_Tilt_R"],
182
+ // Eye Direction (convenience)
183
+ 61: ["Eye_L_Look_L", "Eye_R_Look_L"],
184
+ 62: ["Eye_L_Look_R", "Eye_R_Look_R"],
185
+ 63: ["Eye_L_Look_Up", "Eye_R_Look_Up"],
186
+ 64: ["Eye_L_Look_Down", "Eye_R_Look_Down"],
187
+ // Single-eye controls (Left eye)
188
+ 65: ["Eye_L_Look_L"],
189
+ 66: ["Eye_L_Look_R"],
190
+ 67: ["Eye_L_Look_Up"],
191
+ 68: ["Eye_L_Look_Down"],
192
+ // Single-eye controls (Right eye)
193
+ 69: ["Eye_R_Look_L"],
194
+ 70: ["Eye_R_Look_R"],
195
+ 71: ["Eye_R_Look_Up"],
196
+ 72: ["Eye_R_Look_Down"],
197
+ // Eye morphs (CC_Base_Eye meshes)
198
+ // EO morphs control the shadow/depth around the eyes
199
+ 80: ["EO Bulge L", "EO Bulge R"],
200
+ 81: ["EO Depth L", "EO Depth R"],
201
+ 82: ["EO Inner Depth L", "EO Inner Depth R"],
202
+ 83: ["EO Inner Height L", "EO Inner Height R"],
203
+ 84: ["EO Inner Width L", "EO Inner Width R"],
204
+ 85: ["EO Outer Depth L", "EO Outer Depth R"],
205
+ 86: ["EO Outer Height L", "EO Outer Height R"],
206
+ 87: ["EO Outer Width L", "EO Outer Width R"],
207
+ 88: ["EO Upper Depth L", "EO Upper Depth R"],
208
+ 89: ["EO Lower Depth L", "EO Lower Depth R"],
209
+ 90: ["EO Center Upper Depth L", "EO Center Upper Depth R"],
210
+ 91: ["EO Center Upper Height L", "EO Center Upper Height R"],
211
+ 92: ["EO Center Lower Depth L", "EO Center Lower Depth R"],
212
+ 93: ["EO Center Lower Height L", "EO Center Lower Height R"],
213
+ 94: ["EO Inner Upper Depth L", "EO Inner Upper Depth R"],
214
+ 95: ["EO Inner Upper Height L", "EO Inner Upper Height R"],
215
+ 96: ["EO Inner Lower Depth L", "EO Inner Lower Depth R"],
216
+ 97: ["EO Inner Lower Height L", "EO Inner Lower Height R"],
217
+ 98: ["EO Outer Upper Depth L", "EO Outer Upper Depth R"],
218
+ 99: ["EO Outer Upper Height L", "EO Outer Upper Height R"],
219
+ 100: ["EO Outer Lower Depth L", "EO Outer Lower Depth R"],
220
+ 101: ["EO Outer Lower Height L", "EO Outer Lower Height R"],
221
+ 102: ["EO Duct Depth L", "EO Duct Depth R"]
222
+ };
223
+ var VISEME_KEYS = [
224
+ "EE",
225
+ "Er",
226
+ "IH",
227
+ "Ah",
228
+ "Oh",
229
+ "W_OO",
230
+ "S_Z",
231
+ "Ch_J",
232
+ "F_V",
233
+ "TH",
234
+ "T_L_D_N",
235
+ "B_M_P",
236
+ "K_G_H_NG",
237
+ "AE",
238
+ "R"
239
+ ];
240
+ var BONE_AU_TO_BINDINGS = {
241
+ // Head turn and tilt (M51-M56) - use HEAD bone only (NECK should not rotate with head)
242
+ // Three.js Y rotation: positive = counter-clockwise from above = head turns LEFT (character POV)
243
+ 51: [
244
+ { node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30 }
245
+ // Head turn left
246
+ ],
247
+ 52: [
248
+ { node: "HEAD", channel: "ry", scale: -1, maxDegrees: 30 }
249
+ // Head turn right
250
+ ],
251
+ 53: [
252
+ { node: "HEAD", channel: "rx", scale: -1, maxDegrees: 20 }
253
+ // Head up
254
+ ],
255
+ 54: [
256
+ { node: "HEAD", channel: "rx", scale: 1, maxDegrees: 20 }
257
+ // Head down
258
+ ],
259
+ 55: [
260
+ { node: "HEAD", channel: "rz", scale: -1, maxDegrees: 15 }
261
+ // Head tilt left
262
+ ],
263
+ 56: [
264
+ { node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15 }
265
+ // Head tilt right
266
+ ],
267
+ // Eyes horizontal (yaw) - CC4 rigs use rz for horizontal eye rotation
268
+ 61: [
269
+ { node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32 },
270
+ // Eyes look left
271
+ { node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 32 }
272
+ ],
273
+ 62: [
274
+ { node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 32 },
275
+ // Eyes look right
276
+ { node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 32 }
277
+ ],
278
+ 63: [
279
+ { node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 32 },
280
+ { node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 32 }
281
+ ],
282
+ 64: [
283
+ { node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 32 },
284
+ { node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 32 }
285
+ ],
286
+ // Single-eye (Left) — horizontal (rz for CC4) and vertical (rx)
287
+ 65: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 15 }],
288
+ 66: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 15 }],
289
+ 67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 12 }],
290
+ 68: [{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 12 }],
291
+ // Single-eye (Right) — horizontal (rz for CC4) and vertical (rx)
292
+ 69: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 15 }],
293
+ 70: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 15 }],
294
+ 71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 12 }],
295
+ 72: [{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 12 }],
296
+ // Jaw / Mouth
297
+ 8: [
298
+ // Lips Toward Each Other - slight jaw open helps sell the lip press
299
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 8 }
300
+ // Small downward rotation (jaw opening slightly)
301
+ ],
302
+ 25: [
303
+ // Lips Part — small jaw open
304
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84 }
305
+ // 73% of 8
306
+ ],
307
+ 26: [
308
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 28 }
309
+ // 73% of 20
310
+ ],
311
+ 27: [
312
+ // Mouth Stretch — larger jaw open
313
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 32 }
314
+ // 73% of 25
315
+ ],
316
+ 29: [
317
+ { node: "JAW", channel: "tz", scale: -1, maxUnits: 0.02 }
318
+ // Negative for forward thrust
319
+ ],
320
+ 30: [
321
+ // Jaw Left
322
+ { node: "JAW", channel: "ry", scale: -1, maxDegrees: 5 }
323
+ ],
324
+ 35: [
325
+ // Jaw Right
326
+ { node: "JAW", channel: "ry", scale: 1, maxDegrees: 5 }
327
+ ],
328
+ // Tongue
329
+ 19: [
330
+ { node: "TONGUE", channel: "tz", scale: -1, maxUnits: 8e-3 }
331
+ ],
332
+ 37: [
333
+ // Tongue Up
334
+ { node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 45 }
335
+ ],
336
+ 38: [
337
+ // Tongue Down
338
+ { node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 45 }
339
+ ],
340
+ 39: [
341
+ // Tongue Left
342
+ { node: "TONGUE", channel: "ry", scale: -1, maxDegrees: 10 }
343
+ ],
344
+ 40: [
345
+ // Tongue Right
346
+ { node: "TONGUE", channel: "ry", scale: 1, maxDegrees: 10 }
347
+ ],
348
+ 41: [
349
+ // Tongue Tilt Left
350
+ { node: "TONGUE", channel: "rx", scale: -1, maxDegrees: 20 }
351
+ ],
352
+ 42: [
353
+ // Tongue Tilt Right
354
+ { node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20 }
355
+ ]
356
+ };
357
+ var CC4_BONE_NODES = {
358
+ EYE_L: "CC_Base_L_Eye",
359
+ EYE_R: "CC_Base_R_Eye",
360
+ HEAD: "CC_Base_Head",
361
+ NECK: "CC_Base_NeckTwist01",
362
+ NECK_TWIST: "CC_Base_NeckTwist02",
363
+ JAW: "CC_Base_JawRoot",
364
+ TONGUE: "CC_Base_Tongue01"
365
+ };
366
+ var CC4_EYE_MESH_NODES = {
367
+ LEFT: "CC_Base_Eye",
368
+ RIGHT: "CC_Base_Eye_1"
369
+ };
370
+ var AU_INFO = {
371
+ // Forehead / Brow (Upper)
372
+ "1": { id: "1", name: "Inner Brow Raiser", muscularBasis: "frontalis (pars medialis)", links: ["https://en.wikipedia.org/wiki/Frontalis_muscle"], faceArea: "Upper", facePart: "Forehead" },
373
+ "2": { id: "2", name: "Outer Brow Raiser", muscularBasis: "frontalis (pars lateralis)", links: ["https://en.wikipedia.org/wiki/Frontalis_muscle"], faceArea: "Upper", facePart: "Forehead" },
374
+ "4": { id: "4", name: "Brow Lowerer", muscularBasis: "corrugator/depressor supercilii", links: ["https://en.wikipedia.org/wiki/Corrugator_supercilii"], faceArea: "Upper", facePart: "Forehead" },
375
+ // Eyelids / Eyes (Upper)
376
+ "5": { id: "5", name: "Upper Lid Raiser", muscularBasis: "levator palpebrae superioris", links: ["https://en.wikipedia.org/wiki/Levator_palpebrae_superioris"], faceArea: "Upper", facePart: "Eyelids" },
377
+ "6": { id: "6", name: "Cheek Raiser", muscularBasis: "orbicularis oculi (pars orbitalis)", links: ["https://en.wikipedia.org/wiki/Orbicularis_oculi"], faceArea: "Upper", facePart: "Cheeks" },
378
+ "7": { id: "7", name: "Lid Tightener", muscularBasis: "orbicularis oculi (pars palpebralis)", links: ["https://en.wikipedia.org/wiki/Orbicularis_oculi"], faceArea: "Upper", facePart: "Eyelids" },
379
+ "43": { id: "43", name: "Eyes Closed", muscularBasis: "orbicularis oculi", links: ["https://en.wikipedia.org/wiki/Orbicularis_oculi_muscle"], faceArea: "Upper", facePart: "Eyelids" },
380
+ "45": { id: "45", name: "Blink", muscularBasis: "orbicularis oculi", links: ["https://en.wikipedia.org/wiki/Orbicularis_oculi_muscle"], faceArea: "Upper", facePart: "Eyelids" },
381
+ "61": { id: "61", name: "Eyes Turn Left", faceArea: "Upper", facePart: "Eyes" },
382
+ "62": { id: "62", name: "Eyes Turn Right", faceArea: "Upper", facePart: "Eyes" },
383
+ "63": { id: "63", name: "Eyes Up", faceArea: "Upper", facePart: "Eyes" },
384
+ "64": { id: "64", name: "Eyes Down", faceArea: "Upper", facePart: "Eyes" },
385
+ "65": { id: "65", name: "Left Eye Look Left", faceArea: "Upper", facePart: "Eyes" },
386
+ "66": { id: "66", name: "Left Eye Look Right", faceArea: "Upper", facePart: "Eyes" },
387
+ "67": { id: "67", name: "Left Eye Look Up", faceArea: "Upper", facePart: "Eyes" },
388
+ "68": { id: "68", name: "Left Eye Look Down", faceArea: "Upper", facePart: "Eyes" },
389
+ "69": { id: "69", name: "Right Eye Look Left", faceArea: "Upper", facePart: "Eyes" },
390
+ "70": { id: "70", name: "Right Eye Look Right", faceArea: "Upper", facePart: "Eyes" },
391
+ "71": { id: "71", name: "Right Eye Look Up", faceArea: "Upper", facePart: "Eyes" },
392
+ "72": { id: "72", name: "Right Eye Look Down", faceArea: "Upper", facePart: "Eyes" },
393
+ // Nose / Cheeks
394
+ "9": { id: "9", name: "Nose Wrinkler", muscularBasis: "levator labii superioris alaeque nasi", links: ["https://en.wikipedia.org/wiki/Levator_labii_superioris_alaeque_nasi"], faceArea: "Upper", facePart: "Nose" },
395
+ "34": { id: "34", name: "Cheek Puff", faceArea: "Lower", facePart: "Cheeks" },
396
+ // Mouth (Lower)
397
+ "8": { id: "8", name: "Lips Toward Each Other", muscularBasis: "orbicularis oris", links: ["https://en.wikipedia.org/wiki/Orbicularis_oris"], faceArea: "Lower", facePart: "Mouth" },
398
+ "10": { id: "10", name: "Upper Lip Raiser", muscularBasis: "levator labii superioris", links: ["https://en.wikipedia.org/wiki/Levator_labii_superioris"], faceArea: "Lower", facePart: "Mouth" },
399
+ "11": { id: "11", name: "Nasolabial Deepener", muscularBasis: "zygomaticus minor", links: ["https://en.wikipedia.org/wiki/Zygomaticus_minor"], faceArea: "Lower", facePart: "Cheeks" },
400
+ "12": { id: "12", name: "Lip Corner Puller", muscularBasis: "zygomaticus major", links: ["https://en.wikipedia.org/wiki/Zygomaticus_major"], faceArea: "Lower", facePart: "Mouth" },
401
+ "13": { id: "13", name: "Sharp Lip Puller", muscularBasis: "levator anguli oris", links: ["https://en.wikipedia.org/wiki/Levator_anguli_oris"], faceArea: "Lower", facePart: "Mouth" },
402
+ "14": { id: "14", name: "Dimpler", muscularBasis: "buccinator", links: ["https://en.wikipedia.org/wiki/Buccinator"], faceArea: "Lower", facePart: "Cheeks" },
403
+ "15": { id: "15", name: "Lip Corner Depressor", muscularBasis: "depressor anguli oris", links: ["https://en.wikipedia.org/wiki/Depressor_anguli_oris"], faceArea: "Lower", facePart: "Mouth" },
404
+ "16": { id: "16", name: "Lower Lip Depressor", muscularBasis: "depressor labii inferioris", links: ["https://en.wikipedia.org/wiki/Depressor_labii_inferioris"], faceArea: "Lower", facePart: "Mouth" },
405
+ "17": { id: "17", name: "Chin Raiser", muscularBasis: "mentalis", links: ["https://en.wikipedia.org/wiki/Mentalis"], faceArea: "Lower", facePart: "Chin" },
406
+ "18": { id: "18", name: "Lip Pucker", faceArea: "Lower", facePart: "Mouth" },
407
+ "20": { id: "20", name: "Lip Stretcher", muscularBasis: "risorius + platysma", links: ["https://en.wikipedia.org/wiki/Risorius", "https://en.wikipedia.org/wiki/Platysma"], faceArea: "Lower", facePart: "Mouth" },
408
+ "22": { id: "22", name: "Lip Funneler", muscularBasis: "orbicularis oris", links: ["https://en.wikipedia.org/wiki/Orbicularis_oris"], faceArea: "Lower", facePart: "Mouth" },
409
+ "23": { id: "23", name: "Lip Tightener", muscularBasis: "orbicularis oris", faceArea: "Lower", facePart: "Mouth" },
410
+ "24": { id: "24", name: "Lip Presser", muscularBasis: "orbicularis oris", faceArea: "Lower", facePart: "Mouth" },
411
+ "25": { id: "25", name: "Lips Part", faceArea: "Lower", facePart: "Mouth" },
412
+ "27": { id: "27", name: "Mouth Stretch", muscularBasis: "pterygoids + digastric", links: ["https://en.wikipedia.org/wiki/Pterygoid_bone", "https://en.wikipedia.org/wiki/Digastric_muscle"], faceArea: "Lower", facePart: "Mouth" },
413
+ "28": { id: "28", name: "Lip Suck", muscularBasis: "orbicularis oris", faceArea: "Lower", facePart: "Mouth" },
414
+ // Tongue (Lower)
415
+ "19": { id: "19", name: "Tongue Show", faceArea: "Lower", facePart: "Tongue" },
416
+ "36": { id: "36", name: "Tongue Bulge", faceArea: "Lower", facePart: "Tongue" },
417
+ "37": { id: "37", name: "Tongue Up", faceArea: "Lower", facePart: "Tongue" },
418
+ "38": { id: "38", name: "Tongue Down", faceArea: "Lower", facePart: "Tongue" },
419
+ "39": { id: "39", name: "Tongue Left", faceArea: "Lower", facePart: "Tongue" },
420
+ "40": { id: "40", name: "Tongue Right", faceArea: "Lower", facePart: "Tongue" },
421
+ "41": { id: "41", name: "Tongue Tilt Left", faceArea: "Lower", facePart: "Tongue" },
422
+ "42": { id: "42", name: "Tongue Tilt Right", faceArea: "Lower", facePart: "Tongue" },
423
+ // Extended tongue controls (CC4-specific morphs)
424
+ "73": { id: "73", name: "Tongue Narrow", faceArea: "Lower", facePart: "Tongue" },
425
+ "74": { id: "74", name: "Tongue Wide", faceArea: "Lower", facePart: "Tongue" },
426
+ "75": { id: "75", name: "Tongue Roll", faceArea: "Lower", facePart: "Tongue" },
427
+ "76": { id: "76", name: "Tongue Tip Up", faceArea: "Lower", facePart: "Tongue" },
428
+ "77": { id: "77", name: "Tongue Tip Down", faceArea: "Lower", facePart: "Tongue" },
429
+ // Jaw (Lower)
430
+ "26": { id: "26", name: "Jaw Drop", muscularBasis: "masseter (relax temporalis)", links: ["https://en.wikipedia.org/wiki/Masseter_muscle"], faceArea: "Lower", facePart: "Jaw" },
431
+ "29": { id: "29", name: "Jaw Thrust", faceArea: "Lower", facePart: "Jaw" },
432
+ "30": { id: "30", name: "Jaw Left", faceArea: "Lower", facePart: "Jaw" },
433
+ "31": { id: "31", name: "Jaw Clencher", muscularBasis: "masseter + temporalis", faceArea: "Lower", facePart: "Jaw" },
434
+ "32": { id: "32", name: "Lip Bite", muscularBasis: "orbicularis oris", faceArea: "Lower", facePart: "Mouth" },
435
+ "35": { id: "35", name: "Jaw Right", faceArea: "Lower", facePart: "Jaw" },
436
+ // Head position (M51-M56 in FACS notation)
437
+ "51": { id: "51", name: "Head Turn Left", faceArea: "Upper", facePart: "Head" },
438
+ "52": { id: "52", name: "Head Turn Right", faceArea: "Upper", facePart: "Head" },
439
+ "53": { id: "53", name: "Head Up", faceArea: "Upper", facePart: "Head" },
440
+ "54": { id: "54", name: "Head Down", faceArea: "Upper", facePart: "Head" },
441
+ "55": { id: "55", name: "Head Tilt Left", faceArea: "Upper", facePart: "Head" },
442
+ "56": { id: "56", name: "Head Tilt Right", faceArea: "Upper", facePart: "Head" },
443
+ // Eye (Upper) - EO morphs applied to CC_Base_Eye meshes
444
+ "80": { id: "80", name: "Eye Bulge", faceArea: "Upper", facePart: "Eye" },
445
+ "81": { id: "81", name: "Eye Depth", faceArea: "Upper", facePart: "Eye" },
446
+ "82": { id: "82", name: "Eye Inner Depth", faceArea: "Upper", facePart: "Eye" },
447
+ "83": { id: "83", name: "Eye Inner Height", faceArea: "Upper", facePart: "Eye" },
448
+ "84": { id: "84", name: "Eye Inner Width", faceArea: "Upper", facePart: "Eye" },
449
+ "85": { id: "85", name: "Eye Outer Depth", faceArea: "Upper", facePart: "Eye" },
450
+ "86": { id: "86", name: "Eye Outer Height", faceArea: "Upper", facePart: "Eye" },
451
+ "87": { id: "87", name: "Eye Outer Width", faceArea: "Upper", facePart: "Eye" },
452
+ "88": { id: "88", name: "Eye Upper Depth", faceArea: "Upper", facePart: "Eye" },
453
+ "89": { id: "89", name: "Eye Lower Depth", faceArea: "Upper", facePart: "Eye" },
454
+ "90": { id: "90", name: "Eye Center Upper Depth", faceArea: "Upper", facePart: "Eye" },
455
+ "91": { id: "91", name: "Eye Center Upper Height", faceArea: "Upper", facePart: "Eye" },
456
+ "92": { id: "92", name: "Eye Center Lower Depth", faceArea: "Upper", facePart: "Eye" },
457
+ "93": { id: "93", name: "Eye Center Lower Height", faceArea: "Upper", facePart: "Eye" },
458
+ "94": { id: "94", name: "Eye Inner Upper Depth", faceArea: "Upper", facePart: "Eye" },
459
+ "95": { id: "95", name: "Eye Inner Upper Height", faceArea: "Upper", facePart: "Eye" },
460
+ "96": { id: "96", name: "Eye Inner Lower Depth", faceArea: "Upper", facePart: "Eye" },
461
+ "97": { id: "97", name: "Eye Inner Lower Height", faceArea: "Upper", facePart: "Eye" },
462
+ "98": { id: "98", name: "Eye Outer Upper Depth", faceArea: "Upper", facePart: "Eye" },
463
+ "99": { id: "99", name: "Eye Outer Upper Height", faceArea: "Upper", facePart: "Eye" },
464
+ "100": { id: "100", name: "Eye Outer Lower Depth", faceArea: "Upper", facePart: "Eye" },
465
+ "101": { id: "101", name: "Eye Outer Lower Height", faceArea: "Upper", facePart: "Eye" },
466
+ "102": { id: "102", name: "Eye Duct Depth", faceArea: "Upper", facePart: "Eye" }
467
+ };
468
+ var AU_MIX_DEFAULTS = {
469
+ 31: 0.7,
470
+ 32: 0.7,
471
+ 33: 0.7,
472
+ 54: 0.7,
473
+ 55: 0.7,
474
+ 56: 0.7,
475
+ // head
476
+ 61: 0.5,
477
+ 62: 0.5,
478
+ 63: 0.5,
479
+ 64: 0.5,
480
+ // eyes
481
+ 25: 0.5,
482
+ 26: 0.5,
483
+ 27: 0.5,
484
+ // jaw open (lips part, jaw drop, mouth stretch)
485
+ 30: 0.5,
486
+ 35: 0.5
487
+ // jaw left/right
488
+ };
489
+ var MORPH_TO_MESH = {
490
+ // Face/AU morphs affect the main face mesh and both eyebrow meshes.
491
+ face: ["CC_Base_Body_1", "Male_Bushy_1", "Male_Bushy_2"],
492
+ viseme: ["CC_Base_Body_1"],
493
+ eye: ["CC_Base_EyeOcclusion_1", "CC_Base_EyeOcclusion_2"],
494
+ tearLine: ["CC_Base_TearLine_1", "CC_Base_TearLine_2"],
495
+ tongue: ["CC_Base_Tongue"],
496
+ hair: ["Side_part_wavy_1", "Side_part_wavy_2"]
497
+ };
498
+ var CC4_PRESET = {
499
+ auToMorphs: AU_TO_MORPHS,
500
+ auToBones: BONE_AU_TO_BINDINGS,
501
+ boneNodes: CC4_BONE_NODES,
502
+ morphToMesh: MORPH_TO_MESH,
503
+ visemeKeys: VISEME_KEYS,
504
+ auMixDefaults: AU_MIX_DEFAULTS,
505
+ auInfo: AU_INFO,
506
+ eyeMeshNodes: CC4_EYE_MESH_NODES
507
+ };
508
+
509
+ // src/engines/three/LoomLargeThree.ts
510
+ var deg2rad = (d) => d * Math.PI / 180;
511
+ function clamp01(x) {
512
+ return x < 0 ? 0 : x > 1 ? 1 : x;
513
+ }
514
+ var _LoomLargeThree = class _LoomLargeThree {
515
+ constructor(config = {}, animation) {
516
+ // Configuration
517
+ __publicField(this, "config");
518
+ // Animation system (injectable)
519
+ __publicField(this, "animation");
520
+ // State
521
+ __publicField(this, "auValues", {});
522
+ __publicField(this, "rigReady", false);
523
+ __publicField(this, "missingBoneWarnings", /* @__PURE__ */ new Set());
524
+ // Rotation state
525
+ __publicField(this, "rotations", {});
526
+ __publicField(this, "pendingCompositeNodes", /* @__PURE__ */ new Set());
527
+ __publicField(this, "isPaused", false);
528
+ __publicField(this, "translations", {});
529
+ // Mesh references
530
+ __publicField(this, "faceMesh", null);
531
+ __publicField(this, "meshes", []);
532
+ __publicField(this, "model", null);
533
+ __publicField(this, "meshByName", /* @__PURE__ */ new Map());
534
+ __publicField(this, "morphCache", /* @__PURE__ */ new Map());
535
+ // Bones
536
+ __publicField(this, "bones", {});
537
+ __publicField(this, "mixWeights", {});
538
+ // Viseme state
539
+ __publicField(this, "visemeValues", new Array(15).fill(0));
540
+ this.config = config.auMappings || CC4_PRESET;
541
+ this.mixWeights = { ...this.config.auMixDefaults };
542
+ this.animation = animation || new AnimationThree();
543
+ }
544
+ // ============================================================================
545
+ // PUBLIC API
546
+ // ============================================================================
547
+ onReady(payload) {
548
+ const { meshes, model } = payload;
549
+ this.meshes = meshes;
550
+ this.model = model;
551
+ this.meshByName.clear();
552
+ this.morphCache.clear();
553
+ model.traverse((obj) => {
554
+ if (obj.isMesh && obj.name) {
555
+ const infl = obj.morphTargetInfluences;
556
+ if (Array.isArray(infl) && infl.length > 0) {
557
+ this.meshByName.set(obj.name, obj);
558
+ }
559
+ }
560
+ });
561
+ const faceMeshNames = this.config.morphToMesh?.face || [];
562
+ const defaultFace = meshes.find((m) => faceMeshNames.includes(m.name));
563
+ if (defaultFace) {
564
+ this.faceMesh = defaultFace;
565
+ } else {
566
+ const candidate = meshes.find((m) => {
567
+ const dict = m.morphTargetDictionary;
568
+ return dict && typeof dict === "object" && "Brow_Drop_L" in dict;
569
+ });
570
+ this.faceMesh = candidate || null;
571
+ }
572
+ this.bones = this.resolveBones(model);
573
+ this.rigReady = true;
574
+ this.missingBoneWarnings.clear();
575
+ this.initBoneRotations();
576
+ }
577
+ update(deltaSeconds) {
578
+ const dtSeconds = Math.max(0, deltaSeconds || 0);
579
+ if (dtSeconds <= 0 || this.isPaused) return;
580
+ this.animation.tick(dtSeconds);
581
+ this.flushPendingComposites();
582
+ }
583
+ dispose() {
584
+ this.clearTransitions();
585
+ this.meshes = [];
586
+ this.model = null;
587
+ this.bones = {};
588
+ }
589
+ // ============================================================================
590
+ // AU CONTROL
591
+ // ============================================================================
592
+ setAU(id, v, balance) {
593
+ if (typeof id === "string") {
594
+ const match = id.match(/^(\d+)([LR])$/i);
595
+ if (match) {
596
+ const au = Number(match[1]);
597
+ const side = match[2].toUpperCase();
598
+ const sideBalance = side === "L" ? -1 : 1;
599
+ this.setAU(au, v, sideBalance);
600
+ return;
601
+ }
602
+ const n = Number(id);
603
+ if (!Number.isNaN(n)) {
604
+ this.setAU(n, v, balance);
605
+ }
606
+ return;
607
+ }
608
+ this.auValues[id] = v;
609
+ const keys = this.config.auToMorphs[id] || [];
610
+ if (keys.length) {
611
+ const mixWeight = this.isMixedAU(id) ? this.getAUMixWeight(id) : 1;
612
+ const base = clamp01(v) * mixWeight;
613
+ const meshNames = this.getMeshNamesForAU(id);
614
+ const leftKeys = keys.filter((k) => /(_L$| L$|Left$)/i.test(k));
615
+ const rightKeys = keys.filter((k) => /(_R$| R$|Right$)/i.test(k));
616
+ const centerKeys = keys.filter((k) => !/(_L$| L$|Left$|_R$| R$|Right$)/i.test(k));
617
+ const { left: leftVal, right: rightVal } = this.computeSideValues(base, balance);
618
+ if (leftKeys.length || rightKeys.length) {
619
+ for (const k of leftKeys) this.setMorph(k, leftVal, meshNames);
620
+ for (const k of rightKeys) this.setMorph(k, rightVal, meshNames);
621
+ } else {
622
+ centerKeys.push(...keys);
623
+ }
624
+ for (const k of centerKeys) {
625
+ this.setMorph(k, base, meshNames);
626
+ }
627
+ }
628
+ const bindings = this.config.auToBones[id];
629
+ if (bindings) {
630
+ for (const binding of bindings) {
631
+ if (binding.channel === "rx" || binding.channel === "ry" || binding.channel === "rz") {
632
+ const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
633
+ this.updateBoneRotation(binding.node, axis, v * binding.scale, binding.maxDegrees ?? 0);
634
+ } else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
635
+ if (binding.maxUnits !== void 0) {
636
+ this.updateBoneTranslation(binding.node, binding.channel, v * binding.scale, binding.maxUnits);
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }
642
+ transitionAU(id, to, durationMs = 200, balance) {
643
+ const numId = typeof id === "string" ? Number(id.replace(/[^\d]/g, "")) : id;
644
+ const target = clamp01(to);
645
+ const morphKeys = this.config.auToMorphs[numId] || [];
646
+ const bindings = this.config.auToBones[numId] || [];
647
+ const mixWeight = this.isMixedAU(numId) ? this.getAUMixWeight(numId) : 1;
648
+ const base = target * mixWeight;
649
+ const { left: leftVal, right: rightVal } = this.computeSideValues(base, balance);
650
+ this.auValues[numId] = target;
651
+ const handles = [];
652
+ const meshNames = this.getMeshNamesForAU(numId);
653
+ const leftKeys = morphKeys.filter((k) => /(_L$|Left$)/.test(k));
654
+ const rightKeys = morphKeys.filter((k) => /(_R$|Right$)/.test(k));
655
+ const centerKeys = morphKeys.filter((k) => !/(_L$|Left$|_R$|Right$)/.test(k));
656
+ if (leftKeys.length || rightKeys.length) {
657
+ for (const k of leftKeys) {
658
+ handles.push(this.transitionMorph(k, leftVal, durationMs, meshNames));
659
+ }
660
+ for (const k of rightKeys) {
661
+ handles.push(this.transitionMorph(k, rightVal, durationMs, meshNames));
662
+ }
663
+ } else {
664
+ centerKeys.push(...morphKeys);
665
+ }
666
+ for (const k of centerKeys) {
667
+ handles.push(this.transitionMorph(k, base, durationMs, meshNames));
668
+ }
669
+ for (const binding of bindings) {
670
+ if (binding.channel === "rx" || binding.channel === "ry" || binding.channel === "rz") {
671
+ const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
672
+ handles.push(this.transitionBoneRotation(binding.node, axis, target * binding.scale, binding.maxDegrees ?? 0, durationMs));
673
+ } else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
674
+ if (binding.maxUnits !== void 0) {
675
+ handles.push(this.transitionBoneTranslation(binding.node, binding.channel, target * binding.scale, binding.maxUnits, durationMs));
676
+ }
677
+ }
678
+ }
679
+ return this.combineHandles(handles);
680
+ }
681
+ getAU(id) {
682
+ return this.auValues[id] ?? 0;
683
+ }
684
+ // ============================================================================
685
+ // MORPH CONTROL
686
+ // ============================================================================
687
+ setMorph(key, v, meshNames) {
688
+ const val = clamp01(v);
689
+ const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
690
+ const cached = this.morphCache.get(key);
691
+ if (cached) {
692
+ for (const target of cached) {
693
+ target.infl[target.idx] = val;
694
+ }
695
+ return;
696
+ }
697
+ const targets = [];
698
+ if (targetMeshes.length) {
699
+ for (const name of targetMeshes) {
700
+ const mesh = this.meshByName.get(name);
701
+ if (!mesh) continue;
702
+ const dict = mesh.morphTargetDictionary;
703
+ const infl = mesh.morphTargetInfluences;
704
+ if (!dict || !infl) continue;
705
+ const idx = dict[key];
706
+ if (idx !== void 0) {
707
+ targets.push({ infl, idx });
708
+ infl[idx] = val;
709
+ }
710
+ }
711
+ } else {
712
+ for (const mesh of this.meshes) {
713
+ const dict = mesh.morphTargetDictionary;
714
+ const infl = mesh.morphTargetInfluences;
715
+ if (!dict || !infl) continue;
716
+ const idx = dict[key];
717
+ if (idx !== void 0) {
718
+ targets.push({ infl, idx });
719
+ infl[idx] = val;
720
+ }
721
+ }
722
+ }
723
+ if (targets.length > 0) {
724
+ this.morphCache.set(key, targets);
725
+ }
726
+ }
727
+ transitionMorph(key, to, durationMs = 120, meshNames) {
728
+ const transitionKey = `morph_${key}`;
729
+ const from = this.getMorphValue(key);
730
+ const target = clamp01(to);
731
+ return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.setMorph(key, value, meshNames));
732
+ }
733
+ // ============================================================================
734
+ // VISEME CONTROL
735
+ // ============================================================================
736
+ setViseme(visemeIndex, value, jawScale = 1) {
737
+ if (visemeIndex < 0 || visemeIndex >= this.config.visemeKeys.length) return;
738
+ const val = clamp01(value);
739
+ this.visemeValues[visemeIndex] = val;
740
+ const morphKey = this.config.visemeKeys[visemeIndex];
741
+ this.setMorph(morphKey, val);
742
+ const jawAmount = _LoomLargeThree.VISEME_JAW_AMOUNTS[visemeIndex] * val * jawScale;
743
+ if (Math.abs(jawScale) > 1e-6 && Math.abs(jawAmount) > 1e-6) {
744
+ this.updateBoneRotation("JAW", "roll", jawAmount, _LoomLargeThree.JAW_MAX_DEGREES);
745
+ }
746
+ }
747
+ transitionViseme(visemeIndex, to, durationMs = 80, jawScale = 1) {
748
+ if (visemeIndex < 0 || visemeIndex >= this.config.visemeKeys.length) {
749
+ return { promise: Promise.resolve(), pause: () => {
750
+ }, resume: () => {
751
+ }, cancel: () => {
752
+ } };
753
+ }
754
+ const morphKey = this.config.visemeKeys[visemeIndex];
755
+ const target = clamp01(to);
756
+ this.visemeValues[visemeIndex] = target;
757
+ const morphHandle = this.transitionMorph(morphKey, target, durationMs);
758
+ const jawAmount = _LoomLargeThree.VISEME_JAW_AMOUNTS[visemeIndex] * target * jawScale;
759
+ if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
760
+ return morphHandle;
761
+ }
762
+ const jawHandle = this.transitionBoneRotation("JAW", "roll", jawAmount, _LoomLargeThree.JAW_MAX_DEGREES, durationMs);
763
+ return this.combineHandles([morphHandle, jawHandle]);
764
+ }
765
+ // ============================================================================
766
+ // MIX WEIGHT CONTROL
767
+ // ============================================================================
768
+ setAUMixWeight(id, weight) {
769
+ this.mixWeights[id] = clamp01(weight);
770
+ const v = this.auValues[id] ?? 0;
771
+ if (v > 0) this.setAU(id, v);
772
+ const boneBindings = this.config.auToBones[id];
773
+ if (boneBindings) {
774
+ for (const binding of boneBindings) {
775
+ this.pendingCompositeNodes.add(binding.node);
776
+ }
777
+ }
778
+ }
779
+ getAUMixWeight(id) {
780
+ return this.mixWeights[id] ?? this.config.auMixDefaults?.[id] ?? 1;
781
+ }
782
+ // ============================================================================
783
+ // PLAYBACK CONTROL
784
+ // ============================================================================
785
+ pause() {
786
+ this.isPaused = true;
787
+ }
788
+ resume() {
789
+ this.isPaused = false;
790
+ }
791
+ getPaused() {
792
+ return this.isPaused;
793
+ }
794
+ clearTransitions() {
795
+ this.animation.clearTransitions();
796
+ }
797
+ getActiveTransitionCount() {
798
+ return this.animation.getActiveTransitionCount();
799
+ }
800
+ resetToNeutral() {
801
+ this.auValues = {};
802
+ this.initBoneRotations();
803
+ this.clearTransitions();
804
+ for (const m of this.meshes) {
805
+ const infl = m.morphTargetInfluences;
806
+ if (!infl) continue;
807
+ for (let i = 0; i < infl.length; i++) {
808
+ infl[i] = 0;
809
+ }
810
+ }
811
+ Object.values(this.bones).forEach((entry) => {
812
+ if (!entry) return;
813
+ entry.obj.position.copy(entry.basePos);
814
+ entry.obj.quaternion.copy(entry.baseQuat);
815
+ });
816
+ }
817
+ // ============================================================================
818
+ // MESH CONTROL
819
+ // ============================================================================
820
+ getMeshList() {
821
+ if (!this.model) return [];
822
+ const result = [];
823
+ this.model.traverse((obj) => {
824
+ if (obj.isMesh) {
825
+ result.push({
826
+ name: obj.name,
827
+ visible: obj.visible,
828
+ morphCount: obj.morphTargetInfluences?.length || 0
829
+ });
830
+ }
831
+ });
832
+ return result;
833
+ }
834
+ setMeshVisible(meshName, visible) {
835
+ if (!this.model) return;
836
+ this.model.traverse((obj) => {
837
+ if (obj.isMesh && obj.name === meshName) {
838
+ obj.visible = visible;
839
+ }
840
+ });
841
+ }
842
+ // ============================================================================
843
+ // CONFIGURATION
844
+ // ============================================================================
845
+ setAUMappings(mappings) {
846
+ this.config = mappings;
847
+ this.mixWeights = { ...mappings.auMixDefaults };
848
+ }
849
+ getAUMappings() {
850
+ return this.config;
851
+ }
852
+ // ============================================================================
853
+ // PRIVATE METHODS
854
+ // ============================================================================
855
+ computeSideValues(base, balance) {
856
+ const b = Math.max(-1, Math.min(1, balance ?? 0));
857
+ if (b === 0) return { left: base, right: base };
858
+ if (b < 0) return { left: base, right: base * (1 + b) };
859
+ return { left: base * (1 - b), right: base };
860
+ }
861
+ getMeshNamesForAU(auId) {
862
+ const info = this.config.auInfo?.[String(auId)];
863
+ if (!info?.facePart) return this.config.morphToMesh?.face || [];
864
+ switch (info.facePart) {
865
+ case "Tongue":
866
+ return this.config.morphToMesh?.tongue || [];
867
+ case "Eye":
868
+ return this.config.morphToMesh?.eye || [];
869
+ default:
870
+ return this.config.morphToMesh?.face || [];
871
+ }
872
+ }
873
+ getMorphValue(key) {
874
+ if (this.faceMesh) {
875
+ const dict = this.faceMesh.morphTargetDictionary;
876
+ const infl = this.faceMesh.morphTargetInfluences;
877
+ if (dict && infl) {
878
+ const idx = dict[key];
879
+ if (idx !== void 0) return infl[idx] ?? 0;
880
+ }
881
+ return 0;
882
+ }
883
+ for (const mesh of this.meshes) {
884
+ const dict = mesh.morphTargetDictionary;
885
+ const infl = mesh.morphTargetInfluences;
886
+ if (!dict || !infl) continue;
887
+ const idx = dict[key];
888
+ if (idx !== void 0) return infl[idx] ?? 0;
889
+ }
890
+ return 0;
891
+ }
892
+ isMixedAU(id) {
893
+ return !!(this.config.auToMorphs[id]?.length && this.config.auToBones[id]?.length);
894
+ }
895
+ initBoneRotations() {
896
+ const zeroAxis = { value: 0, maxRadians: 0 };
897
+ this.rotations = {};
898
+ this.pendingCompositeNodes.clear();
899
+ const allBoneKeys = Array.from(
900
+ new Set(Object.values(this.config.auToBones).flat().map((binding) => binding.node))
901
+ );
902
+ for (const node of allBoneKeys) {
903
+ this.rotations[node] = { pitch: { ...zeroAxis }, yaw: { ...zeroAxis }, roll: { ...zeroAxis } };
904
+ this.pendingCompositeNodes.add(node);
905
+ }
906
+ }
907
+ updateBoneRotation(nodeKey, axis, value, maxDegrees) {
908
+ if (!this.rotations[nodeKey]) return;
909
+ this.rotations[nodeKey][axis] = { value: Math.max(-1, Math.min(1, value)), maxRadians: deg2rad(maxDegrees) };
910
+ this.pendingCompositeNodes.add(nodeKey);
911
+ }
912
+ updateBoneTranslation(nodeKey, channel, value, maxUnits) {
913
+ if (!this.translations[nodeKey]) this.translations[nodeKey] = { x: 0, y: 0, z: 0 };
914
+ const clamped = Math.max(-1, Math.min(1, value));
915
+ const offset = clamped * maxUnits;
916
+ if (channel === "tx") this.translations[nodeKey].x = offset;
917
+ else if (channel === "ty") this.translations[nodeKey].y = offset;
918
+ else this.translations[nodeKey].z = offset;
919
+ this.pendingCompositeNodes.add(nodeKey);
920
+ }
921
+ transitionBoneRotation(nodeKey, axis, to, maxDegrees, durationMs = 200) {
922
+ const transitionKey = `bone_${nodeKey}_${axis}`;
923
+ const from = this.rotations[nodeKey]?.[axis]?.value ?? 0;
924
+ const target = Math.max(-1, Math.min(1, to));
925
+ return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.updateBoneRotation(nodeKey, axis, value, maxDegrees));
926
+ }
927
+ transitionBoneTranslation(nodeKey, channel, to, maxUnits, durationMs = 200) {
928
+ const transitionKey = `boneT_${nodeKey}_${channel}`;
929
+ const current = this.translations[nodeKey] || { x: 0, y: 0, z: 0 };
930
+ const currentOffset = channel === "tx" ? current.x : channel === "ty" ? current.y : current.z;
931
+ const from = maxUnits !== 0 ? Math.max(-1, Math.min(1, currentOffset / maxUnits)) : 0;
932
+ const target = Math.max(-1, Math.min(1, to));
933
+ return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.updateBoneTranslation(nodeKey, channel, value, maxUnits));
934
+ }
935
+ flushPendingComposites() {
936
+ if (this.pendingCompositeNodes.size === 0) return;
937
+ for (const nodeKey of this.pendingCompositeNodes) {
938
+ this.applyCompositeRotation(nodeKey);
939
+ }
940
+ this.pendingCompositeNodes.clear();
941
+ }
942
+ applyCompositeRotation(nodeKey) {
943
+ const entry = this.bones[nodeKey];
944
+ if (!entry || !this.model) {
945
+ if (!entry && this.rigReady && !this.missingBoneWarnings.has(nodeKey)) {
946
+ this.missingBoneWarnings.add(nodeKey);
947
+ }
948
+ return;
949
+ }
950
+ const { obj, basePos, baseEuler } = entry;
951
+ const rotState = this.rotations[nodeKey];
952
+ if (!rotState) return;
953
+ const yawRad = rotState.yaw.maxRadians * rotState.yaw.value;
954
+ const pitchRad = rotState.pitch.maxRadians * rotState.pitch.value;
955
+ const rollRad = rotState.roll.maxRadians * rotState.roll.value;
956
+ obj.position.copy(basePos);
957
+ const t = this.translations[nodeKey];
958
+ if (t) {
959
+ obj.position.x += t.x;
960
+ obj.position.y += t.y;
961
+ obj.position.z += t.z;
962
+ }
963
+ obj.rotation.set(baseEuler.x + pitchRad, baseEuler.y + yawRad, baseEuler.z + rollRad, baseEuler.order);
964
+ obj.updateMatrixWorld(false);
965
+ this.model.updateMatrixWorld(true);
966
+ }
967
+ resolveBones(root) {
968
+ const resolved = {};
969
+ const snapshot = (obj) => ({
970
+ obj,
971
+ basePos: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
972
+ baseQuat: obj.quaternion.clone(),
973
+ baseEuler: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, order: obj.rotation.order }
974
+ });
975
+ const findNode = (name) => {
976
+ if (!name) return void 0;
977
+ return root.getObjectByName(name);
978
+ };
979
+ for (const [key, nodeName] of Object.entries(this.config.boneNodes)) {
980
+ const node = findNode(nodeName);
981
+ if (node) resolved[key] = snapshot(node);
982
+ }
983
+ if (!resolved.EYE_L && this.config.eyeMeshNodes) {
984
+ const node = findNode(this.config.eyeMeshNodes.LEFT);
985
+ if (node) resolved.EYE_L = snapshot(node);
986
+ }
987
+ if (!resolved.EYE_R && this.config.eyeMeshNodes) {
988
+ const node = findNode(this.config.eyeMeshNodes.RIGHT);
989
+ if (node) resolved.EYE_R = snapshot(node);
990
+ }
991
+ return resolved;
992
+ }
993
+ combineHandles(handles) {
994
+ if (handles.length === 0) return { promise: Promise.resolve(), pause: () => {
995
+ }, resume: () => {
996
+ }, cancel: () => {
997
+ } };
998
+ if (handles.length === 1) return handles[0];
999
+ return {
1000
+ promise: Promise.all(handles.map((h) => h.promise)).then(() => {
1001
+ }),
1002
+ pause: () => handles.forEach((h) => h.pause()),
1003
+ resume: () => handles.forEach((h) => h.resume()),
1004
+ cancel: () => handles.forEach((h) => h.cancel())
1005
+ };
1006
+ }
1007
+ };
1008
+ // Viseme jaw amounts
1009
+ __publicField(_LoomLargeThree, "VISEME_JAW_AMOUNTS", [
1010
+ 0.15,
1011
+ 0.35,
1012
+ 0.25,
1013
+ 0.7,
1014
+ 0.55,
1015
+ 0.3,
1016
+ 0.1,
1017
+ 0.2,
1018
+ 0.08,
1019
+ 0.12,
1020
+ 0.18,
1021
+ 0.02,
1022
+ 0.25,
1023
+ 0.6,
1024
+ 0.4
1025
+ ]);
1026
+ __publicField(_LoomLargeThree, "JAW_MAX_DEGREES", 28);
1027
+ var LoomLargeThree = _LoomLargeThree;
1028
+ function collectMorphMeshes(root) {
1029
+ const meshes = [];
1030
+ root.traverse((obj) => {
1031
+ if (obj.isMesh) {
1032
+ if (Array.isArray(obj.morphTargetInfluences) && obj.morphTargetInfluences.length > 0) {
1033
+ meshes.push(obj);
1034
+ }
1035
+ }
1036
+ });
1037
+ return meshes;
1038
+ }
1039
+
1040
+ // src/physics/HairPhysics.ts
1041
+ var DEFAULT_HAIR_PHYSICS_CONFIG = {
1042
+ mass: 1,
1043
+ stiffness: 15,
1044
+ damping: 0.8,
1045
+ gravity: 9.8,
1046
+ headInfluence: 0.5,
1047
+ windEnabled: false,
1048
+ windStrength: 0,
1049
+ windDirectionX: 1,
1050
+ windDirectionZ: 0,
1051
+ windTurbulence: 0.2,
1052
+ windFrequency: 0.5
1053
+ };
1054
+ var HairPhysics = class {
1055
+ constructor(config = {}) {
1056
+ __publicField(this, "config");
1057
+ __publicField(this, "state");
1058
+ __publicField(this, "time", 0);
1059
+ // Previous head state for velocity calculation
1060
+ __publicField(this, "prevHeadYaw", 0);
1061
+ __publicField(this, "prevHeadPitch", 0);
1062
+ this.config = { ...DEFAULT_HAIR_PHYSICS_CONFIG, ...config };
1063
+ this.state = { x: 0, z: 0, vx: 0, vz: 0 };
1064
+ }
1065
+ /**
1066
+ * Update physics simulation
1067
+ * @param dt Delta time in seconds
1068
+ * @param headState Current head orientation and velocity
1069
+ * @returns Morph values to apply to hair meshes
1070
+ */
1071
+ update(dt, headState) {
1072
+ if (dt <= 0 || dt > 0.1) {
1073
+ return this.computeMorphOutput();
1074
+ }
1075
+ this.time += dt;
1076
+ const { mass, stiffness, damping, gravity, headInfluence, windEnabled } = this.config;
1077
+ const headYawVel = headState.yawVelocity !== 0 ? headState.yawVelocity : (headState.yaw - this.prevHeadYaw) / dt;
1078
+ const headPitchVel = headState.pitchVelocity !== 0 ? headState.pitchVelocity : (headState.pitch - this.prevHeadPitch) / dt;
1079
+ this.prevHeadYaw = headState.yaw;
1080
+ this.prevHeadPitch = headState.pitch;
1081
+ const springFx = -stiffness * this.state.x;
1082
+ const springFz = -stiffness * this.state.z;
1083
+ const dampFx = -damping * this.state.vx;
1084
+ const dampFz = -damping * this.state.vz;
1085
+ const gravityFz = gravity * Math.sin(headState.pitch) * 0.1;
1086
+ const inertiaFx = -headYawVel * headInfluence * mass * 2;
1087
+ const inertiaFz = -headPitchVel * headInfluence * mass * 2;
1088
+ let windFx = 0;
1089
+ let windFz = 0;
1090
+ if (windEnabled && this.config.windStrength > 0) {
1091
+ const { windStrength, windDirectionX, windDirectionZ, windTurbulence, windFrequency } = this.config;
1092
+ const windPhase = this.time * windFrequency * Math.PI * 2;
1093
+ const windOscillation = Math.sin(windPhase);
1094
+ const turbulencePhase = this.time * windFrequency * 3.7;
1095
+ const turbulence = Math.sin(turbulencePhase) * windTurbulence;
1096
+ windFx = windStrength * windDirectionX * (0.5 + 0.5 * windOscillation) + turbulence * -windDirectionZ;
1097
+ windFz = windStrength * windDirectionZ * (0.5 + 0.5 * windOscillation) + turbulence * windDirectionX;
1098
+ }
1099
+ const totalFx = springFx + dampFx + inertiaFx + windFx;
1100
+ const totalFz = springFz + dampFz + gravityFz + inertiaFz + windFz;
1101
+ const ax = totalFx / mass;
1102
+ const az = totalFz / mass;
1103
+ this.state.vx += ax * dt;
1104
+ this.state.vz += az * dt;
1105
+ const maxVel = 10;
1106
+ this.state.vx = Math.max(-maxVel, Math.min(maxVel, this.state.vx));
1107
+ this.state.vz = Math.max(-maxVel, Math.min(maxVel, this.state.vz));
1108
+ this.state.x += this.state.vx * dt;
1109
+ this.state.z += this.state.vz * dt;
1110
+ this.state.x = Math.max(-1, Math.min(1, this.state.x));
1111
+ this.state.z = Math.max(-1, Math.min(1, this.state.z));
1112
+ return this.computeMorphOutput();
1113
+ }
1114
+ /**
1115
+ * Convert physics state to morph target values
1116
+ * Maps pendulum position to left/right/front morphs for both sides
1117
+ */
1118
+ computeMorphOutput() {
1119
+ const { x, z } = this.state;
1120
+ const L_Hair_Left = Math.max(0, -x);
1121
+ const L_Hair_Right = Math.max(0, x);
1122
+ const L_Hair_Front = Math.max(0, z);
1123
+ const R_Hair_Left = Math.max(0, -x);
1124
+ const R_Hair_Right = Math.max(0, x);
1125
+ const R_Hair_Front = Math.max(0, z);
1126
+ return {
1127
+ L_Hair_Left,
1128
+ L_Hair_Right,
1129
+ L_Hair_Front,
1130
+ R_Hair_Left,
1131
+ R_Hair_Right,
1132
+ R_Hair_Front
1133
+ };
1134
+ }
1135
+ /**
1136
+ * Get current physics state (for debugging/UI)
1137
+ */
1138
+ getState() {
1139
+ return { ...this.state };
1140
+ }
1141
+ /**
1142
+ * Update configuration
1143
+ */
1144
+ setConfig(config) {
1145
+ this.config = { ...this.config, ...config };
1146
+ }
1147
+ /**
1148
+ * Get current configuration
1149
+ */
1150
+ getConfig() {
1151
+ return { ...this.config };
1152
+ }
1153
+ /**
1154
+ * Reset physics state to rest position
1155
+ */
1156
+ reset() {
1157
+ this.state = { x: 0, z: 0, vx: 0, vz: 0 };
1158
+ this.time = 0;
1159
+ this.prevHeadYaw = 0;
1160
+ this.prevHeadPitch = 0;
1161
+ }
1162
+ };
1163
+
1164
+ export { AnimationThree, CC4_PRESET, DEFAULT_HAIR_PHYSICS_CONFIG, HairPhysics, LoomLargeThree, collectMorphMeshes, LoomLargeThree as default };
1165
+ //# sourceMappingURL=index.js.map
1166
+ //# sourceMappingURL=index.js.map