rig-play 1.0.0 → 1.1.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
@@ -245,6 +245,24 @@ frame takes the one-`clip-path`-on-the-root path), and a mesh's `uvs` and
245
245
  `triangles` are the same arrays from frame to frame for the same texture
246
246
  mapping (a new array re-rasters the mesh).
247
247
 
248
+ ### Reading the pose
249
+
250
+ Two reads off the pose the player holds — the last `frame`/`advance`/`seek`,
251
+ or the setup pose before `play` — for framing a stage around a rig:
252
+
253
+ ```ts
254
+ const head = player.bone('head'); // { x, y, a, b, c, d, rotationX, rotationY, scaleX, scaleY, active }
255
+ const box = player.bounds(); // { x, y, width, height } | null — world units, Y up
256
+ ```
257
+
258
+ `bone` is the core's world transform for that bone (`rig-c/core`'s `RawBone`,
259
+ unchanged), and an unknown name is refused by name. `bounds` is the box of
260
+ every region's four corners and every mesh's vertices in the frame record the
261
+ renderer takes; a slot under a clip counts only inside the clip polygon's own
262
+ box, an inverse clip removes nothing, and bounding boxes, paths and points do
263
+ not draw and do not count. Both cost nothing until called and read the same
264
+ numbers in `dom` and `canvas` mode.
265
+
248
266
  ### One load, several players
249
267
 
250
268
  One `loadModel` backs any number of players: the regions are cut once, and
@@ -493,6 +511,150 @@ on-device (Chromium, dpr 2, stage under `scale(0.25)`, ~2.2 M CSS px on screen):
493
511
  | 92 meshes | `devicePixelRatio` (2) | 141.1 Mpx | 36–39 |
494
512
  | 92 meshes | `dpr × zoom` (0.5) | 9.1 Mpx | 60–61 |
495
513
 
