spine-rigc 2.11.1 → 2.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/NOTICE.md CHANGED
@@ -38,8 +38,8 @@ Software's terms, not a licence term of this project:
38
38
  development dependency for its own gate, and so does an install with
39
39
  `spine-core` added beside it. Validation is not optional on either: the gate
40
40
  runs through the runtime where it is installed and through rigc's own validator
41
- where it is not, and the two are held to the same verdicts in CI on 49 of the
42
- 50 assertions — the official parser's own parse runs only where the runtime is.
41
+ where it is not, and the two are held to the same verdicts in CI on 50 of the
42
+ 51 assertions — the official parser's own parse runs only where the runtime is.
43
43
 
44
44
  > **If a product integrates a Spine Runtime to play rigc's output, that integration
45
45
  > is subject to the terms above** — rigc neither creates those terms nor removes
package/README.md CHANGED
@@ -61,7 +61,7 @@ it cuts both ways:
61
61
  is a development dependency: this repository uses it to hold rigc's own validator
62
62
  to the round trip's verdicts — in CI, on the public example recipes and every
63
63
  selftest call, and on a private corpus of production rigs measured before each
64
- release. Of the 50 assertions, 49 run on both; `A00_ROUNDTRIP_PARSE`, the official
64
+ release. Of the 51 assertions, 50 run on both; `A00_ROUNDTRIP_PARSE`, the official
65
65
  parser's own parse, runs only where the runtime is installed and is reported as a
66
66
  SKIP elsewhere. Install it beside rigc (`bun add -d @esotericsoftware/spine-core`)
67
67
  and the same `rigc` runs the round trip on every build. The
@@ -557,6 +557,7 @@ commands take it and what its default is.
557
557
  | `build … --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` — losslessly, and gated a second time as the pair that ships. `--page-size`, `--padding`, `--page-edges` and `--pack-shape` tune it — `--pack-shape polygon` lets a neighbour sit inside a mesh's rectangle where its hull is not, and with `--page-edges free` costs 12–56 times `rect`'s pack (3–11 s against 0.2–0.3 s on 90–144 parts over two to four pages; past 5 s from about 60 parts, a second or so below 45 on one page — AUTHORING §0.1) |
558
558
  | `build … --atlas-in <file.atlas>` | the same build with every part resolved to a **region of an existing pack** instead of a loose PNG; a name the atlas lacks, a size the spec disagrees with or a rectangle off its page is refused by name |
559
559
  | `validate <dir>` | re-gates artifacts already on disk |
560
+ | `repack <dir> --out <dir>` | a packed build **repacked from its own output** — `skeleton.json`, `skeleton.atlas` and the pages, with no parts kept — under the packing flags `build --pack` takes, so a packer improvement reaches a build whose parts are gone. Every region is lifted off its page, the skeleton read back with `ingest`, and `build --pack` run over the lifted parts with the same gate; it writes only after every region is shown pixel-identical to the input's, the skeleton byte-identical and the gate green, and refuses by name an atlas it cannot lift exactly. A skeleton the rebuild does not reproduce byte for byte — a build written before 2.2.0, whose header box was its stage — is refused naming every differing path, and written, every path printed, only under `--accept-skeleton-differences`. `--stage-box <slot>` reads the stage from the box a build carries for it (`skeleton.stageBox`), the one way the three files alone carry the stage exactly ([AUTHORING §0.4](docs/AUTHORING.md)) |
560
561
  | `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
561
562
  | `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
