@lovelace_lol/loom3 1.0.41 → 1.0.43

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
@@ -1,5 +1,5 @@
1
1
  import * as THREE2 from 'three';
2
- import { Vector3, Clock, Box3, BufferAttribute, Quaternion, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopPingPong, LoopOnce, LoopRepeat, QuaternionKeyframeTrack, NumberKeyframeTrack, AnimationClip, AnimationMixer, Mesh, PropertyBinding } from 'three';
2
+ import { Vector3, Clock, Box3, BufferAttribute, Quaternion, PropertyBinding, NumberKeyframeTrack, QuaternionKeyframeTrack, VectorKeyframeTrack, AnimationClip, AnimationUtils, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopPingPong, LoopOnce, LoopRepeat, AnimationMixer, Mesh } from 'three';
3
3
 
4
4
  var __defProp = Object.defineProperty;
5
5
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -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;
@@ -272,14 +489,19 @@ var Y_AXIS = new Vector3(0, 1, 0);
272
489
  var Z_AXIS = new Vector3(0, 0, 1);
273
490
  var CLIP_EVENT_METADATA_KEY = "__loom3ClipEvents";
274
491
  var CLIP_EVENT_EPSILON = 1e-4;
492
+ var ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX = "__loom3_additive_delta";
275
493
  var BakedAnimationController = class {
276
494
  constructor(host) {
277
495
  __publicField(this, "host");
496
+ // Clip-backed snippets need a later mixer pass so they can override baked additive tracks.
278
497
  __publicField(this, "animationMixer", null);
498
+ __publicField(this, "clipAnimationMixer", null);
279
499
  __publicField(this, "mixerFinishedListenerAttached", false);
500
+ __publicField(this, "clipMixerFinishedListenerAttached", false);
280
501
  __publicField(this, "animationClips", []);
281
502
  __publicField(this, "bakedSourceClips", /* @__PURE__ */ new Map());
282
503
  __publicField(this, "bakedRuntimeActions", /* @__PURE__ */ new Map());
504
+ __publicField(this, "bakedAdditiveRuntimeClips", /* @__PURE__ */ new Map());
283
505
  __publicField(this, "bakedActionGroups", /* @__PURE__ */ new Map());
284
506
  __publicField(this, "bakedRuntimeClipToSource", /* @__PURE__ */ new Map());
285
507
  __publicField(this, "animationActions", /* @__PURE__ */ new Map());
@@ -304,6 +526,173 @@ var BakedAnimationController = class {
304
526
  action.__actionId = actionId;
305
527
  return actionId;
306
528
  }
529
+ clearActionId(action) {
530
+ if (!action) return;
531
+ const actionId = this.getActionId(action);
532
+ if (actionId) {
533
+ this.actionIdToClip.delete(actionId);
534
+ }
535
+ this.actionIds.delete(action);
536
+ delete action.__actionId;
537
+ }
538
+ uncacheClip(clip, mixer = this.animationMixer) {
539
+ if (!clip || !mixer) return;
540
+ try {
541
+ mixer.uncacheClip(clip);
542
+ } catch {
543
+ }
544
+ }
545
+ uncacheAction(action, mixer = this.animationMixer) {
546
+ if (!action || !mixer) return;
547
+ try {
548
+ const clip = action.getClip();
549
+ if (clip) {
550
+ mixer.uncacheAction(clip);
551
+ mixer.uncacheClip(clip);
552
+ }
553
+ } catch {
554
+ }
555
+ }
556
+ releaseBakedRuntimeAction(runtimeClipName) {
557
+ const action = this.bakedRuntimeActions.get(runtimeClipName);
558
+ if (!action) return;
559
+ try {
560
+ action.stop();
561
+ } catch {
562
+ }
563
+ this.uncacheAction(action);
564
+ this.clearActionId(action);
565
+ this.bakedRuntimeActions.delete(runtimeClipName);
566
+ }
567
+ clearBakedAdditiveRuntimeClip(runtimeClipName) {
568
+ const clip = this.bakedAdditiveRuntimeClips.get(runtimeClipName);
569
+ if (!clip) return;
570
+ this.uncacheClip(clip);
571
+ this.bakedAdditiveRuntimeClips.delete(runtimeClipName);
572
+ }
573
+ clearAllBakedAdditiveRuntimeClips() {
574
+ for (const runtimeClipName of Array.from(this.bakedAdditiveRuntimeClips.keys())) {
575
+ this.clearBakedAdditiveRuntimeClip(runtimeClipName);
576
+ }
577
+ }
578
+ resolveTrackTarget(model, parsed) {
579
+ const targetKey = parsed.objectName === "bones" && parsed.objectIndex ? parsed.objectIndex : parsed.nodeName;
580
+ if (!targetKey) {
581
+ return null;
582
+ }
583
+ return model.getObjectByProperty("uuid", targetKey) ?? PropertyBinding.findNode(model, targetKey) ?? null;
584
+ }
585
+ getMorphTrackBaseValue(target, propertyIndex) {
586
+ if (!target) {
587
+ return 0;
588
+ }
589
+ const meshTarget = target;
590
+ const influences = meshTarget.morphTargetInfluences;
591
+ if (!influences) {
592
+ return 0;
593
+ }
594
+ let morphIndex;
595
+ if (typeof propertyIndex === "number" && Number.isInteger(propertyIndex)) {
596
+ morphIndex = propertyIndex;
597
+ } else if (typeof propertyIndex === "string") {
598
+ if (/^\d+$/.test(propertyIndex)) {
599
+ morphIndex = Number(propertyIndex);
600
+ } else {
601
+ morphIndex = meshTarget.morphTargetDictionary?.[propertyIndex];
602
+ }
603
+ }
604
+ if (morphIndex === void 0) {
605
+ return 0;
606
+ }
607
+ return influences[morphIndex] ?? 0;
608
+ }
609
+ canCreateFirstFrameReferenceTrack(track) {
610
+ const valueSize = track.getValueSize();
611
+ if (!Number.isFinite(valueSize) || valueSize <= 0 || track.values.length < valueSize) {
612
+ return false;
613
+ }
614
+ return track.ValueTypeName === "number" || track.ValueTypeName === "quaternion" || track.ValueTypeName === "vector";
615
+ }
616
+ createFirstFrameReferenceTrack(track) {
617
+ const valueSize = track.getValueSize();
618
+ if (!this.canCreateFirstFrameReferenceTrack(track)) {
619
+ return null;
620
+ }
621
+ const values = Array.from(track.values.slice(0, valueSize));
622
+ if (track.ValueTypeName === "number") {
623
+ return new NumberKeyframeTrack(track.name, [0], values);
624
+ }
625
+ if (track.ValueTypeName === "quaternion") {
626
+ return new QuaternionKeyframeTrack(track.name, [0], values);
627
+ }
628
+ if (track.ValueTypeName === "vector") {
629
+ return new VectorKeyframeTrack(track.name, [0], values);
630
+ }
631
+ return null;
632
+ }
633
+ createAdditiveReferenceTrack(track, model) {
634
+ const trackName = typeof track?.name === "string" ? track.name : "";
635
+ if (!trackName) {
636
+ return null;
637
+ }
638
+ let parsed;
639
+ try {
640
+ parsed = PropertyBinding.parseTrackName(trackName);
641
+ } catch {
642
+ return null;
643
+ }
644
+ const target = this.resolveTrackTarget(model, parsed);
645
+ if (parsed.propertyName === "morphTargetInfluences") {
646
+ return new NumberKeyframeTrack(
647
+ track.name,
648
+ [0],
649
+ [this.getMorphTrackBaseValue(target, parsed.propertyIndex)]
650
+ );
651
+ }
652
+ return this.createFirstFrameReferenceTrack(track);
653
+ }
654
+ createAdditiveRuntimeClip(runtimeClip) {
655
+ const model = this.host.getModel();
656
+ if (!model) {
657
+ return null;
658
+ }
659
+ const additiveTracks = [];
660
+ const referenceTracks = [];
661
+ for (const track of runtimeClip.tracks) {
662
+ const referenceTrack = this.createAdditiveReferenceTrack(track, model);
663
+ if (!referenceTrack) {
664
+ continue;
665
+ }
666
+ additiveTracks.push(track.clone());
667
+ referenceTracks.push(referenceTrack);
668
+ }
669
+ const additiveClip = new AnimationClip(
670
+ `${runtimeClip.name}${ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX}`,
671
+ runtimeClip.duration,
672
+ additiveTracks
673
+ );
674
+ if (additiveTracks.length > 0) {
675
+ const referenceClip = new AnimationClip(
676
+ `${runtimeClip.name}${ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX}_reference`,
677
+ 0,
678
+ referenceTracks
679
+ );
680
+ AnimationUtils.makeClipAdditive(additiveClip, 0, referenceClip);
681
+ }
682
+ return additiveClip;
683
+ }
684
+ getOrCreateBakedAdditiveRuntimeClip(runtimeClip) {
685
+ const cached = this.bakedAdditiveRuntimeClips.get(runtimeClip.name);
686
+ if (cached) {
687
+ return cached;
688
+ }
689
+ const additiveClip = this.createAdditiveRuntimeClip(runtimeClip);
690
+ if (!additiveClip) {
691
+ return null;
692
+ }
693
+ this.bakedAdditiveRuntimeClips.set(runtimeClip.name, additiveClip);
694
+ return additiveClip;
695
+ }
307
696
  setClipEventMetadata(clip, metadata) {
308
697
  const userData = clip.userData ?? (clip.userData = {});
309
698
  userData[CLIP_EVENT_METADATA_KEY] = metadata;
@@ -589,21 +978,28 @@ var BakedAnimationController = class {
589
978
  }
590
979
  return 0;
591
980
  }
592
- getOrCreateBakedRuntimeAction(sourceClipName, channel) {
981
+ getOrCreateBakedRuntimeAction(sourceClipName, channel, blendMode = "replace") {
593
982
  const bakedClip = this.getBakedSourceClip(sourceClipName);
594
983
  const runtimeClip = bakedClip?.runtimeClips.find((entry) => entry.channel === channel)?.clip;
595
984
  if (!runtimeClip) {
596
985
  return null;
597
986
  }
987
+ const desiredClip = blendMode === "additive" ? this.getOrCreateBakedAdditiveRuntimeClip(runtimeClip) : runtimeClip;
988
+ if (!desiredClip) {
989
+ return null;
990
+ }
598
991
  const existing = this.bakedRuntimeActions.get(runtimeClip.name);
599
- if (existing) {
992
+ if (existing?.getClip() === desiredClip) {
600
993
  return existing;
601
994
  }
602
995
  this.ensureMixer();
603
996
  if (!this.animationMixer) {
604
997
  return null;
605
998
  }
606
- const action = this.animationMixer.clipAction(runtimeClip);
999
+ if (existing) {
1000
+ this.releaseBakedRuntimeAction(runtimeClip.name);
1001
+ }
1002
+ const action = this.animationMixer.clipAction(desiredClip);
607
1003
  this.bakedRuntimeActions.set(runtimeClip.name, action);
608
1004
  return action;
609
1005
  }
@@ -621,7 +1017,15 @@ var BakedAnimationController = class {
621
1017
  }
622
1018
  const channelActions = /* @__PURE__ */ new Map();
623
1019
  for (const runtimeClip of bakedClip.runtimeClips) {
624
- const action = this.getOrCreateBakedRuntimeAction(clipName, runtimeClip.channel);
1020
+ const channelBlendMode = resolveBakedChannelBlendMode(
1021
+ runtimeClip.channel,
1022
+ playbackState.requestedBlendMode
1023
+ ) ?? "replace";
1024
+ const action = this.getOrCreateBakedRuntimeAction(
1025
+ clipName,
1026
+ runtimeClip.channel,
1027
+ channelBlendMode
1028
+ );
625
1029
  if (action) {
626
1030
  channelActions.set(runtimeClip.channel, action);
627
1031
  }
@@ -649,12 +1053,7 @@ var BakedAnimationController = class {
649
1053
  if (typeof this.host.getMeshNamesForAU === "function") {
650
1054
  return this.host.getMeshNamesForAU(auId) || [];
651
1055
  }
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 || [];
1056
+ return getMeshNamesForAUProfile(config, auId);
658
1057
  }
659
1058
  getMeshNamesForViseme(config, explicitMeshNames) {
660
1059
  if (explicitMeshNames && explicitMeshNames.length > 0) {
@@ -663,18 +1062,41 @@ var BakedAnimationController = class {
663
1062
  if (typeof this.host.getMeshNamesForViseme === "function") {
664
1063
  return this.host.getMeshNamesForViseme() || [];
665
1064
  }
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 || [];
1065
+ return getMeshNamesForVisemeProfile(config);
1066
+ }
1067
+ hasActiveAdditivePlayback() {
1068
+ for (const [clipName, group] of this.bakedActionGroups) {
1069
+ const state = this.playbackState.get(clipName);
1070
+ if (state?.blendMode !== "additive") {
1071
+ continue;
1072
+ }
1073
+ for (const action of group.channelActions.values()) {
1074
+ if (action.isRunning() && !action.paused) {
1075
+ return true;
1076
+ }
1077
+ }
1078
+ }
1079
+ for (const [clipName, action] of this.animationActions) {
1080
+ const state = this.playbackState.get(clipName);
1081
+ if (state?.blendMode !== "additive") {
1082
+ continue;
1083
+ }
1084
+ if (action.isRunning() && !action.paused) {
1085
+ return true;
1086
+ }
1087
+ }
1088
+ return false;
670
1089
  }
671
1090
  update(dtSeconds) {
672
1091
  if (this.animationMixer) {
1092
+ this.animationMixer.update(dtSeconds);
1093
+ }
1094
+ if (this.clipAnimationMixer) {
673
1095
  const snapshots = Array.from(this.clipMonitors.values()).map((monitor) => ({
674
1096
  actionId: monitor.actionId,
675
1097
  previousTime: monitor.action.time
676
1098
  }));
677
- this.animationMixer.update(dtSeconds);
1099
+ this.clipAnimationMixer.update(dtSeconds);
678
1100
  for (const { actionId, previousTime } of snapshots) {
679
1101
  const monitor = this.clipMonitors.get(actionId);
680
1102
  if (!monitor) continue;
@@ -693,16 +1115,27 @@ var BakedAnimationController = class {
693
1115
  }
694
1116
  }
695
1117
  }
1118
+ if (this.hasActiveAdditivePlayback()) {
1119
+ this.host.reapplyProceduralState?.();
1120
+ }
696
1121
  }
697
1122
  dispose() {
698
1123
  this.stopAllAnimations();
1124
+ this.clearAllBakedAdditiveRuntimeClips();
699
1125
  if (this.animationMixer) {
700
1126
  this.animationMixer.stopAllAction();
701
1127
  this.animationMixer = null;
702
1128
  }
1129
+ if (this.clipAnimationMixer) {
1130
+ this.clipAnimationMixer.stopAllAction();
1131
+ this.clipAnimationMixer = null;
1132
+ }
1133
+ this.mixerFinishedListenerAttached = false;
1134
+ this.clipMixerFinishedListenerAttached = false;
703
1135
  this.animationClips = [];
704
1136
  this.bakedSourceClips.clear();
705
1137
  this.bakedRuntimeActions.clear();
1138
+ this.bakedAdditiveRuntimeClips.clear();
706
1139
  this.bakedActionGroups.clear();
707
1140
  this.bakedRuntimeClipToSource.clear();
708
1141
  this.animationActions.clear();
@@ -725,6 +1158,8 @@ var BakedAnimationController = class {
725
1158
  if (this.animationMixer) {
726
1159
  for (const bakedClip of this.bakedSourceClips.values()) {
727
1160
  for (const runtimeClip of bakedClip.runtimeClips) {
1161
+ this.releaseBakedRuntimeAction(runtimeClip.clip.name);
1162
+ this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
728
1163
  try {
729
1164
  this.animationMixer.uncacheAction(runtimeClip.clip);
730
1165
  } catch {
@@ -736,6 +1171,7 @@ var BakedAnimationController = class {
736
1171
  }
737
1172
  }
738
1173
  }
1174
+ this.clearAllBakedAdditiveRuntimeClips();
739
1175
  for (const clipName of this.bakedSourceClips.keys()) {
740
1176
  this.playbackState.delete(clipName);
741
1177
  this.clipSources.delete(clipName);
@@ -776,6 +1212,8 @@ var BakedAnimationController = class {
776
1212
  if (this.animationMixer) {
777
1213
  for (const runtimeClip of bakedClip.runtimeClips) {
778
1214
  const action = this.bakedRuntimeActions.get(runtimeClip.clip.name);
1215
+ this.releaseBakedRuntimeAction(runtimeClip.clip.name);
1216
+ this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
779
1217
  try {
780
1218
  this.animationMixer.uncacheAction(runtimeClip.clip);
781
1219
  } catch {
@@ -784,7 +1222,6 @@ var BakedAnimationController = class {
784
1222
  this.animationMixer.uncacheClip(runtimeClip.clip);
785
1223
  } catch {
786
1224
  }
787
- this.bakedRuntimeActions.delete(runtimeClip.clip.name);
788
1225
  this.bakedRuntimeClipToSource.delete(runtimeClip.clip.name);
789
1226
  const actionId = this.getActionId(action);
790
1227
  if (actionId && action) {
@@ -793,6 +1230,10 @@ var BakedAnimationController = class {
793
1230
  }
794
1231
  }
795
1232
  }
1233
+ for (const runtimeClip of bakedClip.runtimeClips) {
1234
+ this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
1235
+ this.bakedRuntimeActions.delete(runtimeClip.clip.name);
1236
+ }
796
1237
  this.animationClips = this.animationClips.filter((entry) => entry.name !== clipName);
797
1238
  this.bakedSourceClips.delete(clipName);
798
1239
  this.bakedActionGroups.delete(clipName);
@@ -853,12 +1294,12 @@ var BakedAnimationController = class {
853
1294
  const actionId = this.getActionId(action);
854
1295
  const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
855
1296
  action.stop();
856
- if (!isBaked && this.animationMixer) {
1297
+ if (!isBaked && this.clipAnimationMixer) {
857
1298
  try {
858
1299
  const clip = action.getClip();
859
1300
  if (clip) {
860
- this.animationMixer.uncacheAction(clip);
861
- this.animationMixer.uncacheClip(clip);
1301
+ this.clipAnimationMixer.uncacheAction(clip);
1302
+ this.clipAnimationMixer.uncacheClip(clip);
862
1303
  }
863
1304
  } catch {
864
1305
  }
@@ -880,11 +1321,11 @@ var BakedAnimationController = class {
880
1321
  const actionId = this.getActionId(clipAction);
881
1322
  try {
882
1323
  clipAction.stop();
883
- if (this.animationMixer) {
1324
+ if (this.clipAnimationMixer) {
884
1325
  const clip = clipAction.getClip();
885
1326
  if (clip) {
886
- this.animationMixer.uncacheAction(clip);
887
- this.animationMixer.uncacheClip(clip);
1327
+ this.clipAnimationMixer.uncacheAction(clip);
1328
+ this.clipAnimationMixer.uncacheClip(clip);
888
1329
  }
889
1330
  }
890
1331
  } catch {
@@ -1088,8 +1529,23 @@ var BakedAnimationController = class {
1088
1529
  next.blendMode = this.getBakedAggregateBlendMode(clipName, next);
1089
1530
  const bakedGroup = this.bakedActionGroups.get(clipName);
1090
1531
  if (bakedGroup) {
1091
- for (const [channel, action2] of bakedGroup.channelActions) {
1532
+ for (const [channel, currentAction] of Array.from(bakedGroup.channelActions)) {
1533
+ const channelBlendMode = resolveBakedChannelBlendMode(channel, next.requestedBlendMode) ?? "replace";
1534
+ const previousTime = currentAction.time;
1535
+ const wasActive = currentAction.isRunning() || currentAction.paused;
1536
+ const wasPaused = currentAction.paused;
1537
+ const action2 = this.getOrCreateBakedRuntimeAction(clipName, channel, channelBlendMode);
1538
+ if (!action2) {
1539
+ bakedGroup.channelActions.delete(channel);
1540
+ continue;
1541
+ }
1092
1542
  this.applyPlaybackStateToBakedAction(action2, next, channel);
1543
+ action2.time = Math.max(0, Math.min(action2.getClip().duration, previousTime));
1544
+ if (action2 !== currentAction && wasActive) {
1545
+ action2.play();
1546
+ }
1547
+ action2.paused = wasPaused;
1548
+ bakedGroup.channelActions.set(channel, action2);
1093
1549
  }
1094
1550
  }
1095
1551
  this.setPlaybackState(clipName, next);
@@ -1104,6 +1560,10 @@ var BakedAnimationController = class {
1104
1560
  seekAnimation(clipName, time) {
1105
1561
  const bakedGroup = this.bakedActionGroups.get(clipName);
1106
1562
  if (bakedGroup) {
1563
+ const state = this.getPlaybackStateSnapshot(clipName, {
1564
+ loop: true,
1565
+ source: this.clipSources.get(clipName) ?? "baked"
1566
+ });
1107
1567
  const duration2 = this.getBakedSourceClip(clipName)?.sourceClip.duration ?? 0;
1108
1568
  const clamped = Math.max(0, Math.min(duration2, Number.isFinite(time) ? time : 0));
1109
1569
  for (const action2 of bakedGroup.channelActions.values()) {
@@ -1111,6 +1571,10 @@ var BakedAnimationController = class {
1111
1571
  }
1112
1572
  try {
1113
1573
  this.animationMixer?.update(0);
1574
+ this.clipAnimationMixer?.update(0);
1575
+ if (state.blendMode === "additive") {
1576
+ this.host.reapplyProceduralState?.();
1577
+ }
1114
1578
  } catch {
1115
1579
  }
1116
1580
  return;
@@ -1120,7 +1584,11 @@ var BakedAnimationController = class {
1120
1584
  const duration = action.getClip().duration;
1121
1585
  action.time = Math.max(0, Math.min(duration, Number.isFinite(time) ? time : 0));
1122
1586
  try {
1123
- this.animationMixer?.update(0);
1587
+ this.clipAnimationMixer?.update(0);
1588
+ const state = this.playbackState.get(clipName);
1589
+ if (state?.blendMode === "additive") {
1590
+ this.host.reapplyProceduralState?.();
1591
+ }
1124
1592
  } catch {
1125
1593
  }
1126
1594
  }
@@ -1128,6 +1596,9 @@ var BakedAnimationController = class {
1128
1596
  if (this.animationMixer) {
1129
1597
  this.animationMixer.timeScale = timeScale;
1130
1598
  }
1599
+ if (this.clipAnimationMixer) {
1600
+ this.clipAnimationMixer.timeScale = timeScale;
1601
+ }
1131
1602
  }
1132
1603
  getAnimationState(clipName) {
1133
1604
  const bakedClip = this.getBakedSourceClip(clipName);
@@ -1457,8 +1928,8 @@ var BakedAnimationController = class {
1457
1928
  return clip;
1458
1929
  }
1459
1930
  playClip(clip, options) {
1460
- this.ensureMixer();
1461
- if (!this.animationMixer) {
1931
+ const mixer = this.ensureClipMixer();
1932
+ if (!mixer) {
1462
1933
  console.warn("[Loom3] playClip: No model loaded, cannot create mixer");
1463
1934
  return null;
1464
1935
  }
@@ -1476,7 +1947,7 @@ var BakedAnimationController = class {
1476
1947
  actionId = this.setActionId(action, clip.name);
1477
1948
  }
1478
1949
  if (!action) {
1479
- action = this.animationMixer.clipAction(clip);
1950
+ action = mixer.clipAction(clip);
1480
1951
  actionId = this.setActionId(action, clip.name);
1481
1952
  }
1482
1953
  const existingClip = this.animationClips.find((c) => c.name === clip.name);
@@ -1537,15 +2008,13 @@ var BakedAnimationController = class {
1537
2008
  },
1538
2009
  stop: () => {
1539
2010
  action.stop();
1540
- if (this.animationMixer) {
1541
- try {
1542
- this.animationMixer.uncacheAction(clip);
1543
- } catch {
1544
- }
1545
- try {
1546
- this.animationMixer.uncacheClip(clip);
1547
- } catch {
1548
- }
2011
+ try {
2012
+ mixer.uncacheAction(clip);
2013
+ } catch {
2014
+ }
2015
+ try {
2016
+ mixer.uncacheClip(clip);
2017
+ } catch {
1549
2018
  }
1550
2019
  this.clipActions.delete(clip.name);
1551
2020
  this.animationActions.delete(clip.name);
@@ -1586,7 +2055,7 @@ var BakedAnimationController = class {
1586
2055
  const clamped = Math.max(0, Math.min(clip.duration, t));
1587
2056
  action.time = clamped;
1588
2057
  try {
1589
- this.animationMixer?.update(0);
2058
+ mixer.update(0);
1590
2059
  } catch {
1591
2060
  }
1592
2061
  this.syncClipMonitorTime(monitor, clamped, true);
@@ -1619,16 +2088,16 @@ var BakedAnimationController = class {
1619
2088
  return this.playClip(clip, { ...options, source: options?.source ?? "clip" });
1620
2089
  }
1621
2090
  cleanupSnippet(name) {
1622
- if (!this.animationMixer || !this.host.getModel()) return;
2091
+ if (!this.host.getModel()) return;
1623
2092
  for (const [clipName, action] of Array.from(this.clipActions.entries())) {
1624
2093
  if (clipName === name || clipName.startsWith(`${name}_`)) {
1625
2094
  const actionId = this.getActionId(action);
1626
2095
  try {
1627
2096
  action.stop();
1628
2097
  const clip = action.getClip();
1629
- if (clip) {
1630
- this.animationMixer.uncacheAction(clip);
1631
- this.animationMixer.uncacheClip(clip);
2098
+ if (clip && this.clipAnimationMixer) {
2099
+ this.clipAnimationMixer.uncacheAction(clip);
2100
+ this.clipAnimationMixer.uncacheClip(clip);
1632
2101
  }
1633
2102
  } catch {
1634
2103
  }
@@ -1656,7 +2125,10 @@ var BakedAnimationController = class {
1656
2125
  clipActions: Array.from(this.clipActions.entries()).map(([k, a]) => ({ name: k, actionId: this.getActionId(a) })),
1657
2126
  animationActions: Array.from(this.animationActions.entries()).map(([k, a]) => ({ name: k, actionId: this.getActionId(a) })),
1658
2127
  clipHandles: Array.from(this.clipHandles.entries()).map(([k, h]) => ({ name: k, actionId: h.actionId })),
1659
- mixerActions: (this.animationMixer?._actions || []).map((a) => ({ name: a?.getClip?.()?.name || "", actionId: this.getActionId(a) }))
2128
+ mixerActions: [
2129
+ ...this.animationMixer?._actions || [],
2130
+ ...this.clipAnimationMixer?._actions || []
2131
+ ].map((a) => ({ name: a?.getClip?.()?.name || "", actionId: this.getActionId(a) }))
1660
2132
  });
1661
2133
  console.log("[Loom3] updateClipParams start", debugSnapshot());
1662
2134
  const apply = (action) => {
@@ -1719,7 +2191,7 @@ var BakedAnimationController = class {
1719
2191
  }
1720
2192
  addMorphTracks(tracks, morphKey, keyframes, intensityScale, meshNames) {
1721
2193
  const config = this.host.getConfig();
1722
- const hasExplicitMeshes = !!(meshNames && meshNames.length > 0);
2194
+ const hasExplicitMeshes = meshNames !== void 0;
1723
2195
  const targetMeshNames = hasExplicitMeshes ? meshNames : config.morphToMesh?.face || [];
1724
2196
  const targetMeshes = targetMeshNames.length ? targetMeshNames.map((name) => this.host.getMeshByName(name)).filter(Boolean) : [];
1725
2197
  const addTrackForMesh = (mesh) => {
@@ -1743,7 +2215,7 @@ var BakedAnimationController = class {
1743
2215
  addMorphIndexTracks(tracks, morphIndex, keyframes, intensityScale, meshNames) {
1744
2216
  if (!Number.isInteger(morphIndex) || morphIndex < 0) return;
1745
2217
  const config = this.host.getConfig();
1746
- const hasExplicitMeshes = !!(meshNames && meshNames.length > 0);
2218
+ const hasExplicitMeshes = meshNames !== void 0;
1747
2219
  const targetMeshNames = hasExplicitMeshes ? meshNames : config.morphToMesh?.face || [];
1748
2220
  const targetMeshes = targetMeshNames.length ? targetMeshNames.map((name) => this.host.getMeshByName(name)).filter(Boolean) : [];
1749
2221
  const addTrackForMesh = (mesh) => {
@@ -1770,35 +2242,48 @@ var BakedAnimationController = class {
1770
2242
  this.animationMixer = new AnimationMixer(model);
1771
2243
  }
1772
2244
  if (this.animationMixer && !this.mixerFinishedListenerAttached) {
1773
- this.animationMixer.addEventListener("finished", (event) => {
1774
- const action = event.action;
1775
- const actionId = this.getActionId(action);
1776
- if (actionId) {
1777
- const monitor = this.clipMonitors.get(actionId);
1778
- if (monitor) {
1779
- monitor.finishedPending = true;
1780
- return;
1781
- }
1782
- }
1783
- const clip = action.getClip();
1784
- const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
1785
- if (bakedRuntime) {
1786
- const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
1787
- if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
1788
- group.resolveFinished();
1789
- }
1790
- return;
1791
- }
1792
- const callback = this.animationFinishedCallbacks.get(clip.name);
1793
- if (callback) {
1794
- callback();
1795
- this.animationFinishedCallbacks.delete(clip.name);
1796
- }
1797
- });
2245
+ this.animationMixer.addEventListener("finished", (event) => this.handleMixerFinished(event));
1798
2246
  this.mixerFinishedListenerAttached = true;
1799
2247
  }
1800
2248
  return this.animationMixer;
1801
2249
  }
2250
+ ensureClipMixer() {
2251
+ const model = this.host.getModel();
2252
+ if (!model) return null;
2253
+ if (!this.clipAnimationMixer) {
2254
+ this.clipAnimationMixer = new AnimationMixer(model);
2255
+ }
2256
+ if (this.clipAnimationMixer && !this.clipMixerFinishedListenerAttached) {
2257
+ this.clipAnimationMixer.addEventListener("finished", (event) => this.handleMixerFinished(event));
2258
+ this.clipMixerFinishedListenerAttached = true;
2259
+ }
2260
+ return this.clipAnimationMixer;
2261
+ }
2262
+ handleMixerFinished(event) {
2263
+ const action = event.action;
2264
+ const actionId = this.getActionId(action);
2265
+ if (actionId) {
2266
+ const monitor = this.clipMonitors.get(actionId);
2267
+ if (monitor) {
2268
+ monitor.finishedPending = true;
2269
+ return;
2270
+ }
2271
+ }
2272
+ const clip = action.getClip();
2273
+ const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
2274
+ if (bakedRuntime) {
2275
+ const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
2276
+ if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
2277
+ group.resolveFinished();
2278
+ }
2279
+ return;
2280
+ }
2281
+ const callback = this.animationFinishedCallbacks.get(clip.name);
2282
+ if (callback) {
2283
+ callback();
2284
+ this.animationFinishedCallbacks.delete(clip.name);
2285
+ }
2286
+ }
1802
2287
  createAnimationHandle(clipName, action, finishedPromise) {
1803
2288
  return {
1804
2289
  actionId: this.getActionId(action),
@@ -2377,6 +2862,159 @@ var VISEME_JAW_AMOUNTS = [
2377
2862
  0.5
2378
2863
  // 14: W_OO
2379
2864
  ];
2865
+ var CC4_VISEME_SYSTEM_ID = "cc4-arkit-15";
2866
+ var CC4_VISEME_SLOTS = [
2867
+ {
2868
+ id: "ae",
2869
+ label: "AE",
2870
+ order: 0,
2871
+ providerIds: { azure: [4], sapi: [4] },
2872
+ phonemes: ["AE", "EH", "EY", "UH"],
2873
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ae|eh|ey)([_ .-]|$)"],
2874
+ features: { jawOpen: 0.75, lipSpread: 0.35 },
2875
+ defaultJawAmount: VISEME_JAW_AMOUNTS[0]
2876
+ },
2877
+ {
2878
+ id: "ah",
2879
+ label: "Ah",
2880
+ order: 1,
2881
+ providerIds: { azure: [1, 2, 9, 11, 12], sapi: [1, 2, 9, 11, 12] },
2882
+ phonemes: ["AA", "AE", "AH", "AX", "AW", "AY", "HH"],
2883
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ah|aa|aah|open)([_ .-]|$)"],
2884
+ features: { jawOpen: 0.8 },
2885
+ defaultJawAmount: VISEME_JAW_AMOUNTS[1]
2886
+ },
2887
+ {
2888
+ id: "b-m-p",
2889
+ label: "B_M_P",
2890
+ order: 2,
2891
+ providerIds: { azure: [0, 21], sapi: [0, 21] },
2892
+ phonemes: ["B", "M", "P"],
2893
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(b[_ .-]?m[_ .-]?p|bmp|closed|sil|rest)([_ .-]|$)"],
2894
+ features: { jawOpen: 0, lipClosed: 1 },
2895
+ defaultJawAmount: VISEME_JAW_AMOUNTS[2]
2896
+ },
2897
+ {
2898
+ id: "ch-j",
2899
+ label: "Ch_J",
2900
+ order: 3,
2901
+ providerIds: { azure: [16], sapi: [16] },
2902
+ phonemes: ["CH", "JH", "SH", "ZH"],
2903
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ch|j|sh|zh)([_ .-]|$)"],
2904
+ features: { jawOpen: 0.3, fricative: 0.6 },
2905
+ defaultJawAmount: VISEME_JAW_AMOUNTS[3]
2906
+ },
2907
+ {
2908
+ id: "ee",
2909
+ label: "EE",
2910
+ order: 4,
2911
+ providerIds: { azure: [6], sapi: [6] },
2912
+ phonemes: ["IY", "IH", "IX", "Y"],
2913
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ee|iy|y)([_ .-]|$)"],
2914
+ features: { jawOpen: 0.2, lipSpread: 0.8 },
2915
+ defaultJawAmount: VISEME_JAW_AMOUNTS[4]
2916
+ },
2917
+ {
2918
+ id: "er",
2919
+ label: "Er",
2920
+ order: 5,
2921
+ providerIds: { azure: [5], sapi: [5] },
2922
+ phonemes: ["ER"],
2923
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(er)([_ .-]|$)"],
2924
+ features: { jawOpen: 0.35, lipRound: 0.35 },
2925
+ defaultJawAmount: VISEME_JAW_AMOUNTS[5]
2926
+ },
2927
+ {
2928
+ id: "f-v",
2929
+ label: "F_V",
2930
+ order: 6,
2931
+ providerIds: { azure: [18], sapi: [18] },
2932
+ phonemes: ["F", "V"],
2933
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(f[_ .-]?v|fv)([_ .-]|$)"],
2934
+ features: { jawOpen: 0.1, fricative: 1 },
2935
+ defaultJawAmount: VISEME_JAW_AMOUNTS[6]
2936
+ },
2937
+ {
2938
+ id: "ih",
2939
+ label: "Ih",
2940
+ order: 7,
2941
+ providerIds: { azure: [6], sapi: [6] },
2942
+ phonemes: ["IH", "IX"],
2943
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(ih|ix)([_ .-]|$)"],
2944
+ features: { jawOpen: 0.2, lipSpread: 0.55 },
2945
+ defaultJawAmount: VISEME_JAW_AMOUNTS[7]
2946
+ },
2947
+ {
2948
+ id: "k-g-h-ng",
2949
+ label: "K_G_H_NG",
2950
+ order: 8,
2951
+ providerIds: { azure: [20], sapi: [20] },
2952
+ phonemes: ["K", "G", "NG"],
2953
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(k[_ .-]?g[_ .-]?h?[_ .-]?ng|kg|ng)([_ .-]|$)"],
2954
+ features: { jawOpen: 0.35 },
2955
+ defaultJawAmount: VISEME_JAW_AMOUNTS[8]
2956
+ },
2957
+ {
2958
+ id: "oh",
2959
+ label: "Oh",
2960
+ order: 9,
2961
+ providerIds: { azure: [3, 8, 10], sapi: [3, 8, 10] },
2962
+ phonemes: ["AO", "OW", "OY"],
2963
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(oh|ao|ow|oy)([_ .-]|$)"],
2964
+ features: { jawOpen: 0.6, lipRound: 0.8 },
2965
+ defaultJawAmount: VISEME_JAW_AMOUNTS[9]
2966
+ },
2967
+ {
2968
+ id: "r",
2969
+ label: "R",
2970
+ order: 10,
2971
+ providerIds: { azure: [13], sapi: [13] },
2972
+ phonemes: ["R"],
2973
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(r)([_ .-]|$)"],
2974
+ features: { jawOpen: 0.35, lipRound: 0.5 },
2975
+ defaultJawAmount: VISEME_JAW_AMOUNTS[10]
2976
+ },
2977
+ {
2978
+ id: "s-z",
2979
+ label: "S_Z",
2980
+ order: 11,
2981
+ providerIds: { azure: [15], sapi: [15] },
2982
+ phonemes: ["S", "Z"],
2983
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(s[_ .-]?z|sz)([_ .-]|$)"],
2984
+ features: { jawOpen: 0.1, fricative: 1 },
2985
+ defaultJawAmount: VISEME_JAW_AMOUNTS[11]
2986
+ },
2987
+ {
2988
+ id: "t-l-d-n",
2989
+ label: "T_L_D_N",
2990
+ order: 12,
2991
+ providerIds: { azure: [14, 19], sapi: [14, 19] },
2992
+ phonemes: ["T", "L", "D", "N"],
2993
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(t[_ .-]?l[_ .-]?d[_ .-]?n|tldn|l)([_ .-]|$)"],
2994
+ features: { jawOpen: 0.3, tongueTip: 1 },
2995
+ defaultJawAmount: VISEME_JAW_AMOUNTS[12]
2996
+ },
2997
+ {
2998
+ id: "th",
2999
+ label: "Th",
3000
+ order: 13,
3001
+ providerIds: { azure: [17], sapi: [17] },
3002
+ phonemes: ["TH", "DH"],
3003
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(th|dh)([_ .-]|$)"],
3004
+ features: { jawOpen: 0.15, tongueTip: 0.8, fricative: 0.8 },
3005
+ defaultJawAmount: VISEME_JAW_AMOUNTS[13]
3006
+ },
3007
+ {
3008
+ id: "w-oo",
3009
+ label: "W_OO",
3010
+ order: 14,
3011
+ providerIds: { azure: [7], sapi: [7] },
3012
+ phonemes: ["W", "UW"],
3013
+ matchers: ["(^|[_ .-])(v|viseme)?[_ .-]?(w[_ .-]?oo|woo|uw|oo)([_ .-]|$)"],
3014
+ features: { jawOpen: 0.5, lipRound: 1 },
3015
+ defaultJawAmount: VISEME_JAW_AMOUNTS[14]
3016
+ }
3017
+ ];
2380
3018
  var isMixedAU = (id) => {
2381
3019
  const morphs = AU_TO_MORPHS[id];
2382
3020
  const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
@@ -2688,6 +3326,23 @@ var AU_FACEPART_TO_MESH_CATEGORY = {
2688
3326
  Eyelids: "eye",
2689
3327
  Tongue: "tongue"
2690
3328
  };
3329
+ var CC4_MAPPING_SECTIONS = [
3330
+ { id: "Forehead", label: "Forehead", kind: "au", order: 0, meshCategory: "face", facePart: "Forehead" },
3331
+ { id: "Eyelids", label: "Eyelids", kind: "au", order: 1, meshCategory: "eye", facePart: "Eyelids" },
3332
+ { id: "Eyes", label: "Eyes", kind: "au", order: 2, meshCategory: "eye", facePart: "Eyes" },
3333
+ { id: "Cheeks", label: "Cheeks", kind: "au", order: 3, meshCategory: "face", facePart: "Cheeks" },
3334
+ { id: "Nose", label: "Nose", kind: "au", order: 4, meshCategory: "face", facePart: "Nose" },
3335
+ { id: "Mouth", label: "Mouth", kind: "au", order: 5, meshCategory: "face", facePart: "Mouth" },
3336
+ { id: "Chin", label: "Chin", kind: "au", order: 6, meshCategory: "face", facePart: "Chin" },
3337
+ { id: "Jaw", label: "Jaw", kind: "au", order: 7, meshCategory: "face", facePart: "Jaw" },
3338
+ { id: "Tongue", label: "Tongue", kind: "au", order: 8, meshCategory: "tongue", facePart: "Tongue" },
3339
+ { id: "Head", label: "Head", kind: "au", order: 9, meshCategory: "face", facePart: "Head" },
3340
+ { id: "Joint Controls", label: "Joint Controls", kind: "au", order: 10, meshCategory: "face", facePart: "Joint Controls" },
3341
+ { id: "Eye", label: "Eye", kind: "au", order: 11, meshCategory: "eye", facePart: "Eye" },
3342
+ { id: "Hair", label: "Hair", kind: "hair", order: 12, meshCategory: "hair" },
3343
+ { id: "Visemes", label: "Visemes", kind: "viseme", order: 13, meshCategory: "viseme" },
3344
+ { id: "Unmapped", label: "Unmapped", kind: "unmapped", order: 14, meshCategory: "face" }
3345
+ ];
2691
3346
  var CC4_HAIR_PHYSICS = {
2692
3347
  stiffness: 7.5,
2693
3348
  damping: 0.18,
@@ -2734,7 +3389,10 @@ var CC4_PRESET = {
2734
3389
  suffixPattern: CC4_SUFFIX_PATTERN,
2735
3390
  morphToMesh: MORPH_TO_MESH,
2736
3391
  auFacePartToMeshCategory: AU_FACEPART_TO_MESH_CATEGORY,
3392
+ mappingSections: CC4_MAPPING_SECTIONS,
2737
3393
  visemeKeys: VISEME_KEYS,
3394
+ visemeSystemId: CC4_VISEME_SYSTEM_ID,
3395
+ visemeSlots: CC4_VISEME_SLOTS,
2738
3396
  visemeMeshCategory: "viseme",
2739
3397
  visemeJawAmounts: VISEME_JAW_AMOUNTS,
2740
3398
  auMixDefaults: AU_MIX_DEFAULTS,
@@ -3621,8 +4279,13 @@ function extendPresetWithProfile(base, extension) {
3621
4279
  boneNodes: mergeRecord(base.boneNodes, extension.boneNodes),
3622
4280
  morphToMesh: mergeRecord(base.morphToMesh, extension.morphToMesh),
3623
4281
  auFacePartToMeshCategory: base.auFacePartToMeshCategory || extension.auFacePartToMeshCategory ? mergeRecord(base.auFacePartToMeshCategory || {}, extension.auFacePartToMeshCategory || {}) : void 0,
4282
+ mappingSections: extension.mappingSections ? [...extension.mappingSections] : base.mappingSections ? [...base.mappingSections] : void 0,
3624
4283
  visemeKeys: extension.visemeKeys ? [...extension.visemeKeys] : [...base.visemeKeys],
4284
+ visemeSystemId: extension.visemeSystemId ?? base.visemeSystemId,
4285
+ visemeSlots: extension.visemeSlots ? [...extension.visemeSlots] : base.visemeSlots ? [...base.visemeSlots] : void 0,
4286
+ visemeBindings: base.visemeBindings || extension.visemeBindings ? mergeRecord(base.visemeBindings || {}, extension.visemeBindings || {}) : void 0,
3625
4287
  visemeMeshCategory: extension.visemeMeshCategory ?? base.visemeMeshCategory,
4288
+ visemeJawAmounts: extension.visemeJawAmounts ? [...extension.visemeJawAmounts] : base.visemeJawAmounts ? [...base.visemeJawAmounts] : void 0,
3626
4289
  auMixDefaults: base.auMixDefaults || extension.auMixDefaults ? mergeRecord(base.auMixDefaults || {}, extension.auMixDefaults || {}) : void 0,
3627
4290
  auInfo: base.auInfo || extension.auInfo ? mergeRecord(base.auInfo || {}, extension.auInfo || {}) : void 0,
3628
4291
  eyeMeshNodes: extension.eyeMeshNodes ?? base.eyeMeshNodes,
@@ -4584,7 +5247,8 @@ var _Loom3 = class _Loom3 {
4584
5247
  getCompositeRotations: () => this.compositeRotations,
4585
5248
  computeSideValues: (base, balance) => this.computeSideValues(base, balance),
4586
5249
  getAUMixWeight: (auId) => this.getAUMixWeight(auId),
4587
- isMixedAU: (auId) => this.isMixedAU(auId)
5250
+ isMixedAU: (auId) => this.isMixedAU(auId),
5251
+ reapplyProceduralState: () => this.reapplyProceduralStateAfterBakedUpdate()
4588
5252
  });
4589
5253
  this.hairPhysics = new HairPhysicsController({
4590
5254
  getMeshByName: (name) => this.meshByName.get(name),
@@ -5188,6 +5852,21 @@ var _Loom3 = class _Loom3 {
5188
5852
  const jawHandle = this.transitionBoneRotation("JAW", "pitch", jawAmount, durationMs);
5189
5853
  return this.combineHandles([morphHandle, jawHandle]);
5190
5854
  }
5855
+ setVisemeById(slotId, value, jawScale = 1) {
5856
+ const index = getVisemeSlotIndex(this.config, slotId);
5857
+ if (index < 0) return;
5858
+ this.setViseme(index, value, jawScale);
5859
+ }
5860
+ transitionVisemeById(slotId, to, durationMs = 80, jawScale = 1) {
5861
+ const index = getVisemeSlotIndex(this.config, slotId);
5862
+ if (index < 0) {
5863
+ return { promise: Promise.resolve(), pause: () => {
5864
+ }, resume: () => {
5865
+ }, cancel: () => {
5866
+ } };
5867
+ }
5868
+ return this.transitionViseme(index, to, durationMs, jawScale);
5869
+ }
5191
5870
  // ============================================================================
5192
5871
  // MIX WEIGHT CONTROL
5193
5872
  // ============================================================================
@@ -5280,6 +5959,30 @@ var _Loom3 = class _Loom3 {
5280
5959
  this.model.updateMatrixWorld(true);
5281
5960
  }
5282
5961
  }
5962
+ reapplyProceduralStateAfterBakedUpdate() {
5963
+ if (!this.model) {
5964
+ return;
5965
+ }
5966
+ let hasActiveOverrides = false;
5967
+ for (const [auIdStr, value] of Object.entries(this.auValues)) {
5968
+ if (value <= 0) continue;
5969
+ const auId = Number(auIdStr);
5970
+ if (Number.isNaN(auId)) continue;
5971
+ hasActiveOverrides = true;
5972
+ this.setAU(auId, value, this.auBalances[auId]);
5973
+ }
5974
+ for (let visemeIndex = 0; visemeIndex < this.visemeValues.length; visemeIndex += 1) {
5975
+ const value = this.visemeValues[visemeIndex] ?? 0;
5976
+ if (value <= 0) continue;
5977
+ hasActiveOverrides = true;
5978
+ this.setViseme(visemeIndex, value, this.visemeJawScales[visemeIndex] ?? 1);
5979
+ }
5980
+ if (!hasActiveOverrides) {
5981
+ return;
5982
+ }
5983
+ this.flushPendingComposites();
5984
+ this.model.updateMatrixWorld(true);
5985
+ }
5283
5986
  // ============================================================================
5284
5987
  // MESH CONTROL
5285
5988
  // ============================================================================
@@ -5533,21 +6236,10 @@ var _Loom3 = class _Loom3 {
5533
6236
  * Routing is driven by `auFacePartToMeshCategory` in profile config.
5534
6237
  */
5535
6238
  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 || [];
6239
+ return getMeshNamesForAUProfile(this.config, auId);
5546
6240
  }
5547
6241
  getMeshNamesForViseme() {
5548
- const m = this.config.morphToMesh;
5549
- const category = this.config.visemeMeshCategory || (m?.viseme ? "viseme" : "face");
5550
- return m?.[category] || m?.face || [];
6242
+ return getMeshNamesForVisemeProfile(this.config);
5551
6243
  }
5552
6244
  // ============================================================================
5553
6245
  // HAIR PHYSICS
@@ -5815,7 +6507,7 @@ var _Loom3 = class _Loom3 {
5815
6507
  );
5816
6508
  }
5817
6509
  getVisemeJawAmount(visemeIndex) {
5818
- return this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
6510
+ return getVisemeJawAmounts(this.config)?.[visemeIndex] ?? this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
5819
6511
  }
5820
6512
  collectResolvedExpressionMorphTargets() {
5821
6513
  const targets = [];
@@ -6293,7 +6985,11 @@ var PROFILE_OVERRIDE_KEYS = [
6293
6985
  "rightMorphSuffixes",
6294
6986
  "morphToMesh",
6295
6987
  "auFacePartToMeshCategory",
6988
+ "mappingSections",
6296
6989
  "visemeKeys",
6990
+ "visemeSystemId",
6991
+ "visemeSlots",
6992
+ "visemeBindings",
6297
6993
  "visemeMeshCategory",
6298
6994
  "visemeJawAmounts",
6299
6995
  "auMixDefaults",
@@ -7393,6 +8089,33 @@ function validateMappingConfig(config) {
7393
8089
  }
7394
8090
  visemeSeen.add(key);
7395
8091
  }
8092
+ const morphCategories = new Set(Object.keys(config.morphToMesh || {}));
8093
+ if (config.visemeMeshCategory && !morphCategories.has(config.visemeMeshCategory)) {
8094
+ push(
8095
+ "error",
8096
+ "VISEME_MESH_CATEGORY_MISSING",
8097
+ `visemeMeshCategory "${config.visemeMeshCategory}" is not present in morphToMesh`,
8098
+ { category: config.visemeMeshCategory }
8099
+ );
8100
+ }
8101
+ for (const [facePart, category] of Object.entries(config.auFacePartToMeshCategory || {})) {
8102
+ if (!morphCategories.has(category)) {
8103
+ push(
8104
+ "error",
8105
+ "AU_MESH_CATEGORY_MISSING",
8106
+ `AU facePart "${facePart}" routes to missing morphToMesh category "${category}"`,
8107
+ { facePart, category }
8108
+ );
8109
+ }
8110
+ }
8111
+ if (config.visemeJawAmounts && config.visemeJawAmounts.length !== (config.visemeKeys || []).length) {
8112
+ push(
8113
+ "warning",
8114
+ "VISEME_JAW_AMOUNT_LENGTH_MISMATCH",
8115
+ "visemeJawAmounts length does not match visemeKeys length",
8116
+ { visemeKeys: (config.visemeKeys || []).length, visemeJawAmounts: config.visemeJawAmounts.length }
8117
+ );
8118
+ }
7396
8119
  if (config.auMixDefaults) {
7397
8120
  for (const key of Object.keys(config.auMixDefaults)) {
7398
8121
  const auId = Number(key);
@@ -7846,6 +8569,6 @@ async function analyzeModel(options) {
7846
8569
  };
7847
8570
  }
7848
8571
 
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 };
8572
+ 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
8573
  //# sourceMappingURL=index.js.map
7851
8574
  //# sourceMappingURL=index.js.map