@aura3d/animation 4.7.12 → 5.0.0

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.
Files changed (198) hide show
  1. package/dist/AnimationAction.d.ts +80 -1
  2. package/dist/AnimationAction.d.ts.map +1 -1
  3. package/dist/AnimationAction.js +450 -80
  4. package/dist/AnimationAction.js.map +1 -1
  5. package/dist/AnimationClip.d.ts.map +1 -1
  6. package/dist/AnimationClip.js.map +1 -1
  7. package/dist/AnimationClipEvents.d.ts.map +1 -1
  8. package/dist/AnimationClipEvents.js.map +1 -1
  9. package/dist/AnimationClipRegistry.d.ts.map +1 -1
  10. package/dist/AnimationClipRegistry.js.map +1 -1
  11. package/dist/AnimationController.d.ts.map +1 -1
  12. package/dist/AnimationController.js.map +1 -1
  13. package/dist/AnimationEvents.d.ts +23 -0
  14. package/dist/AnimationEvents.d.ts.map +1 -1
  15. package/dist/AnimationEvents.js +145 -36
  16. package/dist/AnimationEvents.js.map +1 -1
  17. package/dist/AnimationLayer.d.ts.map +1 -1
  18. package/dist/AnimationLayer.js.map +1 -1
  19. package/dist/AnimationMixer.d.ts +58 -3
  20. package/dist/AnimationMixer.d.ts.map +1 -1
  21. package/dist/AnimationMixer.js +366 -12
  22. package/dist/AnimationMixer.js.map +1 -1
  23. package/dist/AnimationObjectGroup.d.ts +36 -12
  24. package/dist/AnimationObjectGroup.d.ts.map +1 -1
  25. package/dist/AnimationObjectGroup.js +219 -208
  26. package/dist/AnimationObjectGroup.js.map +1 -1
  27. package/dist/AnimationStateGraph.d.ts.map +1 -1
  28. package/dist/AnimationStateGraph.js.map +1 -1
  29. package/dist/AnimationStateMachine.d.ts.map +1 -1
  30. package/dist/AnimationStateMachine.js.map +1 -1
  31. package/dist/AnimationTrack.d.ts +7 -1
  32. package/dist/AnimationTrack.d.ts.map +1 -1
  33. package/dist/AnimationTrack.js +87 -10
  34. package/dist/AnimationTrack.js.map +1 -1
  35. package/dist/AnimationUtils.d.ts +16 -14
  36. package/dist/AnimationUtils.d.ts.map +1 -1
  37. package/dist/AnimationUtils.js +148 -109
  38. package/dist/AnimationUtils.js.map +1 -1
  39. package/dist/BlendTree.d.ts.map +1 -1
  40. package/dist/BlendTree.js.map +1 -1
  41. package/dist/Bone.d.ts.map +1 -1
  42. package/dist/Bone.js.map +1 -1
  43. package/dist/ECSAnimationBridge.d.ts.map +1 -1
  44. package/dist/ECSAnimationBridge.js.map +1 -1
  45. package/dist/FighterAnimationAdapter.d.ts.map +1 -1
  46. package/dist/FighterAnimationAdapter.js.map +1 -1
  47. package/dist/FootIk.d.ts.map +1 -1
  48. package/dist/FootIk.js.map +1 -1
  49. package/dist/HumanoidBoneInference.d.ts.map +1 -1
  50. package/dist/HumanoidBoneInference.js.map +1 -1
  51. package/dist/HumanoidRetargeting.d.ts.map +1 -1
  52. package/dist/HumanoidRetargeting.js.map +1 -1
  53. package/dist/IK.d.ts.map +1 -1
  54. package/dist/IK.js.map +1 -1
  55. package/dist/Inertialization.d.ts.map +1 -1
  56. package/dist/Inertialization.js.map +1 -1
  57. package/dist/Keyframe.d.ts +15 -1
  58. package/dist/Keyframe.d.ts.map +1 -1
  59. package/dist/Keyframe.js +34 -1
  60. package/dist/Keyframe.js.map +1 -1
  61. package/dist/LocomotionController.d.ts.map +1 -1
  62. package/dist/LocomotionController.js.map +1 -1
  63. package/dist/LocomotionEvidence.d.ts.map +1 -1
  64. package/dist/LocomotionEvidence.js.map +1 -1
  65. package/dist/LocomotionKit.d.ts.map +1 -1
  66. package/dist/LocomotionKit.js.map +1 -1
  67. package/dist/MotionQuality.d.ts.map +1 -1
  68. package/dist/MotionQuality.js.map +1 -1
  69. package/dist/PropertyBinding.d.ts +42 -22
  70. package/dist/PropertyBinding.d.ts.map +1 -1
  71. package/dist/PropertyBinding.js +390 -293
  72. package/dist/PropertyBinding.js.map +1 -1
  73. package/dist/RigQuality.d.ts.map +1 -1
  74. package/dist/RigQuality.js.map +1 -1
  75. package/dist/RootMotion.d.ts.map +1 -1
  76. package/dist/RootMotion.js.map +1 -1
  77. package/dist/SceneAnimationBridge.d.ts +7 -0
  78. package/dist/SceneAnimationBridge.d.ts.map +1 -1
  79. package/dist/SceneAnimationBridge.js +48 -3
  80. package/dist/SceneAnimationBridge.js.map +1 -1
  81. package/dist/Skeleton.d.ts.map +1 -1
  82. package/dist/Skeleton.js.map +1 -1
  83. package/dist/Skinning.d.ts.map +1 -1
  84. package/dist/Skinning.js.map +1 -1
  85. package/dist/SpringBones.d.ts.map +1 -1
  86. package/dist/SpringBones.js.map +1 -1
  87. package/dist/browser-index.d.ts +1 -1
  88. package/dist/browser-index.d.ts.map +1 -1
  89. package/dist/browser-index.js +1 -1
  90. package/dist/browser-index.js.map +1 -1
  91. package/dist/contracts/index.d.ts.map +1 -1
  92. package/dist/contracts/index.js.map +1 -1
  93. package/dist/contracts/pose.d.ts.map +1 -1
  94. package/dist/contracts/pose.js.map +1 -1
  95. package/dist/experimental/CrowdAnimation.d.ts.map +1 -1
  96. package/dist/experimental/CrowdAnimation.js.map +1 -1
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +1 -1
  100. package/dist/index.js.map +1 -1
  101. package/dist/lanes/index.d.ts.map +1 -1
  102. package/dist/lanes/index.js.map +1 -1
  103. package/dist/lanes/prd01.d.ts.map +1 -1
  104. package/dist/lanes/prd01.js.map +1 -1
  105. package/dist/lanes/prd02.d.ts.map +1 -1
  106. package/dist/lanes/prd02.js.map +1 -1
  107. package/dist/lanes/prd03.d.ts.map +1 -1
  108. package/dist/lanes/prd03.js.map +1 -1
  109. package/dist/lanes/prd04.d.ts.map +1 -1
  110. package/dist/lanes/prd04.js.map +1 -1
  111. package/dist/lanes/prd05.d.ts.map +1 -1
  112. package/dist/lanes/prd05.js.map +1 -1
  113. package/dist/lanes/prd06.d.ts.map +1 -1
  114. package/dist/lanes/prd06.js.map +1 -1
  115. package/dist/lanes/prd07.d.ts.map +1 -1
  116. package/dist/lanes/prd07.js.map +1 -1
  117. package/dist/lanes/prd08.d.ts.map +1 -1
  118. package/dist/lanes/prd08.js.map +1 -1
  119. package/dist/lanes/prd09.d.ts.map +1 -1
  120. package/dist/lanes/prd09.js.map +1 -1
  121. package/dist/lanes/prd10.d.ts.map +1 -1
  122. package/dist/lanes/prd10.js.map +1 -1
  123. package/dist/lanes/prd11.d.ts.map +1 -1
  124. package/dist/lanes/prd11.js.map +1 -1
  125. package/dist/lanes/prd12.d.ts.map +1 -1
  126. package/dist/lanes/prd12.js.map +1 -1
  127. package/dist/lanes/prd13.d.ts.map +1 -1
  128. package/dist/lanes/prd13.js.map +1 -1
  129. package/dist/lanes/prd14.d.ts.map +1 -1
  130. package/dist/lanes/prd14.js.map +1 -1
  131. package/dist/lanes/prd15.d.ts.map +1 -1
  132. package/dist/lanes/prd15.js.map +1 -1
  133. package/dist/library/performanceStateGraph.d.ts.map +1 -1
  134. package/dist/library/performanceStateGraph.js.map +1 -1
  135. package/dist/library/standardHumanoidClips.d.ts.map +1 -1
  136. package/dist/library/standardHumanoidClips.js.map +1 -1
  137. package/dist/pose/BoneMask.d.ts.map +1 -1
  138. package/dist/pose/BoneMask.js.map +1 -1
  139. package/dist/pose/CcdIkConstraint.d.ts.map +1 -1
  140. package/dist/pose/CcdIkConstraint.js.map +1 -1
  141. package/dist/pose/CompiledClip.d.ts +15 -8
  142. package/dist/pose/CompiledClip.d.ts.map +1 -1
  143. package/dist/pose/CompiledClip.js +82 -50
  144. package/dist/pose/CompiledClip.js.map +1 -1
  145. package/dist/pose/LookAtConstraint.d.ts.map +1 -1
  146. package/dist/pose/LookAtConstraint.js.map +1 -1
  147. package/dist/pose/MotionMetrics.d.ts.map +1 -1
  148. package/dist/pose/MotionMetrics.js.map +1 -1
  149. package/dist/pose/PoseBuffer.d.ts.map +1 -1
  150. package/dist/pose/PoseBuffer.js.map +1 -1
  151. package/dist/pose/PoseInertializer.d.ts.map +1 -1
  152. package/dist/pose/PoseInertializer.js.map +1 -1
  153. package/dist/pose/PoseMixer.d.ts +196 -19
  154. package/dist/pose/PoseMixer.d.ts.map +1 -1
  155. package/dist/pose/PoseMixer.js +1209 -188
  156. package/dist/pose/PoseMixer.js.map +1 -1
  157. package/dist/pose/Retarget.d.ts +4 -3
  158. package/dist/pose/Retarget.d.ts.map +1 -1
  159. package/dist/pose/Retarget.js +32 -15
  160. package/dist/pose/Retarget.js.map +1 -1
  161. package/dist/pose/RetargetCache.d.ts +12 -6
  162. package/dist/pose/RetargetCache.d.ts.map +1 -1
  163. package/dist/pose/RetargetCache.js +253 -29
  164. package/dist/pose/RetargetCache.js.map +1 -1
  165. package/dist/pose/RetargetWorker.d.ts.map +1 -1
  166. package/dist/pose/RetargetWorker.js +5 -0
  167. package/dist/pose/RetargetWorker.js.map +1 -1
  168. package/dist/pose/SkeletonBinding.d.ts.map +1 -1
  169. package/dist/pose/SkeletonBinding.js.map +1 -1
  170. package/dist/pose/blendKernels.d.ts.map +1 -1
  171. package/dist/pose/blendKernels.js.map +1 -1
  172. package/dist/pose/makeClipAdditive.d.ts +7 -14
  173. package/dist/pose/makeClipAdditive.d.ts.map +1 -1
  174. package/dist/pose/makeClipAdditive.js +53 -85
  175. package/dist/pose/makeClipAdditive.js.map +1 -1
  176. package/dist/pose/poseMixerFlags.d.ts.map +1 -1
  177. package/dist/pose/poseMixerFlags.js.map +1 -1
  178. package/dist/pose/quatFlat.d.ts.map +1 -1
  179. package/dist/pose/quatFlat.js.map +1 -1
  180. package/dist/pose/retarget.worker.d.ts.map +1 -1
  181. package/dist/pose/retarget.worker.js.map +1 -1
  182. package/dist/threejs-compatibility/AnimationAction.d.ts.map +1 -1
  183. package/dist/threejs-compatibility/AnimationAction.js.map +1 -1
  184. package/dist/threejs-compatibility/AnimationClip.d.ts.map +1 -1
  185. package/dist/threejs-compatibility/AnimationClip.js.map +1 -1
  186. package/dist/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -1
  187. package/dist/threejs-compatibility/AnimationDiagnostics.js.map +1 -1
  188. package/dist/threejs-compatibility/AnimationMixer.d.ts.map +1 -1
  189. package/dist/threejs-compatibility/AnimationMixer.js.map +1 -1
  190. package/dist/threejs-compatibility/MorphTargetMixer.d.ts.map +1 -1
  191. package/dist/threejs-compatibility/MorphTargetMixer.js.map +1 -1
  192. package/dist/threejs-compatibility/Skeleton.d.ts.map +1 -1
  193. package/dist/threejs-compatibility/Skeleton.js.map +1 -1
  194. package/dist/threejs-compatibility/SkinnedMesh.d.ts.map +1 -1
  195. package/dist/threejs-compatibility/SkinnedMesh.js.map +1 -1
  196. package/dist/threejs-compatibility/index.d.ts.map +1 -1
  197. package/dist/threejs-compatibility/index.js.map +1 -1
  198. package/package.json +2 -2
