@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as THREE from 'three';
2
- import { Vector3, Clock, Box3, Quaternion, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopPingPong, LoopOnce, LoopRepeat, QuaternionKeyframeTrack, NumberKeyframeTrack, AnimationClip, AnimationMixer, Mesh } from 'three';
2
+ import { Vector3, Clock, Box3, Quaternion, AdditiveAnimationBlendMode, NormalAnimationBlendMode, LoopPingPong, LoopOnce, LoopRepeat, QuaternionKeyframeTrack, NumberKeyframeTrack, AnimationClip, AnimationMixer, Mesh, PropertyBinding } 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;
@@ -58,6 +58,119 @@ function resolveCurveBalance(curveId, globalBalance, balanceMap) {
58
58
  }
59
59
  return clampBalance(globalBalance);
60
60
  }
61
+ var RUNTIME_CLIP_PREFIX = "__loom3_baked_partition__/";
62
+ var FACE_SAFE_TARGET_RE = /(head|neck|jaw|eye|brow|lid|mouth|lip|face|cheek|nose|tongue|teeth)/i;
63
+ 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;
64
+ var SCENE_LIKE_TARGET_RE = /(camera|cam|scene|world|global|origin|pivot|cube)/i;
65
+ var CHANNEL_ORDER = ["face", "body", "scene"];
66
+ function getRuntimeClipName(sourceClipName, channel) {
67
+ return `${RUNTIME_CLIP_PREFIX}${sourceClipName}/${channel}`;
68
+ }
69
+ function parseTrackTarget(trackName, model) {
70
+ let parsed;
71
+ try {
72
+ parsed = PropertyBinding.parseTrackName(trackName);
73
+ } catch {
74
+ return null;
75
+ }
76
+ const targetKey = parsed.objectName === "bones" && parsed.objectIndex ? String(parsed.objectIndex) : parsed.nodeName;
77
+ const target = targetKey ? model.getObjectByProperty("uuid", targetKey) ?? PropertyBinding.findNode(model, targetKey) : null;
78
+ return {
79
+ propertyName: parsed.propertyName,
80
+ target,
81
+ targetName: target?.name ?? parsed.nodeName ?? ""
82
+ };
83
+ }
84
+ function isSceneTrackTarget(target, targetName) {
85
+ if (!target) return true;
86
+ if (target.isCamera) return true;
87
+ return SCENE_LIKE_TARGET_RE.test(targetName);
88
+ }
89
+ function isFaceSafeTransformTarget(target, targetName, safeTransformTargets) {
90
+ if (target && safeTransformTargets.has(target)) {
91
+ return true;
92
+ }
93
+ if (!targetName) {
94
+ return false;
95
+ }
96
+ if (BODY_LIKE_TARGET_RE.test(targetName) || SCENE_LIKE_TARGET_RE.test(targetName)) {
97
+ return false;
98
+ }
99
+ return FACE_SAFE_TARGET_RE.test(targetName);
100
+ }
101
+ function classifyBakedTrack(track, model, bones) {
102
+ const parsed = parseTrackTarget(track.name, model);
103
+ if (!parsed) {
104
+ return "scene";
105
+ }
106
+ if (parsed.propertyName === "morphTargetInfluences" || parsed.propertyName === "weights") {
107
+ return "face";
108
+ }
109
+ if (isSceneTrackTarget(parsed.target, parsed.targetName)) {
110
+ return "scene";
111
+ }
112
+ if (parsed.propertyName === "quaternion") {
113
+ const safeTransformTargets = new Set(
114
+ Object.values(bones).map((entry) => entry?.obj).filter((entry) => !!entry)
115
+ );
116
+ if (isFaceSafeTransformTarget(parsed.target, parsed.targetName, safeTransformTargets)) {
117
+ return "face";
118
+ }
119
+ }
120
+ return "body";
121
+ }
122
+ function resolveBakedChannelBlendMode(channel, requestedBlendMode) {
123
+ if (channel === "face") {
124
+ return requestedBlendMode === "additive" ? "additive" : "replace";
125
+ }
126
+ if (channel === "body") {
127
+ return "replace";
128
+ }
129
+ return void 0;
130
+ }
131
+ function resolveBakedAggregateBlendMode(channels, requestedBlendMode) {
132
+ if (requestedBlendMode !== "additive") {
133
+ return "replace";
134
+ }
135
+ return channels.some((channel) => channel.channel === "face" && channel.playable && channel.trackCount > 0) ? "additive" : "replace";
136
+ }
137
+ function partitionBakedClip(clip, model, bones) {
138
+ const tracksByChannel = new Map(
139
+ CHANNEL_ORDER.map((channel) => [channel, []])
140
+ );
141
+ for (const track of clip.tracks) {
142
+ const channel = classifyBakedTrack(track, model, bones);
143
+ tracksByChannel.get(channel)?.push(track.clone());
144
+ }
145
+ const runtimeClips = [];
146
+ const channels = [];
147
+ for (const channel of CHANNEL_ORDER) {
148
+ const tracks = tracksByChannel.get(channel) ?? [];
149
+ if (tracks.length === 0) {
150
+ continue;
151
+ }
152
+ const playable = channel !== "scene";
153
+ const blendMode = resolveBakedChannelBlendMode(channel, "additive");
154
+ channels.push({
155
+ channel,
156
+ trackCount: tracks.length,
157
+ playable,
158
+ blendMode
159
+ });
160
+ if (!playable) {
161
+ continue;
162
+ }
163
+ runtimeClips.push({
164
+ channel,
165
+ clip: new AnimationClip(getRuntimeClipName(clip.name, channel), clip.duration, tracks)
166
+ });
167
+ }
168
+ return {
169
+ sourceClip: clip,
170
+ channels,
171
+ runtimeClips
172
+ };
173
+ }
61
174
 
62
175
  // src/engines/three/AnimationThree.ts
