@lovelace_lol/loom3 1.0.42 → 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.cjs CHANGED
@@ -510,14 +510,19 @@ var Y_AXIS = new THREE2.Vector3(0, 1, 0);
510
510
  var Z_AXIS = new THREE2.Vector3(0, 0, 1);
511
511
  var CLIP_EVENT_METADATA_KEY = "__loom3ClipEvents";
512
512
  var CLIP_EVENT_EPSILON = 1e-4;
513
+ var ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX = "__loom3_additive_delta";
513
514
  var BakedAnimationController = class {
514
515
  constructor(host) {
515
516
  __publicField(this, "host");
517
+ // Clip-backed snippets need a later mixer pass so they can override baked additive tracks.
516
518
  __publicField(this, "animationMixer", null);
519
+ __publicField(this, "clipAnimationMixer", null);
517
520
  __publicField(this, "mixerFinishedListenerAttached", false);
521
+ __publicField(this, "clipMixerFinishedListenerAttached", false);
518
522
  __publicField(this, "animationClips", []);
519
523
  __publicField(this, "bakedSourceClips", /* @__PURE__ */ new Map());
520
524
  __publicField(this, "bakedRuntimeActions", /* @__PURE__ */ new Map());
525
+ __publicField(this, "bakedAdditiveRuntimeClips", /* @__PURE__ */ new Map());
521
526
  __publicField(this, "bakedActionGroups", /* @__PURE__ */ new Map());
522
527
  __publicField(this, "bakedRuntimeClipToSource", /* @__PURE__ */ new Map());
523
528
  __publicField(this, "animationActions", /* @__PURE__ */ new Map());
@@ -542,6 +547,173 @@ var BakedAnimationController = class {
542
547
  action.__actionId = actionId;
543
548
  return actionId;
544
549
  }
550
+ clearActionId(action) {
551
+ if (!action) return;
552
+ const actionId = this.getActionId(action);
553
+ if (actionId) {
554
+ this.actionIdToClip.delete(actionId);
555
+ }
556
+ this.actionIds.delete(action);
557
+ delete action.__actionId;
558
+ }
559
+ uncacheClip(clip, mixer = this.animationMixer) {
560
+ if (!clip || !mixer) return;
561
+ try {
562
+ mixer.uncacheClip(clip);
563
+ } catch {
564
+ }
565
+ }
566
+ uncacheAction(action, mixer = this.animationMixer) {
567
+ if (!action || !mixer) return;
568
+ try {
569
+ const clip = action.getClip();
570
+ if (clip) {
571
+ mixer.uncacheAction(clip);
572
+ mixer.uncacheClip(clip);
573
+ }
574
+ } catch {
575
+ }
576
+ }
577
+ releaseBakedRuntimeAction(runtimeClipName) {
578
+ const action = this.bakedRuntimeActions.get(runtimeClipName);
579
+ if (!action) return;
580
+ try {
581
+ action.stop();
582
+ } catch {
583
+ }
584
+ this.uncacheAction(action);
585
+ this.clearActionId(action);
586
+ this.bakedRuntimeActions.delete(runtimeClipName);
587
+ }
588
+ clearBakedAdditiveRuntimeClip(runtimeClipName) {
589
+ const clip = this.bakedAdditiveRuntimeClips.get(runtimeClipName);
590
+ if (!clip) return;
591
+ this.uncacheClip(clip);
592
+ this.bakedAdditiveRuntimeClips.delete(runtimeClipName);
593
+ }
594
+ clearAllBakedAdditiveRuntimeClips() {
595
+ for (const runtimeClipName of Array.from(this.bakedAdditiveRuntimeClips.keys())) {
596
+ this.clearBakedAdditiveRuntimeClip(runtimeClipName);
597
+ }
598
+ }
599
+ resolveTrackTarget(model, parsed) {
600
+ const targetKey = parsed.objectName === "bones" && parsed.objectIndex ? parsed.objectIndex : parsed.nodeName;
601
+ if (!targetKey) {
602
+ return null;
603
+ }
604
+ return model.getObjectByProperty("uuid", targetKey) ?? THREE2.PropertyBinding.findNode(model, targetKey) ?? null;
605
+ }
606
+ getMorphTrackBaseValue(target, propertyIndex) {
607
+ if (!target) {
608
+ return 0;
609
+ }
610
+ const meshTarget = target;
611
+ const influences = meshTarget.morphTargetInfluences;
612
+ if (!influences) {
613
+ return 0;
614
+ }
615
+ let morphIndex;
616
+ if (typeof propertyIndex === "number" && Number.isInteger(propertyIndex)) {
617
+ morphIndex = propertyIndex;
618
+ } else if (typeof propertyIndex === "string") {
619
+ if (/^\d+$/.test(propertyIndex)) {
620
+ morphIndex = Number(propertyIndex);
621
+ } else {
622
+ morphIndex = meshTarget.morphTargetDictionary?.[propertyIndex];
623
+ }
624
+ }
625
+ if (morphIndex === void 0) {
626
+ return 0;
627
+ }
628
+ return influences[morphIndex] ?? 0;
629
+ }
630
+ canCreateFirstFrameReferenceTrack(track) {
631
+ const valueSize = track.getValueSize();
632
+ if (!Number.isFinite(valueSize) || valueSize <= 0 || track.values.length < valueSize) {
633
+ return false;
634
+ }
635
+ return track.ValueTypeName === "number" || track.ValueTypeName === "quaternion" || track.ValueTypeName === "vector";
636
+ }
637
+ createFirstFrameReferenceTrack(track) {
638
+ const valueSize = track.getValueSize();
639
+ if (!this.canCreateFirstFrameReferenceTrack(track)) {
640
+ return null;
641
+ }
642
+ const values = Array.from(track.values.slice(0, valueSize));
643
+ if (track.ValueTypeName === "number") {
644
+ return new THREE2.NumberKeyframeTrack(track.name, [0], values);
645
+ }
646
+ if (track.ValueTypeName === "quaternion") {
647
+ return new THREE2.QuaternionKeyframeTrack(track.name, [0], values);
648
+ }
649
+ if (track.ValueTypeName === "vector") {
650
+ return new THREE2.VectorKeyframeTrack(track.name, [0], values);
651
+ }
652
+ return null;
653
+ }
654
+ createAdditiveReferenceTrack(track, model) {
655
+ const trackName = typeof track?.name === "string" ? track.name : "";
656
+ if (!trackName) {
657
+ return null;
658
+ }
659
+ let parsed;
660
+ try {
661
+ parsed = THREE2.PropertyBinding.parseTrackName(trackName);
662
+ } catch {
663
+ return null;
664
+ }
665
+ const target = this.resolveTrackTarget(model, parsed);
666
+ if (parsed.propertyName === "morphTargetInfluences") {
667
+ return new THREE2.NumberKeyframeTrack(
668
+ track.name,
669
+ [0],
670
+ [this.getMorphTrackBaseValue(target, parsed.propertyIndex)]
671
+ );
672
+ }
673
+ return this.createFirstFrameReferenceTrack(track);
674
+ }
675
+ createAdditiveRuntimeClip(runtimeClip) {
676
+ const model = this.host.getModel();
677
+ if (!model) {
678
+ return null;
679
+ }
680
+ const additiveTracks = [];
681
+ const referenceTracks = [];
682
+ for (const track of runtimeClip.tracks) {
683
+ const referenceTrack = this.createAdditiveReferenceTrack(track, model);
684
+ if (!referenceTrack) {
685
+ continue;
686
+ }
687
+ additiveTracks.push(track.clone());
688
+ referenceTracks.push(referenceTrack);
689
+ }
690
+ const additiveClip = new THREE2.AnimationClip(
691
+ `${runtimeClip.name}${ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX}`,
692
+ runtimeClip.duration,
693
+ additiveTracks
694
+ );
695
+ if (additiveTracks.length > 0) {
696
+ const referenceClip = new THREE2.AnimationClip(
697
+ `${runtimeClip.name}${ADDITIVE_BAKED_RUNTIME_CLIP_SUFFIX}_reference`,
698
+ 0,
699
+ referenceTracks
700
+ );
701
+ THREE2.AnimationUtils.makeClipAdditive(additiveClip, 0, referenceClip);
702
+ }
703
+ return additiveClip;
704
+ }
705
+ getOrCreateBakedAdditiveRuntimeClip(runtimeClip) {
706
+ const cached = this.bakedAdditiveRuntimeClips.get(runtimeClip.name);
707
+ if (cached) {
708
+ return cached;
709
+ }
710
+ const additiveClip = this.createAdditiveRuntimeClip(runtimeClip);
711
+ if (!additiveClip) {
712
+ return null;
713
+ }
714
+ this.bakedAdditiveRuntimeClips.set(runtimeClip.name, additiveClip);
715
+ return additiveClip;
716
+ }
545
717
  setClipEventMetadata(clip, metadata) {
546
718
  const userData = clip.userData ?? (clip.userData = {});
547
719
  userData[CLIP_EVENT_METADATA_KEY] = metadata;
@@ -827,21 +999,28 @@ var BakedAnimationController = class {
827
999
  }
828
1000
  return 0;
829
1001
  }
830
- getOrCreateBakedRuntimeAction(sourceClipName, channel) {
1002
+ getOrCreateBakedRuntimeAction(sourceClipName, channel, blendMode = "replace") {
831
1003
  const bakedClip = this.getBakedSourceClip(sourceClipName);
832
1004
  const runtimeClip = bakedClip?.runtimeClips.find((entry) => entry.channel === channel)?.clip;
833
1005
  if (!runtimeClip) {
834
1006
  return null;
835
1007
  }
1008
+ const desiredClip = blendMode === "additive" ? this.getOrCreateBakedAdditiveRuntimeClip(runtimeClip) : runtimeClip;
1009
+ if (!desiredClip) {
1010
+ return null;
1011
+ }
836
1012
  const existing = this.bakedRuntimeActions.get(runtimeClip.name);
837
- if (existing) {
1013
+ if (existing?.getClip() === desiredClip) {
838
1014
  return existing;
839
1015
  }
840
1016
  this.ensureMixer();
841
1017
  if (!this.animationMixer) {
842
1018
  return null;
843
1019
  }
844
- const action = this.animationMixer.clipAction(runtimeClip);
1020
+ if (existing) {
1021
+ this.releaseBakedRuntimeAction(runtimeClip.name);
1022
+ }
1023
+ const action = this.animationMixer.clipAction(desiredClip);
845
1024
  this.bakedRuntimeActions.set(runtimeClip.name, action);
846
1025
  return action;
847
1026
  }
@@ -859,7 +1038,15 @@ var BakedAnimationController = class {
859
1038
  }
860
1039
  const channelActions = /* @__PURE__ */ new Map();
861
1040
  for (const runtimeClip of bakedClip.runtimeClips) {
862
- const action = this.getOrCreateBakedRuntimeAction(clipName, runtimeClip.channel);
1041
+ const channelBlendMode = resolveBakedChannelBlendMode(
1042
+ runtimeClip.channel,
1043
+ playbackState.requestedBlendMode
1044
+ ) ?? "replace";
1045
+ const action = this.getOrCreateBakedRuntimeAction(
1046
+ clipName,
1047
+ runtimeClip.channel,
1048
+ channelBlendMode
1049
+ );
863
1050
  if (action) {
864
1051
  channelActions.set(runtimeClip.channel, action);
865
1052
  }
@@ -898,13 +1085,39 @@ var BakedAnimationController = class {
898
1085
  }
899
1086
  return getMeshNamesForVisemeProfile(config);
900
1087
  }
1088
+ hasActiveAdditivePlayback() {
1089
+ for (const [clipName, group] of this.bakedActionGroups) {
1090
+ const state = this.playbackState.get(clipName);
1091
+ if (state?.blendMode !== "additive") {
1092
+ continue;
1093
+ }
1094
+ for (const action of group.channelActions.values()) {
1095
+ if (action.isRunning() && !action.paused) {
1096
+ return true;
1097
+ }
1098
+ }
1099
+ }
1100
+ for (const [clipName, action] of this.animationActions) {
1101
+ const state = this.playbackState.get(clipName);
1102
+ if (state?.blendMode !== "additive") {
1103
+ continue;
1104
+ }
1105
+ if (action.isRunning() && !action.paused) {
1106
+ return true;
1107
+ }
1108
+ }
1109
+ return false;
1110
+ }
901
1111
  update(dtSeconds) {
902
1112
  if (this.animationMixer) {
1113
+ this.animationMixer.update(dtSeconds);
1114
+ }
1115
+ if (this.clipAnimationMixer) {
903
1116
  const snapshots = Array.from(this.clipMonitors.values()).map((monitor) => ({
904
1117
  actionId: monitor.actionId,
905
1118
  previousTime: monitor.action.time
906
1119
  }));
907
- this.animationMixer.update(dtSeconds);
1120
+ this.clipAnimationMixer.update(dtSeconds);
908
1121
  for (const { actionId, previousTime } of snapshots) {
909
1122
  const monitor = this.clipMonitors.get(actionId);
910
1123
  if (!monitor) continue;
@@ -923,16 +1136,27 @@ var BakedAnimationController = class {
923
1136
  }
924
1137
  }
925
1138
  }
1139
+ if (this.hasActiveAdditivePlayback()) {
1140
+ this.host.reapplyProceduralState?.();
1141
+ }
926
1142
  }
927
1143
  dispose() {
928
1144
  this.stopAllAnimations();
1145
+ this.clearAllBakedAdditiveRuntimeClips();
929
1146
  if (this.animationMixer) {
930
1147
  this.animationMixer.stopAllAction();
931
1148
  this.animationMixer = null;
932
1149
  }
1150
+ if (this.clipAnimationMixer) {
1151
+ this.clipAnimationMixer.stopAllAction();
1152
+ this.clipAnimationMixer = null;
1153
+ }
1154
+ this.mixerFinishedListenerAttached = false;
1155
+ this.clipMixerFinishedListenerAttached = false;
933
1156
  this.animationClips = [];
934
1157
  this.bakedSourceClips.clear();
935
1158
  this.bakedRuntimeActions.clear();
1159
+ this.bakedAdditiveRuntimeClips.clear();
936
1160
  this.bakedActionGroups.clear();
937
1161
  this.bakedRuntimeClipToSource.clear();
938
1162
  this.animationActions.clear();
@@ -955,6 +1179,8 @@ var BakedAnimationController = class {
955
1179
  if (this.animationMixer) {
956
1180
  for (const bakedClip of this.bakedSourceClips.values()) {
957
1181
  for (const runtimeClip of bakedClip.runtimeClips) {
1182
+ this.releaseBakedRuntimeAction(runtimeClip.clip.name);
1183
+ this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
958
1184
  try {
959
1185
  this.animationMixer.uncacheAction(runtimeClip.clip);
960
1186
  } catch {
@@ -966,6 +1192,7 @@ var BakedAnimationController = class {
966
1192
  }
967
1193
  }
968
1194
  }
1195
+ this.clearAllBakedAdditiveRuntimeClips();
969
1196
  for (const clipName of this.bakedSourceClips.keys()) {
970
1197
  this.playbackState.delete(clipName);
971
1198
  this.clipSources.delete(clipName);
@@ -1006,6 +1233,8 @@ var BakedAnimationController = class {
1006
1233
  if (this.animationMixer) {
1007
1234
  for (const runtimeClip of bakedClip.runtimeClips) {
1008
1235
  const action = this.bakedRuntimeActions.get(runtimeClip.clip.name);
1236
+ this.releaseBakedRuntimeAction(runtimeClip.clip.name);
1237
+ this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
1009
1238
  try {
1010
1239
  this.animationMixer.uncacheAction(runtimeClip.clip);
1011
1240
  } catch {
@@ -1014,7 +1243,6 @@ var BakedAnimationController = class {
1014
1243
  this.animationMixer.uncacheClip(runtimeClip.clip);
1015
1244
  } catch {
1016
1245
  }
1017
- this.bakedRuntimeActions.delete(runtimeClip.clip.name);
1018
1246
  this.bakedRuntimeClipToSource.delete(runtimeClip.clip.name);
1019
1247
  const actionId = this.getActionId(action);
1020
1248
  if (actionId && action) {
@@ -1023,6 +1251,10 @@ var BakedAnimationController = class {
1023
1251
  }
1024
1252
  }
1025
1253
  }
1254
+ for (const runtimeClip of bakedClip.runtimeClips) {
1255
+ this.clearBakedAdditiveRuntimeClip(runtimeClip.clip.name);
1256
+ this.bakedRuntimeActions.delete(runtimeClip.clip.name);
1257
+ }
1026
1258
  this.animationClips = this.animationClips.filter((entry) => entry.name !== clipName);
1027
1259
  this.bakedSourceClips.delete(clipName);
1028
1260
  this.bakedActionGroups.delete(clipName);
@@ -1083,12 +1315,12 @@ var BakedAnimationController = class {
1083
1315
  const actionId = this.getActionId(action);
1084
1316
  const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
1085
1317
  action.stop();
1086
- if (!isBaked && this.animationMixer) {
1318
+ if (!isBaked && this.clipAnimationMixer) {
1087
1319
  try {
1088
1320
  const clip = action.getClip();
1089
1321
  if (clip) {
1090
- this.animationMixer.uncacheAction(clip);
1091
- this.animationMixer.uncacheClip(clip);
1322
+ this.clipAnimationMixer.uncacheAction(clip);
1323
+ this.clipAnimationMixer.uncacheClip(clip);
1092
1324
  }
1093
1325
  } catch {
1094
1326
  }
@@ -1110,11 +1342,11 @@ var BakedAnimationController = class {
1110
1342
  const actionId = this.getActionId(clipAction);
1111
1343
  try {
1112
1344
  clipAction.stop();
1113
- if (this.animationMixer) {
1345
+ if (this.clipAnimationMixer) {
1114
1346
  const clip = clipAction.getClip();
1115
1347
  if (clip) {
1116
- this.animationMixer.uncacheAction(clip);
1117
- this.animationMixer.uncacheClip(clip);
1348
+ this.clipAnimationMixer.uncacheAction(clip);
1349
+ this.clipAnimationMixer.uncacheClip(clip);
1118
1350
  }
1119
1351
  }
1120
1352
  } catch {
@@ -1318,8 +1550,23 @@ var BakedAnimationController = class {
1318
1550
  next.blendMode = this.getBakedAggregateBlendMode(clipName, next);
1319
1551
  const bakedGroup = this.bakedActionGroups.get(clipName);
1320
1552
  if (bakedGroup) {
1321
- for (const [channel, action2] of bakedGroup.channelActions) {
1553
+ for (const [channel, currentAction] of Array.from(bakedGroup.channelActions)) {
1554
+ const channelBlendMode = resolveBakedChannelBlendMode(channel, next.requestedBlendMode) ?? "replace";
1555
+ const previousTime = currentAction.time;
1556
+ const wasActive = currentAction.isRunning() || currentAction.paused;
1557
+ const wasPaused = currentAction.paused;
1558
+ const action2 = this.getOrCreateBakedRuntimeAction(clipName, channel, channelBlendMode);
1559
+ if (!action2) {
1560
+ bakedGroup.channelActions.delete(channel);
1561
+ continue;
1562
+ }
1322
1563
  this.applyPlaybackStateToBakedAction(action2, next, channel);
1564
+ action2.time = Math.max(0, Math.min(action2.getClip().duration, previousTime));
1565
+ if (action2 !== currentAction && wasActive) {
1566
+ action2.play();
1567
+ }
1568
+ action2.paused = wasPaused;
1569
+ bakedGroup.channelActions.set(channel, action2);
1323
1570
  }
1324
1571
  }
1325
1572
  this.setPlaybackState(clipName, next);
@@ -1334,6 +1581,10 @@ var BakedAnimationController = class {
1334
1581
  seekAnimation(clipName, time) {
1335
1582
  const bakedGroup = this.bakedActionGroups.get(clipName);
1336
1583
  if (bakedGroup) {
1584
+ const state = this.getPlaybackStateSnapshot(clipName, {
1585
+ loop: true,
1586
+ source: this.clipSources.get(clipName) ?? "baked"
1587
+ });
1337
1588
  const duration2 = this.getBakedSourceClip(clipName)?.sourceClip.duration ?? 0;
1338
1589
  const clamped = Math.max(0, Math.min(duration2, Number.isFinite(time) ? time : 0));
1339
1590
  for (const action2 of bakedGroup.channelActions.values()) {
@@ -1341,6 +1592,10 @@ var BakedAnimationController = class {
1341
1592
  }
1342
1593
  try {
1343
1594
  this.animationMixer?.update(0);
1595
+ this.clipAnimationMixer?.update(0);
1596
+ if (state.blendMode === "additive") {
1597
+ this.host.reapplyProceduralState?.();
1598
+ }
1344
1599
  } catch {
1345
1600
  }
1346
1601
  return;
@@ -1350,7 +1605,11 @@ var BakedAnimationController = class {
1350
1605
  const duration = action.getClip().duration;
1351
1606
  action.time = Math.max(0, Math.min(duration, Number.isFinite(time) ? time : 0));
1352
1607
  try {
1353
- this.animationMixer?.update(0);
1608
+ this.clipAnimationMixer?.update(0);
1609
+ const state = this.playbackState.get(clipName);
1610
+ if (state?.blendMode === "additive") {
1611
+ this.host.reapplyProceduralState?.();
1612
+ }
1354
1613
  } catch {
1355
1614
  }
1356
1615
  }
@@ -1358,6 +1617,9 @@ var BakedAnimationController = class {
1358
1617
  if (this.animationMixer) {
1359
1618
  this.animationMixer.timeScale = timeScale;
1360
1619
  }
1620
+ if (this.clipAnimationMixer) {
1621
+ this.clipAnimationMixer.timeScale = timeScale;
1622
+ }
1361
1623
  }
1362
1624
  getAnimationState(clipName) {
1363
1625
  const bakedClip = this.getBakedSourceClip(clipName);
@@ -1687,8 +1949,8 @@ var BakedAnimationController = class {
1687
1949
  return clip;
1688
1950
  }
1689
1951
  playClip(clip, options) {
1690
- this.ensureMixer();
1691
- if (!this.animationMixer) {
1952
+ const mixer = this.ensureClipMixer();
1953
+ if (!mixer) {
1692
1954
  console.warn("[Loom3] playClip: No model loaded, cannot create mixer");
1693
1955
  return null;
1694
1956
  }
@@ -1706,7 +1968,7 @@ var BakedAnimationController = class {
1706
1968
  actionId = this.setActionId(action, clip.name);
1707
1969
  }
1708
1970
  if (!action) {
1709
- action = this.animationMixer.clipAction(clip);
1971
+ action = mixer.clipAction(clip);
1710
1972
  actionId = this.setActionId(action, clip.name);
1711
1973
  }
1712
1974
  const existingClip = this.animationClips.find((c) => c.name === clip.name);
@@ -1767,15 +2029,13 @@ var BakedAnimationController = class {
1767
2029
  },
1768
2030
  stop: () => {
1769
2031
  action.stop();
1770
- if (this.animationMixer) {
1771
- try {
1772
- this.animationMixer.uncacheAction(clip);
1773
- } catch {
1774
- }
1775
- try {
1776
- this.animationMixer.uncacheClip(clip);
1777
- } catch {
1778
- }
2032
+ try {
2033
+ mixer.uncacheAction(clip);
2034
+ } catch {
2035
+ }
2036
+ try {
2037
+ mixer.uncacheClip(clip);
2038
+ } catch {
1779
2039
  }
1780
2040
  this.clipActions.delete(clip.name);
1781
2041
  this.animationActions.delete(clip.name);
@@ -1816,7 +2076,7 @@ var BakedAnimationController = class {
1816
2076
  const clamped = Math.max(0, Math.min(clip.duration, t));
1817
2077
  action.time = clamped;
1818
2078
  try {
1819
- this.animationMixer?.update(0);
2079
+ mixer.update(0);
1820
2080
  } catch {
1821
2081
  }
1822
2082
  this.syncClipMonitorTime(monitor, clamped, true);
@@ -1849,16 +2109,16 @@ var BakedAnimationController = class {
1849
2109
  return this.playClip(clip, { ...options, source: options?.source ?? "clip" });
1850
2110
  }
1851
2111
  cleanupSnippet(name) {
1852
- if (!this.animationMixer || !this.host.getModel()) return;
2112
+ if (!this.host.getModel()) return;
1853
2113
  for (const [clipName, action] of Array.from(this.clipActions.entries())) {
1854
2114
  if (clipName === name || clipName.startsWith(`${name}_`)) {
1855
2115
  const actionId = this.getActionId(action);
1856
2116
  try {
1857
2117
  action.stop();
1858
2118
  const clip = action.getClip();
1859
- if (clip) {
1860
- this.animationMixer.uncacheAction(clip);
1861
- this.animationMixer.uncacheClip(clip);
2119
+ if (clip && this.clipAnimationMixer) {
2120
+ this.clipAnimationMixer.uncacheAction(clip);
2121
+ this.clipAnimationMixer.uncacheClip(clip);
1862
2122
  }
1863
2123
  } catch {
1864
2124
  }
@@ -1886,7 +2146,10 @@ var BakedAnimationController = class {
1886
2146
  clipActions: Array.from(this.clipActions.entries()).map(([k, a]) => ({ name: k, actionId: this.getActionId(a) })),
1887
2147
  animationActions: Array.from(this.animationActions.entries()).map(([k, a]) => ({ name: k, actionId: this.getActionId(a) })),
1888
2148
  clipHandles: Array.from(this.clipHandles.entries()).map(([k, h]) => ({ name: k, actionId: h.actionId })),
1889
- mixerActions: (this.animationMixer?._actions || []).map((a) => ({ name: a?.getClip?.()?.name || "", actionId: this.getActionId(a) }))
2149
+ mixerActions: [
2150
+ ...this.animationMixer?._actions || [],
2151
+ ...this.clipAnimationMixer?._actions || []
2152
+ ].map((a) => ({ name: a?.getClip?.()?.name || "", actionId: this.getActionId(a) }))
1890
2153
  });
1891
2154
  console.log("[Loom3] updateClipParams start", debugSnapshot());
1892
2155
  const apply = (action) => {
@@ -2000,35 +2263,48 @@ var BakedAnimationController = class {
2000
2263
  this.animationMixer = new THREE2.AnimationMixer(model);
2001
2264
  }
2002
2265
  if (this.animationMixer && !this.mixerFinishedListenerAttached) {
2003
- this.animationMixer.addEventListener("finished", (event) => {
2004
- const action = event.action;
2005
- const actionId = this.getActionId(action);
2006
- if (actionId) {
2007
- const monitor = this.clipMonitors.get(actionId);
2008
- if (monitor) {
2009
- monitor.finishedPending = true;
2010
- return;
2011
- }
2012
- }
2013
- const clip = action.getClip();
2014
- const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
2015
- if (bakedRuntime) {
2016
- const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
2017
- if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
2018
- group.resolveFinished();
2019
- }
2020
- return;
2021
- }
2022
- const callback = this.animationFinishedCallbacks.get(clip.name);
2023
- if (callback) {
2024
- callback();
2025
- this.animationFinishedCallbacks.delete(clip.name);
2026
- }
2027
- });
2266
+ this.animationMixer.addEventListener("finished", (event) => this.handleMixerFinished(event));
2028
2267
  this.mixerFinishedListenerAttached = true;
2029
2268
  }
2030
2269
  return this.animationMixer;
2031
2270
  }
2271
+ ensureClipMixer() {
2272
+ const model = this.host.getModel();
2273
+ if (!model) return null;
2274
+ if (!this.clipAnimationMixer) {
2275
+ this.clipAnimationMixer = new THREE2.AnimationMixer(model);
2276
+ }
2277
+ if (this.clipAnimationMixer && !this.clipMixerFinishedListenerAttached) {
2278
+ this.clipAnimationMixer.addEventListener("finished", (event) => this.handleMixerFinished(event));
2279
+ this.clipMixerFinishedListenerAttached = true;
2280
+ }
2281
+ return this.clipAnimationMixer;
2282
+ }
2283
+ handleMixerFinished(event) {
2284
+ const action = event.action;
2285
+ const actionId = this.getActionId(action);
2286
+ if (actionId) {
2287
+ const monitor = this.clipMonitors.get(actionId);
2288
+ if (monitor) {
2289
+ monitor.finishedPending = true;
2290
+ return;
2291
+ }
2292
+ }
2293
+ const clip = action.getClip();
2294
+ const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
2295
+ if (bakedRuntime) {
2296
+ const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
2297
+ if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
2298
+ group.resolveFinished();
2299
+ }
2300
+ return;
2301
+ }
2302
+ const callback = this.animationFinishedCallbacks.get(clip.name);
2303
+ if (callback) {
2304
+ callback();
2305
+ this.animationFinishedCallbacks.delete(clip.name);
2306
+ }
2307
+ }
2032
2308
  createAnimationHandle(clipName, action, finishedPromise) {
2033
2309
  return {
2034
2310
  actionId: this.getActionId(action),
@@ -4992,7 +5268,8 @@ var _Loom3 = class _Loom3 {
4992
5268
  getCompositeRotations: () => this.compositeRotations,
4993
5269
  computeSideValues: (base, balance) => this.computeSideValues(base, balance),
4994
5270
  getAUMixWeight: (auId) => this.getAUMixWeight(auId),
4995
- isMixedAU: (auId) => this.isMixedAU(auId)
5271
+ isMixedAU: (auId) => this.isMixedAU(auId),
5272
+ reapplyProceduralState: () => this.reapplyProceduralStateAfterBakedUpdate()
4996
5273
  });
4997
5274
  this.hairPhysics = new HairPhysicsController({
4998
5275
  getMeshByName: (name) => this.meshByName.get(name),
@@ -5703,6 +5980,30 @@ var _Loom3 = class _Loom3 {
5703
5980
  this.model.updateMatrixWorld(true);
5704
5981
  }
5705
5982
  }
5983
+ reapplyProceduralStateAfterBakedUpdate() {
5984
+ if (!this.model) {
5985
+ return;
5986
+ }
5987
+ let hasActiveOverrides = false;
5988
+ for (const [auIdStr, value] of Object.entries(this.auValues)) {
5989
+ if (value <= 0) continue;
5990
+ const auId = Number(auIdStr);
5991
+ if (Number.isNaN(auId)) continue;
5992
+ hasActiveOverrides = true;
5993
+ this.setAU(auId, value, this.auBalances[auId]);
5994
+ }
5995
+ for (let visemeIndex = 0; visemeIndex < this.visemeValues.length; visemeIndex += 1) {
5996
+ const value = this.visemeValues[visemeIndex] ?? 0;
5997
+ if (value <= 0) continue;
5998
+ hasActiveOverrides = true;
5999
+ this.setViseme(visemeIndex, value, this.visemeJawScales[visemeIndex] ?? 1);
6000
+ }
6001
+ if (!hasActiveOverrides) {
6002
+ return;
6003
+ }
6004
+ this.flushPendingComposites();
6005
+ this.model.updateMatrixWorld(true);
6006
+ }
5706
6007
  // ============================================================================
5707
6008
  // MESH CONTROL
5708
6009
  // ============================================================================