loomlarge 1.0.0 → 1.0.1

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var three = require('three');
6
+
5
7
  var __defProp = Object.defineProperty;
6
8
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
9
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -243,121 +245,224 @@ var VISEME_KEYS = [
243
245
  ];
244
246
  var BONE_AU_TO_BINDINGS = {
245
247
  // 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
248
  51: [
248
- { node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30 }
249
+ { node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30, axis: "yaw" }
249
250
  // Head turn left
250
251
  ],
251
252
  52: [
252
- { node: "HEAD", channel: "ry", scale: -1, maxDegrees: 30 }
253
+ { node: "HEAD", channel: "ry", scale: -1, maxDegrees: 30, axis: "yaw" }
253
254
  // Head turn right
254
255
  ],
255
256
  53: [
256
- { node: "HEAD", channel: "rx", scale: -1, maxDegrees: 20 }
257
+ { node: "HEAD", channel: "rx", scale: -1, maxDegrees: 20, axis: "pitch" }
257
258
  // Head up
258
259
  ],
259
260
  54: [
260
- { node: "HEAD", channel: "rx", scale: 1, maxDegrees: 20 }
261
+ { node: "HEAD", channel: "rx", scale: 1, maxDegrees: 20, axis: "pitch" }
261
262
  // Head down
262
263
  ],
263
264
  55: [
264
- { node: "HEAD", channel: "rz", scale: -1, maxDegrees: 15 }
265
+ { node: "HEAD", channel: "rz", scale: -1, maxDegrees: 15, axis: "roll" }
265
266
  // Head tilt left
266
267
  ],
267
268
  56: [
268
- { node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15 }
269
+ { node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15, axis: "roll" }
269
270
  // Head tilt right
270
271
  ],
271
272
  // Eyes horizontal (yaw) - CC4 rigs use rz for horizontal eye rotation
272
273
  61: [
273
- { node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32 },
274
+ { node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32, axis: "yaw" },
274
275
  // Eyes look left
275
- { node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 32 }
276
+ { node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 32, axis: "yaw" }
276
277
  ],
277
278
  62: [
278
- { node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 32 },
279
+ { node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 32, axis: "yaw" },
279
280
  // Eyes look right
280
- { node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 32 }
281
+ { node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 32, axis: "yaw" }
281
282
  ],
282
283
  63: [
283
- { node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 32 },
284
- { node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 32 }
284
+ { node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 32, axis: "pitch" },
285
+ // Eyes Up
286
+ { node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 32, axis: "pitch" }
285
287
  ],
286
288
  64: [
287
- { node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 32 },
288
- { node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 32 }
289
+ { node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 32, axis: "pitch" },
290
+ // Eyes Down
291
+ { node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 32, axis: "pitch" }
289
292
  ],
290
293
  // 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 }],
294
+ 65: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 15, axis: "yaw" }],
295
+ 66: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 15, axis: "yaw" }],
296
+ 67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 12, axis: "pitch" }],
297
+ // Left Eye Up
298
+ 68: [{ node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 12, axis: "pitch" }],
299
+ // Left Eye Down
295
300
  // 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 }],
301
+ 69: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 15, axis: "yaw" }],
302
+ 70: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 15, axis: "yaw" }],
303
+ 71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 12, axis: "pitch" }],
304
+ // Right Eye Up
305
+ 72: [{ node: "EYE_R", channel: "rx", scale: 1, maxDegrees: 12, axis: "pitch" }],
306
+ // Right Eye Down
300
307
  // Jaw / Mouth
301
308
  8: [
302
309
  // 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)
310
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 8, axis: "roll" }
305
311
  ],
306
312
  25: [
307
313
  // Lips Part — small jaw open
308
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84 }
309
- // 73% of 8
314
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84, axis: "roll" }
310
315
  ],
311
316
  26: [
312
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 28 }
313
- // 73% of 20
317
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 28, axis: "roll" }
314
318
  ],
315
319
  27: [
316
320
  // Mouth Stretch — larger jaw open
317
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 32 }
318
- // 73% of 25
321
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 32, axis: "roll" }
319
322
  ],
