@aura3d/animation 4.7.12 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/dist/AnimationAction.d.ts +80 -1
  2. package/dist/AnimationAction.d.ts.map +1 -1
  3. package/dist/AnimationAction.js +450 -80
  4. package/dist/AnimationAction.js.map +1 -1
  5. package/dist/AnimationClip.d.ts.map +1 -1
  6. package/dist/AnimationClip.js.map +1 -1
  7. package/dist/AnimationClipEvents.d.ts.map +1 -1
  8. package/dist/AnimationClipEvents.js.map +1 -1
  9. package/dist/AnimationClipRegistry.d.ts.map +1 -1
  10. package/dist/AnimationClipRegistry.js.map +1 -1
  11. package/dist/AnimationController.d.ts.map +1 -1
  12. package/dist/AnimationController.js.map +1 -1
  13. package/dist/AnimationEvents.d.ts +23 -0
  14. package/dist/AnimationEvents.d.ts.map +1 -1
  15. package/dist/AnimationEvents.js +145 -36
  16. package/dist/AnimationEvents.js.map +1 -1
  17. package/dist/AnimationLayer.d.ts.map +1 -1
  18. package/dist/AnimationLayer.js.map +1 -1
  19. package/dist/AnimationMixer.d.ts +58 -3
  20. package/dist/AnimationMixer.d.ts.map +1 -1
  21. package/dist/AnimationMixer.js +366 -12
  22. package/dist/AnimationMixer.js.map +1 -1
  23. package/dist/AnimationObjectGroup.d.ts +36 -12
  24. package/dist/AnimationObjectGroup.d.ts.map +1 -1
  25. package/dist/AnimationObjectGroup.js +219 -208
  26. package/dist/AnimationObjectGroup.js.map +1 -1
  27. package/dist/AnimationStateGraph.d.ts.map +1 -1
  28. package/dist/AnimationStateGraph.js.map +1 -1
  29. package/dist/AnimationStateMachine.d.ts.map +1 -1
  30. package/dist/AnimationStateMachine.js.map +1 -1
  31. package/dist/AnimationTrack.d.ts +7 -1
  32. package/dist/AnimationTrack.d.ts.map +1 -1
  33. package/dist/AnimationTrack.js +87 -10
  34. package/dist/AnimationTrack.js.map +1 -1
  35. package/dist/AnimationUtils.d.ts +16 -14
  36. package/dist/AnimationUtils.d.ts.map +1 -1
  37. package/dist/AnimationUtils.js +148 -109
  38. package/dist/AnimationUtils.js.map +1 -1
  39. package/dist/BlendTree.d.ts.map +1 -1
  40. package/dist/BlendTree.js.map +1 -1
  41. package/dist/Bone.d.ts.map +1 -1
  42. package/dist/Bone.js.map +1 -1
  43. package/dist/ECSAnimationBridge.d.ts.map +1 -1
  44. package/dist/ECSAnimationBridge.js.map +1 -1
  45. package/dist/FighterAnimationAdapter.d.ts.map +1 -1
  46. package/dist/FighterAnimationAdapter.js.map +1 -1
  47. package/dist/FootIk.d.ts.map +1 -1
  48. package/dist/FootIk.js.map +1 -1
  49. package/dist/HumanoidBoneInference.d.ts.map +1 -1
  50. package/dist/HumanoidBoneInference.js.map +1 -1
  51. package/dist/HumanoidRetargeting.d.ts.map +1 -1
  52. package/dist/HumanoidRetargeting.js.map +1 -1
  53. package/dist/IK.d.ts.map +1 -1
  54. package/dist/IK.js.map +1 -1
  55. package/dist/Inertialization.d.ts.map +1 -1
  56. package/dist/Inertialization.js.map +1 -1
  57. package/dist/Keyframe.d.ts +15 -1
  58. package/dist/Keyframe.d.ts.map +1 -1
  59. package/dist/Keyframe.js +34 -1
  60. package/dist/Keyframe.js.map +1 -1
  61. package/dist/LocomotionController.d.ts.map +1 -1
  62. package/dist/LocomotionController.js.map +1 -1
  63. package/dist/LocomotionEvidence.d.ts.map +1 -1
  64. package/dist/LocomotionEvidence.js.map +1 -1
  65. package/dist/LocomotionKit.d.ts.map +1 -1
  66. package/dist/LocomotionKit.js.map +1 -1
  67. package/dist/MotionQuality.d.ts.map +1 -1
  68. package/dist/MotionQuality.js.map +1 -1
  69. package/dist/PropertyBinding.d.ts +42 -22
  70. package/dist/PropertyBinding.d.ts.map +1 -1
  71. package/dist/PropertyBinding.js +390 -293
  72. package/dist/PropertyBinding.js.map +1 -1
  73. package/dist/RigQuality.d.ts.map +1 -1
  74. package/dist/RigQuality.js.map +1 -1
  75. package/dist/RootMotion.d.ts.map +1 -1
  76. package/dist/RootMotion.js.map +1 -1
  77. package/dist/SceneAnimationBridge.d.ts +7 -0
  78. package/dist/SceneAnimationBridge.d.ts.map +1 -1
  79. package/dist/SceneAnimationBridge.js +48 -3
  80. package/dist/SceneAnimationBridge.js.map +1 -1
  81. package/dist/Skeleton.d.ts.map +1 -1
  82. package/dist/Skeleton.js.map +1 -1
  83. package/dist/Skinning.d.ts.map +1 -1
  84. package/dist/Skinning.js.map +1 -1
  85. package/dist/SpringBones.d.ts.map +1 -1
  86. package/dist/SpringBones.js.map +1 -1
  87. package/dist/browser-index.d.ts +1 -1
  88. package/dist/browser-index.d.ts.map +1 -1
  89. package/dist/browser-index.js +1 -1
  90. package/dist/browser-index.js.map +1 -1
  91. package/dist/contracts/index.d.ts.map +1 -1
  92. package/dist/contracts/index.js.map +1 -1
  93. package/dist/contracts/pose.d.ts.map +1 -1
  94. package/dist/contracts/pose.js.map +1 -1
  95. package/dist/experimental/CrowdAnimation.d.ts.map +1 -1
  96. package/dist/experimental/CrowdAnimation.js.map +1 -1
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +1 -1
  100. package/dist/index.js.map +1 -1
  101. package/dist/lanes/index.d.ts.map +1 -1
  102. package/dist/lanes/index.js.map +1 -1
  103. package/dist/lanes/prd01.d.ts.map +1 -1
  104. package/dist/lanes/prd01.js.map +1 -1
  105. package/dist/lanes/prd02.d.ts.map +1 -1
  106. package/dist/lanes/prd02.js.map +1 -1
  107. package/dist/lanes/prd03.d.ts.map +1 -1
  108. package/dist/lanes/prd03.js.map +1 -1
  109. package/dist/lanes/prd04.d.ts.map +1 -1
  110. package/dist/lanes/prd04.js.map +1 -1
  111. package/dist/lanes/prd05.d.ts.map +1 -1
  112. package/dist/lanes/prd05.js.map +1 -1
  113. package/dist/lanes/prd06.d.ts.map +1 -1
  114. package/dist/lanes/prd06.js.map +1 -1
  115. package/dist/lanes/prd07.d.ts.map +1 -1
  116. package/dist/lanes/prd07.js.map +1 -1
  117. package/dist/lanes/prd08.d.ts.map +1 -1
  118. package/dist/lanes/prd08.js.map +1 -1
  119. package/dist/lanes/prd09.d.ts.map +1 -1
  120. package/dist/lanes/prd09.js.map +1 -1
  121. package/dist/lanes/prd10.d.ts.map +1 -1
  122. package/dist/lanes/prd10.js.map +1 -1
  123. package/dist/lanes/prd11.d.ts.map +1 -1
  124. package/dist/lanes/prd11.js.map +1 -1
  125. package/dist/lanes/prd12.d.ts.map +1 -1
  126. package/dist/lanes/prd12.js.map +1 -1
  127. package/dist/lanes/prd13.d.ts.map +1 -1
  128. package/dist/lanes/prd13.js.map +1 -1
  129. package/dist/lanes/prd14.d.ts.map +1 -1
  130. package/dist/lanes/prd14.js.map +1 -1
  131. package/dist/lanes/prd15.d.ts.map +1 -1
  132. package/dist/lanes/prd15.js.map +1 -1
  133. package/dist/library/performanceStateGraph.d.ts.map +1 -1
  134. package/dist/library/performanceStateGraph.js.map +1 -1
  135. package/dist/library/standardHumanoidClips.d.ts.map +1 -1
  136. package/dist/library/standardHumanoidClips.js.map +1 -1
  137. package/dist/pose/BoneMask.d.ts.map +1 -1
  138. package/dist/pose/BoneMask.js.map +1 -1
  139. package/dist/pose/CcdIkConstraint.d.ts.map +1 -1
  140. package/dist/pose/CcdIkConstraint.js.map +1 -1
  141. package/dist/pose/CompiledClip.d.ts +15 -8
  142. package/dist/pose/CompiledClip.d.ts.map +1 -1
  143. package/dist/pose/CompiledClip.js +82 -50
  144. package/dist/pose/CompiledClip.js.map +1 -1
  145. package/dist/pose/LookAtConstraint.d.ts.map +1 -1
  146. package/dist/pose/LookAtConstraint.js.map +1 -1
  147. package/dist/pose/MotionMetrics.d.ts.map +1 -1
  148. package/dist/pose/MotionMetrics.js.map +1 -1
  149. package/dist/pose/PoseBuffer.d.ts.map +1 -1
  150. package/dist/pose/PoseBuffer.js.map +1 -1
  151. package/dist/pose/PoseInertializer.d.ts.map +1 -1
  152. package/dist/pose/PoseInertializer.js.map +1 -1
  153. package/dist/pose/PoseMixer.d.ts +196 -19
  154. package/dist/pose/PoseMixer.d.ts.map +1 -1
  155. package/dist/pose/PoseMixer.js +1209 -188
  156. package/dist/pose/PoseMixer.js.map +1 -1
  157. package/dist/pose/Retarget.d.ts +4 -3
  158. package/dist/pose/Retarget.d.ts.map +1 -1
  159. package/dist/pose/Retarget.js +32 -15
  160. package/dist/pose/Retarget.js.map +1 -1
  161. package/dist/pose/RetargetCache.d.ts +12 -6
  162. package/dist/pose/RetargetCache.d.ts.map +1 -1
  163. package/dist/pose/RetargetCache.js +253 -29
  164. package/dist/pose/RetargetCache.js.map +1 -1
  165. package/dist/pose/RetargetWorker.d.ts.map +1 -1
  166. package/dist/pose/RetargetWorker.js +5 -0
  167. package/dist/pose/RetargetWorker.js.map +1 -1
  168. package/dist/pose/SkeletonBinding.d.ts.map +1 -1
  169. package/dist/pose/SkeletonBinding.js.map +1 -1
  170. package/dist/pose/blendKernels.d.ts.map +1 -1
  171. package/dist/pose/blendKernels.js.map +1 -1
  172. package/dist/pose/makeClipAdditive.d.ts +7 -14
  173. package/dist/pose/makeClipAdditive.d.ts.map +1 -1
  174. package/dist/pose/makeClipAdditive.js +53 -85
  175. package/dist/pose/makeClipAdditive.js.map +1 -1
  176. package/dist/pose/poseMixerFlags.d.ts.map +1 -1
  177. package/dist/pose/poseMixerFlags.js.map +1 -1
  178. package/dist/pose/quatFlat.d.ts.map +1 -1
  179. package/dist/pose/quatFlat.js.map +1 -1
  180. package/dist/pose/retarget.worker.d.ts.map +1 -1
  181. package/dist/pose/retarget.worker.js.map +1 -1
  182. package/dist/threejs-compatibility/AnimationAction.d.ts.map +1 -1
  183. package/dist/threejs-compatibility/AnimationAction.js.map +1 -1
  184. package/dist/threejs-compatibility/AnimationClip.d.ts.map +1 -1
  185. package/dist/threejs-compatibility/AnimationClip.js.map +1 -1
  186. package/dist/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -1
  187. package/dist/threejs-compatibility/AnimationDiagnostics.js.map +1 -1
  188. package/dist/threejs-compatibility/AnimationMixer.d.ts.map +1 -1
  189. package/dist/threejs-compatibility/AnimationMixer.js.map +1 -1
  190. package/dist/threejs-compatibility/MorphTargetMixer.d.ts.map +1 -1
  191. package/dist/threejs-compatibility/MorphTargetMixer.js.map +1 -1
  192. package/dist/threejs-compatibility/Skeleton.d.ts.map +1 -1
  193. package/dist/threejs-compatibility/Skeleton.js.map +1 -1
  194. package/dist/threejs-compatibility/SkinnedMesh.d.ts.map +1 -1
  195. package/dist/threejs-compatibility/SkinnedMesh.js.map +1 -1
  196. package/dist/threejs-compatibility/index.d.ts.map +1 -1
  197. package/dist/threejs-compatibility/index.js.map +1 -1
  198. package/package.json +2 -2
