spine-rigc 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -195,7 +195,7 @@ What the flags mean:
195
195
  | --- | --- |
196
196
  | `--rig` | the rig spec — skeleton structure |
197
197
  | `--motion` | the motion spec — time |
198
- | `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths and `skeleton.images` are written relative to it. 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** |
198
+ | `--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/1`: the bones, slots, skins, constraints, events and animations the Spine files were emitted from, in the spec's order, and the rest of what posing them takes), written after the same gate as the pair and ending in `spine.sha256`, the digest of the `skeleton.json` written beside it. 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** |
199
199
  | `--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 |
200
200
  | `--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** |
201
201
  | `--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** |
@@ -210,6 +210,7 @@ What the flags mean:
210
210
  | `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`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 |
211
211
  | `--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 |
212
212
  | `--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** |
213
+ | `--poser` | `render` and `check`: `core` or `spine` — which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes, with the atlas beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core. Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `the "default" skin names` (no `--skin`, and the default skin names a skin-required bone or constraint, each named) · `and no "default" one` (no `--skin`, and skins with none named `default` — spine-core draws nothing there either, so the render stops at `there is nothing to draw`, exit 2, and only `--poser core` prints this refusal) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row |
213
214
  | `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
214
215
  | `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
215
216
  | `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
@@ -661,8 +662,9 @@ written against — about a page that is refused either way; and a size-matching
661
662
  WebP page let through that way builds green under the default profile and writes
662
663
  a directory whose next `render` refuses the page.
663
664
 
664
- **What `--out` holds afterwards:** `skeleton.json` and a `skeleton.atlas` that is
665
- the pack, page paths pointing back at the pack's own PNGs — so `rigc validate
665
+ **What `--out` holds afterwards:** `skeleton.json`, the model document
666
+ `skeleton.model.json`, and a `skeleton.atlas` that is the pack, page paths
667
+ pointing back at the pack's own PNGs — so `rigc validate
666
668
  <that directory>` reads it green with no flags, exactly as it reads a loose
667
669
  build's. Add `--copy-images` and the pack's page PNGs are copied in beside the
668
670
  skeleton and the page names become their basenames, which is the same directory
@@ -1332,7 +1334,7 @@ that is a defect in this guide: report it.
1332
1334
  | `x`, `y` | setup-pose bounding box origin | `0` — and refused outright beside a stated absence, below |
1333
1335
  | `width`, `height` | setup-pose bounding box size, **or both `null` for "this skeleton declares no stage"** | falls back to the manifest's crop; **with neither the number nor the `null`, the compile fails** |
1334
1336
  | `fps` | nonessential editor hint | `SkeletonData.fps` stays 30 |
1335
- | `referenceScale` | 4.2+ physics/scale reference | parser default 100 |
1337
+ | `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) |
1336
1338
  | `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 |
1337
1339
  | `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 |
1338
1340
 
@@ -1423,7 +1425,7 @@ it would simply be a second root.
1423
1425
  | `skin` | `BoneData.skinRequired` | `false` |
1424
1426
  | `color` | `rrggbbaa`, editor affordance | — |
1425
1427
  | `icon` | the editor's icon for this bone, e.g. `arrowsB`; editor affordance, no rendering effect. Copied through verbatim — no assertion checks the name, because the icon vocabulary is the editor's and an unknown one is not an error | — |
