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