@@ -1,7 +1,9 @@
1
- import { sampleClipEvents } from "../AnimationClipEvents.js";
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.weight = options.weight ?? 1;
45
- this.timeScale = options.timeScale ?? 1;
46
- if (options.loopMode)
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.playing = false;
62
- this.paused = false;
63
- this.time = 0;
64
- this.loopCount = -1;
65
- this.pingPongForward = true;
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.paused = true;
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.loopMode = mode;
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.time = other.time;
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, 1);
114
+ return this.scheduleFade(duration, 0, this.weight);
95
115
  }
96
116
  fadeOut(duration) {
97
- return this.scheduleFade(duration, 1, 0);
117
+ return this.scheduleFade(duration, this.currentWeight(this.mixer.time), 0);
98
118
  }
99
119
  fadeTo(weight, duration) {
100
- return this.scheduleFade(duration, this.weight, weight);
120
+ return this.scheduleFade(duration, this.currentWeight(this.mixer.time), weight);
101
121
  }
102
- /** three r185 warp: multiplicative timescale interpolant over mixer time (values = start/base → end/base). */
122
+ /** Linearly change the signed playback rate over unpaused mixer time. */
103
123
  warp(startTimeScale, endTimeScale, duration) {
104
- const now = this.mixer.time;
105
- const base = this.timeScale;
106
- this.warpState = { startTime: now, duration, from: startTimeScale / base, to: endTimeScale / base };
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(1.0, startEndRatio, duration);
134
- this.warp(endStartRatio, 1.0, duration);
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
- const t = Math.min(Math.max((now - fade.startTime) / fade.duration, 0), 1);
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
- if (interpolant === 0)
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 timescale at mixer.time, per r185 `_updateTimeScale`. */
176
- updateTimeScale(now) {
421
+ /** Effective instantaneous rate, including a scheduled warp. */
422
+ updateTimeScale(_now, finishPausedWarp = false) {
177
423
  let timeScale = 0;
178
- if (!this.paused) {
179
- timeScale = this.timeScale;
424
+ if (!this.paused || finishPausedWarp) {
425
+ let rate = this.timeScale;
180
426
  const warp = this.warpState;
181
427
  if (warp !== null) {
182
- const end = warp.startTime + warp.duration;
183
- const t = Math.min(Math.max((now - warp.startTime) / warp.duration, 0), 1);
184
- timeScale *= warp.from + (warp.to - warp.from) * t;
185
- if (now > end) {
186
- if (timeScale === 0) {
187
- this.paused = true;
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
- /** Advance clip time per r185 `_updateTime`; returns {wrapped, finished}. */
202
- updateTime(deltaTime) {
203
- const duration = this.clip.duration;
204
- const previous = this.time;
205
- let time = previous + deltaTime;
206
- let wrapped = false;
207
- let finished = false;
208
- if (this.loopMode === "once") {
209
- if (this.loopCount === -1)
210
- this.loopCount = 0;
211
- if (time >= duration) {
212
- time = duration;
213
- finished = true;
214
- }
215
- else if (time < 0) {
216
- time = 0;
217
- finished = true;
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
- else {
229
- // repeat / pingpong
230
- if (this.loopCount === -1) {
231
- if (deltaTime >= 0) {
232
- this.loopCount = 0;
233
- }
234
- }
235
- if (duration > 0 && (time >= duration || time < 0)) {
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
- if (this.loopMode === "pingpong") {
251
- if ((this.loopCount & 1) === 1)
252
- this.pingPongForward = false;
253
- else
254
- this.pingPongForward = true;
255
- if (!this.pingPongForward)
256
- time = duration - time;
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
- this.time = time;
260
- return { wrapped, finished, previous };
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
- if (!this.actions.includes(action))
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
- bindings.push({ trackIndex: i, boneIndex: indices[0], channel: parsed.channel });
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.lastDt = dt;
542
- this.time += dt;
543
- const scaledDt = dt * this.timeScale;
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
- for (const group of groups.values()) {
587
- if (group.length < 2)
588
- continue;
589
- const leader = group[0];
590
- const phase = leader.normalizedPhase;
591
- for (let i = 1; i < group.length; i += 1) {
592
- const member = group[i];
593
- member.time = phase * member.clip.duration;
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
- for (const invocation of events) {
597
- for (const listener of this.eventListeners)
598
- listener(invocation);
599
- }
600
- if (this.inertializer !== null && this.inertializerElapsed !== Infinity) {
601
- this.inertializerElapsed += scaledDt;
602
- }
603
- // Sweep layers whose actions have all faded out.
604
- for (let i = this.layers.length - 1; i >= 0; i -= 1) {
605
- const layer = this.layers[i];
606
- if (layer.actions.length > 0 && layer.actions.every((a) => !a.enabled || !a.playing)) {
607
- for (const action of layer.actions)
608
- action.stop();
609
- this.layers.splice(i, 1);
610
- this.layerByName.delete(layer.name);
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
- else {
613
- pruneFinishedLayerActions(layer);
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