@lovelace_lol/loom3 1.0.34 → 1.0.36

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
@@ -79,6 +79,119 @@ function resolveCurveBalance(curveId, globalBalance, balanceMap) {
79
79
  }
80
80
  return clampBalance(globalBalance);
81
81
  }
82
+ var RUNTIME_CLIP_PREFIX = "__loom3_baked_partition__/";
83
+ var FACE_SAFE_TARGET_RE = /(head|neck|jaw|eye|brow|lid|mouth|lip|face|cheek|nose|tongue|teeth)/i;
84
+ var BODY_LIKE_TARGET_RE = /(root|armature|hips?|pelvis|spine|waist|chest|torso|shoulder|arm|forearm|hand|finger|leg|thigh|calf|knee|foot|toe|tail|wing|fin|body|abdomen|clavicle)/i;
85
+ var SCENE_LIKE_TARGET_RE = /(camera|cam|scene|world|global|origin|pivot|cube)/i;
86
+ var CHANNEL_ORDER = ["face", "body", "scene"];
87
+ function getRuntimeClipName(sourceClipName, channel) {
88
+ return `${RUNTIME_CLIP_PREFIX}${sourceClipName}/${channel}`;
89
+ }
90
+ function parseTrackTarget(trackName, model) {
91
+ let parsed;
92
+ try {
93
+ parsed = THREE.PropertyBinding.parseTrackName(trackName);
94
+ } catch {
95
+ return null;
96
+ }
97
+ const targetKey = parsed.objectName === "bones" && parsed.objectIndex ? String(parsed.objectIndex) : parsed.nodeName;
98
+ const target = targetKey ? model.getObjectByProperty("uuid", targetKey) ?? THREE.PropertyBinding.findNode(model, targetKey) : null;
99
+ return {
100
+ propertyName: parsed.propertyName,
101
+ target,
102
+ targetName: target?.name ?? parsed.nodeName ?? ""
103
+ };
104
+ }
105
+ function isSceneTrackTarget(target, targetName) {
106
+ if (!target) return true;
107
+ if (target.isCamera) return true;
108
+ return SCENE_LIKE_TARGET_RE.test(targetName);
109
+ }
110
+ function isFaceSafeTransformTarget(target, targetName, safeTransformTargets) {
111
+ if (target && safeTransformTargets.has(target)) {
112
+ return true;
113
+ }
114
+ if (!targetName) {
115
+ return false;
116
+ }
117
+ if (BODY_LIKE_TARGET_RE.test(targetName) || SCENE_LIKE_TARGET_RE.test(targetName)) {
118
+ return false;
119
+ }
120
+ return FACE_SAFE_TARGET_RE.test(targetName);
121
+ }
122
+ function classifyBakedTrack(track, model, bones) {
123
+ const parsed = parseTrackTarget(track.name, model);
124
+ if (!parsed) {
125
+ return "scene";
126
+ }
127
+ if (parsed.propertyName === "morphTargetInfluences" || parsed.propertyName === "weights") {
128
+ return "face";
129
+ }
130
+ if (isSceneTrackTarget(parsed.target, parsed.targetName)) {
131
+ return "scene";
132
+ }
133
+ if (parsed.propertyName === "quaternion") {
134
+ const safeTransformTargets = new Set(
135
+ Object.values(bones).map((entry) => entry?.obj).filter((entry) => !!entry)
136
+ );
137
+ if (isFaceSafeTransformTarget(parsed.target, parsed.targetName, safeTransformTargets)) {
138
+ return "face";
139
+ }
140
+ }
141
+ return "body";
142
+ }
143
+ function resolveBakedChannelBlendMode(channel, requestedBlendMode) {
144
+ if (channel === "face") {
145
+ return requestedBlendMode === "additive" ? "additive" : "replace";
146
+ }
147
+ if (channel === "body") {
148
+ return "replace";
149
+ }
150
+ return void 0;
151
+ }
152
+ function resolveBakedAggregateBlendMode(channels, requestedBlendMode) {
153
+ if (requestedBlendMode !== "additive") {
154
+ return "replace";
155
+ }
156
+ return channels.some((channel) => channel.channel === "face" && channel.playable && channel.trackCount > 0) ? "additive" : "replace";
157
+ }
158
+ function partitionBakedClip(clip, model, bones) {
159
+ const tracksByChannel = new Map(
160
+ CHANNEL_ORDER.map((channel) => [channel, []])
161
+ );
162
+ for (const track of clip.tracks) {
163
+ const channel = classifyBakedTrack(track, model, bones);
164
+ tracksByChannel.get(channel)?.push(track.clone());
165
+ }
166
+ const runtimeClips = [];
167
+ const channels = [];
168
+ for (const channel of CHANNEL_ORDER) {
169
+ const tracks = tracksByChannel.get(channel) ?? [];
170
+ if (tracks.length === 0) {
171
+ continue;
172
+ }
173
+ const playable = channel !== "scene";
174
+ const blendMode = resolveBakedChannelBlendMode(channel, "additive");
175
+ channels.push({
176
+ channel,
177
+ trackCount: tracks.length,
178
+ playable,
179
+ blendMode
180
+ });
181
+ if (!playable) {
182
+ continue;
183
+ }
184
+ runtimeClips.push({
185
+ channel,
186
+ clip: new THREE.AnimationClip(getRuntimeClipName(clip.name, channel), clip.duration, tracks)
187
+ });
188
+ }
189
+ return {
190
+ sourceClip: clip,
191
+ channels,
192
+ runtimeClips
193
+ };
194
+ }
82
195
 
83
196
  // src/engines/three/AnimationThree.ts
84
197
  var easeInOutQuad = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
