rig-play 1.11.1 → 1.11.2

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/index.js CHANGED
@@ -8777,6 +8777,116 @@ function applySetToPose(view, raw, set) {
8777
8777
  clips
8778
8778
  };
8779
8779
  }
8780
+ const SEEK_FINE_STEP_LIMIT = 4096;
8781
+ const SEEK_COARSE_STRIDE = 8;
8782
+ const SEEK_EXACT_LIMIT = 1024;
8783
+ function capturePhysicsContext(ctx) {
8784
+ const states = /* @__PURE__ */ new Map();
8785
+ for (const [name, s] of ctx.states) states.set(name, { ...s });
8786
+ return {
8787
+ phase: ctx.phase,
8788
+ time: ctx.time,
8789
+ last: ctx.last,
8790
+ loop: ctx.loop,
8791
+ env: ctx.env,
8792
+ states
8793
+ };
8794
+ }
8795
+ function restorePhysicsContext(ctx, snap) {
8796
+ ctx.phase = snap.phase;
8797
+ ctx.time = snap.time;
8798
+ ctx.last = snap.last;
8799
+ if (snap.loop === void 0) delete ctx.loop;
8800
+ else ctx.loop = snap.loop;
8801
+ if (snap.env === void 0) delete ctx.env;
8802
+ else ctx.env = snap.env;
8803
+ ctx.states.clear();
8804
+ for (const [name, s] of snap.states) ctx.states.set(name, { ...s });
8805
+ }
8806
+ function captureStepperState(stepper) {
8807
+ if ("first" in stepper) {
8808
+ const internal2 = stepper;
8809
+ return {
8810
+ kind: "track",
8811
+ steps: internal2.steps,
8812
+ latest: internal2.latest,
8813
+ refused: internal2.refused,
8814
+ trackTime: internal2.track.trackTime,
8815
+ last: internal2.track.last,
8816
+ ctx: capturePhysicsContext(internal2.track.ctx)
8817
+ };
8818
+ }
8819
+ const internal = stepper;
8820
+ const entry = internal.mixer.tracks[0] ?? null;
8821
+ return {
8822
+ kind: "tracks",
8823
+ started: internal.started,
8824
+ steps: internal.steps,
8825
+ time: internal.time,
8826
+ refused: internal.refused,
8827
+ animationsChanged: internal.mixer.animationsChanged,
8828
+ propertyIds: new Map(internal.mixer.propertyIds),
8829
+ entry: entry === null ? null : {
8830
+ delay: entry.delay,
8831
+ trackTime: entry.trackTime,
8832
+ trackLast: entry.trackLast,
8833
+ nextTrackLast: entry.nextTrackLast,
8834
+ trackEnd: entry.trackEnd,
8835
+ animationLast: entry.animationLast,
8836
+ nextAnimationLast: entry.nextAnimationLast,
8837
+ mixTime: entry.mixTime,
8838
+ mixDuration: entry.mixDuration,
8839
+ totalAlpha: entry.totalAlpha,
8840
+ alpha: entry.alpha,
8841
+ additive: entry.additive,
8842
+ keepHold: entry.keepHold,
8843
+ timelineMode: [...entry.timelineMode],
8844
+ timelineHoldMix: [...entry.timelineHoldMix],
8845
+ timelinesRotation: [...entry.timelinesRotation]
8846
+ },
8847
+ ctx: capturePhysicsContext(internal.path.ctx)
8848
+ };
8849
+ }
8850
+ function restoreStepperState(stepper, snap) {
8851
+ if (snap.kind === "track") {
8852
+ const internal2 = stepper;
8853
+ internal2.steps = snap.steps;
8854
+ internal2.latest = snap.latest;
8855
+ internal2.refused = snap.refused;
8856
+ internal2.track.trackTime = snap.trackTime;
8857
+ internal2.track.last = snap.last;
8858
+ restorePhysicsContext(internal2.track.ctx, snap.ctx);
8859
+ return;
8860
+ }
8861
+ const internal = stepper;
8862
+ internal.started = snap.started;
8863
+ internal.steps = snap.steps;
8864
+ internal.time = snap.time;
8865
+ internal.refused = snap.refused;
8866
+ internal.mixer.animationsChanged = snap.animationsChanged;
8867
+ internal.mixer.propertyIds.clear();
8868
+ for (const [k, v] of snap.propertyIds) internal.mixer.propertyIds.set(k, v);
8869
+ const entry = internal.mixer.tracks[0] ?? null;
8870
+ if (entry !== null && snap.entry !== null) {
8871
+ entry.delay = snap.entry.delay;
8872
+ entry.trackTime = snap.entry.trackTime;
8873
+ entry.trackLast = snap.entry.trackLast;
8874
+ entry.nextTrackLast = snap.entry.nextTrackLast;
8875
+ entry.trackEnd = snap.entry.trackEnd;
8876
+ entry.animationLast = snap.entry.animationLast;
8877
+ entry.nextAnimationLast = snap.entry.nextAnimationLast;
8878
+ entry.mixTime = snap.entry.mixTime;
8879
+ entry.mixDuration = snap.entry.mixDuration;
8880
+ entry.totalAlpha = snap.entry.totalAlpha;
8881
+ entry.alpha = snap.entry.alpha;
8882
+ entry.additive = snap.entry.additive;
8883
+ entry.keepHold = snap.entry.keepHold;
8884
+ entry.timelineMode = [...snap.entry.timelineMode];
8885
+ entry.timelineHoldMix = [...snap.entry.timelineHoldMix];
8886
+ entry.timelinesRotation = [...snap.entry.timelinesRotation];
8887
+ }
8888
+ restorePhysicsContext(internal.path.ctx, snap.ctx);
8889
+ }
8780
8890
  class RigPlayer {
8781
8891
  /**
8782
8892
  * The renderer this player draws through — its knobs and counters are the
@@ -8831,6 +8941,7 @@ class RigPlayer {
8831
8941
  /** The un-overridden pose from the track/tracks (or setup pose when `setSet` is called before `play`). */
8832
8942
  rawPose = null;
8833
8943
  setup = null;
8944
+ seekCache = null;
8834
8945
  setList;
8835
8946
  setsByName;
8836
8947
  activeSet = null;
@@ -8928,6 +9039,7 @@ class RigPlayer {
8928
9039
  const alpha = options.alpha ?? 1;
8929
9040
  const additive = options.additive ?? false;
8930
9041
  this.paused = false;
9042
+ this.clearSeekCache();
8931
9043
  const plain = track === 0 && mix === 0 && alpha === 1 && !additive;
8932
9044
  if (!plain) this.beyondBase = true;
8933
9045
  if (this.tracks === null && plain && !this.trackMode) {
@@ -8956,6 +9068,7 @@ class RigPlayer {
8956
9068
  queue(name, options = {}) {
8957
9069
  const track = options.track ?? 0;
8958
9070
  this.beyondBase = true;
9071
+ this.clearSeekCache();
8959
9072
  const tracks = this.compose();
8960
9073
  tracks.addAnimation(track, name, { loop: options.loop ?? true, mixDuration: options.mix ?? 0, delay: options.delay ?? 0, alpha: options.alpha ?? 1, additive: options.additive ?? false });
8961
9074
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8968,6 +9081,7 @@ class RigPlayer {
8968
9081
  */
8969
9082
  clear(track = 0, options = {}) {
8970
9083
  this.beyondBase = true;
9084
+ this.clearSeekCache();
8971
9085
  const tracks = this.compose();
8972
9086
  tracks.setEmptyAnimation(track, options.mix ?? 0);
8973
9087
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8981,6 +9095,7 @@ class RigPlayer {
8981
9095
  */
8982
9096
  queueClear(track = 0, options = {}) {
8983
9097
  this.beyondBase = true;
9098
+ this.clearSeekCache();
8984
9099
  const tracks = this.compose();
8985
9100
  tracks.addEmptyAnimation(track, options.mix ?? 0, options.delay ?? 0);
8986
9101
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8995,6 +9110,7 @@ class RigPlayer {
8995
9110
  */
8996
9111
  setAlpha(track, alpha) {
8997
9112
  this.beyondBase = true;
9113
+ this.clearSeekCache();
8998
9114
  const tracks = this.compose();
8999
9115
  tracks.setAlpha(track, alpha);
9000
9116
  this.setRawPose(tracks.step(0));
@@ -9025,6 +9141,7 @@ class RigPlayer {
9025
9141
  */
9026
9142
  compose() {
9027
9143
  if (this.tracks !== null) return this.tracks;
9144
+ this.clearSeekCache();
9028
9145
  const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9029
9146
  const single = this.track;
9030
9147
  const time = single?.trackTime ?? 0;
@@ -9061,8 +9178,10 @@ class RigPlayer {
9061
9178
  environment(environment) {
9062
9179
  if (environment === null) {
9063
9180
  this.env = null;
9181
+ this.clearSeekCache();
9064
9182
  this.stepper()?.setEnvironment(DEFAULT_ENVIRONMENT);
9065
9183
  } else if (environment !== void 0) {
9184
+ this.clearSeekCache();
9066
9185
  const live = this.stepper();
9067
9186
  if (live) {
9068
9187
  live.setEnvironment(environment);
@@ -9101,20 +9220,99 @@ class RigPlayer {
9101
9220
  this.paused = false;
9102
9221
  }
9103
9222
  /**
9104
- * Moves a single animation on track 0 to `time`: the tracks are reopened
9105
- * with that animation and stepped forward in `seekStep` increments (the
9106
- * last one shorter) until the time is reached. Events and completes crossed
9107
- * on the way are not fired. It has a meaning for one animation on one
9108
- * track only — the player cannot replay when a mix, a second track or a
9109
- * queue was asked for — so after any of those it refuses by name; step the
9110
- * player forward instead.
9223
+ * Moves a single animation on track 0 to `time`: snapshot-backed deterministic
9224
+ * seek (#141) that produces the exact pose and physics state of reopening at 0
9225
+ * and stepping forward in `seekStep` increments (the last one shorter) until
9226
+ * `time` is reached. Events and completes crossed on the way are not fired.
9227
+ * It has a meaning for one animation on one track only — the player cannot
9228
+ * replay when a mix, a second track or a queue was asked for — so after any
9229
+ * of those it refuses by name; step the player forward instead.
9111
9230
  */
9112
9231
  seek(time) {
9113
9232
  if (this.animation === null) throw new Error("seek before play: no animation is open");
9114
9233
  if (!(time >= 0) || !Number.isFinite(time)) throw new Error(`seek time must be a finite time at or above 0, got ${String(time)}`);
9115
9234
  if (this.beyondBase) throw new Error("seek is defined for a single animation on track 0; after a mix, another track, a queue or a clear the player cannot replay to a time — step it forward with frame/advance instead");
9116
- this.reopen();
9117
- this.stepTo(time);
9235
+ const cache = this.ensureSeekCache();
9236
+ const stepper = this.tracks ?? this.track;
9237
+ const exactHit = cache.exact.get(time);
9238
+ if (exactHit !== void 0) {
9239
+ restoreStepperState(stepper, exactHit.state);
9240
+ this.rawPose = exactHit.rawPose;
9241
+ this.pose = exactHit.pose;
9242
+ return;
9243
+ }
9244
+ const steps = Math.floor(time / this.seekStep);
9245
+ while (cache.frontierSteps < steps) {
9246
+ const raw = cache.frontier.step(this.seekStep);
9247
+ cache.frontierSteps++;
9248
+ if (cache.frontierSteps <= SEEK_FINE_STEP_LIMIT || cache.frontierSteps % SEEK_COARSE_STRIDE === 0) {
9249
+ cache.grid.set(cache.frontierSteps, {
9250
+ state: captureStepperState(cache.frontier),
9251
+ rawPose: raw
9252
+ });
9253
+ }
9254
+ }
9255
+ const gridHit = cache.grid.get(steps);
9256
+ if (gridHit !== void 0) {
9257
+ restoreStepperState(stepper, gridHit.state);
9258
+ this.rawPose = gridHit.rawPose;
9259
+ } else {
9260
+ const baseStep = steps - steps % SEEK_COARSE_STRIDE;
9261
+ const baseSnap = cache.grid.get(baseStep);
9262
+ restoreStepperState(stepper, baseSnap.state);
9263
+ this.rawPose = baseSnap.rawPose;
9264
+ for (let i = baseStep; i < steps; i++) this.rawPose = stepper.step(this.seekStep);
9265
+ }
9266
+ const rest = time - stepper.trackTime;
9267
+ if (rest > 0) this.rawPose = stepper.step(rest);
9268
+ if (this.rawPose !== null) this.setRawPose(this.rawPose);
9269
+ if (this.rawPose !== null && this.pose !== null) {
9270
+ if (cache.exact.size >= SEEK_EXACT_LIMIT) {
9271
+ const oldest = cache.exact.keys().next().value;
9272
+ if (oldest !== void 0) cache.exact.delete(oldest);
9273
+ }
9274
+ cache.exact.set(time, {
9275
+ state: captureStepperState(stepper),
9276
+ rawPose: this.rawPose,
9277
+ pose: this.pose
9278
+ });
9279
+ }
9280
+ }
9281
+ clearSeekCache() {
9282
+ this.seekCache = null;
9283
+ }
9284
+ ensureSeekCache() {
9285
+ const mode = this.tracks !== null ? "tracks" : "track";
9286
+ if (this.seekCache !== null && this.seekCache.mode === mode) {
9287
+ return this.seekCache;
9288
+ }
9289
+ if (this.tracks === null && this.track === null) {
9290
+ this.reopen();
9291
+ }
9292
+ let frontier;
9293
+ let firstPose;
9294
+ if (mode === "tracks") {
9295
+ const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9296
+ tracks.setAnimation(0, this.animation, { loop: this.loop, mixDuration: 0 });
9297
+ firstPose = tracks.start();
9298
+ this.applyEnvironment(tracks);
9299
+ frontier = tracks;
9300
+ } else {
9301
+ const track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9302
+ firstPose = track.first;
9303
+ this.applyEnvironment(track);
9304
+ frontier = track;
9305
+ }
9306
+ const grid = /* @__PURE__ */ new Map();
9307
+ grid.set(0, { state: captureStepperState(frontier), rawPose: firstPose });
9308
+ this.seekCache = {
9309
+ mode,
9310
+ frontier,
9311
+ frontierSteps: 0,
9312
+ grid,
9313
+ exact: /* @__PURE__ */ new Map()
9314
+ };
9315
+ return this.seekCache;
9118
9316
  }
9119
9317
  /** Whatever is open, opened again with track 0's animation as `play` last set it, at pose 0: the single track, or the tracks under `tracks: true`. */
9120
9318
  reopen() {
@@ -9217,6 +9415,7 @@ class RigPlayer {
9217
9415
  this.source = frameSource(view, this.read.parsed);
9218
9416
  this.skinName = name;
9219
9417
  this.setup = null;
9418
+ this.clearSeekCache();
9220
9419
  if (this.animation !== null && (this.track !== null || this.tracks !== null)) {
9221
9420
  const time = this.time;
9222
9421
  this.reopen();
@@ -9259,6 +9458,7 @@ class RigPlayer {
9259
9458
  }
9260
9459
  if (name === null) {
9261
9460
  this.activeSet = null;
9461
+ this.clearSeekCache();
9262
9462
  const base2 = this.rawPose ?? (this.setup ?? (this.setup = poseRawSetup(this.view)));
9263
9463
  this.rawPose = base2;
9264
9464
  this.pose = base2;
@@ -9271,6 +9471,7 @@ class RigPlayer {
9271
9471
  );
9272
9472
  }
9273
9473
  this.activeSet = found;
9474
+ this.clearSeekCache();
9274
9475
  const base = this.rawPose ?? (this.setup ?? (this.setup = poseRawSetup(this.view)));
9275
9476
  this.rawPose = base;
9276
9477
  this.pose = applySetToPose(this.view, base, found);
@@ -9320,6 +9521,7 @@ class RigPlayer {
9320
9521
  set adjust(fn) {
9321
9522
  if (fn !== null && typeof fn !== "function") throw new Error(`adjust must be a function or null, got ${typeof fn}`);
9322
9523
  this.adjustFn = fn;
9524
+ this.clearSeekCache();
9323
9525
  this.repose();
9324
9526
  }
9325
9527
  /**
@@ -9330,6 +9532,7 @@ class RigPlayer {
9330
9532
  * changed, then `draw`. Nothing before `play`.
9331
9533
  */
9332
9534
  repose() {
9535
+ this.clearSeekCache();
9333
9536
  const stepper = this.tracks ?? this.track;
9334
9537
  if (stepper === null || this.pose === null) return;
9335
9538
  this.setRawPose(stepper.step(0));
@@ -9422,6 +9625,7 @@ class RigPlayer {
9422
9625
  /** Removes the renderer's elements and hands its GPU pages back. The assets stay the caller's. */
9423
9626
  dispose() {
9424
9627
  this.renderer.dispose();
9628
+ this.clearSeekCache();
9425
9629
  this.tracks = null;
9426
9630
  this.track = null;
9427
9631
  this.pose = null;