@@ -1,4 +1,6 @@
1
- import { eventsBetween } from "./AnimationEvents.js";
1
+ import { AnimationClip } from "./AnimationClip.js";
2
+ import { collectAnimationEvents, DEFAULT_MAX_ANIMATION_EVENTS, MAX_ANIMATION_EVENT_CATCH_UP } from "./AnimationEvents.js";
3
+ import { AnimationTrack } from "./AnimationTrack.js";
2
4
  export class AnimationAction {
3
5
  clip;
4
6
  time = 0;
@@ -25,14 +27,20 @@ export class AnimationAction {
25
27
  clampWhenFinished = false;
26
28
  playing = false;
27
29
  paused = false;
30
+ /** Maximum marker callbacks produced by one update. Excess is reported in playback signals. */
31
+ maxEventCatchUp = DEFAULT_MAX_ANIMATION_EVENTS;
32
+ /** Signed unwrapped clip-time distance actually traveled by the last update. */
33
+ lastPlaybackDelta = 0;
28
34
  pingPongForward = true;
29
35
  loopsCompleted = 0;
30
36
  signalLoopDelta = 0;
31
37
  signalFinishedDirection = 0;
38
+ signalEventOverflow = 0;
32
39
  fadeDuration = 0;
33
40
  fadeElapsed = 0;
34
41
  fadeStartWeight = 1;
35
42
  fadeTargetWeight = 1;
43
+ warpState = null;
36
44
  constructor(clip) {
37
45
  this.clip = clip;
38
46
  }
@@ -50,6 +58,7 @@ export class AnimationAction {
50
58
  this.loopsCompleted = 0;
51
59
  this.signalLoopDelta = 0;
52
60
  this.signalFinishedDirection = 0;
61
+ this.signalEventOverflow = 0;
53
62
  }
54
63
  this.playing = true;
55
64
  this.paused = false;
@@ -68,6 +77,12 @@ export class AnimationAction {
68
77
  this.time = 0;
69
78
  this.pingPongForward = true;
70
79
  this.loopsCompleted = 0;
80
+ this.lastPlaybackDelta = 0;
81
+ this.signalLoopDelta = 0;
82
+ this.signalFinishedDirection = 0;
83
+ this.signalEventOverflow = 0;
84
+ this.fadeDuration = 0;
85
+ this.warpState = null;
71
86
  return this;
72
87
  }
73
88
  setWeight(weight) {
@@ -91,6 +106,9 @@ export class AnimationAction {
91
106
  * `repeat`/`pingpong` default to `Infinity`.
92
107
  */
93
108
  setLoop(loopMode, repetitions) {
109
+ if (loopMode !== "once" && loopMode !== "repeat" && loopMode !== "pingpong") {
110
+ throw new Error(`AnimationAction.setLoop: unknown loop mode ${String(loopMode)}.`);
111
+ }
94
112
  if (repetitions !== undefined) {
95
113
  if (!(repetitions === Infinity) && (!Number.isFinite(repetitions) || repetitions < 0)) {
96
114
  throw new Error(`AnimationAction.setLoop: repetitions must be a non-negative number or Infinity, got ${repetitions}.`);
@@ -107,6 +125,21 @@ export class AnimationAction {
107
125
  this.loopsCompleted = 0;
108
126
  this.signalLoopDelta = 0;
109
127
  this.signalFinishedDirection = 0;
128
+ this.signalEventOverflow = 0;
129
+ this.lastPlaybackDelta = 0;
130
+ return this;
131
+ }
132
+ /** Copy the phase and reflected direction of another action, optionally scaling its phase. */
133
+ synchronizePlayback(other, normalized = false) {
134
+ if (!(other instanceof AnimationAction))
135
+ throw new Error("AnimationAction.synchronizePlayback: expected an AnimationAction.");
136
+ this.time = normalized
137
+ ? (other.clip.duration > 0 ? other.time / other.clip.duration * this.clip.duration : 0)
138
+ : Math.min(other.time, this.clip.duration);
139
+ this.pingPongForward = other.pingPongForward;
140
+ this.loopsCompleted = other.loopsCompleted;
141
+ this.timeScale = other.timeScale;
142
+ this.warpState = null;
110
143
  return this;
111
144
  }
112
145
  seek(time) {
@@ -133,6 +166,25 @@ export class AnimationAction {
133
166
  }
134
167
  return this;
135
168
  }
169
+ /** Linearly interpolate the signed playback rate over unpaused mixer time. */
170
+ warp(startTimeScale, endTimeScale, duration) {
171
+ if (![startTimeScale, endTimeScale, duration].every(Number.isFinite) ||
172
+ !Number.isFinite(endTimeScale - startTimeScale) || duration < 0) {
173
+ throw new Error("AnimationAction.warp: rates and duration must be finite, and duration must be non-negative.");
174
+ }
175
+ if (duration === 0) {
176
+ this.warpState = null;
177
+ this.timeScale = endTimeScale;
178
+ }
179
+ else {
180
+ this.warpState = { duration, elapsed: 0, from: startTimeScale, to: endTimeScale };
181
+ }
182
+ return this;
183
+ }
184
+ /** @internal Convert travel through a monotonic linear-rate segment to mixer time. */
185
+ static consumedWarpDuration(segment, travelled) {
186
+ return consumedWarpDuration(segment, travelled);
187
+ }
136
188
  /**
137
189
  * Advances playback by `delta` seconds scaled by `timeScale` (signed — a
138
190
  * negative product reverses the playhead) and returns the clip marker events
@@ -152,7 +204,7 @@ export class AnimationAction {
152
204
  if (!Number.isFinite(this.timeScale)) {
153
205
  throw new Error("AnimationAction.update: timeScale must be finite; call AnimationAction.setTimeScale with a valid value.");
154
206
  }
155
- if (this.fadeDuration > 0) {
207
+ if (this.playing && this.fadeDuration > 0) {
156
208
  this.fadeElapsed = Math.min(this.fadeDuration, this.fadeElapsed + Math.abs(delta));
157
209
  const t = this.fadeElapsed / this.fadeDuration;
158
210
  this.weight = this.fadeStartWeight + (this.fadeTargetWeight - this.fadeStartWeight) * t;
@@ -160,92 +212,149 @@ export class AnimationAction {
160
212
  this.fadeDuration = 0;
161
213
  }
162
214
  }
215
+ this.lastPlaybackDelta = 0;
163
216
  if (!this.playing || this.paused || this.clip.duration === 0) {
164
217
  return [];
165
218
  }
166
- const previous = this.time;
167
- const scaledDelta = delta * this.timeScale;
168
- const duration = this.clip.duration;
169
- let looped = false;
170
- let finishedThisUpdate = false;
171
- let next;
172
- if (this.loopMode === "repeat") {
173
- next = previous + scaledDelta;
174
- if (next >= duration || next < 0) {
175
- const loopDelta = Math.floor(next / duration);
176
- next -= duration * loopDelta;
177
- this.loopsCompleted += Math.abs(loopDelta);
178
- looped = true;
179
- if (this.loopsCompleted >= this.repetitions) {
180
- next = scaledDelta >= 0 ? duration : 0;
181
- finishedThisUpdate = true;
182
- this.finish(scaledDelta >= 0 ? 1 : -1);
183
- }
184
- else {
185
- // three dispatches 'loop' for intermediate wraps but 'finished'
186
- // instead of 'loop' on the wrap that exhausts repetitions.
187
- this.signalLoopDelta += loopDelta;
188
- }
189
- }
219
+ if (!Number.isSafeInteger(this.maxEventCatchUp) || this.maxEventCatchUp < 0 || this.maxEventCatchUp > MAX_ANIMATION_EVENT_CATCH_UP)
220
+ throw new Error(`AnimationAction.update: maxEventCatchUp must be an integer from 0 to ${MAX_ANIMATION_EVENT_CATCH_UP}.`);
221
+ const warp = this.warpState;
222
+ if (!warp) {
223
+ const scaledDelta = delta * this.timeScale;
224
+ if (!Number.isFinite(scaledDelta))
225
+ throw new Error("AnimationAction.update: scaled delta must be finite.");
226
+ return scaledDelta === 0 ? [] : this.advancePlayback(scaledDelta, this.maxEventCatchUp);
190
227
  }
191
- else if (this.loopMode === "once") {
192
- next = previous + scaledDelta;
193
- if (scaledDelta !== 0) {
194
- if (next >= duration) {
195
- next = duration;
196
- this.finish(1);
197
- }
198
- else if (next <= 0) {
199
- next = 0;
200
- this.finish(-1);
228
+ const segments = AnimationAction.warpSegments(delta, warp);
229
+ const events = [];
230
+ for (const segment of segments) {
231
+ if (!this.playing || this.paused)
232
+ break;
233
+ const previousDelta = this.lastPlaybackDelta;
234
+ if (segment.distance !== 0) {
235
+ for (const event of this.advancePlayback(segment.distance, this.maxEventCatchUp - events.length))
236
+ events.push(event);
237
+ }
238
+ const finished = !this.playing || this.paused;
239
+ if (segment.inWarp) {
240
+ const consumed = finished
241
+ ? consumedWarpDuration(segment, Math.abs(this.lastPlaybackDelta - previousDelta))
242
+ : segment.duration;
243
+ warp.elapsed = Math.min(warp.duration, segment.elapsed + consumed);
244
+ if (warp.elapsed === warp.duration) {
245
+ this.timeScale = warp.to;
246
+ this.warpState = null;
201
247
  }
202
248
  }
249
+ if (finished)
250
+ break;
251
+ }
252
+ if (this.warpState === null && warp.to === 0 && this.playing)
253
+ this.paused = true;
254
+ return events;
255
+ }
256
+ /** @internal Integrate monotonic parts separately so a rate reversal retains marker crossings. */
257
+ static warpSegments(delta, warp) {
258
+ const amount = Math.abs(delta);
259
+ const span = Math.min(amount, warp.duration - warp.elapsed);
260
+ const endElapsed = warp.elapsed + span;
261
+ const rateAt = (elapsed) => warp.from + (warp.to - warp.from) * (elapsed / warp.duration);
262
+ const startRate = rateAt(warp.elapsed);
263
+ const endRate = rateAt(endElapsed);
264
+ const direction = delta < 0 ? -1 : 1;
265
+ const segments = [];
266
+ const add = (elapsed, duration, from, to, inWarp) => {
267
+ const distance = direction * duration * (from / 2 + to / 2);
268
+ if (!Number.isFinite(distance))
269
+ throw new Error("AnimationAction.update: warped playback span must be finite.");
270
+ segments.push({ elapsed, duration, from, to, inWarp, distance });
271
+ };
272
+ if ((startRate < 0 && endRate > 0) || (startRate > 0 && endRate < 0)) {
273
+ const firstSpan = span * (-startRate / (endRate - startRate));
274
+ add(warp.elapsed, firstSpan, startRate, 0, true);
275
+ add(warp.elapsed + firstSpan, span - firstSpan, 0, endRate, true);
203
276
  }
204
277
  else {
205
- // pingpong: clip time bounces between the 0 and duration boundaries; each
206
- // reflection counts one repetition (three's loopCount parity).
207
- next = this.pingPongForward ? previous + scaledDelta : previous - scaledDelta;
208
- let crossings = 0;
209
- let exhausted = false;
210
- while (!exhausted && (next > duration || next < 0)) {
211
- next = next > duration ? 2 * duration - next : -next;
212
- this.pingPongForward = !this.pingPongForward;
213
- crossings++;
214
- if (this.loopsCompleted + crossings >= this.repetitions) {
215
- exhausted = true;
216
- }
278
+ add(warp.elapsed, span, startRate, endRate, true);
279
+ }
280
+ if (endElapsed === warp.duration) {
281
+ add(warp.duration, amount - span, warp.to, warp.to, false);
282
+ }
283
+ return segments;
284
+ }
285
+ advancePlayback(scaledDelta, maxEvents) {
286
+ const previous = this.time;
287
+ const duration = this.clip.duration;
288
+ if (!Number.isFinite(previous) || previous < 0 || previous > duration)
289
+ throw new Error("AnimationAction.update: time must be finite and within the clip duration.");
290
+ if (this.loopMode !== "once" && this.loopMode !== "repeat" && this.loopMode !== "pingpong")
291
+ throw new Error("AnimationAction.update: unknown loop mode.");
292
+ if (this.repetitions !== Infinity && (!Number.isFinite(this.repetitions) || this.repetitions < 0))
293
+ throw new Error("AnimationAction.update: repetitions must be non-negative or Infinity.");
294
+ const sign = scaledDelta > 0 ? 1 : -1;
295
+ const direction = this.loopMode === "pingpong" && !this.pingPongForward ? -sign : sign;
296
+ const phase = this.loopMode === "pingpong" && !this.pingPongForward ? 2 * duration - previous : previous;
297
+ let distance = Math.abs(scaledDelta);
298
+ const first = direction > 0 ? duration - previous : previous;
299
+ // Repeated decimal frame intervals often land one ulp below a boundary.
300
+ // Normalize only within floating-point rounding error of that boundary.
301
+ const nearest = first + Math.round((distance - first) / duration) * duration;
302
+ const tolerance = Math.min(duration * 1e-12, 8 * Number.EPSILON * Math.max(duration, distance, Math.abs(previous), Number.MIN_VALUE));
303
+ if (nearest >= 0 && Number.isFinite(nearest) && Math.abs(distance - nearest) <= tolerance)
304
+ distance = nearest;
305
+ const available = this.loopMode === "once" ? 1 : Math.max(1, Math.ceil(this.repetitions - this.loopsCompleted));
306
+ let crossings = 0;
307
+ if (this.loopMode === "repeat" && sign < 0) {
308
+ // The reverse seam is entered when moving past zero; arriving exactly at
309
+ // zero leaves that endpoint visible until the next update.
310
+ if (distance > first)
311
+ crossings = Math.ceil((distance - first) / duration);
312
+ // A finite action must nevertheless finish as soon as it reaches its
313
+ // final boundary. Waiting for seam entry would leave it playing at zero
314
+ // until a subsequent nonzero update and defer the finished signal.
315
+ if (available !== Infinity && distance >= first) {
316
+ const arrivals = 1 + Math.floor((distance - first) / duration);
317
+ if (arrivals >= available)
318
+ crossings = available;
217
319
  }
218
- if (!exhausted) {
219
- // Landing exactly on the boundary the playhead moves toward counts as a
220
- // wrap (three counts t === duration as a completed loop).
221
- const clipVelocity = this.pingPongForward ? scaledDelta : -scaledDelta;
222
- if ((clipVelocity > 0 && next === duration) || (clipVelocity < 0 && next === 0)) {
320
+ }
321
+ else if (distance >= first) {
322
+ crossings = 1 + Math.floor((distance - first) / duration);
323
+ }
324
+ const finished = crossings > 0 && crossings >= available;
325
+ if (finished)
326
+ crossings = available;
327
+ if (!Number.isSafeInteger(crossings))
328
+ throw new Error("AnimationAction.update: playback crosses more than the safe boundary count.");
329
+ const travelled = finished ? first + (crossings - 1) * duration : distance;
330
+ if (!Number.isSafeInteger(this.loopsCompleted + crossings) || !Number.isFinite(phase) ||
331
+ !Number.isFinite(phase + sign * travelled) || (this.loopMode === "pingpong" && !Number.isFinite(2 * duration))) {
332
+ throw new Error("AnimationAction.update: playback span exceeds the supported numeric range.");
333
+ }
334
+ this.lastPlaybackDelta += sign * travelled;
335
+ let next = previous + direction * travelled;
336
+ if (crossings > 0) {
337
+ const tail = finished ? 0 : travelled - first - (crossings - 1) * duration;
338
+ if (this.loopMode === "pingpong") {
339
+ const odd = crossings % 2 === 1;
340
+ const boundary = direction > 0 ? (odd ? duration : 0) : (odd ? 0 : duration);
341
+ next = boundary + (odd ? -direction : direction) * tail;
342
+ if (odd && !finished)
223
343
  this.pingPongForward = !this.pingPongForward;
224
- crossings++;
225
- if (this.loopsCompleted + crossings >= this.repetitions) {
226
- exhausted = true;
227
- }
228
- }
229
- }
230
- this.loopsCompleted += crossings;
231
- if (crossings > 0 && !exhausted) {
232
- this.signalLoopDelta += scaledDelta < 0 ? -crossings : crossings;
233
- looped = true;
234
344
  }
235
- if (exhausted) {
236
- // The final parity selects which boundary the clip clamps to (three's
237
- // loopCount odd/even → duration/0).
238
- next = this.pingPongForward ? duration : 0;
239
- this.finish(scaledDelta >= 0 ? 1 : -1);
345
+ else {
346
+ const boundary = direction > 0 ? duration : 0;
347
+ next = finished ? boundary : (direction > 0 ? 0 : duration) + direction * tail;
240
348
  }
349
+ this.signalLoopDelta += sign * (crossings - (finished ? 1 : 0));
350
+ this.loopsCompleted += crossings;
241
351
  }
242
- this.time = next;
243
- if (this.clip.events.length === 0) {
244
- return [];
245
- }
246
- // A wrap that exhausts `repetitions` clamps the playhead to the boundary —
247
- // for marker purposes that is a single un-wrapped span from `previous`.
248
- return eventsBetween(this.clip.events, this.clip.name, previous, next, this.clip.duration, looped && !finishedThisUpdate, scaledDelta >= 0);
352
+ this.time = Math.max(0, Math.min(duration, next));
353
+ if (finished)
354
+ this.finish(sign);
355
+ const traversal = collectAnimationEvents(this.clip.events, this.clip.name, phase, phase + sign * travelled, duration, this.loopMode, this.loopsCompleted - crossings + (this.loopMode === "pingpong" && first === 0 && crossings > 0 && !finished ? 1 : 0), maxEvents, finished);
356
+ this.signalEventOverflow += traversal.overflow;
357
+ return traversal.events;
249
358
  }
250
359
  /**
251
360
  * Drains the `loop`/`finished` signals accumulated by the last `update`. The
@@ -256,16 +365,15 @@ export class AnimationAction {
256
365
  consumePlaybackSignals() {
257
366
  const signals = {
258
367
  loopDelta: this.signalLoopDelta,
259
- finishedDirection: this.signalFinishedDirection
368
+ finishedDirection: this.signalFinishedDirection,
369
+ eventOverflow: this.signalEventOverflow
260
370
  };
261
371
  this.signalLoopDelta = 0;
262
372
  this.signalFinishedDirection = 0;
373
+ this.signalEventOverflow = 0;
263
374
  return signals;
264
375
  }
265
376
  finish(direction) {
266
- if (this.signalFinishedDirection !== 0) {
267
- return;
268
- }
269
377
  this.signalFinishedDirection = direction;
270
378
  if (this.clampWhenFinished) {
271
379
  this.paused = true;
@@ -283,8 +391,270 @@ export class AnimationAction {
283
391
  timeScale: this.timeScale,
284
392
  playing: this.playing,
285
393
  paused: this.paused,
286
- loopMode: this.loopMode
394
+ loopMode: this.loopMode,
395
+ state: {
396
+ version: 1,
397
+ repetitions: this.repetitions === Infinity ? null : this.repetitions,
398
+ clampWhenFinished: this.clampWhenFinished,
399
+ maxEventCatchUp: this.maxEventCatchUp,
400
+ pingPongForward: this.pingPongForward,
401
+ loopsCompleted: this.loopsCompleted,
402
+ fade: this.fadeDuration > 0 ? {
403
+ duration: this.fadeDuration, elapsed: this.fadeElapsed,
404
+ startWeight: this.fadeStartWeight, targetWeight: this.fadeTargetWeight
405
+ } : null,
406
+ warp: this.warpState ? { ...this.warpState } : null,
407
+ lastPlaybackDelta: this.lastPlaybackDelta,
408
+ signals: {
409
+ loopDelta: this.signalLoopDelta,
410
+ finishedDirection: this.signalFinishedDirection,
411
+ eventOverflow: this.signalEventOverflow === Infinity ? null : this.signalEventOverflow
412
+ }
413
+ }
414
+ };
415
+ }
416
+ /**
417
+ * Capture a JSON-safe, restorable action state. The identity covers the full
418
+ * clip, including keyframes, interpolation and event payloads; custom clip
419
+ * implementations and non-JSON event payloads cannot be silently dropped.
420
+ */
421
+ serialize() {
422
+ const snapshot = this.snapshot();
423
+ const serialized = {
424
+ ...snapshot,
425
+ state: { ...snapshot.state, clipIdentity: animationClipIdentity(this.clip) }
287
426
  };
427
+ parseActionSnapshot(serialized, this.clip);
428
+ rejectActionNegativeZero(serialized);
429
+ return serialized;
430
+ }
431
+ /**
432
+ * Restore in place without calling `play()`, emitting events, or applying a
433
+ * pose. Pre-versioned snapshots retain their fields but start with unlimited
434
+ * repetitions, forward reflection, no transition and no pending signals.
435
+ */
436
+ restore(snapshot) {
437
+ const parsed = parseActionSnapshot(snapshot, this.clip);
438
+ this.time = parsed.time;
439
+ this.weight = parsed.weight;
440
+ this.timeScale = parsed.timeScale;
441
+ this.playing = parsed.playing;
442
+ this.paused = parsed.paused;
443
+ this.loopMode = parsed.loopMode;
444
+ this.repetitions = parsed.repetitions;
445
+ this.clampWhenFinished = parsed.clampWhenFinished;
446
+ this.maxEventCatchUp = parsed.maxEventCatchUp;
447
+ this.pingPongForward = parsed.pingPongForward;
448
+ this.loopsCompleted = parsed.loopsCompleted;
449
+ this.fadeDuration = parsed.fade?.duration ?? 0;
450
+ this.fadeElapsed = parsed.fade?.elapsed ?? 0;
451
+ this.fadeStartWeight = parsed.fade?.startWeight ?? parsed.weight;
452
+ this.fadeTargetWeight = parsed.fade?.targetWeight ?? parsed.weight;
453
+ this.warpState = parsed.warp ? { ...parsed.warp } : null;
454
+ this.lastPlaybackDelta = parsed.lastPlaybackDelta;
455
+ this.signalLoopDelta = parsed.signals.loopDelta;
456
+ this.signalFinishedDirection = parsed.signals.finishedDirection;
457
+ this.signalEventOverflow = parsed.signals.eventOverflow;
458
+ return this;
459
+ }
460
+ }
461
+ function rejectActionNegativeZero(value) {
462
+ if (typeof value === "number" && Object.is(value, -0)) {
463
+ throw new Error("AnimationAction.serialize: negative zero cannot round-trip through JSON.");
464
+ }
465
+ if (value && typeof value === "object") {
466
+ for (const nested of Object.values(value))
467
+ rejectActionNegativeZero(nested);
468
+ }
469
+ }
470
+ /** Invert the integral of a monotonic linear rate on one warp segment. */
471
+ function consumedWarpDuration(segment, travelled) {
472
+ const total = Math.abs(segment.distance);
473
+ if (total === 0)
474
+ return segment.duration;
475
+ const ratio = Math.min(1, Math.max(0, travelled / total));
476
+ if (ratio === 0)
477
+ return 0;
478
+ if (ratio >= 1 - 8 * Number.EPSILON)
479
+ return segment.duration;
480
+ const scale = Math.max(Math.abs(segment.from), Math.abs(segment.to));
481
+ const from = Math.abs(segment.from) / scale;
482
+ const to = Math.abs(segment.to) / scale;
483
+ const area = ratio * (from / 2 + to / 2);
484
+ const denominator = from + Math.sqrt(Math.max(0, from * from + 2 * (to - from) * area));
485
+ const fraction = denominator === 0 ? 0 : 2 * area / denominator;
486
+ return segment.duration * Math.max(0, Math.min(1, fraction));
487
+ }
488
+ function actionRecord(value, label, allowed, required = allowed) {
489
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
490
+ (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
491
+ throw new Error(`AnimationAction.restore: ${label} must be a plain object.`);
492
+ }
493
+ const descriptors = Object.getOwnPropertyDescriptors(value);
494
+ if (Object.getOwnPropertySymbols(value).length || Object.keys(descriptors).some(key => !allowed.includes(key) || !("value" in descriptors[key]) || !descriptors[key].enumerable)) {
495
+ throw new Error(`AnimationAction.restore: ${label} contains unsupported fields or callbacks.`);
496
+ }
497
+ if (required.some(key => !Object.prototype.hasOwnProperty.call(descriptors, key))) {
498
+ throw new Error(`AnimationAction.restore: ${label} is missing required fields.`);
499
+ }
500
+ return value;
501
+ }
502
+ function actionNumber(value, label, minimum = -Infinity) {
503
+ if (typeof value !== "number" || !Number.isFinite(value) || value < minimum) {
504
+ throw new Error(`AnimationAction.restore: ${label} must be a finite number${minimum === 0 ? " >= 0" : ""}.`);
505
+ }
506
+ return value;
507
+ }
508
+ function actionBoolean(value, label) {
509
+ if (typeof value !== "boolean")
510
+ throw new Error(`AnimationAction.restore: ${label} must be boolean.`);
511
+ return value;
512
+ }
513
+ function parseActionSnapshot(input, clip) {
514
+ const baseKeys = ["clipName", "duration", "time", "weight", "timeScale", "playing", "paused", "loopMode"];
515
+ const inputRecord = actionRecord(input, "snapshot", [...baseKeys, "state", "active"], baseKeys);
516
+ if (inputRecord.clipName !== clip.name || inputRecord.duration !== clip.duration) {
517
+ throw new Error("AnimationAction.restore: clip name and duration must match.");
518
+ }
519
+ const time = actionNumber(inputRecord.time, "time", 0);
520
+ if (time > clip.duration)
521
+ throw new Error("AnimationAction.restore: time exceeds clip duration.");
522
+ const weight = actionNumber(inputRecord.weight, "weight", 0);
523
+ const timeScale = actionNumber(inputRecord.timeScale, "timeScale");
524
+ const playing = actionBoolean(inputRecord.playing, "playing");
525
+ const paused = actionBoolean(inputRecord.paused, "paused");
526
+ const loopMode = inputRecord.loopMode;
527
+ if (loopMode !== "once" && loopMode !== "repeat" && loopMode !== "pingpong")
528
+ throw new Error("AnimationAction.restore: invalid loop mode.");
529
+ if ("active" in inputRecord && (typeof inputRecord.active !== "boolean" || inputRecord.active !== (playing && !paused && weight > 0))) {
530
+ throw new Error("AnimationAction.restore: active must agree with playback state.");
531
+ }
532
+ const base = {
533
+ time, weight, timeScale, playing, paused, loopMode, repetitions: Infinity, clampWhenFinished: false,
534
+ maxEventCatchUp: DEFAULT_MAX_ANIMATION_EVENTS, pingPongForward: true, loopsCompleted: 0,
535
+ fade: null, warp: null, lastPlaybackDelta: 0,
536
+ signals: { loopDelta: 0, finishedDirection: 0, eventOverflow: 0 }
537
+ };
538
+ if (!("state" in inputRecord))
539
+ return base;
540
+ const state = actionRecord(inputRecord.state, "state", [
541
+ "version", "clipIdentity", "repetitions", "clampWhenFinished", "maxEventCatchUp", "pingPongForward",
542
+ "loopsCompleted", "fade", "warp", "lastPlaybackDelta", "signals"
543
+ ]);
544
+ if (state.version !== 1)
545
+ throw new Error("AnimationAction.restore: unsupported state version.");
546
+ if (typeof state.clipIdentity !== "string" || state.clipIdentity !== animationClipIdentity(clip)) {
547
+ throw new Error("AnimationAction.restore: clip identity does not match; use serialize() for restorable snapshots.");
548
+ }
549
+ base.repetitions = state.repetitions === null ? Infinity : actionNumber(state.repetitions, "repetitions", 0);
550
+ base.clampWhenFinished = actionBoolean(state.clampWhenFinished, "clampWhenFinished");
551
+ base.pingPongForward = actionBoolean(state.pingPongForward, "pingPongForward");
552
+ base.maxEventCatchUp = actionNumber(state.maxEventCatchUp, "maxEventCatchUp", 0);
553
+ if (!Number.isSafeInteger(base.maxEventCatchUp) || base.maxEventCatchUp > MAX_ANIMATION_EVENT_CATCH_UP)
554
+ throw new Error("AnimationAction.restore: invalid maxEventCatchUp.");
555
+ base.loopsCompleted = actionNumber(state.loopsCompleted, "loopsCompleted", 0);
556
+ if (!Number.isSafeInteger(base.loopsCompleted))
557
+ throw new Error("AnimationAction.restore: invalid loopsCompleted.");
558
+ base.lastPlaybackDelta = actionNumber(state.lastPlaybackDelta, "lastPlaybackDelta");
559
+ if (state.fade !== null) {
560
+ const fade = actionRecord(state.fade, "fade", ["duration", "elapsed", "startWeight", "targetWeight"]);
561
+ const duration = actionNumber(fade.duration, "fade duration", 0);
562
+ const elapsed = actionNumber(fade.elapsed, "fade elapsed", 0);
563
+ if (duration === 0 || elapsed >= duration)
564
+ throw new Error("AnimationAction.restore: invalid fade interval.");
565
+ base.fade = { duration, elapsed, startWeight: actionNumber(fade.startWeight, "fade startWeight", 0), targetWeight: actionNumber(fade.targetWeight, "fade targetWeight", 0) };
566
+ }
567
+ if (state.warp !== null) {
568
+ const warp = actionRecord(state.warp, "warp", ["duration", "elapsed", "from", "to"]);
569
+ const duration = actionNumber(warp.duration, "warp duration", 0);
570
+ const elapsed = actionNumber(warp.elapsed, "warp elapsed", 0);
571
+ const from = actionNumber(warp.from, "warp from");
572
+ const to = actionNumber(warp.to, "warp to");
573
+ if (duration === 0 || elapsed >= duration || !Number.isFinite(to - from))
574
+ throw new Error("AnimationAction.restore: invalid warp interval.");
575
+ base.warp = { duration, elapsed, from, to };
576
+ }
577
+ const signals = actionRecord(state.signals, "signals", ["loopDelta", "finishedDirection", "eventOverflow"]);
578
+ const loopDelta = actionNumber(signals.loopDelta, "loopDelta");
579
+ const finishedDirection = signals.finishedDirection;
580
+ const eventOverflow = signals.eventOverflow === null ? Infinity : actionNumber(signals.eventOverflow, "eventOverflow", 0);
581
+ if (!Number.isSafeInteger(loopDelta) || (finishedDirection !== -1 && finishedDirection !== 0 && finishedDirection !== 1) ||
582
+ (eventOverflow !== Infinity && !Number.isSafeInteger(eventOverflow)))
583
+ throw new Error("AnimationAction.restore: invalid playback signals.");
584
+ base.signals = { loopDelta, finishedDirection, eventOverflow };
585
+ return base;
586
+ }
587
+ /**
588
+ * A canonical, collision-free clip identity rather than a name/duration hash.
589
+ * It deliberately rejects accessors, custom implementations, callbacks and
590
+ * values that JSON would change or discard.
591
+ */
592
+ function animationClipIdentity(clip) {
593
+ if (Object.getPrototypeOf(clip) !== AnimationClip.prototype ||
594
+ !hasOnlyDataFields(clip, ["name", "tracks", "events", "duration"])) {
595
+ throw new Error("AnimationAction.serialize: custom clip implementations cannot be serialized.");
596
+ }
597
+ assertAnimationArray(clip.tracks);
598
+ const tracks = clip.tracks.map(track => {
599
+ if (Object.getPrototypeOf(track) !== AnimationTrack.prototype ||
600
+ !hasOnlyDataFields(track, ["target", "valueType", "valueSize", "keyframes", "duration"])) {
601
+ throw new Error("AnimationAction.serialize: custom track implementations cannot be serialized.");
602
+ }
603
+ return { target: track.target, valueType: track.valueType, valueSize: track.valueSize, duration: track.duration, keyframes: track.keyframes };
604
+ });
605
+ return canonicalAnimationJSON({ name: clip.name, duration: clip.duration, tracks, events: clip.events }, new Set(), 0);
606
+ }
607
+ function hasOnlyDataFields(value, allowed) {
608
+ const descriptors = Object.getOwnPropertyDescriptors(value);
609
+ return Object.getOwnPropertySymbols(value).length === 0 &&
610
+ Object.keys(descriptors).every(key => allowed.includes(key) && "value" in descriptors[key] && descriptors[key].enumerable);
611
+ }
612
+ function assertAnimationArray(value) {
613
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || Object.getOwnPropertySymbols(value).length) {
614
+ throw new Error("AnimationAction.serialize: clip contains a nonserializable array.");
615
+ }
616
+ const descriptors = Object.getOwnPropertyDescriptors(value);
617
+ const keys = Object.keys(descriptors);
618
+ if (keys.length !== value.length + 1 || keys.some(key => key !== "length" && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length))) {
619
+ throw new Error("AnimationAction.serialize: clip contains a sparse or nonserializable array.");
620
+ }
621
+ for (let index = 0; index < value.length; index++) {
622
+ const item = descriptors[String(index)];
623
+ if (!item || !("value" in item) || !item.enumerable)
624
+ throw new Error("AnimationAction.serialize: clip contains a sparse or nonserializable array.");
625
+ }
626
+ }
627
+ function canonicalAnimationJSON(value, ancestors, depth) {
628
+ if (value === null)
629
+ return "null";
630
+ if (typeof value === "string" || typeof value === "boolean")
631
+ return JSON.stringify(value);
632
+ if (typeof value === "number" && Number.isFinite(value))
633
+ return Object.is(value, -0) ? "-0" : JSON.stringify(value);
634
+ if (typeof value !== "object" || depth > 128)
635
+ throw new Error("AnimationAction.serialize: clip contains a nonserializable value or callback.");
636
+ if (ancestors.has(value))
637
+ throw new Error("AnimationAction.serialize: clip contains a cycle.");
638
+ const array = Array.isArray(value);
639
+ const prototype = Object.getPrototypeOf(value);
640
+ if ((array && prototype !== Array.prototype) || (!array && prototype !== Object.prototype && prototype !== null) ||
641
+ Object.getOwnPropertySymbols(value).length)
642
+ throw new Error("AnimationAction.serialize: clip contains a nonserializable object.");
643
+ const descriptors = Object.getOwnPropertyDescriptors(value);
644
+ ancestors.add(value);
645
+ try {
646
+ if (array) {
647
+ assertAnimationArray(value);
648
+ return `[${value.map((entry) => canonicalAnimationJSON(entry, ancestors, depth + 1)).join(",")}]`;
649
+ }
650
+ const keys = Object.keys(descriptors).sort();
651
+ if (keys.some(key => !("value" in descriptors[key]) || !descriptors[key].enumerable)) {
652
+ throw new Error("AnimationAction.serialize: clip contains an accessor or nonserializable property.");
653
+ }
654
+ return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalAnimationJSON(descriptors[key].value, ancestors, depth + 1)}`).join(",")}}`;
655
+ }
656
+ finally {
657
+ ancestors.delete(value);
288
658
  }
289
659
  }
290
660
  //# sourceMappingURL=AnimationAction.js.map