@rig-c/play 1.16.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -106,6 +106,7 @@ declare class MeshGlBlitter {
106
106
  private readonly canvas;
107
107
  private readonly gl;
108
108
  private readonly uResolution;
109
+ private readonly uOrigin;
109
110
  private readonly meshProgram;
110
111
  private readonly meshPos;
111
112
  private readonly meshUV;
@@ -1 +1 @@
1
- {"version":3,"file":"MeshGlBlitter.d.ts","sourceRoot":"","sources":["../src/MeshGlBlitter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MeshGlBlitter.d.ts","sourceRoot":"","sources":["../src/MeshGlBlitter.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -6790,11 +6790,12 @@ const VERTEX_SHADER = `
6790
6790
  attribute vec2 aPos;
6791
6791
  attribute vec2 aUV;
6792
6792
  uniform vec2 uResolution;
6793
+ uniform vec2 uOrigin;
6793
6794
  varying vec2 vUV;
6794
6795
  void main() {
6795
6796
  // aPos is in offscreen-canvas device px, top-left origin (matching the
6796
6797
  // top-left-origin drawImage read of the same rect).
6797
- vec2 clip = aPos / uResolution * 2.0 - 1.0;
6798
+ vec2 clip = (aPos - uOrigin) / uResolution * 2.0 - 1.0;
6798
6799
  gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
6799
6800
  vUV = aUV;
6800
6801
  }`;
@@ -6818,6 +6819,7 @@ class MeshGlBlitter {
6818
6819
  canvas = document.createElement("canvas");
6819
6820
  gl;
6820
6821
  uResolution;
6822
+ uOrigin;
6821
6823
  meshProgram;
6822
6824
  meshPos;
6823
6825
  meshUV;
@@ -6860,8 +6862,10 @@ class MeshGlBlitter {
6860
6862
  this.meshProgram = program;
6861
6863
  gl.useProgram(program);
6862
6864
  const uResolution = gl.getUniformLocation(program, "uResolution");
6863
- if (!uResolution) throw new Error("uResolution not found");
6865
+ const uOrigin = gl.getUniformLocation(program, "uOrigin");
6866
+ if (!uResolution || !uOrigin) throw new Error("mesh program uniforms not found");
6864
6867
  this.uResolution = uResolution;
6868
+ this.uOrigin = uOrigin;
6865
6869
  gl.uniform1i(gl.getUniformLocation(program, "uTex"), 0);
6866
6870
  this.meshPos = gl.getAttribLocation(program, "aPos");
6867
6871
  this.meshUV = gl.getAttribLocation(program, "aUV");
@@ -6924,11 +6928,8 @@ class MeshGlBlitter {
6924
6928
  Math.ceil(Math.max(height, this.canvas.height) / GROW_STEP) * GROW_STEP
6925
6929
  );
6926
6930
  }
6927
- const bufW = this.canvas.width;
6928
6931
  const bufH = this.canvas.height;
6929
- gl.viewport(0, 0, bufW, bufH);
6930
6932
  this.bindMesh();
6931
- gl.uniform2f(this.uResolution, bufW, bufH);
6932
6933
  let floats = 0;
6933
6934
  for (const job of jobs) floats += job.triangles.length * 4;
6934
6935
  if (this.vertexData.length < floats) {
@@ -6957,6 +6958,9 @@ class MeshGlBlitter {
6957
6958
  gl.bindTexture(gl.TEXTURE_2D, this.textureFor(job.page, job.pma));
6958
6959
  boundPage = job.page;
6959
6960
  }
6961
+ gl.viewport(packX[i], bufH - packY[i] - job.height, job.width, job.height);
6962
+ gl.uniform2f(this.uResolution, job.width, job.height);
6963
+ gl.uniform2f(this.uOrigin, packX[i], packY[i]);
6960
6964
  gl.scissor(packX[i], bufH - packY[i] - job.height, job.width, job.height);
6961
6965
  gl.clear(gl.COLOR_BUFFER_BIT);
6962
6966
  gl.drawArrays(gl.TRIANGLES, first, job.triangles.length);
@@ -8579,7 +8583,7 @@ function readSetupMesh(assets, target) {
8579
8583
  return readSetupMesh$1(underNoSkin(assets.document.doc), target);
8580
8584
  }
8581
8585
  const SETS_SPECS = ["spine-parts-sets/1", "rig-parts-sets/1"];
8582
- const SET_KEYS_NOT_POSED = ["drawnLength", "registration", "threshold"];
8586
+ const SET_KEYS_NOT_POSED = ["drawnLength", "registration", "threshold", "focus"];
8583
8587
  const SET_KEYS = ["slots", "hides", "drawn", ...SET_KEYS_NOT_POSED];
8584
8588
  const SETS_DOCUMENT_KEYS = ["spec", "sets", "threshold"];
8585
8589
  function isRecord(value) {
@@ -8701,6 +8705,11 @@ function readSetsText(text, where, doc) {
8701
8705
  drawn[boneName] = degrees;
8702
8706
  }
8703
8707
  }
8708
+ if (item.focus !== void 0) {
8709
+ if (!Array.isArray(item.focus) || item.focus.length !== 2 || typeof item.focus[0] !== "number" || !Number.isFinite(item.focus[0]) || typeof item.focus[1] !== "number" || !Number.isFinite(item.focus[1])) {
8710
+ throw new Error(`${where}: set "${name}" "focus" must be [x, y] of two finite numbers`);
8711
+ }
8712
+ }
8704
8713
  out.push(
8705
8714
  Object.freeze({
8706
8715
  name,
@@ -8962,6 +8971,20 @@ function frameOf(source, pose, color = [1, 1, 1, 1], draws = () => true) {
8962
8971
  return { order };
8963
8972
  }
8964
8973
  const DEFAULT_ENVIRONMENT = Object.freeze({ wind: Object.freeze([1, 0]), gravity: Object.freeze([0, 1]) });
8974
+ function setEaseOption(call, options) {
8975
+ const ease = options?.ease;
8976
+ if (ease === void 0) return 0;
8977
+ if (typeof ease !== "number" || !Number.isFinite(ease) || ease < 0) {
8978
+ throw new Error(`${call}: options.ease must be a finite number of seconds at or above 0, got ${typeof ease === "number" ? String(ease) : JSON.stringify(ease)}`);
8979
+ }
8980
+ return ease;
8981
+ }
8982
+ function easeProgress(ease) {
8983
+ return Math.min(Math.max(ease.elapsed / ease.duration, 0), 1);
8984
+ }
8985
+ function smoothstep(u) {
8986
+ return u * u * (3 - 2 * u);
8987
+ }
8965
8988
  function setPhysicsOption(call, options) {
8966
8989
  if (options === void 0) return void 0;
8967
8990
  if (options === null || typeof options !== "object") {
@@ -9285,6 +9308,12 @@ class RigPlayer {
9285
9308
  drawnPose = null;
9286
9309
  /** Test-only (#173's planted control): `carry` maps every state through the identity instead of the anchor's turn. Not API. */
9287
9310
  carryPlant = false;
9311
+ /** An eased set switch in flight (#174), or null. */
9312
+ easing = null;
9313
+ /** The last positive step `advance` took: what predicts the next one when an ease starts. */
9314
+ lastStep = 0;
9315
+ /** Test-only (#174's planted controls): swap the attachments at u = 0, or ease by w = 1.2u (1 at u = 1). Not API. */
9316
+ easePlant = null;
9288
9317
  adjustFn = null;
9289
9318
  /** Current preview patches, reapplied through the public core API on replay; no edit history. */
9290
9319
  physicsPatches = /* @__PURE__ */ new Map();
@@ -9295,7 +9324,8 @@ class RigPlayer {
9295
9324
  draws = () => true;
9296
9325
  /** Handed to every track the player opens; reads the current `adjust` so setting it later needs no reopen. */
9297
9326
  adjustHook = (locals, ctx) => {
9298
- if (this.drawnPose !== null) for (const [bone2, rotation] of this.drawnPose) locals.bone(bone2).rotation = rotation;
9327
+ if (this.easing !== null) this.applyEase(this.easing, locals);
9328
+ else if (this.drawnPose !== null) for (const [bone2, rotation] of this.drawnPose) locals.bone(bone2).rotation = rotation;
9299
9329
  if (this.adjustFn !== null) this.adjustFn(locals, ctx);
9300
9330
  };
9301
9331
  listeners = {
@@ -9504,11 +9534,12 @@ class RigPlayer {
9504
9534
  if (time > 0) this.stepTo(time);
9505
9535
  return tracks;
9506
9536
  }
9507
- /** Stores `raw` and applies `activeSet` when one is active. Zero work when `activeSet === null`. */
9537
+ /** Stores `raw` and applies the shown set (`activeSet`, or before an ease's swap the composition it eases from). Zero work when none. */
9508
9538
  setRawPose(raw) {
9509
9539
  this.rawPose = raw;
9510
9540
  this.heldParameters = null;
9511
- this.pose = this.activeSet === null ? raw : applySetToPose(this.view, raw, this.activeSet);
9541
+ const set = this.shownSet();
9542
+ this.pose = set === null ? raw : applySetToPose(this.view, raw, set);
9512
9543
  this.poseGeneration++;
9513
9544
  this.cachedIdentity = null;
9514
9545
  }
@@ -9626,6 +9657,7 @@ class RigPlayer {
9626
9657
  if (this.animation === null) throw new Error("seek before play: no animation is open");
9627
9658
  if (!(time >= 0) || !Number.isFinite(time)) throw new Error(`seek time must be a finite time at or above 0, got ${String(time)}`);
9628
9659
  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");
9660
+ this.easing = null;
9629
9661
  const cache = this.ensureSeekCache();
9630
9662
  const stepper = this.tracks ?? this.track;
9631
9663
  const exactHit = cache.exact.get(time);
@@ -9781,6 +9813,8 @@ class RigPlayer {
9781
9813
  if (this.pose === null) return;
9782
9814
  const step = dt * this.timeScale;
9783
9815
  if (this.paused || !(step > 0)) return;
9816
+ this.lastStep = step;
9817
+ if (this.easing !== null) this.advanceEase(this.easing, step);
9784
9818
  const tracks = this.tracks;
9785
9819
  if (tracks !== null) {
9786
9820
  const before2 = [];
@@ -9823,6 +9857,7 @@ class RigPlayer {
9823
9857
  this.source = frameSource(view, this.read.parsed);
9824
9858
  this.skinName = name;
9825
9859
  this.setup = null;
9860
+ this.easing = null;
9826
9861
  this.clearSeekCache();
9827
9862
  if (this.animation !== null && (this.track !== null || this.tracks !== null)) {
9828
9863
  const time = this.time;
@@ -9881,7 +9916,8 @@ class RigPlayer {
9881
9916
  throw new Error(`cannot setSet(${JSON.stringify(name)}): this model has no sets.json`);
9882
9917
  }
9883
9918
  const physics = setPhysicsOption("setSet", options);
9884
- this.activate(name === null ? [] : [this.namedSet(name)], physics);
9919
+ const ease = setEaseOption("setSet", options);
9920
+ this.activate(name === null ? [] : [this.namedSet(name)], physics, ease);
9885
9921
  }
9886
9922
  /**
9887
9923
  * Makes exactly `names` the active attachment sets (#168), together: each
@@ -9903,19 +9939,42 @@ class RigPlayer {
9903
9939
  * under a re-posed bone. A later `seek` replays from the start with the sets
9904
9940
  * then active and does not reproduce a carried switch. `setSet`, `addSet`
9905
9941
  * and `removeSet` take the same option.
9942
+ *
9943
+ * `options.ease` (#174, seconds, a finite number at or above 0) eases the
9944
+ * bones the call re-poses instead of snapping them. Each bone whose drawn
9945
+ * rotation the call adds, removes or changes turns from what it shows to
9946
+ * its new local rotation along the shorter way, as
9947
+ * `start + shortestΔ(start, end) · smoothstep(elapsed / ease)`: going on,
9948
+ * from the clip's value at the time to the drawn local; going off, from the
9949
+ * drawn local to the clip's value. The attachments the pose draws stay the
9950
+ * previous ones until the frame whose `elapsed / ease` is nearest ½ (the
9951
+ * next step predicted from the current one; a tie goes to the later frame),
9952
+ * then become the call's; `activeSets` and `slotIdentity().sets` name the
9953
+ * call's sets at once, while `slotIdentity().slots` follows what is drawn.
9954
+ * On the frame the elapsed time reaches `ease` the player is in exactly the
9955
+ * instant switch's state. The elapsed time is the player's step
9956
+ * (`dt × timeScale`), so a paused player or a zero step holds the ease;
9957
+ * before `play`, or when no bone's rotation changes, the switch is instant.
9958
+ * A call during an ease eases again from the angles shown (a call without
9959
+ * `ease` lands it at once), and `seek` or a skin change lands it at once.
9960
+ * With `physics: 'carry'` the chains under the eased bones are carried on
9961
+ * every eased frame, by the turn that frame's blend makes; with `'reset'`
9962
+ * they restart once, at the call. `0` or absent is the instant switch on
9963
+ * the same code path; anything else throws by name before anything changes.
9906
9964
  */
9907
9965
  setSets(names, options) {
9908
9966
  if (this.setList.length === 0) {
9909
9967
  throw new Error(`cannot setSets(${JSON.stringify(names)}): this model has no sets.json`);
9910
9968
  }
9911
9969
  const physics = setPhysicsOption("setSets", options);
9970
+ const ease = setEaseOption("setSets", options);
9912
9971
  const sets = names.map((name) => this.namedSet(name));
9913
9972
  for (let i = 0; i < sets.length; i++) {
9914
9973
  for (let j = 0; j < i; j++) {
9915
9974
  if (sets[i] === sets[j]) throw new Error(`set ${JSON.stringify(sets[i].name)} is named twice in setSets`);
9916
9975
  }
9917
9976
  }
9918
- this.activate(sets, physics);
9977
+ this.activate(sets, physics, ease);
9919
9978
  }
9920
9979
  /**
9921
9980
  * Activates `name` beside the sets already active (#168): a set already
@@ -9927,9 +9986,10 @@ class RigPlayer {
9927
9986
  throw new Error(`cannot addSet(${JSON.stringify(name)}): this model has no sets.json`);
9928
9987
  }
9929
9988
  const physics = setPhysicsOption("addSet", options);
9989
+ const ease = setEaseOption("addSet", options);
9930
9990
  const found = this.namedSet(name);
9931
9991
  if (this.activeList.includes(found)) return;
9932
- this.activate([...this.activeList, found], physics);
9992
+ this.activate([...this.activeList, found], physics, ease);
9933
9993
  }
9934
9994
  /**
9935
9995
  * Deactivates `name`, keeping every other active set (#168). An unknown name
@@ -9940,9 +10000,10 @@ class RigPlayer {
9940
10000
  throw new Error(`cannot removeSet(${JSON.stringify(name)}): this model has no sets.json`);
9941
10001
  }
9942
10002
  const physics = setPhysicsOption("removeSet", options);
10003
+ const ease = setEaseOption("removeSet", options);
9943
10004
  const found = this.namedSet(name);
9944
10005
  if (!this.activeList.includes(found)) return;
9945
- this.activate(this.activeList.filter((s) => s !== found), physics);
10006
+ this.activate(this.activeList.filter((s) => s !== found), physics, ease);
9946
10007
  }
9947
10008
  /** The declared set named `name`, or a throw naming it and the available sets. */
9948
10009
  namedSet(name) {
@@ -9990,16 +10051,16 @@ class RigPlayer {
9990
10051
  }
9991
10052
  /**
9992
10053
  * The physics under the bones a set switch re-poses (#173), before the
9993
- * repose step: the drawn rotations went from `before` to `after` (already
9994
- * `drawnPose`), and every physics constraint whose bone is one of the
10054
+ * repose step: the `changed` bones' rotations went from what the held pose
10055
+ * shows to what the player now applies (`drawnPose`, or an ease's next
10056
+ * blend, #174), and every physics constraint whose bone is one of the
9995
10057
  * changed bones or a descendant of one is carried or reset. Nothing to do
9996
10058
  * before `play` (the setup pose carries no physics state) or when no
9997
10059
  * constraint hangs under a changed bone.
9998
10060
  */
9999
- handOverPhysics(before, after, mode) {
10061
+ handOverPhysics(changed, mode) {
10000
10062
  const stepper = this.tracks ?? this.track;
10001
10063
  if (stepper === null || this.pose === null || this.rawPose === null) return;
10002
- const changed = drawnChanged(before, after);
10003
10064
  if (changed.size === 0) return;
10004
10065
  const parentOf = new Map(this.view.bones.map((b) => [b.name, b.parent]));
10005
10066
  const anchorOf = (bone2) => {
@@ -10079,11 +10140,112 @@ class RigPlayer {
10079
10140
  s.ty = c * tx + d * s.ty;
10080
10141
  }
10081
10142
  }
10143
+ /**
10144
+ * The composition the held pose draws: `activeSet`, except before an ease's
10145
+ * swap (#174), when it is the composition the ease started from.
10146
+ */
10147
+ shownSet() {
10148
+ const easing = this.easing;
10149
+ return easing === null || easing.swapped ? this.activeSet : easing.fromSet;
10150
+ }
10151
+ /** The local an eased bone shows at the ease's current progress, given the clip's local `clip`. */
10152
+ easedLocal(easing, from, to, clip) {
10153
+ const start = from ?? clip;
10154
+ const end = to ?? clip;
10155
+ const u = easeProgress(easing);
10156
+ const w = this.easePlant === "overshoot" ? u < 1 ? 1.2 * u : 1 : smoothstep(u);
10157
+ return start + normaliseDegrees(end - start) * w;
10158
+ }
10159
+ /**
10160
+ * The ease's part of `adjustHook` (#174): every drawn bone the ease does not
10161
+ * turn takes its drawn local, as without an ease; every eased bone reads
10162
+ * the clip's local (what the timelines left) and then takes its blend.
10163
+ */
10164
+ applyEase(easing, locals) {
10165
+ for (const bone2 of easing.bones.keys()) easing.clip.set(bone2, locals.bone(bone2).rotation);
10166
+ if (this.drawnPose !== null) {
10167
+ for (const [bone2, rotation] of this.drawnPose) if (!easing.bones.has(bone2)) locals.bone(bone2).rotation = rotation;
10168
+ }
10169
+ for (const [bone2, { from, to }] of easing.bones) {
10170
+ locals.bone(bone2).rotation = this.easedLocal(easing, from, to, easing.clip.get(bone2));
10171
+ }
10172
+ }
10173
+ /**
10174
+ * Starts an eased switch (#174) to the sets already made active, from what
10175
+ * the held pose shows: an eased bone's start is the local it shows (fixed),
10176
+ * or the clip's value for a bone that showed the clip's; its end is the new
10177
+ * drawn local, or the clip's value for a bone whose set went. Returns false,
10178
+ * changing nothing, when there is nothing to ease — before `play` (no step
10179
+ * to ease over) or when no bone's rotation changes — and the call is then
10180
+ * an instant one.
10181
+ */
10182
+ startEase(duration, shownBefore, drawn, physics) {
10183
+ const stepper = this.tracks ?? this.track;
10184
+ if (stepper === null || this.pose === null) return false;
10185
+ const before = this.drawnPose;
10186
+ const after = drawn === void 0 ? null : drawnLocals(this.view, drawn);
10187
+ const previous = this.easing;
10188
+ const candidates = drawnChanged(before, after);
10189
+ if (previous !== null) for (const bone2 of previous.bones.keys()) candidates.add(bone2);
10190
+ const bones = /* @__PURE__ */ new Map();
10191
+ for (const bone2 of candidates) {
10192
+ const to = after?.get(bone2) ?? null;
10193
+ let from;
10194
+ const eased = previous?.bones.get(bone2);
10195
+ if (previous !== null && eased !== void 0) {
10196
+ from = previous.elapsed > 0 ? this.easedLocal(previous, eased.from, eased.to, previous.clip.get(bone2)) : eased.from;
10197
+ } else {
10198
+ from = before?.get(bone2) ?? null;
10199
+ }
10200
+ if (from === to) continue;
10201
+ bones.set(bone2, { from, to });
10202
+ }
10203
+ if (bones.size === 0) return false;
10204
+ const easing = {
10205
+ duration,
10206
+ elapsed: 0,
10207
+ bones,
10208
+ clip: /* @__PURE__ */ new Map(),
10209
+ fromSet: shownBefore,
10210
+ swapped: false,
10211
+ physics
10212
+ };
10213
+ if (this.easePlant === "swap-at-0" || this.swapsNow(0, this.lastStep / duration)) easing.swapped = true;
10214
+ this.easing = easing;
10215
+ this.drawnPose = after;
10216
+ if (physics === "reset") this.handOverPhysics(new Set(bones.keys()), "reset");
10217
+ this.reposeDrawn();
10218
+ return true;
10219
+ }
10220
+ /**
10221
+ * Whether the frame at progress `u` is the one nearest ½, the next frame
10222
+ * being at `next`: it is when `u` reached ½ or the next one would be
10223
+ * farther from it; a tie (within 1e-9) goes to the later frame.
10224
+ */
10225
+ swapsNow(u, next) {
10226
+ if (u >= 0.5) return true;
10227
+ return next > u && 0.5 - u < next - 0.5 - 1e-9;
10228
+ }
10229
+ /**
10230
+ * One step of an eased switch (#174), before the track steps: the elapsed
10231
+ * time advances by the step, the attachments swap when this is the frame
10232
+ * nearest ½ (`swapsNow`, the next step predicted by this one), and the
10233
+ * ease ends — the instant switch's state, `drawnPose` alone — on the frame
10234
+ * it reaches its duration. With `physics: 'carry'`, the chains under the
10235
+ * eased bones are carried by the turn this frame's blend makes, read by a
10236
+ * trial `step(0)` at the previous time (`handOverPhysics`).
10237
+ */
10238
+ advanceEase(easing, step) {
10239
+ easing.elapsed += step;
10240
+ if (easing.elapsed >= easing.duration) this.easing = null;
10241
+ else if (!easing.swapped && this.swapsNow(easing.elapsed / easing.duration, (easing.elapsed + step) / easing.duration)) easing.swapped = true;
10242
+ if (easing.physics === "carry") this.handOverPhysics(new Set(easing.bones.keys()), "carry");
10243
+ }
10082
10244
  /**
10083
10245
  * Makes `sets` (distinct) the active sets: refuses any two touching one slot
10084
10246
  * before changing anything, then applies their composition to the held pose.
10085
10247
  */
10086
- activate(sets, physics) {
10248
+ activate(sets, physics, ease = 0) {
10087
10249
  for (let i = 0; i < sets.length; i++) {
10088
10250
  for (let j = 0; j < i; j++) {
10089
10251
  const other = this.touched(sets[j]);
@@ -10105,14 +10267,27 @@ class RigPlayer {
10105
10267
  }
10106
10268
  }
10107
10269
  const drawnBefore = this.activeSet?.drawn;
10270
+ const shownBefore = this.shownSet();
10108
10271
  this.activeList = this.setList.filter((s) => sets.includes(s));
10109
10272
  this.activeSet = composeSets(this.activeList);
10110
10273
  this.clearSeekCache();
10111
10274
  const drawn = this.activeSet?.drawn;
10275
+ if (ease > 0 && this.startEase(ease, shownBefore, drawn, physics)) return;
10276
+ const easing = this.easing;
10277
+ if (easing !== null) {
10278
+ this.easing = null;
10279
+ const posedBefore = this.drawnPose;
10280
+ this.drawnPose = drawn === void 0 ? null : drawnLocals(this.view, drawn);
10281
+ const changed = drawnChanged(posedBefore, this.drawnPose);
10282
+ for (const bone2 of easing.bones.keys()) changed.add(bone2);
10283
+ if (physics !== void 0) this.handOverPhysics(changed, physics);
10284
+ this.reposeDrawn();
10285
+ return;
10286
+ }
10112
10287
  if (drawn !== drawnBefore && !(drawn === void 0 && drawnBefore === void 0)) {
10113
10288
  const posedBefore = this.drawnPose;
10114
10289
  this.drawnPose = drawn === void 0 ? null : drawnLocals(this.view, drawn);
10115
- if (physics !== void 0) this.handOverPhysics(posedBefore, this.drawnPose, physics);
10290
+ if (physics !== void 0) this.handOverPhysics(drawnChanged(posedBefore, this.drawnPose), physics);
10116
10291
  this.reposeDrawn();
10117
10292
  return;
10118
10293
  }
@@ -10304,9 +10479,10 @@ class RigPlayer {
10304
10479
  const setupShownSlots = new Set(setupPose.shown.map((s) => s.slot));
10305
10480
  const rawRowBySlot = new Map(raw.slots.map((r) => [r[0], r]));
10306
10481
  const slots = [];
10482
+ const shownSet = this.shownSet();
10307
10483
  for (let z = 0; z < pose.drawOrder.length; z++) {
10308
10484
  const slotName = pose.drawOrder[z];
10309
- if (this.activeSet !== null && hiddenBySet(slotName, this.activeSet)) {
10485
+ if (shownSet !== null && hiddenBySet(slotName, shownSet)) {
10310
10486
  slots.push(Object.freeze({
10311
10487
  slot: slotName,
10312
10488
  z,
@@ -10341,7 +10517,7 @@ class RigPlayer {
10341
10517
  continue;
10342
10518
  }
10343
10519
  const rawRow = rawRowBySlot.get(slotName);
10344
- const overriddenBySet = this.activeSet !== null && slotName in this.activeSet.slots;
10520
+ const overriddenBySet = shownSet !== null && slotName in shownSet.slots;
10345
10521
  const setupHadAttachment = setupShownSlots.has(slotName);
10346
10522
  const isTimelineNull = !overriddenBySet && (setupHadAttachment && rawRow?.[1] === null || this.isSlotKeyedNullByTimeline(slotName, raw.animationTime));
10347
10523
  slots.push(Object.freeze({
@@ -10406,6 +10582,7 @@ class RigPlayer {
10406
10582
  this.renderer.dispose();
10407
10583
  this.clearSeekCache();
10408
10584
  this.cachedIdentity = null;
10585
+ this.easing = null;
10409
10586
  this.tracks = null;
10410
10587
  this.track = null;
10411
10588
  this.pose = null;