@lovelace_lol/loom3 1.0.35 → 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