@lovelace_lol/loom3 1.0.3 → 1.0.4

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
@@ -658,8 +658,19 @@ interface Profile {
658
658
  rightMorphSuffixes?: string[];
659
659
  /** Morph category to mesh names (e.g., 'face' → ['CC_Base_Body_1']) */
660
660
  morphToMesh: Record<string, string[]>;
661
+ /**
662
+ * Optional map from AU `facePart` labels (from `auInfo`) to `morphToMesh` categories.
663
+ * This makes AU mesh routing fully preset/profile configurable.
664
+ * Example: { Eye: 'eye', Eyes: 'eye', Eyelids: 'eye', Tongue: 'tongue' }.
665
+ */
666
+ auFacePartToMeshCategory?: Record<string, string>;
661
667
  /** Viseme targets in order (typically 15 phoneme positions) */
662
668
  visemeKeys: MorphTargetRef[];
669
+ /**
670
+ * Optional `morphToMesh` category to use for viseme morph routing.
671
+ * Falls back to `morphToMesh.viseme` (if present), then `morphToMesh.face`.
672
+ */
673
+ visemeMeshCategory?: string;
663
674
  /** Optional: Jaw opening amounts per viseme index (0-1). Used for auto-generating jaw rotation in clips. */
664
675
  visemeJawAmounts?: number[];
665
676
  /** Optional: Default mix weights for bone/morph blending (0 = morph only, 1 = bone only) */
@@ -1516,9 +1527,10 @@ declare class Loom3 implements LoomLarge {
1516
1527
  getProfile(): Profile;
1517
1528
  /**
1518
1529
  * Get the mesh names that should receive morph influences for a given AU.
1519
- * Routes by facePart: Tongue tongue, Eye → eye, everything else → face.
1530
+ * Routing is driven by `auFacePartToMeshCategory` in profile config.
1520
1531
  */
1521
1532
  getMeshNamesForAU(auId: number): string[];
1533
+ getMeshNamesForViseme(): string[];
1522
1534
  registerHairObjects(objects: Object3D[]): Array<{
1523
1535
  name: string;
1524
1536
  isMesh: boolean;
package/dist/index.d.ts CHANGED
@@ -658,8 +658,19 @@ interface Profile {
658
658
  rightMorphSuffixes?: string[];
659
659
  /** Morph category to mesh names (e.g., 'face' → ['CC_Base_Body_1']) */
660
660
  morphToMesh: Record<string, string[]>;
661
+ /**
662
+ * Optional map from AU `facePart` labels (from `auInfo`) to `morphToMesh` categories.
663
+ * This makes AU mesh routing fully preset/profile configurable.
664
+ * Example: { Eye: 'eye', Eyes: 'eye', Eyelids: 'eye', Tongue: 'tongue' }.
665
+ */
666
+ auFacePartToMeshCategory?: Record<string, string>;
661
667
  /** Viseme targets in order (typically 15 phoneme positions) */
662
668
  visemeKeys: MorphTargetRef[];
669
+ /**
670
+ * Optional `morphToMesh` category to use for viseme morph routing.
671
+ * Falls back to `morphToMesh.viseme` (if present), then `morphToMesh.face`.
672
+ */
673
+ visemeMeshCategory?: string;
663
674
  /** Optional: Jaw opening amounts per viseme index (0-1). Used for auto-generating jaw rotation in clips. */
664
675
  visemeJawAmounts?: number[];
665
676
  /** Optional: Default mix weights for bone/morph blending (0 = morph only, 1 = bone only) */
@@ -1516,9 +1527,10 @@ declare class Loom3 implements LoomLarge {
1516
1527
  getProfile(): Profile;
1517
1528
  /**
1518
1529
  * Get the mesh names that should receive morph influences for a given AU.
1519
- * Routes by facePart: Tongue tongue, Eye → eye, everything else → face.
1530
+ * Routing is driven by `auFacePartToMeshCategory` in profile config.
1520
1531
  */
1521
1532
  getMeshNamesForAU(auId: number): string[];
1533
+ getMeshNamesForViseme(): string[];
1522
1534
  registerHairObjects(objects: Object3D[]): Array<{
1523
1535
  name: string;
1524
1536
  isMesh: boolean;
package/dist/index.js CHANGED
@@ -137,8 +137,22 @@ var BakedAnimationController = class {
137
137
  return this.host.getMeshNamesForAU(auId) || [];
138
138
  }
139
139
  const facePart = config.auInfo?.[String(auId)]?.facePart;
140
- if (facePart === "Tongue") return config.morphToMesh?.tongue || [];
141
- if (facePart === "Eye") return config.morphToMesh?.eye || [];
140
+ if (facePart) {
141
+ const category = config.auFacePartToMeshCategory?.[facePart];
142
+ if (category) return config.morphToMesh?.[category] || [];
143
+ }
144
+ return config.morphToMesh?.face || [];
145
+ }
146
+ getMeshNamesForViseme(config, explicitMeshNames) {
147
+ if (explicitMeshNames && explicitMeshNames.length > 0) {
148
+ return explicitMeshNames;
149
+ }
150
+ if (typeof this.host.getMeshNamesForViseme === "function") {
151
+ return this.host.getMeshNamesForViseme() || [];
152
+ }
153
+ const category = config.visemeMeshCategory || (config.morphToMesh?.viseme ? "viseme" : "face");
154
+ const visemeMeshes = config.morphToMesh?.[category];
155
+ if (visemeMeshes && visemeMeshes.length > 0) return visemeMeshes;
142
156
  return config.morphToMesh?.face || [];
143
157
  }
144
158
  update(dtSeconds) {
@@ -434,11 +448,12 @@ var BakedAnimationController = class {
434
448
  if (isNumericAU(curveId)) {
435
449
  const auId = Number(curveId);
436
450
  if (isVisemeIndex(curveId)) {
451
+ const visemeMeshNames = this.getMeshNamesForViseme(config, meshNames);
437
452
  const visemeKey = config.visemeKeys[auId];
438
453
  if (typeof visemeKey === "number") {
439
- this.addMorphIndexTracks(tracks, visemeKey, keyframes, intensityScale, meshNames);
454
+ this.addMorphIndexTracks(tracks, visemeKey, keyframes, intensityScale, visemeMeshNames);
440
455
  } else if (visemeKey) {
441
- this.addMorphTracks(tracks, visemeKey, keyframes, intensityScale, meshNames);
456
+ this.addMorphTracks(tracks, visemeKey, keyframes, intensityScale, visemeMeshNames);
442
457
  }
443
458
  } else {
444
459
  const auMeshNames = this.getMeshNamesForAU(auId, config, meshNames);
@@ -1763,6 +1778,12 @@ var MORPH_TO_MESH = {
1763
1778
  tongue: ["CC_Base_Tongue", "CC_Base_Tongue_1"],
1764
1779
  hair: ["Side_part_wavy", "Side_part_wavy_1", "Side_part_wavy_2"]
1765
1780
  };
1781
+ var AU_FACEPART_TO_MESH_CATEGORY = {
1782
+ Eye: "eye",
1783
+ Eyes: "eye",
1784
+ Eyelids: "eye",
1785
+ Tongue: "tongue"
1786
+ };
1766
1787
  var CC4_HAIR_PHYSICS = {
1767
1788
  stiffness: 7.5,
1768
1789
  damping: 0.18,
@@ -1808,7 +1829,9 @@ var CC4_PRESET = {
1808
1829
  bonePrefix: CC4_BONE_PREFIX,
1809
1830
  suffixPattern: CC4_SUFFIX_PATTERN,
1810
1831
  morphToMesh: MORPH_TO_MESH,
1832
+ auFacePartToMeshCategory: AU_FACEPART_TO_MESH_CATEGORY,
1811
1833
  visemeKeys: VISEME_KEYS,
1834
+ visemeMeshCategory: "viseme",
1812
1835
  visemeJawAmounts: VISEME_JAW_AMOUNTS,
1813
1836
  auMixDefaults: AU_MIX_DEFAULTS,
1814
1837
  auInfo: AU_INFO,
@@ -2128,9 +2151,10 @@ var HairPhysicsController = class {
2128
2151
  if (meshName) {
2129
2152
  targetMesh = this.registeredHairObjects.get(meshName);
2130
2153
  } else {
2131
- for (const [name, mesh] of this.registeredHairObjects) {
2132
- const info = CC4_MESHES[name];
2133
- if (info?.category === "hair") {
2154
+ const hairMeshNames = this.getHairMeshNames();
2155
+ for (const name of hairMeshNames) {
2156
+ const mesh = this.registeredHairObjects.get(name) || this.host.getMeshByName(name);
2157
+ if (mesh) {
2134
2158
  targetMesh = mesh;
2135
2159
  break;
2136
2160
  }
@@ -2224,6 +2248,15 @@ var HairPhysicsController = class {
2224
2248
  }
2225
2249
  getHairMeshNames() {
2226
2250
  if (this.cachedHairMeshNames) return this.cachedHairMeshNames;
2251
+ if (typeof this.host.getSelectedHairMeshNames === "function") {
2252
+ const selectedHairMeshNames = this.host.getSelectedHairMeshNames() || [];
2253
+ const resolved = selectedHairMeshNames.filter((name) => {
2254
+ const mesh = this.registeredHairObjects.get(name) || this.host.getMeshByName(name);
2255
+ return !!mesh;
2256
+ });
2257
+ this.cachedHairMeshNames = Array.from(new Set(resolved));
2258
+ return this.cachedHairMeshNames;
2259
+ }
2227
2260
  const names = [];
2228
2261
  this.registeredHairObjects.forEach((mesh, name) => {
2229
2262
  const info = CC4_MESHES[name];
@@ -2239,6 +2272,18 @@ var HairPhysicsController = class {
2239
2272
  this.cachedHairMeshNames = names;
2240
2273
  return names;
2241
2274
  }
2275
+ refreshMeshSelection() {
2276
+ this.cachedHairMeshNames = null;
2277
+ this.idleClipDirty = true;
2278
+ this.gravityClipDirty = true;
2279
+ this.impulseClipDirty = true;
2280
+ this.warnMissingHairMorphTargets();
2281
+ if (this.hairPhysicsEnabled) {
2282
+ this.startIdleClip();
2283
+ this.startGravityClip();
2284
+ this.buildImpulseClips();
2285
+ }
2286
+ }
2242
2287
  supportsMixerClips() {
2243
2288
  return typeof this.host.buildClip === "function";
2244
2289
  }
@@ -2253,7 +2298,11 @@ var HairPhysicsController = class {
2253
2298
  return;
2254
2299
  }
2255
2300
  const hairMeshNames = this.getHairMeshNames();
2256
- if (hairMeshNames.length === 0) return;
2301
+ if (hairMeshNames.length === 0) {
2302
+ this.stopIdleClip();
2303
+ this.idleClipDirty = false;
2304
+ return;
2305
+ }
2257
2306
  if (!this.idleClipDirty && this.idleClipHandle) return;
2258
2307
  this.stopIdleClip();
2259
2308
  const duration = Math.max(0.5, cfg.idleClipDuration);
@@ -2271,7 +2320,11 @@ var HairPhysicsController = class {
2271
2320
  startGravityClip() {
2272
2321
  if (!this.hairPhysicsEnabled || !this.supportsMixerClips()) return;
2273
2322
  const hairMeshNames = this.getHairMeshNames();
2274
- if (hairMeshNames.length === 0) return;
2323
+ if (hairMeshNames.length === 0) {
2324
+ this.stopGravityClip();
2325
+ this.gravityClipDirty = false;
2326
+ return;
2327
+ }
2275
2328
  if (!this.gravityClipDirty && this.gravityClipHandle) return;
2276
2329
  this.stopGravityClip();
2277
2330
  const morphTargets = this.hairPhysicsConfig.morphTargets;
@@ -2324,7 +2377,11 @@ var HairPhysicsController = class {
2324
2377
  buildImpulseClips() {
2325
2378
  if (!this.hairPhysicsEnabled || !this.supportsMixerClips()) return;
2326
2379
  const hairMeshNames = this.getHairMeshNames();
2327
- if (hairMeshNames.length === 0) return;
2380
+ if (hairMeshNames.length === 0) {
2381
+ this.stopImpulseClips();
2382
+ this.impulseClipDirty = false;
2383
+ return;
2384
+ }
2328
2385
  if (!this.impulseClipDirty && this.impulseClips.left && this.impulseClips.right && this.impulseClips.front) {
2329
2386
  return;
2330
2387
  }
@@ -2660,7 +2717,9 @@ function resolveProfile(base, override) {
2660
2717
  auToBones: mergeRecord(base.auToBones, override.auToBones),
2661
2718
  boneNodes: mergeRecord(base.boneNodes, override.boneNodes),
2662
2719
  morphToMesh: mergeRecord(base.morphToMesh, override.morphToMesh),
2720
+ auFacePartToMeshCategory: base.auFacePartToMeshCategory || override.auFacePartToMeshCategory ? mergeRecord(base.auFacePartToMeshCategory || {}, override.auFacePartToMeshCategory || {}) : void 0,
2663
2721
  visemeKeys: override.visemeKeys ? [...override.visemeKeys] : [...base.visemeKeys],
2722
+ visemeMeshCategory: override.visemeMeshCategory ?? base.visemeMeshCategory,
2664
2723
  auMixDefaults: base.auMixDefaults || override.auMixDefaults ? mergeRecord(base.auMixDefaults || {}, override.auMixDefaults || {}) : void 0,
2665
2724
  auInfo: base.auInfo || override.auInfo ? mergeRecord(base.auInfo || {}, override.auInfo || {}) : void 0,
2666
2725
  eyeMeshNodes: override.eyeMeshNodes ?? base.eyeMeshNodes,
@@ -3498,6 +3557,7 @@ var _Loom3 = class _Loom3 {
3498
3557
  this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
3499
3558
  this.hairPhysics = new HairPhysicsController({
3500
3559
  getMeshByName: (name) => this.meshByName.get(name),
3560
+ getSelectedHairMeshNames: () => this.config.morphToMesh?.hair || [],
3501
3561
  buildClip: (clipName, curves, options) => this.buildClip(clipName, curves, options),
3502
3562
  cleanupSnippet: (name) => this.cleanupSnippet(name)
3503
3563
  });
@@ -3506,6 +3566,7 @@ var _Loom3 = class _Loom3 {
3506
3566
  getMeshes: () => this.meshes,
3507
3567
  getMeshByName: (name) => this.meshByName.get(name),
3508
3568
  getMeshNamesForAU: (auId) => this.getMeshNamesForAU(auId),
3569
+ getMeshNamesForViseme: () => this.getMeshNamesForViseme(),
3509
3570
  getBones: () => this.bones,
3510
3571
  getConfig: () => this.config,
3511
3572
  getCompositeRotations: () => this.compositeRotations,
@@ -3602,7 +3663,8 @@ var _Loom3 = class _Loom3 {
3602
3663
  }
3603
3664
  for (let i = 0; i < (this.config.visemeKeys || []).length; i += 1) {
3604
3665
  const key = this.config.visemeKeys[i];
3605
- const targets = typeof key === "number" ? this.resolveMorphTargetsByIndex(key) : this.resolveMorphTargets(key);
3666
+ const visemeMeshNames = this.getMeshNamesForViseme();
3667
+ const targets = typeof key === "number" ? this.resolveMorphTargetsByIndex(key, visemeMeshNames) : this.resolveMorphTargets(key, visemeMeshNames);
3606
3668
  this.resolvedVisemeTargets[i] = targets;
3607
3669
  }
3608
3670
  }
@@ -4034,10 +4096,11 @@ var _Loom3 = class _Loom3 {
4034
4096
  this.applyMorphTargets(targets, val);
4035
4097
  } else {
4036
4098
  const morphKey = this.config.visemeKeys[visemeIndex];
4099
+ const visemeMeshNames = this.getMeshNamesForViseme();
4037
4100
  if (typeof morphKey === "number") {
4038
- this.setMorphInfluence(morphKey, val);
4101
+ this.setMorphInfluence(morphKey, val, visemeMeshNames);
4039
4102
  } else if (typeof morphKey === "string") {
4040
- this.setMorph(morphKey, val);
4103
+ this.setMorph(morphKey, val, visemeMeshNames);
4041
4104
  }
4042
4105
  }
4043
4106
  const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * val * jawScale;
@@ -4055,7 +4118,8 @@ var _Loom3 = class _Loom3 {
4055
4118
  const morphKey = this.config.visemeKeys[visemeIndex];
4056
4119
  const target = clamp012(to);
4057
4120
  this.visemeValues[visemeIndex] = target;
4058
- const morphHandle = typeof morphKey === "number" ? this.transitionMorphInfluence(morphKey, target, durationMs) : this.transitionMorph(morphKey, target, durationMs);
4121
+ const visemeMeshNames = this.getMeshNamesForViseme();
4122
+ const morphHandle = typeof morphKey === "number" ? this.transitionMorphInfluence(morphKey, target, durationMs, visemeMeshNames) : this.transitionMorph(morphKey, target, durationMs, visemeMeshNames);
4059
4123
  const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * target * jawScale;
4060
4124
  if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
4061
4125
  return morphHandle;
@@ -4359,6 +4423,7 @@ var _Loom3 = class _Loom3 {
4359
4423
  if (this.model) {
4360
4424
  this.rebuildMorphTargetsCache();
4361
4425
  }
4426
+ this.hairPhysics.refreshMeshSelection();
4362
4427
  this.applyHairPhysicsProfileConfig();
4363
4428
  }
4364
4429
  getProfile() {
@@ -4366,20 +4431,24 @@ var _Loom3 = class _Loom3 {
4366
4431
  }
4367
4432
  /**
4368
4433
  * Get the mesh names that should receive morph influences for a given AU.
4369
- * Routes by facePart: Tongue tongue, Eye → eye, everything else → face.
4434
+ * Routing is driven by `auFacePartToMeshCategory` in profile config.
4370
4435
  */
4371
4436
  getMeshNamesForAU(auId) {
4372
4437
  const m = this.config.morphToMesh;
4373
4438
  const info = this.config.auInfo?.[String(auId)];
4374
- if (!info?.facePart) return m?.face || [];
4375
- switch (info.facePart) {
4376
- case "Tongue":
4377
- return m?.tongue || [];
4378
- case "Eye":
4379
- return m?.eye || [];
4380
- default:
4381
- return m?.face || [];
4439
+ const facePart = info?.facePart;
4440
+ if (facePart) {
4441
+ const category = this.config.auFacePartToMeshCategory?.[facePart];
4442
+ if (category) {
4443
+ return m?.[category] || [];
4444
+ }
4382
4445
  }
4446
+ return m?.face || [];
4447
+ }
4448
+ getMeshNamesForViseme() {
4449
+ const m = this.config.morphToMesh;
4450
+ const category = this.config.visemeMeshCategory || (m?.viseme ? "viseme" : "face");
4451
+ return m?.[category] || m?.face || [];
4383
4452
  }
4384
4453
  // ============================================================================
4385
4454
  // HAIR PHYSICS