@lovelace_lol/loom3 1.0.41 → 1.0.42

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
@@ -39,6 +39,223 @@ function getCompositeAxisBinding(nodeKey, axisConfig, direction, getValue, auToB
39
39
  return null;
40
40
  }
41
41
 
42
+ // src/mappings/visemeSystem.ts
43
+ function hasOwn(value, key) {
44
+ return Boolean(value && Object.prototype.hasOwnProperty.call(value, key));
45
+ }
46
+ function toSlotId(label, index) {
47
+ const normalized = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
48
+ return normalized || `viseme-${index}`;
49
+ }
50
+ function bindingTargets(binding) {
51
+ if (!binding) return [];
52
+ const targets = binding.targets?.map((target) => target.morph).filter((morph) => morph !== "");
53
+ if (targets && targets.length > 0) return targets;
54
+ return binding.morph !== void 0 && binding.morph !== "" ? [binding.morph] : [];
55
+ }
56
+ function getProfileVisemeSlots(profile) {
57
+ if (profile.visemeSlots && profile.visemeSlots.length > 0) {
58
+ return [...profile.visemeSlots].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
59
+ }
60
+ return (profile.visemeKeys || []).map((key, index) => {
61
+ const label = typeof key === "string" && key ? key : `Viseme ${index}`;
62
+ return {
63
+ id: toSlotId(label, index),
64
+ label,
65
+ order: index,
66
+ defaultJawAmount: profile.visemeJawAmounts?.[index]
67
+ };
68
+ });
69
+ }
70
+ function getVisemeSlotIndex(profile, slotId) {
71
+ return getProfileVisemeSlots(profile).findIndex((slot) => slot.id === slotId);
72
+ }
73
+ function compileVisemeKeys(profile) {
74
+ const slots = getProfileVisemeSlots(profile);
75
+ if (!profile.visemeBindings) return [...profile.visemeKeys || []];
76
+ return slots.map((slot, index) => {
77
+ const target = bindingTargets(profile.visemeBindings?.[slot.id])[0];
78
+ return target ?? profile.visemeKeys?.[index] ?? "";
79
+ });
80
+ }
81
+ function getVisemeJawAmounts(profile) {
82
+ const slots = getProfileVisemeSlots(profile);
83
+ if (slots.length === 0) return profile.visemeJawAmounts ? [...profile.visemeJawAmounts] : void 0;
84
+ return slots.map((slot, index) => slot.defaultJawAmount ?? profile.visemeJawAmounts?.[index] ?? 0);
85
+ }
86
+ function resolveVisemeMeshCategory(profile) {
87
+ const morphToMesh = profile.morphToMesh || {};
88
+ if (profile.visemeMeshCategory) return profile.visemeMeshCategory;
89
+ if (hasOwn(morphToMesh, "viseme")) return "viseme";
90
+ return "face";
91
+ }
92
+ function getMeshNamesForVisemeProfile(profile) {
93
+ const morphToMesh = profile.morphToMesh || {};
94
+ const category = resolveVisemeMeshCategory(profile);
95
+ if (hasOwn(morphToMesh, category)) {
96
+ return Array.isArray(morphToMesh[category]) ? [...morphToMesh[category]] : [];
97
+ }
98
+ return profile.visemeMeshCategory ? [] : [...morphToMesh.face || []];
99
+ }
100
+ function getMeshNamesForAUProfile(profile, auId) {
101
+ const morphToMesh = profile.morphToMesh || {};
102
+ const facePart = profile.auInfo?.[String(auId)]?.facePart;
103
+ const category = facePart ? profile.auFacePartToMeshCategory?.[facePart] : void 0;
104
+ if (category) return Array.isArray(morphToMesh[category]) ? [...morphToMesh[category]] : [];
105
+ return [...morphToMesh.face || []];
106
+ }
107
+ function compileMatcher(pattern) {
108
+ try {
109
+ return new RegExp(pattern, "i");
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ function classifyVisemeMorph(morphName, profile) {
115
+ const slots = getProfileVisemeSlots(profile);
116
+ const matches = [];
117
+ for (const slot of slots) {
118
+ const explicitTargets = bindingTargets(profile.visemeBindings?.[slot.id]);
119
+ if (explicitTargets.some((target) => String(target).toLowerCase() === morphName.toLowerCase())) {
120
+ matches.push({
121
+ slotId: slot.id,
122
+ label: slot.label,
123
+ confidence: 1,
124
+ reason: "explicit"
125
+ });
126
+ continue;
127
+ }
128
+ for (const pattern of slot.matchers || []) {
129
+ const matcher = compileMatcher(pattern);
130
+ if (matcher?.test(morphName)) {
131
+ matches.push({
132
+ slotId: slot.id,
133
+ label: slot.label,
134
+ confidence: 0.75,
135
+ reason: "regex",
136
+ pattern
137
+ });
138
+ }
139
+ }
140
+ }
141
+ return matches;
142
+ }
143
+ function buildMappingEditorModel(profile, morphNames = []) {
144
+ const sections = /* @__PURE__ */ new Map();
145
+ const configuredSections = profile.mappingSections || [];
146
+ const configuredById = new Map(configuredSections.map((section) => [section.id, section]));
147
+ let nextOrder = configuredSections.length;
148
+ for (const section of configuredSections) {
149
+ sections.set(section.id, { ...section });
150
+ }
151
+ const getOrder = (id, fallback) => {
152
+ const configured = configuredById.get(id);
153
+ if (configured) return configured.order;
154
+ if (fallback !== void 0) return fallback;
155
+ const order = nextOrder;
156
+ nextOrder += 1;
157
+ return order;
158
+ };
159
+ const auSectionOrders = /* @__PURE__ */ new Map();
160
+ for (const [auId, info] of Object.entries(profile.auInfo || {})) {
161
+ const label = info.facePart || "Unmapped";
162
+ auSectionOrders.set(label, Math.min(auSectionOrders.get(label) ?? Number.MAX_SAFE_INTEGER, Number(auId)));
163
+ }
164
+ if (configuredSections.length === 0 && auSectionOrders.size > 0) {
165
+ nextOrder = Math.max(...auSectionOrders.values()) + 1;
166
+ }
167
+ for (const info of Object.values(profile.auInfo || {})) {
168
+ const label = info.facePart || "Unmapped";
169
+ const configured = configuredById.get(label);
170
+ const meshCategory = profile.auFacePartToMeshCategory?.[label] || "face";
171
+ sections.set(label, {
172
+ ...configured,
173
+ id: label,
174
+ label: configured?.label || label,
175
+ kind: "au",
176
+ order: getOrder(label, auSectionOrders.get(label)),
177
+ meshCategory: configured?.meshCategory || meshCategory,
178
+ facePart: label
179
+ });
180
+ }
181
+ const configuredVisemes = configuredById.get("Visemes");
182
+ sections.set("Visemes", {
183
+ ...configuredVisemes,
184
+ id: "Visemes",
185
+ label: configuredVisemes?.label || "Visemes",
186
+ kind: "viseme",
187
+ order: getOrder("Visemes"),
188
+ meshCategory: configuredVisemes?.meshCategory || resolveVisemeMeshCategory(profile)
189
+ });
190
+ if (hasOwn(profile.morphToMesh, "hair")) {
191
+ const configuredHair = configuredById.get("Hair");
192
+ sections.set("Hair", {
193
+ ...configuredHair,
194
+ id: "Hair",
195
+ label: configuredHair?.label || "Hair",
196
+ kind: "hair",
197
+ order: getOrder("Hair"),
198
+ meshCategory: configuredHair?.meshCategory || "hair"
199
+ });
200
+ }
201
+ const configuredUnmapped = configuredById.get("Unmapped");
202
+ sections.set("Unmapped", {
203
+ ...configuredUnmapped,
204
+ id: "Unmapped",
205
+ label: configuredUnmapped?.label || "Unmapped",
206
+ kind: "unmapped",
207
+ order: getOrder("Unmapped"),
208
+ meshCategory: configuredUnmapped?.meshCategory || "face"
209
+ });
210
+ const candidates = morphNames.map((morph) => {
211
+ const matches = classifyVisemeMorph(morph, profile);
212
+ if (matches.length === 0) {
213
+ return { morph, sectionId: "Unmapped", kind: "unmapped", matches };
214
+ }
215
+ const explicit = matches.filter((match) => match.reason === "explicit");
216
+ if (explicit.length > 0) {
217
+ return { morph, sectionId: "Visemes", kind: "explicit", matches: explicit };
218
+ }
219
+ return {
220
+ morph,
221
+ sectionId: "Visemes",
222
+ kind: matches.length > 1 ? "conflict" : "candidate",
223
+ matches
224
+ };
225
+ });
226
+ return {
227
+ sections: Array.from(sections.values()).sort((a, b) => a.order - b.order || a.label.localeCompare(b.label)),
228
+ candidates
229
+ };
230
+ }
231
+ function mapProviderVisemeToSlot(profile, event) {
232
+ const slots = getProfileVisemeSlots(profile);
233
+ const provider = event.provider.toLowerCase();
234
+ if (event.id !== void 0) {
235
+ const id = String(event.id);
236
+ const index = slots.findIndex(
237
+ (slot) => (slot.providerIds?.[provider] || []).some((candidate) => String(candidate) === id)
238
+ );
239
+ if (index >= 0) {
240
+ return { slotId: slots[index].id, index, confidence: 1, reason: "provider" };
241
+ }
242
+ }
243
+ if (event.phoneme) {
244
+ const phoneme = event.phoneme.toLowerCase();
245
+ const index = slots.findIndex(
246
+ (slot) => (slot.phonemes || []).some((candidate) => candidate.toLowerCase() === phoneme)
247
+ );
248
+ if (index >= 0) {
249
+ return { slotId: slots[index].id, index, confidence: 0.8, reason: "phoneme" };
250
+ }
251
+ }
252
+ const restIndex = slots.findIndex((slot) => slot.id === "rest" || slot.features?.lipClosed === 1);
253
+ if (restIndex >= 0) {
254
+ return { slotId: slots[restIndex].id, index: restIndex, confidence: 0.25, reason: "rest" };
255
+ }
256
+ return null;
257
+ }
258
+
42
259
  // src/engines/three/balanceUtils.ts
43
260
  function clampBalance(value) {
44
261
  if (!Number.isFinite(value)) return 0;
@@ -649,12 +866,7 @@ var BakedAnimationController = class {
649
866
  if (typeof this.host.getMeshNamesForAU === "function") {
650
867
  return this.host.getMeshNamesForAU(auId) || [];
651
868
  }
652
- const facePart = config.auInfo?.[String(auId)]?.facePart;
653
- if (facePart) {
654
- const category = config.auFacePartToMeshCategory?.[facePart];
655
- if (category) return config.morphToMesh?.[category] || [];
656
- }
657
- return config.morphToMesh?.face || [];
869
+ return getMeshNamesForAUProfile(config, auId);
658
870
  }
659
871
  getMeshNamesForViseme(config, explicitMeshNames) {
660
872
  if (explicitMeshNames && explicitMeshNames.length > 0) {
@@ -663,10 +875,7 @@ var BakedAnimationController = class {
663
875
  if (typeof this.host.getMeshNamesForViseme === "function") {
664
876
  return this.host.getMeshNamesForViseme() || [];
665
877
  }
666
- const category = config.visemeMeshCategory || (config.morphToMesh?.viseme ? "viseme" : "face");
667
- const visemeMeshes = config.morphToMesh?.[category];
668
- if (visemeMeshes && visemeMeshes.length > 0) return visemeMeshes;
669
- return config.morphToMesh?.face || [];
878
+ return getMeshNamesForVisemeProfile(config);
670
879
  }
671
880
  update(dtSeconds) {
672
881
  if (this.animationMixer) {
@@ -1719,7 +1928,7 @@ var BakedAnimationController = class {
1719
1928
  }
1720
1929
  addMorphTracks(tracks, morphKey, keyframes, intensityScale, meshNames) {
1721
1930
  const config = this.host.getConfig();
1722
- const hasExplicitMeshes = !!(meshNames && meshNames.length > 0);
1931
+ const hasExplicitMeshes = meshNames !== void 0;
1723
1932
  const targetMeshNames = hasExplicitMeshes ? meshNames : config.morphToMesh?.face || [];
1724
1933
  const targetMeshes = targetMeshNames.length ? targetMeshNames.map((name) => this.host.getMeshByName(name)).filter(Boolean) : [];
1725
1934
  const addTrackForMesh = (mesh) => {
@@ -1743,7 +1952,7 @@ var BakedAnimationController = class {
1743
1952
  addMorphIndexTracks(tracks, morphIndex, keyframes, intensityScale, meshNames) {
1744
1953
  if (!Number.isInteger(morphIndex) || morphIndex < 0) return;
1745
1954
  const config = this.host.getConfig();
1746
- const hasExplicitMeshes = !!(meshNames && meshNames.length > 0);
1955
+ const hasExplicitMeshes = meshNames !== void 0;
1747
1956
  const targetMeshNames = hasExplicitMeshes ? meshNames : config.morphToMesh?.face || [];
1748
1957
  const targetMeshes = targetMeshNames.length ? targetMeshNames.map((name) => this.host.getMeshByName(name)).filter(Boolean) : [];
1749
1958
  const addTrackForMesh = (mesh) => {
@@ -2377,6 +2586,159 @@ var VISEME_JAW_AMOUNTS = [
2377
2586
  0.5
2378
2587
  // 14: W_OO
2379
2588
  ];
2589
+ var CC4_VISEME_SYSTEM_ID = "cc4-arkit-15";
2590
+ var CC4_VISEME_SLOTS = [
2591
+ {
2592
+ id: "ae",
2593
+ label: "AE",
2594
+ order: 0,
2595
+ providerIds: { azure: [4], sapi: [4] },
2596
+ phonemes: ["AE", "EH", "EY", "UH"],
2597
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ae|eh|ey)([_ .-]|$)"],
2598
+ features: { jawOpen: 0.75, lipSpread: 0.35 },
2599
+ defaultJawAmount: VISEME_JAW_AMOUNTS[0]
2600
+ },
2601
+ {
2602
+ id: "ah",
2603
+ label: "Ah",
2604
+ order: 1,
2605
+ providerIds: { azure: [1, 2, 9, 11, 12], sapi: [1, 2, 9, 11, 12] },
2606
+ phonemes: ["AA", "AE", "AH", "AX", "AW", "AY", "HH"],
2607
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ah|aa|aah|open)([_ .-]|$)"],
2608
+ features: { jawOpen: 0.8 },
2609
+ defaultJawAmount: VISEME_JAW_AMOUNTS[1]
2610
+ },
2611
+ {
2612
+ id: "b-m-p",
2613
+ label: "B_M_P",
2614
+ order: 2,
2615
+ providerIds: { azure: [0, 21], sapi: [0, 21] },
2616
+ phonemes: ["B", "M", "P"],
2617
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(b[_ .-]?m[_ .-]?p|bmp|closed|sil|rest)([_ .-]|$)"],
2618
+ features: { jawOpen: 0, lipClosed: 1 },
2619
+ defaultJawAmount: VISEME_JAW_AMOUNTS[2]
2620
+ },
2621
+ {
2622
+ id: "ch-j",
2623
+ label: "Ch_J",
2624
+ order: 3,
2625
+ providerIds: { azure: [16], sapi: [16] },
2626
+ phonemes: ["CH", "JH", "SH", "ZH"],
2627
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ch|j|sh|zh)([_ .-]|$)"],
2628
+ features: { jawOpen: 0.3, fricative: 0.6 },
2629
+ defaultJawAmount: VISEME_JAW_AMOUNTS[3]
2630
+ },
2631
+ {
2632
+ id: "ee",
2633
+ label: "EE",
2634
+ order: 4,
2635
+ providerIds: { azure: [6], sapi: [6] },
2636
+ phonemes: ["IY", "IH", "IX", "Y"],
2637
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ee|iy|y)([_ .-]|$)"],
2638
+ features: { jawOpen: 0.2, lipSpread: 0.8 },
2639
+ defaultJawAmount: VISEME_JAW_AMOUNTS[4]
2640
+ },
2641
+ {
2642
+ id: "er",
2643
+ label: "Er",
2644
+ order: 5,
2645
+ providerIds: { azure: [5], sapi: [5] },
2646
+ phonemes: ["ER"],
2647
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(er)([_ .-]|$)"],
2648
+ features: { jawOpen: 0.35, lipRound: 0.35 },
2649
+ defaultJawAmount: VISEME_JAW_AMOUNTS[5]
2650
+ },
2651
+ {
2652
+ id: "f-v",
2653
+ label: "F_V",
2654
+ order: 6,
2655
+ providerIds: { azure: [18], sapi: [18] },
2656
+ phonemes: ["F", "V"],
2657
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(f[_ .-]?v|fv)([_ .-]|$)"],
2658
+ features: { jawOpen: 0.1, fricative: 1 },
2659
+ defaultJawAmount: VISEME_JAW_AMOUNTS[6]
2660
+ },
2661
+ {
2662
+ id: "ih",
2663
+ label: "Ih",
2664
+ order: 7,
2665
+ providerIds: { azure: [6], sapi: [6] },
2666
+ phonemes: ["IH", "IX"],
2667
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ih|ix)([_ .-]|$)"],
2668
+ features: { jawOpen: 0.2, lipSpread: 0.55 },
2669
+ defaultJawAmount: VISEME_JAW_AMOUNTS[7]
2670
+ },
2671
+ {
2672
+ id: "k-g-h-ng",
2673
+ label: "K_G_H_NG",
2674
+ order: 8,
2675
+ providerIds: { azure: [20], sapi: [20] },
2676
+ phonemes: ["K", "G", "NG"],
2677
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(k[_ .-]?g[_ .-]?h?[_ .-]?ng|kg|ng)([_ .-]|$)"],
2678
+ features: { jawOpen: 0.35 },
2679
+ defaultJawAmount: VISEME_JAW_AMOUNTS[8]
2680
+ },
2681
+ {
2682
+ id: "oh",
2683
+ label: "Oh",
2684
+ order: 9,
2685
+ providerIds: { azure: [3, 8, 10], sapi: [3, 8, 10] },
2686
+ phonemes: ["AO", "OW", "OY"],
2687
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(oh|ao|ow|oy)([_ .-]|$)"],
2688
+ features: { jawOpen: 0.6, lipRound: 0.8 },
2689
+ defaultJawAmount: VISEME_JAW_AMOUNTS[9]
2690
+ },
2691
+ {
2692
+ id: "r",
2693
+ label: "R",
2694
+ order: 10,
2695
+ providerIds: { azure: [13], sapi: [13] },
2696
+ phonemes: ["R"],
2697
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(r)([_ .-]|$)"],
2698
+ features: { jawOpen: 0.35, lipRound: 0.5 },
2699
+ defaultJawAmount: VISEME_JAW_AMOUNTS[10]
2700
+ },
2701
+ {
2702
+ id: "s-z",
2703
+ label: "S_Z",
2704
+ order: 11,
2705
+ providerIds: { azure: [15], sapi: [15] },
2706
+ phonemes: ["S", "Z"],
2707
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(s[_ .-]?z|sz)([_ .-]|$)"],
2708
+ features: { jawOpen: 0.1, fricative: 1 },
2709
+ defaultJawAmount: VISEME_JAW_AMOUNTS[11]
2710
+ },
2711
+ {
2712
+ id: "t-l-d-n",
2713
+ label: "T_L_D_N",
2714
+ order: 12,
2715
+ providerIds: { azure: [14, 19], sapi: [14, 19] },
2716
+ phonemes: ["T", "L", "D", "N"],
2717
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(t[_ .-]?l[_ .-]?d[_ .-]?n|tldn|l)([_ .-]|$)"],
2718
+ features: { jawOpen: 0.3, tongueTip: 1 },
2719
+ defaultJawAmount: VISEME_JAW_AMOUNTS[12]
2720
+ },
2721
+ {
2722
+ id: "th",
2723
+ label: "Th",
2724
+ order: 13,
2725
+ providerIds: { azure: [17], sapi: [17] },
2726
+ phonemes: ["TH", "DH"],
2727
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(th|dh)([_ .-]|$)"],
2728
+ features: { jawOpen: 0.15, tongueTip: 0.8, fricative: 0.8 },
2729
+ defaultJawAmount: VISEME_JAW_AMOUNTS[13]
2730
+ },
2731
+ {
2732
+ id: "w-oo",
2733
+ label: "W_OO",
2734
+ order: 14,
2735
+ providerIds: { azure: [7], sapi: [7] },
2736
+ phonemes: ["W", "UW"],
2737
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(w[_ .-]?oo|woo|uw|oo)([_ .-]|$)"],
2738
+ features: { jawOpen: 0.5, lipRound: 1 },
2739
+ defaultJawAmount: VISEME_JAW_AMOUNTS[14]
2740
+ }
2741
+ ];
2380
2742
  var isMixedAU = (id) => {
2381
2743
  const morphs = AU_TO_MORPHS[id];
2382
2744
  const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
@@ -2688,6 +3050,23 @@ var AU_FACEPART_TO_MESH_CATEGORY = {
2688
3050
  Eyelids: "eye",
2689
3051
  Tongue: "tongue"
2690
3052
  };
3053
+ var CC4_MAPPING_SECTIONS = [
3054
+ { id: "Forehead", label: "Forehead", kind: "au", order: 0, meshCategory: "face", facePart: "Forehead" },
3055
+ { id: "Eyelids", label: "Eyelids", kind: "au", order: 1, meshCategory: "eye", facePart: "Eyelids" },
3056
+ { id: "Eyes", label: "Eyes", kind: "au", order: 2, meshCategory: "eye", facePart: "Eyes" },
3057
+ { id: "Cheeks", label: "Cheeks", kind: "au", order: 3, meshCategory: "face", facePart: "Cheeks" },
3058
+ { id: "Nose", label: "Nose", kind: "au", order: 4, meshCategory: "face", facePart: "Nose" },
3059
+ { id: "Mouth", label: "Mouth", kind: "au", order: 5, meshCategory: "face", facePart: "Mouth" },
3060
+ { id: "Chin", label: "Chin", kind: "au", order: 6, meshCategory: "face", facePart: "Chin" },
3061
+ { id: "Jaw", label: "Jaw", kind: "au", order: 7, meshCategory: "face", facePart: "Jaw" },
3062
+ { id: "Tongue", label: "Tongue", kind: "au", order: 8, meshCategory: "tongue", facePart: "Tongue" },
3063
+ { id: "Head", label: "Head", kind: "au", order: 9, meshCategory: "face", facePart: "Head" },
3064
+ { id: "Joint Controls", label: "Joint Controls", kind: "au", order: 10, meshCategory: "face", facePart: "Joint Controls" },
3065
+ { id: "Eye", label: "Eye", kind: "au", order: 11, meshCategory: "eye", facePart: "Eye" },
3066
+ { id: "Hair", label: "Hair", kind: "hair", order: 12, meshCategory: "hair" },
3067
+ { id: "Visemes", label: "Visemes", kind: "viseme", order: 13, meshCategory: "viseme" },
3068
+ { id: "Unmapped", label: "Unmapped", kind: "unmapped", order: 14, meshCategory: "face" }
3069
+ ];
2691
3070
  var CC4_HAIR_PHYSICS = {
2692
3071
  stiffness: 7.5,
2693
3072
  damping: 0.18,
@@ -2734,7 +3113,10 @@ var CC4_PRESET = {
2734
3113
  suffixPattern: CC4_SUFFIX_PATTERN,
2735
3114
  morphToMesh: MORPH_TO_MESH,
2736
3115
  auFacePartToMeshCategory: AU_FACEPART_TO_MESH_CATEGORY,
3116
+ mappingSections: CC4_MAPPING_SECTIONS,
2737
3117
  visemeKeys: VISEME_KEYS,
3118
+ visemeSystemId: CC4_VISEME_SYSTEM_ID,
3119
+ visemeSlots: CC4_VISEME_SLOTS,
2738
3120
  visemeMeshCategory: "viseme",
2739
3121
  visemeJawAmounts: VISEME_JAW_AMOUNTS,
2740
3122
  auMixDefaults: AU_MIX_DEFAULTS,
@@ -3621,8 +4003,13 @@ function extendPresetWithProfile(base, extension) {
3621
4003
  boneNodes: mergeRecord(base.boneNodes, extension.boneNodes),
3622
4004
  morphToMesh: mergeRecord(base.morphToMesh, extension.morphToMesh),
3623
4005
  auFacePartToMeshCategory: base.auFacePartToMeshCategory || extension.auFacePartToMeshCategory ? mergeRecord(base.auFacePartToMeshCategory || {}, extension.auFacePartToMeshCategory || {}) : void 0,
4006
+ mappingSections: extension.mappingSections ? [...extension.mappingSections] : base.mappingSections ? [...base.mappingSections] : void 0,
3624
4007
  visemeKeys: extension.visemeKeys ? [...extension.visemeKeys] : [...base.visemeKeys],
4008
+ visemeSystemId: extension.visemeSystemId ?? base.visemeSystemId,
4009
+ visemeSlots: extension.visemeSlots ? [...extension.visemeSlots] : base.visemeSlots ? [...base.visemeSlots] : void 0,
4010
+ visemeBindings: base.visemeBindings || extension.visemeBindings ? mergeRecord(base.visemeBindings || {}, extension.visemeBindings || {}) : void 0,
3625
4011
  visemeMeshCategory: extension.visemeMeshCategory ?? base.visemeMeshCategory,
4012
+ visemeJawAmounts: extension.visemeJawAmounts ? [...extension.visemeJawAmounts] : base.visemeJawAmounts ? [...base.visemeJawAmounts] : void 0,
3626
4013
  auMixDefaults: base.auMixDefaults || extension.auMixDefaults ? mergeRecord(base.auMixDefaults || {}, extension.auMixDefaults || {}) : void 0,
3627
4014
  auInfo: base.auInfo || extension.auInfo ? mergeRecord(base.auInfo || {}, extension.auInfo || {}) : void 0,
3628
4015
  eyeMeshNodes: extension.eyeMeshNodes ?? base.eyeMeshNodes,
@@ -5188,6 +5575,21 @@ var _Loom3 = class _Loom3 {
5188
5575
  const jawHandle = this.transitionBoneRotation("JAW", "pitch", jawAmount, durationMs);
5189
5576
  return this.combineHandles([morphHandle, jawHandle]);
5190
5577
  }
5578
+ setVisemeById(slotId, value, jawScale = 1) {
5579
+ const index = getVisemeSlotIndex(this.config, slotId);
5580
+ if (index < 0) return;
5581
+ this.setViseme(index, value, jawScale);
5582
+ }
5583
+ transitionVisemeById(slotId, to, durationMs = 80, jawScale = 1) {
5584
+ const index = getVisemeSlotIndex(this.config, slotId);
5585
+ if (index < 0) {
5586
+ return { promise: Promise.resolve(), pause: () => {
5587
+ }, resume: () => {
5588
+ }, cancel: () => {
5589
+ } };
5590
+ }
5591
+ return this.transitionViseme(index, to, durationMs, jawScale);
5592
+ }
5191
5593
  // ============================================================================
5192
5594
  // MIX WEIGHT CONTROL
5193
5595
  // ============================================================================
@@ -5533,21 +5935,10 @@ var _Loom3 = class _Loom3 {
5533
5935
  * Routing is driven by `auFacePartToMeshCategory` in profile config.
5534
5936
  */
5535
5937
  getMeshNamesForAU(auId) {
5536
- const m = this.config.morphToMesh;
5537
- const info = this.config.auInfo?.[String(auId)];
5538
- const facePart = info?.facePart;
5539
- if (facePart) {
5540
- const category = this.config.auFacePartToMeshCategory?.[facePart];
5541
- if (category) {
5542
- return m?.[category] || [];
5543
- }
5544
- }
5545
- return m?.face || [];
5938
+ return getMeshNamesForAUProfile(this.config, auId);
5546
5939
  }
5547
5940
  getMeshNamesForViseme() {
5548
- const m = this.config.morphToMesh;
5549
- const category = this.config.visemeMeshCategory || (m?.viseme ? "viseme" : "face");
5550
- return m?.[category] || m?.face || [];
5941
+ return getMeshNamesForVisemeProfile(this.config);
5551
5942
  }
5552
5943
  // ============================================================================
5553
5944
  // HAIR PHYSICS
@@ -5815,7 +6206,7 @@ var _Loom3 = class _Loom3 {
5815
6206
  );
5816
6207
  }
5817
6208
  getVisemeJawAmount(visemeIndex) {
5818
- return this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
6209
+ return getVisemeJawAmounts(this.config)?.[visemeIndex] ?? this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
5819
6210
  }
5820
6211
  collectResolvedExpressionMorphTargets() {
5821
6212
  const targets = [];
@@ -6293,7 +6684,11 @@ var PROFILE_OVERRIDE_KEYS = [
6293
6684
  "rightMorphSuffixes",
6294
6685
  "morphToMesh",
6295
6686
  "auFacePartToMeshCategory",
6687
+ "mappingSections",
6296
6688
  "visemeKeys",
6689
+ "visemeSystemId",
6690
+ "visemeSlots",
6691
+ "visemeBindings",
6297
6692
  "visemeMeshCategory",
6298
6693
  "visemeJawAmounts",
6299
6694
  "auMixDefaults",
@@ -7393,6 +7788,33 @@ function validateMappingConfig(config) {
7393
7788
  }
7394
7789
  visemeSeen.add(key);
7395
7790
  }
7791
+ const morphCategories = new Set(Object.keys(config.morphToMesh || {}));
7792
+ if (config.visemeMeshCategory && !morphCategories.has(config.visemeMeshCategory)) {
7793
+ push(
7794
+ "error",
7795
+ "VISEME_MESH_CATEGORY_MISSING",
7796
+ `visemeMeshCategory "${config.visemeMeshCategory}" is not present in morphToMesh`,
7797
+ { category: config.visemeMeshCategory }
7798
+ );
7799
+ }
7800
+ for (const [facePart, category] of Object.entries(config.auFacePartToMeshCategory || {})) {
7801
+ if (!morphCategories.has(category)) {
7802
+ push(
7803
+ "error",
7804
+ "AU_MESH_CATEGORY_MISSING",
7805
+ `AU facePart "${facePart}" routes to missing morphToMesh category "${category}"`,
7806
+ { facePart, category }
7807
+ );
7808
+ }
7809
+ }
7810
+ if (config.visemeJawAmounts && config.visemeJawAmounts.length !== (config.visemeKeys || []).length) {
7811
+ push(
7812
+ "warning",
7813
+ "VISEME_JAW_AMOUNT_LENGTH_MISMATCH",
7814
+ "visemeJawAmounts length does not match visemeKeys length",
7815
+ { visemeKeys: (config.visemeKeys || []).length, visemeJawAmounts: config.visemeJawAmounts.length }
7816
+ );
7817
+ }
7396
7818
  if (config.auMixDefaults) {
7397
7819
  for (const key of Object.keys(config.auMixDefaults)) {
7398
7820
  const auId = Number(key);
@@ -7846,6 +8268,6 @@ async function analyzeModel(options) {
7846
8268
  };
7847
8269
  }
7848
8270
 
7849
- export { AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, DEFAULT_HAIR_PHYSICS_CONFIG, FISH_AU_MAPPING_CONFIG, HairPhysics, Loom3, Loom3 as Loom3Three, Loom3 as LoomLargeThree, MORPH_TO_MESH, VISEME_JAW_AMOUNTS, VISEME_KEYS, analyzeModel, applyCharacterProfileToPreset, collectMorphMeshes, computeCameraRelativeGazeOffset, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getModelForwardDirection, getPreset, getPresetWithProfile, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, resolvePreset, resolvePresetWithOverrides, suggestBestPreset, validateMappingConfig, validateMappings };
8271
+ export { AU_INFO, AU_MAPPING_CONFIG, AU_MIX_DEFAULTS, AU_TO_MORPHS, AnimationThree, BETTA_FISH_PRESET, BLENDING_MODES, BONE_AU_TO_BINDINGS, CC4_BONE_NODES, CC4_BONE_PREFIX, CC4_EYE_MESH_NODES, CC4_MAPPING_SECTIONS, CC4_MESHES, CC4_PRESET, CC4_SUFFIX_PATTERN, CC4_VISEME_SLOTS, CC4_VISEME_SYSTEM_ID, COMPOSITE_ROTATIONS, CONTINUUM_LABELS, CONTINUUM_PAIRS_MAP, DEFAULT_HAIR_PHYSICS_CONFIG, FISH_AU_MAPPING_CONFIG, HairPhysics, Loom3, Loom3 as Loom3Three, Loom3 as LoomLargeThree, MORPH_TO_MESH, VISEME_JAW_AMOUNTS, VISEME_KEYS, analyzeModel, applyCharacterProfileToPreset, buildMappingEditorModel, collectMorphMeshes, compileVisemeKeys, computeCameraRelativeGazeOffset, detectFacingDirection, extendCharacterConfigWithPreset, extendPresetWithProfile, extractFromGLTF, extractModelData, extractProfileOverrides, findFaceCenter, fuzzyNameMatch, generateMappingCorrections, getMeshNamesForAUProfile, getMeshNamesForVisemeProfile, getModelForwardDirection, getPreset, getPresetWithProfile, getProfileVisemeSlots, getVisemeJawAmounts, getVisemeSlotIndex, hasLeftRightMorphs, isMixedAU, isPresetCompatible, mapProviderVisemeToSlot, mergeRegionsByName as mergeCharacterRegionsByName, resolveBoneName, resolveBoneNames, resolveFaceCenter, resolvePreset, resolvePresetWithOverrides, resolveVisemeMeshCategory, suggestBestPreset, validateMappingConfig, validateMappings };
7850
8272
  //# sourceMappingURL=index.js.map
7851
8273
  //# sourceMappingURL=index.js.map