562
563
  | `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid |
@@ -575,9 +576,9 @@ first three work on any reference you have, and `bench` is a repository workflow
575
576
  and `bun run fetch-examples`. The reasoning behind them is in
576
577
  [the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
577
578
 
578
- `build` and `validate` both default to `--profile spine` — the 34 validity rules, which
579
+ `build` and `validate` both default to `--profile spine` — the 35 validity rules, which
579
580
  ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
580
- adds all 50: the other 16 are one renderer's policy and one canvas budget's, and they
581
+ adds all 51: the other 16 are one renderer's policy and one canvas budget's, and they
581
582
  fire on perfectly correct editor-produced Spine data, which is why they are opt-in.
582
583
  ⇒ **That reason is about foreign data and does not carry to a rig you are authoring
583
584
  yourself: author under `--profile spine-html` and read the extra 16 as findings, and
@@ -649,7 +650,12 @@ specs are still written — a spec plus a list of what is missing from it beats
649
650
  `diff` reports the box and gates nothing on it. 🔁 The box an export's header carries
650
651
  is its setup-pose bounding box, and it becomes the rebuild's *stage*; the rebuild's own
651
652
  header is computed again, because since issue #907 `build` writes the setup-pose
652
- bounding box there — never the stage — held to spine-core's `getBounds`.
653
+ bounding box there — never the stage — held to spine-core's `getBounds`. 📦 A rig
654
+ that has to carry its stage in the Spine files asks for it as a bounding box
655
+ (`skeleton.stageBox`, written by `build` from the stage and held by
656
+ `A50_STAGE_BOX_IS_THE_STAGE`), and `ingest --stage-box <slot>` reads that box back as
657
+ the rebuild's stage — [AUTHORING §3.1](docs/AUTHORING.md) has the field and the
658
+ paragraph for a consumer of the files.
653
659
  - **An animation's duration.** The format has no such field. The largest key time is
654
660
  the only derivable answer and it is what a runtime plays to; it is wrong for an
655
661
  animation that holds its last pose past its last key, so it is recorded as a finding
@@ -728,7 +734,7 @@ letting `A17` blame the editor for the harness's own doing.
728
734
  | 📥 **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
729
735
  | 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
730
736
  | 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | **the Spine 4.3 format, field by field.** Every key the 4.3 JSON parser and atlas reader take, what each defaults to, and the parser line it comes from — the rows rigc's refusals cite by part number. Ships in the package too |
731
- | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 50 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
737
+ | 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 51 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
732
738
  | 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
733
739
  | 🔬 [SURVEY_2026-08-22.md](https://github.com/firejune/rigc/blob/main/docs/SURVEY_2026-08-22.md) | the survey the ladder was climbed by: rigc and the official examples as measured on 2026-08-22, and the gap list ordered by rung. A dated record, not kept current. Repository material |
734
740
  | 🧬 [GENERATIONS.md](https://github.com/firejune/rigc/blob/main/docs/GENERATIONS.md) | **Spine data from another generation.** Why a 3.8–4.2 file read as 4.3 fails in silence, the policy that follows (detect from `skeleton.spine`, never guess, play on the matching runtime), what `A16` and `ingest` do with such a file, and the editor as the migration path. Repository material |
@@ -778,7 +784,7 @@ quality."* All six, with their verdicts, are in
778
784
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
779
785
 
780
786
  The whole dossier — the yardstick, `diff` and `check` and what neither of them can
781
- see, every rung, the run viewer, the 50 assertions and the selftest behind them — is
787
+ see, every rung, the run viewer, the 51 assertions and the selftest behind them — is
782
788
  [docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
783
789
  Live rung status is
784
790
  [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
package/docs/AUTHORING.md CHANGED
@@ -219,19 +219,19 @@ What the flags mean:
219
219
  | --- | --- |
220
220
  | `--rig` | the rig spec — skeleton structure |
221
221
  | `--motion` | the motion spec — time |
222
- | `--out` | directory for `skeleton.json` + `skeleton.atlas`, and on `build` `skeleton.model.json` beside them; atlas page paths and `skeleton.images` are written relative to it. `skeleton.model.json` is rigc's own record of the compiled rig (`rigc-compiled/3`: the stage the header declares, or `null`; the bones, slots, skins, constraints, events and animations the Spine files were emitted from, in the spec's order, and `editorOrder`, the order `skeleton.json` lists the skins, each skin's slot keys and the animations in; the rest of what posing them takes; then `pages`, every page of the `skeleton.atlas` written beside it, its `pma` and its `scale:` line (`null` where it states none), and where each region sits on it — after `--pack` the packed pages, after `--copy-images` the copies' names), written after the same gate as the pair and ending in `spine.sha256`, the digest of the `skeleton.json` written beside it. A `rigc-compiled/2` document (rigc 1.7) states no stage, editor order, `pma` or `scale`, and is still read: `render` and `check` then read those off `skeleton.json` and the atlas, and the `poser` line names both files. A `rigc-compiled/1` document (rigc 1.6) has no `pages` either and is still read: the core then takes the placement from the atlas beside it, the `poser` line says so, and with no atlas there `render` and `check` refuse it naming the file. A `rigc-compiled/2` or `/3` build is rendered and checked with its `skeleton.atlas` gone (`--candidate` below). It is what rigc's own posing core (`src/core/`) poses: `render` and `check` — and `bench --frames`, which folds `check` in — pose a build through it, and `--poser` below says when they do not. On `check` it is a directory of **pictures** instead: one per frame the table lists (every compared frame under `--all-frames`), reference · candidate · difference · overlay at the comparison grid's native size, as `<dir>/<set>/f####.png` beside a `frames.json` that says what they are pictures of. Each `<dir>/<set>/` is cleared first; a file at `<dir>`, or a directory that is or holds `--frames`, is refused by name — **§9.2.1** |
222
+ | `--out` | directory for `skeleton.json` + `skeleton.atlas`, and on `build` `skeleton.model.json` beside them; atlas page paths and `skeleton.images` are written relative to it. `skeleton.model.json` is rigc's own record of the compiled rig (`rigc-compiled/3`: the stage the header declares, or `null`; the bones, slots, skins, constraints, events and animations the Spine files were emitted from, in the spec's order, and `editorOrder`, the order `skeleton.json` lists the skins, each skin's slot keys and the animations in; the rest of what posing them takes; then `pages`, every page of the `skeleton.atlas` written beside it, its `pma` and its `scale:` line (`null` where it states none), and where each region sits on it — after `--pack` the packed pages, after `--copy-images` the copies' names), written after the same gate as the pair and ending in `spine.sha256`, the digest of the `skeleton.json` written beside it. A `rigc-compiled/2` document (rigc 1.7) states no stage, editor order, `pma` or `scale`, and is still read: `render` and `check` then read those off `skeleton.json` and the atlas, and the `poser` line names both files. A `rigc-compiled/1` document (rigc 1.6) has no `pages` either and is still read: the core then takes the placement from the atlas beside it, the `poser` line says so, and with no atlas there `render` and `check` refuse it naming the file. A `rigc-compiled/2` or `/3` build is rendered and checked with its `skeleton.atlas` gone (`--candidate` below). It is what rigc's own posing core (`src/core/`) poses: `render` and `check` — and `bench --frames`, which folds `check` in — pose a build through it, and `--poser` below says when they do not. On `check` it is a directory of **pictures** instead: one per frame the table lists (every compared frame under `--all-frames`), reference · candidate · difference · overlay at the comparison grid's native size, as `<dir>/<set>/f####.png` beside a `frames.json` that says what they are pictures of. Each `<dir>/<set>/` is cleared first; a file at `<dir>`, or a directory that is or holds `--frames`, is refused by name — **§9.2.1**. On `repack` it is the directory the repacked build is written into — the same files `build --pack` writes — and it has to be absent or empty — **§0.4** |
223
223
  | `--copy-images` | `build` only: also copies every page **the emitted atlas names** into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own, and points `skeleton.images` at `--out` itself so the editor's import finds the parts beside the skeleton (§3.1 says why it is spelled `../<out>/` and not `./`). Under `--atlas-in` those pages are the pack's, not one per part (**§0.2**). Without it, page paths point at the source art |
224
224
  | `--pack` | `build` only: arrange every part onto **shared** atlas page(s), written into `--out` as real PNGs, instead of one page per part. Lossless — nothing is resampled, trimmed or rotated. The default is one page per part — **§0.1** |
225
- | `--page-size` | `build --pack` only: the largest page edge (default `2048`). A ceiling, not the size: page edges are powers of two and the one written is the smallest that holds the pack — **§0.1** |
226
- | `--padding` | `build --pack` only: the gutter each region reserves on every side (default `2`), filled by extending the region's own edge pixels outwards. `0` is not a legal-but-tight choice, it is bleed — **§0.1** |
227
- | `--page-edges` | `build --pack` only: `pot` (default) or `free`. `pot` keeps both page edges powers of two; `free` sizes the page to the parts — a smaller page, at the cost of region attachments sampling within one least significant bit of the loose build instead of exactly. Any other value is refused by name — **§0.1** |
228
- | `--pack-shape` | `build --pack` only: `rect` (default) or `polygon`. `rect` keeps every region's cell apart; `polygon` packs a region that only meshes draw by its emitted hull, so a neighbour may sit inside its rectangle where the hull is not, with the padding kept between footprints. A region attachment's footprint stays its rectangle. Any other value is refused by name, and so is the flag without `--pack` — **§0.1** |
225
+ | `--page-size` | `build --pack` only: the largest page edge (default `2048`). A ceiling, not the size: page edges are powers of two and the one written is the smallest that holds the pack — **§0.1**. `repack` takes it with the same meaning — **§0.4** |
226
+ | `--padding` | `build --pack` only: the gutter each region reserves on every side (default `2`), filled by extending the region's own edge pixels outwards. `0` is not a legal-but-tight choice, it is bleed — **§0.1**. `repack` takes it with the same meaning — **§0.4** |
227
+ | `--page-edges` | `build --pack` only: `pot` (default) or `free`. `pot` keeps both page edges powers of two; `free` sizes the page to the parts — a smaller page, at the cost of region attachments sampling within one least significant bit of the loose build instead of exactly. Any other value is refused by name — **§0.1**. `repack` takes it with the same meaning — **§0.4** |
228
+ | `--pack-shape` | `build --pack` only: `rect` (default) or `polygon`. `rect` keeps every region's cell apart; `polygon` packs a region that only meshes draw by its emitted hull, so a neighbour may sit inside its rectangle where the hull is not, with the padding kept between footprints. A region attachment's footprint stays its rectangle. Any other value is refused by name, and so is the flag without `--pack` — **§0.1**. `repack` takes it with the same meaning, and always packs — **§0.4** |
229
229
  | `--atlas-in` | `build` and `explain`: resolve every part against the **regions of a pre-packed `.atlas`** instead of against loose PNGs. Region geometry is read from the file and sizes are descaled by the page's `scale:`; `build` re-emits the atlas into `--out`, re-anchored, and `explain` writes nothing and poses through it — **§0.2**. On `explain` it is the flag that makes a **size-only** spec readable at all (`ingest --art none`), because posing resolves every attachment against an atlas; without it that pair is refused by name rather than thrown through (§5.1) |