514
+ ## Coming from spine-html 0.8
515
+
516
+ This is not a drop-in upgrade. 0.8 drew a Spine export (`.json` or `.skel`, plus its
517
+ `.atlas`) that `@esotericsoftware/spine-core` parsed and posed: the caller built the
518
+ `Skeleton` and the `AnimationState`, advanced them each frame, and handed the skeleton to
519
+ `renderer.render(skeleton)`. 1.0 loads one file, the `skeleton.model.json` that
520
+ `rigc build` writes, poses it through `rig-c/core` (bundled into the package, with no peer
521
+ dependency), and `RigPlayer` owns the track, so the caller's frame is `player.frame(dt)`.
522
+ A Spine export is refused by name before anything is fetched: the data has to be a rigc
523
+ build, and this package declares no loader for anything else.
524
+
525
+ What did not move: a positioned root element is still the rig's origin (Y up) with its
526
+ layout and scaling left to the caller, the DOM tier keeps its knobs under a new class
527
+ name, and unloading is still elements first, bitmaps second.
528
+
529
+ ### 0.8 → 1.0
530
+
531
+ | 0.8.0 (`spine-html`) | 1.0.0 (`rig-play`) | Note |
532
+ | --- | --- | --- |
533
+ | `npm i spine-html @esotericsoftware/spine-core` | `npm i rig-play`, and `npm i -D rig-c` for the build tool | 0.8 declared spine-core as a peer dependency (`>=4.0.0 <4.4.0`). 1.0.0 declares no dependencies and no peer dependencies. |
534
+ | `loadSkeletonAssets(options)` | `loadModel(options)` | One URL instead of two. The `.atlas` and `skeleton.json` a build writes beside the document are not fetched. |
535
+ | `LoadSkeletonAssetsOptions.atlasUrl`, `skeletonUrl` | `LoadModelOptions.modelUrl` | |
536
+ | `resolvePage(pageName, atlasUrl)` | `resolvePage(pageName, modelUrl)` | The default now resolves a page name against the model URL's directory. |
537
+ | `crossOrigin`, `fetch` | `crossOrigin`, `fetch` | Unchanged. |
538
+ | `scale` (`LoadSkeletonAssetsOptions`, `LoadSkeletonJsonOptions`) | none | See below. |
539
+ | `SkeletonAssets` | `ModelAssets` | |
540
+ | `SkeletonAssets.data` | `ModelAssets.document`, `animations`, `skins`, `stage`, `spec` | `document` is opaque (`ModelDocument`); the player poses it. What a caller used to read off the skeleton data is stated as `animations` (name and duration), `skins` (names) and `stage` (the stage the build declared, or null). |
541
+ | `SkeletonAssets.atlas` | `ModelAssets.pageImages` | The page images, keyed by page name. There is no atlas object; where each region sits is in the document's `pages` section. |
542
+ | `SkeletonAssets.regionImages` | `ModelAssets.regionImages` | Same `Map<string, RegionImage>`. |
543
+ | `SkeletonAssets.dispose()`, `AtlasAssets.dispose()` | `ModelAssets.dispose()` | Same contract: frees the blob URLs the load minted, idempotent, called after the players are disposed. |
544
+ | `loadAtlasAssets` + `loadSkeletonJson` (one atlas, several skeletons) | none | One `loadModel` backs any number of `RigPlayer`s of the same rig. Two rigs are two loads, and each cuts its own regions. See below. |
545
+ | `loadSkeletonBinary` from `spine-html/binary` | none | 1.0.0 has one entry point. A `.skel` URL is refused by name. |
546
+ | `unpackRegions(atlas, pageImages)` | `unpackRegions(pages, pageImages)` | The first argument is the document's `pages` section (`readonly PageLayout[]`) instead of a `TextureAtlas`. |
547
+ | `revokeRegions(images)` | `revokeRegions(images)` | Unchanged. |
548
+ | `RegionImage` | `RegionImage` | Unchanged (`url`, `width`, `height`, in atlas units). |
549
+ | `DomTexture` | none | Not exported; there is no atlas page to attach a texture to. |
550
+ | `new Skeleton(assets.data)` and `new AnimationState(new AnimationStateData(assets.data))` | `new RigPlayer(root, assets, options?)` | The player holds the pose and the track. |
551
+ | `state.setAnimation(0, 'walk', true)` | `player.play('walk', { loop: true })` | `loop` defaults to true. `play` opens the animation at its first pose with the physics reset there. `player.current` names what is playing. |
552
+ | `state.update(delta)`, `state.apply(skeleton)`, `skeleton.update(delta)`, `skeleton.updateWorldTransform(Physics.update)`, `renderer.render(skeleton)` | `player.frame(dt)` | Or `player.advance(dt)` then `player.draw()`, to time the two apart. |
553
+ | time scale (the caller's own, on the spine-core side) | `player.timeScale` | Multiplies every step. 0 holds the pose without re-posing it; a negative value is not stepped. |
554
+ | holding a pose (the caller stopped advancing) | `player.pause()`, `player.resume()` | `frame` keeps drawing the held pose. |
555
+ | moving to a time (the caller's own, on the spine-core side) | `player.seek(time)`, `RigPlayerOptions.seekStep`, `player.time` | A seek reopens the animation and steps forward in `seekStep` increments (1/60 s by default). Events and completes crossed on the way are not fired. |
556
+ | events (the caller's own listener on the spine-core side) | `player.on('event', listener)`, `player.on('complete', listener)` | `'event'` hands a `RigEvent` (`name`, `time`, `int`, `float`, `string`); `'complete'` hands `{ animation }`. `on` returns a function that removes the listener. |
557
+ | skin (set by the caller on its `Skeleton`) | `RigPlayerOptions.skin` | A constructor option. The names are in `ModelAssets.skins`. Without it the rig is posed with no skin set. |
558
+ | `SpineHtmlRenderer` | `DomRenderer`, reached as `player.renderer` | In `mode: 'dom'`, the default. `mode: 'canvas'` is new and makes `player.renderer` a `CanvasRenderer`. |
559
+ | `new SpineHtmlRenderer(root, regionImages)` | `new DomRenderer(root, regionImages, pages?)` | Only for drawing your own frames; `RigPlayer` constructs its renderer. `pages` is the page-image map the mesh tier samples (`ModelAssets.pageImages`). A hand-built `regionImages` map is still accepted. |
560
+ | `renderer.render(skeleton)` | `renderer.render(frame)` | Takes a `PosedFrame`, not a skeleton. `player.posedFrame()` returns the one the player would draw. |
561
+ | `renderer.pixelRatio` | `player.renderer.pixelRatio` | On both renderers. Carried over when `player.mode` changes. |
562
+ | `renderer.syncPixelRatio()` | `DomRenderer.syncPixelRatio()` | DOM mode only. `player.renderer` is typed as either renderer, so narrow it (`instanceof DomRenderer`) before calling. |
563
+ | `renderer.meshBackingPixels` | `DomRenderer.meshBackingPixels` | DOM mode only. |
564
+ | `renderer.meshBackend`, `meshBackendActive`, type `MeshBackend` | `DomRenderer.meshBackend`, `meshBackendActive`, type `MeshBackend` | Same values (`'canvas2d'` default, `'webgl'`). Also settable up front as `RigPlayerOptions.meshBackend`. |
565
+ | `renderer.triangleExpand` | `DomRenderer.triangleExpand` | Same default (0.5). |
566
+ | `renderer.clipping` | `player.renderer.clipping` | On both renderers. Carried over when `player.mode` changes. |
567
+ | `clipCount`, `clipSkipCount`, `clipWriteCount` | same names on `DomRenderer` | `clipSkipCount` counts less than it did: a second clip met while one is in force, and a clip on an inactive bone, no longer reach the frame, so they are not counted. |
568
+ | `meshCount`, `meshReuseCount`, `canvasReallocCount`, `triangleCount` | same names on `DomRenderer` | |
569
+ | tint: skeleton × slot × attachment colour | `player.color` × slot × attachment | `player.color` is `[r, g, b, a]` and takes the place of the skeleton's colour. The product arrives as `tint` on each `PosedSlot`. |
570
+ | dark (two-colour) tint: unsupported | `DomRenderer`: unsupported. `CanvasRenderer`: drawn | Carried as `dark` on a `PosedSlot`. The canvas mode's canvas2d fallback draws alpha only. |
571
+ | `renderer.dispose()` | `player.dispose()` | Then `assets.dispose()`, as before. `DomRenderer.dispose()` and `CanvasRenderer.dispose()` exist for a renderer you constructed yourself. |
572
+
573
+ ### No counterpart in 1.0.0
574
+
575
+ Things a 0.8 caller could do because it held the spine-core objects, or through a 0.8
576
+ export, for which 1.0.0 declares nothing:
577
+
578
+ - **A Spine export as input.** `loadSkeletonJson`, `loadSkeletonBinary`, `loadAtlasAssets`,
579
+ the `spine-html/binary` entry and `DomTexture` have no counterpart, and neither does
580
+ choosing which spine-core generation reads the data. `loadModel` reads a compiled model
581
+ document and nothing else.
582
+ - **Scaling the skeleton as it is read.** `LoadModelOptions` declares no `scale`. Scaling
583
+ the root element is still the caller's.
584
+ - **One unpacked atlas shared by different skeletons.** A document is one rig, and each
585
+ `loadModel` cuts its own regions and owns its own blob URLs. Sharing is per rig: one
586
+ load, several players.
587
+ - **More than one track, mixing between animations, or queueing one after another.**
588
+ `RigPlayer` declares one track; `play` reopens it at the animation's first pose.
589
+ - **Reading or writing a bone.** No bone is exposed: nothing sets a bone's local values
590
+ between the step and the draw, and nothing returns a bone's world transform. What is
591
+ readable is the posed slots: `player.posedFrame()` carries each drawn slot's world
592
+ `corners` or `vertices`.
593
+ - **A bounds query for the current pose.** None is declared. `ModelAssets.stage` is the
594
+ stage the document states (or null), which is the rig's declared framing and not the
595
+ box of the pose being drawn.
596
+ - **Changing the skin after construction.** `skin` is a `RigPlayerOptions` field and
597
+ `RigPlayer` declares no way to change it later; a second player over the same assets
598
+ is the declared route.
599
+ - **Changing one slot from code** (its attachment, its colour). `RigPlayer` declares only
600
+ the whole-rig `color`. The frame record is a plain object, so a caller that draws its
601
+ own frames through `DomRenderer.render` or `CanvasRenderer.render` decides what is in
602
+ it.
603
+ - **Choosing how physics is stepped each frame.** 0.8's loop passed `Physics.update`
604
+ itself; 1.0.0 declares no physics option. `play` and `seek` reset physics at the
605
+ animation's first pose and every step advances it.
606
+ - **Track callbacks other than the two declared.** `on` accepts `'event'` and
607
+ `'complete'`.
608
+
609
+ Knobs on `DomRenderer` that `CanvasRenderer` does not declare:
610
+
611
+ - `syncPixelRatio()`. `CanvasRenderer.pixelRatio` exists and is set by hand
612
+ (`devicePixelRatio * rootScale`).
613
+ - `meshBackend` and `meshBackendActive`. The canvas mode has its own pair, `backend` and
614
+ `backendActive` (type `CanvasBackend`, `'webgl'` by default).
615
+ - `triangleExpand`, `meshBackingPixels`, `clipWriteCount`, `meshCount`, `meshReuseCount`,
616
+ `triangleCount`. The canvas mode's counters are `drawCalls`, `blendFlushes`,
617
+ `trianglesDrawn`, `framesReused` and `blits`.
618
+
619
+ Both declare `pixelRatio`, `clipping`, `clipCount`, `clipSkipCount`, `canvasReallocCount`,
620
+ `render` and `dispose`. Only `pixelRatio` and `clipping` carry over when `player.mode`
621
+ changes.
622
+
623
+ ### Before and after
624
+
625
+ 0.8.0:
626
+
627
+ ```ts
628
+ import { Skeleton, AnimationState, AnimationStateData, Physics } from '@esotericsoftware/spine-core';
629
+ import { loadSkeletonAssets, SpineHtmlRenderer } from 'spine-html';
630
+ const assets = await loadSkeletonAssets({ atlasUrl: '/hero/hero.atlas', skeletonUrl: '/hero/hero.json' });
631
+ const skeleton = new Skeleton(assets.data);
632
+ const state = new AnimationState(new AnimationStateData(assets.data));
633
+ state.setAnimation(0, 'walk', true);
634
+ const renderer = new SpineHtmlRenderer(rootElement, assets.regionImages);
635
+ function frame(delta: number) {
636
+ state.update(delta);
637
+ state.apply(skeleton);
638
+ skeleton.update(delta);
639
+ skeleton.updateWorldTransform(Physics.update);
640
+ renderer.render(skeleton);
641
+ }
642
+ renderer.dispose(); assets.dispose(); // on unload
643
+ ```
644
+
645
+ 1.0.0:
646
+
647
+ ```ts
648
+ import { loadModel, RigPlayer } from 'rig-play';
649
+ const assets = await loadModel({ modelUrl: '/rigs/walk/skeleton.model.json' });
650
+ const player = new RigPlayer(rootElement, assets);
651
+ player.play('walk', { loop: true });
652
+ function frame(delta: number) {
653
+ player.frame(delta);
654
+ }
655
+ player.dispose(); assets.dispose(); // on unload
656
+ ```
657
+
496
658
  ## Demo (this repository)
497
659
 
498
660
  ```bash
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { type CanvasBackend, CanvasRenderer, type CanvasViewport } from './Canva
2
2
  export { DomRenderer, type MeshBackend } from './DomRenderer.js';
3
3
  export { type PageLayout, type PageRegion, type RegionImage, revokeRegions, unpackRegions } from './DomTexture.js';
4
4
  export { type LoadModelOptions, loadModel, type ModelAssets, type ModelDocument } from './loadModel.js';
5
- export { type RenderMode, type RigEvent, RigPlayer, type RigPlayerOptions } from './player.js';
5
+ export { type BoneWorld, type PoseBounds, type RenderMode, type RigEvent, RigPlayer, type RigPlayerOptions } 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,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACxG,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/F,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,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACnH,OAAO,EAAE,KAAK,gBAAgB,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACxG,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,QAAQ,EAAE,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAChI,YAAY,EACV,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -4515,6 +4515,8 @@ function rawNumber(v) {
4515
4515
  function constraintRecords(doc) {
4516
4516
  return doc.constraints.flatMap((c) => c.record === void 0 ? [] : [c.record]);
4517
4517
  }
4518
+ const NOT_ADMITTED = [];
4519
+ const POSED_BLOCKS = ["setup.bones", "setup.slots", "setup.drawOrder", "setup.attachments", "setup.clips", "setup.clipped"];
4518
4520
  const activeOfView = /* @__PURE__ */ new WeakMap();
4519
4521
  function activeBones(doc) {
4520
4522
  const kept = activeOfView.get(doc);
@@ -4582,6 +4584,97 @@ function resolveShown(doc, slotName, placeholder) {
4582
4584
  const agree = rows.every((r) => r.name === rows[0].name && r.path === rows[0].path);
4583
4585
  return agree ? filling[0] : { conflict: rows.map((r) => ({ skin: r.skin, shown: r.name, path: r.path })) };
4584
4586
  }
4587
+ function poseSetup(doc, plant = {}, physics) {
4588
+ const evaluate = plant.evaluate ?? worldTransforms;
4589
+ const resolve = plant.shown ?? shownAttachment;
4590
+ const colour = plant.colour ?? readColour;
4591
+ const blend = plant.blend ?? readBlend;
4592
+ const round = plant.round ?? gridRound;
4593
+ const bonesWhy = constraintsAbsentWhy(doc, solverRules(plant.solver));
4594
+ let bones = null;
4595
+ let setupWorld = null;
4596
+ const applied = [];
4597
+ if (bonesWhy === null) {
4598
+ const active = activeBones(doc);
4599
+ const records = constraintRecords(doc);
4600
+ setupWorld = applyConstraints(doc.bones, evaluate(doc.bones, active), active, plant.constraints ? plant.constraints(records) : records, null, applied, physics, void 0, solverRules(plant.solver));
4601
+ const world = setupWorld;
4602
+ bones = doc.bones.map((b) => {
4603
+ const t = world.get(b.name);
4604
+ if (t === void 0) throw new CoreInputError(`the evaluator returned no transform for bone "${b.name}"`);
4605
+ return [b.name, round(t.worldX), round(t.worldY), round(t.a), round(t.b), round(t.c), round(t.d), active.has(b.name) ? 1 : 0, b.parent ?? null];
4606
+ });
4607
+ }
4608
+ const conflicts = [];
4609
+ const slotRows = [];
4610
+ const liveBones = activeBones(doc);
4611
+ const slotGate = plant.slotTimelines ?? slotTimelinesApply;
4612
+ for (const slot of doc.slots) {
4613
+ const pose = {
4614
+ placeholder: slot.setup,
4615
+ light: slot.color === void 0 ? [1, 1, 1, 1] : [...colour(slot.color)],
4616
+ dark: slot.dark === void 0 ? null : readColour(slot.dark).slice(0, 3)
4617
+ };
4618
+ if (slotGate(doc, slot, liveBones)) applySliderSlots(slot.name, pose, applied);
4619
+ const posedRecord = { ...slot, setup: pose.placeholder };
4620
+ const shown = pose.placeholder === null ? null : resolve(doc, posedRecord);
4621
+ if (shown !== null && "conflict" in shown) {
4622
+ conflicts.push(`slot "${slot.name}" placeholder "${pose.placeholder}" is filled by skins ${shown.conflict.map((c) => `"${c.skin}" (shows ${JSON.stringify(c.shown)}, path ${JSON.stringify(c.path)})`).join(", ")}`);
4623
+ continue;
4624
+ }
4625
+ const row = shown === null ? null : shownRow(shown);
4626
+ const [r, g, b, a] = pose.light;
4627
+ const dark = pose.dark;
4628
+ slotRows.push([
4629
+ slot.name,
4630
+ row === null ? null : row.name,
4631
+ round(r),
4632
+ round(g),
4633
+ round(b),
4634
+ round(a),
4635
+ dark === null ? null : [round(dark[0]), round(dark[1]), round(dark[2])],
4636
+ row === null ? null : row.path,
4637
+ blend(slot)
4638
+ ]);
4639
+ }
4640
+ const slotsWhy = (bonesWhy === null ? null : slidersWhy(doc)) ?? (conflicts.length === 0 ? null : `${conflicts.join("; ")} — under --skin all the LAST of them in the Spine file's skin order wins, and that order is the emitter's (default first, the rest in the editor's order), not the model's; the per-skin dumps (--skin <name>) judge it`);
4641
+ const slots = slotsWhy === null ? slotRows : null;
4642
+ const upstream = [bonesWhy === null ? null : `setup.bones is absent (${bonesWhy}), and every vertex goes through a bone's world matrix`, slotsWhy === null ? null : "setup.slots is absent, so what a slot shows is not posed"].filter((x) => x !== null);
4643
+ const orderWhy = bonesWhy === null ? null : slidersKeyingWhy(doc, "drawOrder");
4644
+ let drawOrder = null;
4645
+ if (orderWhy === null) {
4646
+ let order = doc.slots.map((_s, i) => i);
4647
+ for (const app of applied) order = (plant.drawOrder ?? drawOrderAt)(doc.slots.length, app.timelines.drawOrder, app.at) ?? order;
4648
+ drawOrder = order.map((i) => doc.slots[i].name);
4649
+ }
4650
+ let attachments = null;
4651
+ let clips = null;
4652
+ let clipsWhy = upstream.length === 0 ? null : upstream.join("; ");
4653
+ let attachmentsWhy = clipsWhy;
4654
+ let clipped = null;
4655
+ let clippedWhy = clipsWhy ?? orderWhy;
4656
+ let shownInOrder = null;
4657
+ if (clipsWhy === null && setupWorld !== null && drawOrder !== null) {
4658
+ const world = setupWorld;
4659
+ const placeholders = new Map(doc.slots.map((sl) => [sl.name, sl.setup]));
4660
+ const states = attachmentStates(doc, resolve, placeholders, null, applied, plant);
4661
+ const rank = new Map(drawOrder.map((n, i) => [n, i]));
4662
+ const shown = [...states.shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
4663
+ shownInOrder = states.why.length === 0 ? shown : null;
4664
+ const posed = poseGeometry(shown, world, sourceOfDoc(doc), round, { region: plant.region, vertices: plant.vertices }, drawWalkOf(doc, drawOrder, plant));
4665
+ const stateWhy = states.why.length === 0 ? null : `${states.why.join("; ")}`;
4666
+ attachments = stateWhy === null ? posed.attachments : null;
4667
+ attachmentsWhy = stateWhy ?? posed.attachmentsWhy;
4668
+ clips = stateWhy === null ? posed.clips : null;
4669
+ clipsWhy = stateWhy;
4670
+ clipped = stateWhy === null ? posed.clipped : null;
4671
+ clippedWhy = stateWhy ?? posed.clippedWhy;
4672
+ }
4673
+ const why = { "setup.bones": bonesWhy, "setup.slots": slotsWhy, "setup.drawOrder": orderWhy, "setup.attachments": attachmentsWhy, "setup.clips": clipsWhy, "setup.clipped": clippedWhy };
4674
+ const absent = NOT_ADMITTED.map(([block, reason]) => [block, reason]);
4675
+ for (const block of POSED_BLOCKS) if (why[block] !== null) absent.push([block, why[block]]);
4676
+ return { setup: { bones, slots, drawOrder, attachments, clips, clipped }, absent, world: setupWorld, shown: shownInOrder };
4677
+ }
4585
4678
  function drawWalkOf(doc, order, plant = {}) {
4586
4679
  const active = activeBones(doc);
4587
4680
  return {
@@ -4601,6 +4694,22 @@ function sourceOfDoc(doc) {
4601
4694
  return g?.kind === "mesh" ? g.vertices : sourceProblem(doc.skins, skin, slot, source) ?? `the linked mesh's source "${source}" carries no vertices`;
4602
4695
  };
4603
4696
  }
4697
+ function slidersKeyingWhy(doc, group) {
4698
+ const keyed = doc.constraints.flatMap((c) => {
4699
+ if (c.kind !== "slider") return [];
4700
+ const anim = doc.animations.find((a) => a.name === c.animation);
4701
+ return anim !== void 0 && anim.timelines[group].length > 0 ? [`slider "${c.name}" applies animation "${c.animation}", which keys the draw order`] : [];
4702
+ });
4703
+ return keyed.length === 0 ? null : `${keyed.join("; ")} — a slider's time is read off the bones, which are absent`;
4704
+ }
4705
+ function slidersWhy(doc) {
4706
+ const keyed = doc.constraints.flatMap((c) => {
4707
+ if (c.kind !== "slider") return [];
4708
+ const slots = doc.animations.find((a) => a.name === c.animation)?.slots ?? [];
4709
+ return slots.length === 0 ? [] : [`slider "${c.name}" applies animation "${c.animation}", which keys slot(s) ${slots.map((x) => `"${x}"`).join(", ")}`];
4710
+ });
4711
+ return keyed.length === 0 ? null : `${keyed.join("; ")} — the oracle's setup applies sliders, and a slider poses the slots its animation keys at a time read off the bones, which are absent`;
4712
+ }
4604
4713
  function drawOrderOf(shown, order) {
4605
4714
  const rank = new Map(order.map((n, r) => [n, r]));
4606
4715
  return [...shown].sort((a, b) => (rank.get(a.slot) ?? 0) - (rank.get(b.slot) ?? 0));
@@ -4677,6 +4786,22 @@ function rawDrawn(doc, shown, world, order, plant, mode = RAW_MODE) {
4677
4786
  const clipped = geometry.drawnClipped.map((c) => [c[0], c[1], c[2], c[3].map((v) => numberOf(v, `clipped vertex of slot "${c[0]}"`, mode)), c[4].map((v) => numberOf(v, `clipped uv of slot "${c[0]}"`, mode)), c[5]]);
4678
4787
  return { drawn, clips, clipped };
4679
4788
  }
4789
+ function poseRawSetup(doc, plant = {}) {
4790
+ return setupPoseIn(doc, plant, RAW_MODE);
4791
+ }
4792
+ function setupPoseIn(doc, plant, mode) {
4793
+ const { world, shown, slots, drawOrder } = setupPosed(doc, plant, mode);
4794
+ const drawn = rawDrawn(doc, shown, world, drawOrder, plant, mode);
4795
+ return { trackTime: 0, animationTime: 0, bones: rawBones(doc, world), slots, drawOrder, ...drawn, events: [], shown: drawOrderOf(shown, drawOrder) };
4796
+ }
4797
+ function setupPosed(doc, plant, mode) {
4798
+ const posed = poseSetup(doc, { ...plant, ...roundPlant(mode) }, freshStepContext(plant.physicsStep));
4799
+ const absent = posed.absent.filter(([block]) => block !== "setup.clipped");
4800
+ if (absent.length > 0) throw new CoreInputError(`the raw setup pose leaves ${absent.map(([b, why]) => `${b} out (${why})`).join("; ")}`);
4801
+ const { setup, world, shown } = posed;
4802
+ if (world === null || shown === null || setup.slots === null || setup.drawOrder === null) throw new CoreInputError("the raw setup pose was not posed");
4803
+ return { world, shown, slots: setup.slots, drawOrder: setup.drawOrder };
4804
+ }
4680
4805
  function loopedTime(trackTime, duration) {
4681
4806
  return duration === 0 ? 0 : trackTime % duration;
4682
4807
  }
@@ -7095,6 +7220,7 @@ class RigPlayer {
7095
7220
  loop = true;
7096
7221
  paused = false;
7097
7222
  pose = null;
7223
+ setup = null;
7098
7224
  listeners = {
7099
7225
  event: /* @__PURE__ */ new Set(),
7100
7226
  complete: /* @__PURE__ */ new Set()
@@ -7216,6 +7342,79 @@ class RigPlayer {
7216
7342
  if (this.pose === null) return;
7217
7343
  this.renderer.render(this.posedFrame());
7218
7344
  }
7345
+ /**
7346
+ * Where `name` is in the pose the player holds — the pose of the last
7347
+ * `advance`/`frame`/`seek`, or the setup pose before `play`. Read off the
7348
+ * core's pose, so it is the same in `dom` and `canvas` mode and costs nothing
7349
+ * until called. An unknown bone is refused by name.
7350
+ */
7351
+ bone(name) {
7352
+ const pose = this.posed();
7353
+ for (const bone2 of pose.bones) {
7354
+ if (bone2.name !== name) continue;
7355
+ return {
7356
+ name,
7357
+ active: bone2.active,
7358
+ x: bone2.worldX,
7359
+ y: bone2.worldY,
7360
+ a: bone2.a,
7361
+ b: bone2.b,
7362
+ c: bone2.c,
7363
+ d: bone2.d,
7364
+ rotationX: bone2.rotationX,
7365
+ rotationY: bone2.rotationY,
7366
+ scaleX: bone2.scaleX,
7367
+ scaleY: bone2.scaleY
7368
+ };
7369
+ }
7370
+ throw new Error(`no bone named ${JSON.stringify(name)} in this document (bones: ${pose.bones.map((b) => b.name).join(", ")})`);
7371
+ }
7372
+ /**
7373
+ * The box of what the pose the player holds draws, in world units, Y up:
7374
+ * every region's four corners and every mesh's vertices, in the same frame
7375
+ * record the renderer takes (`posedFrame`). A slot under a clip counts only
7376
+ * inside the clip polygon's own box (an inverse clip removes nothing from
7377
+ * the count); bounding boxes, paths and points are not drawn and do not
7378
+ * count. `null` when nothing draws. The same numbers in `dom` and `canvas`
7379
+ * mode; nothing is computed until called.
7380
+ */
7381
+ bounds() {
7382
+ const frame2 = this.pose === null ? frameOf(this.source, this.posed(), this.color) : this.posedFrame();
7383
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
7384
+ for (const slot of frame2.order) {
7385
+ const points = slot.kind === "region" ? slot.corners : slot.vertices;
7386
+ let lo = -Infinity, lb = -Infinity, hi = Infinity, hb = Infinity;
7387
+ if (slot.clip && !slot.clip.inverse) {
7388
+ const c = slot.clip.polygon;
7389
+ lo = Infinity;
7390
+ lb = Infinity;
7391
+ hi = -Infinity;
7392
+ hb = -Infinity;
7393
+ for (let i = 0; i < c.length; i += 2) {
7394
+ if (c[i] < lo) lo = c[i];
7395
+ if (c[i] > hi) hi = c[i];
7396
+ if (c[i + 1] < lb) lb = c[i + 1];
7397
+ if (c[i + 1] > hb) hb = c[i + 1];
7398
+ }
7399
+ }
7400
+ for (let i = 0; i < points.length; i += 2) {
7401
+ const x = Math.min(Math.max(points[i], lo), hi);
7402
+ const y = Math.min(Math.max(points[i + 1], lb), hb);
7403
+ if (x < minX) minX = x;
7404
+ if (x > maxX) maxX = x;
7405
+ if (y < minY) minY = y;
7406
+ if (y > maxY) maxY = y;
7407
+ }
7408
+ }
7409
+ if (minX === Infinity) return null;
7410
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
7411
+ }
7412
+ /** The pose the reads above see: the track's, or the setup pose before `play` (posed once). */
7413
+ posed() {
7414
+ if (this.pose !== null) return this.pose;
7415
+ if (this.setup === null) this.setup = poseRawSetup(this.view);
7416
+ return this.setup;
7417
+ }
7219
7418
  /** The pose the player holds, as the frame record the renderer draws. */
7220
7419
  posedFrame() {
7221
7420
  if (this.pose === null) return { order: [] };