@aura3d/animation 4.7.13 → 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.
- 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/pose/PoseMixer.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnimationClip } from "../AnimationClip.js";
|
|
2
|
+
import { AnimationAction } from "../AnimationAction.js";
|
|
2
3
|
import { compileClip, createTrackCursors, sampleTrackInto } from "./CompiledClip.js";
|
|
3
4
|
import { lerpVec3Flat, multiplyQuatFlat, slerpQuatFlat } from "./quatFlat.js";
|
|
4
5
|
import { isClipAdditive } from "./makeClipAdditive.js";
|
|
6
|
+
import { PoseInertializer } from "./PoseInertializer.js";
|
|
5
7
|
export class PoseAction {
|
|
6
8
|
mixer;
|
|
7
9
|
clipName;
|
|
@@ -9,18 +11,11 @@ export class PoseAction {
|
|
|
9
11
|
bindings;
|
|
10
12
|
cursors;
|
|
11
13
|
additive;
|
|
14
|
+
playback;
|
|
12
15
|
weight = 1;
|
|
13
16
|
timeScale = 1;
|
|
14
|
-
time = 0;
|
|
15
|
-
playing = false;
|
|
16
|
-
paused = false;
|
|
17
17
|
enabled = true;
|
|
18
|
-
loopMode = "repeat";
|
|
19
|
-
repetitions = Infinity;
|
|
20
|
-
clampWhenFinished = false;
|
|
21
18
|
syncGroupName = null;
|
|
22
|
-
pingPongForward = true;
|
|
23
|
-
loopCount = -1;
|
|
24
19
|
fade = null;
|
|
25
20
|
warpState = null;
|
|
26
21
|
restoreTimeScale = null;
|
|
@@ -40,70 +35,103 @@ export class PoseAction {
|
|
|
40
35
|
this.bindings = mixer.resolveBindings(clip);
|
|
41
36
|
this.cursors = createTrackCursors(clip);
|
|
42
37
|
const source = mixer.sourceClip(clipName);
|
|
38
|
+
if (source === undefined)
|
|
39
|
+
throw new Error(`ANIMATION_CLIP_NOT_FOUND: clip "${clipName}" is not registered.`);
|
|
40
|
+
this.playback = new AnimationAction(source);
|
|
43
41
|
this.additive = options.additive ?? (source !== undefined && isClipAdditive(source));
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
46
|
-
|
|
47
|
-
this.loopMode = options.loopMode;
|
|
48
|
-
if (options.repetitions !== undefined)
|
|
49
|
-
this.repetitions = options.repetitions;
|
|
42
|
+
this.setEffectiveWeight(options.weight ?? 1);
|
|
43
|
+
this.setEffectiveTimeScale(options.timeScale ?? 1);
|
|
44
|
+
this.setLoop(options.loopMode ?? "repeat", options.repetitions ?? Infinity);
|
|
50
45
|
this.clampWhenFinished = options.clampWhenFinished ?? false;
|
|
51
46
|
this.syncGroupName = options.syncGroup ?? null;
|
|
52
47
|
}
|
|
48
|
+
get time() { return this.playback.time; }
|
|
49
|
+
set time(value) { this.playback.time = value; }
|
|
50
|
+
get playing() { return this.playback.playing; }
|
|
51
|
+
set playing(value) { this.playback.playing = value; }
|
|
52
|
+
get paused() { return this.playback.paused; }
|
|
53
|
+
set paused(value) { this.playback.paused = value; }
|
|
54
|
+
get loopMode() { return this.playback.loopMode; }
|
|
55
|
+
set loopMode(value) { this.playback.loopMode = value; }
|
|
56
|
+
get repetitions() { return this.playback.repetitions; }
|
|
57
|
+
set repetitions(value) { this.playback.repetitions = value; }
|
|
58
|
+
get clampWhenFinished() { return this.playback.clampWhenFinished; }
|
|
59
|
+
set clampWhenFinished(value) { this.playback.clampWhenFinished = value; }
|
|
60
|
+
get lastPlaybackDelta() { return this.playback.lastPlaybackDelta; }
|
|
61
|
+
/** @internal The core clip, rather than an independently compiled clip, owns playback. */
|
|
62
|
+
get hasPlaybackDuration() { return this.playback.clip.duration > 0; }
|
|
63
|
+
/** Maximum markers retained by a single update; excess crossings are reported by the mixer. */
|
|
64
|
+
get maxEventCatchUp() { return this.playback.maxEventCatchUp; }
|
|
65
|
+
set maxEventCatchUp(value) { this.playback.maxEventCatchUp = value; }
|
|
53
66
|
play() {
|
|
54
|
-
this.playing = true;
|
|
55
|
-
this.paused = false;
|
|
56
|
-
this.enabled = true;
|
|
57
67
|
this.mixer.activate(this);
|
|
68
|
+
this.playback.play();
|
|
69
|
+
this.enabled = true;
|
|
58
70
|
return this;
|
|
59
71
|
}
|
|
60
72
|
stop() {
|
|
61
|
-
this.
|
|
62
|
-
this.
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
73
|
+
this.playback.stop();
|
|
74
|
+
this.fade = null;
|
|
75
|
+
this.warpState = null;
|
|
76
|
+
this.restoreTimeScale = null;
|
|
77
|
+
this.effectiveWeight = 0;
|
|
78
|
+
this.effectiveTimeScale = 0;
|
|
66
79
|
this.mixer.deactivate(this);
|
|
67
80
|
return this;
|
|
68
81
|
}
|
|
69
82
|
pause() {
|
|
70
|
-
this.
|
|
83
|
+
this.playback.pause();
|
|
71
84
|
return this;
|
|
72
85
|
}
|
|
73
86
|
setEffectiveWeight(weight) {
|
|
87
|
+
if (!Number.isFinite(weight) || weight < 0)
|
|
88
|
+
throw new Error("PoseAction weight must be finite and non-negative.");
|
|
74
89
|
this.weight = weight;
|
|
75
90
|
return this;
|
|
76
91
|
}
|
|
77
92
|
setEffectiveTimeScale(timeScale) {
|
|
93
|
+
if (!Number.isFinite(timeScale))
|
|
94
|
+
throw new Error("PoseAction timeScale must be finite.");
|
|
78
95
|
this.timeScale = timeScale;
|
|
79
96
|
return this;
|
|
80
97
|
}
|
|
81
98
|
setLoop(mode, repetitions = Infinity) {
|
|
82
|
-
this.
|
|
83
|
-
this.repetitions = repetitions;
|
|
99
|
+
this.playback.setLoop(mode, repetitions);
|
|
84
100
|
return this;
|
|
85
101
|
}
|
|
86
102
|
/** three r185 syncWith: copy time + timeScale and stop any warping. */
|
|
87
103
|
syncWith(other) {
|
|
88
|
-
this.
|
|
104
|
+
this.playback.synchronizePlayback(other.playback);
|
|
89
105
|
this.timeScale = other.timeScale;
|
|
90
106
|
this.stopWarping();
|
|
91
107
|
return this;
|
|
92
108
|
}
|
|
109
|
+
/** Align the starting phase and reflection direction without changing this action's rate. */
|
|
110
|
+
synchronizePhaseWith(other) {
|
|
111
|
+
this.playback.synchronizePlayback(other.playback, true);
|
|
112
|
+
}
|
|
93
113
|
fadeIn(duration) {
|
|
94
|
-
return this.scheduleFade(duration, 0,
|
|
114
|
+
return this.scheduleFade(duration, 0, this.weight);
|
|
95
115
|
}
|
|
96
116
|
fadeOut(duration) {
|
|
97
|
-
return this.scheduleFade(duration,
|
|
117
|
+
return this.scheduleFade(duration, this.currentWeight(this.mixer.time), 0);
|
|
98
118
|
}
|
|
99
119
|
fadeTo(weight, duration) {
|
|
100
|
-
return this.scheduleFade(duration, this.
|
|
120
|
+
return this.scheduleFade(duration, this.currentWeight(this.mixer.time), weight);
|
|
101
121
|
}
|
|
102
|
-
/**
|
|
122
|
+
/** Linearly change the signed playback rate over unpaused mixer time. */
|
|
103
123
|
warp(startTimeScale, endTimeScale, duration) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
if (![startTimeScale, endTimeScale, duration].every(Number.isFinite) ||
|
|
125
|
+
!Number.isFinite(endTimeScale - startTimeScale) || duration < 0) {
|
|
126
|
+
throw new Error("PoseAction warp rates and duration must be finite and duration non-negative.");
|
|
127
|
+
}
|
|
128
|
+
if (duration === 0) {
|
|
129
|
+
this.warpState = null;
|
|
130
|
+
this.timeScale = this.restoreTimeScale ?? endTimeScale;
|
|
131
|
+
this.restoreTimeScale = null;
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
this.warpState = { duration, elapsed: 0, from: startTimeScale, to: endTimeScale };
|
|
107
135
|
return this;
|
|
108
136
|
}
|
|
109
137
|
stopWarping() {
|
|
@@ -130,8 +158,8 @@ export class PoseAction {
|
|
|
130
158
|
const endStartRatio = fadeOutDuration === 0 ? 1 : fadeInDuration / fadeOutDuration;
|
|
131
159
|
fadeOutAction.restoreTimeScale = fadeOutAction.timeScale;
|
|
132
160
|
this.restoreTimeScale = this.timeScale;
|
|
133
|
-
fadeOutAction.warp(
|
|
134
|
-
this.warp(endStartRatio,
|
|
161
|
+
fadeOutAction.warp(fadeOutAction.timeScale, fadeOutAction.timeScale * startEndRatio, duration);
|
|
162
|
+
this.warp(this.timeScale * endStartRatio, this.timeScale, duration);
|
|
135
163
|
}
|
|
136
164
|
return this;
|
|
137
165
|
}
|
|
@@ -141,30 +169,248 @@ export class PoseAction {
|
|
|
141
169
|
get normalizedPhase() {
|
|
142
170
|
return this.clip.duration > 0 ? this.time / this.clip.duration : 0;
|
|
143
171
|
}
|
|
172
|
+
/** A complete, JSON-safe snapshot; unlike a time/weight diagnostic it can be restored. */
|
|
173
|
+
snapshot() {
|
|
174
|
+
return this.serialize();
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Capture both the core playback state and the pose-specific envelopes,
|
|
178
|
+
* synchronization, bindings and layer state. Clip data and executable
|
|
179
|
+
* callbacks are not copied; their identities must be rebound on restore.
|
|
180
|
+
*/
|
|
181
|
+
serialize() {
|
|
182
|
+
this.mixer.assertSnapshotOperation("serialize");
|
|
183
|
+
assertPoseActionData(this);
|
|
184
|
+
assertPosePlaybackData(this.playback);
|
|
185
|
+
const currentWarp = this.warpState;
|
|
186
|
+
const startTime = currentWarp === null ? 0 : this.mixer.time - currentWarp.elapsed;
|
|
187
|
+
const warp = currentWarp === null ? null : {
|
|
188
|
+
startTime,
|
|
189
|
+
duration: currentWarp.duration,
|
|
190
|
+
from: currentWarp.from,
|
|
191
|
+
to: currentWarp.to,
|
|
192
|
+
...(this.mixer.time - startTime === currentWarp.elapsed ? {} : { elapsed: currentWarp.elapsed })
|
|
193
|
+
};
|
|
194
|
+
const snapshot = {
|
|
195
|
+
version: 1,
|
|
196
|
+
clipName: this.clipName,
|
|
197
|
+
compiledIdentity: poseCompiledIdentity(this.clip),
|
|
198
|
+
targetIdentity: JSON.stringify(capturePoseSkeleton(this.mixer.skeleton)),
|
|
199
|
+
mixerTime: this.mixer.time,
|
|
200
|
+
playback: this.playback.serialize(),
|
|
201
|
+
bindings: readPoseBindings(this.bindings, this.mixer.skeleton.boneCount, this.clip.tracks.length),
|
|
202
|
+
cursors: readPoseTypedNumbers(this.cursors, Uint32Array, this.clip.tracks.length, "action cursors"),
|
|
203
|
+
additive: this.additive,
|
|
204
|
+
weight: this.weight,
|
|
205
|
+
timeScale: this.timeScale,
|
|
206
|
+
enabled: this.enabled,
|
|
207
|
+
syncGroupName: this.syncGroupName,
|
|
208
|
+
fade: readPoseEnvelope(this.fade, "fade", true),
|
|
209
|
+
warp: readPoseWarp(warp),
|
|
210
|
+
restoreTimeScale: this.restoreTimeScale,
|
|
211
|
+
effectiveWeight: this.effectiveWeight,
|
|
212
|
+
effectiveTimeScale: this.effectiveTimeScale,
|
|
213
|
+
layerMask: this.layerMask === null ? null : readPoseTypedNumbers(this.layerMask, Float32Array, this.mixer.skeleton.boneCount, "action mask"),
|
|
214
|
+
layerBlendMode: this.layerBlendMode
|
|
215
|
+
};
|
|
216
|
+
this.prepareRestore(snapshot, this.mixer.time);
|
|
217
|
+
rejectPoseNegativeZero(snapshot);
|
|
218
|
+
return snapshot;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Restore in place without playing, dispatching events or registering the
|
|
222
|
+
* action. Its existing mixer membership is unchanged. Envelopes are rebased
|
|
223
|
+
* to the destination mixer's current clock.
|
|
224
|
+
*/
|
|
225
|
+
restore(snapshot) {
|
|
226
|
+
return this.restoreAtMixerTime(snapshot, this.mixer.time);
|
|
227
|
+
}
|
|
228
|
+
/** @internal Stage a restored action against the clock a mixer will commit. */
|
|
229
|
+
restoreAtMixerTime(snapshot, targetTime) {
|
|
230
|
+
this.mixer.assertSnapshotOperation("restore");
|
|
231
|
+
this.assertRestoreWritable();
|
|
232
|
+
const state = this.prepareRestore(snapshot, targetTime);
|
|
233
|
+
const layerMask = state.layerMask === null ? null : new Float32Array(state.layerMask);
|
|
234
|
+
this.playback.restore(state.playback);
|
|
235
|
+
this.weight = state.weight;
|
|
236
|
+
this.timeScale = state.timeScale;
|
|
237
|
+
this.enabled = state.enabled;
|
|
238
|
+
this.syncGroupName = state.syncGroupName;
|
|
239
|
+
this.fade = state.fade;
|
|
240
|
+
this.warpState = state.warp === null ? null : {
|
|
241
|
+
duration: state.warp.duration,
|
|
242
|
+
elapsed: state.warp.elapsed,
|
|
243
|
+
from: state.warp.from,
|
|
244
|
+
to: state.warp.to
|
|
245
|
+
};
|
|
246
|
+
this.restoreTimeScale = state.restoreTimeScale;
|
|
247
|
+
this.effectiveWeight = state.effectiveWeight;
|
|
248
|
+
this.effectiveTimeScale = state.effectiveTimeScale;
|
|
249
|
+
this.layerMask = layerMask;
|
|
250
|
+
this.layerBlendMode = state.layerBlendMode;
|
|
251
|
+
this.cursors.set(state.cursors);
|
|
252
|
+
return this;
|
|
253
|
+
}
|
|
254
|
+
prepareRestore(input, targetTime) {
|
|
255
|
+
const keys = [
|
|
256
|
+
"version", "clipName", "compiledIdentity", "targetIdentity", "mixerTime", "playback", "bindings", "cursors",
|
|
257
|
+
"additive", "weight", "timeScale", "enabled", "syncGroupName", "fade", "warp",
|
|
258
|
+
"restoreTimeScale", "effectiveWeight", "effectiveTimeScale", "layerMask", "layerBlendMode"
|
|
259
|
+
];
|
|
260
|
+
const record = poseRecord(input, "action snapshot", keys);
|
|
261
|
+
if (record.version !== 1)
|
|
262
|
+
throw new Error("PoseAction.restore: unsupported snapshot version.");
|
|
263
|
+
if (record.clipName !== this.clipName || typeof record.compiledIdentity !== "string" ||
|
|
264
|
+
record.compiledIdentity !== poseCompiledIdentity(this.clip)) {
|
|
265
|
+
throw new Error("PoseAction.restore: registered clip or compiled clip identity does not match.");
|
|
266
|
+
}
|
|
267
|
+
if (typeof record.targetIdentity !== "string" ||
|
|
268
|
+
record.targetIdentity !== JSON.stringify(capturePoseSkeleton(this.mixer.skeleton))) {
|
|
269
|
+
throw new Error("PoseAction.restore: rebound skeleton target does not match.");
|
|
270
|
+
}
|
|
271
|
+
const mixerTime = poseNumber(record.mixerTime, "action mixerTime");
|
|
272
|
+
const nextTime = poseNumber(targetTime, "destination mixerTime");
|
|
273
|
+
const shift = nextTime - mixerTime;
|
|
274
|
+
if (!Number.isFinite(shift))
|
|
275
|
+
throw new Error("PoseAction.restore: mixer clock offset is not finite.");
|
|
276
|
+
const playbackRecord = poseRecord(record.playback, "core playback", [
|
|
277
|
+
"clipName", "duration", "time", "weight", "timeScale", "playing", "paused", "loopMode", "state", "active"
|
|
278
|
+
], ["clipName", "duration", "time", "weight", "timeScale", "playing", "paused", "loopMode", "state"]);
|
|
279
|
+
if (playbackRecord.state === undefined)
|
|
280
|
+
throw new Error("PoseAction.restore: versioned core playback state is required.");
|
|
281
|
+
// Validation takes place on a temporary action before any live state changes.
|
|
282
|
+
const playback = new AnimationAction(this.playback.clip).restore(record.playback).serialize();
|
|
283
|
+
const bindings = readPoseBindings(record.bindings, this.mixer.skeleton.boneCount, this.clip.tracks.length);
|
|
284
|
+
const currentBindings = readPoseBindings(this.bindings, this.mixer.skeleton.boneCount, this.clip.tracks.length);
|
|
285
|
+
if (JSON.stringify(bindings) !== JSON.stringify(currentBindings)) {
|
|
286
|
+
throw new Error("PoseAction.restore: target bindings do not match the rebound skeleton.");
|
|
287
|
+
}
|
|
288
|
+
const cursors = readPoseNumberArray(record.cursors, this.clip.tracks.length, "action cursors")
|
|
289
|
+
.map(value => {
|
|
290
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff)
|
|
291
|
+
throw new Error("PoseAction.restore: invalid track cursor.");
|
|
292
|
+
return value;
|
|
293
|
+
});
|
|
294
|
+
const additive = poseBoolean(record.additive, "action additive");
|
|
295
|
+
if (additive !== this.additive)
|
|
296
|
+
throw new Error("PoseAction.restore: additive mode does not match.");
|
|
297
|
+
const syncGroupName = record.syncGroupName;
|
|
298
|
+
if (syncGroupName !== null && typeof syncGroupName !== "string")
|
|
299
|
+
throw new Error("PoseAction.restore: invalid synchronization group.");
|
|
300
|
+
const fade = rebasePoseEnvelope(readPoseEnvelope(record.fade, "fade", true), shift);
|
|
301
|
+
const capturedWarp = readPoseWarp(record.warp);
|
|
302
|
+
let warp = null;
|
|
303
|
+
if (capturedWarp !== null) {
|
|
304
|
+
const elapsed = capturedWarp.elapsed ?? Math.max(0, Math.min(capturedWarp.duration, mixerTime - capturedWarp.startTime));
|
|
305
|
+
if (!Number.isFinite(elapsed))
|
|
306
|
+
throw new Error("PoseAction.restore: invalid warp elapsed time.");
|
|
307
|
+
const rebased = rebasePoseEnvelope(capturedWarp, shift);
|
|
308
|
+
warp = { ...rebased, elapsed };
|
|
309
|
+
}
|
|
310
|
+
const restoreTimeScale = record.restoreTimeScale === null ? null : poseNumber(record.restoreTimeScale, "restoreTimeScale");
|
|
311
|
+
if (restoreTimeScale !== null && warp === null)
|
|
312
|
+
throw new Error("PoseAction.restore: restoreTimeScale requires a scheduled warp.");
|
|
313
|
+
const layerBlendMode = record.layerBlendMode;
|
|
314
|
+
if (layerBlendMode !== null && layerBlendMode !== "override" && layerBlendMode !== "additive") {
|
|
315
|
+
throw new Error("PoseAction.restore: invalid layer blend mode.");
|
|
316
|
+
}
|
|
317
|
+
const layerMask = record.layerMask === null ? null : readPoseFloatArray(record.layerMask, this.mixer.skeleton.boneCount, "action mask");
|
|
318
|
+
const result = {
|
|
319
|
+
version: 1, clipName: this.clipName, compiledIdentity: record.compiledIdentity, targetIdentity: record.targetIdentity,
|
|
320
|
+
mixerTime: nextTime, playback, bindings, cursors, additive,
|
|
321
|
+
weight: poseNumber(record.weight, "action weight", 0),
|
|
322
|
+
timeScale: poseNumber(record.timeScale, "action timeScale"),
|
|
323
|
+
enabled: poseBoolean(record.enabled, "action enabled"),
|
|
324
|
+
syncGroupName, fade, warp, restoreTimeScale,
|
|
325
|
+
effectiveWeight: poseNumber(record.effectiveWeight, "action effectiveWeight", 0),
|
|
326
|
+
effectiveTimeScale: poseNumber(record.effectiveTimeScale, "action effectiveTimeScale"),
|
|
327
|
+
layerMask, layerBlendMode
|
|
328
|
+
};
|
|
329
|
+
rejectPoseNegativeZero(result);
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
/** @internal Validate writable owned state before a mixer starts its commit. */
|
|
333
|
+
assertRestoreWritable() {
|
|
334
|
+
assertPoseActionData(this);
|
|
335
|
+
assertPosePlaybackData(this.playback);
|
|
336
|
+
assertPoseWritable(this, [
|
|
337
|
+
"weight", "timeScale", "enabled", "syncGroupName", "fade", "warpState", "restoreTimeScale",
|
|
338
|
+
"effectiveWeight", "effectiveTimeScale", "layerMask", "layerBlendMode"
|
|
339
|
+
], "action");
|
|
340
|
+
assertPoseWritable(this.playback, [
|
|
341
|
+
"time", "weight", "timeScale", "playing", "paused", "loopMode", "repetitions", "clampWhenFinished",
|
|
342
|
+
"maxEventCatchUp", "pingPongForward", "loopsCompleted", "fadeDuration", "fadeElapsed",
|
|
343
|
+
"fadeStartWeight", "fadeTargetWeight", "warpState", "lastPlaybackDelta", "signalLoopDelta",
|
|
344
|
+
"signalFinishedDirection", "signalEventOverflow"
|
|
345
|
+
], "core playback");
|
|
346
|
+
readPoseTypedNumbers(this.cursors, Uint32Array, this.clip.tracks.length, "action cursors");
|
|
347
|
+
}
|
|
348
|
+
/** @internal Stop an old handle without invoking overridable mixer callbacks. */
|
|
349
|
+
retireForRestore() {
|
|
350
|
+
AnimationAction.prototype.stop.call(this.playback);
|
|
351
|
+
this.fade = null;
|
|
352
|
+
this.warpState = null;
|
|
353
|
+
this.restoreTimeScale = null;
|
|
354
|
+
this.effectiveWeight = 0;
|
|
355
|
+
this.effectiveTimeScale = 0;
|
|
356
|
+
}
|
|
357
|
+
/** @internal Advance only the part of a frame during which playback was active. */
|
|
358
|
+
advanceWarp(elapsed) {
|
|
359
|
+
const warp = this.warpState;
|
|
360
|
+
if (warp === null)
|
|
361
|
+
return;
|
|
362
|
+
// Mirror the core's per-segment accumulation, including rounding at a
|
|
363
|
+
// rate reversal, rather than recomputing elapsed from the mixer clock.
|
|
364
|
+
for (const segment of AnimationAction.warpSegments(elapsed, warp)) {
|
|
365
|
+
if (segment.inWarp)
|
|
366
|
+
warp.elapsed = Math.min(warp.duration, segment.elapsed + segment.duration);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** @internal A warp ending at zero pauses even when a sync leader keeps moving. */
|
|
370
|
+
warpPauseOffset() {
|
|
371
|
+
const warp = this.warpState;
|
|
372
|
+
return warp !== null && (this.restoreTimeScale ?? warp.to) === 0
|
|
373
|
+
? Math.max(0, warp.duration - warp.elapsed)
|
|
374
|
+
: null;
|
|
375
|
+
}
|
|
376
|
+
/** @internal Preserve the whole frame's signed travel across warp segments. */
|
|
377
|
+
setPlaybackDelta(delta) {
|
|
378
|
+
this.playback.lastPlaybackDelta = delta;
|
|
379
|
+
}
|
|
144
380
|
scheduleFade(duration, weightNow, weightThen) {
|
|
381
|
+
if (!Number.isFinite(duration) || duration < 0 || !Number.isFinite(weightThen) || weightThen < 0) {
|
|
382
|
+
throw new Error("PoseAction fade weight and duration must be finite and non-negative.");
|
|
383
|
+
}
|
|
145
384
|
const now = this.mixer.time;
|
|
146
385
|
if (duration <= 0) {
|
|
147
386
|
this.fade = null;
|
|
148
387
|
this.weight = weightThen;
|
|
388
|
+
if (weightThen === 0)
|
|
389
|
+
this.enabled = false;
|
|
149
390
|
return this;
|
|
150
391
|
}
|
|
151
392
|
this.fade = { startTime: now, duration, from: weightNow, to: weightThen };
|
|
152
393
|
return this;
|
|
153
394
|
}
|
|
395
|
+
currentWeight(now) {
|
|
396
|
+
const fade = this.fade;
|
|
397
|
+
if (fade === null)
|
|
398
|
+
return this.weight;
|
|
399
|
+
const t = Math.min(Math.max((now - fade.startTime) / fade.duration, 0), 1);
|
|
400
|
+
return fade.from + (fade.to - fade.from) * t;
|
|
401
|
+
}
|
|
154
402
|
/** Effective weight at mixer.time, per r185 `_updateWeight`. */
|
|
155
403
|
updateWeight(now) {
|
|
156
404
|
let weight = 0;
|
|
157
405
|
if (this.enabled) {
|
|
158
|
-
weight = this.weight;
|
|
159
406
|
const fade = this.fade;
|
|
407
|
+
weight = this.currentWeight(now);
|
|
160
408
|
if (fade !== null) {
|
|
161
409
|
const end = fade.startTime + fade.duration;
|
|
162
|
-
|
|
163
|
-
const interpolant = fade.from + (fade.to - fade.from) * t;
|
|
164
|
-
weight *= interpolant;
|
|
165
|
-
if (now > end) {
|
|
410
|
+
if (now >= end) {
|
|
166
411
|
this.fade = null;
|
|
167
|
-
|
|
412
|
+
this.weight = fade.to;
|
|
413
|
+
if (weight === 0)
|
|
168
414
|
this.enabled = false;
|
|
169
415
|
}
|
|
170
416
|
}
|
|
@@ -172,93 +418,125 @@ export class PoseAction {
|
|
|
172
418
|
this.effectiveWeight = weight;
|
|
173
419
|
return weight;
|
|
174
420
|
}
|
|
175
|
-
/** Effective
|
|
176
|
-
updateTimeScale(
|
|
421
|
+
/** Effective instantaneous rate, including a scheduled warp. */
|
|
422
|
+
updateTimeScale(_now, finishPausedWarp = false) {
|
|
177
423
|
let timeScale = 0;
|
|
178
|
-
if (!this.paused) {
|
|
179
|
-
|
|
424
|
+
if (!this.paused || finishPausedWarp) {
|
|
425
|
+
let rate = this.timeScale;
|
|
180
426
|
const warp = this.warpState;
|
|
181
427
|
if (warp !== null) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
else {
|
|
190
|
-
if (this.restoreTimeScale !== null)
|
|
191
|
-
timeScale = this.restoreTimeScale;
|
|
192
|
-
this.timeScale = timeScale;
|
|
428
|
+
const t = Math.min(warp.elapsed / warp.duration, 1);
|
|
429
|
+
rate = warp.from + (warp.to - warp.from) * t;
|
|
430
|
+
if (warp.elapsed >= warp.duration) {
|
|
431
|
+
rate = this.restoreTimeScale ?? warp.to;
|
|
432
|
+
this.timeScale = rate;
|
|
433
|
+
if (rate === 0 && this.playing && !this.paused) {
|
|
434
|
+
this.playback.pause();
|
|
193
435
|
}
|
|
194
436
|
this.warpState = null;
|
|
437
|
+
this.restoreTimeScale = null;
|
|
195
438
|
}
|
|
196
439
|
}
|
|
440
|
+
if (!this.paused)
|
|
441
|
+
timeScale = rate;
|
|
197
442
|
}
|
|
198
443
|
this.effectiveTimeScale = timeScale;
|
|
199
444
|
return timeScale;
|
|
200
445
|
}
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (finished) {
|
|
220
|
-
if (this.clampWhenFinished)
|
|
221
|
-
this.paused = true;
|
|
222
|
-
else
|
|
223
|
-
this.enabled = false;
|
|
224
|
-
this.time = time;
|
|
225
|
-
return { wrapped, finished, previous };
|
|
226
|
-
}
|
|
446
|
+
/** Integrate each monotonic part of a warp, retaining any direction change. */
|
|
447
|
+
integratedTimeScaleSegments(from, to) {
|
|
448
|
+
if (this.paused || to <= from)
|
|
449
|
+
return [];
|
|
450
|
+
if (!Number.isFinite(this.timeScale))
|
|
451
|
+
throw new Error("PoseAction timeScale must be finite.");
|
|
452
|
+
const warp = this.warpState;
|
|
453
|
+
if (warp === null)
|
|
454
|
+
return this.integratedTimeScaleSpans(to - from).map(segment => segment.distance);
|
|
455
|
+
// Keep this interval-query API relative to the current mixer clock. Actual
|
|
456
|
+
// playback uses the local elapsed value directly, without subtracting clocks.
|
|
457
|
+
const start = this.mixer.time - warp.elapsed;
|
|
458
|
+
const segments = [];
|
|
459
|
+
const beforeEnd = Math.min(to, start);
|
|
460
|
+
if (beforeEnd > from) {
|
|
461
|
+
const distance = (beforeEnd - from) * this.timeScale;
|
|
462
|
+
if (distance !== 0)
|
|
463
|
+
segments.push(distance);
|
|
227
464
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const loopDelta = Math.floor(time / duration);
|
|
237
|
-
time -= duration * loopDelta;
|
|
238
|
-
this.loopCount += Math.abs(loopDelta);
|
|
239
|
-
wrapped = true;
|
|
240
|
-
const pending = this.repetitions - this.loopCount;
|
|
241
|
-
if (pending <= 0) {
|
|
242
|
-
if (this.clampWhenFinished)
|
|
243
|
-
this.paused = true;
|
|
244
|
-
else
|
|
245
|
-
this.enabled = false;
|
|
246
|
-
this.time = deltaTime > 0 ? duration : 0;
|
|
247
|
-
return { wrapped, finished: true, previous };
|
|
248
|
-
}
|
|
465
|
+
const begin = Math.max(from, start);
|
|
466
|
+
if (to > begin) {
|
|
467
|
+
const elapsed = Math.max(0, Math.min(warp.duration, begin - start));
|
|
468
|
+
for (const segment of AnimationAction.warpSegments(to - begin, { ...warp, elapsed })) {
|
|
469
|
+
const rate = this.restoreTimeScale ?? segment.to;
|
|
470
|
+
const distance = segment.inWarp ? segment.distance : segment.duration * (rate / 2 + rate / 2);
|
|
471
|
+
if (distance !== 0)
|
|
472
|
+
segments.push(distance);
|
|
249
473
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
474
|
+
}
|
|
475
|
+
return segments;
|
|
476
|
+
}
|
|
477
|
+
/** @internal Use the same signed, action-local warp integration as core playback. */
|
|
478
|
+
integratedTimeScaleSpans(delta) {
|
|
479
|
+
if (this.paused || delta === 0)
|
|
480
|
+
return [];
|
|
481
|
+
if (!Number.isFinite(delta))
|
|
482
|
+
throw new Error("PoseAction update delta must be finite.");
|
|
483
|
+
if (!Number.isFinite(this.timeScale))
|
|
484
|
+
throw new Error("PoseAction timeScale must be finite.");
|
|
485
|
+
const direction = delta < 0 ? -1 : 1;
|
|
486
|
+
const warp = this.warpState;
|
|
487
|
+
if (warp === null) {
|
|
488
|
+
const distance = delta * this.timeScale;
|
|
489
|
+
if (!Number.isFinite(distance))
|
|
490
|
+
throw new Error("PoseAction scaled playback span must be finite.");
|
|
491
|
+
const rate = direction * this.timeScale;
|
|
492
|
+
return [{ startOffset: 0, duration: Math.abs(delta), from: rate, to: rate, distance }];
|
|
493
|
+
}
|
|
494
|
+
let offset = 0;
|
|
495
|
+
return AnimationAction.warpSegments(delta, warp).map(segment => {
|
|
496
|
+
const restoredRate = !segment.inWarp ? this.restoreTimeScale : null;
|
|
497
|
+
const from = direction * (restoredRate ?? segment.from);
|
|
498
|
+
const to = direction * (restoredRate ?? segment.to);
|
|
499
|
+
const distance = restoredRate === null
|
|
500
|
+
? segment.distance
|
|
501
|
+
: direction * segment.duration * (restoredRate / 2 + restoredRate / 2);
|
|
502
|
+
if (!Number.isFinite(distance)) {
|
|
503
|
+
throw new Error("PoseAction warped playback span must be finite.");
|
|
257
504
|
}
|
|
505
|
+
const result = { startOffset: offset, duration: segment.duration, from, to, distance };
|
|
506
|
+
offset += segment.duration;
|
|
507
|
+
return result;
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/** Advance through the core action so signed playheads and marker boundaries agree. */
|
|
511
|
+
updateTime(deltaTime) {
|
|
512
|
+
const previous = this.time;
|
|
513
|
+
// The caller has already applied both mixer and action rates.
|
|
514
|
+
const events = this.playback.update(deltaTime);
|
|
515
|
+
const signals = this.playback.consumePlaybackSignals();
|
|
516
|
+
if (signals.finishedDirection !== 0 && !this.clampWhenFinished)
|
|
517
|
+
this.enabled = false;
|
|
518
|
+
return { wrapped: signals.loopDelta !== 0, finished: signals.finishedDirection !== 0, previous, events, eventOverflow: signals.eventOverflow };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/** Stop synchronized travel at the follower's own warp-to-zero boundary. */
|
|
522
|
+
function limitTimeScaleSegments(segments, limit) {
|
|
523
|
+
const limited = [];
|
|
524
|
+
for (const segment of segments) {
|
|
525
|
+
if (segment.startOffset >= limit)
|
|
526
|
+
break;
|
|
527
|
+
if (segment.startOffset + segment.duration <= limit) {
|
|
528
|
+
limited.push(segment);
|
|
529
|
+
continue;
|
|
258
530
|
}
|
|
259
|
-
|
|
260
|
-
|
|
531
|
+
const duration = limit - segment.startOffset;
|
|
532
|
+
const to = segment.from + (segment.to - segment.from) * (duration / segment.duration);
|
|
533
|
+
const distance = duration * (segment.from / 2 + to / 2);
|
|
534
|
+
if (!Number.isFinite(distance))
|
|
535
|
+
throw new Error("PoseAction limited playback span must be finite.");
|
|
536
|
+
limited.push({ ...segment, duration, to, distance });
|
|
537
|
+
break;
|
|
261
538
|
}
|
|
539
|
+
return limited;
|
|
262
540
|
}
|
|
263
541
|
/** Drop layer actions that stopped or finished fading out, so replaced clips do not pile up. */
|
|
264
542
|
function pruneFinishedLayerActions(layer) {
|
|
@@ -275,12 +553,16 @@ export class PoseMixer {
|
|
|
275
553
|
skeleton;
|
|
276
554
|
inertializer;
|
|
277
555
|
time = 0;
|
|
556
|
+
/** Markers omitted by the catch-up bounds in the most recent update. */
|
|
557
|
+
lastEventOverflow = 0;
|
|
278
558
|
timeScale = 1;
|
|
279
559
|
clips = new Map();
|
|
280
560
|
actions = [];
|
|
281
561
|
layers = [];
|
|
282
562
|
layerByName = new Map();
|
|
283
563
|
eventListeners = new Set();
|
|
564
|
+
disposed = false;
|
|
565
|
+
updateDepth = 0;
|
|
284
566
|
latestPose = null;
|
|
285
567
|
previousPose = null;
|
|
286
568
|
inertializerElapsed = Infinity;
|
|
@@ -318,19 +600,30 @@ export class PoseMixer {
|
|
|
318
600
|
this.addTouchedCount = 0;
|
|
319
601
|
}
|
|
320
602
|
setEffectiveTimeScale(timeScale) {
|
|
603
|
+
this.assertAlive();
|
|
604
|
+
if (!Number.isFinite(timeScale))
|
|
605
|
+
throw new Error("PoseMixer timeScale must be finite.");
|
|
321
606
|
this.timeScale = timeScale;
|
|
322
607
|
return this;
|
|
323
608
|
}
|
|
324
609
|
get effectiveTimeScale() {
|
|
325
610
|
return this.timeScale;
|
|
326
611
|
}
|
|
612
|
+
/** @internal Reject snapshots taken while an update is traversing callbacks. */
|
|
613
|
+
assertSnapshotOperation(operation) {
|
|
614
|
+
this.assertAlive();
|
|
615
|
+
if (this.updateDepth !== 0)
|
|
616
|
+
throw new Error(`PoseMixer.${operation}: cannot ${operation} during an update.`);
|
|
617
|
+
}
|
|
327
618
|
/** Register a clip under a name (compiles it for flat sampling). */
|
|
328
619
|
addClip(name, clip) {
|
|
620
|
+
this.assertAlive();
|
|
329
621
|
this.clips.set(name, { source: clip, compiled: compileClip(clip) });
|
|
330
622
|
return this;
|
|
331
623
|
}
|
|
332
624
|
/** Register a pre-compiled clip (returns undefined if unknown). */
|
|
333
625
|
addCompiledClip(name, compiled, source) {
|
|
626
|
+
this.assertAlive();
|
|
334
627
|
this.clips.set(name, { source, compiled });
|
|
335
628
|
return this;
|
|
336
629
|
}
|
|
@@ -338,6 +631,7 @@ export class PoseMixer {
|
|
|
338
631
|
return this.clips.get(name)?.source;
|
|
339
632
|
}
|
|
340
633
|
clipAction(name, options = {}) {
|
|
634
|
+
this.assertAlive();
|
|
341
635
|
const entry = this.clips.get(name);
|
|
342
636
|
if (!entry) {
|
|
343
637
|
throw new Error(`ANIMATION_CLIP_NOT_FOUND: clip "${name}" is not registered. Available: ${[...this.clips.keys()].join(", ")}`);
|
|
@@ -357,6 +651,7 @@ export class PoseMixer {
|
|
|
357
651
|
* to compute covered-bone sets for `restPoseReset: false`).
|
|
358
652
|
*/
|
|
359
653
|
activeActions() {
|
|
654
|
+
this.assertAlive();
|
|
360
655
|
const all = [];
|
|
361
656
|
for (const action of this.actions) {
|
|
362
657
|
if (action.playing && action.enabled)
|
|
@@ -379,6 +674,7 @@ export class PoseMixer {
|
|
|
379
674
|
* name — `""` for the base layer — for `animationState()` snapshots (C-37).
|
|
380
675
|
*/
|
|
381
676
|
activeActionEntries() {
|
|
677
|
+
this.assertAlive();
|
|
382
678
|
const all = [];
|
|
383
679
|
for (const action of this.actions) {
|
|
384
680
|
if (action.playing && action.enabled)
|
|
@@ -392,6 +688,266 @@ export class PoseMixer {
|
|
|
392
688
|
}
|
|
393
689
|
return all;
|
|
394
690
|
}
|
|
691
|
+
/** Strict, restorable snapshot; the external skeleton must be acknowledged. */
|
|
692
|
+
snapshot(options = {}) {
|
|
693
|
+
return this.serialize(options);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Capture mixer-owned state only. The caller must rebind an equivalent
|
|
697
|
+
* skeleton and matching registered clips; event listeners must be removed and
|
|
698
|
+
* reattached explicitly. Active inertializer transitions cannot yet be
|
|
699
|
+
* serialized because their captured velocities are externally owned.
|
|
700
|
+
*/
|
|
701
|
+
serialize(options = {}) {
|
|
702
|
+
this.assertSnapshotOperation("serialize");
|
|
703
|
+
this.assertGraphData();
|
|
704
|
+
const captureOptions = poseRecord(options, "serialization options", ["externalTarget"], []);
|
|
705
|
+
if (captureOptions.externalTarget !== "rebind") {
|
|
706
|
+
throw new Error("PoseMixer.serialize: the external skeleton target requires externalTarget: 'rebind'.");
|
|
707
|
+
}
|
|
708
|
+
if (this.eventListeners.size !== 0) {
|
|
709
|
+
throw new Error("PoseMixer.serialize: event callbacks/listeners cannot be serialized; unsubscribe and reattach them explicitly.");
|
|
710
|
+
}
|
|
711
|
+
if (this.inertializer !== null)
|
|
712
|
+
assertPoseInertializerData(this.inertializer, this.skeleton.boneCount);
|
|
713
|
+
if (this.inertializer !== null && (this.inertializer.active || this.inertializerElapsed !== Infinity)) {
|
|
714
|
+
throw new Error("PoseMixer.serialize: active inertializer transitions cannot be serialized.");
|
|
715
|
+
}
|
|
716
|
+
const masks = [];
|
|
717
|
+
const indices = new Map();
|
|
718
|
+
const seenActions = new Set();
|
|
719
|
+
const maskIndex = (mask) => {
|
|
720
|
+
if (mask === null)
|
|
721
|
+
return null;
|
|
722
|
+
const existing = indices.get(mask);
|
|
723
|
+
if (existing !== undefined)
|
|
724
|
+
return existing;
|
|
725
|
+
const index = masks.length;
|
|
726
|
+
masks.push(readPoseTypedNumbers(mask, Float32Array, this.skeleton.boneCount, "layer mask"));
|
|
727
|
+
indices.set(mask, index);
|
|
728
|
+
return index;
|
|
729
|
+
};
|
|
730
|
+
const captureAction = (action) => {
|
|
731
|
+
if (seenActions.has(action))
|
|
732
|
+
throw new Error("PoseMixer.serialize: an action belongs to more than one mixer layer.");
|
|
733
|
+
if (!(action instanceof PoseAction))
|
|
734
|
+
throw new Error("PoseMixer.serialize: invalid action.");
|
|
735
|
+
assertPoseActionData(action);
|
|
736
|
+
if (action.mixer !== this)
|
|
737
|
+
throw new Error("PoseMixer.serialize: action belongs to another mixer.");
|
|
738
|
+
seenActions.add(action);
|
|
739
|
+
return { action: action.serialize(), mask: maskIndex(action.layerMask) };
|
|
740
|
+
};
|
|
741
|
+
const snapshot = {
|
|
742
|
+
version: 1,
|
|
743
|
+
time: this.time,
|
|
744
|
+
timeScale: this.timeScale,
|
|
745
|
+
lastEventOverflow: this.lastEventOverflow === Infinity ? null : this.lastEventOverflow,
|
|
746
|
+
lastDt: this.lastDt,
|
|
747
|
+
target: capturePoseSkeleton(this.skeleton),
|
|
748
|
+
clips: this.captureClipCatalog(),
|
|
749
|
+
masks,
|
|
750
|
+
actions: this.actions.map(captureAction),
|
|
751
|
+
layers: this.layers.map(layer => ({
|
|
752
|
+
name: layer.name,
|
|
753
|
+
blendMode: layer.blendMode,
|
|
754
|
+
weight: layer.weight,
|
|
755
|
+
mask: maskIndex(layer.mask),
|
|
756
|
+
actions: layer.actions.map(captureAction)
|
|
757
|
+
})),
|
|
758
|
+
inertializer: this.inertializer === null ? null : {
|
|
759
|
+
latestPose: this.latestPose === null ? null : capturePoseBuffer(this.latestPose, this.skeleton.boneCount, "latest pose"),
|
|
760
|
+
previousPose: this.previousPose === null ? null : capturePoseBuffer(this.previousPose, this.skeleton.boneCount, "previous pose"),
|
|
761
|
+
halfLife: this.inertializerHalfLife
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
this.prepareRestore(snapshot);
|
|
765
|
+
rejectPoseNegativeZero(snapshot);
|
|
766
|
+
return snapshot;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Atomically rebind to this mixer's skeleton and registered clips. No pose or
|
|
770
|
+
* callbacks are applied. Existing actions are stopped and replaced; callers
|
|
771
|
+
* should reacquire action handles via activeActions()/activeActionEntries().
|
|
772
|
+
* Existing destination listeners remain attached.
|
|
773
|
+
*/
|
|
774
|
+
restore(snapshot) {
|
|
775
|
+
this.assertSnapshotOperation("restore");
|
|
776
|
+
this.assertGraphData();
|
|
777
|
+
const plan = this.prepareRestore(snapshot);
|
|
778
|
+
assertPoseWritable(this, [
|
|
779
|
+
"time", "timeScale", "lastEventOverflow", "lastDt", "latestPose", "previousPose",
|
|
780
|
+
"inertializerHalfLife", "inertializerElapsed"
|
|
781
|
+
], "mixer");
|
|
782
|
+
assertPoseMutableArray(this.actions, "existing base actions");
|
|
783
|
+
assertPoseMutableArray(this.layers, "existing layers");
|
|
784
|
+
if (Object.getPrototypeOf(this.layerByName) !== Map.prototype || Reflect.ownKeys(this.layerByName).length !== 0) {
|
|
785
|
+
throw new Error("PoseMixer.restore: existing layer index contains unsupported callbacks or properties.");
|
|
786
|
+
}
|
|
787
|
+
const previous = new Set(this.actions);
|
|
788
|
+
for (const layer of this.layers) {
|
|
789
|
+
const record = poseRecord(layer, "existing layer", ["name", "blendMode", "mask", "weight", "actions"]);
|
|
790
|
+
for (const action of poseArray(record.actions, "existing layer actions"))
|
|
791
|
+
previous.add(action);
|
|
792
|
+
}
|
|
793
|
+
for (const action of previous) {
|
|
794
|
+
if (!(action instanceof PoseAction)) {
|
|
795
|
+
throw new Error("PoseMixer.restore: existing action belongs to another mixer or is invalid.");
|
|
796
|
+
}
|
|
797
|
+
assertPoseActionData(action);
|
|
798
|
+
if (action.mixer !== this)
|
|
799
|
+
throw new Error("PoseMixer.restore: existing action belongs to another mixer.");
|
|
800
|
+
action.assertRestoreWritable();
|
|
801
|
+
}
|
|
802
|
+
for (const action of previous)
|
|
803
|
+
PoseAction.prototype.retireForRestore.call(action);
|
|
804
|
+
this.actions.length = 0;
|
|
805
|
+
for (const action of plan.actions)
|
|
806
|
+
this.actions.push(action);
|
|
807
|
+
this.layers.length = 0;
|
|
808
|
+
for (const layer of plan.layers)
|
|
809
|
+
this.layers.push(layer);
|
|
810
|
+
this.layerByName.clear();
|
|
811
|
+
for (const layer of plan.layers)
|
|
812
|
+
this.layerByName.set(layer.name, layer);
|
|
813
|
+
this.time = plan.time;
|
|
814
|
+
this.timeScale = plan.timeScale;
|
|
815
|
+
this.lastEventOverflow = plan.lastEventOverflow;
|
|
816
|
+
this.lastDt = plan.lastDt;
|
|
817
|
+
this.latestPose = plan.latestPose;
|
|
818
|
+
this.previousPose = plan.previousPose;
|
|
819
|
+
this.inertializerHalfLife = plan.halfLife;
|
|
820
|
+
this.inertializerElapsed = Infinity;
|
|
821
|
+
return this;
|
|
822
|
+
}
|
|
823
|
+
prepareRestore(input) {
|
|
824
|
+
const record = poseRecord(input, "mixer snapshot", [
|
|
825
|
+
"version", "time", "timeScale", "lastEventOverflow", "lastDt", "target", "clips", "masks", "actions", "layers", "inertializer"
|
|
826
|
+
]);
|
|
827
|
+
if (record.version !== 1)
|
|
828
|
+
throw new Error("PoseMixer.restore: unsupported snapshot version.");
|
|
829
|
+
const time = poseNumber(record.time, "mixer time");
|
|
830
|
+
const timeScale = poseNumber(record.timeScale, "mixer timeScale");
|
|
831
|
+
const lastDt = poseNumber(record.lastDt, "lastDt", 0);
|
|
832
|
+
const lastEventOverflow = record.lastEventOverflow === null
|
|
833
|
+
? Infinity : poseNumber(record.lastEventOverflow, "lastEventOverflow", 0);
|
|
834
|
+
if (lastEventOverflow !== Infinity && !Number.isInteger(lastEventOverflow)) {
|
|
835
|
+
throw new Error("PoseMixer.restore: invalid event overflow.");
|
|
836
|
+
}
|
|
837
|
+
const target = readPoseSkeleton(record.target, this.skeleton.boneCount);
|
|
838
|
+
if (JSON.stringify(target) !== JSON.stringify(capturePoseSkeleton(this.skeleton))) {
|
|
839
|
+
throw new Error("PoseMixer.restore: rebound skeleton target does not match.");
|
|
840
|
+
}
|
|
841
|
+
const clips = readPoseClipCatalog(record.clips);
|
|
842
|
+
if (JSON.stringify(clips) !== JSON.stringify(this.captureClipCatalog())) {
|
|
843
|
+
throw new Error("PoseMixer.restore: registered source/compiled clip catalog or identity does not match.");
|
|
844
|
+
}
|
|
845
|
+
const masks = poseArray(record.masks, "masks").map((mask, index) => new Float32Array(readPoseFloatArray(mask, this.skeleton.boneCount, `mask ${index}`)));
|
|
846
|
+
const usedMasks = new Set();
|
|
847
|
+
const maskAt = (value) => {
|
|
848
|
+
if (value === null)
|
|
849
|
+
return null;
|
|
850
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value >= masks.length) {
|
|
851
|
+
throw new Error("PoseMixer.restore: invalid mask reference.");
|
|
852
|
+
}
|
|
853
|
+
usedMasks.add(value);
|
|
854
|
+
return masks[value];
|
|
855
|
+
};
|
|
856
|
+
const restoreAction = (inputAction) => {
|
|
857
|
+
const wrapper = poseRecord(inputAction, "mixer action", ["action", "mask"]);
|
|
858
|
+
const actionRecord = poseRecord(wrapper.action, "action snapshot", [
|
|
859
|
+
"version", "clipName", "compiledIdentity", "targetIdentity", "mixerTime", "playback", "bindings", "cursors",
|
|
860
|
+
"additive", "weight", "timeScale", "enabled", "syncGroupName", "fade", "warp",
|
|
861
|
+
"restoreTimeScale", "effectiveWeight", "effectiveTimeScale", "layerMask", "layerBlendMode"
|
|
862
|
+
]);
|
|
863
|
+
if (typeof actionRecord.clipName !== "string" || typeof actionRecord.additive !== "boolean") {
|
|
864
|
+
throw new Error("PoseMixer.restore: invalid action clip or additive mode.");
|
|
865
|
+
}
|
|
866
|
+
if (actionRecord.mixerTime !== time)
|
|
867
|
+
throw new Error("PoseMixer.restore: action and mixer clocks do not match.");
|
|
868
|
+
const action = this.clipAction(actionRecord.clipName, { additive: actionRecord.additive });
|
|
869
|
+
action.restoreAtMixerTime(wrapper.action, time);
|
|
870
|
+
const mask = maskAt(wrapper.mask);
|
|
871
|
+
if ((mask === null) !== (action.layerMask === null) ||
|
|
872
|
+
(mask !== null && JSON.stringify([...mask]) !== JSON.stringify([...action.layerMask]))) {
|
|
873
|
+
throw new Error("PoseMixer.restore: action mask does not match its mask reference.");
|
|
874
|
+
}
|
|
875
|
+
action.layerMask = mask;
|
|
876
|
+
return action;
|
|
877
|
+
};
|
|
878
|
+
const actions = poseArray(record.actions, "base actions").map(restoreAction);
|
|
879
|
+
const names = new Set();
|
|
880
|
+
const layers = poseArray(record.layers, "layers").map(inputLayer => {
|
|
881
|
+
const layer = poseRecord(inputLayer, "layer", ["name", "blendMode", "weight", "mask", "actions"]);
|
|
882
|
+
if (typeof layer.name !== "string" || names.has(layer.name))
|
|
883
|
+
throw new Error("PoseMixer.restore: invalid or duplicate layer name.");
|
|
884
|
+
names.add(layer.name);
|
|
885
|
+
if (layer.blendMode !== "override" && layer.blendMode !== "additive")
|
|
886
|
+
throw new Error("PoseMixer.restore: invalid layer blend mode.");
|
|
887
|
+
return {
|
|
888
|
+
name: layer.name,
|
|
889
|
+
blendMode: layer.blendMode,
|
|
890
|
+
weight: poseNumber(layer.weight, "layer weight", 0),
|
|
891
|
+
mask: maskAt(layer.mask),
|
|
892
|
+
actions: poseArray(layer.actions, "layer actions").map(restoreAction)
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
if (usedMasks.size !== masks.length)
|
|
896
|
+
throw new Error("PoseMixer.restore: unreferenced mask data.");
|
|
897
|
+
let latestPose = null;
|
|
898
|
+
let previousPose = null;
|
|
899
|
+
let halfLife = 0.15;
|
|
900
|
+
if ((record.inertializer === null) !== (this.inertializer === null)) {
|
|
901
|
+
throw new Error("PoseMixer.restore: rebound inertializer configuration does not match.");
|
|
902
|
+
}
|
|
903
|
+
if (record.inertializer !== null) {
|
|
904
|
+
assertPoseInertializerData(this.inertializer, this.skeleton.boneCount);
|
|
905
|
+
if (this.inertializer.active || this.inertializerElapsed !== Infinity) {
|
|
906
|
+
throw new Error("PoseMixer.restore: active inertializer transitions cannot be replaced.");
|
|
907
|
+
}
|
|
908
|
+
const history = poseRecord(record.inertializer, "inertializer history", ["latestPose", "previousPose", "halfLife"]);
|
|
909
|
+
latestPose = history.latestPose === null ? null : restorePoseBuffer(history.latestPose, this.skeleton.boneCount, "latest pose");
|
|
910
|
+
previousPose = history.previousPose === null ? null : restorePoseBuffer(history.previousPose, this.skeleton.boneCount, "previous pose");
|
|
911
|
+
halfLife = poseNumber(history.halfLife, "inertializer halfLife");
|
|
912
|
+
}
|
|
913
|
+
rejectPoseNegativeZero(input);
|
|
914
|
+
return { time, timeScale, lastEventOverflow, lastDt, actions, layers, latestPose, previousPose, halfLife };
|
|
915
|
+
}
|
|
916
|
+
assertGraphData() {
|
|
917
|
+
poseArray(this.actions, "existing base actions");
|
|
918
|
+
const layers = poseArray(this.layers, "existing layers");
|
|
919
|
+
if (Object.getPrototypeOf(this.layerByName) !== Map.prototype || Reflect.ownKeys(this.layerByName).length !== 0 ||
|
|
920
|
+
this.layerByName.size !== layers.length) {
|
|
921
|
+
throw new Error("PoseMixer: layer index contains unsupported or inconsistent state.");
|
|
922
|
+
}
|
|
923
|
+
for (const input of layers) {
|
|
924
|
+
const layer = poseRecord(input, "existing layer", ["name", "blendMode", "mask", "weight", "actions"]);
|
|
925
|
+
if (typeof layer.name !== "string" || this.layerByName.get(layer.name) !== input) {
|
|
926
|
+
throw new Error("PoseMixer: layer index does not match layer membership.");
|
|
927
|
+
}
|
|
928
|
+
poseArray(layer.actions, "existing layer actions");
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
captureClipCatalog() {
|
|
932
|
+
if (Object.getPrototypeOf(this.clips) !== Map.prototype || Reflect.ownKeys(this.clips).length !== 0) {
|
|
933
|
+
throw new Error("PoseMixer.serialize: clip registry contains unsupported callbacks or properties.");
|
|
934
|
+
}
|
|
935
|
+
const clips = [...this.clips].map(([name, input]) => {
|
|
936
|
+
if (typeof name !== "string")
|
|
937
|
+
throw new Error("PoseMixer.serialize: invalid registered clip name.");
|
|
938
|
+
const entry = poseRecord(input, "registered clip", ["source", "compiled"]);
|
|
939
|
+
const source = entry.source;
|
|
940
|
+
assertPoseSourceClip(source);
|
|
941
|
+
const playback = new AnimationAction(source).serialize();
|
|
942
|
+
return {
|
|
943
|
+
name,
|
|
944
|
+
sourceIdentity: playback.state.clipIdentity,
|
|
945
|
+
compiledIdentity: poseCompiledIdentity(entry.compiled),
|
|
946
|
+
additive: isClipAdditive(source)
|
|
947
|
+
};
|
|
948
|
+
});
|
|
949
|
+
return readPoseClipCatalog(clips);
|
|
950
|
+
}
|
|
395
951
|
/**
|
|
396
952
|
* `crossFadeTo(clip, seconds, options)` — mirrors `node.play` semantics:
|
|
397
953
|
* the current active base action fades out over `seconds` (default driven by
|
|
@@ -428,7 +984,7 @@ export class PoseMixer {
|
|
|
428
984
|
activeBaseAction() {
|
|
429
985
|
for (let i = this.actions.length - 1; i >= 0; i -= 1) {
|
|
430
986
|
const action = this.actions[i];
|
|
431
|
-
if (!action.additive && action.playing)
|
|
987
|
+
if (!action.additive && action.playing && action.enabled)
|
|
432
988
|
return action;
|
|
433
989
|
}
|
|
434
990
|
return null;
|
|
@@ -487,11 +1043,13 @@ export class PoseMixer {
|
|
|
487
1043
|
}
|
|
488
1044
|
}
|
|
489
1045
|
onEvent(listener) {
|
|
1046
|
+
this.assertAlive();
|
|
490
1047
|
this.eventListeners.add(listener);
|
|
491
1048
|
return () => this.eventListeners.delete(listener);
|
|
492
1049
|
}
|
|
493
1050
|
activate(action) {
|
|
494
|
-
|
|
1051
|
+
this.assertAlive();
|
|
1052
|
+
if (!this.actions.includes(action) && !this.layers.some(layer => layer.actions.includes(action)))
|
|
495
1053
|
this.actions.push(action);
|
|
496
1054
|
}
|
|
497
1055
|
deactivate(action) {
|
|
@@ -507,10 +1065,17 @@ export class PoseMixer {
|
|
|
507
1065
|
const parsed = parseTrackTarget(track.target);
|
|
508
1066
|
if (parsed === null)
|
|
509
1067
|
continue;
|
|
510
|
-
const indices = this.skeleton.jointIndicesByName.get(parsed.nodeName)
|
|
1068
|
+
const indices = this.skeleton.jointIndicesByName.get(parsed.nodeName)
|
|
1069
|
+
?? this.skeleton.jointIndicesByName.get(unescapeNodeName(parsed.nodeName));
|
|
511
1070
|
if (!indices || indices.length === 0)
|
|
512
1071
|
continue;
|
|
513
|
-
|
|
1072
|
+
const expectedType = parsed.channel === "rotation" ? "quaternion" : "vector3";
|
|
1073
|
+
const expectedStride = parsed.channel === "rotation" ? 4 : 3;
|
|
1074
|
+
if (track.valueType !== expectedType || track.stride !== expectedStride) {
|
|
1075
|
+
throw new Error(`POSE_TRACK_TYPE_MISMATCH: ${track.target} requires ${expectedType} (${expectedStride} components), got ${track.valueType} (${track.stride}).`);
|
|
1076
|
+
}
|
|
1077
|
+
for (const boneIndex of indices)
|
|
1078
|
+
bindings.push({ trackIndex: i, boneIndex, channel: parsed.channel });
|
|
514
1079
|
}
|
|
515
1080
|
return bindings;
|
|
516
1081
|
}
|
|
@@ -538,82 +1103,170 @@ export class PoseMixer {
|
|
|
538
1103
|
* times/weights, and emit clip events between old and new action times.
|
|
539
1104
|
*/
|
|
540
1105
|
update(dt) {
|
|
541
|
-
this.
|
|
542
|
-
this.
|
|
543
|
-
|
|
544
|
-
const events = [];
|
|
545
|
-
const allActions = [...this.actions];
|
|
546
|
-
for (const layer of this.layers)
|
|
547
|
-
allActions.push(...layer.actions);
|
|
548
|
-
for (const action of allActions) {
|
|
549
|
-
if (!action.playing && !action.enabled)
|
|
550
|
-
continue;
|
|
551
|
-
const effectiveTimeScale = action.updateTimeScale(this.time);
|
|
552
|
-
const clipDelta = scaledDt * effectiveTimeScale;
|
|
553
|
-
const { previous, wrapped } = action.updateTime(clipDelta);
|
|
554
|
-
const weight = action.updateWeight(this.time);
|
|
555
|
-
if (!action.enabled && !action.playing)
|
|
556
|
-
this.deactivate(action);
|
|
557
|
-
if (!action.enabled)
|
|
558
|
-
continue;
|
|
559
|
-
const source = this.clips.get(action.clipName);
|
|
560
|
-
if (source !== undefined && source.source.events.length > 0 && weight > 0) {
|
|
561
|
-
const to = action.time;
|
|
562
|
-
const invocations = sampleClipEvents({ id: action.clipName, duration: source.compiled.duration, events: source.source.events }, {
|
|
563
|
-
from: previous,
|
|
564
|
-
to,
|
|
565
|
-
duration: source.compiled.duration,
|
|
566
|
-
loop: wrapped,
|
|
567
|
-
direction: to >= previous ? 1 : -1,
|
|
568
|
-
playbackTime: this.time
|
|
569
|
-
});
|
|
570
|
-
events.push(...invocations);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
// Aura extension: sync groups keep normalized phase aligned to the leader
|
|
574
|
-
// (first activated member) — three r185 has no phase sync. Only enabled
|
|
575
|
-
// members take part: a faded-out action stays `playing` (enabled=false, as
|
|
576
|
-
// in r185) and must not keep leading the group — it would slave the
|
|
577
|
-
// incoming clip to its own (restored) rate after the crossfade.
|
|
578
|
-
const groups = new Map();
|
|
579
|
-
for (const action of allActions) {
|
|
580
|
-
if (action.syncGroupName === null || !action.playing || !action.enabled)
|
|
581
|
-
continue;
|
|
582
|
-
const group = groups.get(action.syncGroupName) ?? [];
|
|
583
|
-
group.push(action);
|
|
584
|
-
groups.set(action.syncGroupName, group);
|
|
1106
|
+
this.assertAlive();
|
|
1107
|
+
if (!Number.isFinite(dt) || !Number.isFinite(this.timeScale)) {
|
|
1108
|
+
throw new Error("PoseMixer update delta and timeScale must be finite.");
|
|
585
1109
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1110
|
+
this.updateDepth += 1;
|
|
1111
|
+
try {
|
|
1112
|
+
this.lastDt = Math.abs(dt);
|
|
1113
|
+
const scaledDt = dt * this.timeScale;
|
|
1114
|
+
if (!Number.isFinite(scaledDt))
|
|
1115
|
+
throw new Error("PoseMixer scaled delta must be finite.");
|
|
1116
|
+
// Durations use elapsed mixer time even during reverse playback. Fades
|
|
1117
|
+
// continue while an action is paused; its warp and playhead do not.
|
|
1118
|
+
const elapsedDt = Math.abs(scaledDt);
|
|
1119
|
+
this.time += elapsedDt;
|
|
1120
|
+
const events = [];
|
|
1121
|
+
this.lastEventOverflow = 0;
|
|
1122
|
+
const allActions = [...this.actions];
|
|
1123
|
+
for (const layer of this.layers)
|
|
1124
|
+
allActions.push(...layer.actions);
|
|
1125
|
+
// Establish the same phase and reflected direction before advancing. This
|
|
1126
|
+
// lets followers report markers they actually cross, including when a new
|
|
1127
|
+
// action joins a group while its leader is moving backwards.
|
|
1128
|
+
const leaders = new Map();
|
|
1129
|
+
const leaderFor = new Map();
|
|
1130
|
+
for (const action of allActions) {
|
|
1131
|
+
if (action.syncGroupName === null || !action.playing || !action.enabled)
|
|
1132
|
+
continue;
|
|
1133
|
+
const leader = leaders.get(action.syncGroupName);
|
|
1134
|
+
if (leader === undefined)
|
|
1135
|
+
leaders.set(action.syncGroupName, action);
|
|
1136
|
+
else {
|
|
1137
|
+
if (!action.paused)
|
|
1138
|
+
action.synchronizePhaseWith(leader);
|
|
1139
|
+
leaderFor.set(action, leader);
|
|
1140
|
+
}
|
|
594
1141
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1142
|
+
const advances = new Map();
|
|
1143
|
+
for (const action of allActions) {
|
|
1144
|
+
if (!action.playing || !action.enabled) {
|
|
1145
|
+
// Finished fades must not retain an ever-growing list of active actions.
|
|
1146
|
+
if (action.playing && !action.enabled)
|
|
1147
|
+
action.stop();
|
|
1148
|
+
else
|
|
1149
|
+
action.setPlaybackDelta(0);
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
const wasPaused = action.paused;
|
|
1153
|
+
const leader = leaderFor.get(action);
|
|
1154
|
+
const leaderAdvances = leader === undefined ? undefined : advances.get(leader);
|
|
1155
|
+
const pauseOffset = action.warpPauseOffset();
|
|
1156
|
+
let clipSegments = wasPaused || !action.hasPlaybackDuration ? [] : leaderAdvances !== undefined
|
|
1157
|
+
? leader.clip.duration > 0
|
|
1158
|
+
? leaderAdvances.map(segment => {
|
|
1159
|
+
const ratio = action.clip.duration / leader.clip.duration;
|
|
1160
|
+
return {
|
|
1161
|
+
...segment, from: segment.from * ratio, to: segment.to * ratio,
|
|
1162
|
+
distance: segment.distance * ratio
|
|
1163
|
+
};
|
|
1164
|
+
})
|
|
1165
|
+
: []
|
|
1166
|
+
: action.integratedTimeScaleSpans(scaledDt);
|
|
1167
|
+
if (pauseOffset !== null && pauseOffset < elapsedDt) {
|
|
1168
|
+
clipSegments = limitTimeScaleSegments(clipSegments, pauseOffset);
|
|
1169
|
+
}
|
|
1170
|
+
const actualAdvances = [];
|
|
1171
|
+
const actionEvents = [];
|
|
1172
|
+
let actionOverflow = 0;
|
|
1173
|
+
let playbackDelta = 0;
|
|
1174
|
+
let finishOffset = null;
|
|
1175
|
+
const catchUpLimit = action.maxEventCatchUp;
|
|
1176
|
+
if (!Number.isSafeInteger(catchUpLimit) || catchUpLimit < 0) {
|
|
1177
|
+
throw new Error("PoseAction maxEventCatchUp must be a non-negative safe integer.");
|
|
1178
|
+
}
|
|
1179
|
+
const authoredLoopMode = action.loopMode;
|
|
1180
|
+
try {
|
|
1181
|
+
// A sync group is leader-driven: followers cross the same normalized
|
|
1182
|
+
// seams and reflections even when their standalone loop mode differs.
|
|
1183
|
+
if (leader !== undefined)
|
|
1184
|
+
action.loopMode = leader.loopMode;
|
|
1185
|
+
for (const segment of clipSegments) {
|
|
1186
|
+
action.maxEventCatchUp = Math.max(0, catchUpLimit - actionEvents.length);
|
|
1187
|
+
const result = action.updateTime(segment.distance);
|
|
1188
|
+
const travelled = action.lastPlaybackDelta;
|
|
1189
|
+
const finished = !action.playing || action.paused;
|
|
1190
|
+
const consumed = finished ? AnimationAction.consumedWarpDuration(segment, Math.abs(travelled)) : segment.duration;
|
|
1191
|
+
const endRate = segment.duration > 0
|
|
1192
|
+
? segment.from + (segment.to - segment.from) * (consumed / segment.duration)
|
|
1193
|
+
: segment.from;
|
|
1194
|
+
actualAdvances.push({ ...segment, duration: consumed, to: endRate, distance: travelled });
|
|
1195
|
+
playbackDelta += travelled;
|
|
1196
|
+
actionOverflow += result.eventOverflow;
|
|
1197
|
+
for (const event of result.events)
|
|
1198
|
+
actionEvents.push({ previous: result.previous, event });
|
|
1199
|
+
if (finished) {
|
|
1200
|
+
finishOffset = segment.startOffset + consumed;
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
finally {
|
|
1206
|
+
action.maxEventCatchUp = catchUpLimit;
|
|
1207
|
+
action.loopMode = authoredLoopMode;
|
|
1208
|
+
}
|
|
1209
|
+
action.setPlaybackDelta(playbackDelta);
|
|
1210
|
+
advances.set(action, actualAdvances);
|
|
1211
|
+
if (!wasPaused && action.hasPlaybackDuration) {
|
|
1212
|
+
action.advanceWarp(Math.min(elapsedDt, finishOffset ?? elapsedDt, pauseOffset ?? elapsedDt));
|
|
1213
|
+
}
|
|
1214
|
+
action.updateTimeScale(this.time, !wasPaused && action.paused);
|
|
1215
|
+
action.updateWeight(this.time);
|
|
1216
|
+
if (!action.enabled) {
|
|
1217
|
+
if (action.playing)
|
|
1218
|
+
action.stop();
|
|
1219
|
+
else
|
|
1220
|
+
this.deactivate(action);
|
|
1221
|
+
}
|
|
1222
|
+
this.lastEventOverflow += actionOverflow;
|
|
1223
|
+
for (const { previous, event: marker } of actionEvents) {
|
|
1224
|
+
const { clipName: _clipName, direction, loopCount, ...event } = marker;
|
|
1225
|
+
events.push({
|
|
1226
|
+
clipId: action.clipName,
|
|
1227
|
+
event,
|
|
1228
|
+
time: event.time,
|
|
1229
|
+
previousTime: previous,
|
|
1230
|
+
localTime: event.time,
|
|
1231
|
+
normalizedTime: action.clip.duration > 0 ? event.time / action.clip.duration : 0,
|
|
1232
|
+
loopCount,
|
|
1233
|
+
direction,
|
|
1234
|
+
playbackTime: this.time
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
611
1237
|
}
|
|
612
|
-
|
|
613
|
-
|
|
1238
|
+
// Preserve the established leader-driven phase contract even if actions
|
|
1239
|
+
// have different loop modes; marker traversal above uses each action's mode.
|
|
1240
|
+
for (const [member, leader] of leaderFor) {
|
|
1241
|
+
if (member.playing && member.enabled && !member.paused && leader.playing && leader.enabled) {
|
|
1242
|
+
member.time = leader.normalizedPhase * member.clip.duration;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
for (const invocation of events) {
|
|
1246
|
+
for (const listener of this.eventListeners)
|
|
1247
|
+
listener(invocation);
|
|
614
1248
|
}
|
|
1249
|
+
if (this.inertializer !== null && this.inertializerElapsed !== Infinity) {
|
|
1250
|
+
this.inertializerElapsed += Math.abs(scaledDt);
|
|
1251
|
+
}
|
|
1252
|
+
// Sweep layers whose actions have all faded out.
|
|
1253
|
+
for (let i = this.layers.length - 1; i >= 0; i -= 1) {
|
|
1254
|
+
const layer = this.layers[i];
|
|
1255
|
+
if (layer.actions.length > 0 && layer.actions.every((a) => !a.enabled || !a.playing)) {
|
|
1256
|
+
for (const action of layer.actions)
|
|
1257
|
+
action.stop();
|
|
1258
|
+
this.layers.splice(i, 1);
|
|
1259
|
+
this.layerByName.delete(layer.name);
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
pruneFinishedLayerActions(layer);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return events;
|
|
1266
|
+
}
|
|
1267
|
+
finally {
|
|
1268
|
+
this.updateDepth -= 1;
|
|
615
1269
|
}
|
|
616
|
-
return events;
|
|
617
1270
|
}
|
|
618
1271
|
/**
|
|
619
1272
|
* Evaluate the final pose into `out` (PoseBuffer with skeleton.boneCount
|
|
@@ -621,6 +1274,7 @@ export class PoseMixer {
|
|
|
621
1274
|
* (incremental-weighted, rest fills below Σw=1), then each layer in order.
|
|
622
1275
|
*/
|
|
623
1276
|
evaluate(out) {
|
|
1277
|
+
this.assertAlive();
|
|
624
1278
|
const n = this.skeleton.boneCount;
|
|
625
1279
|
const rest = this.skeleton.restPose;
|
|
626
1280
|
this.previousPose = this.latestPose;
|
|
@@ -654,6 +1308,7 @@ export class PoseMixer {
|
|
|
654
1308
|
* `evaluate`, so the two paths stay within the 1e-4 parity bar.
|
|
655
1309
|
*/
|
|
656
1310
|
evaluateSamples(samples, out) {
|
|
1311
|
+
this.assertAlive();
|
|
657
1312
|
const rest = this.skeleton.restPose;
|
|
658
1313
|
out.positions.set(rest.positions);
|
|
659
1314
|
out.rotations.set(rest.rotations);
|
|
@@ -685,6 +1340,30 @@ export class PoseMixer {
|
|
|
685
1340
|
this.applyAdditiveAccumulators(out);
|
|
686
1341
|
this.resetAccumulators();
|
|
687
1342
|
}
|
|
1343
|
+
/** Stop all actions and release clip, binding, and event-listener references. */
|
|
1344
|
+
dispose() {
|
|
1345
|
+
if (this.disposed)
|
|
1346
|
+
return;
|
|
1347
|
+
for (const action of [...this.actions])
|
|
1348
|
+
action.stop();
|
|
1349
|
+
for (const layer of this.layers)
|
|
1350
|
+
for (const action of layer.actions)
|
|
1351
|
+
action.stop();
|
|
1352
|
+
this.actions.length = 0;
|
|
1353
|
+
this.layers.length = 0;
|
|
1354
|
+
this.layerByName.clear();
|
|
1355
|
+
this.clips.clear();
|
|
1356
|
+
this.statelessBindings.clear();
|
|
1357
|
+
this.statelessCursors.clear();
|
|
1358
|
+
this.eventListeners.clear();
|
|
1359
|
+
this.latestPose = null;
|
|
1360
|
+
this.previousPose = null;
|
|
1361
|
+
this.disposed = true;
|
|
1362
|
+
}
|
|
1363
|
+
assertAlive() {
|
|
1364
|
+
if (this.disposed)
|
|
1365
|
+
throw new Error("PoseMixer has been disposed.");
|
|
1366
|
+
}
|
|
688
1367
|
applyBase(out) {
|
|
689
1368
|
const scratch = this.scratchClip;
|
|
690
1369
|
for (const action of this.actions) {
|
|
@@ -896,4 +1575,346 @@ function parseTrackTarget(target) {
|
|
|
896
1575
|
return null;
|
|
897
1576
|
return { nodeName: target.slice(0, separator), channel };
|
|
898
1577
|
}
|
|
1578
|
+
function unescapeNodeName(name) {
|
|
1579
|
+
return name.replace(/\\(.)/gu, "$1");
|
|
1580
|
+
}
|
|
1581
|
+
function poseRecord(value, label, allowed, required = allowed) {
|
|
1582
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) ||
|
|
1583
|
+
(Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
|
|
1584
|
+
throw new Error(`PoseMixer.restore: ${label} must be a plain object.`);
|
|
1585
|
+
}
|
|
1586
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1587
|
+
if (Object.getOwnPropertySymbols(value).length || Object.keys(descriptors).some(key => !allowed.includes(key) || !("value" in descriptors[key]) || !descriptors[key].enumerable)) {
|
|
1588
|
+
throw new Error(`PoseMixer.restore: ${label} contains unsupported fields, accessors or callbacks.`);
|
|
1589
|
+
}
|
|
1590
|
+
if (required.some(key => !Object.prototype.hasOwnProperty.call(descriptors, key))) {
|
|
1591
|
+
throw new Error(`PoseMixer.restore: ${label} is missing required fields.`);
|
|
1592
|
+
}
|
|
1593
|
+
return value;
|
|
1594
|
+
}
|
|
1595
|
+
function poseArray(value, label) {
|
|
1596
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || Object.getOwnPropertySymbols(value).length) {
|
|
1597
|
+
throw new Error(`PoseMixer.restore: ${label} must be a plain array.`);
|
|
1598
|
+
}
|
|
1599
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1600
|
+
const keys = Object.keys(descriptors);
|
|
1601
|
+
if (keys.length !== value.length + 1 || keys.some(key => key !== "length" &&
|
|
1602
|
+
(!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length))) {
|
|
1603
|
+
throw new Error(`PoseMixer.restore: ${label} contains sparse or unsupported array properties.`);
|
|
1604
|
+
}
|
|
1605
|
+
for (let index = 0; index < value.length; index++) {
|
|
1606
|
+
const descriptor = descriptors[String(index)];
|
|
1607
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
1608
|
+
throw new Error(`PoseMixer.restore: ${label} contains sparse entries or accessors.`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return value;
|
|
1612
|
+
}
|
|
1613
|
+
function poseNumber(value, label, minimum = -Infinity) {
|
|
1614
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || Object.is(value, -0)) {
|
|
1615
|
+
throw new Error(`PoseMixer.restore: ${label} must be a finite, JSON-roundtrippable number.`);
|
|
1616
|
+
}
|
|
1617
|
+
return value;
|
|
1618
|
+
}
|
|
1619
|
+
function poseBoolean(value, label) {
|
|
1620
|
+
if (typeof value !== "boolean")
|
|
1621
|
+
throw new Error(`PoseMixer.restore: ${label} must be boolean.`);
|
|
1622
|
+
return value;
|
|
1623
|
+
}
|
|
1624
|
+
function readPoseNumberArray(value, length, label) {
|
|
1625
|
+
const values = poseArray(value, label);
|
|
1626
|
+
if (values.length !== length)
|
|
1627
|
+
throw new Error(`PoseMixer.restore: ${label} has the wrong length.`);
|
|
1628
|
+
return values.map((item, index) => poseNumber(item, `${label}[${index}]`));
|
|
1629
|
+
}
|
|
1630
|
+
function readPoseFloatArray(value, length, label) {
|
|
1631
|
+
return readPoseNumberArray(value, length, label).map(number => {
|
|
1632
|
+
if (Math.fround(number) !== number)
|
|
1633
|
+
throw new Error(`PoseMixer.restore: ${label} must contain exact Float32 values.`);
|
|
1634
|
+
return number;
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
function readPoseTypedNumbers(value, constructor, length, label) {
|
|
1638
|
+
if (!(value instanceof constructor) || Object.getPrototypeOf(value) !== constructor.prototype ||
|
|
1639
|
+
Object.getOwnPropertySymbols(value).length || value.length !== length) {
|
|
1640
|
+
throw new Error(`PoseMixer.serialize: ${label} must be an unmodified ${constructor.name} of the expected length.`);
|
|
1641
|
+
}
|
|
1642
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1643
|
+
if (Object.keys(descriptors).length !== value.length || Object.keys(descriptors).some(key => !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= value.length ||
|
|
1644
|
+
!("value" in descriptors[key]) || !descriptors[key].enumerable)) {
|
|
1645
|
+
throw new Error(`PoseMixer.serialize: ${label} contains unsupported properties or callbacks.`);
|
|
1646
|
+
}
|
|
1647
|
+
return Array.from(value, (item, index) => poseNumber(item, `${label}[${index}]`));
|
|
1648
|
+
}
|
|
1649
|
+
function readPoseBindings(value, boneCount, trackCount) {
|
|
1650
|
+
return poseArray(value, "track bindings").map(entry => {
|
|
1651
|
+
const binding = poseRecord(entry, "track binding", ["trackIndex", "boneIndex", "channel"]);
|
|
1652
|
+
const trackIndex = poseNumber(binding.trackIndex, "binding trackIndex", 0);
|
|
1653
|
+
const boneIndex = poseNumber(binding.boneIndex, "binding boneIndex", 0);
|
|
1654
|
+
if (!Number.isSafeInteger(trackIndex) || trackIndex >= trackCount ||
|
|
1655
|
+
!Number.isSafeInteger(boneIndex) || boneIndex >= boneCount ||
|
|
1656
|
+
(binding.channel !== "position" && binding.channel !== "rotation" && binding.channel !== "scale")) {
|
|
1657
|
+
throw new Error("PoseMixer.restore: invalid track binding.");
|
|
1658
|
+
}
|
|
1659
|
+
return { trackIndex, boneIndex, channel: binding.channel };
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
function readPoseEnvelope(value, label, nonnegative) {
|
|
1663
|
+
if (value === null)
|
|
1664
|
+
return null;
|
|
1665
|
+
const record = poseRecord(value, label, ["startTime", "duration", "from", "to"]);
|
|
1666
|
+
const startTime = poseNumber(record.startTime, `${label} startTime`);
|
|
1667
|
+
const duration = poseNumber(record.duration, `${label} duration`, 0);
|
|
1668
|
+
const from = poseNumber(record.from, `${label} from`, nonnegative ? 0 : -Infinity);
|
|
1669
|
+
const to = poseNumber(record.to, `${label} to`, nonnegative ? 0 : -Infinity);
|
|
1670
|
+
if (duration === 0 || !Number.isFinite(startTime + duration) || !Number.isFinite(to - from)) {
|
|
1671
|
+
throw new Error(`PoseMixer.restore: invalid ${label} interval.`);
|
|
1672
|
+
}
|
|
1673
|
+
return { startTime, duration, from, to };
|
|
1674
|
+
}
|
|
1675
|
+
function readPoseWarp(value) {
|
|
1676
|
+
if (value === null)
|
|
1677
|
+
return null;
|
|
1678
|
+
const record = poseRecord(value, "warp", ["startTime", "duration", "from", "to", "elapsed"], ["startTime", "duration", "from", "to"]);
|
|
1679
|
+
const envelope = readPoseEnvelope({
|
|
1680
|
+
startTime: record.startTime, duration: record.duration, from: record.from, to: record.to
|
|
1681
|
+
}, "warp", false);
|
|
1682
|
+
if (!Object.prototype.hasOwnProperty.call(record, "elapsed"))
|
|
1683
|
+
return envelope;
|
|
1684
|
+
const elapsed = poseNumber(record.elapsed, "warp elapsed", 0);
|
|
1685
|
+
if (elapsed > envelope.duration)
|
|
1686
|
+
throw new Error("PoseMixer.restore: invalid warp elapsed time.");
|
|
1687
|
+
return { ...envelope, elapsed };
|
|
1688
|
+
}
|
|
1689
|
+
function rebasePoseEnvelope(envelope, offset) {
|
|
1690
|
+
if (envelope === null)
|
|
1691
|
+
return null;
|
|
1692
|
+
const startTime = envelope.startTime + offset;
|
|
1693
|
+
if (!Number.isFinite(startTime) || !Number.isFinite(startTime + envelope.duration)) {
|
|
1694
|
+
throw new Error("PoseAction.restore: rebased envelope is not finite.");
|
|
1695
|
+
}
|
|
1696
|
+
return { ...envelope, startTime };
|
|
1697
|
+
}
|
|
1698
|
+
function capturePoseBuffer(value, boneCount, label) {
|
|
1699
|
+
const record = poseRecord(value, label, ["boneCount", "positions", "rotations", "scales"]);
|
|
1700
|
+
if (record.boneCount !== boneCount)
|
|
1701
|
+
throw new Error(`PoseMixer.serialize: ${label} has the wrong bone count.`);
|
|
1702
|
+
return {
|
|
1703
|
+
positions: readPoseTypedNumbers(record.positions, Float32Array, boneCount * 3, `${label} positions`),
|
|
1704
|
+
rotations: readPoseTypedNumbers(record.rotations, Float32Array, boneCount * 4, `${label} rotations`),
|
|
1705
|
+
scales: readPoseTypedNumbers(record.scales, Float32Array, boneCount * 3, `${label} scales`)
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
function readPoseBuffer(value, boneCount, label) {
|
|
1709
|
+
const record = poseRecord(value, label, ["positions", "rotations", "scales"]);
|
|
1710
|
+
return {
|
|
1711
|
+
positions: readPoseFloatArray(record.positions, boneCount * 3, `${label} positions`),
|
|
1712
|
+
rotations: readPoseFloatArray(record.rotations, boneCount * 4, `${label} rotations`),
|
|
1713
|
+
scales: readPoseFloatArray(record.scales, boneCount * 3, `${label} scales`)
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
function restorePoseBuffer(value, boneCount, label) {
|
|
1717
|
+
const pose = readPoseBuffer(value, boneCount, label);
|
|
1718
|
+
return {
|
|
1719
|
+
boneCount,
|
|
1720
|
+
positions: new Float32Array(pose.positions),
|
|
1721
|
+
rotations: new Float32Array(pose.rotations),
|
|
1722
|
+
scales: new Float32Array(pose.scales)
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
function readPoseSkeleton(value, boneCount) {
|
|
1726
|
+
const record = poseRecord(value, "skeleton target", ["jointNames", "parentIndices", "missingJointSlots", "nameBindings", "restPose"]);
|
|
1727
|
+
const jointNames = poseArray(record.jointNames, "joint names");
|
|
1728
|
+
const parents = readPoseNumberArray(record.parentIndices, boneCount, "parent indices");
|
|
1729
|
+
const missing = poseArray(record.missingJointSlots, "missing joint slots");
|
|
1730
|
+
if (jointNames.length !== boneCount || jointNames.some(name => typeof name !== "string") ||
|
|
1731
|
+
parents.some(index => !Number.isSafeInteger(index) || index < -1 || index >= boneCount) ||
|
|
1732
|
+
missing.length !== boneCount || missing.some(value => typeof value !== "boolean")) {
|
|
1733
|
+
throw new Error("PoseMixer.restore: invalid skeleton target topology.");
|
|
1734
|
+
}
|
|
1735
|
+
const seenNames = new Set();
|
|
1736
|
+
const nameBindings = poseArray(record.nameBindings, "skeleton name bindings").map(value => {
|
|
1737
|
+
const entry = poseRecord(value, "skeleton name binding", ["name", "indices"]);
|
|
1738
|
+
if (typeof entry.name !== "string" || seenNames.has(entry.name))
|
|
1739
|
+
throw new Error("PoseMixer.restore: invalid skeleton name binding.");
|
|
1740
|
+
seenNames.add(entry.name);
|
|
1741
|
+
const indices = poseArray(entry.indices, "skeleton name indices").map(value => poseNumber(value, "skeleton joint index", 0));
|
|
1742
|
+
if (indices.some(index => !Number.isSafeInteger(index) || index >= boneCount))
|
|
1743
|
+
throw new Error("PoseMixer.restore: invalid skeleton joint index.");
|
|
1744
|
+
return { name: entry.name, indices };
|
|
1745
|
+
});
|
|
1746
|
+
nameBindings.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1747
|
+
return {
|
|
1748
|
+
jointNames: jointNames, parentIndices: parents, missingJointSlots: missing,
|
|
1749
|
+
nameBindings, restPose: readPoseBuffer(record.restPose, boneCount, "rest pose")
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
function capturePoseSkeleton(skeleton) {
|
|
1753
|
+
const boneCount = poseNumber(skeleton.boneCount, "skeleton boneCount", 0);
|
|
1754
|
+
if (!Number.isSafeInteger(boneCount))
|
|
1755
|
+
throw new Error("PoseMixer.serialize: invalid skeleton bone count.");
|
|
1756
|
+
const nodeIndices = readPoseNumberArray(skeleton.jointNodeIndices, boneCount, "skeleton node indices");
|
|
1757
|
+
const missing = poseArray(skeleton.missingNodeIndices, "missing node indices")
|
|
1758
|
+
.map(value => poseNumber(value, "missing node index", 0));
|
|
1759
|
+
if (nodeIndices.some(value => !Number.isSafeInteger(value) || value < 0) ||
|
|
1760
|
+
missing.some(value => !Number.isSafeInteger(value) || !nodeIndices.includes(value))) {
|
|
1761
|
+
throw new Error("PoseMixer.serialize: invalid skeleton node indices.");
|
|
1762
|
+
}
|
|
1763
|
+
const nameMap = skeleton.jointIndicesByName;
|
|
1764
|
+
if (!(nameMap instanceof Map) || Object.getPrototypeOf(nameMap) !== Map.prototype ||
|
|
1765
|
+
Reflect.ownKeys(nameMap).length !== 0) {
|
|
1766
|
+
throw new Error("PoseMixer.serialize: skeleton name bindings must be an unmodified Map.");
|
|
1767
|
+
}
|
|
1768
|
+
const target = {
|
|
1769
|
+
jointNames: skeleton.jointNames,
|
|
1770
|
+
parentIndices: skeleton.parentIndices,
|
|
1771
|
+
missingJointSlots: nodeIndices.map(index => missing.includes(index)),
|
|
1772
|
+
nameBindings: [...nameMap].map(([name, indices]) => ({ name, indices })),
|
|
1773
|
+
restPose: capturePoseBuffer(skeleton.restPose, boneCount, "rest pose")
|
|
1774
|
+
};
|
|
1775
|
+
return readPoseSkeleton(target, boneCount);
|
|
1776
|
+
}
|
|
1777
|
+
function assertPoseActionData(action) {
|
|
1778
|
+
const allowed = [
|
|
1779
|
+
"mixer", "clipName", "clip", "bindings", "cursors", "additive", "playback", "weight", "timeScale",
|
|
1780
|
+
"enabled", "syncGroupName", "fade", "warpState", "restoreTimeScale", "effectiveWeight",
|
|
1781
|
+
"effectiveTimeScale", "layerMask", "layerBlendMode"
|
|
1782
|
+
];
|
|
1783
|
+
const descriptors = Object.getOwnPropertyDescriptors(action);
|
|
1784
|
+
if (Object.getPrototypeOf(action) !== PoseAction.prototype || Object.getOwnPropertySymbols(action).length ||
|
|
1785
|
+
Object.keys(descriptors).some(key => !allowed.includes(key) || !("value" in descriptors[key]) || !descriptors[key].enumerable)) {
|
|
1786
|
+
throw new Error("PoseAction.serialize: custom action fields, accessors and callbacks cannot be serialized.");
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
function assertPosePlaybackData(playback) {
|
|
1790
|
+
const allowed = [
|
|
1791
|
+
"clip", "time", "weight", "timeScale", "loopMode", "repetitions", "clampWhenFinished", "playing", "paused",
|
|
1792
|
+
"maxEventCatchUp", "lastPlaybackDelta", "pingPongForward", "loopsCompleted", "signalLoopDelta",
|
|
1793
|
+
"signalFinishedDirection", "signalEventOverflow", "fadeDuration", "fadeElapsed", "fadeStartWeight",
|
|
1794
|
+
"fadeTargetWeight", "warpState"
|
|
1795
|
+
];
|
|
1796
|
+
const descriptors = Object.getOwnPropertyDescriptors(playback);
|
|
1797
|
+
if (Object.getPrototypeOf(playback) !== AnimationAction.prototype || Object.getOwnPropertySymbols(playback).length ||
|
|
1798
|
+
Object.keys(descriptors).some(key => !allowed.includes(key) || !("value" in descriptors[key]) || !descriptors[key].enumerable)) {
|
|
1799
|
+
throw new Error("PoseAction: custom core playback fields, accessors and callbacks cannot be serialized or restored.");
|
|
1800
|
+
}
|
|
1801
|
+
const warp = descriptors.warpState?.value;
|
|
1802
|
+
if (warp !== null && warp !== undefined)
|
|
1803
|
+
poseRecord(warp, "core playback warp", ["duration", "elapsed", "from", "to"]);
|
|
1804
|
+
}
|
|
1805
|
+
function assertPoseInertializerData(inertializer, boneCount) {
|
|
1806
|
+
const allowed = ["captured", "positionVelocities", "scaleVelocities", "angularSpeeds", "angularAxes", "boneCount", "halfLife"];
|
|
1807
|
+
const descriptors = Object.getOwnPropertyDescriptors(inertializer);
|
|
1808
|
+
if (Object.getPrototypeOf(inertializer) !== PoseInertializer.prototype || Object.getOwnPropertySymbols(inertializer).length ||
|
|
1809
|
+
Object.keys(descriptors).some(key => !allowed.includes(key) || !("value" in descriptors[key]) || !descriptors[key].enumerable) ||
|
|
1810
|
+
descriptors.boneCount?.value !== boneCount) {
|
|
1811
|
+
throw new Error("PoseMixer: custom inertializers, callbacks or mismatched bone counts cannot be serialized or restored.");
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
function assertPoseWritable(value, keys, label) {
|
|
1815
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1816
|
+
if (keys.some(key => !descriptors[key] || !("value" in descriptors[key]) || !descriptors[key].writable)) {
|
|
1817
|
+
throw new Error(`PoseMixer.restore: ${label} contains non-writable state or accessors.`);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
function assertPoseMutableArray(value, label) {
|
|
1821
|
+
const array = poseArray(value, label);
|
|
1822
|
+
const descriptors = Object.getOwnPropertyDescriptors(array);
|
|
1823
|
+
if (!Object.isExtensible(array) || !Object.getOwnPropertyDescriptor(array, "length")?.writable ||
|
|
1824
|
+
Object.keys(descriptors).some(key => key !== "length" && (!descriptors[key].configurable || !descriptors[key].writable))) {
|
|
1825
|
+
throw new Error(`PoseMixer.restore: ${label} is not mutable.`);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
function assertPoseSourceClip(value) {
|
|
1829
|
+
if (value === null || typeof value !== "object" || Object.getPrototypeOf(value) !== AnimationClip.prototype) {
|
|
1830
|
+
throw new Error("PoseMixer.serialize: custom source clips cannot be serialized.");
|
|
1831
|
+
}
|
|
1832
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1833
|
+
const keys = ["name", "tracks", "events", "duration"];
|
|
1834
|
+
if (Object.getOwnPropertySymbols(value).length || keys.some(key => !descriptors[key]) ||
|
|
1835
|
+
Object.keys(descriptors).some(key => !keys.includes(key) || !("value" in descriptors[key]) || !descriptors[key].enumerable)) {
|
|
1836
|
+
throw new Error("PoseMixer.serialize: source clip contains unsupported fields, accessors or callbacks.");
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
function readPoseClipCatalog(value) {
|
|
1840
|
+
const names = new Set();
|
|
1841
|
+
const clips = poseArray(value, "registered clips").map(input => {
|
|
1842
|
+
const clip = poseRecord(input, "registered clip identity", ["name", "sourceIdentity", "compiledIdentity", "additive"]);
|
|
1843
|
+
if (typeof clip.name !== "string" || names.has(clip.name) ||
|
|
1844
|
+
typeof clip.sourceIdentity !== "string" || typeof clip.compiledIdentity !== "string") {
|
|
1845
|
+
throw new Error("PoseMixer.restore: invalid or duplicate registered clip identity.");
|
|
1846
|
+
}
|
|
1847
|
+
names.add(clip.name);
|
|
1848
|
+
return {
|
|
1849
|
+
name: clip.name,
|
|
1850
|
+
sourceIdentity: clip.sourceIdentity,
|
|
1851
|
+
compiledIdentity: clip.compiledIdentity,
|
|
1852
|
+
additive: poseBoolean(clip.additive, "registered clip additive mode")
|
|
1853
|
+
};
|
|
1854
|
+
});
|
|
1855
|
+
clips.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
1856
|
+
return clips;
|
|
1857
|
+
}
|
|
1858
|
+
function rejectPoseNegativeZero(value) {
|
|
1859
|
+
if (typeof value === "number" && Object.is(value, -0)) {
|
|
1860
|
+
throw new Error("PoseMixer.serialize: negative zero cannot round-trip through JSON.");
|
|
1861
|
+
}
|
|
1862
|
+
if (value && typeof value === "object") {
|
|
1863
|
+
for (const nested of Object.values(value))
|
|
1864
|
+
rejectPoseNegativeZero(nested);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* Collision-free identity for the actual compiled samples, not just their
|
|
1869
|
+
* source clip. `addCompiledClip` permits independently supplied flat arrays.
|
|
1870
|
+
*/
|
|
1871
|
+
function poseCompiledIdentity(clip) {
|
|
1872
|
+
return canonicalPoseData(clip, new Set(), 0);
|
|
1873
|
+
}
|
|
1874
|
+
function canonicalPoseData(value, ancestors, depth) {
|
|
1875
|
+
if (value === null)
|
|
1876
|
+
return "null";
|
|
1877
|
+
if (typeof value === "string")
|
|
1878
|
+
return `s${JSON.stringify(value)}`;
|
|
1879
|
+
if (typeof value === "boolean")
|
|
1880
|
+
return value ? "b1" : "b0";
|
|
1881
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1882
|
+
return `n${Object.is(value, -0) ? "-0" : JSON.stringify(value)}`;
|
|
1883
|
+
if (typeof value !== "object" || depth > 128)
|
|
1884
|
+
throw new Error("PoseMixer.serialize: compiled clip contains nonserializable data or callbacks.");
|
|
1885
|
+
if (ancestors.has(value))
|
|
1886
|
+
throw new Error("PoseMixer.serialize: compiled clip contains a cycle.");
|
|
1887
|
+
ancestors.add(value);
|
|
1888
|
+
try {
|
|
1889
|
+
if (Array.isArray(value)) {
|
|
1890
|
+
const values = poseArray(value, "compiled clip array");
|
|
1891
|
+
return `a[${values.map(entry => canonicalPoseData(entry, ancestors, depth + 1)).join(",")}]`;
|
|
1892
|
+
}
|
|
1893
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1894
|
+
const typed = prototype === Float32Array.prototype ? "f32" : prototype === Float64Array.prototype ? "f64"
|
|
1895
|
+
: prototype === Uint8Array.prototype ? "u8" : prototype === Uint32Array.prototype ? "u32" : null;
|
|
1896
|
+
if (typed !== null) {
|
|
1897
|
+
const array = value;
|
|
1898
|
+
const descriptors = Object.getOwnPropertyDescriptors(array);
|
|
1899
|
+
if (Object.getOwnPropertySymbols(array).length || Object.keys(descriptors).length !== array.length ||
|
|
1900
|
+
Object.keys(descriptors).some(key => !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= array.length ||
|
|
1901
|
+
!("value" in descriptors[key]) || !descriptors[key].enumerable)) {
|
|
1902
|
+
throw new Error("PoseMixer.serialize: compiled clip typed array contains unsupported properties.");
|
|
1903
|
+
}
|
|
1904
|
+
return `t${typed}[${Array.from(array, entry => canonicalPoseData(entry, ancestors, depth + 1)).join(",")}]`;
|
|
1905
|
+
}
|
|
1906
|
+
if ((prototype !== Object.prototype && prototype !== null) || Object.getOwnPropertySymbols(value).length) {
|
|
1907
|
+
throw new Error("PoseMixer.serialize: compiled clip contains a nonserializable object.");
|
|
1908
|
+
}
|
|
1909
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
1910
|
+
const keys = Object.keys(descriptors).sort();
|
|
1911
|
+
if (keys.some(key => !("value" in descriptors[key]) || !descriptors[key].enumerable)) {
|
|
1912
|
+
throw new Error("PoseMixer.serialize: compiled clip contains an accessor or nonserializable property.");
|
|
1913
|
+
}
|
|
1914
|
+
return `o{${keys.map(key => `${JSON.stringify(key)}:${canonicalPoseData(descriptors[key].value, ancestors, depth + 1)}`).join(",")}}`;
|
|
1915
|
+
}
|
|
1916
|
+
finally {
|
|
1917
|
+
ancestors.delete(value);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
899
1920
|
//# sourceMappingURL=PoseMixer.js.map
|