1426
- | `from` | **rigc extension** — take `x`/`y` (and optionally `rotation`) from a cut manifest | — |
1428
+ | `from` | **rigc extension** — take `x`/`y` from a cut manifest: `from.anchor`, `from.slotWindow` or `from.meshCenter`, exactly one. `from.rotation` is `"axis"` (the manifest's `axis.deg`, negated into y-up) or `"anchor"` (the third element of the named anchor, a screen-space facing angle); any other value is refused by name — it used to build as no rotation at all | — |
1427
1429
 
1428
1430
  ⚠️ Spine 4.0/4.1 called `inherit` **`transform`**. That old key still *loads* in 4.3
1429
1431
  and the inheritance silently falls back to Normal — assertion `A02` refuses it.
@@ -1437,7 +1439,7 @@ and the inheritance silently falls back to Normal — assertion `A02` refuses it
1437
1439
  | `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) — **except** on a slot nothing fills, where it can only be `null` and may be left out |
1438
1440
  | `color` | `rrggbbaa` tint | opaque white |
1439
1441
  | `dark` | two-colour tint, `rrggbb`. The **setup** half; §4.4's `rgba2` and `rgb2` tracks key it over time and require it | — (🚫 `A12` under `spine-html`) |
1440
- | `blend` | `normal` · `additive` · `multiply` · `screen` | `normal` |
1442
+ | `blend` | `normal` · `additive` · `multiply` · `screen`. Only the first letter's case is free: `Additive` reads as `additive`, while `ADDITIVE` or `mUlTiPlY` reads as **no mode** in the runtime with no error, so `build` refuses it by name (§5.1). Emitted as written | `normal` |
1441
1443
 
1442
1444
  ✅ **Every slot you declare is emitted, in this order.** A slot nothing fills — no
1443
1445
  skin entry, no manifest part — is emitted **empty**: `name` and `bone`, and no
@@ -1532,6 +1534,23 @@ or a timeline key at the time given — and, for a rig spec you wrote, in `rigc
1532
1534
  which prints every bone's local transform. A frame's time is the framing pass's own
1533
1535
  (every animation at 60 fps); the key that overflowed is at or just before it.
1534
1536
 
1537
+ 🦴 **Nor is it said of a skeleton whose every drawn slot hangs from a bone the skin
1538
+ leaves unposed.** A `"skin": true` bone the posed skin does not activate — or any bone
1539
+ below one — has no world transform, and both posers draw its slots through the zero
1540
+ matrix: every vertex at the origin, a box with no extent, and a frame that would be
1541
+ 0x0. That is refused by name before anything is written, exit 2 —
1542
+ ``rigc render: under no skin, every drawn slot hangs from a bone that skin leaves
1543
+ unposed — 1 drawn slot: slot "block" on bone "block", which skin "extra" poses — so no
1544
+ drawn vertex has a world transform and the frame would be 0x0: pose it under a skin
1545
+ that poses those bones (`--skin`), or name the bones in the skin it is posed under``.
1546
+ The fix is the skin, not the art: render under one of the skins named, or list the
1547
+ bone in the skin you render (§3.4.1). The same sentence comes from `render
1548
+ --geometry`, from `rigc check` (as `rigc check: …`), and from `framingViewport` as an
1549
+ `UnframeablePoseError`, whichever poser runs; it names the first three drawn slots,
1550
+ then how many more. Its sibling, `… every drawn vertex sits at the one point (x, y) —
1551
+ …`, is a pose whose bones ARE posed and collapse every vertex onto one point — a bone
1552
+ scaled to 0 on a static rig does it — and there the fix is the bone.
1553
+
1535
1554
  **Region attachment** ([Spine: region attachments](http://esotericsoftware.com/spine-regions)),
1536
1555
  the default `type`:
1537
1556
 
@@ -1803,7 +1822,7 @@ region, a trigger volume — that moves with the skeleton and draws nothing.
1803
1822
  | `vertexCount` | **required**, 3 or more. No default — the paragraph above is why |
1804
1823
  | `vertices` | the unweighted `x, y` run, in the slot bone's local space; or the index-encoded weighted run, behind `boneIndexing` |
1805
1824
  | `weights` | the by-name form: one entry per vertex, each a list of `{ "bone": …, "x": …, "y": …, "weight": … }`, each pair in that bone's local space. Never beside `vertices` |
1806
- | `boneIndexing` | `"name"` (the default) or `"raw"`, which opts a weighted `vertices` run into Spine's index encoding. `"raw"` beside `weights` is refused: `weights` always binds by name |
1825
+ | `boneIndexing` | `"name"` (the default) or `"raw"`, which opts a weighted `vertices` run into Spine's index encoding. `"raw"` beside `weights` is refused: `weights` always binds by name. Any other value — `"named"` included — is refused by name; it used to build as `"name"` |
1807
1826
  | `color` | `rrggbbaa`. **No default in the file** — omitted, the parser never calls `setFromString` and the attachment keeps the runtime's own colour. It is an editor affordance, the colour the polygon is drawn in there; rigc emits it verbatim when stated and leaves the key out when not |
1808
1827
 
1809
1828
  **Clipping polygon** ([Spine: clipping](http://esotericsoftware.com/spine-clipping)) —
@@ -1861,7 +1880,8 @@ is the wrap-around curve from `x = 180` back to `x = 0`, which nothing reads. Th
1861
1880
  `worldX = 45.000000`.
1862
1881
 
1863
1882
  The generators are `ring`, `ribbon`, `contour`, `grid` and `segments` (see
1864
- [`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
1883
+ [`src/mesh.ts`](../src/mesh.ts)), and a generator's `kind` is required: any other
1884
+ value, or none, is refused naming the five; the first two encode a deformation model rather
1865
1885
  than a table of numbers, which is why they are code invoked by data. `contour`
1866
1886
  and `grid` are geometry: they pin every vertex to the slot bone and exist to give
1867
1887
  a `deform` timeline (§4.12) somewhere to push. `segments` is both at once — a
@@ -2190,6 +2210,13 @@ by 245 levels and the same sheet reports **6.08°** — and `A39` refuses at bot
2190
2210
  to 0°. The number is not lying. 99 % of that mesh still turns to 64.80° and one
2191
2211
  triangle does not, and nothing on the first line says so.
2192
2212
 
2213
+ When several triangles fold at the same angle to the six decimals the report
2214
+ carries, the triangle named is the **lowest-numbered** of them — the one `A39`
2215
+ names first when a key turns just past the ceiling, since it lists the
2216
+ triangles a key reverses in ascending order. The choice is made on the reported
2217
+ angle rather than on the full double, so a platform's last-bit difference in
2218
+ `atan` or `pow` does not change which triangle, step and share are printed.
2219
+
2193
2220
  | the figure | how to read it |
2194
2221
  | --- | --- |
2195
2222
  | `x1.003` — the **1st percentile over the ceiling** | near 1 means a *band* of the mesh reaches the limit together, and near 10 means **one triangle** does, which is what a bad texel looks like. The clean and stray sheets above read `x1.003` and `x10.652`. ⚠️ A band is **not** sufficient evidence of a form, which is the third case: **an outline is a band**. A depth sheet estimated over cut-out art has a cliff along the whole silhouette, so its ceiling is a band too and this figure reads 1.02–2.17 — healthy — on a mesh whose angle means nothing. The row below is what tells those two apart |
@@ -5450,11 +5477,12 @@ out of it.)
5450
5477
 
5451
5478
  🔢 **A fourth surface is a pose that is not finite**, which `render` and `check`
5452
5479
  refuse before drawing anything, naming the bone or vertex and its value.
5453
- `A10_NO_NAN_AFTER_STEPPING` reads the same scan, so on a skeleton with an animation
5454
- `validate` gets there first and in the same words: a bone `rotation` of `1e309`, in
5455
- the setup pose or in a key, leaves the bone's world position finite and its matrix
5456
- `NaN`, and A10 names the bone and the term. What it does not see is a skeleton with
5457
- no animation (A10 SKIPs, so `render` is where that one is named) and a skin other
5480
+ `A10_NO_NAN_AFTER_STEPPING` reads the same scan, so `validate` gets there first and
5481
+ in the same words: a bone `rotation` of `1e309`, in the setup pose or in a key,
5482
+ leaves the bone's world position finite and its matrix `NaN`, and A10 names the bone
5483
+ and the term. That holds on a skeleton with no animation too — A10 reads its setup
5484
+ pose, which is the whole of what a static rig shows, and says on the stats line that
5485
+ it had nothing to step (`nanStepping=skipped`). What it does not see is a skin other
5458
5486
  than the default, which A10 does not pose. The sentence, and where to look for the
5459
5487
  number, is under *nothing to draw* in §3.4.
5460
5488
 
@@ -5581,7 +5609,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5581
5609
  | `a rig spec needs a non-empty "bones" array` | §3.2 — a rig with no bones has nothing to hang a slot on; every one has at least a root |
5582
5610
  | `a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)` | §3.3 — write `[]` for a rig that draws nothing. ⚠️ These three arrive only when the key is really absent: **misspelt**, it is the unknown-key refusal above, naming what you wrote |
5583
5611
  | `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
5584
- | `<file>: <where> is a string "5"; a number is required` (also `is a boolean true`, `is an array [1]`, `is null`, `is a number 7; a string is required`, `…; an array of numbers is required`, `…; a boolean (true or false) is required`, `…; a string or null is required`) | §3 / §4 — write the value as the JSON type its field holds: a number as a JSON number (`5`, not `"5"`), a flag as `true`/`false`, a name as a string. Every field of the three input files has one type, and a value of another is refused by the field before any reader does arithmetic on it — which it used to: `"x": "5"` built as `5`, `true` as `1` and `[1]` as `1`, and a `null` built too, all green. An array is typed element by element, so the refusal names the index (`weights[3][1].x`, `vertices[0]`). `null` is refused as `null` except where the field states it means something (`skeleton.width`/`height`, `skeleton.audio`, a slot's and a setup entry's `attachment`, a deform key's `vertices`, the manifest's `image`, `states`, `contact_depth` and `cap_containment_ceiling`). `<where>` is the rig spec's wording (`bone "hip" x`, `skin "default" slot "s" attachment "a" rotation`, `constraint "aim" mix`), the dotted path in the cut manifest (`crop.h`, `parts[2].offset[0]`), and the motion spec's own path (`` `animations."walk".ik[0].keys[0].mix` ``). In the rig spec and the manifest it runs right after the key scan and before every other check, so a field check that used to name a type (a mesh name that is not a string, a `lengths` entry spelt as a string) now reads as this row; in the motion spec it runs after that file's own field checks, which keep their sentences. A closed set of names (`blend`, `inherit`, a constraint's `type`, a generator's `kind`) keeps its own refusal, which lists the names that exist |
5612
+ | `<file>: <where> is a string "5"; a number is required` (also `is a boolean true`, `is an array [1]`, `is null`, `is a number 7; a string is required`, `…; an array of numbers is required`, `…; a boolean (true or false) is required`, `…; a string or null is required`) | §3 / §4 — write the value as the JSON type its field holds: a number as a JSON number (`5`, not `"5"`), a flag as `true`/`false`, a name as a string. Every field of the three input files has one type, and a value of another is refused by the field before any reader does arithmetic on it — which it used to: `"x": "5"` built as `5`, `true` as `1` and `[1]` as `1`, and a `null` built too, all green. An array is typed element by element, so the refusal names the index (`weights[3][1].x`, `vertices[0]`). `null` is refused as `null` except where the field states it means something (`skeleton.width`/`height`, `skeleton.audio`, a slot's and a setup entry's `attachment`, a deform key's `vertices`, the manifest's `image`, `states`, `contact_depth` and `cap_containment_ceiling`). `<where>` is the rig spec's wording (`bone "hip" x`, `skin "default" slot "s" attachment "a" rotation`, `constraint "aim" mix`), the dotted path in the cut manifest (`crop.h`, `parts[2].offset[0]`), and the motion spec's own path (`` `animations."walk".ik[0].keys[0].mix` ``). In the rig spec and the manifest it runs right after the key scan and before every other check, so a field check that used to name a type (a mesh name that is not a string, a `lengths` entry spelt as a string) now reads as this row; in the motion spec it runs after that file's own field checks, which keep their sentences. A closed set of names is not a type, and is the next row's |
5613
+ | `<file>: <where> is "foo"; one of "name", "raw" — anything else was read as …` (also `bone "b" from.rotation is 5; one of "axis", "anchor"`, the manifest's `parts[1].mesh.kind is "foo"; one of "ring", "ribbon"`, and `skin "s" slot "x" attachment "a" generator.kind is "foo"; one of "ring", "ribbon", "contour", "grid", "segments"`, or `… generator.kind is absent; …`) | §3.2 / §3.4 — write one of the names listed. A field that holds one of a closed set of names is refused, with the set, when it holds anything else — a number too, since a number is not a name. Most such fields have a reader that refuses by name with its own sentence (`blend`, `inherit`, a constraint's `type`, `scaleY`); these four had none, and each built green or crashed: `boneIndexing` read as `"name"`, `from.rotation` as no rotation, the manifest's `mesh.kind` as a ribbon by one reader and a ring by two others, and a generator `kind` threw a `TypeError`. The sentence after the dash says what the value used to be read as, which is the build you had before this refusal. The first three run right after the type row's walk, before every other check; the generator's `kind` is refused earlier, by the key scan, because it chooses which keys the rest of the generator may hold |
5585
5614
  | `<file>: <where> is Infinity; a number in this file is finite at float32 precision, at most ±3.4028234663852886e+38, because the skeleton is written as float32 — …` (also `is -Infinity`, `is 1e+308`, and `is NaN` from a library caller) | §3 — state the number you meant, inside float32's range. One walk over the whole file, run right after the key scan and before any other rig-spec check, so a range rule never prints `NaN`. `<where>` is every number in the rig spec — a bone (`bone "hip" x`), a slot, a skin's attachment down to its arrays (`skin "default" slot "s" attachment "a" weights[3][1].x`), a constraint (`constraint "aim" mixRotate`), an event, `skeleton`, `invariants.meshTriangles` — and the dotted path in the cut manifest (`crop.h`) and in the motion spec (`` `animations.idle.tracks[0].keys[1].v[0]` ``, after the motion spec's own field checks, which keep their sentences). ⚠️ It sees numbers only: `"x": "NaN"` is a string, and the row above refuses it as one |
5586
5615
  | `two bones are called "X"` | bone names are the join key; rename one |
5587
5616
  | `two ik constraints are called "X" — a constraint resolves by name AND type (\`SkeletonData.findConstraint\`), so names are unique PER KIND: an ik and a transform constraint may share one, two of a kind may not` | §3.5 — rename one of the two. The kind in the sentence is the pair's own, so `two transform constraints are called "X"` is the same refusal on another kind; a name shared **across** kinds is not this error |
@@ -5617,6 +5646,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5617
5646
  | `hull vertices must trace the outline in order; the triangles' outline runs …, so vertex a has to follow vertex b in the list, and vertex c does` | §3.4 — renumber along the printed walk |
5618
5647
  | `the triangles do not tile the outline: …` / `the triangles' outline is not one closed loop: …` | §3.4 — a doubled triangle, an unused vertex, a pinch or a hole in `triangles` |
5619
5648
  | `vertex N binds bone "X", which the rig does not declare as a bone` | §3.4 — an authored mesh's `weights` bind by NAME, like everything else in a rig spec. Fix the spelling, or declare the bone. The message names the skin, the slot, the placeholder and the vertex, because an index would name none of them |
5649
+ | `vertex N binds bone index I, which is not in the bone list` | §3.4 — a `"boneIndexing": "raw"` run on a mesh, bounding box, clipping polygon or path names a bone by its place in the bone array, and I is no place in it: past the end, negative or not a whole number. Count the rig's `bones` from 0, or give the bindings by name as `weights`, which cannot miss this way. ⚠️ **Refused earlier, not newly** (issue #917): the run is decoded to bone names once, when the attachment is built, and this is where an index that names no bone stops. Before that a mesh compiled and `A20` named the index at the gate, a bounding box, clipping polygon or path with its `lengths` stated compiled and `A33` named it, and only a path whose `lengths` rigc measures refused here, in this sentence. `MV04` plants it on each of the four kinds |
5650
+ | `the raw weighted run has a bone count of C at index I` / `the raw weighted run is truncated — vertex N claims C bone(s) and the array ends first` / `the raw weighted run decodes to N vertices but there are M uv pairs` | §3.4 — a mesh's `"boneIndexing": "raw"` run is not `boneCount, (index, x, y, weight) × boneCount` per vertex, one vertex per uv pair. These are the polygon's three sentences (whose last clause there reads `but vertexCount is M`), now asked of a mesh as well. Fix the run, or give the bindings by name as `weights`. ⚠️ **Refused earlier, not newly** (issue #917): the mesh run is decoded to bone names when the mesh is built, and a run that does not decode cannot be. Before that it compiled and `A20` named it at the gate (`has weights for 5 vertices but 4 uv pairs`, `vertex 3 has weight NaN`). `MV04` plants each |
5620
5651
  | `image "X.png" is not on disk at …` | fix the name, or point `--images` at the right directory |
5621
5652
  | `image "X.png": /…/X.png is a WebP image (…), not a PNG: its first 12 byte(s) are …` | the file is there and is not a PNG — the name ends in `.png` and the bytes decide. Re-export it as PNG; the same sentence says **truncated** for a PNG that runs out before its `IEND`, and then the repair is a whole copy (§0.2) |
5622
5653
  | `a "contour" generator traces the part's own alpha, and "X.png" is lifted off a packed page at the coordinates the atlas states, which on this file are not where its texels are — so there is no silhouette here to trace, only another part of the page. page "p.png" declares …` | the page's PNG is not the size its atlas declares, and the rest of the message is `A06`'s sentence for it: re-declare the page with the `scale:` header it names, or re-export the page at its declared size (§0.2) |
@@ -5685,6 +5716,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5685
5716
  | `"lengths"[i] is V, below …` / `"lengths"[i] is "…"; every entry is a finite cumulative length` | §3.4 — the array is cumulative, so each entry is at least the one before it |
5686
5717
  | `rig constraint "X": slot "Y" has no path attachment in any skin` | §3.5.1 — give that slot a `"type": "path"` attachment, or aim the constraint at the slot that has one |
5687
5718
  | `rig constraint "X": rotateMode is "CHAINSCALE"; known: Tangent, Chain, ChainScale` | §3.5.1 — only the first letter's case is free; anything else resolves to `undefined` in the parser |
5719
+ | `<file>: slot "X" has blend "ADDITIVE"; known: normal, additive, multiply, screen (only the first letter's case is free — …)` | §3.3 — write the mode in lower case, or with only its first letter upper-cased. `SkeletonJson` reads `blend` through `Utils.enumValue`, which upper-cases the first character and looks the rest up exactly, so `ADDITIVE` loaded as no mode at all (`tools/pose_oracle.ts dump` reads it as `null`), with no error. rigc used to compare the name case-insensitively and build that file green |
5688
5720
  | `rig constraint "X": applies animation "Y", which the motion spec does not declare (it declares: …)` | §3.5.2 — fix the slider's `animation`, or add it to the motion spec |
5689
5721
  | `rig constraint "X": declares both a "bone" and "time"` | §3.5.2 — `bone` picks the model and `time` belongs to the other one |
5690
5722
  | `rig constraint "X": declares "property" but no "bone"` | §3.5.2 — name the driving bone, or key `slider.<name>.time` instead |
@@ -5808,7 +5840,7 @@ Fix A00 and run it again.
5808
5840
  | `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` |
5809
5841
  | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. There is no `spine-html` clause here — a placeholder is free to differ from the region its `path` names **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once |
5810
5842
  | `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
5811
- | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked **SKIP** when the skeleton carries no animation: the NaN is produced by stepping, and a static rig is never stepped — the same subject `A09` skips on |
5843
+ | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked. 🧍 **A skeleton with no animation is still read**: its setup pose is the whole of what it shows, so A10 reads it — the world transform and the vertices as above, and the bone's mode and the slot colours a stepped frame is read for, which on a static rig nothing else would reach — and names it `the setup pose` (`the setup pose: slot "x" colour is non-finite`). The stepping half had nothing, and a PASS row carries no detail, so the stats line says so: `nanStepping=skipped`, beside `animations=0`. `A09` does not follow — a declared duration is a fact about an animation and nothing else, so a static rig still gives it nothing. **SKIP** only when the skeleton carries **no bone and no animation**: there is then no pose to read and nothing to step |
5812
5844
  | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
5813
5845
  | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
5814
5846
  | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
@@ -5816,9 +5848,9 @@ Fix A00 and run it again.
5816
5848
  | `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. The rule assumes meshes are mostly static — the renderer skips redrawing a mesh nothing moved — so it has three states. **FAIL per bone** when the rig declares nothing: `idle keys bone "X", which drives a mesh — meshes never idle-skip`, and the **first** of those lines, once per run, names the case: `K bone(s) keyed by idle drive meshes; if this idle is meant to deform them (a painting rig), declare invariants.idleDrivesMeshes: { "why": … } in the rig spec — or, where the motion belongs to a pivot above the mesh, key that pivot one link up (FACE.md §3)`. It rides the first finding, so the count of FAIL lines is still the count of bones. **SKIP with the cost** when the rig declares `invariants.idleDrivesMeshes` (§3.7): `declared by the rig ("<why>"): idle keys N bone(s) that drive M mesh attachment(s) totalling V vertices — "a", "b" — and each of those meshes is recomputed on every frame it is shown`. **FAIL on a stale declaration** — declared, and the `idle` keys no mesh-driving bone, carries no bone timeline or does not exist: `the rig "X" declares invariants.idleDrivesMeshes ("…"), but "idle" keys no bone that drives a mesh, so the declaration switches off nothing — remove it`. Otherwise **SKIP** when there is no `idle` animation, or when the one there is carries no bone timeline — a rule whose subject does not exist is unmeasured and not satisfied |
5817
5849
  | `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
5818
5850
  | `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. **SKIP** when the atlas declares no page — as it is for `A06`, `A19` and `A27`; see `A07` |
5819
- | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
5851
+ | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed — in `skeleton.json`, `skeleton.atlas` or the model document `skeleton.model.json`, the one named (the document's first differing line with it). That is a compiler bug, not a spec bug — report it |
5820
5852
  | `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes**; the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes: the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art. ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page**: a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it**, and the clause above does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so a scan would come back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappears**, the scan finding a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. ⚠️ **A page file that cannot be read as PNG at all is the same non-measurement for every part on it**: one row per page naming its parts and pointing at `A06`, which names what the file is. **SKIP** when the atlas declares no page |
5821
- | `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not. Fix the rig spec's `controls`/`chain`, a `segments` generator's `bones` (its `influence` line names a bone no vertex binds), or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
5853
+ | `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. On a rig spec's `"boneIndexing": "raw"` mesh an index out of range or a run that does not decode is refused at compile first (§5.1, issue #917), so here it names a skeleton rigc did not compile. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not. Fix the rig spec's `controls`/`chain`, a `segments` generator's `bones` (its `influence` line names a bone no vertex binds), or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
5822
5854
  | `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1. **SKIPs** by name on authored or linked geometry and on a `segments` mesh, whose outline is weighted by distance like the rest of it — none of them claims a pinned rim |
5823
5855
  | `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment |
5824
5856
  | `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too — or has `damping` outside `[0, 1]`, which says `` physics "C" has damping 1.5; must be inside [0, 1] — the per-step decay is `damping ** (60 * step)`, and the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: above 1 every velocity grows on every step and the offset diverges ``, the bound read off the row the key's refusal reads; `1` and `0` are inside, since both are finite at every rate — **at rest, and on every physics timeline key**. The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps. As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `` physics "C" has mix 0 and none of the 3 animations keys its mix above 0; the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted — rest it above 0, or key its mix above 0 in an animation ``. ⚠️ Every reason A23 prints for a setup value is that value's row `basis` arm (§4.4's table) — an arithmetic arm names its expression (`physics "C" has massInverse Infinity; mass must be > 0 — at 0 …`), a behavioural one says it is rigc's call and what the value does. A setup `mix` **below** 0 is told the jiggle is applied inverted, not that it is muted: such a constraint is measured moving its bone by exactly the opposite of a positive mix. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md)), so this sentence is met on a file, never on a decompiled rebuild. **SKIP** when the skeleton declares no physics constraint — the same sentence `A36` and `A37` print for their own constraint types |
@@ -5831,7 +5863,7 @@ Fix A00 and run it again.
5831
5863
  | `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
5832
5864
  | `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted |
5833
5865
  | `A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
5834
- | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index, a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
5866
+ | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index (on a rig spec's raw run rigc refuses both at compile first — §5.1, issue #917), a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
5835
5867
  | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path`, `physics` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.4, §4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. The **empty** name under `physics` is not a miss — it is the timeline that names no constraint (§4.4's `"*"`), and it is refused only when it reaches none: `animation "A" physics constraint "" timeline "P": a physics group that names no constraint writes every physics constraint declaring "PGlobal": true, and none of "C", … does — the parser loads it, the runtime walks every constraint, and no constraint takes the key`. Who it reaches is asked of the runtime's own `PhysicsConstraintTimeline.global` on the file's constraints — the reading `A23` and `A42` share. **SKIP** when no animation carries one |
5836
5868
  | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11). **SKIP** when no animation carries a deform timeline |
5837
5869
  | `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying any of them **above 0** (§3.5.1, §4.12). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. The third reads the values the loaded timeline poses — keys and Bezier samples alike — the way `A23` reads a physics `mix`, so a `mix` timeline keying 0 only is not a rescue: [measured] it poses every bone exactly where no timeline does. The detail says both halves: `path constraint "P" has mixRotate 0, mixX 0 and mixY 0 at setup and none of the 2 animations keys its mix above 0; update() returns on all-zero mixes, so nothing ever puts a bone on the path — rest one of the three above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no path constraint |
@@ -6567,6 +6599,21 @@ remember:
6567
6599
  skin alone)`. `check.json` carries the same two under `skin` and
6568
6600
  `referenceSkin`.
6569
6601
 
6602
+ 🔎 **The report also names the poser that drew the candidate**, on a `poser` line
6603
+ under `skin`, in the words `render` prints (issue #968):
6604
+ `poser rigc core — …/skeleton.model.json` for a rigc build the core poses, and
6605
+ `poser spine-core — <why>` otherwise, `<why>` in the words the `--poser` row of
6606
+ the flag table near the top lists — a Spine export (`no skeleton.model.json
6607
+ beside …`), a `skeleton.json` edited after the build (both digests named), a
6608
+ refusal of the core's, or `--poser spine`, among them. `check.json` carries it as
6609
+ `"poser": { "name": "core" | "spine", "note": "<the line>" }`, and `--out`'s
6610
+ `frames.json` under `comparison.poser`. One choice per run: every set, the setup
6611
+ pose and the non-finite sentence are posed through the same poser, and a core
6612
+ refusal partway re-poses all of it on spine-core. `--poser core|spine` forces the
6613
+ choice with `render`'s semantics (`core` on an input that cannot carry it exits 2,
6614
+ nothing written). The two posers are measured to write the same report, byte for
6615
+ byte but the poser line, on every tree row.
6616
+
6570
6617
  A skin name the candidate does not declare is refused with the ones it does —
6571
6618
  `the candidate declares no skin "path"; it declares [default, patch, torn]`.
6572
6619
 
@@ -8519,10 +8566,11 @@ instead:
8519
8566
  - **A mesh attachment writes `path` and `color` right after `type`, `path` before `color` — measured on a production set (#791), not on `examples/`.**
8520
8567
  18 meshes whose `path` differs from their name wrote it second, and 1 with a
8521
8568
  `color` wrote that second; no mesh carried both. The row does not list them,
8522
- so the position is the constructor's — `meshTextureKeys` in
8523
- [`src/compile.ts`](../src/compile.ts), which every mesh route spreads right
8524
- after `type`. A region's `path` and a linked mesh's keys stay where
8525
- they were: neither was in the set, and an analogy is not a measurement.
8569
+ so the position is the constructor's — `emitMesh` in
8570
+ [`src/emit_spine.ts`](../src/emit_spine.ts), which writes the two keys every
8571
+ mesh route computes with `meshTextureKeys` right after `type`. A region's
8572
+ `path` and a linked mesh's keys stay where they were: neither was in the set,
8573
+ and an analogy is not a measurement.
8526
8574
  - **A skin's slot keys are compared after `foldBeforeComparing`, which reads U+3000 as U+0020 and U+FF10–U+FF19 as U+0030–U+0039** —
8527
8575
  the ideographic space as a space, the full-width digits as `0`–`9`
8528
8576
  (`EDITOR_NAME_FOLD`). One map of 95 slot keys, 7 of them carrying those
@@ -8981,7 +9029,8 @@ near zero. A generated cut also disagrees with its picture, which can only make
8981
9029
  harder to place. So the edge is where `pose` fails even on ideal cuts, and never a
8982
9030
  guarantee above it.
8983
9031
 
8984
- Re-run it with `bun tools/pose_floor.ts --textures <level> --json <out>` (one level per
9032
+ Re-run it from a clone of the repository — the tool is repository material, not in the
9033
+ published package — with `bun tools/pose_floor.ts --textures <level> --json <out>` (one level per
8985
9034
  process parallelises it), then `bun tools/pose_floor.ts --from <a.json>,<b.json>,…` to
8986
9035
  print the table, derive the edge and list the misses per cell. `PO22` in
8987
9036
  `bun run selftest` re-measures three of its cells, `PO24` holds those three cells to
package/docs/INGEST.md CHANGED
@@ -189,6 +189,7 @@ rigc render --candidate examples/spineboy/export/spineboy-pro.json \
189
189
  rigc render
190
190
  .. skeleton …/examples/spineboy/export/spineboy-pro.json
191
191
  .. atlas …/examples/spineboy/export/spineboy.atlas
192
+ .. poser spine-core — no skeleton.model.json beside …/examples/spineboy/export/spineboy-pro.json — a Spine export, not a rigc build
192
193
  .. 200x186px at 8 fps, 1 set(s) -> …/render/sb
193
194
  .. walk 9 frame(s), 1.000s + contact.png -> …/render/sb/walk@8fps
194
195
  rigc: wrote …/render/sb/frames.json
package/docs/MOTION.md CHANGED
@@ -982,9 +982,11 @@ are entirely the motion spec's business.
982
982
 
983
983
  ### 4. In-between it
984
984
 
985
- Duration 0.55 s, proposed (§3.3). Three easings (§3.4). The arm is the driver; the flag
986
- is the next link out and it is light, so §3.7's table puts its extreme **+20–35 %** after
987
- the arm's and gives it an overshoot. *"Snap"* buys an anticipation (§3.6) and an
985
+ Duration 0.55 s, proposed (§3.3). Three easings (§3.4). The arm is the driver. The flag
986
+ is the last thing on it and it is cloth, which is the row §3.7's table gives to *a
987
+ pennant* — not the *next link out* row, because nothing hangs off the flag — so its
988
+ extreme lands **+20–35 %** after the arm's, it overshoots, and it settles last with one
989
+ crossing. *"Snap"* buys an anticipation (§3.6) and an
988
990
  overshoot (§3.8). The post is planted: ⛔ **no timeline**.
989
991
 
990
992
  `semaphore.motion.json` — complete, nothing trimmed:
@@ -1020,8 +1022,8 @@ overshoot (§3.8). The post is planted: ⛔ **no timeline**.
1020
1022
  "keys": [
1021
1023
  { "t": 0, "v": [-22.2], "ease": "gather" },
1022
1024
  { "t": 0.09, "v": [-30.1], "ease": "charge" },
1023
- { "t": 0.38, "v": [-3.8], "ease": "settle" },
1024
- { "t": 0.48, "v": [-15.4], "ease": "settle" },
1025
+ { "t": 0.47, "v": [-3.8], "ease": "settle" },
1026
+ { "t": 0.51, "v": [-15.4], "ease": "settle" },
1025
1027
  { "t": 0.55, "v": [-11.7] }
1026
1028
  ]
1027
1029
  }
@@ -1040,8 +1042,8 @@ which is which is worth being able to point at:
1040
1042
  | arm `t: 0.55` = 18.3, flag `t: 0.55` = −11.7 | **given** — pose B. The flag's is `6.6 − 18.3`: both world rotations converted first, then differenced, because a child's track carries a **local** rotation under a rotated parent |
1041
1043
  | arm `t: 0.07` = −66.4 | §3.6 — 4.5° against an 80° excursion (5.6 %), at 13 % of the duration |
1042
1044
  | arm `t: 0.32` = 24.7 | §3.8 — 6.4° past the end value (8 %), at 58 % of the duration |
1043
- | flag `t: 0.09`, `t: 0.38` | §3.7 — the flag's extreme lands at 69 % against the arm's 58 %, an offset of **+11 %**, and it drags the other way first |
1044
- | flag `t: 0.48` = −15.4 | §3.7's *one crossing* for a loose part: it comes back past its own end value before settling |
1045
+ | flag `t: 0.09`, `t: 0.47` | §3.7 — the flag's extreme lands at 85 % of the duration against the arm's 58 %, an offset of **+27 %**, the middle of its row's band, and it drags the other way first. Not the top of the band: at 35 % (`t: 0.51`) the crossing and the settle below would share 0.04 s, and the 24 fps render would still show the flag at its extreme one frame before the last |
1046
+ | flag `t: 0.51` = −15.4 | §3.7's *one crossing* for a loose part: it comes back past its own end value before settling, keyed halfway between its extreme and the end |
1045
1047
  | the three easings | §3.4 — one that gathers, one that arrives slowly, one symmetric. The **last key of each track carries no easing**, because there is nothing after it to ease towards (AUTHORING §4.5) |
1046
1048
  | the post's absent track | §3.7 — a planted part gets no timeline |
1047
1049
 
@@ -1055,6 +1057,7 @@ rigc build --rig semaphore.rig.json --motion semaphore.motion.json --images part
1055
1057
  .. pages=3 regions=3 bones=4 slots=3 animations=1 version=4.3.13 regionAttachments=3 meshAttachments=0 physicsConstraints=0 rig=semaphore profile=spine
1056
1058
  rigc: wrote …/semaphore/spine/skeleton.json
1057
1059
  rigc: wrote …/semaphore/spine/skeleton.atlas
1060
+ rigc: wrote …/semaphore/spine/skeleton.model.json
1058
1061
  rigc: look at it: rigc preview --candidate …/semaphore/spine
1059
1062
  ```
1060
1063
 
@@ -1071,7 +1074,9 @@ Open `render/raise@24fps/contact.png` **before anything else** — fourteen fram
1071
1074
  grid, and spacing is a comparison across frames rather than a property of any one of
1072
1075
  them. What to check on it, and it is not a score: frame 0 is pose A — the flag hanging off
1073
1076
  the arm's collar, not over its middle — the last frame is pose B, the anticipation dips
1074
- *after* frame 0, and the flag's extreme is visibly later than the arm's.
1077
+ *after* frame 0, and the flag's extreme is visibly later than the arm's: the arm peaks at
1078
+ frame 8 while the flag is still closing on the arm's line, it keeps closing through
1079
+ frame 11 as the arm eases back, and at frame 12 it drops past its resting angle.
1075
1080
 
1076
1081
  🚫 **Do not run `rigc check` against `poseA.png` and `poseB.png`.** Two pictures are not
1077
1082
  a frame set: `check` reads the second as the frame 1/12 s in, at its default rate. And
@@ -1115,21 +1120,21 @@ rigc vote --candidate spine --candidate spine-b
1115
1120
 
1116
1121
  ```
1117
1122
  rigc vote
1118
- .. ballot bce70ad82fe90941
1123
+ .. ballot 862f444c58d10e3e
1119
1124
  .. animation raise
1120
- .. A sha256:6740d3dbf304… 3 page(s), 0.4 KiB <- …/semaphore/spine/skeleton.json
1125
+ .. A sha256:c49e458df44e… 3 page(s), 0.4 KiB <- …/semaphore/spine/skeleton.json
1121
1126
  .. B sha256:d1bda24d7c36… 3 page(s), 0.4 KiB <- …/semaphore/spine-b/skeleton.json
1122
1127
  .. the page shows A/B and nothing else — the paths above are in its manifest, never on the screen
1123
1128
  .. embedded every candidate's skeleton, atlas and page(s) as data URIs; the player itself loads from unpkg (@4.3.*), so the first open needs a network
1124
1129
  rigc: wrote …/semaphore/ballot.html (22.3 KiB — open it in a browser)
1125
- rigc: then record the saved vote with rigc vote --record vote-bce70ad82fe90941.json --ballot …/semaphore/ballot.html
1130
+ rigc: then record the saved vote with rigc vote --record vote-862f444c58d10e3e.json --ballot …/semaphore/ballot.html
1126
1131
  ```
1127
1132
 
1128
1133
  A person opens that page, watches two loops, picks one, and saves the small JSON it hands
1129
1134
  them. Then:
1130
1135
 
1131
1136
  ```bash
1132
- rigc vote --record vote-bce70ad82fe90941.json --ballot ballot.html
1137
+ rigc vote --record vote-862f444c58d10e3e.json --ballot ballot.html
1133
1138
  ```
1134
1139
 
1135
1140
  ```
@@ -1140,8 +1145,8 @@ rigc vote --record vote-bce70ad82fe90941.json --ballot ballot.html
1140
1145
  PASS V04_CHOICE_IS_ON_THE_BALLOT
1141
1146
  PASS V05_REASON_CODE_FITS_THE_CHOICE
1142
1147
  PASS V06_NOT_ALREADY_RECORDED
1143
- .. winner A = sha256:6740d3dbf3043ff39c5f90577407c381f6841ff6af921517be7a0cfd3d7eb9d3, reason code preferred
1144
- .. coverage 2 candidate(s): A=sha256:6740d3dbf304… B=sha256:d1bda24d7c36…
1148
+ .. winner A = sha256:c49e458df44efd277aa6e50ff8fa1962c1211c7617079d63fbc4cf8008eaf0c5, reason code preferred
1149
+ .. coverage 2 candidate(s): A=sha256:c49e458df44e… B=sha256:d1bda24d7c36…
1145
1150
  rigc: appended line 1 to …/semaphore/votes.jsonl
1146
1151
  ```
1147
1152
 
@@ -1169,9 +1174,13 @@ that the ends are stateable by construction (`pose`), that the file is checkable
1169
1174
  (`vote`).
1170
1175
 
1171
1176
  🚫 **No scoring of end-pose reach, and nothing here to add one to.** At L1 and L2 the
1172
- end poses are given conditions; a number saying how near the animation got to them is a
1173
- number about the pose estimator. This is why §6 does not run `check` on the two pictures
1174
- and why no threshold, tolerance or pass bar appears anywhere in this document. The
1177
+ end poses are given conditions, and what a picture gives is a **pose**: the rotations are
1178
+ `pose`'s readings, which the spec states by construction, so a number for how near the
1179
+ animation got to *them* is a number about the pose estimator. The **offsets** are the
1180
+ other half — derived from the pictures rather than given — and a rig can miss its
1181
+ picture by a wrong one with every key exact. Catching that is a look, not a score (§6.5's
1182
+ frame-0 check). This is why §6 does not run `check` on the two pictures and why no
1183
+ threshold, tolerance or pass bar appears anywhere in this document. The
1175
1184
  residuals in a `pose` report are trust signals about *placements*, and AUTHORING §11.1
1176
1185
  says the same from the instrument's side.
1177
1186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
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, round-trips it through spine-core and named assertions, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/atlas.ts CHANGED
@@ -397,6 +397,31 @@ export function parseAtlasText(text: string): ParsedAtlas {
397
397
  return { pages, regions, lines };
398
398
  }
399
399
 
400
+ /**
401
+ * The region an attachment draws under a name, and the page it sits on —
402
+ * `TextureAtlas.findRegion` and `region.page`, as the loader resolves a
403
+ * region, a mesh and each frame of a series (issue #967).
404
+ *
405
+ * The FIRST region of that name in file order: an atlas naming two regions
406
+ * alike draws the first (measured through spine-core 4.3.13's loader, whose
407
+ * attachment drew the first of two regions named `seq`, and whose `index:`
408
+ * lines did not enter the lookup — a series' frame is found by its NAME,
409
+ * `frameRegionName` in [`src/core/uvs.ts`](core/uvs.ts)). The name is matched
410
+ * exactly as `parseAtlasText` keeps it, the raw line: a region line `art `
411
+ * is not found as `art`, as the runtime does not find it.
412
+ *
413
+ * Returned as a function over the parsed atlas so the core — which reads no
414
+ * file and links nothing — takes it as an input (`UvLookup`); the page UVs
415
+ * themselves are the core's (`regionPageUvs`, `computeUvs`), measured there.
416
+ */
417
+ export function atlasRegionLookup(parsed: ParsedAtlas): (name: string) => { page: AtlasPage; region: AtlasRegion } | null {
418
+ const first = new Map<string, { page: AtlasPage; region: AtlasRegion }>();
419
+ for (const page of parsed.pages) {
420
+ for (const region of page.regions) if (!first.has(region.name)) first.set(region.name, { page, region });
421
+ }
422
+ return (name) => first.get(name) ?? null;
423
+ }
424
+
400
425
  /**
401
426
  * The same atlas text with every page's name line replaced.
402
427
  *