@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.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