loomlarge 0.1.0 → 0.1.6

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.d.cts CHANGED
@@ -35,7 +35,7 @@ interface AUMappingConfig {
35
35
  /**
36
36
  * Helper type for mesh categories in morphToMesh
37
37
  */
38
- type MorphCategory = 'face' | 'viseme' | 'eye' | 'tearLine' | 'tongue' | 'hair';
38
+ type MorphCategory$1 = 'face' | 'viseme' | 'eye' | 'tearLine' | 'tongue' | 'hair';
39
39
 
40
40
  /**
41
41
  * LoomLarge - Core Type Definitions
@@ -203,10 +203,21 @@ interface LoomLarge {
203
203
  onReady(payload: ReadyPayload): void;
204
204
  /**
205
205
  * Update animation state. Call each frame with delta time in seconds.
206
+ * If using start(), this is called automatically.
206
207
  */
207
208
  update(deltaSeconds: number): void;
209
+ /**
210
+ * Start the internal animation loop (RAF-based).
211
+ * Automatically calls update() each frame with delta time.
212
+ */
213
+ start(): void;
214
+ /**
215
+ * Stop the internal animation loop.
216
+ */
217
+ stop(): void;
208
218
  /**
209
219
  * Dispose engine resources and cleanup.
220
+ * Stops the animation loop and clears all transitions.
210
221
  */
211
222
  dispose(): void;
212
223
  /**
@@ -369,11 +380,18 @@ declare class LoomLargeThree implements LoomLarge {
369
380
  private bones;
370
381
  private mixWeights;
371
382
  private visemeValues;
383
+ private clock;
384
+ private rafId;
385
+ private running;
372
386
  private static readonly VISEME_JAW_AMOUNTS;
373
387
  private static readonly JAW_MAX_DEGREES;
374
388
  constructor(config?: LoomLargeConfig, animation?: Animation);
375
389
  onReady(payload: ReadyPayload): void;
376
390
  update(deltaSeconds: number): void;
391
+ /** Start the internal RAF loop */
392
+ start(): void;
393
+ /** Stop the internal RAF loop */
394
+ stop(): void;
377
395
  dispose(): void;
378
396
  setAU(id: number | string, v: number, balance?: number): void;
379
397
  transitionAU(id: number | string, to: number, durationMs?: number, balance?: number): TransitionHandle;
@@ -539,6 +557,70 @@ interface HairPhysics$1 {
539
557
  * and AU metadata that we painstakingly worked through.
540
558
  */
541
559
 
560
+ declare const AU_TO_MORPHS: Record<number, string[]>;
561
+ declare const VISEME_KEYS: string[];
562
+ declare const BONE_AU_TO_BINDINGS: Record<number, BoneBinding[]>;
563
+ /** Check if an AU has both morphs and bones (can blend between them) */
564
+ declare const isMixedAU: (id: number) => boolean;
565
+ /** Check if an AU has separate left/right morphs */
566
+ declare const hasLeftRightMorphs: (auId: number) => boolean;
567
+ declare const COMPOSITE_ROTATIONS: CompositeRotation[];
568
+ /**
569
+ * Continuum pair mappings - precomputed from COMPOSITE_ROTATIONS
570
+ * Maps AU ID to its continuum partner info for bidirectional axes
571
+ * (e.g., AU 51 "Head Left" is paired with AU 52 "Head Right")
572
+ */
573
+ declare const CONTINUUM_PAIRS_MAP: Record<number, {
574
+ pairId: number;
575
+ isNegative: boolean;
576
+ axis: 'pitch' | 'yaw' | 'roll';
577
+ node: 'JAW' | 'HEAD' | 'EYE_L' | 'EYE_R' | 'TONGUE';
578
+ }>;
579
+ /**
580
+ * Human-readable labels for continuum pairs
581
+ * Key format: "negativeAU-positiveAU"
582
+ * Used by UI components (ContinuumSlider, AUSection) to display friendly axis names
583
+ */
584
+ declare const CONTINUUM_LABELS: Record<string, string>;
585
+ declare const CC4_BONE_NODES: {
586
+ readonly EYE_L: "CC_Base_L_Eye";
587
+ readonly EYE_R: "CC_Base_R_Eye";
588
+ readonly HEAD: "CC_Base_Head";
589
+ readonly NECK: "CC_Base_NeckTwist01";
590
+ readonly NECK_TWIST: "CC_Base_NeckTwist02";
591
+ readonly JAW: "CC_Base_JawRoot";
592
+ readonly TONGUE: "CC_Base_Tongue01";
593
+ };
594
+ declare const CC4_EYE_MESH_NODES: {
595
+ readonly LEFT: "CC_Base_Eye";
596
+ readonly RIGHT: "CC_Base_Eye_1";
597
+ };
598
+ declare const AU_INFO: Record<string, AUInfo>;
599
+ /** Default mix weights (0 = morph only, 1 = bone only) */
600
+ declare const AU_MIX_DEFAULTS: Record<number, number>;
601
+ type MeshCategory = 'body' | 'eye' | 'eyeOcclusion' | 'tearLine' | 'teeth' | 'tongue' | 'hair' | 'eyebrow' | 'cornea' | 'eyelash';
602
+ /** Blending mode names (matches Three.js constants) */
603
+ type BlendingMode = 'Normal' | 'Additive' | 'Subtractive' | 'Multiply' | 'None';
604
+ /** Material settings for mesh rendering */
605
+ interface MeshMaterialSettings {
606
+ renderOrder?: number;
607
+ transparent?: boolean;
608
+ opacity?: number;
609
+ depthWrite?: boolean;
610
+ depthTest?: boolean;
611
+ blending?: BlendingMode;
612
+ }
613
+ /** Mesh info including category, morph count, and optional material settings */
614
+ interface CC4MeshInfo {
615
+ category: MeshCategory;
616
+ morphCount: number;
617
+ material?: MeshMaterialSettings;
618
+ }
619
+ /** Exact mesh name -> category mapping from the character GLB */
620
+ declare const CC4_MESHES: Record<string, CC4MeshInfo>;
621
+ type MorphCategory = 'face' | 'viseme' | 'eye' | 'tearLine' | 'tongue' | 'hair';
622
+ /** Which mesh each morph category applies to */
623
+ declare const MORPH_TO_MESH: Record<MorphCategory, string[]>;
542
624
  declare const CC4_PRESET: AUMappingConfig;
543
625
 
544
626
  /**
@@ -628,4 +710,4 @@ declare class HairPhysics {
628
710
  reset(): void;
629
711
  }
630
712
 
631
- export { type AUInfo, type AUMappingConfig, type Animation, AnimationThree, type BoneBinding, type BoneKey, CC4_PRESET, type CompositeRotation, type CompositeRotationState, DEFAULT_HAIR_PHYSICS_CONFIG, type HairMorphOutput$1 as HairMorphOutput, HairPhysics, type HairPhysicsConfig$1 as HairPhysicsConfig, type HairPhysics$1 as HairPhysicsInterface, type HairMorphOutput as HairPhysicsMorphOutput, type HairPhysicsState, type HairState, type HairStrand, type HeadState$1 as HeadState, type LoomEuler, type LoomLarge, type LoomLargeConfig, LoomLargeThree, type LoomMesh, type LoomObject3D, type LoomQuaternion, type LoomVector3, type MeshInfo, type MorphCategory, type ReadyPayload, type RotationAxis, type RotationAxisState, type RotationsState, type TransitionHandle, collectMorphMeshes, LoomLargeThree as default };
713
+ export { type AUInfo, type AUMappingConfig, AU_INFO, AU_MIX_DEFAULTS, AU_TO_MORPHS, type Animation, AnimationThree, BONE_AU_TO_BINDINGS, type BoneBinding, type BoneKey, CC4_BONE_NODES, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, type CompositeRotation, type CompositeRotationState, DEFAULT_HAIR_PHYSICS_CONFIG, type HairMorphOutput$1 as HairMorphOutput, HairPhysics, type HairPhysicsConfig$1 as HairPhysicsConfig, type HairPhysics$1 as HairPhysicsInterface, type HairMorphOutput as HairPhysicsMorphOutput, type HairPhysicsState, type HairState, type HairStrand, type HeadState$1 as HeadState, type LoomEuler, type LoomLarge, type LoomLargeConfig, LoomLargeThree, type LoomMesh, type LoomObject3D, type LoomQuaternion, type LoomVector3, MORPH_TO_MESH, type MeshInfo, type MorphCategory$1 as MorphCategory, type ReadyPayload, type RotationAxis, type RotationAxisState, type RotationsState, type TransitionHandle, VISEME_KEYS, collectMorphMeshes, LoomLargeThree as default, hasLeftRightMorphs, isMixedAU };
package/dist/index.d.ts CHANGED
@@ -35,7 +35,7 @@ interface AUMappingConfig {
35
35
  /**
36
36
  * Helper type for mesh categories in morphToMesh
37
37
  */
38
- type MorphCategory = 'face' | 'viseme' | 'eye' | 'tearLine' | 'tongue' | 'hair';
38
+ type MorphCategory$1 = 'face' | 'viseme' | 'eye' | 'tearLine' | 'tongue' | 'hair';
39
39
 
40
40
  /**
41
41
  * LoomLarge - Core Type Definitions
@@ -203,10 +203,21 @@ interface LoomLarge {
203
203
  onReady(payload: ReadyPayload): void;
204
204
  /**
205
205
  * Update animation state. Call each frame with delta time in seconds.
206
+ * If using start(), this is called automatically.
206
207
  */
207
208
  update(deltaSeconds: number): void;
209
+ /**
210
+ * Start the internal animation loop (RAF-based).
211
+ * Automatically calls update() each frame with delta time.
212
+ */
213
+ start(): void;
214
+ /**
215
+ * Stop the internal animation loop.
216
+ */
217
+ stop(): void;
208
218
  /**
209
219
  * Dispose engine resources and cleanup.
220
+ * Stops the animation loop and clears all transitions.
210
221
  */
211
222
  dispose(): void;
212
223
  /**
@@ -369,11 +380,18 @@ declare class LoomLargeThree implements LoomLarge {
369
380
  private bones;
370
381
  private mixWeights;
371
382
  private visemeValues;
383
+ private clock;
384
+ private rafId;
385
+ private running;
372
386
  private static readonly VISEME_JAW_AMOUNTS;
373
387
  private static readonly JAW_MAX_DEGREES;
374
388
  constructor(config?: LoomLargeConfig, animation?: Animation);
375
389
  onReady(payload: ReadyPayload): void;
376
390
  update(deltaSeconds: number): void;
391
+ /** Start the internal RAF loop */
392
+ start(): void;
393
+ /** Stop the internal RAF loop */
394
+ stop(): void;
377
395
  dispose(): void;
378
396
  setAU(id: number | string, v: number, balance?: number): void;
379
397
  transitionAU(id: number | string, to: number, durationMs?: number, balance?: number): TransitionHandle;
@@ -539,6 +557,70 @@ interface HairPhysics$1 {
539
557
  * and AU metadata that we painstakingly worked through.
540
558
  */
541
559
 
560
+ declare const AU_TO_MORPHS: Record<number, string[]>;
561
+ declare const VISEME_KEYS: string[];
562
+ declare const BONE_AU_TO_BINDINGS: Record<number, BoneBinding[]>;
563
+ /** Check if an AU has both morphs and bones (can blend between them) */
564
+ declare const isMixedAU: (id: number) => boolean;
565
+ /** Check if an AU has separate left/right morphs */
566
+ declare const hasLeftRightMorphs: (auId: number) => boolean;
567
+ declare const COMPOSITE_ROTATIONS: CompositeRotation[];
568
+ /**
569
+ * Continuum pair mappings - precomputed from COMPOSITE_ROTATIONS
570
+ * Maps AU ID to its continuum partner info for bidirectional axes
571
+ * (e.g., AU 51 "Head Left" is paired with AU 52 "Head Right")
572
+ */
573
+ declare const CONTINUUM_PAIRS_MAP: Record<number, {
574
+ pairId: number;
575
+ isNegative: boolean;
576
+ axis: 'pitch' | 'yaw' | 'roll';
577
+ node: 'JAW' | 'HEAD' | 'EYE_L' | 'EYE_R' | 'TONGUE';
578
+ }>;
579
+ /**
580
+ * Human-readable labels for continuum pairs
581
+ * Key format: "negativeAU-positiveAU"
582
+ * Used by UI components (ContinuumSlider, AUSection) to display friendly axis names
583
+ */
584
+ declare const CONTINUUM_LABELS: Record<string, string>;
585
+ declare const CC4_BONE_NODES: {
586
+ readonly EYE_L: "CC_Base_L_Eye";
587
+ readonly EYE_R: "CC_Base_R_Eye";
588
+ readonly HEAD: "CC_Base_Head";
589
+ readonly NECK: "CC_Base_NeckTwist01";
590
+ readonly NECK_TWIST: "CC_Base_NeckTwist02";
591
+ readonly JAW: "CC_Base_JawRoot";
592
+ readonly TONGUE: "CC_Base_Tongue01";
593
+ };
594
+ declare const CC4_EYE_MESH_NODES: {
595
+ readonly LEFT: "CC_Base_Eye";
596
+ readonly RIGHT: "CC_Base_Eye_1";
597
+ };
598
+ declare const AU_INFO: Record<string, AUInfo>;
599
+ /** Default mix weights (0 = morph only, 1 = bone only) */
600
+ declare const AU_MIX_DEFAULTS: Record<number, number>;
601
+ type MeshCategory = 'body' | 'eye' | 'eyeOcclusion' | 'tearLine' | 'teeth' | 'tongue' | 'hair' | 'eyebrow' | 'cornea' | 'eyelash';
602
+ /** Blending mode names (matches Three.js constants) */
603
+ type BlendingMode = 'Normal' | 'Additive' | 'Subtractive' | 'Multiply' | 'None';
604
+ /** Material settings for mesh rendering */
605
+ interface MeshMaterialSettings {
606
+ renderOrder?: number;
607
+ transparent?: boolean;
608
+ opacity?: number;
609
+ depthWrite?: boolean;
610
+ depthTest?: boolean;
611
+ blending?: BlendingMode;
612
+ }
613
+ /** Mesh info including category, morph count, and optional material settings */
614
+ interface CC4MeshInfo {
615
+ category: MeshCategory;
616
+ morphCount: number;
617
+ material?: MeshMaterialSettings;
618
+ }
619
+ /** Exact mesh name -> category mapping from the character GLB */
620
+ declare const CC4_MESHES: Record<string, CC4MeshInfo>;
621
+ type MorphCategory = 'face' | 'viseme' | 'eye' | 'tearLine' | 'tongue' | 'hair';
622
+ /** Which mesh each morph category applies to */
623
+ declare const MORPH_TO_MESH: Record<MorphCategory, string[]>;
542
624
  declare const CC4_PRESET: AUMappingConfig;
543
625
 
544
626
  /**
@@ -628,4 +710,4 @@ declare class HairPhysics {
628
710
  reset(): void;
629
711
  }
630
712
 
631
- export { type AUInfo, type AUMappingConfig, type Animation, AnimationThree, type BoneBinding, type BoneKey, CC4_PRESET, type CompositeRotation, type CompositeRotationState, DEFAULT_HAIR_PHYSICS_CONFIG, type HairMorphOutput$1 as HairMorphOutput, HairPhysics, type HairPhysicsConfig$1 as HairPhysicsConfig, type HairPhysics$1 as HairPhysicsInterface, type HairMorphOutput as HairPhysicsMorphOutput, type HairPhysicsState, type HairState, type HairStrand, type HeadState$1 as HeadState, type LoomEuler, type LoomLarge, type LoomLargeConfig, LoomLargeThree, type LoomMesh, type LoomObject3D, type LoomQuaternion, type LoomVector3, type MeshInfo, type MorphCategory, type ReadyPayload, type RotationAxis, type RotationAxisState, type RotationsState, type TransitionHandle, collectMorphMeshes, LoomLargeThree as default };
713
+ export { type AUInfo, type AUMappingConfig, AU_INFO, AU_MIX_DEFAULTS, AU_TO_MORPHS, type Animation, AnimationThree, BONE_AU_TO_BINDINGS, type BoneBinding, type BoneKey, CC4_BONE_NODES, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, type CompositeRotation, type CompositeRotationState, DEFAULT_HAIR_PHYSICS_CONFIG, type HairMorphOutput$1 as HairMorphOutput, HairPhysics, type HairPhysicsConfig$1 as HairPhysicsConfig, type HairPhysics$1 as HairPhysicsInterface, type HairMorphOutput as HairPhysicsMorphOutput, type HairPhysicsState, type HairState, type HairStrand, type HeadState$1 as HeadState, type LoomEuler, type LoomLarge, type LoomLargeConfig, LoomLargeThree, type LoomMesh, type LoomObject3D, type LoomQuaternion, type LoomVector3, MORPH_TO_MESH, type MeshInfo, type MorphCategory$1 as MorphCategory, type ReadyPayload, type RotationAxis, type RotationAxisState, type RotationsState, type TransitionHandle, VISEME_KEYS, collectMorphMeshes, LoomLargeThree as default, hasLeftRightMorphs, isMixedAU };
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);
@@ -354,6 +356,107 @@ var BONE_AU_TO_BINDINGS = {
354
356
  { node: "TONGUE", channel: "rx", scale: 1, maxDegrees: 20 }
355
357
  ]
356
358
  };
359
+ var isMixedAU = (id) => !!(AU_TO_MORPHS[id]?.length && BONE_AU_TO_BINDINGS[id]?.length);
360
+ var hasLeftRightMorphs = (auId) => {
361
+ const keys = AU_TO_MORPHS[auId] || [];
362
+ return keys.some((k) => /_L$|_R$| L$| R$|Left$|Right$/i.test(k));
363
+ };
364
+ var COMPOSITE_ROTATIONS = [
365
+ {
366
+ node: "JAW",
367
+ pitch: { aus: [25, 26, 27], axis: "rz" },
368
+ // Jaw drop (opens mouth downward)
369
+ yaw: { aus: [30, 35], axis: "ry", negative: 30, positive: 35 },
370
+ // Jaw lateral (left/right)
371
+ roll: null
372
+ // Jaw doesn't have roll
373
+ },
374
+ {
375
+ node: "HEAD",
376
+ pitch: { aus: [54, 53], axis: "rx", negative: 54, positive: 53 },
377
+ // Head down/up
378
+ yaw: { aus: [51, 52], axis: "ry", negative: 51, positive: 52 },
379
+ // Head turn left/right
380
+ roll: { aus: [55, 56], axis: "rz", negative: 55, positive: 56 }
381
+ // Head tilt left/right
382
+ },
383
+ {
384
+ node: "EYE_L",
385
+ pitch: { aus: [64, 63], axis: "rx", negative: 64, positive: 63 },
386
+ // Eyes down/up
387
+ yaw: { aus: [61, 62], axis: "rz", negative: 61, positive: 62 },
388
+ // Eyes left/right (rz for CC4)
389
+ roll: null
390
+ // Eyes don't have roll
391
+ },
392
+ {
393
+ node: "EYE_R",
394
+ pitch: { aus: [64, 63], axis: "rx", negative: 64, positive: 63 },
395
+ // Eyes down/up
396
+ yaw: { aus: [61, 62], axis: "rz", negative: 61, positive: 62 },
397
+ // Eyes left/right (rz for CC4)
398
+ roll: null
399
+ // Eyes don't have roll
400
+ },
401
+ {
402
+ node: "TONGUE",
403
+ pitch: { aus: [38, 37], axis: "rz", negative: 38, positive: 37 },
404
+ // Tongue down/up
405
+ yaw: { aus: [39, 40], axis: "ry", negative: 39, positive: 40 },
406
+ // Tongue left/right
407
+ roll: { aus: [41, 42], axis: "rx", negative: 41, positive: 42 }
408
+ // Tongue tilt left/right
409
+ }
410
+ ];
411
+ var CONTINUUM_PAIRS_MAP = {
412
+ // Eyes horizontal (yaw) - both eyes share same AUs
413
+ 61: { pairId: 62, isNegative: true, axis: "yaw", node: "EYE_L" },
414
+ 62: { pairId: 61, isNegative: false, axis: "yaw", node: "EYE_L" },
415
+ // Eyes vertical (pitch)
416
+ 64: { pairId: 63, isNegative: true, axis: "pitch", node: "EYE_L" },
417
+ 63: { pairId: 64, isNegative: false, axis: "pitch", node: "EYE_L" },
418
+ // Head yaw (turn left/right)
419
+ 51: { pairId: 52, isNegative: true, axis: "yaw", node: "HEAD" },
420
+ 52: { pairId: 51, isNegative: false, axis: "yaw", node: "HEAD" },
421
+ // Head pitch (up/down)
422
+ 54: { pairId: 53, isNegative: true, axis: "pitch", node: "HEAD" },
423
+ 53: { pairId: 54, isNegative: false, axis: "pitch", node: "HEAD" },
424
+ // Head roll (tilt left/right)
425
+ 55: { pairId: 56, isNegative: true, axis: "roll", node: "HEAD" },
426
+ 56: { pairId: 55, isNegative: false, axis: "roll", node: "HEAD" },
427
+ // Jaw yaw (left/right)
428
+ 30: { pairId: 35, isNegative: true, axis: "yaw", node: "JAW" },
429
+ 35: { pairId: 30, isNegative: false, axis: "yaw", node: "JAW" },
430
+ // Tongue yaw (left/right)
431
+ 39: { pairId: 40, isNegative: true, axis: "yaw", node: "TONGUE" },
432
+ 40: { pairId: 39, isNegative: false, axis: "yaw", node: "TONGUE" },
433
+ // Tongue pitch (up/down)
434
+ 38: { pairId: 37, isNegative: true, axis: "pitch", node: "TONGUE" },
435
+ 37: { pairId: 38, isNegative: false, axis: "pitch", node: "TONGUE" },
436
+ // Tongue roll (tilt left/right)
437
+ 41: { pairId: 42, isNegative: true, axis: "roll", node: "TONGUE" },
438
+ 42: { pairId: 41, isNegative: false, axis: "roll", node: "TONGUE" },
439
+ // Extended tongue morphs (continuum pairs)
440
+ 73: { pairId: 74, isNegative: true, axis: "yaw", node: "TONGUE" },
441
+ // Tongue Narrow/Wide
442
+ 74: { pairId: 73, isNegative: false, axis: "yaw", node: "TONGUE" },
443
+ 76: { pairId: 77, isNegative: false, axis: "pitch", node: "TONGUE" },
444
+ // Tongue Tip Up/Down
445
+ 77: { pairId: 76, isNegative: true, axis: "pitch", node: "TONGUE" }
446
+ };
447
+ var CONTINUUM_LABELS = {
448
+ "61-62": "Eyes \u2014 Horizontal",
449
+ "64-63": "Eyes \u2014 Vertical",
450
+ "51-52": "Head \u2014 Horizontal",
451
+ "54-53": "Head \u2014 Vertical",
452
+ "55-56": "Head \u2014 Tilt",
453
+ "30-35": "Jaw \u2014 Horizontal",
454
+ "38-37": "Tongue \u2014 Vertical",
455
+ "39-40": "Tongue \u2014 Horizontal",
456
+ "41-42": "Tongue \u2014 Tilt",
457
+ "73-74": "Tongue \u2014 Width",
458
+ "76-77": "Tongue Tip \u2014 Vertical"
459
+ };
357
460
  var CC4_BONE_NODES = {
358
461
  EYE_L: "CC_Base_L_Eye",
359
462
  EYE_R: "CC_Base_R_Eye",
@@ -486,6 +589,41 @@ var AU_MIX_DEFAULTS = {
486
589
  35: 0.5
487
590
  // jaw left/right
488
591
  };
592
+ var CC4_MESHES = {
593
+ // Body (6 meshes, 80 morphs each) - default render order 0
594
+ "CC_Base_Body_1": { category: "body", morphCount: 80 },
595
+ "CC_Base_Body_2": { category: "body", morphCount: 80 },
596
+ "CC_Base_Body_3": { category: "body", morphCount: 80 },
597
+ "CC_Base_Body_4": { category: "body", morphCount: 80 },
598
+ "CC_Base_Body_5": { category: "body", morphCount: 80 },
599
+ "CC_Base_Body_6": { category: "body", morphCount: 80 },
600
+ // Eyes (bone-driven, no morphs) - render first (behind everything)
601
+ "CC_Base_Eye": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
602
+ "CC_Base_Eye_1": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
603
+ "CC_Base_Eye_2": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
604
+ "CC_Base_Eye_3": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
605
+ "CC_Base_Eye_4": { category: "eye", morphCount: 0, material: { renderOrder: -10 } },
606
+ // Eye occlusion (94 morphs each) - render on top of eyes with transparency support
607
+ "CC_Base_EyeOcclusion_1": { category: "eyeOcclusion", morphCount: 94, material: { renderOrder: 2, transparent: true, opacity: 1, depthWrite: true, depthTest: true, blending: "Normal" } },
608
+ "CC_Base_EyeOcclusion_2": { category: "eyeOcclusion", morphCount: 94, material: { renderOrder: 2, transparent: true, opacity: 1, depthWrite: true, depthTest: true, blending: "Normal" } },
609
+ // Tear lines (90 morphs each) - on top of eyes/face
610
+ "CC_Base_TearLine_1": { category: "tearLine", morphCount: 90, material: { renderOrder: 2 } },
611
+ "CC_Base_TearLine_2": { category: "tearLine", morphCount: 90, material: { renderOrder: 2 } },
612
+ // Cornea (no morphs) - render first with eyes
613
+ "CC_Base_Cornea": { category: "cornea", morphCount: 0, material: { renderOrder: -10 } },
614
+ "CC_Base_Cornea_1": { category: "cornea", morphCount: 0, material: { renderOrder: -10 } },
615
+ // Teeth (no morphs, follow jaw bone) - default render order
616
+ "CC_Base_Teeth_1": { category: "teeth", morphCount: 0 },
617
+ "CC_Base_Teeth_2": { category: "teeth", morphCount: 0 },
618
+ // Tongue (23 morphs) - default render order
619
+ "CC_Base_Tongue": { category: "tongue", morphCount: 23 },
620
+ // Eyebrows (91 morphs each) - above face
621
+ "Male_Bushy_1": { category: "eyebrow", morphCount: 91, material: { renderOrder: 5 } },
622
+ "Male_Bushy_2": { category: "eyebrow", morphCount: 91, material: { renderOrder: 5 } },
623
+ // Hair (14 styling morphs each) - render last (on top of everything)
624
+ "Side_part_wavy_1": { category: "hair", morphCount: 14, material: { renderOrder: 10 } },
625
+ "Side_part_wavy_2": { category: "hair", morphCount: 14, material: { renderOrder: 10 } }
626
+ };
489
627
  var MORPH_TO_MESH = {
490
628
  // Face/AU morphs affect the main face mesh and both eyebrow meshes.
491
629
  face: ["CC_Base_Body_1", "Male_Bushy_1", "Male_Bushy_2"],
@@ -537,6 +675,10 @@ var _LoomLargeThree = class _LoomLargeThree {
537
675
  __publicField(this, "mixWeights", {});
538
676
  // Viseme state
539
677
  __publicField(this, "visemeValues", new Array(15).fill(0));
678
+ // Internal RAF loop
679
+ __publicField(this, "clock", new Clock());
680
+ __publicField(this, "rafId", null);
681
+ __publicField(this, "running", false);
540
682
  this.config = config.auMappings || CC4_PRESET;
541
683
  this.mixWeights = { ...this.config.auMixDefaults };
542
684
  this.animation = animation || new AnimationThree();
@@ -580,7 +722,30 @@ var _LoomLargeThree = class _LoomLargeThree {
580
722
  this.animation.tick(dtSeconds);
581
723
  this.flushPendingComposites();
582
724
  }
725
+ /** Start the internal RAF loop */
726
+ start() {
727
+ if (this.running) return;
728
+ this.running = true;
729
+ this.clock.start();
730
+ const tick = () => {
731
+ if (!this.running) return;
732
+ const dt = this.clock.getDelta();
733
+ this.update(dt);
734
+ this.rafId = requestAnimationFrame(tick);
735
+ };
736
+ this.rafId = requestAnimationFrame(tick);
737
+ }
738
+ /** Stop the internal RAF loop */
739
+ stop() {
740
+ this.running = false;
741
+ if (this.rafId !== null) {
742
+ cancelAnimationFrame(this.rafId);
743
+ this.rafId = null;
744
+ }
745
+ this.clock.stop();
746
+ }
583
747
  dispose() {
748
+ this.stop();
584
749
  this.clearTransitions();
585
750
  this.meshes = [];
586
751
  this.model = null;
@@ -1161,6 +1326,6 @@ var HairPhysics = class {
1161
1326
  }
1162
1327
  };
1163
1328
 
1164
- export { AnimationThree, CC4_PRESET, DEFAULT_HAIR_PHYSICS_CONFIG, HairPhysics, LoomLargeThree, collectMorphMeshes, LoomLargeThree as default };
1329
+ export { AU_INFO, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, 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
1330
  //# sourceMappingURL=index.js.map
1166
1331
  //# sourceMappingURL=index.js.map