230
230
  | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** — every `.png` in it is a part, in name order. For `chainfit` it is only where each attachment's image name **resolves**: the candidate decides what the parts are, so extra PNGs are unused and a missing name is refused by name (§12.3) |
231
231
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
232
232
  | `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
233
233
  | `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
234
- | `--profile` | `spine` = the 34 validity rules (**the default**) · `spine-html` = all 50, opt-in |
234
+ | `--profile` | `spine` = the 35 validity rules (**the default**) · `spine-html` = all 51, opt-in |
235
235
  | `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. A directory holding no `skeleton.json`, or a path with nothing at it, is refused `nothing at <path>`, exit 2, nothing written, by every command that takes a build — `validate <dir>` and a directory given to `diff` included (`RC27`). `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
236
236
  | `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
237
237
  | `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
@@ -962,6 +962,7 @@ repository builds on every run.
962
962
  | `--art none` | state `width`/`height` only, so the rebuild is `build --atlas-in <pack.atlas>` and every part resolves out of the pack |
963
963
  | `--images <dir>` | **write** the rig spec's own `images` directory, spelled relative to `--out`, so the rebuild is a plain `build --rig … --motion … --out …`. Without it the field is left out and every `image` resolves against `--out` itself, which holds the specs and no art — so every rebuild has to repeat `build --images <dir>`. Refused together with `--art none`, which writes no `image` for it to be the base of |
964
964
  | `--stage x,y,w,h` | a stage to **add** to a skeleton whose header carries no box (a header's box — its setup-pose bounding box — is otherwise read as the rebuild's stage) — without it the absence is carried as `"width": null, "height": null`. An editor export *may* be such a file; every export under `examples/` carries a box and `ingest` reads it straight through — so passing the flag at one of them is **refused**, naming both boxes, rather than silently doing nothing |
965
+ | `--stage-box <slot>` | read the stage from the bounding box in that slot — what `build` writes for a rig that states `skeleton.stageBox` (§3.1) — in place of the header's box, which is the setup-pose bounding box. The rebuilt spec asks for the same box (`skeleton.stageBox`) rather than transcribing it, so a rigc build that carries one rebuilds byte for byte from `skeleton.json` and its atlas alone. Only a box `build` could write back is read; everything else in the slot is refused by name (below). Without the flag no slot is read as the stage because of its name |
965
966
  | `--name <n>` | the rig spec's `name`, which the motion spec's `archetype` must equal (default: the file's basename) |
966
967
 
967
968
  ⚠️ **`ingest --images` and `build --images` point opposite ways.** `build --images`
@@ -998,6 +999,28 @@ editor's command-line import and export carried a header box through **verbatim*
998
999
  a forged `0, 0, 1, 1` came back as `width 1, height 1` — so the editor does not
999
1000
  recompute it on a round trip either; whatever box a file states travels.)
1000
1001
 
