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