63
176
  var easeInOutQuad = (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
@@ -163,6 +276,10 @@ var BakedAnimationController = class {
163
276
  __publicField(this, "animationMixer", null);
164
277
  __publicField(this, "mixerFinishedListenerAttached", false);
165
278
  __publicField(this, "animationClips", []);
279
+ __publicField(this, "bakedSourceClips", /* @__PURE__ */ new Map());
280
+ __publicField(this, "bakedRuntimeActions", /* @__PURE__ */ new Map());
281
+ __publicField(this, "bakedActionGroups", /* @__PURE__ */ new Map());
282
+ __publicField(this, "bakedRuntimeClipToSource", /* @__PURE__ */ new Map());
166
283
  __publicField(this, "animationActions", /* @__PURE__ */ new Map());
167
284
  __publicField(this, "animationFinishedCallbacks", /* @__PURE__ */ new Map());
168
285
  __publicField(this, "clipActions", /* @__PURE__ */ new Map());
@@ -191,6 +308,7 @@ var BakedAnimationController = class {
191
308
  const rawWeight = options?.weight ?? options?.intensity ?? clipOptions?.mixerWeight ?? 1;
192
309
  const weight = Number.isFinite(rawWeight) ? Math.max(0, rawWeight) : 1;
193
310
  const loopMode = options?.loopMode ?? (typeof options?.loop === "boolean" ? options.loop ? "repeat" : "once" : defaults.loop ? "repeat" : "once");
311
+ const requestedBlendMode = options?.blendMode ?? (clipOptions?.mixerAdditive ? "additive" : "replace");
194
312
  return {
195
313
  source: options?.source ?? defaults.source,
196
314
  loop: loopMode !== "once",
@@ -200,7 +318,8 @@ var BakedAnimationController = class {
200
318
  playbackRate,
201
319
  weight,
202
320
  balance: Number.isFinite(options?.balance) ? options?.balance ?? 0 : 0,
203
- blendMode: options?.blendMode ?? (clipOptions?.mixerAdditive ? "additive" : "replace"),
321
+ requestedBlendMode,
322
+ blendMode: requestedBlendMode,
204
323
  easing: options?.easing ?? "linear"
205
324
  };
206
325
  }
@@ -260,15 +379,49 @@ var BakedAnimationController = class {
260
379
  next.balance = Math.max(-1, Math.min(1, options.balance));
261
380
  }
262
381
  if (options.blendMode) {
263
- next.blendMode = options.blendMode;
382
+ next.requestedBlendMode = options.blendMode;
264
383
  } else if (typeof clipOptions?.mixerAdditive === "boolean") {
265
- next.blendMode = clipOptions.mixerAdditive ? "additive" : "replace";
384
+ next.requestedBlendMode = clipOptions.mixerAdditive ? "additive" : "replace";
266
385
  }
386
+ next.blendMode = next.requestedBlendMode;
267
387
  if (options.easing) {
268
388
  next.easing = options.easing;
269
389
  }
270
390
  return next;
271
391
  }
392
+ isBakedSourceClip(clipName) {
393
+ return this.bakedSourceClips.has(clipName);
394
+ }
395
+ getBakedSourceClip(clipName) {
396
+ return this.bakedSourceClips.get(clipName);
397
+ }
398
+ getBakedChannelInfo(clipName, playbackState) {
399
+ const bakedClip = this.getBakedSourceClip(clipName);
400
+ if (!bakedClip) {
401
+ return void 0;
402
+ }
403
+ const requestedBlendMode = playbackState?.requestedBlendMode ?? "replace";
404
+ return bakedClip.channels.map((channel) => ({
405
+ ...channel,
406
+ blendMode: resolveBakedChannelBlendMode(channel.channel, requestedBlendMode)
407
+ }));
408
+ }
409
+ getBakedAggregateBlendMode(clipName, playbackState) {
410
+ const channels = this.getBakedChannelInfo(clipName, playbackState);
411
+ if (!channels) {
412
+ return playbackState?.requestedBlendMode ?? playbackState?.blendMode ?? "replace";
413
+ }
414
+ return resolveBakedAggregateBlendMode(
415
+ channels,
416
+ playbackState?.requestedBlendMode ?? "replace"
417
+ );
418
+ }
419
+ applyPlaybackStateToBakedAction(action, state, channel) {
420
+ this.applyPlaybackState(action, {
421
+ ...state,
422
+ blendMode: resolveBakedChannelBlendMode(channel, state.requestedBlendMode) ?? "replace"
423
+ });
424
+ }
272
425
  resolveStartTime(duration, state, explicitStartTime) {
273
426
  if (typeof explicitStartTime === "number" && Number.isFinite(explicitStartTime)) {
274
427
  return Math.max(0, Math.min(duration, explicitStartTime));
@@ -278,8 +431,13 @@ var BakedAnimationController = class {
278
431
  }
279
432
  return 0;
280
433
  }
281
- getOrCreateBakedAction(clipName) {
282
- const existing = this.animationActions.get(clipName);
434
+ getOrCreateBakedRuntimeAction(sourceClipName, channel) {
435
+ const bakedClip = this.getBakedSourceClip(sourceClipName);
436
+ const runtimeClip = bakedClip?.runtimeClips.find((entry) => entry.channel === channel)?.clip;
437
+ if (!runtimeClip) {
438
+ return null;
439
+ }
440
+ const existing = this.bakedRuntimeActions.get(runtimeClip.name);
283
441
  if (existing) {
284
442
  return existing;
285
443
  }
@@ -287,13 +445,44 @@ var BakedAnimationController = class {
287
445
  if (!this.animationMixer) {
288
446
  return null;
289
447
  }
290
- const clip = this.animationClips.find((entry) => entry.name === clipName);
291
- if (!clip || (this.clipSources.get(clipName) ?? "baked") !== "baked") {
448
+ const action = this.animationMixer.clipAction(runtimeClip);
449
+ this.bakedRuntimeActions.set(runtimeClip.name, action);
450
+ return action;
451
+ }
452
+ getRepresentativeBakedAction(clipName) {
453
+ const group = this.bakedActionGroups.get(clipName);
454
+ if (!group) {
292
455
  return null;
293
456
  }
294
- const action = this.animationMixer.clipAction(clip);
295
- this.animationActions.set(clipName, action);
296
- return action;
457
+ return group.channelActions.values().next().value ?? null;
458
+ }
459
+ createBakedActionGroup(clipName, playbackState) {
460
+ const bakedClip = this.getBakedSourceClip(clipName);
461
+ if (!bakedClip) {
462
+ return null;
463
+ }
464
+ const channelActions = /* @__PURE__ */ new Map();
465
+ for (const runtimeClip of bakedClip.runtimeClips) {
466
+ const action = this.getOrCreateBakedRuntimeAction(clipName, runtimeClip.channel);
467
+ if (action) {
468
+ channelActions.set(runtimeClip.channel, action);
469
+ }
470
+ }
471
+ if (channelActions.size === 0) {
472
+ return null;
473
+ }
474
+ let resolveFinished = () => {
475
+ };
476
+ const finishedPromise = new Promise((resolve) => {
477
+ resolveFinished = resolve;
478
+ });
479
+ return {
480
+ actionId: makeActionId(),
481
+ channelActions,
482
+ pendingFinishedChannels: playbackState.loopMode === "once" ? new Set(channelActions.keys()) : /* @__PURE__ */ new Set(),
483
+ finishedPromise,
484
+ resolveFinished
485
+ };
297
486
  }
298
487
  getMeshNamesForAU(auId, config, explicitMeshNames) {
299
488
  if (explicitMeshNames && explicitMeshNames.length > 0) {
@@ -333,6 +522,10 @@ var BakedAnimationController = class {
333
522
  this.animationMixer = null;
334
523
  }
335
524
  this.animationClips = [];
525
+ this.bakedSourceClips.clear();
526
+ this.bakedRuntimeActions.clear();
527
+ this.bakedActionGroups.clear();
528
+ this.bakedRuntimeClipToSource.clear();
336
529
  this.animationActions.clear();
337
530
  this.animationFinishedCallbacks.clear();
338
531
  this.clipActions.clear();
@@ -341,17 +534,47 @@ var BakedAnimationController = class {
341
534
  this.playbackState.clear();
342
535
  }
343
536
  loadAnimationClips(clips) {
344
- if (!this.host.getModel()) {
537
+ const model = this.host.getModel();
538
+ if (!model) {
345
539
  console.warn("Loom3: Cannot load animation clips before calling onReady()");
346
540
  return;
347
541
  }
542
+ for (const clipName of this.bakedSourceClips.keys()) {
543
+ this.stopAnimation(clipName);
544
+ }
545
+ if (this.animationMixer) {
546
+ for (const bakedClip of this.bakedSourceClips.values()) {
547
+ for (const runtimeClip of bakedClip.runtimeClips) {
548
+ try {
549
+ this.animationMixer.uncacheAction(runtimeClip.clip);
550
+ } catch {
551
+ }
552
+ try {
553
+ this.animationMixer.uncacheClip(runtimeClip.clip);
554
+ } catch {
555
+ }
556
+ }
557
+ }
558
+ }
559
+ for (const clipName of this.bakedSourceClips.keys()) {
560
+ this.playbackState.delete(clipName);
561
+ this.clipSources.delete(clipName);
562
+ }
563
+ this.bakedSourceClips.clear();
564
+ this.bakedRuntimeActions.clear();
565
+ this.bakedActionGroups.clear();
566
+ this.bakedRuntimeClipToSource.clear();
348
567
  this.ensureMixer();
349
- this.animationClips = clips;
350
- for (const clip of this.animationClips) {
351
- this.clipSources.set(clip.name, "baked");
352
- if (!this.animationActions.has(clip.name) && this.animationMixer) {
353
- const action = this.animationMixer.clipAction(clip);
354
- this.animationActions.set(clip.name, action);
568
+ const partitionedClips = clips.map((clip) => partitionBakedClip(clip, model, this.host.getBones()));
569
+ this.animationClips = partitionedClips.map((clip) => clip.sourceClip);
570
+ for (const bakedClip of partitionedClips) {
571
+ this.bakedSourceClips.set(bakedClip.sourceClip.name, bakedClip);
572
+ this.clipSources.set(bakedClip.sourceClip.name, "baked");
573
+ for (const runtimeClip of bakedClip.runtimeClips) {
574
+ this.bakedRuntimeClipToSource.set(runtimeClip.clip.name, {
575
+ sourceClipName: bakedClip.sourceClip.name,
576
+ channel: runtimeClip.channel
577
+ });
355
578
  }
356
579
  }
357
580
  }
@@ -360,84 +583,91 @@ var BakedAnimationController = class {
360
583
  name: clip.name,
361
584
  duration: clip.duration,
362
585
  trackCount: clip.tracks.length,
363
- source: this.clipSources.get(clip.name) ?? "baked"
586
+ source: this.clipSources.get(clip.name) ?? "baked",
587
+ channels: this.getBakedSourceClip(clip.name)?.channels
364
588
  }));
365
589
  }
366
590
  removeAnimationClip(clipName) {
367
- const clip = this.animationClips.find((entry) => entry.name === clipName);
368
- if (!clip || (this.clipSources.get(clipName) ?? "baked") !== "baked") {
591
+ const bakedClip = this.getBakedSourceClip(clipName);
592
+ if (!bakedClip) {
369
593
  return false;
370
594
  }
371
- const relatedActions = /* @__PURE__ */ new Set();
372
- const bakedAction = this.animationActions.get(clipName);
373
- const clipAction = this.clipActions.get(clipName);
374
- if (bakedAction) relatedActions.add(bakedAction);
375
- if (clipAction) relatedActions.add(clipAction);
376
595
  this.stopAnimation(clipName);
377
596
  if (this.animationMixer) {
378
- for (const action of relatedActions) {
597
+ for (const runtimeClip of bakedClip.runtimeClips) {
598
+ const action = this.bakedRuntimeActions.get(runtimeClip.clip.name);
379
599
  try {
380
- this.animationMixer.uncacheAction(clip);
600
+ this.animationMixer.uncacheAction(runtimeClip.clip);
381
601
  } catch {
382
602
  }
383
603
  try {
384
- this.animationMixer.uncacheClip(clip);
604
+ this.animationMixer.uncacheClip(runtimeClip.clip);
385
605
  } catch {
386
606
  }
607
+ this.bakedRuntimeActions.delete(runtimeClip.clip.name);
608
+ this.bakedRuntimeClipToSource.delete(runtimeClip.clip.name);
387
609
  const actionId = this.getActionId(action);
388
- if (actionId) {
610
+ if (actionId && action) {
389
611
  this.actionIdToClip.delete(actionId);
612
+ this.actionIds.delete(action);
390
613
  }
391
- this.actionIds.delete(action);
392
614
  }
393
615
  }
394
616
  this.animationClips = this.animationClips.filter((entry) => entry.name !== clipName);
395
- this.animationActions.delete(clipName);
396
- this.clipActions.delete(clipName);
397
- this.clipHandles.delete(clipName);
398
- this.animationFinishedCallbacks.delete(clipName);
617
+ this.bakedSourceClips.delete(clipName);
618
+ this.bakedActionGroups.delete(clipName);
399
619
  this.playbackState.delete(clipName);
400
620
  this.clipSources.delete(clipName);
401
621
  return true;
402
622
  }
403
623
  playAnimation(clipName, options = {}) {
404
- const action = this.getOrCreateBakedAction(clipName);
405
- if (!action) {
624
+ const bakedClip = this.getBakedSourceClip(clipName);
625
+ if (!bakedClip) {
406
626
  console.warn(`Loom3: Animation clip "${clipName}" not found`);
407
627
  return null;
408
628
  }
409
- if (!this.getActionId(action)) {
410
- this.setActionId(action, clipName);
411
- }
412
629
  const playbackState = this.mergePlaybackOptions(
413
630
  this.getPlaybackStateSnapshot(clipName, { loop: true, source: "baked" }),
414
631
  options
415
632
  );
633
+ playbackState.blendMode = this.getBakedAggregateBlendMode(clipName, playbackState);
634
+ const actionGroup = this.createBakedActionGroup(clipName, playbackState);
635
+ if (!actionGroup) {
636
+ console.warn(`Loom3: Animation clip "${clipName}" has no character-runtime channels to play`);
637
+ return null;
638
+ }
416
639
  const crossfadeDuration = options.crossfadeDuration ?? 0;
417
640
  const clampWhenFinished = options.clampWhenFinished ?? playbackState.loopMode === "once";
418
- const startTime = this.resolveStartTime(action.getClip().duration, playbackState, options.startTime);
419
- this.applyPlaybackState(action, playbackState);
420
- action.clampWhenFinished = clampWhenFinished;
421
- if (crossfadeDuration > 0) {
422
- action.reset();
423
- action.fadeIn(crossfadeDuration);
424
- } else {
425
- action.reset();
641
+ const startTime = this.resolveStartTime(bakedClip.sourceClip.duration, playbackState, options.startTime);
642
+ for (const [channel, action] of actionGroup.channelActions) {
643
+ this.applyPlaybackStateToBakedAction(action, playbackState, channel);
644
+ action.clampWhenFinished = clampWhenFinished;
645
+ if (crossfadeDuration > 0) {
646
+ action.reset();
647
+ action.fadeIn(crossfadeDuration);
648
+ } else {
649
+ action.reset();
650
+ }
651
+ action.time = startTime;
652
+ action.play();
426
653
  }
427
- action.time = startTime;
428
- action.play();
429
- this.animationActions.set(clipName, action);
654
+ this.bakedActionGroups.set(clipName, actionGroup);
430
655
  this.setPlaybackState(clipName, playbackState);
431
- let resolveFinished;
432
- const finishedPromise = new Promise((resolve) => {
433
- resolveFinished = resolve;
434
- });
435
- if (playbackState.loopMode === "once") {
436
- this.animationFinishedCallbacks.set(clipName, () => resolveFinished());
437
- }
438
- return this.createAnimationHandle(clipName, action, finishedPromise);
656
+ return this.createBakedAnimationHandle(clipName, actionGroup);
439
657
  }
440
658
  stopAnimation(clipName) {
659
+ const bakedGroup = this.bakedActionGroups.get(clipName);
660
+ if (bakedGroup) {
661
+ for (const action2 of bakedGroup.channelActions.values()) {
662
+ action2.stop();
663
+ try {
664
+ action2.paused = false;
665
+ } catch {
666
+ }
667
+ }
668
+ this.bakedActionGroups.delete(clipName);
669
+ return;
670
+ }
441
671
  const action = this.animationActions.get(clipName);
442
672
  if (action) {
443
673
  const isBaked = (this.clipSources.get(clipName) ?? "baked") === "baked";
@@ -485,6 +715,7 @@ var BakedAnimationController = class {
485
715
  }
486
716
  stopAllAnimations() {
487
717
  for (const clipName of /* @__PURE__ */ new Set([
718
+ ...this.bakedActionGroups.keys(),
488
719
  ...this.animationActions.keys(),
489
720
  ...this.clipActions.keys()
490
721
  ])) {
@@ -492,18 +723,39 @@ var BakedAnimationController = class {
492
723
  }
493
724
  }
494
725
  pauseAnimation(clipName) {
726
+ const bakedGroup = this.bakedActionGroups.get(clipName);
727
+ if (bakedGroup) {
728
+ for (const action2 of bakedGroup.channelActions.values()) {
729
+ action2.paused = true;
730
+ }
731
+ return;
732
+ }
495
733
  const action = this.animationActions.get(clipName);
496
734
  if (action) {
497
735
  action.paused = true;
498
736
  }
499
737
  }
500
738
  resumeAnimation(clipName) {
739
+ const bakedGroup = this.bakedActionGroups.get(clipName);
740
+ if (bakedGroup) {
741
+ for (const action2 of bakedGroup.channelActions.values()) {
742
+ action2.paused = false;
743
+ }
744
+ return;
745
+ }
501
746
  const action = this.animationActions.get(clipName);
502
747
  if (action) {
503
748
  action.paused = false;
504
749
  }
505
750
  }
506
751
  pauseAllAnimations() {
752
+ for (const group of this.bakedActionGroups.values()) {
753
+ for (const action of group.channelActions.values()) {
754
+ if (action.isRunning()) {
755
+ action.paused = true;
756
+ }
757
+ }
758
+ }
507
759
  for (const action of this.animationActions.values()) {
508
760
  if (action.isRunning()) {
509
761
  action.paused = true;
@@ -511,6 +763,13 @@ var BakedAnimationController = class {
511
763
  }
512
764
  }
513
765
  resumeAllAnimations() {
766
+ for (const group of this.bakedActionGroups.values()) {
767
+ for (const action of group.channelActions.values()) {
768
+ if (action.paused) {
769
+ action.paused = false;
770
+ }
771
+ }
772
+ }
514
773
  for (const action of this.animationActions.values()) {
515
774
  if (action.paused) {
516
775
  action.paused = false;
@@ -518,76 +777,161 @@ var BakedAnimationController = class {
518
777
  }
519
778
  }
520
779
  setAnimationSpeed(clipName, speed) {
521
- const action = this.getOrCreateBakedAction(clipName);
522
- if (action) {
780
+ if (this.isBakedSourceClip(clipName)) {
523
781
  const next = this.getPlaybackStateSnapshot(clipName, {
524
782
  loop: true,
525
783
  source: this.clipSources.get(clipName) ?? "baked"
526
784
  });
527
785
  next.playbackRate = Number.isFinite(speed) ? Math.max(0, Math.abs(speed)) : 1;
786
+ const bakedGroup = this.bakedActionGroups.get(clipName);
787
+ if (bakedGroup) {
788
+ for (const [channel, action2] of bakedGroup.channelActions) {
789
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
790
+ }
791
+ }
792
+ this.setPlaybackState(clipName, next);
793
+ return;
794
+ }
795
+ const action = this.animationActions.get(clipName);
796
+ if (action) {
797
+ const next = this.getPlaybackStateSnapshot(clipName, {
798
+ loop: true,
799
+ source: this.clipSources.get(clipName) ?? "clip"
800
+ });
801
+ next.playbackRate = Number.isFinite(speed) ? Math.max(0, Math.abs(speed)) : 1;
528
802
  this.applyPlaybackState(action, next);
529
803
  this.setPlaybackState(clipName, next);
530
804
  }
531
805
  }
532
806
  setAnimationIntensity(clipName, intensity) {
533
- const action = this.getOrCreateBakedAction(clipName);
534
- if (action) {
807
+ if (this.isBakedSourceClip(clipName)) {
535
808
  const next = this.getPlaybackStateSnapshot(clipName, {
536
809
  loop: true,
537
810
  source: this.clipSources.get(clipName) ?? "baked"
538
811
  });
539
812
  next.weight = Number.isFinite(intensity) ? Math.max(0, intensity) : 1;
813
+ const bakedGroup = this.bakedActionGroups.get(clipName);
814
+ if (bakedGroup) {
815
+ for (const [channel, action2] of bakedGroup.channelActions) {
816
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
817
+ }
818
+ }
819
+ this.setPlaybackState(clipName, next);
820
+ return;
821
+ }
822
+ const action = this.animationActions.get(clipName);
823
+ if (action) {
824
+ const next = this.getPlaybackStateSnapshot(clipName, {
825
+ loop: true,
826
+ source: this.clipSources.get(clipName) ?? "clip"
827
+ });
828
+ next.weight = Number.isFinite(intensity) ? Math.max(0, intensity) : 1;
540
829
  action.setEffectiveWeight(next.weight);
541
830
  this.setPlaybackState(clipName, next);
542
831
  }
543
832
  }
544
833
  setAnimationLoopMode(clipName, loopMode) {
545
- const action = this.getOrCreateBakedAction(clipName);
546
- if (!action) return;
547
834
  const next = this.getPlaybackStateSnapshot(clipName, {
548
835
  loop: true,
549
- source: this.clipSources.get(clipName) ?? "baked"
836
+ source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
550
837
  });
551
838
  next.loopMode = loopMode;
552
839
  next.loop = loopMode !== "once";
840
+ if (this.isBakedSourceClip(clipName)) {
841
+ const bakedGroup = this.bakedActionGroups.get(clipName);
842
+ if (bakedGroup) {
843
+ for (const [channel, action2] of bakedGroup.channelActions) {
844
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
845
+ }
846
+ }
847
+ this.setPlaybackState(clipName, next);
848
+ return;
849
+ }
850
+ const action = this.animationActions.get(clipName);
851
+ if (!action) return;
553
852
  this.applyPlaybackState(action, next);
554
853
  this.setPlaybackState(clipName, next);
555
854
  }
556
855
  setAnimationRepeatCount(clipName, repeatCount) {
557
- const action = this.getOrCreateBakedAction(clipName);
558
- if (!action) return;
559
856
  const next = this.getPlaybackStateSnapshot(clipName, {
560
857
  loop: true,
561
- source: this.clipSources.get(clipName) ?? "baked"
858
+ source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
562
859
  });
563
860
  next.repeatCount = typeof repeatCount === "number" && Number.isFinite(repeatCount) ? Math.max(0, repeatCount) : void 0;
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;
564
873
  this.applyPlaybackState(action, next);
565
874
  this.setPlaybackState(clipName, next);
566
875
  }
567
876
  setAnimationReverse(clipName, reverse) {
568
- const action = this.getOrCreateBakedAction(clipName);
569
- if (!action) return;
570
877
  const next = this.getPlaybackStateSnapshot(clipName, {
571
878
  loop: true,
572
- source: this.clipSources.get(clipName) ?? "baked"
879
+ source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
573
880
  });
574
881
  next.reverse = !!reverse;
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;
575
894
  this.applyPlaybackState(action, next);
576
895
  this.setPlaybackState(clipName, next);
577
896
  }
578
897
  setAnimationBlendMode(clipName, blendMode) {
579
- const action = this.getOrCreateBakedAction(clipName);
580
- if (!action) return;
581
898
  const next = this.getPlaybackStateSnapshot(clipName, {
582
899
  loop: true,
583
- source: this.clipSources.get(clipName) ?? "baked"
900
+ source: this.clipSources.get(clipName) ?? (this.isBakedSourceClip(clipName) ? "baked" : "clip")
584
901
  });
902
+ next.requestedBlendMode = blendMode;
903
+ if (this.isBakedSourceClip(clipName)) {
904
+ next.blendMode = this.getBakedAggregateBlendMode(clipName, next);
905
+ const bakedGroup = this.bakedActionGroups.get(clipName);
906
+ if (bakedGroup) {
907
+ for (const [channel, action2] of bakedGroup.channelActions) {
908
+ this.applyPlaybackStateToBakedAction(action2, next, channel);
909
+ }
910
+ }
911
+ this.setPlaybackState(clipName, next);
912
+ return;
913
+ }
585
914
  next.blendMode = blendMode;
915
+ const action = this.animationActions.get(clipName);
916
+ if (!action) return;
586
917
  this.applyPlaybackState(action, next);
587
918
  this.setPlaybackState(clipName, next);
588
919
  }
589
920
  seekAnimation(clipName, time) {
590
- const action = this.getOrCreateBakedAction(clipName) ?? this.animationActions.get(clipName);
921
+ const bakedGroup = this.bakedActionGroups.get(clipName);
922
+ if (bakedGroup) {
923
+ const duration2 = this.getBakedSourceClip(clipName)?.sourceClip.duration ?? 0;
924
+ const clamped = Math.max(0, Math.min(duration2, Number.isFinite(time) ? time : 0));
925
+ for (const action2 of bakedGroup.channelActions.values()) {
926
+ action2.time = clamped;
927
+ }
928
+ try {
929
+ this.animationMixer?.update(0);
930
+ } catch {
931
+ }
932
+ return;
933
+ }
934
+ const action = this.animationActions.get(clipName);
591
935
  if (!action) return;
592
936
  const duration = action.getClip().duration;
593
937
  action.time = Math.max(0, Math.min(duration, Number.isFinite(time) ? time : 0));
@@ -602,6 +946,40 @@ var BakedAnimationController = class {
602
946
  }
603
947
  }
604
948
  getAnimationState(clipName) {
949
+ const bakedClip = this.getBakedSourceClip(clipName);
950
+ if (bakedClip) {
951
+ const state2 = this.playbackState.get(clipName);
952
+ const action2 = this.getRepresentativeBakedAction(clipName);
953
+ if (!state2 && !action2) {
954
+ return null;
955
+ }
956
+ const loopMode2 = state2?.loopMode ?? (action2?.loop === LoopPingPong ? "pingpong" : action2?.loop === LoopOnce ? "once" : "repeat");
957
+ const playbackRate2 = state2?.playbackRate ?? Math.abs(action2?.getEffectiveTimeScale?.() ?? 1);
958
+ const reverse2 = state2?.reverse ?? (action2?.getEffectiveTimeScale?.() ?? 1) < 0;
959
+ const pausedValues = this.bakedActionGroups.get(clipName) ? Array.from(this.bakedActionGroups.get(clipName).channelActions.values()).map((entry) => entry.paused) : [];
960
+ return {
961
+ name: bakedClip.sourceClip.name,
962
+ actionId: this.bakedActionGroups.get(clipName)?.actionId,
963
+ source: state2?.source ?? this.clipSources.get(clipName) ?? "baked",
964
+ isPlaying: this.bakedActionGroups.get(clipName) ? Array.from(this.bakedActionGroups.get(clipName).channelActions.values()).some((entry) => entry.isRunning() && !entry.paused) : false,
965
+ isPaused: pausedValues.length > 0 ? pausedValues.every(Boolean) : false,
966
+ time: action2?.time ?? 0,
967
+ duration: bakedClip.sourceClip.duration,
968
+ speed: playbackRate2,
969
+ playbackRate: playbackRate2,
970
+ reverse: reverse2,
971
+ weight: state2?.weight ?? action2?.getEffectiveWeight?.() ?? 1,
972
+ balance: state2?.balance ?? 0,
973
+ requestedBlendMode: state2?.requestedBlendMode ?? "replace",
974
+ blendMode: this.getBakedAggregateBlendMode(clipName, state2),
975
+ channels: this.getBakedChannelInfo(clipName, state2),
976
+ easing: state2?.easing ?? "linear",
977
+ loop: loopMode2 !== "once",
978
+ loopMode: loopMode2,
979
+ repeatCount: state2?.repeatCount,
980
+ isLooping: loopMode2 !== "once"
981
+ };
982
+ }
605
983
  const action = this.animationActions.get(clipName);
606
984
  if (!action) return null;
607
985
  const clip = action.getClip();
@@ -622,7 +1000,9 @@ var BakedAnimationController = class {
622
1000
  reverse,
623
1001
  weight: state?.weight ?? action.getEffectiveWeight(),
624
1002
  balance: state?.balance ?? 0,
1003
+ requestedBlendMode: state?.requestedBlendMode ?? state?.blendMode ?? "replace",
625
1004
  blendMode: state?.blendMode ?? "replace",
1005
+ channels: state?.source === "baked" ? this.getBakedChannelInfo(clipName, state) : void 0,
626
1006
  easing: state?.easing ?? "linear",
627
1007
  loop: loopMode !== "once",
628
1008
  loopMode,
@@ -632,6 +1012,12 @@ var BakedAnimationController = class {
632
1012
  }
633
1013
  getPlayingAnimations() {
634
1014
  const playing = [];
1015
+ for (const name of this.bakedActionGroups.keys()) {
1016
+ const state = this.getAnimationState(name);
1017
+ if (state?.isPlaying) {
1018
+ playing.push(state);
1019
+ }
1020
+ }
635
1021
  for (const [name, action] of this.animationActions) {
636
1022
  if (action.isRunning()) {
637
1023
  const state = this.getAnimationState(name);
@@ -641,6 +1027,13 @@ var BakedAnimationController = class {
641
1027
  return playing;
642
1028
  }
643
1029
  crossfadeTo(clipName, duration = 0.3, options = {}) {
1030
+ for (const group of this.bakedActionGroups.values()) {
1031
+ for (const action of group.channelActions.values()) {
1032
+ if (action.isRunning()) {
1033
+ action.fadeOut(duration);
1034
+ }
1035
+ }
1036
+ }
644
1037
  for (const action of this.animationActions.values()) {
645
1038
  if (action.isRunning()) {
646
1039
  action.fadeOut(duration);
@@ -1166,6 +1559,14 @@ var BakedAnimationController = class {
1166
1559
  this.animationMixer.addEventListener("finished", (event) => {
1167
1560
  const action = event.action;
1168
1561
  const clip = action.getClip();
1562
+ const bakedRuntime = this.bakedRuntimeClipToSource.get(clip.name);
1563
+ if (bakedRuntime) {
1564
+ const group = this.bakedActionGroups.get(bakedRuntime.sourceClipName);
1565
+ if (group && group.pendingFinishedChannels.delete(bakedRuntime.channel) && group.pendingFinishedChannels.size === 0) {
1566
+ group.resolveFinished();
1567
+ }
1568
+ return;
1569
+ }
1169
1570
  const callback = this.animationFinishedCallbacks.get(clip.name);
1170
1571
  if (callback) {
1171
1572
  callback();
@@ -1190,6 +1591,20 @@ var BakedAnimationController = class {
1190
1591
  finished: finishedPromise
1191
1592
  };
1192
1593
  }
1594
+ createBakedAnimationHandle(clipName, group) {
1595
+ return {
1596
+ actionId: group.actionId,
1597
+ stop: () => this.stopAnimation(clipName),
1598
+ pause: () => this.pauseAnimation(clipName),
1599
+ resume: () => this.resumeAnimation(clipName),
1600
+ setSpeed: (speed) => this.setAnimationSpeed(clipName, speed),
1601
+ setWeight: (weight) => this.setAnimationIntensity(clipName, weight),
1602
+ seekTo: (time) => this.seekAnimation(clipName, time),
1603
+ getState: () => this.getAnimationState(clipName),
1604
+ crossfadeTo: (targetClip, dur) => this.crossfadeTo(targetClip, dur),
1605
+ finished: group.finishedPromise
1606
+ };
1607
+ }
1193
1608
  };
1194
1609
 
1195
1610
  // src/presets/cc4.ts
@@ -3916,7 +4331,8 @@ var _Loom3 = class _Loom3 {
3916
4331
  __publicField(this, "bones", {});
3917
4332
  __publicField(this, "mixWeights", {});
3918
4333
  // Viseme state
3919
- __publicField(this, "visemeValues", new Array(15).fill(0));
4334
+ __publicField(this, "visemeValues", []);
4335
+ __publicField(this, "visemeJawScales", []);
3920
4336
  __publicField(this, "bakedAnimations");
3921
4337
  __publicField(this, "hairPhysics");
3922
4338
  // Internal animation loop
@@ -3929,6 +4345,7 @@ var _Loom3 = class _Loom3 {
3929
4345
  const basePreset = config.presetType ? getPreset(config.presetType) : CC4_PRESET;
3930
4346
  this.config = extendPresetWithProfile(basePreset, config.profile);
3931
4347
  this.mixWeights = { ...this.config.auMixDefaults };
4348
+ this.syncVisemeRuntimeState();
3932
4349
  this.animation = animation || new AnimationThree();
3933
4350
  this.compositeRotations = this.config.compositeRotations || COMPOSITE_ROTATIONS;
3934
4351
  this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
@@ -4461,6 +4878,7 @@ var _Loom3 = class _Loom3 {
4461
4878
  if (visemeIndex < 0 || visemeIndex >= this.config.visemeKeys.length) return;
4462
4879
  const val = clamp012(value);
4463
4880
  this.visemeValues[visemeIndex] = val;
4881
+ this.visemeJawScales[visemeIndex] = jawScale;
4464
4882
  const targets = this.resolvedVisemeTargets[visemeIndex];
4465
4883
  if (targets && targets.length > 0) {
4466
4884
  this.applyMorphTargets(targets, val);
@@ -4473,7 +4891,7 @@ var _Loom3 = class _Loom3 {
4473
4891
  this.setMorph(morphKey, val, visemeMeshNames);
4474
4892
  }
4475
4893
  }
4476
- const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * val * jawScale;
4894
+ const jawAmount = this.getVisemeJawAmount(visemeIndex) * val * jawScale;
4477
4895
  if (Math.abs(jawScale) > 1e-6 && Math.abs(jawAmount) > 1e-6) {
4478
4896
  this.updateBoneRotation("JAW", "pitch", jawAmount);
4479
4897
  }
@@ -4488,9 +4906,10 @@ var _Loom3 = class _Loom3 {
4488
4906
  const morphKey = this.config.visemeKeys[visemeIndex];
4489
4907
  const target = clamp012(to);
4490
4908
  this.visemeValues[visemeIndex] = target;
4909
+ this.visemeJawScales[visemeIndex] = jawScale;
4491
4910
  const visemeMeshNames = this.getMeshNamesForViseme();
4492
4911
  const morphHandle = typeof morphKey === "number" ? this.transitionMorphInfluence(morphKey, target, durationMs, visemeMeshNames) : this.transitionMorph(morphKey, target, durationMs, visemeMeshNames);
4493
- const jawAmount = _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] * target * jawScale;
4912
+ const jawAmount = this.getVisemeJawAmount(visemeIndex) * target * jawScale;
4494
4913
  if (Math.abs(jawScale) <= 1e-6 || Math.abs(jawAmount) <= 1e-6) {
4495
4914
  return morphHandle;
4496
4915
  }
@@ -4544,6 +4963,9 @@ var _Loom3 = class _Loom3 {
4544
4963
  }
4545
4964
  resetToNeutral() {
4546
4965
  this.auValues = {};
4966
+ this.visemeValues = new Array(this.config.visemeKeys.length).fill(0);
4967
+ this.visemeJawScales = new Array(this.config.visemeKeys.length).fill(1);
4968
+ this.translations = {};
4547
4969
  this.initBoneRotations();
4548
4970
  this.clearTransitions();
4549
4971
  for (const m of this.meshes) {
@@ -4559,6 +4981,33 @@ var _Loom3 = class _Loom3 {
4559
4981
  entry.obj.quaternion.copy(entry.baseQuat);
4560
4982
  });
4561
4983
  }
4984
+ reinitializeRuntimeStateFromCurrentControls(staleMorphTargets = []) {
4985
+ this.clearTransitions();
4986
+ this.resetMorphTargetHandles(staleMorphTargets);
4987
+ this.translations = {};
4988
+ this.initBoneRotations();
4989
+ Object.values(this.bones).forEach((entry) => {
4990
+ if (!entry) return;
4991
+ entry.obj.position.copy(entry.basePos);
4992
+ entry.obj.quaternion.copy(entry.baseQuat);
4993
+ entry.obj.updateMatrixWorld(false);
4994
+ });
4995
+ for (const [auIdStr, value] of Object.entries(this.auValues)) {
4996
+ if (value <= 0) continue;
4997
+ const auId = Number(auIdStr);
4998
+ if (Number.isNaN(auId)) continue;
4999
+ this.setAU(auId, value, this.auBalances[auId]);
5000
+ }
5001
+ for (let visemeIndex = 0; visemeIndex < this.visemeValues.length; visemeIndex += 1) {
5002
+ const value = this.visemeValues[visemeIndex] ?? 0;
5003
+ if (value <= 0) continue;
5004
+ this.setViseme(visemeIndex, value, this.visemeJawScales[visemeIndex] ?? 1);
5005
+ }
5006
+ if (this.model) {
5007
+ this.flushPendingComposites();
5008
+ this.model.updateMatrixWorld(true);
5009
+ }
5010
+ }
4562
5011
  // ============================================================================
4563
5012
  // MESH CONTROL
4564
5013
  // ============================================================================
@@ -4789,12 +5238,20 @@ var _Loom3 = class _Loom3 {
4789
5238
  }
4790
5239
  setProfile(profile) {
4791
5240
  this.config = profile;
5241
+ this.compositeRotations = this.config.compositeRotations || COMPOSITE_ROTATIONS;
5242
+ this.auToCompositeMap = buildAUToCompositeMap(this.compositeRotations);
4792
5243
  this.mixWeights = { ...profile.auMixDefaults };
5244
+ this.syncVisemeRuntimeState();
5245
+ let staleMorphTargets = [];
4793
5246
  if (this.model) {
5247
+ staleMorphTargets = this.collectResolvedExpressionMorphTargets();
5248
+ this.bones = this.resolveBones(this.model);
5249
+ this.missingBoneWarnings.clear();
4794
5250
  this.rebuildMorphTargetsCache();
4795
5251
  }
4796
5252
  this.hairPhysics.refreshMeshSelection();
4797
5253
  this.applyHairPhysicsProfileConfig();
5254
+ this.reinitializeRuntimeStateFromCurrentControls(staleMorphTargets);
4798
5255
  }
4799
5256
  getProfile() {
4800
5257
  return this.config;
@@ -4932,6 +5389,39 @@ var _Loom3 = class _Loom3 {
4932
5389
  getMorphIndexCacheKey(index, meshNames) {
4933
5390
  return meshNames?.length ? `idx:${index}@${meshNames.join(",")}` : `idx:${index}`;
4934
5391
  }
5392
+ syncVisemeRuntimeState() {
5393
+ const visemeCount = this.config.visemeKeys.length;
5394
+ this.visemeValues = Array.from(
5395
+ { length: visemeCount },
5396
+ (_, index) => this.visemeValues[index] ?? 0
5397
+ );
5398
+ this.visemeJawScales = Array.from(
5399
+ { length: visemeCount },
5400
+ (_, index) => this.visemeJawScales[index] ?? 1
5401
+ );
5402
+ }
5403
+ getVisemeJawAmount(visemeIndex) {
5404
+ return this.config.visemeJawAmounts?.[visemeIndex] ?? _Loom3.VISEME_JAW_AMOUNTS[visemeIndex] ?? 0;
5405
+ }
5406
+ collectResolvedExpressionMorphTargets() {
5407
+ const targets = [];
5408
+ for (const resolved of this.resolvedAUMorphTargets.values()) {
5409
+ targets.push(...resolved.left, ...resolved.right, ...resolved.center);
5410
+ }
5411
+ for (const resolved of this.resolvedVisemeTargets) {
5412
+ if (resolved?.length) {
5413
+ targets.push(...resolved);
5414
+ }
5415
+ }
5416
+ return targets;
5417
+ }
5418
+ resetMorphTargetHandles(targets) {
5419
+ for (const { infl, idx } of targets) {
5420
+ if (idx < infl.length) {
5421
+ infl[idx] = 0;
5422
+ }
5423
+ }
5424
+ }
4935
5425
  isMixedAU(id) {
4936
5426
  const morphs = this.config.auToMorphs[id];
4937
5427
  const hasMorphs = !!(morphs?.left?.length || morphs?.right?.length || morphs?.center?.length);
@@ -5073,12 +5563,25 @@ var _Loom3 = class _Loom3 {
5073
5563
  }
5074
5564
  resolveBones(root) {
5075
5565
  const resolved = {};
5566
+ const previousBones = this.bones;
5076
5567
  const snapshot = (obj) => ({
5077
5568
  obj,
5078
5569
  basePos: { x: obj.position.x, y: obj.position.y, z: obj.position.z },
5079
5570
  baseQuat: obj.quaternion.clone(),
5080
5571
  baseEuler: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, order: obj.rotation.order }
5081
5572
  });
5573
+ const snapshotPreservingBasePose = (obj) => {
5574
+ const existing = Object.values(previousBones).find((entry) => entry?.obj === obj);
5575
+ if (!existing) {
5576
+ return snapshot(obj);
5577
+ }
5578
+ return {
5579
+ obj,
5580
+ basePos: { ...existing.basePos },
5581
+ baseQuat: existing.baseQuat.clone(),
5582
+ baseEuler: { ...existing.baseEuler }
5583
+ };
5584
+ };
5082
5585
  const prefix = this.config.bonePrefix || "";
5083
5586
  const suffix = this.config.boneSuffix || "";
5084
5587
  const suffixRegex = this.config.suffixPattern ? new RegExp(this.config.suffixPattern) : null;
@@ -5109,19 +5612,19 @@ var _Loom3 = class _Loom3 {
5109
5612
  for (const [key, nodeName] of Object.entries(this.config.boneNodes)) {
5110
5613
  const node = findNode(nodeName);
5111
5614
  if (node) {
5112
- resolved[key] = snapshot(node);
5615
+ resolved[key] = snapshotPreservingBasePose(node);
5113
5616
  }
5114
5617
  }
5115
5618
  if (!resolved.EYE_L && this.config.eyeMeshNodes) {
5116
5619
  const node = findNode(this.config.eyeMeshNodes.LEFT);
5117
5620
  if (node) {
5118
- resolved.EYE_L = snapshot(node);
5621
+ resolved.EYE_L = snapshotPreservingBasePose(node);
5119
5622
  }
5120
5623
  }
5121
5624
  if (!resolved.EYE_R && this.config.eyeMeshNodes) {
5122
5625
  const node = findNode(this.config.eyeMeshNodes.RIGHT);
5123
5626
  if (node) {
5124
- resolved.EYE_R = snapshot(node);
5627
+ resolved.EYE_R = snapshotPreservingBasePose(node);
5125
5628
  }
5126
5629
  }
5127
5630
  return resolved;