1002
+ 📦 **`--stage-box <slot>`: the stage a rig carried in its Spine files** (issue #1168). A
1003
+ build whose rig asked for `skeleton.stageBox` carries its stage as a bounding box, and
1004
+ a caller holding only `skeleton.json`, the atlas and its pages names the slot. The
1005
+ box's four corners are the rebuild's stage — read in place of the header's box, and
1006
+ printed as `stage read from the bounding box in slot "…" (--stage-box)` — and the
1007
+ spec states `skeleton.stageBox` instead of an attachment, so `build` writes the box
1008
+ again from the stage. Refused by name, as an `IngestError` before anything is written:
1009
+
1010
+ | what the slot holds | the refusal names |
1011
+ | --- | --- |
1012
+ | no such slot | *the skeleton has no slot "…"; it declares [...]* |
1013
+ | a slot on a bone below the root, or on a root that states a setup transform | *which is not the root* / *which states rotation 90* |
1014
+ | no attachment, several, or one in a skin other than `default` | *the default skin holds no attachment …* / *is also filled by skin(s) …* |
1015
+ | an attachment that is not a bounding box, states another `name`, or is not four unweighted vertices | *is a "clipping", not a bounding box* / *states vertexCount … the stage is four unweighted corners* |
1016
+ | four vertices that are not one axis-aligned rectangle of positive size | *which is not the four corners of one axis-aligned rectangle of positive size* |
1017
+ | `--stage` beside it, or a model document beside the skeleton stating another stage | *two sources for one value* / *two statements of one stage that disagree* |
1018
+
1019
+ A box `build` spells differently — its corners in another order, the `color` the
1020
+ editor writes on every box it exports — is read, and is the lossy
1021
+ `STAGE_BOX_REWRITTEN` ([INGEST.md](INGEST.md) §2.0): the rebuild carries the same
1022
+ rectangle, spelled `build`'s way.
1023
+
1001
1024
  ⛔ **The flag is refused beside a box the file states.** Two sources for one value, and
1002
1025
  the file is the one that was measured — so `ingest` names both boxes and stops rather
1003
1026
  than writing one of them and saying nothing. Drop the flag, or correct `skeleton` in the
@@ -1250,6 +1273,195 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
1250
1273
  `both-unacceptable` is the one that means *propose again* rather than *adopt
1251
1274
  either* — check for it before you treat a ballot as settled.
1252
1275
 
1276
+ ### 0.4 Repacking a build whose parts were not kept — `repack`
1277
+
1278
+ A build directory holds `skeleton.json`, `skeleton.atlas`, the pages and
1279
+ `skeleton.model.json`, and a project that keeps only those — no loose part PNGs —
1280
+ could not take up a packer change (`--page-edges free`, `--pack-shape polygon`)
1281
+ without regenerating its parts upstream. `rigc repack` packs such a build again,
1282
+ from its own output:
1283
+
1284
+ ```bash
1285
+ bun cli.ts repack spine/ --out spine.free/ --page-edges free --pack-shape polygon
1286
+ # .. lift 10 region(s) off 1 page(s), each named once: 0 turned on its page, 0 trimmed of whitespace — …
1287
+ # .. stage 640 x 700 at 0,0 — read from the skeleton.model.json beside it (its spine.sha256 is this skeleton's). …
1288
+ # .. check (a) regions: 10 of 10 pixel-identical, by name, to the same region lifted off the input's pages — 7 over the whole rectangle, 3 that only meshes draw over the footprint …
1289
+ # .. check (b) skeleton.json: byte-identical to the input's — the pack owns none of it
1290
+ # .. check (c) gate: green — the report above, on the compile and on the packed pair
1291
+ # rigc: wrote /abs/path/spine.free/skeleton.png
1292
+ ```
1293
+
1294
+ It is three steps, run in process through the tree's own functions: every region
1295
+ is **lifted** off its page by its atlas bounds (`extractRegion`, the inverse of
1296
+ what the runtime samples — `rotate:` and `offsets:` included), the skeleton is
1297
+ **read back** with `ingest --art loose` (an `image` per attachment, named after the
1298
+ region it draws, which is the file the lift wrote), and **`build --pack`** runs over
1299
+ the two specs and the lifted parts — the same body, the same gate, and on each entry
1300
+ that entry's gate (§0's paragraph on the two entries). They run in a work directory
1301
+ under the system temp directory that the command removes when it ends, green or
1302
+ refused or a gate that exits; only the build is copied into `--out`, so the output is
1303
+ the same kind of directory as the input and can itself be repacked. The lifted parts
1304
+ are not kept: the output's pages hold every one of them, and `repack` is the command
1305
+ that lifts them.
1306
+
1307
+ | flag | what it decides |
1308
+ | --- | --- |
1309
+ | `<dir \| skeleton.json>` | the build to repack — a directory holding `skeleton.json` and `skeleton.atlas`, or a `skeleton.json` path |
1310
+ | `--out <dir>` | where the repacked build is written: `skeleton.json`, `skeleton.atlas`, `skeleton.model.json` and the pages. **Absent or empty**, and refused otherwise (exit 2): a page an earlier pack wrote and this one does not would stay beside the new atlas with nothing naming it — and the input's own directory is never empty |
1311
+ | `--atlas <path>` | the atlas, when it is not `skeleton.atlas` beside the skeleton |
1312
+ | `--page-size`, `--padding`, `--page-edges`, `--pack-shape` | `build --pack`'s, with the same defaults and the same meaning (§0.1). `repack` always packs |
1313
+ | `--profile` | the gate's profile, as on `build` |
1314
+ | `--stage x,y,w,h` | handed to `ingest` as its own `--stage` (§0.3) — a box for a skeleton that states none, refused beside one that does |
1315
+ | `--stage-box <slot>` | handed to `ingest` as its own `--stage-box` (§0.3): the stage read from the bounding box a build writes for a rig that states `skeleton.stageBox` — the exact way to carry the stage through a repack of the three files alone (*The stage*, below). `ingest`'s refusals reach the caller in its own words |
1316
+ | `--accept-skeleton-differences` | write the rebuild even where its `skeleton.json` differs from the input's, with **every** differing path printed and both values (default: refused naming them, check (b) below). Nothing else relaxes: check (a) and the gate hold exactly as without it. Beside a skeleton that comes back byte-identical, check (b)'s line says the flag accepted nothing |
1317
+
1318
+ Every other flag `build` takes is refused by name, with the reason `repack` does not
1319
+ take it (`--rig`, `--motion`, `--images`, `--manifest`, `--copy-images`, `--pack`,
1320
+ `--atlas-in`, `--cut`, `--cuts`); any other flag is refused listing the ones it takes.
1321
+
1322
+ **Nothing reaches `--out` until three things are shown**, each on its own line:
1323
+
1324
+ - **(a) every region** lifted off the **new** pages is pixel-identical, by name, to
1325
+ the same region lifted off the **input's**. Under `--pack-shape rect` that is the
1326
+ whole rectangle. Under `polygon` a region that only meshes draw owns its
1327
+ **footprint** on the page — its hull and triangles, dilated by the padding — and a
1328
+ neighbour may sit in the rest of its rectangle by design (§0.1), so that region is
1329
+ compared over the footprint the packer kept as its own (`packFootprints` and
1330
+ `footprintCell`, the packer's own two functions). Measured: a whole-rectangle
1331
+ comparison refused `gallery/flex` repacked `--page-edges free --pack-shape polygon`
1332
+ on 4 of its 20 regions, each one a neighbour's texels in a mesh's empty corner.
1333
+ The lift carries those texels, so a later `rect` repack of a polygon build keeps
1334
+ them where nothing samples them: `gallery/nod` taken `pot`/`rect` → `free`/`polygon`
1335
+ → `pot`/`rect` comes back with its original `skeleton.json` and `skeleton.atlas`,
1336
+ and a page that differs only in one mesh-only region outside its footprint.
1337
+ A region the input atlas has and the rebuild does not is named here too — a
1338
+ rebuild packs the regions this skeleton's attachments draw and no other, so an
1339
+ atlas several skeletons share comes back short;
1340
+ - **(b) `skeleton.json`** is byte-identical to the input's. A repack is entitled to
1341
+ change **nothing** in the skeleton: attachments name their regions and the atlas
1342
+ alone says where a region sits, so the pack owns the atlas and the pages and none
1343
+ of the skeleton. Measured on every repack below, the rebuilt skeleton was the
1344
+ input's bytes. A difference is refused naming each path with both values — it is
1345
+ something other than the pack — and the refusal names
1346
+ `--accept-skeleton-differences`, which writes the rebuild anyway and prints every
1347
+ differing path as `N place(s) differ from the input, accepted by
1348
+ --accept-skeleton-differences`. The flag is general on purpose: the command can say
1349
+ **where** two skeletons differ and never **why**, so a switch for one cause (the
1350
+ header box below) would be a guess about the cause wearing a flag's name;
1351
+ - **(c) the gate** is green — `build --pack`'s two reports, on the compile and on the
1352
+ packed pair, printed above the three lines.
1353
+
1354
+ Under the settings the input was packed with, a fourth line says the atlas and every
1355
+ page came back byte-identical to the input's — **40 of 40** such repacks below. That
1356
+ is a measurement, not the guarantee: under other settings the pages differ by design,
1357
+ and what is held is (a), (b) and (c). `skeleton.model.json` is the rebuild's own
1358
+ record — of a rig read back out of the skeleton, its `rig.archetype` the skeleton
1359
+ file's basename as `ingest` names it, its `pages` the new pack — and is not the
1360
+ input's bytes; its `spine.sha256` names the `skeleton.json` beside it, as every
1361
+ build's does.
1362
+
1363
+ **What it was measured on.** The seven gallery rigs, each built `--pack` under
1364
+ `pot`/`free` × `rect`/`polygon` and under `--page-size 1024` (two pages for two of
1365
+ them; two more do not fit a 1024 page and are not built), each repacked under its own
1366
+ settings and under `free` + `polygon`; each built without `--pack` (one page per part,
1367
+ pages outside the directory) and repacked; and each `pot`/`rect` build reduced to its
1368
+ three kinds of file, with no `skeleton.model.json`. **80 of 80 repacked green**,
1369
+ every region pixel-identical, every skeleton the input's bytes, at about 1.4 s a rig.
1370
+
1371
+ **What it accepts and what it refuses.** Measured over those 80 inputs and the twelve
1372
+ editor exports under `examples/` — each repacked directly, and each first rebuilt by
1373
+ rigc through `ingest --art none` and `build --atlas-in` (the shape a rigc build that
1374
+ carries an editor's pack has) — and, for a shape refused for one reason, with that
1375
+ refusal set aside to see whether the lift alone was exact:
1376
+
1377
+ | atlas shape | occurs in | lifts exactly | repack |
1378
+ | --- | --- | --- | --- |
1379
+ | rigc's own pack, any `--page-edges`/`--pack-shape`, one page or several | the 80 gallery inputs | yes | **accepted** |
1380
+ | one part per page, pages outside the build directory (`build` without `--pack`) | the gallery, built unpacked | yes | **accepted** |
1381
+ | a region turned on its page (`rotate: 90`, `270`; `180` read the same way) | three editor exports, once each | yes — the turned regions of `5-squash-and-stretch` (90) and `7-anticipation` (270) came back identical, and the selftest lays a whole rig turned and trimmed (`RPK09`) | **accepted**: the pack writes it unturned |
1382
+ | a region trimmed of transparent border (`offsets:`) | one editor atlas (`spineboy-run`, refused for the reasons below) | yes — `RPK09` | **accepted**: lifted at its full drawing size, transparent where trimmed |
1383
+ | a region two attachments share | three editor exports | yes — lifted once | **accepted** |
1384
+ | a page `scale:` other than 1 | all twelve editor exports (0.4, 0.5) | yes — every region the skeleton draws came back identical | **refused**: rigc's packer writes every page at scale 1, so the repacked atlas would state texels coarser than the drawings as the drawings' own size |
1385
+ | `pma: true` | `spineboy-run` | — | **refused**: rigc's packer writes `pma: false`, and the same bytes would be blended as straight colour |
1386
+ | a region the atlas names twice | `spineboy-run` (a numbered series) | — | **refused**: one part per name, and the runtime draws the first of two |
1387
+ | `index:` on a region | `spineboy-run` | — | **refused**: rigc's packer writes no `index` line |
1388
+ | `filter` other than `Linear, Linear`; `format`; `repeat`; `split`/`pad`; a `rotate:` value outside `0/90/180/270/true/false`; any other line | none here | — | **refused**: the repacked atlas would drop or contradict it |
1389
+ | regions no attachment of this skeleton draws | three editor atlases each serving two skeletons | yes | **refused by (a)**: they do not come back |
1390
+ | an editor's `skeleton.json` itself | the twelve exports, repacked directly | — | **refused by (b)** were it reached: the rebuild writes rigc's `spine` version, drops `hash`, and computes the header box again (§0.3) |
1391
+
1392
+ ⚠️ **Why the loose-plate route was refused by `A08` on editor exports, and why this
1393
+ one is not.** Rebuilding a skeleton from loose parts after `ingest --art none` — the
1394
+ spec an `--atlas-in` build is made from — fails `A08_REGION_NAMES_MATCH_ATTACHMENTS`
1395
+ on every attachment: a `none` spec states each attachment's size and names **no
1396
+ `image`**, so `--images <dir>` resolves nothing (`0 part page(s)`) and no region exists
1397
+ for any attachment to join. Measured on `3-timing-and-spacing`: both attachments
1398
+ refused, exactly that way. It is the spec's art mode, not a shape of the atlas —
1399
+ not the region names, not stripped whitespace, not the page scale. `repack` reads
1400
+ the skeleton with `--art loose`, which names an `image` per attachment after the
1401
+ region it draws; the twelve exports rebuilt that way from their lifted regions all
1402
+ built green.
1403
+
1404
+ **A build written before 2.2.0.** Until 2.2.0 `build` wrote the rig's **stage** into
1405
+ the header's `x`, `y`, `width`, `height`; since #907/#1117 it writes the setup-pose
1406
+ bounding box there. Such a build repacks with every region identical and is refused by
1407
+ (b) on those four fields and nothing else — measured on a consumer's 35 production
1408
+ builds kept as the three kinds of file: (a) held on all 35, (b) refused 34 (the one
1409
+ that passed had been rebuilt with 2.10.1), every refusal the same four paths, for
1410
+ example `skeleton.width: 832 in the input, 321.00003 in the rebuild`. With
1411
+ `--accept-skeleton-differences` the repack is written, the four paths are printed as
1412
+ accepted, and the output's header is the setup-pose box, as any build of this rigc's
1413
+ has. This is the one case where `ingest`'s header rule gives the true stage **by
1414
+ construction**: the header it reads *is* the stage the build was made with, so the
1415
+ stage line's box is the rig's own, and `skeleton.model.json` carries it from then on.
1416
+ ⚠️ After the repack the header is no longer the stage, and a consumer that read the
1417
+ header as its stage must stop: read `skeleton.model.json`'s `stage` — or the stage
1418
+ box #1168 adds to the build — instead. The selftest builds this shape from a gallery
1419
+ rig's own document (`RPK13`–`RPK15`).
1420
+
1421
+ **The stage.** `ingest`'s rules decide it, and the `stage` line says which applied.
1422
+ A repack learns the stage one of three ways, and each is exact in its own case:
1423
+
1424
+ - **`--stage-box <slot>`** — the bounding box a build writes for a rig that states
1425
+ `skeleton.stageBox` (§3.1, issue #1168), read by `ingest --stage-box`. Exact for
1426
+ any build that carries one, from the three files alone: the rebuilt spec asks for
1427
+ the same box, the skeleton comes back byte for byte, and `skeleton.model.json`'s
1428
+ `stage` is the declared one (`RPK17`). `ingest`'s refusals reach the caller as
1429
+ `rigc repack: ingest refused the skeleton: …` (exit 2, nothing written) — a slot that
1430
+ is not such a box, `--stage` beside it, or a `skeleton.model.json` beside the build
1431
+ that states another stage (`RPK19`);
1432
+ - **the `skeleton.model.json` beside the skeleton**, when its `spine.sha256` is the
1433
+ skeleton's digest. Exact for any rigc build that kept its document;
1434
+ - **the header's box**, when neither is there. On a build written before 2.2.0 that box
1435
+ is the stage itself (above), so this is exact there by construction; on a rigc build
1436
+ since 2.2.0 it is the **setup-pose bounding box**, not the stage, and nothing in the
1437
+ skeleton says which — `gallery/nod`'s `640 x 700` stage reads back as `640 x 725`
1438
+ without its document. The line says so, and names `--stage-box`.
1439
+
1440
+ ⚠️ A build that carries a stage box, repacked **without** `--stage-box` and without its
1441
+ document, is written with its skeleton byte for byte — the box is transcribed as an
1442
+ ordinary bounding-box attachment and written back the same — but the rebuilt
1443
+ `skeleton.model.json`'s stage is the header's setup-pose box and carries no box
1444
+ (`RPK18`). Check (b) cannot see that, because the stage is not in the skeleton's bytes
1445
+ either way; the stage line is where it shows. Pass `--stage-box <slot>` for such a
1446
+ build. (`--stage` adds a box only to a skeleton that states none, which is `ingest`'s
1447
+ rule.)
1448
+
1449
+ **What each refusal says, and what to change:**
1450
+
1451
+ | `rigc repack: …` | exit | change |
1452
+ | --- | --- | --- |
1453
+ | `<atlas> cannot be lifted exactly — N reason(s), and nothing was written:` then one line per reason (`page … is not on disk`, `page … declares size … and … is …`, `page … states scale: …`, `… pma: true`, `region … is named twice`, `regions … differ only in case`, `region … states index: …`, `region … occupies … which leaves the … page`, `region … keeps … which does not fit inside it`, `region … is not a relative file name …`) | 1 | the atlas or its pages: put the page back, or repack the build a rigc pack wrote — a foreign pack with those shapes is rebuilt from its parts, not repacked |
1454
+ | `the atlas and skeleton.model.json beside it disagree — region "…" x: … in the atlas, … in the document` | 1 | one of the two was edited after the build wrote them: restore the atlas, or drop the document if the atlas is the record you mean |
1455
+ | `the repack lost something, so nothing was written to <out>:` then `(a) …` region lines | 1 | a region that does not come back, or a texel apart: the input atlas carries regions this skeleton does not draw (repack the build that has only its own), or the lift was not what the pack drew from |
1456
+ | the same, then `(b) skeleton.json: N place(s) differ from the input, which the pack does not own:` and the paths | 1 | the skeleton is not what this rigc writes from what it states. Read the paths: if the rebuild is the skeleton you want — a build written before 2.2.0, whose four header fields are its stage, is the measured case — pass `--accept-skeleton-differences`; otherwise rebuild it yourself (`ingest`, then `build`) |
1457
+ | `ingest found N blocker(s) …` / `ingest refused the skeleton: …` | 1 / 2 | the skeleton cannot be read back exactly (§0.3's findings); `--stage` beside a stated box is `ingest`'s refusal |
1458
+ | `--out <dir> is not empty …` / `is a file …` | 2 | name a new directory |
1459
+ | `repack takes no --<flag>: …` | 2 | drop the flag |
1460
+
1461
+ A red gate is `build`'s own report and its `rigc: N assertion(s) failed — nothing
1462
+ written`, followed by `rigc repack: nothing was written to <out>; the work directory
1463
+ … was removed`.
1464
+
1253
1465
  ---
1254
1466
 
1255
1467
  ## 1. The two files
@@ -1567,6 +1779,7 @@ that is a defect in this guide: report it.
1567
1779
  | `referenceScale` | 4.2+ physics/scale reference: a physics constraint's `wind` and `gravity` act over it | parser default 100. Carried as stated — never rounded — and the header leaves it out at exactly 100. `skeleton.model.json` always states it, as `referenceScale` after `spec`: the number stated, or 100 when none is — the value the Spine file is read as — and rigc's posing core steps wind and gravity over it (issue #958) |
1568
1780
  | `images` | where the editor's import looks for the part PNGs, as a path from the skeleton file | **written for you**: under `--copy-images` the `--out` directory itself, spelled `../<its basename>/` (a literal `./` is dropped by the editor on import; a named directory is kept and every part is found — measured on 4.3.23); otherwise the relative path from `--out` to the one directory the spec names every part PNG in (the rig's images directory, or the manifest's plates). A declared value is carried through verbatim — and overridden by `--copy-images`, which moved the parts. Parts spread over several directories have no single true path, so nothing is written |
1569
1781
  | `audio` | nonessential: where the editor looks for the skeleton's audio files, as a path from the skeleton file — a string, or `null` for none | **not written unless you state it.** rigc has no audio to point at, so this is a value a spec states or does not; stated, it is carried verbatim, `null` included, because `null` is what an editor export writes when no audio folder is set (all twelve under `examples/` do) and `ingest` carries it from there. Anything but a string or `null` is a compile error naming the value |
1782
+ | `stageBox` | **rigc's own field**: carry the stage in the Spine files as a bounding-box attachment — `{ "slot": …, "attachment": … }`, see *The stage in the shipped files* below | absent: no box, and no byte of any build moves. Both names required; the numbers are never yours to write |
1570
1783
 
1571
1784
  `spine` and `hash` are not yours to write: rigc emits its own version label
1572
1785
  (`A16` re-checks it is on the 4.3 line) and inventing a hash would claim an export
@@ -1605,6 +1818,62 @@ figure's area on a figure that fills a quarter of its crop. Now:
1605
1818
 
1606
1819
  You do not author the box and there is no field for it: it is a measurement of the rig.
1607
1820
 
1821
+ 📦 **For a consumer of the Spine files: the header is not the stage.** Up to 2.1.3
1822
+ `skeleton.json`'s header `x`/`y`/`width`/`height` held the rig's stage; from 2.2.0 they
1823
+ hold the setup-pose bounding box, as the format defines them — a reader that fits a
1824
+ rig by taking the header for the stage is mis-sized in silence (one consumer's rig
1825
+ stood 1.108 times larger and shifted up, with no gate failing). The stage is in the
1826
+ rig spec and in `skeleton.model.json`'s `stage`, and neither ships with a Spine
1827
+ runtime's three files. A rig that has to carry its stage there asks for it:
1828
+
1829
+ ```json
1830
+ "skeleton": { "width": 832, "height": 1216, "stageBox": { "slot": "stage", "attachment": "stage" } },
1831
+ "slots": [ …, { "name": "stage", "bone": "root", "attachment": "stage" } ]
1832
+ ```
1833
+
1834
+ and `build` writes a `boundingbox` attachment named `stage` in the `default` skin
1835
+ under slot `stage`, whose four vertices are the stage's corners in Spine world —
1836
+ `(x, y)`, `(x + width, y)`, `(x + width, y + height)`, `(x, y + height)`, bottom-left
1837
+ first and counter-clockwise, y up. The consumer reads it with the runtime it already
1838
+ has: the slot's attachment by name (`skeletonData.defaultSkin.getAttachment(slot.index,
1839
+ "stage")` in spine-core), whose `vertices` are those four corners, or the slot's
1840
+ bounding box at the setup pose (`SkeletonBounds`), and fits the rig to that. A key
1841
+ the format does not define would be read by no runtime and dropped by the editor; a
1842
+ bounding box is returned by every runtime by slot and name, in JSON and in binary,
1843
+ and comes back from the editor with the same vertices (measured: a build carrying one
1844
+ imported into Spine 4.3.26 and exported again, the box `[0, 0, 640, 0, 640, 700, 0,
1845
+ 700]` came back as it went, the editor adding only its own `color`).
1846
+
1847
+ - **The numbers are the stage's**, never typed: the field names where the box goes,
1848
+ `build` writes what is in it. Absent, no build writes one and no byte moves.
1849
+ - **The slot is yours to declare** in `slots` — where it stands is its place in the
1850
+ draw order, and a bounding box draws nothing — and **nothing else may fill it**: the
1851
+ box is its slot's one attachment. Its setup pose is stated the way any slot's is
1852
+ (the slot's `attachment`, or the motion spec's `setup`); name the box there and a
1853
+ reader of the slot's current attachment sees it at setup.
1854
+ - **The slot hangs on the root, and the root states no setup transform** — no `x`,
1855
+ `y`, `rotation`, `scale`, `shear`. The vertices are then the stage's own numbers,
1856
+ readable off the attachment without posing anything. A bone below the root is
1857
+ refused because the runtime spells an unrotated frame's `b` as cos 90° at its pi,
1858
+ −2.3e-8, once per level: on the root that residue is the one the header's own box
1859
+ already carries (gallery/nod's header `x` is `-0.000016`, 700 × −2.3e-8), and the
1860
+ gate holds the posed box through exactly that frame; below it, it compounds.
1861
+ - **The claim is the setup pose's.** An animation that keys the root moves the box
1862
+ with everything else on it; the stage is the attachment's vertices, or the box read
1863
+ at setup.
1864
+ - **The header box does not move**: a bounding box is not counted in it (above).
1865
+ - **`A50_STAGE_BOX_IS_THE_STAGE`** holds the box to the stage on both suppliers — the
1866
+ stored vertices exactly, and the posed box at setup on the header's grid — and
1867
+ SKIPs a rig that does not ask (§5.2). `ingest --stage-box <slot>` reads it back as
1868
+ the stage (§0.3).
1869
+
1870
+ Refused at compile, each naming `skeleton.stageBox`: a slot `slots` does not declare
1871
+ (*slot "…" is not one the rig declares*); a slot something else fills (*is also
1872
+ filled by …*); a slot on a bone below the root (*which is not the root*); a root that
1873
+ states a setup transform (*states a setup transform (rotation 90)*); a stage declared
1874
+ absent (*declares no stage (width: null, height: null) — a box around nothing*); a
1875
+ missing name (*states no "attachment"*).
1876
+
1608
1877
  ⭐ **A skeleton may declare no stage, and saying so is not the same as saying
1609
1878
  nothing.** Write the pair as `null`:
1610
1879
 
@@ -6149,6 +6418,7 @@ Fix A00 and run it again.
6149
6418
  | `A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | an ik constraint resting at `mix` 0 that no animation keys **away from 0** (§4.9, §4.12). `IkConstraint.update` returns on `mix === 0`, so it sits in the update cache and moves nothing. The keys are read the way `A23`/`A36`/`A37` read theirs — every value the loaded timeline poses on its `mix` channel, Bezier samples included — so a timeline keying 0 only is no rescue: [measured] it poses every bone exactly where the same rig with no constraint does. Live is the runtime's `!== 0`, so a negative mix is not refused. `ik constraint "C" has mix 0 at setup and none of the 1 animation keys its mix above 0; update() returns on mix 0, so "upper" never reaches for "goal" — rest it above 0, or key its mix above 0 in an animation, or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "C", "type": "ik", "why": … }]`. A constraint the rig spec declares in `invariants.consumerDrivenMix` (§3.7) is not measured, and one the file also switches on is refused: `ik constraint "C" is declared in the rig spec as invariants.consumerDrivenMix, and the file already switches it on — … — so the declaration exempts nothing; drop the entry`. **SKIP** when the skeleton declares no ik constraint, and **SKIP by name** when every ik constraint it declares is declared consumer-driven: `every ik constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "C" (why: …): it rests muted and none of the 1 animation keys its mix above 0`. With a live one beside it the rule measures that one and the stats line names the declared (`ikConsumerDriven`) |
6150
6419
  | `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | a transform constraint none of whose mixes **for a property it drives** is away from 0 at setup or on any value an animation poses (§4.10, §4.12), or one whose `properties` name no `to` at all. A property is applied only when its own mix `!== 0`, and a key that omits a mix reads it as 1, so the six-mix early return of `TransformConstraint.update` would take a key of `mixRotate: 0` alone as a rescue — [measured] that key, and one keying `mixX` 1 on a rotate-only constraint, pose every bone exactly where no constraint does. A negative mix runs, and five transforms in the editor's example exports rest at −1. `transform constraint "C" drives rotate and has mixRotate 0 at setup, and none of the 1 animation keys its mix above 0; a mix is read only for a property the constraint drives, and update() skips each one at 0, so nothing ever moves "follower" — rest mixRotate above 0, or key its mix above 0 in an animation, or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "C", "type": "transform", "why": … }]`. A constraint the rig spec declares in `invariants.consumerDrivenMix` (§3.7) is not measured, and one the file also switches on is refused as `A47`'s is; one that drives no property is refused with its own sentence declared or not, since no mix it carries is read by anybody. **SKIP** when the skeleton declares no transform constraint, and **SKIP by name** when every transform constraint it declares is declared consumer-driven: `every transform constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "C" (why: …): it rests muted and none of the 1 animation keys its mix above 0`. With a live one beside it the stats line names the declared (`transformConsumerDriven`) |
6151
6420
  | `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` | renderer | two regions on one page whose rectangles overlap **and** whose footprints overlap — so one drawing samples texels that are the other's. A region's **footprint** is what samples it: a region a mesh names (its `path`, else its name, every frame of a `sequence` included) draws the mesh's hull loop and its triangles, its UVs placed on the page as `MeshAttachment.computeUVs` places them (a **linked mesh** draws its source's); a region a region attachment names, or that nothing names, draws its whole rectangle — and so does a mesh region whose UVs leave 0..1, whose hull or triangles do not index its vertices, or that is turned by `rotate:`, and every region when a `sequence` cannot be read. For two rectangles that is exactly the clause `A06` held until issue #1099 — an editor alias, a sequence frame, two rectangles moved over the same texels are refused as they were — and it accepts what `--pack-shape polygon` writes: a neighbour inside a mesh's rectangle where the hull is not (§0.1). **Touching is not overlapping**: interiors are compared, every comparison `FOOTPRINT_SLACK` (1e-6 texels) in favour of apart. `regions "ear_l" (341,696 112x420) and "mouth" (332,709 96x48) overlap on page "skeleton.png", and so do their footprints — "ear_l" draws the hull of mesh "ear_l" (skin "default", slot "ear_l"), "mouth" draws its whole rectangle, which region attachment "mouth" (skin "default", slot "mouth") draws; what they draw overlaps by 1916.5512 texel(s) of area inside 347,709 81x48. One drawing samples texels that are the other's: move one region clear of the other's footprint, or pack the page again` — `gallery/nod`'s polygon pack with `mouth` moved onto a triangle of the `ear_l` mesh (`PK88`). Nothing about the **padding** is read — the atlas and the model document do not state it — and the `2 · padding` the packer keeps is the packer's own (`PK81`). A renderer rule because two regions over the same texels is what an editor's packer writes for an alias or a sequence: 49 pairs on four of the ten atlases in `examples/`. **SKIP** when the atlas declares no page or no region, and when no page carries two regions — every unpacked build |
6421
+ | `A50_STAGE_BOX_IS_THE_STAGE` | both | the stage box a rig asked for (`skeleton.stageBox`, §3.1) is not the stage the model document states. Read on the data a consumer reads and on the pose it reads: the slot carries, in the `default` skin under the asked name, an unweighted bounding box of four vertices whose **stored** vertices are the stage's corners exactly — `(x, y)`, `(x + w, y)`, `(x + w, y + h)`, `(x, y + h)`, as the float32 the runtime loads — and whose **world** vertices at the setup pose (constraints applied, no physics, no skin set) are those corners as the runtime poses them on an unrotated root, compared on the header's 1e-6 grid at float32. `the stage box (slot "stage", attachment "stage"): vertex 1 is stored as (65, 0) and the stage's corner is (64, 0) — the stage 0,0 64x48, bottom-left first and counter-clockwise`; a root moved at setup reads *vertex 0 poses at (1, 0) at the setup pose and the stage's corner poses at (0, 0) on an unrotated root, on the header's grid — something moves the box's bone at setup (a constraint, or a pose the root does not state)*; a slot the skeleton lacks, no attachment of that name, another type, or a weighted box are each named too. Through spine-core it reads `findSlot`, `defaultSkin.getAttachment` and `computeWorldVertices` on a fresh skeleton after `setupPose()` and `updateWorldTransform(Physics.none)`; on the model side the document's record and rigc's core poser, held to spine-core's poses at tolerance 0 (`PG06`, `PG07`). **SKIP** where nothing asks: *nothing asks for a stage box: no rigc-compiled/3 model document beside this skeleton states one …* — a rig without the field, and any skeleton with no `/3` document beside it, an export among them |
6152
6422
 
6153
6423
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
6154
6424
  clauses are gated by profile.
package/docs/INGEST.md CHANGED
@@ -525,6 +525,20 @@ at the policy rather than implying the file was read.
525
525
  `(reported)`, so no score reads them and an absurd box is green nearly everywhere.
526
526
  ⛔ **The flag is refused beside a box the file states** — two sources for one value,
527
527
  both named, and the file is the record of what was measured;
528
+ 📦 **`--stage-box <slot>` reads the stage a rig carried in its Spine files** (issue
529
+ #1168): a rig whose spec states `skeleton.stageBox` ([AUTHORING §3.1](AUTHORING.md))
530
+ ships its stage as a bounding box in that slot, and a caller who has only
531
+ `skeleton.json`, the atlas and its pages names the slot. The box's four corners are
532
+ read as the stage in place of the header's box, and the rebuilt spec asks for the same
533
+ box rather than transcribing it, so `build` writes it again from the stage — a rigc
534
+ build carrying one rebuilds byte for byte. Only a box `build` could write back is read:
535
+ a slot the skeleton lacks, a slot on a bone other than an unmoved root, a slot holding
536
+ anything but one bounding box in the `default` skin, a box not four unweighted corners
537
+ of one axis-aligned rectangle — each is refused by name, as is `--stage` beside the
538
+ flag, and a model document beside the skeleton stating another stage. A box `build`
539
+ spells differently (its corners in another order, the editor's `color`) is the lossy
540
+ `STAGE_BOX_REWRITTEN`. Without the flag no slot is read as the stage because of its
541
+ name, and the box is transcribed as the attachment it is;
528
542
  - **each animation's duration** — the format has no such field. The largest key time
529
543
  is used, stated in the motion spec's `note`, and recorded as a finding per
530
544
  animation. Edit it if you know the real number.
@@ -571,7 +585,9 @@ under, whether it changes the exit code, what it means, and what to do about it.
571
585
  rather than rejected — so an exit of 1 means *a blocker was recorded*, and both specs
572
586
  are on disk either way. (The one thing `ingest` does refuse outright is an option that
573
587
  contradicts the file, which is not a finding: `--stage` beside a box the skeleton
574
- declares.)
588
+ declares — and, under `--stage-box <slot>`, a slot that holds no box `build` could
589
+ write back, `--stage` beside it, or a model document beside it stating another
590
+ stage (§2.0's stage box, below).)
575
591
 
576
592
  | code | gutter | exit | what it means | what to do |
577
593
  | --- | --- | --- | --- | --- |
@@ -602,6 +618,7 @@ declares.)
602
618
  | `SLOT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a slot | as `BONE_FIELD` |
603
619
  | `SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline the motion spec has no track for. The spec has a track for all six the format has, so what still reaches this line is a name **outside** the format — `sequence` written on a slot rather than an attachment is the likeliest — and the detail says so: the runtime's own reader throws `Invalid timeline type for a slot` on it, so no player loads that file either. The detail names the tracks the spec does have and, when the format has any it lacks, those too, **both read off the tables**. `rgb` and `alpha` are carried under their own names and on their own key times, never folded into one `rgba` — that would state each channel at the other's key times, a value nobody keyed | fix the timeline's name, or accept that the rebuild plays nothing there |
604
620
  | `SPEC_REFUSED` | `BLOCK` | 1 | the specs were written and **rigc's own parser refuses one of them** — the detail carries that refusal word for word, after the file and the spec it is about. It is the one finding that is not about a single construct: it is whatever `parseRigSpec` or `parseMotionSpec` names, from a shape the format holds and the spec cannot say (a constraint that is `skinRequired` under no skin) to a defect in this decompiler | read the quoted sentence against the skeleton: it names the object. Both specs are on disk for exactly that, and `build` will refuse them until the shape has a spelling — [AUTHORING §5.1](AUTHORING.md) is the list of what a parser says |
621
+ | `STAGE_BOX_REWRITTEN` | `LOSS` | 0 | under `--stage-box <slot>`: the bounding box read as the stage states its corners in another order than `build` writes them (bottom-left first, counter-clockwise), or a `color` — the editor writes one on every box it exports. The rebuilt spec asks for the box (`skeleton.stageBox`), and `build` writes it from the stage, so the rebuild carries the same rectangle spelled `build`'s way. The detail names what changes | nothing. The stage is the same four numbers; only the box's spelling moves |
605
622
  | `TIMELINE_FIELD` | `BLOCK` | 1 | a key field on a bone, path, physics or slider timeline that is not part of that timeline's shape. On an `inherit` key that includes a `curve`: the parser reads `time` and `inherit` there and nothing else, and `build` refuses a curve on that track by name | check the spelling; the field is dropped from the rebuilt key |
606
623
  | `TIMELINE_KEY_RESTATED` | `LOSS` | 0 | an editor omits a channel that equals the parser's default, and the motion spec's `v` is positional, so the omission is written out at that default **in the spec** — and the line is printed only where the **file** will carry it too. The emitter leaves a channel out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so on every key kind with a row the rebuild is the source's own text and nothing is said — measured on the twelve exports, 0 tracks print it. What still prints it is a timeline whose keys have no row (`shearx`, `alpha`, path `spacing`, …), and an `inherit` key: one that omits the mode is written as `normal` — the parser's default — and one spelled with a capital first letter (`NoScale`) as the editor's `noScale`, the same mode either way | nothing. The same values the runtime reads, spelled out — a larger file and the same animation |
607
624
  | `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "2.11.1",
3
+ "version": "2.12.0",
4
4
  "description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -81,7 +81,12 @@ entry that ran.
81
81
  `rigc vote --candidate <a> --candidate <b>` when several candidates are green and
82
82
  only a person can choose between them.
83
83
  6. `rigc validate <out>` re-gates artifacts already on disk, and
84
- `rigc <command> --help` is each command's own flag table.
84
+ `rigc <command> --help` is each command's own flag table. `rigc repack <out>
85
+ --out <dir>` packs a build again under `build --pack`'s packing flags when its
86
+ parts were not kept, and writes only after every region and the skeleton are
87
+ shown unchanged (`--accept-skeleton-differences` writes a rebuild whose skeleton
88
+ differs, every difference printed; `--stage-box <slot>` reads a build's stage box)
89
+ — AUTHORING §0.4.
85
90
  7. Every finished unit ends with `rigc preview --candidate <out>`, and the report
86
91
  names the `.html` it wrote. The hand-off to a person is part of the work.
87
92