@zephyr3d/scene 0.9.9 → 0.9.11

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.
@@ -1,4 +1,4 @@
1
- import { Disposable, weightedAverage, Quaternion, Interpolator, Vector3 } from '@zephyr3d/base';
1
+ import { makeObservable, Disposable, weightedAverage, Quaternion, Observable, Interpolator, Vector3 } from '@zephyr3d/base';
2
2
  import { AnimationClip } from './animation.js';
3
3
  import { NodeRotationTrack } from './rotationtrack.js';
4
4
  import { NodeEulerRotationTrack } from './eulerrotationtrack.js';
@@ -7,6 +7,224 @@ import { NodeScaleTrack } from './scaletrack.js';
7
7
  import { HumanoidBodyRig } from './skeleton.js';
8
8
  import { createSkeletalMaskedAnimationClip } from './animationmask.js';
9
9
 
10
+ let nextAnimationPlaybackId = 1;
11
+ /**
12
+ * Runtime handle for one playback of an AnimationClip.
13
+ * @public
14
+ */ class AnimationPlayback extends Observable {
15
+ /** @internal */ _animationSet;
16
+ /** @internal */ _clip;
17
+ /** @internal */ _options;
18
+ /** @internal */ _id;
19
+ /** @internal */ _frameWaiters;
20
+ /** @internal */ _state;
21
+ /** @internal */ _time;
22
+ /** @internal */ _weight;
23
+ /** @internal */ _speedRatio;
24
+ /** @internal */ _layer;
25
+ /** @internal */ _priority;
26
+ /** @internal */ _interruptible;
27
+ constructor(animationSet, clip, options){
28
+ super();
29
+ this._animationSet = animationSet;
30
+ this._clip = clip;
31
+ this._options = {
32
+ ...options ?? {}
33
+ };
34
+ this._id = this._options.id ?? `animation-playback-${nextAnimationPlaybackId++}`;
35
+ this._state = 'scheduled';
36
+ this._speedRatio = this._options.speedRatio ?? 1;
37
+ this._weight = this._options.weight ?? clip.weight ?? 1;
38
+ this._time = this._speedRatio < 0 ? clip.timeDuration : 0;
39
+ this._layer = this._options.layer ?? 'default';
40
+ this._priority = this._options.priority ?? 0;
41
+ this._interruptible = this._options.interruptible ?? true;
42
+ this._frameWaiters = new Map();
43
+ }
44
+ get id() {
45
+ return this._id;
46
+ }
47
+ get animationSet() {
48
+ return this._animationSet;
49
+ }
50
+ get clip() {
51
+ return this._clip;
52
+ }
53
+ get state() {
54
+ return this._state;
55
+ }
56
+ get time() {
57
+ return this._time;
58
+ }
59
+ set time(value) {
60
+ this.seek(value);
61
+ }
62
+ get normalizedTime() {
63
+ return this._clip.timeDuration > 0 ? this._time / this._clip.timeDuration : 0;
64
+ }
65
+ set normalizedTime(value) {
66
+ this.seek(value * this._clip.timeDuration);
67
+ }
68
+ get weight() {
69
+ return this._weight;
70
+ }
71
+ set weight(value) {
72
+ this._animationSet.setPlaybackWeight(this, value);
73
+ }
74
+ get speedRatio() {
75
+ return this._speedRatio;
76
+ }
77
+ set speedRatio(value) {
78
+ this._animationSet.setPlaybackSpeedRatio(this, value);
79
+ }
80
+ get layer() {
81
+ return this._layer;
82
+ }
83
+ get priority() {
84
+ return this._priority;
85
+ }
86
+ get interruptible() {
87
+ return this._interruptible;
88
+ }
89
+ play() {
90
+ this._animationSet.startPlayback(this);
91
+ return this;
92
+ }
93
+ pause() {
94
+ this._animationSet.pausePlayback(this);
95
+ return this;
96
+ }
97
+ resume() {
98
+ this._animationSet.resumePlayback(this);
99
+ return this;
100
+ }
101
+ stop(options) {
102
+ this._animationSet.stopPlayback(this, options);
103
+ return this;
104
+ }
105
+ seek(time, options) {
106
+ this._animationSet.seekPlayback(this, time, options);
107
+ return this;
108
+ }
109
+ fadeTo(weight, duration) {
110
+ this._animationSet.fadePlaybackTo(this, weight, duration);
111
+ return this;
112
+ }
113
+ crossFadeTo(name, options) {
114
+ const duration = Math.max(options?.duration ?? 0, 0);
115
+ this.stop({
116
+ fadeOut: duration,
117
+ reason: 'interrupted'
118
+ });
119
+ return this._animationSet.play(name, {
120
+ ...options,
121
+ fadeIn: duration
122
+ });
123
+ }
124
+ waitForComplete() {
125
+ return new Promise((resolve)=>{
126
+ this.once('complete', ()=>resolve(this));
127
+ });
128
+ }
129
+ waitForMarker(idOrName) {
130
+ if (this._state === 'stopped' || this._state === 'completed') {
131
+ return Promise.resolve(undefined);
132
+ }
133
+ return new Promise((resolve)=>{
134
+ const handler = (event)=>{
135
+ if (event.marker.id === idOrName || event.marker.name === idOrName) {
136
+ this.off('marker', handler);
137
+ this.off('stop', stop);
138
+ resolve(event);
139
+ }
140
+ };
141
+ const stop = ()=>{
142
+ this.off('marker', handler);
143
+ this.off('stop', stop);
144
+ resolve(undefined);
145
+ };
146
+ this.on('marker', handler);
147
+ this.on('stop', stop);
148
+ });
149
+ }
150
+ waitForFrame(frame) {
151
+ if (this._state === 'stopped' || this._state === 'completed') {
152
+ return Promise.resolve(undefined);
153
+ }
154
+ return new Promise((resolve)=>{
155
+ const waiters = this._frameWaiters.get(frame) ?? [];
156
+ let settled = false;
157
+ let stop;
158
+ const wrappedResolve = (event)=>{
159
+ if (settled) {
160
+ return;
161
+ }
162
+ settled = true;
163
+ this.off('stop', stop);
164
+ resolve(event);
165
+ };
166
+ stop = ()=>{
167
+ const current = this._frameWaiters.get(frame);
168
+ if (current) {
169
+ this._frameWaiters.set(frame, current.filter((item)=>item !== wrappedResolve));
170
+ }
171
+ wrappedResolve(undefined);
172
+ };
173
+ waiters.push(wrappedResolve);
174
+ this._frameWaiters.set(frame, waiters);
175
+ this.on('stop', stop);
176
+ });
177
+ }
178
+ /** @internal */ _getOptions() {
179
+ return this._options;
180
+ }
181
+ /** @internal */ _setState(state) {
182
+ this._state = state;
183
+ }
184
+ /** @internal */ _setTime(time) {
185
+ this._time = time;
186
+ }
187
+ /** @internal */ _setWeight(weight) {
188
+ this._weight = weight;
189
+ }
190
+ /** @internal */ _setSpeedRatio(speedRatio) {
191
+ this._speedRatio = speedRatio;
192
+ }
193
+ /** @internal */ _getWatchedFrames() {
194
+ return [
195
+ ...this._frameWaiters.keys()
196
+ ];
197
+ }
198
+ /** @internal */ _emitStart(event) {
199
+ this.dispatchEvent('start', event);
200
+ }
201
+ /** @internal */ _emitLoop(event) {
202
+ this.dispatchEvent('loop', event);
203
+ }
204
+ /** @internal */ _emitMarker(event) {
205
+ this.dispatchEvent('marker', event);
206
+ }
207
+ /** @internal */ _emitFrame(event) {
208
+ this.dispatchEvent('frame', event);
209
+ const waiters = this._frameWaiters.get(event.frame);
210
+ if (waiters) {
211
+ this._frameWaiters.delete(event.frame);
212
+ waiters.forEach((resolve)=>resolve(event));
213
+ }
214
+ }
215
+ /** @internal */ _emitComplete(event) {
216
+ this.dispatchEvent('complete', event);
217
+ }
218
+ /** @internal */ _emitStop(event) {
219
+ this.dispatchEvent('stop', event);
220
+ }
221
+ /** @internal */ _emitPause(event) {
222
+ this.dispatchEvent('pause', event);
223
+ }
224
+ /** @internal */ _emitResume(event) {
225
+ this.dispatchEvent('resume', event);
226
+ }
227
+ }
10
228
  function cloneInterpolator(src) {
11
229
  return new Interpolator(src.mode, src.target, src.inputs instanceof Float32Array ? new Float32Array(src.inputs) : [
12
230
  ...src.inputs
@@ -407,7 +625,7 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
407
625
  * - Disposing the set releases references to the model, clips, and clears active state.
408
626
  *
409
627
  * @public
410
- */ class AnimationSet extends Disposable {
628
+ */ class AnimationSet extends makeObservable(Disposable)() {
411
629
  /** @internal */ _model;
412
630
  /** @internal */ _animations;
413
631
  /** @internal */ _skeletons;
@@ -417,6 +635,14 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
417
635
  /** @internal */ _activeRigs;
418
636
  /** @internal */ _activeAnimations;
419
637
  /**
638
+ * Callbacks driven by the same logical clock as the animations (see {@link update}).
639
+ *
640
+ * Used by higher-level constructs (e.g. timeline runners) that need to advance
641
+ * wall-clock-independent timers in sync with playback, so they pause/scale with the
642
+ * game clock instead of running on real time.
643
+ * @internal
644
+ */ _timelineTickers;
645
+ /**
420
646
  * Create an AnimationSet controlling the provided model.
421
647
  *
422
648
  * @param model - The SceneNode (model root) controlled by this animation set.
@@ -430,6 +656,19 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
430
656
  this._activeAnimations = new Map();
431
657
  this._skeletons = [];
432
658
  this._rigs = [];
659
+ this._timelineTickers = new Set();
660
+ }
661
+ /**
662
+ * Register a callback advanced by {@link update} using the same delta time as the animations.
663
+ * @internal
664
+ */ _registerTimelineTicker(ticker) {
665
+ this._timelineTickers.add(ticker);
666
+ }
667
+ /**
668
+ * Unregister a callback previously registered with {@link _registerTimelineTicker}.
669
+ * @internal
670
+ */ _unregisterTimelineTicker(ticker) {
671
+ this._timelineTickers.delete(ticker);
433
672
  }
434
673
  /**
435
674
  * The model (SceneNode) controlled by this animation set.
@@ -533,30 +772,99 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
533
772
  * @param deltaInSeconds - Time step in seconds since last update.
534
773
  */ update(deltaInSeconds) {
535
774
  this._activeAnimations.forEach((v, k)=>{
775
+ if (!v.started) {
776
+ v.started = true;
777
+ const event = this.createPlaybackEvent(v, k);
778
+ v.playback._emitStart(event);
779
+ this.dispatchEvent('playbackstart', event);
780
+ }
781
+ if (v.paused) {
782
+ return;
783
+ }
536
784
  if (v.fadeOut > 0 && v.fadeOutStart < 0) {
537
785
  v.fadeOutStart = v.animateTime;
538
786
  }
787
+ if (v.completed) {
788
+ v.animateTime += Math.abs(deltaInSeconds * v.speedRatio);
789
+ if (v.fadeOut > 0 && v.animateTime - v.fadeOutStart >= v.fadeOut) {
790
+ this.stopAnimation(k.name, {
791
+ reason: v.stopReason
792
+ });
793
+ }
794
+ return;
795
+ }
539
796
  // Update animation time
540
797
  if (v.firstFrame) {
541
798
  v.firstFrame = false;
542
799
  } else {
800
+ const previousTime = v.currentTime;
543
801
  const timeAdvance = deltaInSeconds * v.speedRatio;
544
802
  v.currentTime += timeAdvance;
545
803
  v.animateTime += timeAdvance;
804
+ const direction = timeAdvance >= 0 ? 1 : -1;
546
805
  if (k.timeDuration > 0) {
547
- if (v.currentTime > k.timeDuration) {
548
- v.repeatCounter += Math.max(1, Math.floor(v.currentTime / k.timeDuration));
806
+ if (v.rangeEnd !== null && v.speedRatio >= 0 && v.currentTime >= v.rangeEnd) {
807
+ v.currentTime = v.rangeEnd;
808
+ v.playback._setTime(v.currentTime);
809
+ this.completePlayback(k, v);
810
+ return;
811
+ } else if (v.rangeStart !== null && v.speedRatio < 0 && v.currentTime <= v.rangeStart) {
812
+ v.currentTime = v.rangeStart;
813
+ v.playback._setTime(v.currentTime);
814
+ this.completePlayback(k, v);
815
+ return;
816
+ } else if (v.currentTime > k.timeDuration) {
817
+ const loops = Math.max(1, Math.floor(v.currentTime / k.timeDuration));
818
+ if (v.repeat !== 0 && v.repeatCounter + loops >= v.repeat) {
819
+ v.repeatCounter += loops;
820
+ v.currentTime = k.timeDuration;
821
+ v.playback._setTime(v.currentTime);
822
+ this.emitCrossedMarkers(v, k, previousTime, v.currentTime, direction);
823
+ this.emitCrossedFrames(v, k, previousTime, v.currentTime, direction);
824
+ this.completePlayback(k, v);
825
+ return;
826
+ }
827
+ v.repeatCounter += loops;
549
828
  v.currentTime %= k.timeDuration;
829
+ const event = this.createPlaybackEvent(v, k);
830
+ v.playback._emitLoop(event);
831
+ this.dispatchEvent('playbackloop', event);
550
832
  } else if (v.currentTime < 0) {
551
- v.repeatCounter += Math.max(1, Math.ceil(-v.currentTime / k.timeDuration));
833
+ const loops = Math.max(1, Math.ceil(-v.currentTime / k.timeDuration));
834
+ if (v.repeat !== 0 && v.repeatCounter + loops >= v.repeat) {
835
+ v.repeatCounter += loops;
836
+ v.currentTime = 0;
837
+ v.playback._setTime(v.currentTime);
838
+ this.emitCrossedMarkers(v, k, previousTime, v.currentTime, direction);
839
+ this.emitCrossedFrames(v, k, previousTime, v.currentTime, direction);
840
+ this.completePlayback(k, v);
841
+ return;
842
+ }
843
+ v.repeatCounter += loops;
552
844
  v.currentTime = (v.currentTime % k.timeDuration + k.timeDuration) % k.timeDuration;
845
+ const event = this.createPlaybackEvent(v, k);
846
+ v.playback._emitLoop(event);
847
+ this.dispatchEvent('playbackloop', event);
553
848
  }
554
849
  }
850
+ v.playback._setTime(v.currentTime);
851
+ this.emitCrossedMarkers(v, k, previousTime, v.currentTime, direction);
852
+ this.emitCrossedFrames(v, k, previousTime, v.currentTime, direction);
555
853
  if (v.repeat !== 0 && v.repeatCounter >= v.repeat) {
556
- this.stopAnimation(k.name);
854
+ this.completePlayback(k, v);
557
855
  } else if (v.fadeOut > 0) {
558
856
  if (v.animateTime - v.fadeOutStart >= v.fadeOut) {
559
- this.stopAnimation(k.name);
857
+ this.stopAnimation(k.name, {
858
+ reason: v.stopReason
859
+ });
860
+ }
861
+ }
862
+ if (v.fadeToDuration > 0) {
863
+ const t = Math.min(1, (v.animateTime - v.fadeToStart) / v.fadeToDuration);
864
+ v.weight = v.fadeFromWeight + (v.fadeTargetWeight - v.fadeFromWeight) * t;
865
+ v.playback._setWeight(v.weight);
866
+ if (t >= 1) {
867
+ v.fadeToDuration = 0;
560
868
  }
561
869
  }
562
870
  }
@@ -596,6 +904,11 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
596
904
  this._skeletons.forEach((v)=>{
597
905
  v.get()?.apply();
598
906
  });
907
+ // Advance logical-clock timers (e.g. timeline `wait` steps) with the same delta time,
908
+ // so they stay in sync with playback and pause/scale with the game clock.
909
+ if (this._timelineTickers.size > 0) {
910
+ this._timelineTickers.forEach((ticker)=>ticker(deltaInSeconds));
911
+ }
599
912
  }
600
913
  /**
601
914
  * Check whether an animation is currently playing.
@@ -635,7 +948,43 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
635
948
  const info = this._activeAnimations.get(ani);
636
949
  if (info) {
637
950
  info.weight = weight;
951
+ info.targetWeight = weight;
952
+ info.playback._setWeight(weight);
953
+ }
954
+ }
955
+ /**
956
+ * Create a playback handle without starting it.
957
+ */ createPlayback(name, options) {
958
+ const ani = this._animations[name];
959
+ if (!ani) {
960
+ console.error(`Animation ${name} not exists`);
961
+ return null;
638
962
  }
963
+ return new AnimationPlayback(this, ani, options);
964
+ }
965
+ /**
966
+ * Start an animation and return its playback handle.
967
+ */ play(name, options) {
968
+ const playback = this.createPlayback(name, options);
969
+ playback?.play();
970
+ return playback;
971
+ }
972
+ /**
973
+ * Get currently active playbacks.
974
+ */ getPlaybacks(name) {
975
+ const playbacks = [];
976
+ this._activeAnimations.forEach((info, clip)=>{
977
+ if (!name || clip.name === name) {
978
+ playbacks.push(info.playback);
979
+ }
980
+ });
981
+ return playbacks;
982
+ }
983
+ /**
984
+ * Get the currently active playback for a clip.
985
+ */ getPlayback(name) {
986
+ const clip = this._animations[name];
987
+ return clip ? this._activeAnimations.get(clip)?.playback ?? null : null;
639
988
  }
640
989
  /**
641
990
  * Start (or update) playback of an animation clip.
@@ -648,29 +997,55 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
648
997
  * @param name - Name of the animation to play.
649
998
  * @param options - Playback options (repeat, speedRatio, fadeIn).
650
999
  */ playAnimation(name, options) {
651
- const ani = this._animations[name];
1000
+ this.play(name, options);
1001
+ }
1002
+ /**
1003
+ * Start a previously created playback.
1004
+ * @internal
1005
+ */ startPlayback(playback) {
1006
+ const ani = playback.clip;
652
1007
  if (!ani) {
653
- console.error(`Animation ${name} not exists`);
654
1008
  return;
655
1009
  }
656
- if (this.isPlayingAnimation(name)) {
657
- this.stopAnimation(name);
658
- }
1010
+ const options = playback._getOptions();
659
1011
  const fadeIn = Math.max(options?.fadeIn ?? 0, 0);
660
1012
  const repeat = options?.repeat ?? 0;
661
- const speedRatio = options?.speedRatio ?? 1;
662
- const weight = options?.weight ?? ani.weight ?? 1;
1013
+ const speedRatio = playback.speedRatio;
1014
+ const weight = playback.weight;
1015
+ const rangeStart = ani.resolveTimeRef(options?.range?.start);
1016
+ const rangeEnd = ani.resolveTimeRef(options?.range?.end);
1017
+ const syncSource = this.resolvePlaybackSyncSource(options?.sync);
1018
+ const initialTime = this.computePlaybackInitialTime(ani, speedRatio, rangeStart, rangeEnd, options?.sync, syncSource);
1019
+ if (this.isPlayingAnimation(ani.name)) {
1020
+ this.stopAnimation(ani.name, {
1021
+ reason: 'replaced'
1022
+ });
1023
+ }
1024
+ playback._setState('playing');
1025
+ playback._setTime(initialTime);
663
1026
  this._activeAnimations.set(ani, {
1027
+ playback,
664
1028
  repeat,
665
1029
  weight,
1030
+ targetWeight: weight,
666
1031
  speedRatio,
667
1032
  fadeIn,
668
1033
  fadeOut: 0,
669
1034
  repeatCounter: 0,
670
- currentTime: speedRatio < 0 ? ani.timeDuration : 0,
1035
+ currentTime: initialTime,
671
1036
  animateTime: 0,
672
1037
  fadeOutStart: 0,
673
- firstFrame: true
1038
+ fadeToStart: 0,
1039
+ fadeToDuration: 0,
1040
+ fadeFromWeight: weight,
1041
+ fadeTargetWeight: weight,
1042
+ firstFrame: true,
1043
+ started: false,
1044
+ paused: false,
1045
+ stopReason: 'manual',
1046
+ completed: false,
1047
+ rangeStart,
1048
+ rangeEnd
674
1049
  });
675
1050
  ani.tracks?.forEach((v, k)=>{
676
1051
  let nodeTracks = this._activeTracks.get(k);
@@ -707,6 +1082,245 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
707
1082
  }
708
1083
  });
709
1084
  }
1085
+ resolvePlaybackSyncSource(sync) {
1086
+ if (!sync?.target) {
1087
+ return null;
1088
+ }
1089
+ const playback = this.resolveActivePlayback(sync.target);
1090
+ if (!playback) {
1091
+ return null;
1092
+ }
1093
+ const info = this._activeAnimations.get(playback.clip);
1094
+ return info?.playback === playback ? {
1095
+ playback,
1096
+ info,
1097
+ clip: playback.clip
1098
+ } : null;
1099
+ }
1100
+ resolveActivePlayback(target) {
1101
+ const named = this.getPlayback(target);
1102
+ if (named) {
1103
+ return named;
1104
+ }
1105
+ for (const info of this._activeAnimations.values()){
1106
+ if (info.playback.id === target) {
1107
+ return info.playback;
1108
+ }
1109
+ }
1110
+ return null;
1111
+ }
1112
+ computePlaybackInitialTime(clip, speedRatio, rangeStart, rangeEnd, sync, syncSource) {
1113
+ const fallback = speedRatio < 0 ? rangeEnd ?? rangeStart ?? clip.timeDuration : rangeStart ?? 0;
1114
+ if (!sync || !syncSource) {
1115
+ return fallback;
1116
+ }
1117
+ const destination = this.getPlaybackEffectiveRange(clip, rangeStart, rangeEnd);
1118
+ if (destination.duration <= 0) {
1119
+ return fallback;
1120
+ }
1121
+ const offset = Number.isFinite(sync.offset) ? sync.offset ?? 0 : 0;
1122
+ const wrap = sync.wrap ?? true;
1123
+ if ((sync.mode ?? 'normalized') === 'time') {
1124
+ return this.constrainPlaybackTime(syncSource.info.currentTime + offset, destination, wrap);
1125
+ }
1126
+ const source = this.getPlaybackEffectiveRange(syncSource.clip, syncSource.info.rangeStart, syncSource.info.rangeEnd);
1127
+ if (source.duration <= 0) {
1128
+ return fallback;
1129
+ }
1130
+ const phase = (syncSource.info.currentTime - source.start) / source.duration + offset;
1131
+ return this.constrainPlaybackTime(destination.start + phase * destination.duration, destination, wrap);
1132
+ }
1133
+ getPlaybackEffectiveRange(clip, rangeStart, rangeEnd) {
1134
+ const start = rangeStart ?? 0;
1135
+ const end = rangeEnd ?? clip.timeDuration;
1136
+ return start <= end ? {
1137
+ start,
1138
+ end,
1139
+ duration: end - start
1140
+ } : {
1141
+ start: end,
1142
+ end: start,
1143
+ duration: start - end
1144
+ };
1145
+ }
1146
+ constrainPlaybackTime(time, range, wrap) {
1147
+ if (!Number.isFinite(time)) {
1148
+ return range.start;
1149
+ }
1150
+ if (!wrap) {
1151
+ return Math.max(range.start, Math.min(range.end, time));
1152
+ }
1153
+ if (time >= range.start && time < range.end) {
1154
+ return time;
1155
+ }
1156
+ const offset = ((time - range.start) % range.duration + range.duration) % range.duration;
1157
+ return range.start + offset;
1158
+ }
1159
+ /**
1160
+ * Stop a playback handle.
1161
+ * @internal
1162
+ */ stopPlayback(playback, options) {
1163
+ if (this._activeAnimations.get(playback.clip)?.playback === playback) {
1164
+ this.stopAnimation(playback.clip.name, options);
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Pause a playback handle.
1169
+ * @internal
1170
+ */ pausePlayback(playback) {
1171
+ const info = this._activeAnimations.get(playback.clip);
1172
+ if (info?.playback === playback && !info.paused) {
1173
+ info.paused = true;
1174
+ playback._setState('paused');
1175
+ const event = this.createPlaybackEvent(info, playback.clip);
1176
+ playback._emitPause(event);
1177
+ this.dispatchEvent('playbackpause', event);
1178
+ }
1179
+ }
1180
+ /**
1181
+ * Resume a playback handle.
1182
+ * @internal
1183
+ */ resumePlayback(playback) {
1184
+ const info = this._activeAnimations.get(playback.clip);
1185
+ if (info?.playback === playback && info.paused) {
1186
+ info.paused = false;
1187
+ playback._setState('playing');
1188
+ const event = this.createPlaybackEvent(info, playback.clip);
1189
+ playback._emitResume(event);
1190
+ this.dispatchEvent('playbackresume', event);
1191
+ }
1192
+ }
1193
+ /**
1194
+ * Seek a playback handle.
1195
+ * @internal
1196
+ */ seekPlayback(playback, time, options) {
1197
+ const clip = playback.clip;
1198
+ const info = this._activeAnimations.get(clip);
1199
+ const duration = clip.timeDuration;
1200
+ const clampedTime = duration > 0 ? Math.max(0, Math.min(duration, time)) : Math.max(0, time);
1201
+ const previousTime = info?.currentTime ?? playback.time;
1202
+ playback._setTime(clampedTime);
1203
+ if (info?.playback === playback) {
1204
+ info.currentTime = clampedTime;
1205
+ if (options?.emitEvents) {
1206
+ const direction = clampedTime >= previousTime ? 1 : -1;
1207
+ this.emitCrossedMarkers(info, clip, previousTime, clampedTime, direction);
1208
+ this.emitCrossedFrames(info, clip, previousTime, clampedTime, direction);
1209
+ }
1210
+ if (options?.apply) {
1211
+ this.update(0);
1212
+ }
1213
+ }
1214
+ }
1215
+ /**
1216
+ * Set playback weight.
1217
+ * @internal
1218
+ */ setPlaybackWeight(playback, weight) {
1219
+ playback._setWeight(weight);
1220
+ const info = this._activeAnimations.get(playback.clip);
1221
+ if (info?.playback === playback) {
1222
+ info.weight = weight;
1223
+ info.targetWeight = weight;
1224
+ }
1225
+ }
1226
+ /**
1227
+ * Set playback speed ratio.
1228
+ * @internal
1229
+ */ setPlaybackSpeedRatio(playback, speedRatio) {
1230
+ playback._setSpeedRatio(speedRatio);
1231
+ const info = this._activeAnimations.get(playback.clip);
1232
+ if (info?.playback === playback) {
1233
+ info.speedRatio = speedRatio;
1234
+ }
1235
+ }
1236
+ /**
1237
+ * Fade playback weight to a target value.
1238
+ * @internal
1239
+ */ fadePlaybackTo(playback, weight, duration) {
1240
+ const info = this._activeAnimations.get(playback.clip);
1241
+ if (info?.playback === playback) {
1242
+ info.fadeFromWeight = info.weight;
1243
+ info.fadeTargetWeight = weight;
1244
+ info.targetWeight = weight;
1245
+ info.fadeToStart = info.animateTime;
1246
+ info.fadeToDuration = Math.max(duration, 0);
1247
+ if (info.fadeToDuration === 0) {
1248
+ info.weight = weight;
1249
+ playback._setWeight(weight);
1250
+ }
1251
+ } else {
1252
+ playback._setWeight(weight);
1253
+ }
1254
+ }
1255
+ createPlaybackEvent(info, clip) {
1256
+ return {
1257
+ playback: info.playback,
1258
+ animationSet: this,
1259
+ clip,
1260
+ time: info.currentTime,
1261
+ normalizedTime: clip.timeDuration > 0 ? info.currentTime / clip.timeDuration : 0
1262
+ };
1263
+ }
1264
+ completePlayback(clip, info) {
1265
+ if (info.completed) {
1266
+ return;
1267
+ }
1268
+ const event = this.createPlaybackEvent(info, clip);
1269
+ const completionFadeOut = Math.max(info.playback._getOptions().completionFadeOut ?? 0, 0);
1270
+ info.playback._setState('completed');
1271
+ info.playback._emitComplete(event);
1272
+ this.dispatchEvent('playbackcomplete', event);
1273
+ if (completionFadeOut > 0) {
1274
+ info.completed = true;
1275
+ info.fadeOut = completionFadeOut;
1276
+ info.fadeOutStart = info.animateTime;
1277
+ info.stopReason = 'completed';
1278
+ return;
1279
+ }
1280
+ this.stopAnimation(clip.name, {
1281
+ reason: 'completed'
1282
+ });
1283
+ }
1284
+ emitCrossedMarkers(info, clip, fromTime, toTime, direction) {
1285
+ for (const marker of this.getCrossedMarkers(clip, fromTime, toTime, direction)){
1286
+ const event = {
1287
+ ...this.createPlaybackEvent(info, clip),
1288
+ marker,
1289
+ direction
1290
+ };
1291
+ info.playback._emitMarker(event);
1292
+ this.dispatchEvent('marker', event);
1293
+ }
1294
+ }
1295
+ emitCrossedFrames(info, clip, fromTime, toTime, direction) {
1296
+ for (const frame of info.playback._getWatchedFrames()){
1297
+ const frameTime = frame / clip.frameRate;
1298
+ const crossed = direction >= 0 ? this.crossedForward(fromTime, toTime, frameTime, clip.timeDuration) : this.crossedBackward(fromTime, toTime, frameTime, clip.timeDuration);
1299
+ if (crossed) {
1300
+ const event = {
1301
+ ...this.createPlaybackEvent(info, clip),
1302
+ frame
1303
+ };
1304
+ info.playback._emitFrame(event);
1305
+ this.dispatchEvent('frame', event);
1306
+ }
1307
+ }
1308
+ }
1309
+ getCrossedMarkers(clip, fromTime, toTime, direction) {
1310
+ return clip.markers.filter((marker)=>{
1311
+ const markerTime = clip.resolveMarkerTime(marker);
1312
+ if (markerTime === null) {
1313
+ return false;
1314
+ }
1315
+ return direction >= 0 ? this.crossedForward(fromTime, toTime, markerTime, clip.timeDuration) : this.crossedBackward(fromTime, toTime, markerTime, clip.timeDuration);
1316
+ });
1317
+ }
1318
+ crossedForward(fromTime, toTime, targetTime, duration) {
1319
+ return toTime >= fromTime ? targetTime > fromTime && targetTime <= toTime : duration > 0 && targetTime > fromTime && targetTime <= duration || targetTime <= toTime;
1320
+ }
1321
+ crossedBackward(fromTime, toTime, targetTime, duration) {
1322
+ return toTime <= fromTime ? targetTime < fromTime && targetTime >= toTime : duration > 0 && targetTime < fromTime && targetTime >= 0 || targetTime >= toTime;
1323
+ }
710
1324
  /**
711
1325
  * Stop playback of an animation clip.
712
1326
  *
@@ -728,9 +1342,12 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
728
1342
  const info = this._activeAnimations.get(ani);
729
1343
  if (info) {
730
1344
  const fadeOut = Math.max(options?.fadeOut ?? 0, 0);
1345
+ const reason = options?.reason ?? 'manual';
731
1346
  if (fadeOut !== 0) {
732
1347
  info.fadeOut = fadeOut;
733
1348
  info.fadeOutStart = -1;
1349
+ info.stopReason = reason;
1350
+ info.playback._setState('stopping');
734
1351
  } else {
735
1352
  this._activeAnimations.delete(ani);
736
1353
  this._activeTracks.forEach((v)=>{
@@ -768,6 +1385,15 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
768
1385
  }
769
1386
  }
770
1387
  });
1388
+ const event = {
1389
+ ...this.createPlaybackEvent(info, ani),
1390
+ reason
1391
+ };
1392
+ if (reason !== 'completed') {
1393
+ info.playback._setState('stopped');
1394
+ }
1395
+ info.playback._emitStop(event);
1396
+ this.dispatchEvent('playbackstop', event);
771
1397
  }
772
1398
  }
773
1399
  }
@@ -1176,5 +1802,5 @@ function bakeHumanoidRotationTracks(sourceClip, srcSkeleton, dstSkeleton, srcRoo
1176
1802
  }
1177
1803
  }
1178
1804
 
1179
- export { AnimationSet };
1805
+ export { AnimationPlayback, AnimationSet };
1180
1806
  //# sourceMappingURL=animationset.js.map