@@ -184,6 +297,10 @@ var BakedAnimationController = class {
184
297
  __publicField(this, "animationMixer", null);
185
298
  __publicField(this, "mixerFinishedListenerAttached", false);
186
299
  __publicField(this, "animationClips", []);
300
+ __publicField(this, "bakedSourceClips", /* @__PURE__ */ new Map());
301
+ __publicField(this, "bakedRuntimeActions", /* @__PURE__ */ new Map());
302
+ __publicField(this, "bakedActionGroups", /* @__PURE__ */ new Map());
303
+ __publicField(this, "bakedRuntimeClipToSource", /* @__PURE__ */ new Map());
187
304
  __publicField(this, "animationActions", /* @__PURE__ */ new Map());
188
305
  __publicField(this, "animationFinishedCallbacks", /* @__PURE__ */ new Map());
189
306
  __publicField(this, "clipActions", /* @__PURE__ */ new Map());
@@ -212,6 +329,7 @@ var BakedAnimationController = class {
212
329
  const rawWeight = options?.weight ?? options?.intensity ?? clipOptions?.mixerWeight ?? 1;
213
330
  const weight = Number.isFinite(rawWeight) ? Math.max(0, rawWeight) : 1;
214
331
  const loopMode = options?.loopMode ?? (typeof options?.loop === "boolean" ? options.loop ? "repeat" : "once" : defaults.loop ? "repeat" : "once");
332
+ const requestedBlendMode = options?.blendMode ?? (clipOptions?.mixerAdditive ? "additive" : "replace");
215
333
  return {
216
334
  source: options?.source ?? defaults.source,
217
335
  loop: loopMode !== "once",
@@ -221,7 +339,8 @@ var BakedAnimationController = class {
221
339
  playbackRate,
222
340
  weight,
223
341
  balance: Number.isFinite(options?.balance) ? options?.balance ?? 0 : 0,
224
- blendMode: options?.blendMode ?? (clipOptions?.mixerAdditive ? "additive" : "replace"),
342
+ requestedBlendMode,
343
+ blendMode: requestedBlendMode,
225
344
  easing: options?.easing ?? "linear"
226
345
  };
227
346
  }
@@ -281,15 +400,49 @@ var BakedAnimationController = class {
281
400
  next.balance = Math.max(-1, Math.min(1, options.balance));
282
401
  }
283
402
  if (options.blendMode) {
284
- next.blendMode = options.blendMode;
403
+ next.requestedBlendMode = options.blendMode;
285
404
  } else if (typeof clipOptions?.mixerAdditive === "boolean") {
286
- next.blendMode = clipOptions.mixerAdditive ? "additive" : "replace";
405
+ next.requestedBlendMode = clipOptions.mixerAdditive ? "additive" : "replace";
287
406
  }
407
+ next.blendMode = next.requestedBlendMode;
288
408
  if (options.easing) {
289
409
  next.easing = options.easing;
290
410
  }
291
411
  return next;
292
412
  }
413
+ isBakedSourceClip(clipName) {
414
+ return this.bakedSourceClips.has(clipName);
415
+ }
416
+ getBakedSourceClip(clipName) {
417
+ return this.bakedSourceClips.get(clipName);
418
+ }
419
+ getBakedChannelInfo(clipName, playbackState) {
420
+ const bakedClip = this.getBakedSourceClip(clipName);
421
+ if (!bakedClip) {
422
+ return void 0;
423
+ }
424
+ const requestedBlendMode = playbackState?.requestedBlendMode ?? "replace";
425
+ return bakedClip.channels.map((channel) => ({
426
+ ...channel,
427
+ blendMode: resolveBakedChannelBlendMode(channel.channel, requestedBlendMode)
428
+ }));
429
+ }
430
+ getBakedAggregateBlendMode(clipName, playbackState) {
431
+ const channels = this.getBakedChannelInfo(clipName, playbackState);
432
+ if (!channels) {
433
+ return playbackState?.requestedBlendMode ?? playbackState?.blendMode ?? "replace";
434
+ }
435
+ return resolveBakedAggregateBlendMode(
436
+ channels,
437
+ playbackState?.requestedBlendMode ?? "replace"
438
+ );
439
+ }
440
+ applyPlaybackStateToBakedAction(action, state, channel) {
441
+ this.applyPlaybackState(action, {
442
+ ...state,
443
+ blendMode: resolveBakedChannelBlendMode(channel, state.requestedBlendMode) ?? "replace"
444
+ });
445
+ }
293
446
  resolveStartTime(duration, state, explicitStartTime) {
294
447
  if (typeof explicitStartTime === "number" && Number.isFinite(explicitStartTime)) {
295
448
  return Math.max(0, Math.min(duration, explicitStartTime));
@@ -299,8 +452,13 @@ var BakedAnimationController = class {
299
452
  }
300
453
  return 0;
301
454
  }
302
- getOrCreateBakedAction(clipName) {
303
- const existing = this.animationActions.get(clipName);
455
+ getOrCreateBakedRuntimeAction(sourceClipName, channel) {
456
+ const bakedClip = this.getBakedSourceClip(sourceClipName);
457
+ const runtimeClip = bakedClip?.runtimeClips.find((entry) => entry.channel === channel)?.clip;
458
+ if (!runtimeClip) {
459
+ return null;
460
+ }
461
+ const existing = this.bakedRuntimeActions.get(runtimeClip.name);
304
462
  if (existing) {
305
463
  return existing;
306
464
  }
@@ -308,13 +466,44 @@ var BakedAnimationController = class {
308
466
  if (!this.animationMixer) {
309
467
  return null;
310
468
  }
311
- const clip = this.animationClips.find((entry) => entry.name === clipName);
312
- if (!clip || (this.clipSources.get(clipName) ?? "baked") !== "baked") {
469
+ const action = this.animationMixer.clipAction(runtimeClip);
470
+ this.bakedRuntimeActions.set(runtimeClip.name, action);
471
+ return action;
472
+ }
473
+ getRepresentativeBakedAction(clipName) {
474
+ const group = this.bakedActionGroups.get(clipName);
475
+ if (!group) {
313
476
  return null;
314
477
  }
315
- const action = this.animationMixer.clipAction(clip);
316
- this.animationActions.set(clipName, action);
317
- return action;
478
+ return group.channelActions.values().next().value ?? null;
479
+ }
480
+ createBakedActionGroup(clipName, playbackState) {
481
+ const bakedClip = this.getBakedSourceClip(clipName);
482
+ if (!bakedClip) {
483
+ return null;
484
+ }
485
+ const channelActions = /* @__PURE__ */ new Map();
486
+ for (const runtimeClip of bakedClip.runtimeClips) {
487
+ const action = this.getOrCreateBakedRuntimeAction(clipName, runtimeClip.channel);
488
+ if (action) {
489
+ channelActions.set(runtimeClip.channel, action);
490
+ }
491
+ }
492
+ if (channelActions.size === 0) {
493
+ return null;
494
+ }
495
+ let resolveFinished = () => {
496
+ };
497
+ const finishedPromise = new Promise((resolve) => {
498
+ resolveFinished = resolve;
499
+ });
500
+ return {
501
+ actionId: makeActionId(),
502
+ channelActions,
503
+ pendingFinishedChannels: playbackState.loopMode === "once" ? new Set(channelActions.keys()) : /* @__PURE__ */ new Set(),
504
+ finishedPromise,
505
+ resolveFinished
506
+ };
318
507
  }
319
508
  getMeshNamesForAU(auId, config, explicitMeshNames) {
320
509
  if (explicitMeshNames && explicitMeshNames.length > 0) {
@@ -354,6 +543,10 @@ var BakedAnimationController = class {
354
543
  this.animationMixer = null;
355
544
  }
356
545
  this.animationClips = [];
546
+ this.bakedSourceClips.clear();
547
+ this.bakedRuntimeActions.clear();
548
+ this.bakedActionGroups.clear();
549
+ this.bakedRuntimeClipToSource.clear();
357
550
  this.animationActions.clear();
358
551
  this.animationFinishedCallbacks.clear();
359
552
  this.clipActions.clear();
@@ -362,17 +555,47 @@ var BakedAnimationController = class {
362
555
  this.playbackState.clear();
363
556
  }
364
557
  loadAnimationClips(clips) {
365
- if (!this.host.getModel()) {
558
+ const model = this.host.getModel();
559
+ if (!model) {
366
560
  console.warn("Loom3: Cannot load animation clips before calling onReady()");
367
561
  return;
368
562
  }
563
+ for (const clipName of this.bakedSourceClips.keys()) {
564
+ this.stopAnimation(clipName);
565
+ }
566
+ if (this.animationMixer) {
567
+ for (const bakedClip of this.bakedSourceClips.values()) {
568
+ for (const runtimeClip of bakedClip.runtimeClips) {
569
+ try {
570
+ this.animationMixer.uncacheAction(runtimeClip.clip);
571
+ } catch {
572
+ }
573
+ try {
574
+ this.animationMixer.uncacheClip(runtimeClip.clip);
575
+ } catch {
576
+ }
577
+ }
578
+ }
579
+ }
580
+ for (const clipName of this.bakedSourceClips.keys()) {
581
+ this.playbackState.delete(clipName);
582
+ this.clipSources.delete(clipName);
583
+ }
584
+ this.bakedSourceClips.clear();
585
+ this.bakedRuntimeActions.clear();
586
+ this.bakedActionGroups.clear();
587
+ this.bakedRuntimeClipToSource.clear();
369
588
  this.ensureMixer();
370
- this.animationClips = clips;
371
- for (const clip of this.animationClips) {
372
- this.clipSources.set(clip.name, "baked");
373
- if (!this.animationActions.has(clip.name) && this.animationMixer) {
374
- const action = this.animationMixer.clipAction(clip);
375
- this.animationActions.set(clip.name, action);
589
+ const partitionedClips = clips.map((clip) => partitionBakedClip(clip, model, this.host.getBones()));
590
+ this.animationClips = partitionedClips.map((clip) => clip.sourceClip);
591
+ for (const bakedClip of partitionedClips) {
592
+ this.bakedSourceClips.set(bakedClip.sourceClip.name, bakedClip);
593
+ this.clipSources.set(bakedClip.sourceClip.name, "baked");
594
+ for (const runtimeClip of bakedClip.runtimeClips) {
595
+ this.bakedRuntimeClipToSource.set(runtimeClip.clip.name, {
596
+ sourceClipName: bakedClip.sourceClip.name,
597
+ channel: runtimeClip.channel
598
+ });
376
599
  }
377
600
  }
378
601
  }
@@ -381,84 +604,91 @@ var BakedAnimationController = class {
381
604
  name: clip.name,
382
605
  duration: clip.duration,
383
606
  trackCount: clip.tracks.length,
384
- source: this.clipSources.get(clip.name) ?? "baked"
607
+ source: this.clipSources.get(clip.name) ?? "baked",
608
+ channels: this.getBakedSourceClip(clip.name)?.channels
385
609
  }));
386
610
  }
387
611
  removeAnimationClip(clipName) {
388
- const clip = this.animationClips.find((entry) => entry.name === clipName);
389
- if (!clip || (this.clipSources.get(clipName) ?? "baked") !== "baked") {
612
+ const bakedClip = this.getBakedSourceClip(clipName);
613
+ if (!bakedClip) {
390
614
  return false;
391
615
  }
392
- const relatedActions = /* @__PURE__ */ new Set();
393
- const bakedAction = this.animationActions.get(clipName);
394
- const clipAction = this.clipActions.get(clipName);
395
- if (bakedAction) relatedActions.add(bakedAction);
396
- if (clipAction) relatedActions.add(clipAction);
397
616
  this.stopAnimation(clipName);
398
617
  if (this.animationMixer) {
399
- for (const action of relatedActions) {
618
+ for (const runtimeClip of bakedClip.runtimeClips) {
619
+ const action = this.bakedRuntimeActions.get(runtimeClip.clip.name);
400
620
  try {
401
- this.animationMixer.uncacheAction(clip);
621
+ this.animationMixer.uncacheAction(runtimeClip.clip);
402
622
  } catch {
403
623
  }
404
624
  try {
405
- this.animationMixer.uncacheClip(clip);
625
+ this.animationMixer.uncacheClip(runtimeClip.clip);
406
626
  } catch {
407
627
  }
628
+ this.bakedRuntimeActions.delete(runtimeClip.clip.name);
629
+ this.bakedRuntimeClipToSource.delete(runtimeClip.clip.name);
408
630
  const actionId = this.getActionId(action);
409
- if (actionId) {
631
+ if (actionId && action) {
410
632
  this.actionIdToClip.delete(actionId);
633
+ this.actionIds.delete(action);
411
634
  }
412
- this.actionIds.delete(action);
413
635
  }
414
636
  }
415
637
  this.animationClips = this.animationClips.filter((entry) => entry.name !== clipName);
416
- this.animationActions.delete(clipName);
417
- this.clipActions.delete(clipName);
418
- this.clipHandles.delete(clipName);
419
- this.animationFinishedCallbacks.delete(clipName);
638
+ this.bakedSourceClips.delete(clipName);
639
+ this.bakedActionGroups.delete(clipName);
420
640
  this.playbackState.delete(clipName);
421
641
  this.clipSources.delete(clipName);
422
642
  return true;
423
643
  }
424
644
  playAnimation(clipName, options = {}) {
425
- const action = this.getOrCreateBakedAction(clipName);
426
- if (!action) {
645
+ const bakedClip = this.getBakedSourceClip(clipName);
646
+ if (!bakedClip) {
427
647
  console.warn(`Loom3: Animation clip "${clipName}" not found`);
428
648
  return null;
429
649
  }
430
- if (!this.getActionId(action)) {
431
- this.setActionId(action, clipName);
432
- }
433
650
  const playbackState = this.mergePlaybackOptions(
434
651
  this.getPlaybackStateSnapshot(clipName, { loop: true, source: "baked" }),
435
652
  options
436
653
  );
654
+ playbackState.blendMode = this.getBakedAggregateBlendMode(clipName, playbackState);
655
+ const actionGroup = this.createBakedActionGroup(clipName, playbackState);
656
+ if (!actionGroup) {
657
+ console.warn(`Loom3: Animation clip "${clipName}" has no character-runtime channels to play`);
658
+ return null;
659
+ }
437
660
  const crossfadeDuration = options.crossfadeDuration ?? 0;
438
661
  const clampWhenFinished = options.clampWhenFinished ?? playbackState.loopMode === "once";
439
- const startTime = this.resolveStartTime(action.getClip().duration, playbackState, options.startTime);
440
- this.applyPlaybackState(action, playbackState);
441
- action.clampWhenFinished = clampWhenFinished;
442
- if (crossfadeDuration > 0) {
443
- action.reset();
444
- action.fadeIn(crossfadeDuration);
445
- } else {
446
- action.reset();
662
+ const startTime = this.resolveStartTime(bakedClip.sourceClip.duration, playbackState, options.startTime);
663
+ for (const [channel, action] of actionGroup.channelActions) {
664
+ this.applyPlaybackStateToBakedAction(action, playbackState, channel);
665
+ action.clampWhenFinished = clampWhenFinished;
666
+ if (crossfadeDuration > 0) {
667
+ action.reset();
668
+ action.fadeIn(crossfadeDuration);
669
+ } else {
670
+ action.reset();
671
+ }
672
+ action.time = startTime;
673
+ action.play();
447
674
  }
448
- action.time = startTime;
449
- action.play();
450
- this.animationActions.set(clipName, action);
675
+ this.bakedActionGroups.set(clipName, actionGroup);
451
676
  this.setPlaybackState(clipName, playbackState);
452
- let resolveFinished;
453
- const finishedPromise = new Promise((resolve) => {
454
- resolveFinished = resolve;
455
- });
456
- if (playbackState.loopMode === "once") {
457
- this.animationFinishedCallbacks.set(clipName, () => resolveFinished());
458
- }
459
- return this.createAnimationHandle(clipName, action, finishedPromise);
677
+ return this.createBakedAnimationHandle(clipName, actionGroup);
460
678
  }
461
679
  stopAnimation(clipName) {
680
+ const bakedGroup = this.bakedActionGroups.get(clipName);
681
+ if (bakedGroup) {
682
+ for (const action2 of bakedGroup.channelActions.values()) {
683
+ action2.stop();
684
+ try {
685
+ action2.paused = false;
686
+ } catch {
687
+ }
688
+ }
689
+ this.bakedActionGroups.delete(clipName);
690
+ return;
691
+ }
462
692
  const action = this.animationActions.get(clipName);
463
693
  if (action) {
464
694
  const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
@@ -506,6 +736,7 @@ var BakedAnimationController = class {
506
736
  }
507
737
  stopAllAnimations() {
508
738
  for (const clipName of /* @__PURE__ */ new Set([
739
+ ...this.bakedActionGroups.keys(),
509
740
  ...this.animationActions.keys(),
510
741
  ...this.clipActions.keys()
511
742
  ])) {
@@ -513,18 +744,39 @@ var BakedAnimationController = class {
513
744
  }
514
745
  }
515
746
  pauseAnimation(clipName) {
747
+ const bakedGroup = this.bakedActionGroups.get(clipName);
748
+ if (bakedGroup) {
749
+ for (const action2 of bakedGroup.channelActions.values()) {
750
+ action2.paused = true;
751
+ }
752
+ return;
753
+ }
516
754
  const action = this.animationActions.get(clipName);
517
755
  if (action) {
518
756
  action.paused = true;
519
757
  }
520
758
  }
521
759
  resumeAnimation(clipName) {
760
+ const bakedGroup = this.bakedActionGroups.get(clipName);
761
+ if (bakedGroup) {
762
+ for (const action2 of bakedGroup.channelActions.values()) {
763
+ action2.paused = false;
764
+ }
765
+ return;
766
+ }
522
767
  const action = this.animationActions.get(clipName);
523
768
  if (action) {
524
769
  action.paused = false;
525
770
  }
526
771
  }
527
772
  pauseAllAnimations() {
773
+ for (const group of this.bakedActionGroups.values()) {
774
+ for (const action of group.channelActions.values()) {
775
+ if (action.isRunning()) {
776
+ action.paused = true;
777
+ }
778
+ }
779
+ }
528
780
  for (const action of this.animationActions.values()) {
529
781
  if (action.isRunning()) {
530
782
  action.paused = true;
@@ -532,6 +784,13 @@ var BakedAnimationController = class {
532
784
  }
533
785
  }
534
786
  resumeAllAnimations() {
787
+ for (const group of this.bakedActionGroups.values()) {
788
+ for (const action of group.channelActions.values()) {
789
+ if (action.paused) {
790
+ action.paused = false;
791
+ }
792
+ }
793
+ }
535
794
  for (const action of this.animationActions.values()) {
536
795
  if (action.paused) {
537
796
  action.paused = false;
@@ -539,76 +798,161 @@ var BakedAnimationController = class {
539
798
  }
540
799
  }
541
800
  setAnimationSpeed(clipName, speed) {
542
- const action = this.getOrCreateBakedAction(clipName);
543
- if (action) {
801
+ if (this.isBakedSourceClip(clipName)) {
544
802
  const next = this.getPlaybackStateSnapshot(clipName, {
545
803
  loop: true,
546
804
  source: this.clipSources.get(clipName) ?? "baked"
547
805
  });
548
806
  next.playbackRate = Number.isFinite(speed) ? Math.max(0, Math.abs(speed)) : 1;
807
+ const bakedGroup = this.bakedActionGroups.get(clipName);
808
+ if (bakedGroup) {
809
+ for (const [channel, action2] of bakedGroup.channelActions) {
810
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
811
+ }
812
+ }
813
+ this.setPlaybackState(clipName, next);
814
+ return;
815
+ }
816
+ const action = this.animationActions.get(clipName);
817
+ if (action) {
818
+ const next = this.getPlaybackStateSnapshot(clipName, {
819
+ loop: true,
820
+ source: this.clipSources.get(clipName) ?? "clip"
821
+ });
822
+ next.playbackRate = Number.isFinite(speed) ? Math.max(0, Math.abs(speed)) : 1;
549
823
  this.applyPlaybackState(action, next);
550
824
  this.setPlaybackState(clipName, next);
551
825
  }
552
826
  }
553
827
  setAnimationIntensity(clipName, intensity) {
554
- const action = this.getOrCreateBakedAction(clipName);
555
- if (action) {
828
+ if (this.isBakedSourceClip(clipName)) {
556
829
  const next = this.getPlaybackStateSnapshot(clipName, {
557
830
  loop: true,
558
831
  source: this.clipSources.get(clipName) ?? "baked"
559
832
  });
560
833
  next.weight = Number.isFinite(intensity) ? Math.max(0, intensity) : 1;
834
+ const bakedGroup = this.bakedActionGroups.get(clipName);
835
+ if (bakedGroup) {
836
+ for (const [channel, action2] of bakedGroup.channelActions) {
837
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
838
+ }
839
+ }
840
+ this.setPlaybackState(clipName, next);
841
+ return;
842
+ }
843
+ const action = this.animationActions.get(clipName);
844
+ if (action) {
845
+ const next = this.getPlaybackStateSnapshot(clipName, {
846
+ loop: true,
847
+ source: this.clipSources.get(clipName) ?? "clip"
848
+ });
849
+ next.weight = Number.isFinite(intensity) ? Math.max(0, intensity) : 1;
561
850
  action.setEffectiveWeight(next.weight);
562
851
  this.setPlaybackState(clipName, next);
563
852
  }
564
853
  }
565
854
  setAnimationLoopMode(clipName, loopMode) {
566
- const action = this.getOrCreateBakedAction(clipName);
567
- if (!action) return;
568
855
  const next = this.getPlaybackStateSnapshot(clipName, {
569
856
  loop: true,
570
- source: this.clipSources.get(clipName) ?? "baked"
857
+ source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
571
858
  });
572
859
  next.loopMode = loopMode;
573
860
  next.loop = loopMode !== "once";
861
+ if (this.isBakedSourceClip(clipName)) {
862
+ const bakedGroup = this.bakedActionGroups.get(clipName);
863
+ if (bakedGroup) {
864
+ for (const [channel, action2] of bakedGroup.channelActions) {
865
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
866
+ }
867
+ }
868
+ this.setPlaybackState(clipName, next);
869
+ return;
870
+ }
871
+ const action = this.animationActions.get(clipName);
872
+ if (!action) return;
574
873
  this.applyPlaybackState(action, next);
575
874
  this.setPlaybackState(clipName, next);
576
875
  }
577
876
  setAnimationRepeatCount(clipName, repeatCount) {
578
- const action = this.getOrCreateBakedAction(clipName);
579
- if (!action) return;
580
877
  const next = this.getPlaybackStateSnapshot(clipName, {
581
878
  loop: true,
582
- source: this.clipSources.get(clipName) ?? "baked"
879
+ source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
583
880
  });
584
881
  next.repeatCount = typeof repeatCount === "number" && Number.isFinite(repeatCount) ? Math.max(0, repeatCount) : void 0;
882
+ if (this.isBakedSourceClip(clipName)) {
883
+ const bakedGroup = this.bakedActionGroups.get(clipName);
884
+ if (bakedGroup) {
885
+ for (const [channel, action2] of bakedGroup.channelActions) {
886
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
887
+ }
888
+ }
889
+ this.setPlaybackState(clipName, next);
890
+ return;
891
+ }
892
+ const action = this.animationActions.get(clipName);
893
+ if (!action) return;
585
894
  this.applyPlaybackState(action, next);
586
895
  this.setPlaybackState(clipName, next);
587
896
  }
588
897
  setAnimationReverse(clipName, reverse) {
589
- const action = this.getOrCreateBakedAction(clipName);
590
- if (!action) return;
591
898
  const next = this.getPlaybackStateSnapshot(clipName, {
592
899
  loop: true,
593
- source: this.clipSources.get(clipName) ?? "baked"
900
+ source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
594
901
  });
595
902
  next.reverse = !!reverse;
903
+ if (this.isBakedSourceClip(clipName)) {
904
+ const bakedGroup = this.bakedActionGroups.get(clipName);
905
+ if (bakedGroup) {
906
+ for (const [channel, action2] of bakedGroup.channelActions) {
907
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
908
+ }
909
+ }
910
+ this.setPlaybackState(clipName, next);
911
+ return;
912
+ }
913
+ const action = this.animationActions.get(clipName);
914
+ if (!action) return;
596
915
  this.applyPlaybackState(action, next);
597
916
  this.setPlaybackState(clipName, next);
598
917
  }
599
918
  setAnimationBlendMode(clipName, blendMode) {
600
- const action = this.getOrCreateBakedAction(clipName);
601
- if (!action) return;
602
919
  const next = this.getPlaybackStateSnapshot(clipName, {
603
920
  loop: true,
604
- source: this.clipSources.get(clipName) ?? "baked"
921
+ source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
605
922
  });
923
+ next.requestedBlendMode = blendMode;
924
+ if (this.isBakedSourceClip(clipName)) {
925
+ next.blendMode = this.getBakedAggregateBlendMode(clipName, next);
926
+ const bakedGroup = this.bakedActionGroups.get(clipName);
927
+ if (bakedGroup) {
928
+ for (const [channel, action2] of bakedGroup.channelActions) {
929
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
930
+ }
931
+ }
932
+ this.setPlaybackState(clipName, next);
933
+ return;
934
+ }
606
935
  next.blendMode = blendMode;
936
+ const action = this.animationActions.get(clipName);
937
+ if (!action) return;
607
938
  this.applyPlaybackState(action, next);
608
939
  this.setPlaybackState(clipName, next);
609
940
  }
610
941
  seekAnimation(clipName, time) {
611
- const action = this.getOrCreateBakedAction(clipName) ?? this.animationActions.get(clipName);
942
+ const bakedGroup = this.bakedActionGroups.get(clipName);
943
+ if (bakedGroup) {
944
+ const duration2 = this.getBakedSourceClip(clipName)?.sourceClip.duration ?? 0;
945
+ const clamped = Math.max(0, Math.min(duration2, Number.isFinite(time) ? time : 0));
946
+ for (const action2 of bakedGroup.channelActions.values()) {
947
+ action2.time = clamped;
948
+ }
949
+ try {
950
+ this.animationMixer?.update(0);
951
+ } catch {
952
+ }
953
+ return;
954
+ }
955
+ const action = this.animationActions.get(clipName);
612
956
  if (!action) return;
613
957
  const duration = action.getClip().duration;
614
958
  action.time = Math.max(0, Math.min(duration, Number.isFinite(time) ? time : 0));
@@ -623,6 +967,40 @@ var BakedAnimationController = class {
623
967
  }
624
968
  }
625
969
  getAnimationState(clipName) {
970
+ const bakedClip = this.getBakedSourceClip(clipName);
971
+ if (bakedClip) {
972
+ const state2 = this.playbackState.get(clipName);
973
+ const action2 = this.getRepresentativeBakedAction(clipName);
974
+ if (!state2 && !action2) {
975
+ return null;
976
+ }
977
+ const loopMode2 = state2?.loopMode ?? (action2?.loop === THREE.LoopPingPong ? "pingpong" : action2?.loop === THREE.LoopOnce ? "once" : "repeat");
978
+ const playbackRate2 = state2?.playbackRate ?? Math.abs(action2?.getEffectiveTimeScale?.() ?? 1);
979
+ const reverse2 = state2?.reverse ?? (action2?.getEffectiveTimeScale?.() ?? 1) < 0;
980
+ const pausedValues = this.bakedActionGroups.get(clipName) ? Array.from(this.bakedActionGroups.get(clipName).channelActions.values()).map((entry) => entry.paused) : [];
981
+ return {
982
+ name: bakedClip.sourceClip.name,
983
+ actionId: this.bakedActionGroups.get(clipName)?.actionId,
984
+ source: state2?.source ?? this.clipSources.get(clipName) ?? "baked",
985
+ isPlaying: this.bakedActionGroups.get(clipName) ? Array.from(this.bakedActionGroups.get(clipName).channelActions.values()).some((entry) => entry.isRunning() && !entry.paused) : false,
986
+ isPaused: pausedValues.length > 0 ? pausedValues.every(Boolean) : false,
987
+ time: action2?.time ?? 0,
988
+ duration: bakedClip.sourceClip.duration,
989
+ speed: playbackRate2,
990
+ playbackRate: playbackRate2,
991
+ reverse: reverse2,
992
+ weight: state2?.weight ?? action2?.getEffectiveWeight?.() ?? 1,
993
+ balance: state2?.balance ?? 0,
994
+ requestedBlendMode: state2?.requestedBlendMode ?? "replace",
995
+ blendMode: this.getBakedAggregateBlendMode(clipName, state2),
996
+ channels: this.getBakedChannelInfo(clipName, state2),
997
+ easing: state2?.easing ?? "linear",
998
+ loop: loopMode2 !== "once",
999
+ loopMode: loopMode2,
1000
+ repeatCount: state2?.repeatCount,
1001
+ isLooping: loopMode2 !== "once"
1002
+ };
1003
+ }
626
1004
  const action = this.animationActions.get(clipName);
627
1005
  if (!action) return null;
628
1006
  const clip = action.getClip();
@@ -643,7 +1021,9 @@ var BakedAnimationController = class {
643
1021
  reverse,
644
1022
  weight: state?.weight ?? action.getEffectiveWeight(),
645
1023
  balance: state?.balance ?? 0,
1024
+ requestedBlendMode: state?.requestedBlendMode ?? state?.blendMode ?? "replace",
646
1025
  blendMode: state?.blendMode ?? "replace",
1026
+ channels: state?.source === "baked" ? this.getBakedChannelInfo(clipName, state) : void 0,
647
1027
  easing: state?.easing ?? "linear",
648
1028
  loop: loopMode !== "once",
649
1029
  loopMode,
@@ -653,6 +1033,12 @@ var BakedAnimationController = class {
653
1033
  }
654
1034
  getPlayingAnimations() {
655
1035
  const playing = [];
1036
+ for (const name of this.bakedActionGroups.keys()) {
1037
+ const state = this.getAnimationState(name);
1038
+ if (state?.isPlaying) {
1039
+ playing.push(state);
1040
+ }
1041
+ }
656
1042
  for (const [name, action] of this.animationActions) {
657
1043
  if (action.isRunning()) {
658
1044
  const state = this.getAnimationState(name);
@@ -662,6 +1048,13 @@ var BakedAnimationController = class {
662
1048
  return playing;
663
1049
  }
664
1050
  crossfadeTo(clipName, duration = 0.3, options = {}) {
1051
+ for (const group of this.bakedActionGroups.values()) {
1052
+ for (const action of group.channelActions.values()) {
1053
+ if (action.isRunning()) {
1054
+ action.fadeOut(duration);
1055
+ }
1056
+ }
1057
+ }
665
1058
  for (const action of this.animationActions.values()) {
666
1059
  if (action.isRunning()) {
667
1060
  action.fadeOut(duration);
@@ -1187,6 +1580,14 @@ var BakedAnimationController = class {
1187
1580
  this.animationMixer.addEventListener("finished", (event) => {
1188
1581
  const action = event.action;
1189
1582
  const clip = action.getClip();
1583
+ const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
1584
+ if (bakedRuntime) {
1585
+ const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
1586
+ if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
1587
+ group.resolveFinished();
1588
+ }
1589
+ return;
1590
+ }
1190
1591
  const callback = this.animationFinishedCallbacks.get(clip.name);
1191
1592
  if (callback) {
1192
1593
  callback();
@@ -1211,6 +1612,20 @@ var BakedAnimationController = class {
1211
1612
  finished: finishedPromise
1212
1613
  };
1213
1614
  }
1615
+ createBakedAnimationHandle(clipName, group) {
1616
+ return {
1617
+ actionId: group.actionId,
1618
+ stop: () => this.stopAnimation(clipName),
1619
+ pause: () => this.pauseAnimation(clipName),
1620
+ resume: () => this.resumeAnimation(clipName),
1621
+ setSpeed: (speed) => this.setAnimationSpeed(clipName, speed),
1622
+ setWeight: (weight) => this.setAnimationIntensity(clipName, weight),
1623
+ seekTo: (time) => this.seekAnimation(clipName, time),
1624
+ getState: () => this.getAnimationState(clipName),
1625
+ crossfadeTo: (targetClip, dur) => this.crossfadeTo(targetClip, dur),
1626
+ finished: group.finishedPromise
1627
+ };
1628
+ }
1214
1629
  };
1215
1630
 
1216
1631
  // src/presets/cc4.ts
@@ -3937,7 +4352,8 @@ var _Loom3 = class _Loom3 {
3937
4352
  __publicField(this, "bones", {});
3938
4353
  __publicField(this, "mixWeights", {});
3939
4354
  // Viseme state
3940
- __publicField(this, "visemeValues", new Array(15).fill(0));
4355
+ __publicField(this, "visemeValues", []);
4356
+ __publicField(this, "visemeJawScales", []);
3941
4357
  __publicField(this, "bakedAnimations");
3942
4358
  __publicField(this, "hairPhysics");
3943
4359
  // Internal animation loop
@@ -3950,6 +4366,7 @@ var _Loom3 = class _Loom3 {
3950
4366
  const basePreset = config.presetType ? getPreset(config.presetType) : CC4_PRESET;
3951
4367
  this.config = extendPresetWithProfile(basePreset, config.profile);
3952
4368
  this.mixWeights = { ...this.config.auMixDefaults };
4369
+ this.syncVisemeRuntimeState();
3953
4370
  this.animation = animation || new AnimationThree();
3954
4371
  this.compositeRotations = this.config.compositeRotations || COMPOSITE_ROTATIONS;
3955
4372
  this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
@@ -4482,6 +4899,7 @@ var _Loom3 = class _Loom3 {
4482
4899
  if (visemeIndex < 0 || visemeIndex >= this.config.visemeKeys.length) return;
4483
4900
  const val = clamp012(value);
4484
4901
  this.visemeValues[visemeIndex] = val;
4902
+ this.visemeJawScales[visemeIndex] = jawScale;
4485
4903
  const targets = this.resolvedVisemeTargets[visemeIndex];
4486
4904
  if (targets && targets.length > 0) {
4487
4905
  this.applyMorphTargets(targets, val);
@@ -4494,7 +4912,7 @@ var _Loom3 = class _Loom3 {
4494
4912
  this.setMorph(morphKey, val, visemeMeshNames);
4495
4913
  }
4496
4914
  }
4497
- const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * val * jawScale;
4915
+ const jawAmount = this.getVisemeJawAmount(visemeIndex) * val * jawScale;
4498
4916
  if (Math.abs(jawScale) > 1e-6 && Math.abs(jawAmount) > 1e-6) {
4499
4917
  this.updateBoneRotation("JAW", "pitch", jawAmount);
4500
4918
  }
@@ -4509,9 +4927,10 @@ var _Loom3 = class _Loom3 {
4509
4927
  const morphKey = this.config.visemeKeys[visemeIndex];
4510
4928
  const target = clamp012(to);
4511
4929
  this.visemeValues[visemeIndex] = target;
4930
+ this.visemeJawScales[visemeIndex] = jawScale;
4512
4931
  const visemeMeshNames = this.getMeshNamesForViseme();
4513
4932
  const morphHandle = typeof morphKey === "number" ? this.transitionMorphInfluence(morphKey, target, durationMs, visemeMeshNames) : this.transitionMorph(morphKey, target, durationMs, visemeMeshNames);
4514
- const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * target * jawScale;
4933
+ const jawAmount = this.getVisemeJawAmount(visemeIndex) * target * jawScale;
4515
4934
  if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
4516
4935
  return morphHandle;
4517
4936
  }
@@ -4565,6 +4984,9 @@ var _Loom3 = class _Loom3 {
4565
4984
  }
4566
4985
  resetToNeutral() {
4567
4986
  this.auValues = {};
4987
+ this.visemeValues = new Array(this.config.visemeKeys.length).fill(0);
4988
+ this.visemeJawScales = new Array(this.config.visemeKeys.length).fill(1);
4989
+ this.translations = {};
4568
4990
  this.initBoneRotations();
4569
4991
  this.clearTransitions();
4570
4992
  for (const m of this.meshes) {
@@ -4580,6 +5002,33 @@ var _Loom3 = class _Loom3 {
4580
5002
  entry.obj.quaternion.copy(entry.baseQuat);
4581
5003
  });
4582
5004
  }
5005
+ reinitializeRuntimeStateFromCurrentControls(staleMorphTargets = []) {
5006
+ this.clearTransitions();
5007
+ this.resetMorphTargetHandles(staleMorphTargets);
5008
+ this.translations = {};
5009
+ this.initBoneRotations();
5010
+ Object.values(this.bones).forEach((entry) => {
5011
+ if (!entry) return;
5012
+ entry.obj.position.copy(entry.basePos);
5013
+ entry.obj.quaternion.copy(entry.baseQuat);
5014
+ entry.obj.updateMatrixWorld(false);
5015
+ });
5016
+ for (const [auIdStr, value] of Object.entries(this.auValues)) {
5017
+ if (value <= 0) continue;
5018
+ const auId = Number(auIdStr);
5019
+ if (Number.isNaN(auId)) continue;
5020
+ this.setAU(auId, value, this.auBalances[auId]);
5021
+ }
5022
+ for (let visemeIndex = 0; visemeIndex < this.visemeValues.length; visemeIndex += 1) {
5023
+ const value = this.visemeValues[visemeIndex] ?? 0;
5024
+ if (value <= 0) continue;
5025
+ this.setViseme(visemeIndex, value, this.visemeJawScales[visemeIndex] ?? 1);
5026
+ }
5027
+ if (this.model) {
5028
+ this.flushPendingComposites();
5029
+ this.model.updateMatrixWorld(true);
5030
+ }
5031
+ }
4583
5032
  // ============================================================================
4584
5033
  // MESH CONTROL
4585
5034
  // ============================================================================
@@ -4810,12 +5259,20 @@ var _Loom3 = class _Loom3 {
4810
5259
  }
4811
5260
  setProfile(profile) {
4812
5261
  this.config = profile;
5262
+ this.compositeRotations = this.config.compositeRotations || COMPOSITE_ROTATIONS;
5263
+ this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
4813
5264
  this.mixWeights = { ...profile.auMixDefaults };
5265
+ this.syncVisemeRuntimeState();
5266
+ let staleMorphTargets = [];
4814
5267
  if (this.model) {
5268
+ staleMorphTargets = this.collectResolvedExpressionMorphTargets();
5269
+ this.bones = this.resolveBones(this.model);
5270
+ this.missingBoneWarnings.clear();
4815
5271
  this.rebuildMorphTargetsCache();
4816
5272
  }
4817
5273
  this.hairPhysics.refreshMeshSelection();
4818
5274
  this.applyHairPhysicsProfileConfig();
5275
+ this.reinitializeRuntimeStateFromCurrentControls(staleMorphTargets);
4819
5276
  }
4820
5277
  getProfile() {
4821
5278
  return this.config;
@@ -4953,6 +5410,39 @@ var _Loom3 = class _Loom3 {
4953
5410
  getMorphIndexCacheKey(index, meshNames) {
4954
5411
  return meshNames?.length ? `idx:${index}@${meshNames.join(",")}` : `idx:${index}`;
4955
5412
  }
5413
+ syncVisemeRuntimeState() {
5414
+ const visemeCount = this.config.visemeKeys.length;
5415
+ this.visemeValues = Array.from(
5416
+ { length: visemeCount },
5417
+ (_, index) => this.visemeValues[index] ?? 0
5418
+ );
5419
+ this.visemeJawScales = Array.from(
5420
+ { length: visemeCount },
5421
+ (_, index) => this.visemeJawScales[index] ?? 1
5422
+ );
5423
+ }
5424
+ getVisemeJawAmount(visemeIndex) {
5425
+ return this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
5426
+ }
5427
+ collectResolvedExpressionMorphTargets() {
5428
+ const targets = [];
5429
+ for (const resolved of this.resolvedAUMorphTargets.values()) {
5430
+ targets.push(...resolved.left, ...resolved.right, ...resolved.center);
5431
+ }
5432
+ for (const resolved of this.resolvedVisemeTargets) {
5433
+ if (resolved?.length) {
5434
+ targets.push(...resolved);
5435
+ }
5436
+ }
5437
+ return targets;
5438
+ }
5439
+ resetMorphTargetHandles(targets) {
5440
+ for (const { infl, idx } of targets) {
5441
+ if (idx < infl.length) {
5442
+ infl[idx] = 0;
5443
+ }
5444
+ }
5445
+ }
4956
5446
  isMixedAU(id) {
4957
5447
  const morphs = this.config.auToMorphs[id];
4958
5448
  const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
@@ -5094,12 +5584,25 @@ var _Loom3 = class _Loom3 {
5094
5584
  }
5095
5585
  resolveBones(root) {
5096
5586
  const resolved = {};
5587
+ const previousBones = this.bones;
5097
5588
  const snapshot = (obj) => ({
5098
5589
  obj,
5099
5590
  basePos: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
5100
5591
  baseQuat: obj.quaternion.clone(),
5101
5592
  baseEuler: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, order: obj.rotation.order }
5102
5593
  });
5594
+ const snapshotPreservingBasePose = (obj) => {
5595
+ const existing = Object.values(previousBones).find((entry) => entry?.obj === obj);
5596
+ if (!existing) {
5597
+ return snapshot(obj);
5598
+ }
5599
+ return {
5600
+ obj,
5601
+ basePos: { ...existing.basePos },
5602
+ baseQuat: existing.baseQuat.clone(),
5603
+ baseEuler: { ...existing.baseEuler }
5604
+ };
5605
+ };
5103
5606
  const prefix = this.config.bonePrefix || "";
5104
5607
  const suffix = this.config.boneSuffix || "";
5105
5608
  const suffixRegex = this.config.suffixPattern ? new RegExp(this.config.suffixPattern) : null;
@@ -5130,19 +5633,19 @@ var _Loom3 = class _Loom3 {
5130
5633
  for (const [key, nodeName] of Object.entries(this.config.boneNodes)) {
5131
5634
  const node = findNode(nodeName);
5132
5635
  if (node) {
5133
- resolved[key] = snapshot(node);
5636
+ resolved[key] = snapshotPreservingBasePose(node);
5134
5637
  }
5135
5638
  }
5136
5639
  if (!resolved.EYE_L && this.config.eyeMeshNodes) {
5137
5640
  const node = findNode(this.config.eyeMeshNodes.LEFT);
5138
5641
  if (node) {
5139
- resolved.EYE_L = snapshot(node);
5642
+ resolved.EYE_L = snapshotPreservingBasePose(node);
5140
5643
  }
5141
5644
  }
5142
5645
  if (!resolved.EYE_R && this.config.eyeMeshNodes) {
5143
5646
  const node = findNode(this.config.eyeMeshNodes.RIGHT);
5144
5647
  if (node) {
5145
- resolved.EYE_R = snapshot(node);
5648
+ resolved.EYE_R = snapshotPreservingBasePose(node);
5146
5649
  }
5147
5650
  }
5148
5651
  return resolved;