@aura3d/animation 4.7.18 → 5.0.1
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/AnimationAction.d.ts +80 -1
- package/dist/AnimationAction.d.ts.map +1 -1
- package/dist/AnimationAction.js +450 -80
- package/dist/AnimationAction.js.map +1 -1
- package/dist/AnimationClip.d.ts.map +1 -1
- package/dist/AnimationClip.js.map +1 -1
- package/dist/AnimationClipEvents.d.ts.map +1 -1
- package/dist/AnimationClipEvents.js.map +1 -1
- package/dist/AnimationClipRegistry.d.ts.map +1 -1
- package/dist/AnimationClipRegistry.js.map +1 -1
- package/dist/AnimationController.d.ts.map +1 -1
- package/dist/AnimationController.js.map +1 -1
- package/dist/AnimationEvents.d.ts +23 -0
- package/dist/AnimationEvents.d.ts.map +1 -1
- package/dist/AnimationEvents.js +145 -36
- package/dist/AnimationEvents.js.map +1 -1
- package/dist/AnimationLayer.d.ts.map +1 -1
- package/dist/AnimationLayer.js.map +1 -1
- package/dist/AnimationMixer.d.ts +58 -3
- package/dist/AnimationMixer.d.ts.map +1 -1
- package/dist/AnimationMixer.js +366 -12
- package/dist/AnimationMixer.js.map +1 -1
- package/dist/AnimationObjectGroup.d.ts +36 -12
- package/dist/AnimationObjectGroup.d.ts.map +1 -1
- package/dist/AnimationObjectGroup.js +219 -208
- package/dist/AnimationObjectGroup.js.map +1 -1
- package/dist/AnimationStateGraph.d.ts.map +1 -1
- package/dist/AnimationStateGraph.js.map +1 -1
- package/dist/AnimationStateMachine.d.ts.map +1 -1
- package/dist/AnimationStateMachine.js.map +1 -1
- package/dist/AnimationTrack.d.ts +7 -1
- package/dist/AnimationTrack.d.ts.map +1 -1
- package/dist/AnimationTrack.js +87 -10
- package/dist/AnimationTrack.js.map +1 -1
- package/dist/AnimationUtils.d.ts +16 -14
- package/dist/AnimationUtils.d.ts.map +1 -1
- package/dist/AnimationUtils.js +148 -109
- package/dist/AnimationUtils.js.map +1 -1
- package/dist/BlendTree.d.ts.map +1 -1
- package/dist/BlendTree.js.map +1 -1
- package/dist/Bone.d.ts.map +1 -1
- package/dist/Bone.js.map +1 -1
- package/dist/ECSAnimationBridge.d.ts.map +1 -1
- package/dist/ECSAnimationBridge.js.map +1 -1
- package/dist/FighterAnimationAdapter.d.ts.map +1 -1
- package/dist/FighterAnimationAdapter.js.map +1 -1
- package/dist/FootIk.d.ts.map +1 -1
- package/dist/FootIk.js.map +1 -1
- package/dist/HumanoidBoneInference.d.ts.map +1 -1
- package/dist/HumanoidBoneInference.js.map +1 -1
- package/dist/HumanoidRetargeting.d.ts.map +1 -1
- package/dist/HumanoidRetargeting.js.map +1 -1
- package/dist/IK.d.ts.map +1 -1
- package/dist/IK.js.map +1 -1
- package/dist/Inertialization.d.ts.map +1 -1
- package/dist/Inertialization.js.map +1 -1
- package/dist/Keyframe.d.ts +15 -1
- package/dist/Keyframe.d.ts.map +1 -1
- package/dist/Keyframe.js +34 -1
- package/dist/Keyframe.js.map +1 -1
- package/dist/LocomotionController.d.ts.map +1 -1
- package/dist/LocomotionController.js.map +1 -1
- package/dist/LocomotionEvidence.d.ts.map +1 -1
- package/dist/LocomotionEvidence.js.map +1 -1
- package/dist/LocomotionKit.d.ts.map +1 -1
- package/dist/LocomotionKit.js.map +1 -1
- package/dist/MotionQuality.d.ts.map +1 -1
- package/dist/MotionQuality.js.map +1 -1
- package/dist/PropertyBinding.d.ts +42 -22
- package/dist/PropertyBinding.d.ts.map +1 -1
- package/dist/PropertyBinding.js +390 -293
- package/dist/PropertyBinding.js.map +1 -1
- package/dist/RigQuality.d.ts.map +1 -1
- package/dist/RigQuality.js.map +1 -1
- package/dist/RootMotion.d.ts.map +1 -1
- package/dist/RootMotion.js.map +1 -1
- package/dist/SceneAnimationBridge.d.ts +7 -0
- package/dist/SceneAnimationBridge.d.ts.map +1 -1
- package/dist/SceneAnimationBridge.js +48 -3
- package/dist/SceneAnimationBridge.js.map +1 -1
- package/dist/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton.js.map +1 -1
- package/dist/Skinning.d.ts.map +1 -1
- package/dist/Skinning.js.map +1 -1
- package/dist/SpringBones.d.ts.map +1 -1
- package/dist/SpringBones.js.map +1 -1
- package/dist/browser-index.d.ts +1 -1
- package/dist/browser-index.d.ts.map +1 -1
- package/dist/browser-index.js +1 -1
- package/dist/browser-index.js.map +1 -1
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/pose.d.ts.map +1 -1
- package/dist/contracts/pose.js.map +1 -1
- package/dist/experimental/CrowdAnimation.d.ts.map +1 -1
- package/dist/experimental/CrowdAnimation.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lanes/index.d.ts.map +1 -1
- package/dist/lanes/index.js.map +1 -1
- package/dist/lanes/prd01.d.ts.map +1 -1
- package/dist/lanes/prd01.js.map +1 -1
- package/dist/lanes/prd02.d.ts.map +1 -1
- package/dist/lanes/prd02.js.map +1 -1
- package/dist/lanes/prd03.d.ts.map +1 -1
- package/dist/lanes/prd03.js.map +1 -1
- package/dist/lanes/prd04.d.ts.map +1 -1
- package/dist/lanes/prd04.js.map +1 -1
- package/dist/lanes/prd05.d.ts.map +1 -1
- package/dist/lanes/prd05.js.map +1 -1
- package/dist/lanes/prd06.d.ts.map +1 -1
- package/dist/lanes/prd06.js.map +1 -1
- package/dist/lanes/prd07.d.ts.map +1 -1
- package/dist/lanes/prd07.js.map +1 -1
- package/dist/lanes/prd08.d.ts.map +1 -1
- package/dist/lanes/prd08.js.map +1 -1
- package/dist/lanes/prd09.d.ts.map +1 -1
- package/dist/lanes/prd09.js.map +1 -1
- package/dist/lanes/prd10.d.ts.map +1 -1
- package/dist/lanes/prd10.js.map +1 -1
- package/dist/lanes/prd11.d.ts.map +1 -1
- package/dist/lanes/prd11.js.map +1 -1
- package/dist/lanes/prd12.d.ts.map +1 -1
- package/dist/lanes/prd12.js.map +1 -1
- package/dist/lanes/prd13.d.ts.map +1 -1
- package/dist/lanes/prd13.js.map +1 -1
- package/dist/lanes/prd14.d.ts.map +1 -1
- package/dist/lanes/prd14.js.map +1 -1
- package/dist/lanes/prd15.d.ts.map +1 -1
- package/dist/lanes/prd15.js.map +1 -1
- package/dist/library/performanceStateGraph.d.ts.map +1 -1
- package/dist/library/performanceStateGraph.js.map +1 -1
- package/dist/library/standardHumanoidClips.d.ts.map +1 -1
- package/dist/library/standardHumanoidClips.js.map +1 -1
- package/dist/pose/BoneMask.d.ts.map +1 -1
- package/dist/pose/BoneMask.js.map +1 -1
- package/dist/pose/CcdIkConstraint.d.ts.map +1 -1
- package/dist/pose/CcdIkConstraint.js.map +1 -1
- package/dist/pose/CompiledClip.d.ts +15 -8
- package/dist/pose/CompiledClip.d.ts.map +1 -1
- package/dist/pose/CompiledClip.js +82 -50
- package/dist/pose/CompiledClip.js.map +1 -1
- package/dist/pose/LookAtConstraint.d.ts.map +1 -1
- package/dist/pose/LookAtConstraint.js.map +1 -1
- package/dist/pose/MotionMetrics.d.ts.map +1 -1
- package/dist/pose/MotionMetrics.js.map +1 -1
- package/dist/pose/PoseBuffer.d.ts.map +1 -1
- package/dist/pose/PoseBuffer.js.map +1 -1
- package/dist/pose/PoseInertializer.d.ts.map +1 -1
- package/dist/pose/PoseInertializer.js.map +1 -1
- package/dist/pose/PoseMixer.d.ts +196 -19
- package/dist/pose/PoseMixer.d.ts.map +1 -1
- package/dist/pose/PoseMixer.js +1209 -188
- package/dist/pose/PoseMixer.js.map +1 -1
- package/dist/pose/Retarget.d.ts +4 -3
- package/dist/pose/Retarget.d.ts.map +1 -1
- package/dist/pose/Retarget.js +32 -15
- package/dist/pose/Retarget.js.map +1 -1
- package/dist/pose/RetargetCache.d.ts +12 -6
- package/dist/pose/RetargetCache.d.ts.map +1 -1
- package/dist/pose/RetargetCache.js +253 -29
- package/dist/pose/RetargetCache.js.map +1 -1
- package/dist/pose/RetargetWorker.d.ts.map +1 -1
- package/dist/pose/RetargetWorker.js +5 -0
- package/dist/pose/RetargetWorker.js.map +1 -1
- package/dist/pose/SkeletonBinding.d.ts.map +1 -1
- package/dist/pose/SkeletonBinding.js.map +1 -1
- package/dist/pose/blendKernels.d.ts.map +1 -1
- package/dist/pose/blendKernels.js.map +1 -1
- package/dist/pose/makeClipAdditive.d.ts +7 -14
- package/dist/pose/makeClipAdditive.d.ts.map +1 -1
- package/dist/pose/makeClipAdditive.js +53 -85
- package/dist/pose/makeClipAdditive.js.map +1 -1
- package/dist/pose/poseMixerFlags.d.ts.map +1 -1
- package/dist/pose/poseMixerFlags.js.map +1 -1
- package/dist/pose/quatFlat.d.ts.map +1 -1
- package/dist/pose/quatFlat.js.map +1 -1
- package/dist/pose/retarget.worker.d.ts.map +1 -1
- package/dist/pose/retarget.worker.js.map +1 -1
- package/dist/threejs-compatibility/AnimationAction.d.ts.map +1 -1
- package/dist/threejs-compatibility/AnimationAction.js.map +1 -1
- package/dist/threejs-compatibility/AnimationClip.d.ts.map +1 -1
- package/dist/threejs-compatibility/AnimationClip.js.map +1 -1
- package/dist/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -1
- package/dist/threejs-compatibility/AnimationDiagnostics.js.map +1 -1
- package/dist/threejs-compatibility/AnimationMixer.d.ts.map +1 -1
- package/dist/threejs-compatibility/AnimationMixer.js.map +1 -1
- package/dist/threejs-compatibility/MorphTargetMixer.d.ts.map +1 -1
- package/dist/threejs-compatibility/MorphTargetMixer.js.map +1 -1
- package/dist/threejs-compatibility/Skeleton.d.ts.map +1 -1
- package/dist/threejs-compatibility/Skeleton.js.map +1 -1
- package/dist/threejs-compatibility/SkinnedMesh.d.ts.map +1 -1
- package/dist/threejs-compatibility/SkinnedMesh.js.map +1 -1
- package/dist/threejs-compatibility/index.d.ts.map +1 -1
- package/dist/threejs-compatibility/index.js.map +1 -1
- package/package.json +2 -2
package/dist/AnimationAction.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 (
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|