@lovelace_lol/loom3 1.0.40 → 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,
@@ -3573,7 +3955,7 @@ var mergeAnnotationRegion = (base, override) => {
3573
3955
  merged.meshes = override.meshes ? [...override.meshes] : base.meshes ? [...base.meshes] : void 0;
3574
3956
  merged.objects = override.objects ? [...override.objects] : base.objects ? [...base.objects] : void 0;
3575
3957
  merged.children = override.children ? [...override.children] : base.children ? [...base.children] : void 0;
3576
- merged.cameraOffset = override.cameraOffset ? { ...override.cameraOffset } : base.cameraOffset ? { ...base.cameraOffset } : void 0;
3958
+ merged.cameraOffset = override.cameraOffset ? { ...base.cameraOffset, ...override.cameraOffset } : base.cameraOffset ? { ...base.cameraOffset } : void 0;
3577
3959
  merged.style = override.style ? {
3578
3960
  ...base.style,
3579
3961
  ...override.style,
@@ -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",
@@ -6412,22 +6807,32 @@ function mergeRegionsByName(base, override) {
6412
6807
  }
6413
6808
  return Array.from(merged.values());
6414
6809
  }
6810
+ function getAnnotationRegions(value) {
6811
+ return Array.isArray(value) ? value : void 0;
6812
+ }
6813
+ function getLegacyNestedOverrides(config) {
6814
+ return isPlainObject2(config.profile) ? config.profile : {};
6815
+ }
6816
+ function getLegacyRuntimeRegions(config) {
6817
+ return Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0;
6818
+ }
6819
+ function getCanonicalAnnotationOverrides(config) {
6820
+ return mergeRegionsByName(
6821
+ getAnnotationRegions(getLegacyNestedOverrides(config).annotationRegions),
6822
+ getAnnotationRegions(config.annotationRegions)
6823
+ );
6824
+ }
6415
6825
  function extractProfileOverrides(config) {
6416
6826
  const topLevelConfig = config;
6417
- const legacyNestedOverrides = isPlainObject2(config.profile) ? config.profile : {};
6827
+ const legacyNestedOverrides = getLegacyNestedOverrides(config);
6828
+ const canonicalAnnotationOverrides = getCanonicalAnnotationOverrides(config);
6829
+ const legacyRuntimeRegions = getLegacyRuntimeRegions(config);
6830
+ const annotationOverrides = canonicalAnnotationOverrides ?? (legacyRuntimeRegions ? legacyRuntimeRegions.map((region) => cloneRegion(region)) : void 0);
6418
6831
  const overrides = {};
6419
6832
  for (const key of PROFILE_OVERRIDE_KEYS) {
6420
6833
  if (key === "annotationRegions") {
6421
- const topLevelAnnotationRegions = Array.isArray(topLevelConfig.annotationRegions) ? topLevelConfig.annotationRegions : void 0;
6422
- const legacyAnnotationRegions = Array.isArray(legacyNestedOverrides.annotationRegions) ? legacyNestedOverrides.annotationRegions : void 0;
6423
- const legacyRegionFallback = Array.isArray(config.regions) && config.regions.length > 0 ? config.regions : void 0;
6424
- const legacyProfileRegions = mergeRegionsByName(legacyRegionFallback, legacyAnnotationRegions);
6425
- const regions = mergeRegionsByName(
6426
- legacyProfileRegions,
6427
- topLevelAnnotationRegions
6428
- );
6429
- if (regions) {
6430
- overrides.annotationRegions = regions.map((region) => cloneRegion(region));
6834
+ if (annotationOverrides) {
6835
+ overrides.annotationRegions = annotationOverrides.map((region) => cloneRegion(region));
6431
6836
  }
6432
6837
  continue;
6433
6838
  }
@@ -6440,13 +6845,6 @@ function extractProfileOverrides(config) {
6440
6845
  }
6441
6846
  return overrides;
6442
6847
  }
6443
- function hasCanonicalAnnotationRegionOverrides(config) {
6444
- const topLevelConfig = config;
6445
- if (Array.isArray(topLevelConfig.annotationRegions)) {
6446
- return true;
6447
- }
6448
- return isPlainObject2(config.profile) && Array.isArray(config.profile.annotationRegions);
6449
- }
6450
6848
  function applyCharacterProfileToPreset(config) {
6451
6849
  const presetType = config.auPresetType;
6452
6850
  if (!presetType) {
@@ -6478,24 +6876,34 @@ function extendCharacterConfigWithPreset(config) {
6478
6876
  if (!presetType || presetType === "custom") {
6479
6877
  return config;
6480
6878
  }
6879
+ const canonicalAnnotationOverrides = getCanonicalAnnotationOverrides(config);
6880
+ const legacyRuntimeRegions = getLegacyRuntimeRegions(config);
6481
6881
  const profileOverrides = extractProfileOverrides(config);
6482
6882
  const extendedPresetProfile = applyCharacterProfileToPreset(config);
6483
6883
  if (!extendedPresetProfile) {
6484
6884
  return config;
6485
6885
  }
6486
- const presetRegions = extendedPresetProfile.annotationRegions;
6487
- const mergedRegions = hasCanonicalAnnotationRegionOverrides(config) ? mergeRegionsByName(config.regions, presetRegions) : mergeRegionsByName(presetRegions, config.regions);
6488
- const normalizedRegions = normalizeRegionTree(
6489
- mergedRegions,
6886
+ const presetRegionNames = new Set(
6887
+ (getPreset(presetType).annotationRegions ?? []).map((region) => region.name)
6888
+ );
6889
+ const extendedAnnotationRegions = normalizeRegionTree(
6890
+ extendedPresetProfile.annotationRegions,
6891
+ profileOverrides.disabledRegions
6892
+ );
6893
+ const extendedRegionNames = new Set((extendedAnnotationRegions ?? []).map((region) => region.name));
6894
+ const legacyExtraRegions = canonicalAnnotationOverrides && legacyRuntimeRegions ? legacyRuntimeRegions.filter((region) => !presetRegionNames.has(region.name) && !extendedRegionNames.has(region.name)).map((region) => cloneRegion(region)) : void 0;
6895
+ const mergedRegions = normalizeRegionTree(
6896
+ mergeRegionsByName(extendedAnnotationRegions, legacyExtraRegions),
6490
6897
  profileOverrides.disabledRegions
6491
6898
  );
6492
6899
  const extendedRegions = orderExtendedRegions(
6493
- normalizedRegions,
6494
- [config.regions, profileOverrides.annotationRegions, presetRegions]
6900
+ mergedRegions,
6901
+ canonicalAnnotationOverrides ? [extendedAnnotationRegions, legacyExtraRegions] : [legacyRuntimeRegions, extendedAnnotationRegions]
6495
6902
  );
6496
6903
  return {
6497
6904
  ...config,
6498
6905
  ...extendedPresetProfile,
6906
+ annotationRegions: extendedRegions ?? extendedAnnotationRegions,
6499
6907
  regions: extendedRegions ?? config.regions
6500
6908
  };
6501
6909
  }
@@ -7401,6 +7809,33 @@ function validateMappingConfig(config) {
7401
7809
  }
7402
7810
  visemeSeen.add(key);
7403
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
+ }
7404
7839
  if (config.auMixDefaults) {
7405
7840
  for (const key of Object.keys(config.auMixDefaults)) {
7406
7841
  const auId = Number(key);
@@ -7865,9 +8300,12 @@ exports.BONE_AU_TO_BINDINGS = BONE_AU_TO_BINDINGS;
7865
8300
  exports.CC4_BONE_NODES = CC4_BONE_NODES;
7866
8301
  exports.CC4_BONE_PREFIX = CC4_BONE_PREFIX;
7867
8302
  exports.CC4_EYE_MESH_NODES = CC4_EYE_MESH_NODES;
8303
+ exports.CC4_MAPPING_SECTIONS = CC4_MAPPING_SECTIONS;
7868
8304
  exports.CC4_MESHES = CC4_MESHES;
7869
8305
  exports.CC4_PRESET = CC4_PRESET;
7870
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;
7871
8309
  exports.COMPOSITE_ROTATIONS = COMPOSITE_ROTATIONS;
7872
8310
  exports.CONTINUUM_LABELS = CONTINUUM_LABELS;
7873
8311
  exports.CONTINUUM_PAIRS_MAP = CONTINUUM_PAIRS_MAP;
@@ -7882,7 +8320,9 @@ exports.VISEME_JAW_AMOUNTS = VISEME_JAW_AMOUNTS;
7882
8320
  exports.VISEME_KEYS = VISEME_KEYS;
7883
8321
  exports.analyzeModel = analyzeModel;
7884
8322
  exports.applyCharacterProfileToPreset = applyCharacterProfileToPreset;
8323
+ exports.buildMappingEditorModel = buildMappingEditorModel;
7885
8324
  exports.collectMorphMeshes = collectMorphMeshes;
8325
+ exports.compileVisemeKeys = compileVisemeKeys;
7886
8326
  exports.computeCameraRelativeGazeOffset = computeCameraRelativeGazeOffset;
7887
8327
  exports.detectFacingDirection = detectFacingDirection;
7888
8328
  exports.extendCharacterConfigWithPreset = extendCharacterConfigWithPreset;
@@ -7893,18 +8333,25 @@ exports.extractProfileOverrides = extractProfileOverrides;
7893
8333
  exports.findFaceCenter = findFaceCenter;
7894
8334
  exports.fuzzyNameMatch = fuzzyNameMatch;
7895
8335
  exports.generateMappingCorrections = generateMappingCorrections;
8336
+ exports.getMeshNamesForAUProfile = getMeshNamesForAUProfile;
8337
+ exports.getMeshNamesForVisemeProfile = getMeshNamesForVisemeProfile;
7896
8338
  exports.getModelForwardDirection = getModelForwardDirection;
7897
8339
  exports.getPreset = getPreset;
7898
8340
  exports.getPresetWithProfile = getPresetWithProfile;
8341
+ exports.getProfileVisemeSlots = getProfileVisemeSlots;
8342
+ exports.getVisemeJawAmounts = getVisemeJawAmounts;
8343
+ exports.getVisemeSlotIndex = getVisemeSlotIndex;
7899
8344
  exports.hasLeftRightMorphs = hasLeftRightMorphs;
7900
8345
  exports.isMixedAU = isMixedAU;
7901
8346
  exports.isPresetCompatible = isPresetCompatible;
8347
+ exports.mapProviderVisemeToSlot = mapProviderVisemeToSlot;
7902
8348
  exports.mergeCharacterRegionsByName = mergeRegionsByName;
7903
8349
  exports.resolveBoneName = resolveBoneName;
7904
8350
  exports.resolveBoneNames = resolveBoneNames;
7905
8351
  exports.resolveFaceCenter = resolveFaceCenter;
7906
8352
  exports.resolvePreset = resolvePreset;
7907
8353
  exports.resolvePresetWithOverrides = resolvePresetWithOverrides;
8354
+ exports.resolveVisemeMeshCategory = resolveVisemeMeshCategory;
7908
8355
  exports.suggestBestPreset = suggestBestPreset;
7909
8356
  exports.validateMappingConfig = validateMappingConfig;
7910
8357
  exports.validateMappings = validateMappings;