spine-html 1.11.2 → 1.12.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.
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)) {
@@ -8942,6 +8942,9 @@ class RigPlayer {
8942
8942
  rawPose = null;
8943
8943
  setup = null;
8944
8944
  seekCache = null;
8945
+ poseGeneration = 0;
8946
+ cachedIdentity = null;
8947
+ cachedIdentityGen = -1;
8945
8948
  setList;
8946
8949
  setsByName;
8947
8950
  activeSet = null;
@@ -8973,6 +8976,8 @@ class RigPlayer {
8973
8976
  this.meshBackend = options.meshBackend;
8974
8977
  this.adjustFn = options.adjust ?? null;
8975
8978
  if (options.slots !== void 0 && options.slots !== null) this.slots = options.slots;
8979
+ this.poseGeneration = 0;
8980
+ this.cachedIdentity = null;
8976
8981
  this.active = this.create(options.mode ?? "dom");
8977
8982
  }
8978
8983
  /**
@@ -9160,6 +9165,8 @@ class RigPlayer {
9160
9165
  setRawPose(raw) {
9161
9166
  this.rawPose = raw;
9162
9167
  this.pose = this.activeSet === null ? raw : applySetToPose(this.view, raw, this.activeSet);
9168
+ this.poseGeneration++;
9169
+ this.cachedIdentity = null;
9163
9170
  }
9164
9171
  /**
9165
9172
  * The world the rig's physics answers (#121): `wind` and `gravity`, as
@@ -9239,6 +9246,8 @@ class RigPlayer {
9239
9246
  restoreStepperState(stepper, exactHit.state);
9240
9247
  this.rawPose = exactHit.rawPose;
9241
9248
  this.pose = exactHit.pose;
9249
+ this.poseGeneration++;
9250
+ this.cachedIdentity = null;
9242
9251
  return;
9243
9252
  }
9244
9253
  const steps = Math.floor(time / this.seekStep);
@@ -9423,6 +9432,9 @@ class RigPlayer {
9423
9432
  } else if (this.rawPose !== null) {
9424
9433
  this.setup = poseRawSetup(this.view);
9425
9434
  this.setRawPose(this.setup);
9435
+ } else {
9436
+ this.poseGeneration++;
9437
+ this.cachedIdentity = null;
9426
9438
  }
9427
9439
  }
9428
9440
  /**
@@ -9462,6 +9474,8 @@ class RigPlayer {
9462
9474
  const base2 = this.rawPose ?? (this.setup ?? (this.setup = poseRawSetup(this.view)));
9463
9475
  this.rawPose = base2;
9464
9476
  this.pose = base2;
9477
+ this.poseGeneration++;
9478
+ this.cachedIdentity = null;
9465
9479
  return;
9466
9480
  }
9467
9481
  const found = this.setsByName.get(name);
@@ -9475,6 +9489,8 @@ class RigPlayer {
9475
9489
  const base = this.rawPose ?? (this.setup ?? (this.setup = poseRawSetup(this.view)));
9476
9490
  this.rawPose = base;
9477
9491
  this.pose = applySetToPose(this.view, base, found);
9492
+ this.poseGeneration++;
9493
+ this.cachedIdentity = null;
9478
9494
  }
9479
9495
  /**
9480
9496
  * Which slots this player draws (`SlotFilter`), or null for every slot.
@@ -9490,11 +9506,15 @@ class RigPlayer {
9490
9506
  if (filter === null) {
9491
9507
  this.slotFilter = null;
9492
9508
  this.draws = () => true;
9509
+ this.poseGeneration++;
9510
+ this.cachedIdentity = null;
9493
9511
  return;
9494
9512
  }
9495
9513
  if (typeof filter === "function") {
9496
9514
  this.slotFilter = filter;
9497
9515
  this.draws = filter;
9516
+ this.poseGeneration++;
9517
+ this.cachedIdentity = null;
9498
9518
  return;
9499
9519
  }
9500
9520
  if (!Array.isArray(filter)) throw new Error(`slots must be an array of slot names, a function, or null, got ${typeof filter}`);
@@ -9506,6 +9526,8 @@ class RigPlayer {
9506
9526
  const allowed = new Set(filter);
9507
9527
  this.slotFilter = [...filter];
9508
9528
  this.draws = (slot) => allowed.has(slot);
9529
+ this.poseGeneration++;
9530
+ this.cachedIdentity = null;
9509
9531
  }
9510
9532
  /**
9511
9533
  * The per-pose adjustment of bone locals (`BoneAdjust`), or null for none.
@@ -9610,6 +9632,10 @@ class RigPlayer {
9610
9632
  if (this.setup === null) this.setup = poseRawSetup(this.view);
9611
9633
  return this.activeSet === null ? this.setup : applySetToPose(this.view, this.setup, this.activeSet);
9612
9634
  }
9635
+ /** Monotonic pose generation counter matching `posedFrame().generation` and `slotIdentity().generation`. */
9636
+ get generation() {
9637
+ return this.poseGeneration;
9638
+ }
9613
9639
  /**
9614
9640
  * The pose the player holds, as the frame record the renderer draws — under
9615
9641
  * this player's `slots` filter, or under `slots` given here instead (a
@@ -9618,14 +9644,137 @@ class RigPlayer {
9618
9644
  * posed once).
9619
9645
  */
9620
9646
  posedFrame(slots) {
9621
- if (this.pose === null) return { order: [] };
9622
9647
  const draws = slots === void 0 ? this.draws : slots === null ? () => true : typeof slots === "function" ? slots : predicateOf(slots);
9623
- 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;
9624
9772
  }
9625
9773
  /** Removes the renderer's elements and hands its GPU pages back. The assets stay the caller's. */
9626
9774
  dispose() {
9627
9775
  this.renderer.dispose();
9628
9776
  this.clearSeekCache();
9777
+ this.cachedIdentity = null;
9629
9778
  this.tracks = null;
9630
9779
  this.track = null;
9631
9780
  this.pose = null;