320
323
  29: [
321
324
  { node: "JAW", channel: "tz", scale: -1, maxUnits: 0.02 }
322
- // Negative for forward thrust
325
+ // Translation - no axis needed
323
326
  ],
324
327
  30: [
325
328
  // Jaw Left
326
- { node: "JAW", channel: "ry", scale: -1, maxDegrees: 5 }
329
+ { node: "JAW", channel: "ry", scale: -1, maxDegrees: 5, axis: "yaw" }
327
330
  ],
328
331
  35: [
329
332
  // Jaw Right
330
- { node: "JAW", channel: "ry", scale: 1, maxDegrees: 5 }
333
+ { node: "JAW", channel: "ry", scale: 1, maxDegrees: 5, axis: "yaw" }
331
334
  ],
332
335
  // Tongue
333
336
  19: [
334
337
  { node: "TONGUE", channel: "tz", scale: -1, maxUnits: 8e-3 }
338
+ // Translation - no axis needed
335
339
  ],
336
340
  37: [
337
341
  // Tongue Up
338
- { node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 45 }
342
+ { node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 45, axis: "pitch" }
339
343
  ],
340
344
  38: [
341
345
  // Tongue Down
342
- { node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 45 }
346
+ { node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 45, axis: "pitch" }
343
347
  ],
344
348
  39: [
345
349
  // Tongue Left
346
- { node: "TONGUE", channel: "ry", scale: -1, maxDegrees: 10 }
350
+ { node: "TONGUE", channel: "ry", scale: -1, maxDegrees: 10, axis: "yaw" }
347
351
  ],
348
352
  40: [
349
353
  // Tongue Right
350
- { node: "TONGUE", channel: "ry", scale: 1, maxDegrees: 10 }
354
+ { node: "TONGUE", channel: "ry", scale: 1, maxDegrees: 10, axis: "yaw" }
351
355
  ],
352
356
  41: [
353
357
  // Tongue Tilt Left
354
- { node: "TONGUE", channel: "rx", scale: -1, maxDegrees: 20 }
358
+ { node: "TONGUE", channel: "rx", scale: -1, maxDegrees: 20, axis: "roll" }
355
359
  ],
356
360
  42: [
357
361
  // Tongue Tilt Right
358
- { node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20 }
362
+ { node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20, axis: "roll" }
359
363
  ]
360
364
  };
