loomlarge 0.2.1 → 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/README.md CHANGED
@@ -10,16 +10,17 @@ LoomLarge provides pre-built mappings that connect [Facial Action Coding System
10
10
 
11
11
  1. [Installation & Setup](#1-installation--setup)
12
12
  2. [Using Presets](#2-using-presets)
13
- 3. [Extending & Custom Presets](#3-extending--custom-presets)
14
- 4. [Action Unit Control](#4-action-unit-control)
15
- 5. [Mix Weight System](#5-mix-weight-system)
16
- 6. [Composite Rotation System](#6-composite-rotation-system)
17
- 7. [Continuum Pairs](#7-continuum-pairs)
18
- 8. [Direct Morph Control](#8-direct-morph-control)
19
- 9. [Viseme System](#9-viseme-system)
20
- 10. [Transition System](#10-transition-system)
21
- 11. [Playback & State Control](#11-playback--state-control)
22
- 12. [Hair Physics](#12-hair-physics)
13
+ 3. [Getting to Know Your Character](#3-getting-to-know-your-character)
14
+ 4. [Extending & Custom Presets](#4-extending--custom-presets)
15
+ 5. [Action Unit Control](#5-action-unit-control)
16
+ 6. [Mix Weight System](#6-mix-weight-system)
17
+ 7. [Composite Rotation System](#7-composite-rotation-system)
18
+ 8. [Continuum Pairs](#8-continuum-pairs)
19
+ 9. [Direct Morph Control](#9-direct-morph-control)
20
+ 10. [Viseme System](#10-viseme-system)
21
+ 11. [Transition System](#11-transition-system)
22
+ 12. [Playback & State Control](#12-playback--state-control)
23
+ 13. [Hair Physics](#13-hair-physics)
23
24
 
24
25
  ---
25
26
 
@@ -198,7 +199,112 @@ const loom = new LoomLargeThree({ auMappings: CC4_PRESET });
198
199
 
199
200
  ---
200
201
 
201
- ## 3. Extending & Custom Presets
202
+ ## 3. Getting to Know Your Character
203
+
204
+ Before customizing presets or extending mappings, it's helpful to understand what's actually in your character model. LoomLarge provides several methods to inspect meshes, morph targets, and bones.
205
+
206
+ ### Listing meshes
207
+
208
+ Get all meshes in your character with their visibility and morph target counts:
209
+
210
+ ```typescript
211
+ const meshes = loom.getMeshList();
212
+ console.log(meshes);
213
+ // [
214
+ // { name: 'CC_Base_Body', visible: true, morphCount: 142 },
215
+ // { name: 'CC_Base_Tongue', visible: true, morphCount: 12 },
216
+ // { name: 'CC_Base_EyeOcclusion_L', visible: true, morphCount: 8 },
217
+ // { name: 'CC_Base_EyeOcclusion_R', visible: true, morphCount: 8 },
218
+ // { name: 'Male_Bushy_1', visible: true, morphCount: 142 },
219
+ // ...
220
+ // ]
221
+ ```
222
+
223
+ ### Listing morph targets
224
+
225
+ Get all morph target names grouped by mesh:
226
+
227
+ ```typescript
228
+ const morphs = loom.getMorphTargets();
229
+ console.log(morphs);
230
+ // {
231
+ // 'CC_Base_Body': [
232
+ // 'A01_Brow_Inner_Up', 'A02_Brow_Down_Left', 'A02_Brow_Down_Right',
233
+ // 'A04_Brow_Outer_Up_Left', 'A04_Brow_Outer_Up_Right',
234
+ // 'Mouth_Smile_L', 'Mouth_Smile_R', 'Eye_Blink_L', 'Eye_Blink_R',
235
+ // ...
236
+ // ],
237
+ // 'CC_Base_Tongue': [
238
+ // 'V_Tongue_Out', 'V_Tongue_Up', 'V_Tongue_Down', ...
239
+ // ],
240
+ // ...
241
+ // }
242
+ ```
243
+
244
+ This is invaluable when creating custom presets—you need to know the exact morph target names your character uses.
245
+
246
+ ### Listing bones
247
+
248
+ Get all resolved bones with their current positions and rotations (in degrees):
249
+
250
+ ```typescript
251
+ const bones = loom.getBones();
252
+ console.log(bones);
253
+ // {
254
+ // 'HEAD': { position: [0, 156.2, 0], rotation: [0, 0, 0] },
255
+ // 'JAW': { position: [0, 154.1, 2.3], rotation: [0, 0, 0] },
256
+ // 'EYE_L': { position: [-3.2, 160.5, 8.1], rotation: [0, 0, 0] },
257
+ // 'EYE_R': { position: [3.2, 160.5, 8.1], rotation: [0, 0, 0] },
258
+ // 'TONGUE': { position: [0, 152.3, 1.8], rotation: [0, 0, 0] },
259
+ // }
260
+ ```
261
+
262
+ ### Controlling mesh visibility
263
+
264
+ Hide or show individual meshes:
265
+
266
+ ```typescript
267
+ // Hide hair mesh
268
+ loom.setMeshVisible('Side_part_wavy_1', false);
269
+
270
+ // Show it again
271
+ loom.setMeshVisible('Side_part_wavy_1', true);
272
+ ```
273
+
274
+ ### Adjusting material properties
275
+
276
+ Fine-tune render order, transparency, and blending for each mesh:
277
+
278
+ ```typescript
279
+ // Get current material config
280
+ const config = loom.getMeshMaterialConfig('CC_Base_Body');
281
+ console.log(config);
282
+ // {
283
+ // renderOrder: 0,
284
+ // transparent: false,
285
+ // opacity: 1,
286
+ // depthWrite: true,
287
+ // depthTest: true,
288
+ // blending: 'Normal'
289
+ // }
290
+
291
+ // Set custom material config
292
+ loom.setMeshMaterialConfig('CC_Base_EyeOcclusion_L', {
293
+ renderOrder: 10,
294
+ transparent: true,
295
+ opacity: 0.8,
296
+ blending: 'Normal' // 'Normal', 'Additive', 'Subtractive', 'Multiply', 'None'
297
+ });
298
+ ```
299
+
300
+ This is especially useful for:
301
+ - Fixing render order issues (eyebrows behind hair, etc.)
302
+ - Making meshes semi-transparent for debugging
303
+ - Adjusting blending modes for special effects
304
+
305
+ ---
306
+
307
+ ## 4. Extending & Custom Presets
202
308
 
203
309
  ### Extending an existing preset
204
310
 
@@ -275,7 +381,7 @@ const current = loom.getAUMappings();
275
381
 
276
382
  ---
277
383
 
278
- ## 4. Action Unit Control
384
+ ## 5. Action Unit Control
279
385
 
280
386
  Action Units are the core of FACS. Each AU represents a specific muscular movement of the face.
281
387
 
@@ -349,7 +455,7 @@ loom.setAU(12, 0.8, 1); // Right side only
349
455
 
350
456
  ---
351
457
 
352
- ## 5. Mix Weight System
458
+ ## 6. Mix Weight System
353
459
 
354
460
  Some AUs can be driven by both morph targets (blend shapes) AND bone rotations. The mix weight controls the blend between them.
355
461
 
@@ -394,7 +500,7 @@ if (isMixedAU(26)) {
394
500
 
395
501
  ---
396
502
 
397
- ## 6. Composite Rotation System
503
+ ## 7. Composite Rotation System
398
504
 
399
505
  Bones like the head and eyes need multi-axis rotation (pitch, yaw, roll). The composite rotation system handles this automatically.
400
506
 
@@ -452,7 +558,7 @@ loom.setAU(64, 0.4);
452
558
 
453
559
  ---
454
560
 
455
- ## 7. Continuum Pairs
561
+ ## 8. Continuum Pairs
456
562
 
457
563
  Continuum pairs are bidirectional AU pairs that represent opposite directions on the same axis. They're linked so that activating one should deactivate the other.
458
564
 
@@ -498,7 +604,7 @@ const pair = CONTINUUM_PAIRS_MAP[51];
498
604
 
499
605
  ---
500
606
 
501
- ## 8. Direct Morph Control
607
+ ## 9. Direct Morph Control
502
608
 
503
609
  Sometimes you need to control morph targets directly by name, bypassing the AU system.
504
610
 
@@ -537,7 +643,7 @@ LoomLarge caches morph target lookups for performance. The first time you access
537
643
 
538
644
  ---
539
645
 
540
- ## 9. Viseme System
646
+ ## 10. Viseme System
541
647
 
542
648
  Visemes are mouth shapes used for lip-sync. LoomLarge includes 15 visemes with automatic jaw coupling.
543
649
 
@@ -622,7 +728,7 @@ speak([5, 0, 10, 4]);
622
728
 
623
729
  ---
624
730
 
625
- ## 10. Transition System
731
+ ## 11. Transition System
626
732
 
627
733
  All animated changes in LoomLarge go through the transition system, which provides smooth interpolation with easing.
628
734
 
@@ -702,7 +808,7 @@ loom.clearTransitions();
702
808
 
703
809
  ---
704
810
 
705
- ## 11. Playback & State Control
811
+ ## 12. Playback & State Control
706
812
 
707
813
  ### Pausing and resuming
708
814
 
@@ -757,7 +863,7 @@ loom.dispose();
757
863
 
758
864
  ---
759
865
 
760
- ## 12. Hair Physics
866
+ ## 13. Hair Physics
761
867
 
762
868
  LoomLarge includes an experimental hair physics system that simulates hair movement based on head motion.
763
869
 
package/dist/index.cjs CHANGED
@@ -245,119 +245,121 @@ var VISEME_KEYS = [
245
245
  ];
246
246
  var BONE_AU_TO_BINDINGS = {
247
247
  // Head turn and tilt (M51-M56) - use HEAD bone only (NECK should not rotate with head)
248
- // Three.js Y rotation: positive = counter-clockwise from above = head turns LEFT (character POV)
249
248
  51: [
250
- { node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30 }
249
+ { node: "HEAD", channel: "ry", scale: 1, maxDegrees: 30, axis: "yaw" }
251
250
  // Head turn left
252
251
  ],
253
252
  52: [
254
- { node: "HEAD", channel: "ry", scale: -1, maxDegrees: 30 }
253
+ { node: "HEAD", channel: "ry", scale: -1, maxDegrees: 30, axis: "yaw" }
255
254
  // Head turn right
256
255
  ],
257
256
  53: [
258
- { node: "HEAD", channel: "rx", scale: -1, maxDegrees: 20 }
257
+ { node: "HEAD", channel: "rx", scale: -1, maxDegrees: 20, axis: "pitch" }
259
258
  // Head up
260
259
  ],
261
260
  54: [
262
- { node: "HEAD", channel: "rx", scale: 1, maxDegrees: 20 }
261
+ { node: "HEAD", channel: "rx", scale: 1, maxDegrees: 20, axis: "pitch" }
263
262
  // Head down
264
263
  ],
265
264
  55: [
266
- { node: "HEAD", channel: "rz", scale: -1, maxDegrees: 15 }
265
+ { node: "HEAD", channel: "rz", scale: -1, maxDegrees: 15, axis: "roll" }
267
266
  // Head tilt left
268
267
  ],
269
268
  56: [
270
- { node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15 }
269
+ { node: "HEAD", channel: "rz", scale: 1, maxDegrees: 15, axis: "roll" }
271
270
  // Head tilt right
272
271
  ],
273
272
  // Eyes horizontal (yaw) - CC4 rigs use rz for horizontal eye rotation
274
273
  61: [
275
- { node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32 },
274
+ { node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 32, axis: "yaw" },
276
275
  // Eyes look left
277
- { node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 32 }
276
+ { node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 32, axis: "yaw" }
278
277
  ],
279
278
  62: [
280
- { node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 32 },
279
+ { node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 32, axis: "yaw" },
281
280
  // Eyes look right
282
- { node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 32 }
281
+ { node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 32, axis: "yaw" }
283
282
  ],
284
283
  63: [
285
- { node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 32 },
286
- { 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" }
287
287
  ],
288
288
  64: [
289
- { node: "EYE_L", channel: "rx", scale: 1, maxDegrees: 32 },
290
- { 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" }
291
292
  ],
292
293
  // Single-eye (Left) — horizontal (rz for CC4) and vertical (rx)
293
- 65: [{ node: "EYE_L", channel: "rz", scale: -1, maxDegrees: 15 }],
294
- 66: [{ node: "EYE_L", channel: "rz", scale: 1, maxDegrees: 15 }],
295
- 67: [{ node: "EYE_L", channel: "rx", scale: -1, maxDegrees: 12 }],
296
- 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
297
300
  // Single-eye (Right) — horizontal (rz for CC4) and vertical (rx)
298
- 69: [{ node: "EYE_R", channel: "rz", scale: -1, maxDegrees: 15 }],
299
- 70: [{ node: "EYE_R", channel: "rz", scale: 1, maxDegrees: 15 }],
300
- 71: [{ node: "EYE_R", channel: "rx", scale: -1, maxDegrees: 12 }],
301
- 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
302
307
  // Jaw / Mouth
303
308
  8: [
304
309
  // Lips Toward Each Other - slight jaw open helps sell the lip press
305
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 8 }
306
- // Small downward rotation (jaw opening slightly)
310
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 8, axis: "roll" }
307
311
  ],
308
312
  25: [
309
313
  // Lips Part — small jaw open
310
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84 }
311
- // 73% of 8
314
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 5.84, axis: "roll" }
312
315
  ],
313
316
  26: [
314
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 28 }
315
- // 73% of 20
317
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 28, axis: "roll" }
316
318
  ],
317
319
  27: [
318
320
  // Mouth Stretch — larger jaw open
319
- { node: "JAW", channel: "rz", scale: 1, maxDegrees: 32 }
320
- // 73% of 25
321
+ { node: "JAW", channel: "rz", scale: 1, maxDegrees: 32, axis: "roll" }
321
322
  ],
322
323
  29: [
323
324
  { node: "JAW", channel: "tz", scale: -1, maxUnits: 0.02 }
324
- // Negative for forward thrust
325
+ // Translation - no axis needed
325
326
  ],
326
327
  30: [
327
328
  // Jaw Left
328
- { node: "JAW", channel: "ry", scale: -1, maxDegrees: 5 }
329
+ { node: "JAW", channel: "ry", scale: -1, maxDegrees: 5, axis: "yaw" }
329
330
  ],
330
331
  35: [
331
332
  // Jaw Right
332
- { node: "JAW", channel: "ry", scale: 1, maxDegrees: 5 }
333
+ { node: "JAW", channel: "ry", scale: 1, maxDegrees: 5, axis: "yaw" }
333
334
  ],
334
335
  // Tongue
335
336
  19: [
336
337
  { node: "TONGUE", channel: "tz", scale: -1, maxUnits: 8e-3 }
338
+ // Translation - no axis needed
337
339
  ],
338
340
  37: [
339
341
  // Tongue Up
340
- { node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 45 }
342
+ { node: "TONGUE", channel: "rz", scale: -1, maxDegrees: 45, axis: "pitch" }
341
343
  ],
342
344
  38: [
343
345
  // Tongue Down
344
- { node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 45 }
346
+ { node: "TONGUE", channel: "rz", scale: 1, maxDegrees: 45, axis: "pitch" }
345
347
  ],
346
348
  39: [
347
349
  // Tongue Left
348
- { node: "TONGUE", channel: "ry", scale: -1, maxDegrees: 10 }
350
+ { node: "TONGUE", channel: "ry", scale: -1, maxDegrees: 10, axis: "yaw" }
349
351
  ],
350
352
  40: [
351
353
  // Tongue Right
352
- { node: "TONGUE", channel: "ry", scale: 1, maxDegrees: 10 }
354
+ { node: "TONGUE", channel: "ry", scale: 1, maxDegrees: 10, axis: "yaw" }
353
355
  ],
354
356
  41: [
355
357
  // Tongue Tilt Left
356
- { node: "TONGUE", channel: "rx", scale: -1, maxDegrees: 20 }
358
+ { node: "TONGUE", channel: "rx", scale: -1, maxDegrees: 20, axis: "roll" }
357
359
  ],
358
360
  42: [
359
361
  // Tongue Tilt Right
360
- { node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20 }
362
+ { node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20, axis: "roll" }
361
363
  ]
362
364
  };
363
365
  var isMixedAU = (id) => !!(AU_TO_MORPHS[id]?.length && BONE_AU_TO_BINDINGS[id]?.length);
@@ -799,8 +801,9 @@ var _LoomLargeThree = class _LoomLargeThree {
799
801
  if (bindings) {
800
802
  for (const binding of bindings) {
801
803
  if (binding.channel === "rx" || binding.channel === "ry" || binding.channel === "rz") {
802
- const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
803
- 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
+ }
804
807
  } else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
805
808
  if (binding.maxUnits !== void 0) {
806
809
  this.updateBoneTranslation(binding.node, binding.channel, v * binding.scale, binding.maxUnits);
@@ -838,8 +841,9 @@ var _LoomLargeThree = class _LoomLargeThree {
838
841
  }
839
842
  for (const binding of bindings) {
840
843
  if (binding.channel === "rx" || binding.channel === "ry" || binding.channel === "rz") {
841
- const axis = binding.channel === "rx" ? "pitch" : binding.channel === "ry" ? "yaw" : "roll";
842
- 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
+ }
843
847
  } else if (binding.channel === "tx" || binding.channel === "ty" || binding.channel === "tz") {
844
848
  if (binding.maxUnits !== void 0) {
845
849
  handles.push(this.transitionBoneTranslation(binding.node, binding.channel, target * binding.scale, binding.maxUnits, durationMs));
@@ -851,11 +855,16 @@ var _LoomLargeThree = class _LoomLargeThree {
851
855
  getAU(id) {
852
856
  return this.auValues[id] ?? 0;
853
857
  }
854
- // ============================================================================
855
- // MORPH CONTROL
856
- // ============================================================================
857
- setMorph(key, v, meshNames) {
858
+ setMorph(key, v, meshNamesOrTargets) {
858
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;
859
868
  const targetMeshes = meshNames || this.config.morphToMesh?.face || [];
860
869
  const cached = this.morphCache.get(key);
861
870
  if (cached) {
@@ -894,11 +903,54 @@ var _LoomLargeThree = class _LoomLargeThree {
894
903
  this.morphCache.set(key, targets);
895
904
  }
896
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
+ }
897
943
  transitionMorph(key, to, durationMs = 120, meshNames) {
898
944
  const transitionKey = `morph_${key}`;
899
945
  const from = this.getMorphValue(key);
900
946
  const target = clamp01(to);
901
- 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
+ });
902
954
  }
903
955
  // ============================================================================
904
956
  // VISEME CONTROL
@@ -1001,6 +1053,33 @@ var _LoomLargeThree = class _LoomLargeThree {
1001
1053
  });
1002
1054
  return result;
1003
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
+ }
1004
1083
  setMeshVisible(meshName, visible) {
1005
1084
  if (!this.model) return;
1006
1085
  this.model.traverse((obj) => {
@@ -1009,6 +1088,70 @@ var _LoomLargeThree = class _LoomLargeThree {
1009
1088
  }
1010
1089
  });
1011
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
+ }
1012
1155
  // ============================================================================
1013
1156
  // CONFIGURATION
1014
1157
  // ============================================================================
@@ -1200,6 +1343,13 @@ var _LoomLargeThree = class _LoomLargeThree {
1200
1343
  if (typeof settings.depthTest === "boolean") {
1201
1344
  obj.material.depthTest = settings.depthTest;
1202
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;
1203
1353
  }
1204
1354
  });
1205
1355
  }
@@ -1223,6 +1373,19 @@ __publicField(_LoomLargeThree, "VISEME_JAW_AMOUNTS", [
1223
1373
  0.4
1224
1374
  ]);
1225
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
+ });
1226
1389
  var LoomLargeThree = _LoomLargeThree;
1227
1390
  function collectMorphMeshes(root) {
1228
1391
  const meshes = [];
@@ -1236,6 +1399,20 @@ function collectMorphMeshes(root) {
1236
1399
  return meshes;
1237
1400
  }
1238
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
+
1239
1416
  // src/physics/HairPhysics.ts
1240
1417
  var DEFAULT_HAIR_PHYSICS_CONFIG = {
1241
1418
  mass: 1,
@@ -1364,6 +1541,7 @@ exports.AU_INFO = AU_INFO;
1364
1541
  exports.AU_MIX_DEFAULTS = AU_MIX_DEFAULTS;
1365
1542
  exports.AU_TO_MORPHS = AU_TO_MORPHS;
1366
1543
  exports.AnimationThree = AnimationThree;
1544
+ exports.BLENDING_MODES = BLENDING_MODES;
1367
1545
  exports.BONE_AU_TO_BINDINGS = BONE_AU_TO_BINDINGS;
1368
1546
  exports.CC4_BONE_NODES = CC4_BONE_NODES;
1369
1547
  exports.CC4_EYE_MESH_NODES = CC4_EYE_MESH_NODES;