spine-html 1.11.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -520,6 +520,52 @@ active (`null`), track evaluation runs with zero extra work and produces
520
520
  byte-identical poses. An unknown set name throws by name; on a build without
521
521
  `sets.json`, `sets()` is empty (`[]`) and `setSet` throws.
522
522
 
523
+ ### Slot identity snapshot (`slotIdentity()`)
524
+
525
+ To inspect which attachment key, owning skin and visibility state each slot
526
+ holds on the current frame — for a parts explorer or review overlay — without
527
+ `seek`, `repose`, or restarting physics (#144):
528
+
529
+ ```ts
530
+ const snap = player.slotIdentity();
531
+ // {
532
+ // generation: number, // matches player.generation and player.posedFrame().generation
533
+ // set: string | null, // active set name (player.set)
534
+ // skin: string | null, // active skin name (player.skin ?? null)
535
+ // slots: readonly [ // every slot in posed draw order (z = 0 .. slots.length - 1)
536
+ // {
537
+ // slot: string,
538
+ // z: number,
539
+ // attachment: string | null, // resolved attachment key when drawn, null when not drawn
540
+ // skin: string | null, // skin owning the resolved attachment ('default' or named skin), or null
541
+ // reason: 'hidden' | 'no-attachment' | 'timeline-null' | 'set-hide' | null,
542
+ // },
543
+ // ...
544
+ // ],
545
+ // }
546
+ ```
547
+
548
+ Every slot in the posed draw order (`z = 0 .. slots.length - 1`, hidden and null
549
+ ones included) appears in `snap.slots`:
550
+
551
+ - Drawn slots (`reason: null`) appear in the same `z` order as
552
+ `player.posedFrame().order`, naming the resolved attachment key (`attachment`)
553
+ and the skin that owns it (`skin`).
554
+ - Non-drawn slots report `attachment: null`, `skin: null`, and why they are
555
+ absent from `posedFrame().order`:
556
+ - `'set-hide'` — hidden by the active attachment set (`set.hides` or a
557
+ `<base>_still` companion of a slot the set covers or hides).
558
+ - `'timeline-null'` — cleared by an active attachment timeline key (`null`).
559
+ - `'no-attachment'` — no attachment in the setup pose or under the active skin
560
+ (or a non-drawable attachment kind such as clipping/boundingbox/path/point).
561
+ - `'hidden'` — filtered out by `player.slots` or on an inactive bone under the
562
+ current skin.
563
+ - `snap.generation` ticks monotonically with `player.generation` and
564
+ `player.posedFrame().generation` whenever the held pose, active set, active
565
+ skin or slot filter changes. The returned snapshot is frozen and cached per
566
+ generation, so reading it repeatedly on a held pose is allocation-free and
567
+ mutates no pixel or physics state.
568
+
523
569
  ### Several tracks
524
570
 
525
571
  A base on track 0, a layer over it, a one-shot played once and mixed back out —
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { type CanvasBackend, CanvasRenderer, type CanvasViewport } from './Canva
2
2
  export { DomRenderer, type MeshBackend, type MeshSourceRect } from './DomRenderer.js';
3
3
  export { type PageLayout, type PageRegion, type RegionEncoding, type RegionImage, revokeRegions, type UnpackRegionsOptions, unpackRegions } from './DomTexture.js';
4
4
  export { type LoadModelOptions, loadModel, type ModelAssets, type ModelDocument, type RegionLoading, type RigSet } from './loadModel.js';
5
- export { type AdjustableBones, type BoneAdjust, type BoneLocals, type BoneWorld, type PlayOptions, type PoseBounds, type QueueOptions, type RenderMode, type RigComplete, type RigEnvironment, type RigEvent, RigPlayer, type RigPlayerOptions, type SlotFilter, type TrackEntry } from './player.js';
5
+ export { type AdjustableBones, type BoneAdjust, type BoneLocals, type BoneWorld, type PlayOptions, type PoseBounds, type QueueOptions, type RenderMode, type RigComplete, type RigEnvironment, type RigEvent, RigPlayer, type RigPlayerOptions, type SlotFilter, type SlotIdentity, type SlotIdentityReason, type SlotIdentitySnapshot, type TrackEntry } from './player.js';
6
6
  export type { BlendMode, PosedClip, PosedFrame, PosedMesh, PosedRegion, PosedSlot, } from './posedFrame.js';
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnK,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACzI,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AACtS,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnK,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACzI,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC7W,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -8699,9 +8699,16 @@ function predicateOf(names) {
8699
8699
  const allowed = new Set(names);
8700
8700
  return (slot) => allowed.has(slot);
8701
8701
  }
8702
+ function hiddenBySet(slotName, set) {
8703
+ if (set.hides.includes(slotName)) return true;
8704
+ if (slotName.endsWith("_still") && !(slotName in set.slots)) {
8705
+ const base = slotName.slice(0, -"_still".length);
8706
+ if (base in set.slots || set.hides.includes(base)) return true;
8707
+ }
8708
+ return false;
8709
+ }
8702
8710
  function applySetToPose(view, raw, set) {
8703
8711
  const coveredNames = new Set(Object.keys(set.slots));
8704
- const hiddenNames = new Set(set.hides);
8705
8712
  const overrideSlots = view.slots.flatMap((slot) => {
8706
8713
  const setup = set.slots[slot.name];
8707
8714
  return setup !== void 0 ? [{ ...slot, setup }] : [];
@@ -8729,14 +8736,7 @@ function applySetToPose(view, raw, set) {
8729
8736
  const rawShownBySlot = new Map(raw.shown.map((s) => [s.slot, s]));
8730
8737
  const rawDrawnBySlot = new Map(raw.drawn.map((d) => [d.slot, d]));
8731
8738
  const rawClipBySlot = new Map(raw.clips.map((c) => [c[0], c]));
8732
- const hiddenSlot = (slotName) => {
8733
- if (hiddenNames.has(slotName)) return true;
8734
- if (slotName.endsWith("_still") && !coveredNames.has(slotName)) {
8735
- const base = slotName.slice(0, -"_still".length);
8736
- if (coveredNames.has(base) || hiddenNames.has(base)) return true;
8737
- }
8738
- return false;
8739
- };
8739
+ const hiddenSlot = (slotName) => hiddenBySet(slotName, set);
8740
8740
  const slots = raw.slots.map((rawRow) => {
8741
8741
  const slotName = rawRow[0];
8742
8742
  if (hiddenSlot(slotName)) {
@@ -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,10 @@ 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;
8945
+ poseGeneration = 0;
8946
+ cachedIdentity = null;
8947
+ cachedIdentityGen = -1;
8834
8948
  setList;
8835
8949
  setsByName;
8836
8950
  activeSet = null;
@@ -8862,6 +8976,8 @@ class RigPlayer {
8862
8976
  this.meshBackend = options.meshBackend;
8863
8977
  this.adjustFn = options.adjust ?? null;
8864
8978
  if (options.slots !== void 0 && options.slots !== null) this.slots = options.slots;
8979
+ this.poseGeneration = 0;
8980
+ this.cachedIdentity = null;
8865
8981
  this.active = this.create(options.mode ?? "dom");
8866
8982
  }
8867
8983
  /**
@@ -8928,6 +9044,7 @@ class RigPlayer {
8928
9044
  const alpha = options.alpha ?? 1;
8929
9045
  const additive = options.additive ?? false;
8930
9046
  this.paused = false;
9047
+ this.clearSeekCache();
8931
9048
  const plain = track === 0 && mix === 0 && alpha === 1 && !additive;
8932
9049
  if (!plain) this.beyondBase = true;
8933
9050
  if (this.tracks === null && plain && !this.trackMode) {
@@ -8956,6 +9073,7 @@ class RigPlayer {
8956
9073
  queue(name, options = {}) {
8957
9074
  const track = options.track ?? 0;
8958
9075
  this.beyondBase = true;
9076
+ this.clearSeekCache();
8959
9077
  const tracks = this.compose();
8960
9078
  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
9079
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8968,6 +9086,7 @@ class RigPlayer {
8968
9086
  */
8969
9087
  clear(track = 0, options = {}) {
8970
9088
  this.beyondBase = true;
9089
+ this.clearSeekCache();
8971
9090
  const tracks = this.compose();
8972
9091
  tracks.setEmptyAnimation(track, options.mix ?? 0);
8973
9092
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8981,6 +9100,7 @@ class RigPlayer {
8981
9100
  */
8982
9101
  queueClear(track = 0, options = {}) {
8983
9102
  this.beyondBase = true;
9103
+ this.clearSeekCache();
8984
9104
  const tracks = this.compose();
8985
9105
  tracks.addEmptyAnimation(track, options.mix ?? 0, options.delay ?? 0);
8986
9106
  this.trackCount = Math.max(this.trackCount, track + 1);
@@ -8995,6 +9115,7 @@ class RigPlayer {
8995
9115
  */
8996
9116
  setAlpha(track, alpha) {
8997
9117
  this.beyondBase = true;
9118
+ this.clearSeekCache();
8998
9119
  const tracks = this.compose();
8999
9120
  tracks.setAlpha(track, alpha);
9000
9121
  this.setRawPose(tracks.step(0));
@@ -9025,6 +9146,7 @@ class RigPlayer {
9025
9146
  */
9026
9147
  compose() {
9027
9148
  if (this.tracks !== null) return this.tracks;
9149
+ this.clearSeekCache();
9028
9150
  const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9029
9151
  const single = this.track;
9030
9152
  const time = single?.trackTime ?? 0;
@@ -9043,6 +9165,8 @@ class RigPlayer {
9043
9165
  setRawPose(raw) {
9044
9166
  this.rawPose = raw;
9045
9167
  this.pose = this.activeSet === null ? raw : applySetToPose(this.view, raw, this.activeSet);
9168
+ this.poseGeneration++;
9169
+ this.cachedIdentity = null;
9046
9170
  }
9047
9171
  /**
9048
9172
  * The world the rig's physics answers (#121): `wind` and `gravity`, as
@@ -9061,8 +9185,10 @@ class RigPlayer {
9061
9185
  environment(environment) {
9062
9186
  if (environment === null) {
9063
9187
  this.env = null;
9188
+ this.clearSeekCache();
9064
9189
  this.stepper()?.setEnvironment(DEFAULT_ENVIRONMENT);
9065
9190
  } else if (environment !== void 0) {
9191
+ this.clearSeekCache();
9066
9192
  const live = this.stepper();
9067
9193
  if (live) {
9068
9194
  live.setEnvironment(environment);
@@ -9101,20 +9227,101 @@ class RigPlayer {
9101
9227
  this.paused = false;
9102
9228
  }
9103
9229
  /**
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.
9230
+ * Moves a single animation on track 0 to `time`: snapshot-backed deterministic
9231
+ * seek (#141) that produces the exact pose and physics state of reopening at 0
9232
+ * and stepping forward in `seekStep` increments (the last one shorter) until
9233
+ * `time` is reached. Events and completes crossed on the way are not fired.
9234
+ * It has a meaning for one animation on one track only — the player cannot
9235
+ * replay when a mix, a second track or a queue was asked for — so after any
9236
+ * of those it refuses by name; step the player forward instead.
9111
9237
  */
9112
9238
  seek(time) {
9113
9239
  if (this.animation === null) throw new Error("seek before play: no animation is open");
9114
9240
  if (!(time >= 0) || !Number.isFinite(time)) throw new Error(`seek time must be a finite time at or above 0, got ${String(time)}`);
9115
9241
  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);
9242
+ const cache = this.ensureSeekCache();
9243
+ const stepper = this.tracks ?? this.track;
9244
+ const exactHit = cache.exact.get(time);
9245
+ if (exactHit !== void 0) {
9246
+ restoreStepperState(stepper, exactHit.state);
9247
+ this.rawPose = exactHit.rawPose;
9248
+ this.pose = exactHit.pose;
9249
+ this.poseGeneration++;
9250
+ this.cachedIdentity = null;
9251
+ return;
9252
+ }
9253
+ const steps = Math.floor(time / this.seekStep);
9254
+ while (cache.frontierSteps < steps) {
9255
+ const raw = cache.frontier.step(this.seekStep);
9256
+ cache.frontierSteps++;
9257
+ if (cache.frontierSteps <= SEEK_FINE_STEP_LIMIT || cache.frontierSteps % SEEK_COARSE_STRIDE === 0) {
9258
+ cache.grid.set(cache.frontierSteps, {
9259
+ state: captureStepperState(cache.frontier),
9260
+ rawPose: raw
9261
+ });
9262
+ }
9263
+ }
9264
+ const gridHit = cache.grid.get(steps);
9265
+ if (gridHit !== void 0) {
9266
+ restoreStepperState(stepper, gridHit.state);
9267
+ this.rawPose = gridHit.rawPose;
9268
+ } else {
9269
+ const baseStep = steps - steps % SEEK_COARSE_STRIDE;
9270
+ const baseSnap = cache.grid.get(baseStep);
9271
+ restoreStepperState(stepper, baseSnap.state);
9272
+ this.rawPose = baseSnap.rawPose;
9273
+ for (let i = baseStep; i < steps; i++) this.rawPose = stepper.step(this.seekStep);
9274
+ }
9275
+ const rest = time - stepper.trackTime;
9276
+ if (rest > 0) this.rawPose = stepper.step(rest);
9277
+ if (this.rawPose !== null) this.setRawPose(this.rawPose);
9278
+ if (this.rawPose !== null && this.pose !== null) {
9279
+ if (cache.exact.size >= SEEK_EXACT_LIMIT) {
9280
+ const oldest = cache.exact.keys().next().value;
9281
+ if (oldest !== void 0) cache.exact.delete(oldest);
9282
+ }
9283
+ cache.exact.set(time, {
9284
+ state: captureStepperState(stepper),
9285
+ rawPose: this.rawPose,
9286
+ pose: this.pose
9287
+ });
9288
+ }
9289
+ }
9290
+ clearSeekCache() {
9291
+ this.seekCache = null;
9292
+ }
9293
+ ensureSeekCache() {
9294
+ const mode = this.tracks !== null ? "tracks" : "track";
9295
+ if (this.seekCache !== null && this.seekCache.mode === mode) {
9296
+ return this.seekCache;
9297
+ }
9298
+ if (this.tracks === null && this.track === null) {
9299
+ this.reopen();
9300
+ }
9301
+ let frontier;
9302
+ let firstPose;
9303
+ if (mode === "tracks") {
9304
+ const tracks = openTracks(this.view, { reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9305
+ tracks.setAnimation(0, this.animation, { loop: this.loop, mixDuration: 0 });
9306
+ firstPose = tracks.start();
9307
+ this.applyEnvironment(tracks);
9308
+ frontier = tracks;
9309
+ } else {
9310
+ const track = openTrack(this.view, this.animation, { loop: this.loop, reset: "animation", adjust: this.adjustHook, clock: physicsClockStep });
9311
+ firstPose = track.first;
9312
+ this.applyEnvironment(track);
9313
+ frontier = track;
9314
+ }
9315
+ const grid = /* @__PURE__ */ new Map();
9316
+ grid.set(0, { state: captureStepperState(frontier), rawPose: firstPose });
9317
+ this.seekCache = {
9318
+ mode,
9319
+ frontier,
9320
+ frontierSteps: 0,
9321
+ grid,
9322
+ exact: /* @__PURE__ */ new Map()
9323
+ };
9324
+ return this.seekCache;
9118
9325
  }
9119
9326
  /** 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
9327
  reopen() {
@@ -9217,6 +9424,7 @@ class RigPlayer {
9217
9424
  this.source = frameSource(view, this.read.parsed);
9218
9425
  this.skinName = name;
9219
9426
  this.setup = null;
9427
+ this.clearSeekCache();
9220
9428
  if (this.animation !== null && (this.track !== null || this.tracks !== null)) {
9221
9429
  const time = this.time;
9222
9430
  this.reopen();
@@ -9224,6 +9432,9 @@ class RigPlayer {
9224
9432
  } else if (this.rawPose !== null) {
9225
9433
  this.setup = poseRawSetup(this.view);
9226
9434
  this.setRawPose(this.setup);
9435
+ } else {
9436
+ this.poseGeneration++;
9437
+ this.cachedIdentity = null;
9227
9438
  }
9228
9439
  }
9229
9440
  /**
@@ -9259,9 +9470,12 @@ class RigPlayer {
9259
9470
  }
9260
9471
  if (name === null) {
9261
9472
  this.activeSet = null;
9473
+ this.clearSeekCache();
9262
9474
  const base2 = this.rawPose ?? (this.setup ?? (this.setup = poseRawSetup(this.view)));
9263
9475
  this.rawPose = base2;
9264
9476
  this.pose = base2;
9477
+ this.poseGeneration++;
9478
+ this.cachedIdentity = null;
9265
9479
  return;
9266
9480
  }
9267
9481
  const found = this.setsByName.get(name);
@@ -9271,9 +9485,12 @@ class RigPlayer {
9271
9485
  );
9272
9486
  }
9273
9487
  this.activeSet = found;
9488
+ this.clearSeekCache();
9274
9489
  const base = this.rawPose ?? (this.setup ?? (this.setup = poseRawSetup(this.view)));
9275
9490
  this.rawPose = base;
9276
9491
  this.pose = applySetToPose(this.view, base, found);
9492
+ this.poseGeneration++;
9493
+ this.cachedIdentity = null;
9277
9494
  }
9278
9495
  /**
9279
9496
  * Which slots this player draws (`SlotFilter`), or null for every slot.
@@ -9289,11 +9506,15 @@ class RigPlayer {
9289
9506
  if (filter === null) {
9290
9507
  this.slotFilter = null;
9291
9508
  this.draws = () => true;
9509
+ this.poseGeneration++;
9510
+ this.cachedIdentity = null;
9292
9511
  return;
9293
9512
  }
9294
9513
  if (typeof filter === "function") {
9295
9514
  this.slotFilter = filter;
9296
9515
  this.draws = filter;
9516
+ this.poseGeneration++;
9517
+ this.cachedIdentity = null;
9297
9518
  return;
9298
9519
  }
9299
9520
  if (!Array.isArray(filter)) throw new Error(`slots must be an array of slot names, a function, or null, got ${typeof filter}`);
@@ -9305,6 +9526,8 @@ class RigPlayer {
9305
9526
  const allowed = new Set(filter);
9306
9527
  this.slotFilter = [...filter];
9307
9528
  this.draws = (slot) => allowed.has(slot);
9529
+ this.poseGeneration++;
9530
+ this.cachedIdentity = null;
9308
9531
  }
9309
9532
  /**
9310
9533
  * The per-pose adjustment of bone locals (`BoneAdjust`), or null for none.
@@ -9320,6 +9543,7 @@ class RigPlayer {
9320
9543
  set adjust(fn) {
9321
9544
  if (fn !== null && typeof fn !== "function") throw new Error(`adjust must be a function or null, got ${typeof fn}`);
9322
9545
  this.adjustFn = fn;
9546
+ this.clearSeekCache();
9323
9547
  this.repose();
9324
9548
  }
9325
9549
  /**
@@ -9330,6 +9554,7 @@ class RigPlayer {
9330
9554
  * changed, then `draw`. Nothing before `play`.
9331
9555
  */
9332
9556
  repose() {
9557
+ this.clearSeekCache();
9333
9558
  const stepper = this.tracks ?? this.track;
9334
9559
  if (stepper === null || this.pose === null) return;
9335
9560
  this.setRawPose(stepper.step(0));
@@ -9407,6 +9632,10 @@ class RigPlayer {
9407
9632
  if (this.setup === null) this.setup = poseRawSetup(this.view);
9408
9633
  return this.activeSet === null ? this.setup : applySetToPose(this.view, this.setup, this.activeSet);
9409
9634
  }
9635
+ /** Monotonic pose generation counter matching `posedFrame().generation` and `slotIdentity().generation`. */
9636
+ get generation() {
9637
+ return this.poseGeneration;
9638
+ }
9410
9639
  /**
9411
9640
  * The pose the player holds, as the frame record the renderer draws — under
9412
9641
  * this player's `slots` filter, or under `slots` given here instead (a
@@ -9415,13 +9644,137 @@ class RigPlayer {
9415
9644
  * posed once).
9416
9645
  */
9417
9646
  posedFrame(slots) {
9418
- if (this.pose === null) return { order: [] };
9419
9647
  const draws = slots === void 0 ? this.draws : slots === null ? () => true : typeof slots === "function" ? slots : predicateOf(slots);
9420
- return frameOf(this.source, this.pose, this.color, draws);
9648
+ const frame2 = this.pose === null ? { order: [] } : frameOf(this.source, this.pose, this.color, draws);
9649
+ Object.defineProperty(frame2, "generation", {
9650
+ value: this.poseGeneration,
9651
+ enumerable: false,
9652
+ configurable: true
9653
+ });
9654
+ return frame2;
9655
+ }
9656
+ /**
9657
+ * A read-only slot identity snapshot coherent with `posedFrame()` (#144):
9658
+ * for every slot in the posed draw order (`z = 0 .. slots.length - 1`, hidden
9659
+ * and null slots included), its resolved attachment key (`string | null`),
9660
+ * the skin that owns that attachment (`string | null`), and `reason` (`null`
9661
+ * when drawn in `posedFrame().order`, or `'hidden' | 'no-attachment' | 'timeline-null' | 'set-hide'`
9662
+ * when not drawn), together with the active `set`, active `skin`, and frame
9663
+ * `generation`. Read off the held pose without `seek`, `repose`, or physics
9664
+ * restart.
9665
+ */
9666
+ slotIdentity() {
9667
+ if (this.cachedIdentity !== null && this.cachedIdentityGen === this.poseGeneration) {
9668
+ return this.cachedIdentity;
9669
+ }
9670
+ const setupPose = this.setup ?? (this.setup = poseRawSetup(this.view));
9671
+ const pose = this.posed();
9672
+ const raw = this.rawPose ?? setupPose;
9673
+ const shownBySlot = new Map(pose.shown.map((s) => [s.slot, s]));
9674
+ const setupShownSlots = new Set(setupPose.shown.map((s) => s.slot));
9675
+ const rawRowBySlot = new Map(raw.slots.map((r) => [r[0], r]));
9676
+ const slots = [];
9677
+ for (let z = 0; z < pose.drawOrder.length; z++) {
9678
+ const slotName = pose.drawOrder[z];
9679
+ if (this.activeSet !== null && hiddenBySet(slotName, this.activeSet)) {
9680
+ slots.push(Object.freeze({
9681
+ slot: slotName,
9682
+ z,
9683
+ attachment: null,
9684
+ skin: null,
9685
+ reason: "set-hide"
9686
+ }));
9687
+ continue;
9688
+ }
9689
+ const shown = shownBySlot.get(slotName);
9690
+ const isDrawableKind = shown !== void 0 && (shown.geometry.kind === "region" || shown.geometry.kind === "mesh" || shown.geometry.kind === "linkedmesh");
9691
+ if (shown !== void 0 && isDrawableKind) {
9692
+ const bone2 = this.source.boneOf.get(slotName);
9693
+ const boneActive = bone2 !== void 0 && this.source.active.has(bone2);
9694
+ if (this.draws(slotName) && boneActive) {
9695
+ slots.push(Object.freeze({
9696
+ slot: slotName,
9697
+ z,
9698
+ attachment: shown.name,
9699
+ skin: shown.skin,
9700
+ reason: null
9701
+ }));
9702
+ } else {
9703
+ slots.push(Object.freeze({
9704
+ slot: slotName,
9705
+ z,
9706
+ attachment: null,
9707
+ skin: null,
9708
+ reason: "hidden"
9709
+ }));
9710
+ }
9711
+ continue;
9712
+ }
9713
+ const rawRow = rawRowBySlot.get(slotName);
9714
+ const overriddenBySet = this.activeSet !== null && slotName in this.activeSet.slots;
9715
+ const setupHadAttachment = setupShownSlots.has(slotName);
9716
+ const isTimelineNull = !overriddenBySet && (setupHadAttachment && rawRow?.[1] === null || this.isSlotKeyedNullByTimeline(slotName, raw.animationTime));
9717
+ slots.push(Object.freeze({
9718
+ slot: slotName,
9719
+ z,
9720
+ attachment: null,
9721
+ skin: null,
9722
+ reason: isTimelineNull ? "timeline-null" : "no-attachment"
9723
+ }));
9724
+ }
9725
+ const snapshot = Object.freeze({
9726
+ generation: this.poseGeneration,
9727
+ set: this.activeSet?.name ?? null,
9728
+ skin: this.skinName ?? null,
9729
+ slots: Object.freeze(slots)
9730
+ });
9731
+ this.cachedIdentity = snapshot;
9732
+ this.cachedIdentityGen = this.poseGeneration;
9733
+ return snapshot;
9734
+ }
9735
+ /** Whether an active animation's attachment timeline explicitly keyed `null` on `slotName` at `animationTime`. */
9736
+ isSlotKeyedNullByTimeline(slotName, animationTime) {
9737
+ const checkAnimation = (animName, time) => {
9738
+ if (animName === null) return void 0;
9739
+ const anim = this.view.animations.find((a) => a.name === animName);
9740
+ if (anim === void 0) return void 0;
9741
+ const target = anim.timelines.slots.find((s) => s.name === slotName);
9742
+ if (target === void 0) return void 0;
9743
+ let keyed;
9744
+ for (const tl of target.timelines) {
9745
+ if (tl.kind !== "attachment") continue;
9746
+ let activeKey = null;
9747
+ for (const k of tl.keys) {
9748
+ if (k.time <= time) activeKey = k;
9749
+ else break;
9750
+ }
9751
+ if (activeKey !== null) {
9752
+ keyed = activeKey.name === null || activeKey.name === void 0;
9753
+ }
9754
+ }
9755
+ return keyed;
9756
+ };
9757
+ if (this.tracks !== null) {
9758
+ let result = false;
9759
+ for (let t = 0; t < this.trackCount; t++) {
9760
+ const cur = this.tracks.current(t);
9761
+ if (cur !== null && cur.animation !== null) {
9762
+ const k = checkAnimation(cur.animation, cur.animationTime);
9763
+ if (k !== void 0) result = k;
9764
+ }
9765
+ }
9766
+ return result;
9767
+ }
9768
+ if (this.track !== null && this.animation !== null) {
9769
+ return checkAnimation(this.animation, animationTime) === true;
9770
+ }
9771
+ return false;
9421
9772
  }
9422
9773
  /** Removes the renderer's elements and hands its GPU pages back. The assets stay the caller's. */
9423
9774
  dispose() {
9424
9775
  this.renderer.dispose();
9776
+ this.clearSeekCache();
9777
+ this.cachedIdentity = null;
9425
9778
  this.tracks = null;
9426
9779
  this.track = null;
9427
9780
  this.pose = null;