@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.cjs CHANGED
@@ -60,6 +60,223 @@ function getCompositeAxisBinding(nodeKey, axisConfig, direction, getValue, auToB
60
60
  return null;
61
61
  }
62
62
 
63
+ // src/mappings/visemeSystem.ts
64
+ function hasOwn(value, key) {
65
+ return Boolean(value && Object.prototype.hasOwnProperty.call(value, key));
66
+ }
67
+ function toSlotId(label, index) {
68
+ const normalized = label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
69
+ return normalized || `viseme-${index}`;
70
+ }
71
+ function bindingTargets(binding) {
72
+ if (!binding) return [];
73
+ const targets = binding.targets?.map((target) => target.morph).filter((morph) => morph !== "");
74
+ if (targets && targets.length > 0) return targets;
75
+ return binding.morph !== void 0 && binding.morph !== "" ? [binding.morph] : [];
76
+ }
77
+ function getProfileVisemeSlots(profile) {
78
+ if (profile.visemeSlots && profile.visemeSlots.length > 0) {
79
+ return [...profile.visemeSlots].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
80
+ }
81
+ return (profile.visemeKeys || []).map((key, index) => {
82
+ const label = typeof key === "string" && key ? key : `Viseme ${index}`;
83
+ return {
84
+ id: toSlotId(label, index),
85
+ label,
86
+ order: index,
87
+ defaultJawAmount: profile.visemeJawAmounts?.[index]
88
+ };
89
+ });
90
+ }
91
+ function getVisemeSlotIndex(profile, slotId) {
92
+ return getProfileVisemeSlots(profile).findIndex((slot) => slot.id === slotId);
93
+ }
94
+ function compileVisemeKeys(profile) {
95
+ const slots = getProfileVisemeSlots(profile);
96
+ if (!profile.visemeBindings) return [...profile.visemeKeys || []];
97
+ return slots.map((slot, index) => {
98
+ const target = bindingTargets(profile.visemeBindings?.[slot.id])[0];
99
+ return target ?? profile.visemeKeys?.[index] ?? "";
100
+ });
101
+ }
102
+ function getVisemeJawAmounts(profile) {
103
+ const slots = getProfileVisemeSlots(profile);
104
+ if (slots.length === 0) return profile.visemeJawAmounts ? [...profile.visemeJawAmounts] : void 0;
105
+ return slots.map((slot, index) => slot.defaultJawAmount ?? profile.visemeJawAmounts?.[index] ?? 0);
106
+ }
107
+ function resolveVisemeMeshCategory(profile) {
108
+ const morphToMesh = profile.morphToMesh || {};
109
+ if (profile.visemeMeshCategory) return profile.visemeMeshCategory;
110
+ if (hasOwn(morphToMesh, "viseme")) return "viseme";
111
+ return "face";
112
+ }
113
+ function getMeshNamesForVisemeProfile(profile) {
114
+ const morphToMesh = profile.morphToMesh || {};
115
+ const category = resolveVisemeMeshCategory(profile);
116
+ if (hasOwn(morphToMesh, category)) {
117
+ return Array.isArray(morphToMesh[category]) ? [...morphToMesh[category]] : [];
118
+ }
119
+ return profile.visemeMeshCategory ? [] : [...morphToMesh.face || []];
120
+ }
121
+ function getMeshNamesForAUProfile(profile, auId) {
122
+ const morphToMesh = profile.morphToMesh || {};
123
+ const facePart = profile.auInfo?.[String(auId)]?.facePart;
124
+ const category = facePart ? profile.auFacePartToMeshCategory?.[facePart] : void 0;
125
+ if (category) return Array.isArray(morphToMesh[category]) ? [...morphToMesh[category]] : [];
126
+ return [...morphToMesh.face || []];
127
+ }
128
+ function compileMatcher(pattern) {
129
+ try {
130
+ return new RegExp(pattern, "i");
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ function classifyVisemeMorph(morphName, profile) {
136
+ const slots = getProfileVisemeSlots(profile);
137
+ const matches = [];
138
+ for (const slot of slots) {
139
+ const explicitTargets = bindingTargets(profile.visemeBindings?.[slot.id]);
140
+ if (explicitTargets.some((target) => String(target).toLowerCase() === morphName.toLowerCase())) {
141
+ matches.push({
142
+ slotId: slot.id,
143
+ label: slot.label,
144
+ confidence: 1,
145
+ reason: "explicit"
146
+ });
147
+ continue;
148
+ }
149
+ for (const pattern of slot.matchers || []) {
150
+ const matcher = compileMatcher(pattern);
151
+ if (matcher?.test(morphName)) {
152
+ matches.push({
153
+ slotId: slot.id,
154
+ label: slot.label,
155
+ confidence: 0.75,
156
+ reason: "regex",
157
+ pattern
158
+ });
159
+ }
160
+ }
161
+ }
162
+ return matches;
163
+ }
164
+ function buildMappingEditorModel(profile, morphNames = []) {
165
+ const sections = /* @__PURE__ */ new Map();
166
+ const configuredSections = profile.mappingSections || [];
167
+ const configuredById = new Map(configuredSections.map((section) => [section.id, section]));
168
+ let nextOrder = configuredSections.length;
169
+ for (const section of configuredSections) {
170
+ sections.set(section.id, { ...section });
171
+ }
172
+ const getOrder = (id, fallback) => {
173
+ const configured = configuredById.get(id);
174
+ if (configured) return configured.order;
175
+ if (fallback !== void 0) return fallback;
176
+ const order = nextOrder;
177
+ nextOrder += 1;
178
+ return order;
179
+ };
180
+ const auSectionOrders = /* @__PURE__ */ new Map();
181
+ for (const [auId, info] of Object.entries(profile.auInfo || {})) {
182
+ const label = info.facePart || "Unmapped";
183
+ auSectionOrders.set(label, Math.min(auSectionOrders.get(label) ?? Number.MAX_SAFE_INTEGER, Number(auId)));
184
+ }
185
+ if (configuredSections.length === 0 && auSectionOrders.size > 0) {
186
+ nextOrder = Math.max(...auSectionOrders.values()) + 1;
187
+ }
188
+ for (const info of Object.values(profile.auInfo || {})) {
189
+ const label = info.facePart || "Unmapped";
190
+ const configured = configuredById.get(label);
191
+ const meshCategory = profile.auFacePartToMeshCategory?.[label] || "face";
192
+ sections.set(label, {
193
+ ...configured,
194
+ id: label,
195
+ label: configured?.label || label,
196
+ kind: "au",
197
+ order: getOrder(label, auSectionOrders.get(label)),
198
+ meshCategory: configured?.meshCategory || meshCategory,
199
+ facePart: label
200
+ });
201
+ }
202
+ const configuredVisemes = configuredById.get("Visemes");
203
+ sections.set("Visemes", {
204
+ ...configuredVisemes,
205
+ id: "Visemes",
206
+ label: configuredVisemes?.label || "Visemes",
207
+ kind: "viseme",
208
+ order: getOrder("Visemes"),
209
+ meshCategory: configuredVisemes?.meshCategory || resolveVisemeMeshCategory(profile)
210
+ });
211
+ if (hasOwn(profile.morphToMesh, "hair")) {
212
+ const configuredHair = configuredById.get("Hair");
213
+ sections.set("Hair", {
214
+ ...configuredHair,
215
+ id: "Hair",
216
+ label: configuredHair?.label || "Hair",
217
+ kind: "hair",
218
+ order: getOrder("Hair"),
219
+ meshCategory: configuredHair?.meshCategory || "hair"
220
+ });
221
+ }
222
+ const configuredUnmapped = configuredById.get("Unmapped");
223
+ sections.set("Unmapped", {
224
+ ...configuredUnmapped,
225
+ id: "Unmapped",
226
+ label: configuredUnmapped?.label || "Unmapped",
227
+ kind: "unmapped",
228
+ order: getOrder("Unmapped"),
229
+ meshCategory: configuredUnmapped?.meshCategory || "face"
230
+ });
231
+ const candidates = morphNames.map((morph) => {
232
+ const matches = classifyVisemeMorph(morph, profile);
233
+ if (matches.length === 0) {
234
+ return { morph, sectionId: "Unmapped", kind: "unmapped", matches };
235
+ }
236
+ const explicit = matches.filter((match) => match.reason === "explicit");
237
+ if (explicit.length > 0) {
238
+ return { morph, sectionId: "Visemes", kind: "explicit", matches: explicit };
239
+ }
240
+ return {
241
+ morph,
242
+ sectionId: "Visemes",
243
+ kind: matches.length > 1 ? "conflict" : "candidate",
244
+ matches
245
+ };
246
+ });
247
+ return {
248
+ sections: Array.from(sections.values()).sort((a, b) => a.order - b.order || a.label.localeCompare(b.label)),
249
+ candidates
250
+ };
251
+ }
252
+ function mapProviderVisemeToSlot(profile, event) {
253
+ const slots = getProfileVisemeSlots(profile);
254
+ const provider = event.provider.toLowerCase();
255
+ if (event.id !== void 0) {
256
+ const id = String(event.id);
257
+ const index = slots.findIndex(
258
+ (slot) => (slot.providerIds?.[provider] || []).some((candidate) => String(candidate) === id)
259
+ );
260
+ if (index >= 0) {
261
+ return { slotId: slots[index].id, index, confidence: 1, reason: "provider" };
262
+ }
263
+ }
264
+ if (event.phoneme) {
265
+ const phoneme = event.phoneme.toLowerCase();
266
+ const index = slots.findIndex(
267
+ (slot) => (slot.phonemes || []).some((candidate) => candidate.toLowerCase() === phoneme)
268
+ );
269
+ if (index >= 0) {
270
+ return { slotId: slots[index].id, index, confidence: 0.8, reason: "phoneme" };
271
+ }
272
+ }
273
+ const restIndex = slots.findIndex((slot) => slot.id === "rest" || slot.features?.lipClosed === 1);
274
+ if (restIndex >= 0) {
275
+ return { slotId: slots[restIndex].id, index: restIndex, confidence: 0.25, reason: "rest" };
276
+ }
277
+ return null;
278
+ }
279
+
63
280
  // src/engines/three/balanceUtils.ts
64
281
  function clampBalance(value) {
65
282
  if (!Number.isFinite(value)) return 0;
@@ -670,12 +887,7 @@ var BakedAnimationController = class {
670
887
  if (typeof this.host.getMeshNamesForAU === "function") {
671
888
  return this.host.getMeshNamesForAU(auId) || [];
672
889
  }
673
- const facePart = config.auInfo?.[String(auId)]?.facePart;
674
- if (facePart) {
675
- const category = config.auFacePartToMeshCategory?.[facePart];
676
- if (category) return config.morphToMesh?.[category] || [];
677
- }
678
- return config.morphToMesh?.face || [];
890
+ return getMeshNamesForAUProfile(config, auId);
679
891
  }
680
892
  getMeshNamesForViseme(config, explicitMeshNames) {
681
893
  if (explicitMeshNames && explicitMeshNames.length > 0) {
@@ -684,10 +896,7 @@ var BakedAnimationController = class {
684
896
  if (typeof this.host.getMeshNamesForViseme === "function") {
685
897
  return this.host.getMeshNamesForViseme() || [];
686
898
  }
687
- const category = config.visemeMeshCategory || (config.morphToMesh?.viseme ? "viseme" : "face");
688
- const visemeMeshes = config.morphToMesh?.[category];
689
- if (visemeMeshes && visemeMeshes.length > 0) return visemeMeshes;
690
- return config.morphToMesh?.face || [];
899
+ return getMeshNamesForVisemeProfile(config);
691
900
  }
692
901
  update(dtSeconds) {
693
902
  if (this.animationMixer) {
@@ -1740,7 +1949,7 @@ var BakedAnimationController = class {
1740
1949
  }
1741
1950
  addMorphTracks(tracks, morphKey, keyframes, intensityScale, meshNames) {
1742
1951
  const config = this.host.getConfig();
1743
- const hasExplicitMeshes = !!(meshNames && meshNames.length > 0);
1952
+ const hasExplicitMeshes = meshNames !== void 0;
1744
1953
  const targetMeshNames = hasExplicitMeshes ? meshNames : config.morphToMesh?.face || [];
1745
1954
  const targetMeshes = targetMeshNames.length ? targetMeshNames.map((name) => this.host.getMeshByName(name)).filter(Boolean) : [];
1746
1955
  const addTrackForMesh = (mesh) => {
@@ -1764,7 +1973,7 @@ var BakedAnimationController = class {
1764
1973
  addMorphIndexTracks(tracks, morphIndex, keyframes, intensityScale, meshNames) {
1765
1974
  if (!Number.isInteger(morphIndex) || morphIndex < 0) return;
1766
1975
  const config = this.host.getConfig();
1767
- const hasExplicitMeshes = !!(meshNames && meshNames.length > 0);
1976
+ const hasExplicitMeshes = meshNames !== void 0;
1768
1977
  const targetMeshNames = hasExplicitMeshes ? meshNames : config.morphToMesh?.face || [];
1769
1978
  const targetMeshes = targetMeshNames.length ? targetMeshNames.map((name) => this.host.getMeshByName(name)).filter(Boolean) : [];
1770
1979
  const addTrackForMesh = (mesh) => {
@@ -2398,6 +2607,159 @@ var VISEME_JAW_AMOUNTS = [
2398
2607
  0.5
2399
2608
  // 14: W_OO
2400
2609
  ];
2610
+ var CC4_VISEME_SYSTEM_ID = "cc4-arkit-15";
2611
+ var CC4_VISEME_SLOTS = [
2612
+ {
2613
+ id: "ae",
2614
+ label: "AE",
2615
+ order: 0,
2616
+ providerIds: { azure: [4], sapi: [4] },
2617
+ phonemes: ["AE", "EH", "EY", "UH"],
2618
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ae|eh|ey)([_ .-]|$)"],
2619
+ features: { jawOpen: 0.75, lipSpread: 0.35 },
2620
+ defaultJawAmount: VISEME_JAW_AMOUNTS[0]
2621
+ },
2622
+ {
2623
+ id: "ah",
2624
+ label: "Ah",
2625
+ order: 1,
2626
+ providerIds: { azure: [1, 2, 9, 11, 12], sapi: [1, 2, 9, 11, 12] },
2627
+ phonemes: ["AA", "AE", "AH", "AX", "AW", "AY", "HH"],
2628
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ah|aa|aah|open)([_ .-]|$)"],
2629
+ features: { jawOpen: 0.8 },
2630
+ defaultJawAmount: VISEME_JAW_AMOUNTS[1]
2631
+ },
2632
+ {
2633
+ id: "b-m-p",
2634
+ label: "B_M_P",
2635
+ order: 2,
2636
+ providerIds: { azure: [0, 21], sapi: [0, 21] },
2637
+ phonemes: ["B", "M", "P"],
2638
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(b[_ .-]?m[_ .-]?p|bmp|closed|sil|rest)([_ .-]|$)"],
2639
+ features: { jawOpen: 0, lipClosed: 1 },
2640
+ defaultJawAmount: VISEME_JAW_AMOUNTS[2]
2641
+ },
2642
+ {
2643
+ id: "ch-j",
2644
+ label: "Ch_J",
2645
+ order: 3,
2646
+ providerIds: { azure: [16], sapi: [16] },
2647
+ phonemes: ["CH", "JH", "SH", "ZH"],
2648
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ch|j|sh|zh)([_ .-]|$)"],
2649
+ features: { jawOpen: 0.3, fricative: 0.6 },
2650
+ defaultJawAmount: VISEME_JAW_AMOUNTS[3]
2651
+ },
2652
+ {
2653
+ id: "ee",
2654
+ label: "EE",
2655
+ order: 4,
2656
+ providerIds: { azure: [6], sapi: [6] },
2657
+ phonemes: ["IY", "IH", "IX", "Y"],
2658
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ee|iy|y)([_ .-]|$)"],
2659
+ features: { jawOpen: 0.2, lipSpread: 0.8 },
2660
+ defaultJawAmount: VISEME_JAW_AMOUNTS[4]
2661
+ },
2662
+ {
2663
+ id: "er",
2664
+ label: "Er",
2665
+ order: 5,
2666
+ providerIds: { azure: [5], sapi: [5] },
2667
+ phonemes: ["ER"],
2668
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(er)([_ .-]|$)"],
2669
+ features: { jawOpen: 0.35, lipRound: 0.35 },
2670
+ defaultJawAmount: VISEME_JAW_AMOUNTS[5]
2671
+ },
2672
+ {
2673
+ id: "f-v",
2674
+ label: "F_V",
2675
+ order: 6,
2676
+ providerIds: { azure: [18], sapi: [18] },
2677
+ phonemes: ["F", "V"],
2678
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(f[_ .-]?v|fv)([_ .-]|$)"],
2679
+ features: { jawOpen: 0.1, fricative: 1 },
2680
+ defaultJawAmount: VISEME_JAW_AMOUNTS[6]
2681
+ },
2682
+ {
2683
+ id: "ih",
2684
+ label: "Ih",
2685
+ order: 7,
2686
+ providerIds: { azure: [6], sapi: [6] },
2687
+ phonemes: ["IH", "IX"],
2688
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ih|ix)([_ .-]|$)"],
2689
+ features: { jawOpen: 0.2, lipSpread: 0.55 },
2690
+ defaultJawAmount: VISEME_JAW_AMOUNTS[7]
2691
+ },
2692
+ {
2693
+ id: "k-g-h-ng",
2694
+ label: "K_G_H_NG",
2695
+ order: 8,
2696
+ providerIds: { azure: [20], sapi: [20] },
2697
+ phonemes: ["K", "G", "NG"],
2698
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(k[_ .-]?g[_ .-]?h?[_ .-]?ng|kg|ng)([_ .-]|$)"],
2699
+ features: { jawOpen: 0.35 },
2700
+ defaultJawAmount: VISEME_JAW_AMOUNTS[8]
2701
+ },
2702
+ {
2703
+ id: "oh",
2704
+ label: "Oh",
2705
+ order: 9,
2706
+ providerIds: { azure: [3, 8, 10], sapi: [3, 8, 10] },
2707
+ phonemes: ["AO", "OW", "OY"],
2708
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(oh|ao|ow|oy)([_ .-]|$)"],
2709
+ features: { jawOpen: 0.6, lipRound: 0.8 },
2710
+ defaultJawAmount: VISEME_JAW_AMOUNTS[9]
2711
+ },
2712
+ {
2713
+ id: "r",
2714
+ label: "R",
2715
+ order: 10,
2716
+ providerIds: { azure: [13], sapi: [13] },
2717
+ phonemes: ["R"],
2718
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(r)([_ .-]|$)"],
2719
+ features: { jawOpen: 0.35, lipRound: 0.5 },
2720
+ defaultJawAmount: VISEME_JAW_AMOUNTS[10]
2721
+ },
2722
+ {
2723
+ id: "s-z",
2724
+ label: "S_Z",
2725
+ order: 11,
2726
+ providerIds: { azure: [15], sapi: [15] },
2727
+ phonemes: ["S", "Z"],
2728
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(s[_ .-]?z|sz)([_ .-]|$)"],
2729
+ features: { jawOpen: 0.1, fricative: 1 },
2730
+ defaultJawAmount: VISEME_JAW_AMOUNTS[11]
2731
+ },
2732
+ {
2733
+ id: "t-l-d-n",
2734
+ label: "T_L_D_N",
2735
+ order: 12,
2736
+ providerIds: { azure: [14, 19], sapi: [14, 19] },
2737
+ phonemes: ["T", "L", "D", "N"],
2738
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(t[_ .-]?l[_ .-]?d[_ .-]?n|tldn|l)([_ .-]|$)"],
2739
+ features: { jawOpen: 0.3, tongueTip: 1 },
2740
+ defaultJawAmount: VISEME_JAW_AMOUNTS[12]
2741
+ },
2742
+ {
2743
+ id: "th",
2744
+ label: "Th",
2745
+ order: 13,
2746
+ providerIds: { azure: [17], sapi: [17] },
2747
+ phonemes: ["TH", "DH"],
2748
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(th|dh)([_ .-]|$)"],
2749
+ features: { jawOpen: 0.15, tongueTip: 0.8, fricative: 0.8 },
2750
+ defaultJawAmount: VISEME_JAW_AMOUNTS[13]
2751
+ },
2752
+ {
2753
+ id: "w-oo",
2754
+ label: "W_OO",
2755
+ order: 14,
2756
+ providerIds: { azure: [7], sapi: [7] },
2757
+ phonemes: ["W", "UW"],
2758
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(w[_ .-]?oo|woo|uw|oo)([_ .-]|$)"],
2759
+ features: { jawOpen: 0.5, lipRound: 1 },
2760
+ defaultJawAmount: VISEME_JAW_AMOUNTS[14]
2761
+ }
2762
+ ];
2401
2763
  var isMixedAU = (id) => {
2402
2764
  const morphs = AU_TO_MORPHS[id];
2403
2765
  const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
@@ -2709,6 +3071,23 @@ var AU_FACEPART_TO_MESH_CATEGORY = {
2709
3071
  Eyelids: "eye",
2710
3072
  Tongue: "tongue"
2711
3073
  };
3074
+ var CC4_MAPPING_SECTIONS = [
3075
+ { id: "Forehead", label: "Forehead", kind: "au", order: 0, meshCategory: "face", facePart: "Forehead" },
3076
+ { id: "Eyelids", label: "Eyelids", kind: "au", order: 1, meshCategory: "eye", facePart: "Eyelids" },
3077
+ { id: "Eyes", label: "Eyes", kind: "au", order: 2, meshCategory: "eye", facePart: "Eyes" },
3078
+ { id: "Cheeks", label: "Cheeks", kind: "au", order: 3, meshCategory: "face", facePart: "Cheeks" },
3079
+ { id: "Nose", label: "Nose", kind: "au", order: 4, meshCategory: "face", facePart: "Nose" },
3080
+ { id: "Mouth", label: "Mouth", kind: "au", order: 5, meshCategory: "face", facePart: "Mouth" },
3081
+ { id: "Chin", label: "Chin", kind: "au", order: 6, meshCategory: "face", facePart: "Chin" },
3082
+ { id: "Jaw", label: "Jaw", kind: "au", order: 7, meshCategory: "face", facePart: "Jaw" },
3083
+ { id: "Tongue", label: "Tongue", kind: "au", order: 8, meshCategory: "tongue", facePart: "Tongue" },
3084
+ { id: "Head", label: "Head", kind: "au", order: 9, meshCategory: "face", facePart: "Head" },
3085
+ { id: "Joint Controls", label: "Joint Controls", kind: "au", order: 10, meshCategory: "face", facePart: "Joint Controls" },
3086
+ { id: "Eye", label: "Eye", kind: "au", order: 11, meshCategory: "eye", facePart: "Eye" },
3087
+ { id: "Hair", label: "Hair", kind: "hair", order: 12, meshCategory: "hair" },
3088
+ { id: "Visemes", label: "Visemes", kind: "viseme", order: 13, meshCategory: "viseme" },
3089
+ { id: "Unmapped", label: "Unmapped", kind: "unmapped", order: 14, meshCategory: "face" }
3090
+ ];
2712
3091
  var CC4_HAIR_PHYSICS = {
2713
3092
  stiffness: 7.5,
2714
3093
  damping: 0.18,
@@ -2755,7 +3134,10 @@ var CC4_PRESET = {
2755
3134
  suffixPattern: CC4_SUFFIX_PATTERN,
2756
3135
  morphToMesh: MORPH_TO_MESH,
2757
3136
  auFacePartToMeshCategory: AU_FACEPART_TO_MESH_CATEGORY,
3137
+ mappingSections: CC4_MAPPING_SECTIONS,
2758
3138
  visemeKeys: VISEME_KEYS,
3139
+ visemeSystemId: CC4_VISEME_SYSTEM_ID,
3140
+ visemeSlots: CC4_VISEME_SLOTS,
2759
3141
  visemeMeshCategory: "viseme",
2760
3142
  visemeJawAmounts: VISEME_JAW_AMOUNTS,
2761
3143
  auMixDefaults: AU_MIX_DEFAULTS,
@@ -3642,8 +4024,13 @@ function extendPresetWithProfile(base, extension) {
3642
4024
  boneNodes: mergeRecord(base.boneNodes, extension.boneNodes),
3643
4025
  morphToMesh: mergeRecord(base.morphToMesh, extension.morphToMesh),
3644
4026
  auFacePartToMeshCategory: base.auFacePartToMeshCategory || extension.auFacePartToMeshCategory ? mergeRecord(base.auFacePartToMeshCategory || {}, extension.auFacePartToMeshCategory || {}) : void 0,
4027
+ mappingSections: extension.mappingSections ? [...extension.mappingSections] : base.mappingSections ? [...base.mappingSections] : void 0,
3645
4028
  visemeKeys: extension.visemeKeys ? [...extension.visemeKeys] : [...base.visemeKeys],
4029
+ visemeSystemId: extension.visemeSystemId ?? base.visemeSystemId,
4030
+ visemeSlots: extension.visemeSlots ? [...extension.visemeSlots] : base.visemeSlots ? [...base.visemeSlots] : void 0,
4031
+ visemeBindings: base.visemeBindings || extension.visemeBindings ? mergeRecord(base.visemeBindings || {}, extension.visemeBindings || {}) : void 0,
3646
4032
  visemeMeshCategory: extension.visemeMeshCategory ?? base.visemeMeshCategory,
4033
+ visemeJawAmounts: extension.visemeJawAmounts ? [...extension.visemeJawAmounts] : base.visemeJawAmounts ? [...base.visemeJawAmounts] : void 0,
3647
4034
  auMixDefaults: base.auMixDefaults || extension.auMixDefaults ? mergeRecord(base.auMixDefaults || {}, extension.auMixDefaults || {}) : void 0,
3648
4035
  auInfo: base.auInfo || extension.auInfo ? mergeRecord(base.auInfo || {}, extension.auInfo || {}) : void 0,
3649
4036
  eyeMeshNodes: extension.eyeMeshNodes ?? base.eyeMeshNodes,
@@ -5209,6 +5596,21 @@ var _Loom3 = class _Loom3 {
5209
5596
  const jawHandle = this.transitionBoneRotation("JAW", "pitch", jawAmount, durationMs);
5210
5597
  return this.combineHandles([morphHandle, jawHandle]);
5211
5598
  }
5599
+ setVisemeById(slotId, value, jawScale = 1) {
5600
+ const index = getVisemeSlotIndex(this.config, slotId);
5601
+ if (index < 0) return;
5602
+ this.setViseme(index, value, jawScale);
5603
+ }
5604
+ transitionVisemeById(slotId, to, durationMs = 80, jawScale = 1) {
5605
+ const index = getVisemeSlotIndex(this.config, slotId);
5606
+ if (index < 0) {
5607
+ return { promise: Promise.resolve(), pause: () => {
5608
+ }, resume: () => {
5609
+ }, cancel: () => {
5610
+ } };
5611
+ }
5612
+ return this.transitionViseme(index, to, durationMs, jawScale);
5613
+ }
5212
5614
  // ============================================================================
5213
5615
  // MIX WEIGHT CONTROL
5214
5616
  // ============================================================================
@@ -5554,21 +5956,10 @@ var _Loom3 = class _Loom3 {
5554
5956
  * Routing is driven by `auFacePartToMeshCategory` in profile config.
5555
5957
  */
5556
5958
  getMeshNamesForAU(auId) {
5557
- const m = this.config.morphToMesh;
5558
- const info = this.config.auInfo?.[String(auId)];
5559
- const facePart = info?.facePart;
5560
- if (facePart) {
5561
- const category = this.config.auFacePartToMeshCategory?.[facePart];
5562
- if (category) {
5563
- return m?.[category] || [];
5564
- }
5565
- }
5566
- return m?.face || [];
5959
+ return getMeshNamesForAUProfile(this.config, auId);
5567
5960
  }
5568
5961
  getMeshNamesForViseme() {
5569
- const m = this.config.morphToMesh;
5570
- const category = this.config.visemeMeshCategory || (m?.viseme ? "viseme" : "face");
5571
- return m?.[category] || m?.face || [];
5962
+ return getMeshNamesForVisemeProfile(this.config);
5572
5963
  }
5573
5964
  // ============================================================================
5574
5965
  // HAIR PHYSICS
@@ -5836,7 +6227,7 @@ var _Loom3 = class _Loom3 {
5836
6227
  );
5837
6228
  }
5838
6229
  getVisemeJawAmount(visemeIndex) {
5839
- return this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
6230
+ return getVisemeJawAmounts(this.config)?.[visemeIndex] ?? this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
5840
6231
  }
5841
6232
  collectResolvedExpressionMorphTargets() {
5842
6233
  const targets = [];
@@ -6314,7 +6705,11 @@ var PROFILE_OVERRIDE_KEYS = [
6314
6705
  "rightMorphSuffixes",
6315
6706
  "morphToMesh",
6316
6707
  "auFacePartToMeshCategory",
6708
+ "mappingSections",
6317
6709
  "visemeKeys",
6710
+ "visemeSystemId",
6711
+ "visemeSlots",
6712
+ "visemeBindings",
6318
6713
  "visemeMeshCategory",
6319
6714
  "visemeJawAmounts",
6320
6715
  "auMixDefaults",
@@ -7414,6 +7809,33 @@ function validateMappingConfig(config) {
7414
7809
  }
7415
7810
  visemeSeen.add(key);
7416
7811
  }
7812
+ const morphCategories = new Set(Object.keys(config.morphToMesh || {}));
7813
+ if (config.visemeMeshCategory && !morphCategories.has(config.visemeMeshCategory)) {
7814
+ push(
7815
+ "error",
7816
+ "VISEME_MESH_CATEGORY_MISSING",
7817
+ `visemeMeshCategory "${config.visemeMeshCategory}" is not present in morphToMesh`,
7818
+ { category: config.visemeMeshCategory }
7819
+ );
7820
+ }
7821
+ for (const [facePart, category] of Object.entries(config.auFacePartToMeshCategory || {})) {
7822
+ if (!morphCategories.has(category)) {
7823
+ push(
7824
+ "error",
7825
+ "AU_MESH_CATEGORY_MISSING",
7826
+ `AU facePart "${facePart}" routes to missing morphToMesh category "${category}"`,
7827
+ { facePart, category }
7828
+ );
7829
+ }
7830
+ }
7831
+ if (config.visemeJawAmounts && config.visemeJawAmounts.length !== (config.visemeKeys || []).length) {
7832
+ push(
7833
+ "warning",
7834
+ "VISEME_JAW_AMOUNT_LENGTH_MISMATCH",
7835
+ "visemeJawAmounts length does not match visemeKeys length",
7836
+ { visemeKeys: (config.visemeKeys || []).length, visemeJawAmounts: config.visemeJawAmounts.length }
7837
+ );
7838
+ }
7417
7839
  if (config.auMixDefaults) {
7418
7840
  for (const key of Object.keys(config.auMixDefaults)) {
7419
7841
  const auId = Number(key);
@@ -7878,9 +8300,12 @@ exports.BONE_AU_TO_BINDINGS = BONE_AU_TO_BINDINGS;
7878
8300
  exports.CC4_BONE_NODES = CC4_BONE_NODES;
7879
8301
  exports.CC4_BONE_PREFIX = CC4_BONE_PREFIX;
7880
8302
  exports.CC4_EYE_MESH_NODES = CC4_EYE_MESH_NODES;
8303
+ exports.CC4_MAPPING_SECTIONS = CC4_MAPPING_SECTIONS;
7881
8304
  exports.CC4_MESHES = CC4_MESHES;
7882
8305
  exports.CC4_PRESET = CC4_PRESET;
7883
8306
  exports.CC4_SUFFIX_PATTERN = CC4_SUFFIX_PATTERN;
8307
+ exports.CC4_VISEME_SLOTS = CC4_VISEME_SLOTS;
8308
+ exports.CC4_VISEME_SYSTEM_ID = CC4_VISEME_SYSTEM_ID;
7884
8309
  exports.COMPOSITE_ROTATIONS = COMPOSITE_ROTATIONS;
7885
8310
  exports.CONTINUUM_LABELS = CONTINUUM_LABELS;
7886
8311
  exports.CONTINUUM_PAIRS_MAP = CONTINUUM_PAIRS_MAP;
@@ -7895,7 +8320,9 @@ exports.VISEME_JAW_AMOUNTS = VISEME_JAW_AMOUNTS;
7895
8320
  exports.VISEME_KEYS = VISEME_KEYS;
7896
8321
  exports.analyzeModel = analyzeModel;
7897
8322
  exports.applyCharacterProfileToPreset = applyCharacterProfileToPreset;
8323
+ exports.buildMappingEditorModel = buildMappingEditorModel;
7898
8324
  exports.collectMorphMeshes = collectMorphMeshes;
8325
+ exports.compileVisemeKeys = compileVisemeKeys;
7899
8326
  exports.computeCameraRelativeGazeOffset = computeCameraRelativeGazeOffset;
7900
8327
  exports.detectFacingDirection = detectFacingDirection;
7901
8328
  exports.extendCharacterConfigWithPreset = extendCharacterConfigWithPreset;
@@ -7906,18 +8333,25 @@ exports.extractProfileOverrides = extractProfileOverrides;
7906
8333
  exports.findFaceCenter = findFaceCenter;
7907
8334
  exports.fuzzyNameMatch = fuzzyNameMatch;
7908
8335
  exports.generateMappingCorrections = generateMappingCorrections;
8336
+ exports.getMeshNamesForAUProfile = getMeshNamesForAUProfile;
8337
+ exports.getMeshNamesForVisemeProfile = getMeshNamesForVisemeProfile;
7909
8338
  exports.getModelForwardDirection = getModelForwardDirection;
7910
8339
  exports.getPreset = getPreset;
7911
8340
  exports.getPresetWithProfile = getPresetWithProfile;
8341
+ exports.getProfileVisemeSlots = getProfileVisemeSlots;
8342
+ exports.getVisemeJawAmounts = getVisemeJawAmounts;
8343
+ exports.getVisemeSlotIndex = getVisemeSlotIndex;
7912
8344
  exports.hasLeftRightMorphs = hasLeftRightMorphs;
7913
8345
  exports.isMixedAU = isMixedAU;
7914
8346
  exports.isPresetCompatible = isPresetCompatible;
8347
+ exports.mapProviderVisemeToSlot = mapProviderVisemeToSlot;
7915
8348
  exports.mergeCharacterRegionsByName = mergeRegionsByName;
7916
8349
  exports.resolveBoneName = resolveBoneName;
7917
8350
  exports.resolveBoneNames = resolveBoneNames;
7918
8351
  exports.resolveFaceCenter = resolveFaceCenter;
7919
8352
  exports.resolvePreset = resolvePreset;
7920
8353
  exports.resolvePresetWithOverrides = resolvePresetWithOverrides;
8354
+ exports.resolveVisemeMeshCategory = resolveVisemeMeshCategory;
7921
8355
  exports.suggestBestPreset = suggestBestPreset;
7922
8356
  exports.validateMappingConfig = validateMappingConfig;
7923
8357
  exports.validateMappings = validateMappings;