365
+ var isMixedAU = (id) => !!(AU_TO_MORPHS[id]?.length && BONE_AU_TO_BINDINGS[id]?.length);
366
+ var hasLeftRightMorphs = (auId) => {
367
+ const keys = AU_TO_MORPHS[auId] || [];
368
+ return keys.some((k) => /_L$|_R$| L$| R$|Left$|Right$/i.test(k));
369
+ };
370
+ var COMPOSITE_ROTATIONS = [
371
+ {
372
+ node: "JAW",
373
+ pitch: { aus: [25, 26, 27], axis: "rz" },
374
+ // Jaw drop (opens mouth downward)
375
+ yaw: { aus: [30, 35], axis: "ry", negative: 30, positive: 35 },
376
+ // Jaw lateral (left/right)
377
+ roll: null
378
+ // Jaw doesn't have roll
379
+ },
380
+ {
381
+ node: "HEAD",
382
+ pitch: { aus: [54, 53], axis: "rx", negative: 54, positive: 53 },
383
+ // Head down/up
384
+ yaw: { aus: [51, 52], axis: "ry", negative: 51, positive: 52 },
385
+ // Head turn left/right
386
+ roll: { aus: [55, 56], axis: "rz", negative: 55, positive: 56 }
387
+ // Head tilt left/right
388
+ },
389
+ {
390
+ node: "EYE_L",
391
+ pitch: { aus: [64, 63], axis: "rx", negative: 64, positive: 63 },
392
+ // Eyes down/up
393
+ yaw: { aus: [61, 62], axis: "rz", negative: 61, positive: 62 },
394
+ // Eyes left/right (rz for CC4)
395
+ roll: null
396
+ // Eyes don't have roll
397
+ },
398
+ {
399
+ node: "EYE_R",
400
+ pitch: { aus: [64, 63], axis: "rx", negative: 64, positive: 63 },
401
+ // Eyes down/up
402
+ yaw: { aus: [61, 62], axis: "rz", negative: 61, positive: 62 },
403
+ // Eyes left/right (rz for CC4)
404
+ roll: null
405
+ // Eyes don't have roll
406
+ },
407
+ {
408
+ node: "TONGUE",
409
+ pitch: { aus: [38, 37], axis: "rz", negative: 38, positive: 37 },
410
+ // Tongue down/up
411
+ yaw: { aus: [39, 40], axis: "ry", negative: 39, positive: 40 },
412
+ // Tongue left/right
413
+ roll: { aus: [41, 42], axis: "rx", negative: 41, positive: 42 }
414
+ // Tongue tilt left/right
415
+ }
416
+ ];
417
+ var CONTINUUM_PAIRS_MAP = {
418
+ // Eyes horizontal (yaw) - both eyes share same AUs
419
+ 61: { pairId: 62, isNegative: true, axis: "yaw", node: "EYE_L" },
420
+ 62: { pairId: 61, isNegative: false, axis: "yaw", node: "EYE_L" },
421
+ // Eyes vertical (pitch)
422
+ 64: { pairId: 63, isNegative: true, axis: "pitch", node: "EYE_L" },
423
+ 63: { pairId: 64, isNegative: false, axis: "pitch", node: "EYE_L" },
424
+ // Head yaw (turn left/right)
425
+ 51: { pairId: 52, isNegative: true, axis: "yaw", node: "HEAD" },
426
+ 52: { pairId: 51, isNegative: false, axis: "yaw", node: "HEAD" },
427
+ // Head pitch (up/down)
428
+ 54: { pairId: 53, isNegative: true, axis: "pitch", node: "HEAD" },
429
+ 53: { pairId: 54, isNegative: false, axis: "pitch", node: "HEAD" },
430
+ // Head roll (tilt left/right)
431
+ 55: { pairId: 56, isNegative: true, axis: "roll", node: "HEAD" },
432
+ 56: { pairId: 55, isNegative: false, axis: "roll", node: "HEAD" },
433
+ // Jaw yaw (left/right)
434
+ 30: { pairId: 35, isNegative: true, axis: "yaw", node: "JAW" },
435
+ 35: { pairId: 30, isNegative: false, axis: "yaw", node: "JAW" },
436
+ // Tongue yaw (left/right)
437
+ 39: { pairId: 40, isNegative: true, axis: "yaw", node: "TONGUE" },
438
+ 40: { pairId: 39, isNegative: false, axis: "yaw", node: "TONGUE" },
439
+ // Tongue pitch (up/down)
440
+ 38: { pairId: 37, isNegative: true, axis: "pitch", node: "TONGUE" },
441
+ 37: { pairId: 38, isNegative: false, axis: "pitch", node: "TONGUE" },
442
+ // Tongue roll (tilt left/right)
443
+ 41: { pairId: 42, isNegative: true, axis: "roll", node: "TONGUE" },
444
+ 42: { pairId: 41, isNegative: false, axis: "roll", node: "TONGUE" },
445
+ // Extended tongue morphs (continuum pairs)
446
+ 73: { pairId: 74, isNegative: true, axis: "yaw", node: "TONGUE" },
447
+ // Tongue Narrow/Wide
448
+ 74: { pairId: 73, isNegative: false, axis: "yaw", node: "TONGUE" },
449
+ 76: { pairId: 77, isNegative: false, axis: "pitch", node: "TONGUE" },
450
+ // Tongue Tip Up/Down
451
+ 77: { pairId: 76, isNegative: true, axis: "pitch", node: "TONGUE" }
452
+ };
453
+ var CONTINUUM_LABELS = {
454
+ "61-62": "Eyes \u2014 Horizontal",
455
+ "64-63": "Eyes \u2014 Vertical",
456
+ "51-52": "Head \u2014 Horizontal",
457
+ "54-53": "Head \u2014 Vertical",
458
+ "55-56": "Head \u2014 Tilt",
459
+ "30-35": "Jaw \u2014 Horizontal",
460
+ "38-37": "Tongue \u2014 Vertical",
461
+ "39-40": "Tongue \u2014 Horizontal",
462
+ "41-42": "Tongue \u2014 Tilt",
463
+ "73-74": "Tongue \u2014 Width",
464
+ "76-77": "Tongue Tip \u2014 Vertical"
465
+ };
361
466
  var CC4_BONE_NODES = {
362
467
  EYE_L: "CC_Base_L_Eye",
363
468
  EYE_R: "CC_Base_R_Eye",
@@ -490,6 +595,41 @@ var AU_MIX_DEFAULTS = {
490
595
  35: 0.5
491
596
  // jaw left/right
492
597
  };
598
+ var CC4_MESHES = {
599
+ // Body (6 meshes, 80 morphs each) - default render order 0
600
+ "CC_Base_Body_1": { category: "body", morphCount: 80 },
601
+ "CC_Base_Body_2": { category: "body", morphCount: 80 },
602
+ "CC_Base_Body_3": { category: "body", morphCount: 80 },
603
+ "CC_Base_Body_4": { category: "body", morphCount: 80 },
604
+ "CC_Base_Body_5": { category: "body", morphCount: 80 },
605
+ "CC_Base_Body_6": { category: "body", morphCount: 80 },
606
+ // Eyes (bone-driven, no morphs) - render first (behind everything)
607
+ "CC_Base_Eye": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
608
+ "CC_Base_Eye_1": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
609
+ "CC_Base_Eye_2": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
610
+ "CC_Base_Eye_3": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
611
+ "CC_Base_Eye_4": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
612
+ // Eye occlusion (94 morphs each) - render on top of eyes with transparency support
613
+ "CC_Base_EyeOcclusion_1": { category: "eyeOcclusion", morphCount: 94, material: { renderOrder: 2, transparent: true, opacity: 1, depthWrite: true, depthTest: true, blending: "Normal" } },
614
+ "CC_Base_EyeOcclusion_2": { category: "eyeOcclusion", morphCount: 94, material: { renderOrder: 2, transparent: true, opacity: 1, depthWrite: true, depthTest: true, blending: "Normal" } },
615
+ // Tear lines (90 morphs each) - on top of eyes/face
616
+ "CC_Base_TearLine_1": { category: "tearLine", morphCount: 90, material: { renderOrder: 2 } },
617
+ "CC_Base_TearLine_2": { category: "tearLine", morphCount: 90, material: { renderOrder: 2 } },
618
+ // Cornea (no morphs) - render first with eyes
619
+ "CC_Base_Cornea": { category: "cornea", morphCount: 0, material: { renderOrder: -10 } },
620
+ "CC_Base_Cornea_1": { category: "cornea", morphCount: 0, material: { renderOrder: -10 } },
621
+ // Teeth (no morphs, follow jaw bone) - default render order
622
+ "CC_Base_Teeth_1": { category: "teeth", morphCount: 0 },
623
+ "CC_Base_Teeth_2": { category: "teeth", morphCount: 0 },
624
+ // Tongue (23 morphs) - default render order
625
+ "CC_Base_Tongue": { category: "tongue", morphCount: 23 },
626
+ // Eyebrows (91 morphs each) - above face
627
+ "Male_Bushy_1": { category: "eyebrow", morphCount: 91, material: { renderOrder: 5 } },
628
+ "Male_Bushy_2": { category: "eyebrow", morphCount: 91, material: { renderOrder: 5 } },
629
+ // Hair (14 styling morphs each) - render last (on top of everything)
630
+ "Side_part_wavy_1": { category: "hair", morphCount: 14, material: { renderOrder: 10 } },
631
+ "Side_part_wavy_2": { category: "hair", morphCount: 14, material: { renderOrder: 10 } }
632
+ };
493
633
  var MORPH_TO_MESH = {
494
634
  // Face/AU morphs affect the main face mesh and both eyebrow meshes.
495
635
  face: ["CC_Base_Body_1", "Male_Bushy_1", "Male_Bushy_2"],
@@ -541,6 +681,10 @@ var _LoomLargeThree = class _LoomLargeThree {
541
681
  __publicField(this, "mixWeights", {});
542
682
  // Viseme state
543
683
  __publicField(this, "visemeValues", new Array(15).fill(0));
684
+ // Internal RAF loop
685
+ __publicField(this, "clock", new three.Clock());
686
+ __publicField(this, "rafId", null);
687
+ __publicField(this, "running", false);
544
688
  this.config = config.auMappings || CC4_PRESET;
545
689
  this.mixWeights = { ...this.config.auMixDefaults };
546
690
  this.animation = animation || new AnimationThree();
@@ -577,6 +721,7 @@ var _LoomLargeThree = class _LoomLargeThree {
577
721
  this.rigReady = true;
578
722
  this.missingBoneWarnings.clear();
579
723
  this.initBoneRotations();
724
+ this.applyMeshMaterialSettings(model);
580
725
  }
581
726
  update(deltaSeconds) {
582
727
  const dtSeconds = Math.max(0, deltaSeconds || 0);
@@ -584,7 +729,30 @@ var _LoomLargeThree = class _LoomLargeThree {
584
729
  this.animation.tick(dtSeconds);
585
730
  this.flushPendingComposites();
586
731
  }
732
+ /** Start the internal RAF loop */
733
+ start() {
734
+ if (this.running) return;
735
+ this.running = true;
736
+ this.clock.start();
737
+ const tick = () => {
738
+ if (!this.running) return;
739
+ const dt = this.clock.getDelta();
740
+ this.update(dt);
741
+ this.rafId = requestAnimationFrame(tick);
742
+ };
743
+ this.rafId = requestAnimationFrame(tick);
744
+ }
745
+ /** Stop the internal RAF loop */
746
+ stop() {
747
+ this.running = false;
748
+ if (this.rafId !== null) {
749
+ cancelAnimationFrame(this.rafId);
750
+ this.rafId = null;
751
+ }
752
+ this.clock.stop();
753
+ }
587
754
  dispose() {
755
+ this.stop();
588
756
  this.clearTransitions();
589
757
  this.meshes = [];
590
758
  this.model = null;
@@ -633,8 +801,9 @@ var _LoomLargeThree = class _LoomLargeThree {
633
801
  if (bindings) {
634
802
  for (const binding of bindings) {
635
803
  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);
804
+ if (binding.axis) {
805
+ this.updateBoneRotation(binding.node, binding.axis, v * binding.scale, binding.maxDegrees ?? 0);
806
+ }
638
807
  } else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
639
808
  if (binding.maxUnits !== void 0) {
640
809
  this.updateBoneTranslation(binding.node, binding.channel, v * binding.scale, binding.maxUnits);
@@ -672,8 +841,9 @@ var _LoomLargeThree = class _LoomLargeThree {
672
841
  }
673
842
  for (const binding of bindings) {
674
843
  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));
844
+ if (binding.axis) {
845
+ handles.push(this.transitionBoneRotation(binding.node, binding.axis, target * binding.scale, binding.maxDegrees ?? 0, durationMs));
846
+ }
677
847
  } else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
678
848
  if (binding.maxUnits !== void 0) {
679
849
  handles.push(this.transitionBoneTranslation(binding.node, binding.channel, target * binding.scale, binding.maxUnits, durationMs));
@@ -685,11 +855,16 @@ var _LoomLargeThree = class _LoomLargeThree {
685
855
  getAU(id) {
686
856
  return this.auValues[id] ?? 0;
687
857
  }
688
- // ============================================================================
689
- // MORPH CONTROL
690
- // ============================================================================
691
- setMorph(key, v, meshNames) {
858
+ setMorph(key, v, meshNamesOrTargets) {
692
859
  const val = clamp01(v);
860
+ if (Array.isArray(meshNamesOrTargets) && meshNamesOrTargets.length > 0 && typeof meshNamesOrTargets[0] === "object" && "infl" in meshNamesOrTargets[0]) {
861
+ const targets2 = meshNamesOrTargets;
862
+ for (const target of targets2) {
863
+ target.infl[target.idx] = val;
864
+ }
865
+ return;
866
+ }
867
+ const meshNames = meshNamesOrTargets;
693
868
  const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
694
869
  const cached = this.morphCache.get(key);
695
870
  if (cached) {
@@ -728,11 +903,54 @@ var _LoomLargeThree = class _LoomLargeThree {
728
903
  this.morphCache.set(key, targets);
729
904
  }
730
905
  }
906
+ /**
907
+ * Resolve morph key to direct targets for ultra-fast repeated access.
908
+ * Use this when you need to set the same morph many times (e.g., in animation loops).
909
+ */
910
+ resolveMorphTargets(key, meshNames) {
911
+ const cached = this.morphCache.get(key);
912
+ if (cached) return cached;
913
+ const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
914
+ const targets = [];
915
+ if (targetMeshes.length) {
916
+ for (const name of targetMeshes) {
917
+ const mesh = this.meshByName.get(name);
918
+ if (!mesh) continue;
919
+ const dict = mesh.morphTargetDictionary;
920
+ const infl = mesh.morphTargetInfluences;
921
+ if (!dict || !infl) continue;
922
+ const idx = dict[key];
923
+ if (idx !== void 0) {
924
+ targets.push({ infl, idx });
925
+ }
926
+ }
927
+ } else {
928
+ for (const mesh of this.meshes) {
929
+ const dict = mesh.morphTargetDictionary;
930
+ const infl = mesh.morphTargetInfluences;
931
+ if (!dict || !infl) continue;
932
+ const idx = dict[key];
933
+ if (idx !== void 0) {
934
+ targets.push({ infl, idx });
935
+ }
936
+ }
937
+ }
938
+ if (targets.length > 0) {
939
+ this.morphCache.set(key, targets);
940
+ }
941
+ return targets;
942
+ }
731
943
  transitionMorph(key, to, durationMs = 120, meshNames) {
732
944
  const transitionKey = `morph_${key}`;
733
945
  const from = this.getMorphValue(key);
734
946
  const target = clamp01(to);
735
- return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => this.setMorph(key, value, meshNames));
947
+ const targets = this.resolveMorphTargets(key, meshNames);
948
+ return this.animation.addTransition(transitionKey, from, target, durationMs, (value) => {
949
+ const val = clamp01(value);
950
+ for (const t of targets) {
951
+ t.infl[t.idx] = val;
952
+ }
953
+ });
736
954
  }
737
955
  // ============================================================================
738
956
  // VISEME CONTROL
@@ -835,6 +1053,33 @@ var _LoomLargeThree = class _LoomLargeThree {
835
1053
  });
836
1054
  return result;
837
1055
  }
1056
+ /** Get all morph targets grouped by mesh name */
1057
+ getMorphTargets() {
1058
+ const result = {};
1059
+ for (const mesh of this.meshes) {
1060
+ const dict = mesh.morphTargetDictionary;
1061
+ if (dict) {
1062
+ result[mesh.name] = Object.keys(dict).sort();
1063
+ }
1064
+ }
1065
+ return result;
1066
+ }
1067
+ /** Get all resolved bone names and their current transforms */
1068
+ getBones() {
1069
+ const result = {};
1070
+ for (const name of Object.keys(this.bones)) {
1071
+ const entry = this.bones[name];
1072
+ if (entry) {
1073
+ const pos = entry.obj.position;
1074
+ const rot = entry.obj.rotation;
1075
+ result[name] = {
1076
+ position: [pos.x, pos.y, pos.z],
1077
+ rotation: [rot.x * 180 / Math.PI, rot.y * 180 / Math.PI, rot.z * 180 / Math.PI]
1078
+ };
1079
+ }
1080
+ }
1081
+ return result;
1082
+ }
838
1083
  setMeshVisible(meshName, visible) {
839
1084
  if (!this.model) return;
840
1085
  this.model.traverse((obj) => {
@@ -843,6 +1088,70 @@ var _LoomLargeThree = class _LoomLargeThree {
843
1088
  }
844
1089
  });
845
1090
  }
1091
+ /** Get material config for a mesh */
1092
+ getMeshMaterialConfig(meshName) {
1093
+ if (!this.model) return null;
1094
+ let result = null;
1095
+ this.model.traverse((obj) => {
1096
+ if (obj.isMesh && obj.name === meshName) {
1097
+ const mat = obj.material;
1098
+ if (mat) {
1099
+ let blendingName = "Normal";
1100
+ for (const [name, value] of Object.entries(_LoomLargeThree.BLENDING_MODES)) {
1101
+ if (mat.blending === value) {
1102
+ blendingName = name;
1103
+ break;
1104
+ }
1105
+ }
1106
+ result = {
1107
+ renderOrder: obj.renderOrder,
1108
+ transparent: mat.transparent,
1109
+ opacity: mat.opacity,
1110
+ depthWrite: mat.depthWrite,
1111
+ depthTest: mat.depthTest,
1112
+ blending: blendingName
1113
+ };
1114
+ }
1115
+ }
1116
+ });
1117
+ return result;
1118
+ }
1119
+ /** Set material config for a mesh */
1120
+ setMeshMaterialConfig(meshName, config) {
1121
+ if (!this.model) return;
1122
+ this.model.traverse((obj) => {
1123
+ if (obj.isMesh && obj.name === meshName) {
1124
+ const mat = obj.material;
1125
+ if (config.renderOrder !== void 0) {
1126
+ obj.renderOrder = config.renderOrder;
1127
+ }
1128
+ if (mat) {
1129
+ if (config.opacity !== void 0) {
1130
+ mat.opacity = config.opacity;
1131
+ if (config.opacity < 1 && config.transparent === void 0) {
1132
+ mat.transparent = true;
1133
+ }
1134
+ }
1135
+ if (config.transparent !== void 0) {
1136
+ mat.transparent = config.transparent;
1137
+ }
1138
+ if (config.depthWrite !== void 0) {
1139
+ mat.depthWrite = config.depthWrite;
1140
+ }
1141
+ if (config.depthTest !== void 0) {
1142
+ mat.depthTest = config.depthTest;
1143
+ }
1144
+ if (config.blending !== void 0) {
1145
+ const blendValue = _LoomLargeThree.BLENDING_MODES[config.blending];
1146
+ if (blendValue !== void 0) {
1147
+ mat.blending = blendValue;
1148
+ }
1149
+ }
1150
+ mat.needsUpdate = true;
1151
+ }
1152
+ }
1153
+ });
1154
+ }
846
1155
  // ============================================================================
847
1156
  // CONFIGURATION
848
1157
  // ============================================================================
@@ -1008,6 +1317,42 @@ var _LoomLargeThree = class _LoomLargeThree {
1008
1317
  cancel: () => handles.forEach((h) => h.cancel())
1009
1318
  };
1010
1319
  }
1320
+ /**
1321
+ * Apply render order and material settings from CC4_MESHES to all meshes.
1322
+ * This ensures proper layering (e.g., hair renders on top of eyebrows).
1323
+ */
1324
+ applyMeshMaterialSettings(root) {
1325
+ root.traverse((obj) => {
1326
+ if (!obj.isMesh || !obj.name) return;
1327
+ const meshInfo = CC4_MESHES[obj.name];
1328
+ if (!meshInfo?.material) return;
1329
+ const settings = meshInfo.material;
1330
+ if (typeof settings.renderOrder === "number") {
1331
+ obj.renderOrder = settings.renderOrder;
1332
+ }
1333
+ if (obj.material) {
1334
+ if (typeof settings.transparent === "boolean") {
1335
+ obj.material.transparent = settings.transparent;
1336
+ }
1337
+ if (typeof settings.opacity === "number") {
1338
+ obj.material.opacity = settings.opacity;
1339
+ }
1340
+ if (typeof settings.depthWrite === "boolean") {
1341
+ obj.material.depthWrite = settings.depthWrite;
1342
+ }
1343
+ if (typeof settings.depthTest === "boolean") {
1344
+ obj.material.depthTest = settings.depthTest;
1345
+ }
1346
+ if (typeof settings.blending === "string") {
1347
+ const blendValue = _LoomLargeThree.BLENDING_MODES[settings.blending];
1348
+ if (blendValue !== void 0) {
1349
+ obj.material.blending = blendValue;
1350
+ }
1351
+ }
1352
+ obj.material.needsUpdate = true;
1353
+ }
1354
+ });
1355
+ }
1011
1356
  };
1012
1357
  // Viseme jaw amounts
1013
1358
  __publicField(_LoomLargeThree, "VISEME_JAW_AMOUNTS", [
@@ -1028,6 +1373,19 @@ __publicField(_LoomLargeThree, "VISEME_JAW_AMOUNTS", [
1028
1373
  0.4
1029
1374
  ]);
1030
1375
  __publicField(_LoomLargeThree, "JAW_MAX_DEGREES", 28);
1376
+ /** Blending mode options for Three.js materials */
1377
+ __publicField(_LoomLargeThree, "BLENDING_MODES", {
1378
+ "Normal": 1,
1379
+ // THREE.NormalBlending
1380
+ "Additive": 2,
1381
+ // THREE.AdditiveBlending
1382
+ "Subtractive": 3,
1383
+ // THREE.SubtractiveBlending
1384
+ "Multiply": 4,
1385
+ // THREE.MultiplyBlending
1386
+ "None": 0
1387
+ // THREE.NoBlending
1388
+ });
1031
1389
  var LoomLargeThree = _LoomLargeThree;
1032
1390
  function collectMorphMeshes(root) {
1033
1391
  const meshes = [];
@@ -1041,6 +1399,20 @@ function collectMorphMeshes(root) {
1041
1399
  return meshes;
1042
1400
  }
1043
1401
 
1402
+ // src/mappings/types.ts
1403
+ var BLENDING_MODES = {
1404
+ "Normal": 1,
1405
+ // THREE.NormalBlending
1406
+ "Additive": 2,
1407
+ // THREE.AdditiveBlending
1408
+ "Subtractive": 3,
1409
+ // THREE.SubtractiveBlending
1410
+ "Multiply": 4,
1411
+ // THREE.MultiplyBlending
1412
+ "None": 0
1413
+ // THREE.NoBlending
1414
+ };
1415
+
1044
1416
  // src/physics/HairPhysics.ts
1045
1417
  var DEFAULT_HAIR_PHYSICS_CONFIG = {
1046
1418
  mass: 1,
@@ -1165,12 +1537,27 @@ var HairPhysics = class {
1165
1537
  }
1166
1538
  };
1167
1539
 
1540
+ exports.AU_INFO = AU_INFO;
1541
+ exports.AU_MIX_DEFAULTS = AU_MIX_DEFAULTS;
1542
+ exports.AU_TO_MORPHS = AU_TO_MORPHS;
1168
1543
  exports.AnimationThree = AnimationThree;
1544
+ exports.BLENDING_MODES = BLENDING_MODES;
1545
+ exports.BONE_AU_TO_BINDINGS = BONE_AU_TO_BINDINGS;
1546
+ exports.CC4_BONE_NODES = CC4_BONE_NODES;
1547
+ exports.CC4_EYE_MESH_NODES = CC4_EYE_MESH_NODES;
1548
+ exports.CC4_MESHES = CC4_MESHES;
1169
1549
  exports.CC4_PRESET = CC4_PRESET;
1550
+ exports.COMPOSITE_ROTATIONS = COMPOSITE_ROTATIONS;
1551
+ exports.CONTINUUM_LABELS = CONTINUUM_LABELS;
1552
+ exports.CONTINUUM_PAIRS_MAP = CONTINUUM_PAIRS_MAP;
1170
1553
  exports.DEFAULT_HAIR_PHYSICS_CONFIG = DEFAULT_HAIR_PHYSICS_CONFIG;
1171
1554
  exports.HairPhysics = HairPhysics;
1172
1555
  exports.LoomLargeThree = LoomLargeThree;
1556
+ exports.MORPH_TO_MESH = MORPH_TO_MESH;
1557
+ exports.VISEME_KEYS = VISEME_KEYS;
1173
1558
  exports.collectMorphMeshes = collectMorphMeshes;
1174
1559
  exports.default = LoomLargeThree;
1560
+ exports.hasLeftRightMorphs = hasLeftRightMorphs;
1561
+ exports.isMixedAU = isMixedAU;
1175
1562
  //# sourceMappingURL=index.cjs.map
1176
1563
  //# sourceMappingURL=index.cjs.map