spine-rigc 1.0.0 โ†’ 1.0.2

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
@@ -11,8 +11,8 @@ checked against the code that implements it.
11
11
  ๐Ÿšจ **The gate cannot see a wrong animation, and it will not tell you so.** `build`
12
12
  is green when the file is *valid* โ€” parseable, steppable, nothing degenerate in it.
13
13
  Whether the animation is the one you were asked for is a question it does not ask
14
- and has no way to answer. This is not a caveat: rung 1's first honest run shipped a
15
- build with **every easing in the file reversed** and the gate passed it green. If you were given pictures, `check` (**ยง9**) is the half of the loop
14
+ and has no way to answer. This is not a caveat: a build with **every easing in the
15
+ file reversed** gates green. If you were given pictures, `check` (**ยง9**) is the half of the loop
16
16
  that can see that, and a run that skips it has verified nothing about the motion.
17
17
 
18
18
  - Formats and CLI reference: [README.md](../README.md)
@@ -20,10 +20,11 @@ that can see that, and a run that skips it has verified nothing about the motion
20
20
  - The motion spec and emitted shapes: [`src/types.ts`](../src/types.ts)
21
21
  - What the motion spec's own parser proves, and which refusals it deliberately
22
22
  leaves to compile time: [`src/motion.ts`](../src/motion.ts)
23
- - What the format holds and rigc covers: `docs/SPEC_COVERAGE.md` โ€” ๐Ÿšซ **not an
24
- authoring input, and deliberately unlinked**: it inventories the benchmark corpus
25
- skeleton by skeleton, so it is on the ladder run's forbidden list. Named here for a
26
- maintainer, not offered to a run
23
+ - What the Spine 4.3 format holds, field by field: `docs/SPEC_COVERAGE.md` โ€” ๐Ÿšซ **not
24
+ an authoring input, and deliberately unlinked**: it and the dated survey it was split
25
+ from, `docs/SURVEY_2026-08-22.md`, which inventories the benchmark corpus skeleton by
26
+ skeleton, are both on the ladder run's forbidden list. Named here for a maintainer,
27
+ not offered to a run
27
28
  - Working with a skeleton **somebody else authored** โ€” what the CLI will and will not
28
29
  do with a foreign `skeleton.json`, transcribing one into specs, re-pivoting and
29
30
  renaming it: `docs/INGEST.md`. ๐Ÿšซ **Not an authoring input, and deliberately
@@ -52,13 +53,12 @@ property of the tool.
52
53
 
53
54
  ๐Ÿ”’ **A ladder run reads this guide in full and does not follow its references out of
54
55
  it.** The guide is allowed reading; not everything it cites is. Citations here are
55
- provenance for a reader of record โ€” the loop that hit a trap, the issue that closed it โ€”
56
- and following one can arrive at a stored candidate's own spec, at the corpus inventory,
56
+ provenance for a reader of record, and following one can arrive at a stored candidate's own spec, at the corpus inventory,
57
57
  or at the **derivation** of the gate a verdict is read against, none of which a run may
58
58
  open. โญ The gate's **clause statements** are a different matter and a run may read them:
59
59
  they are in
60
60
  [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md), which is
61
- repository material and not in the published package โ€” item 11 of the allowed list (owner ruling 2026-08-29) โ€” the
61
+ repository material and not in the published package โ€” item 11 of the allowed list โ€” the
62
62
  measure, the comparator, the number and the SKIP semantics, with no recorded figure in it.
63
63
  So: read the
64
64
  document, take its numbered sections as the input, and leave its footprints to whoever
@@ -160,11 +160,11 @@ What the flags mean:
160
160
  | `--rig` | the rig spec โ€” skeleton structure |
161
161
  | `--motion` | the motion spec โ€” time |
162
162
  | `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths and `skeleton.images` are written relative to it |
163
- | `--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 (issue #370; ยง3.1 says why it is spelled `../<out>/` and not `./`). Under `--atlas-in` those pages are the pack's, not one per part (issue #693 โ€” **ยง0.2**). Default is unchanged โ€” page paths still point at the source art (issue #217) |
164
- | `--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. Default is unchanged (issue #4) โ€” **ยง0.1** |
163
+ | `--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 |
164
+ | `--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** |
165
165
  | `--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** |
166
166
  | `--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** |
167
- | `--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 ([#697](https://github.com/firejune/rigc/issues/697), ยง5.1) |
167
+ | `--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) |
168
168
  | `--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) |
169
169
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
170
170
  | `--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 |
@@ -202,19 +202,16 @@ unweighted meshes, packed atlases) โ€” reach for it when you are shipping into
202
202
  *that* project, not to be thorough. A report always prints which profile ran and
203
203
  lists what that profile left out, on `PROF` lines.
204
204
 
205
- ๐Ÿ†• *Packed atlases* used to be in that list twice over: `--pack --profile
206
- spine-html` was **refused by name**, because `A06`'s coverage clause said "one
207
- part per page" flat and rigc's own pack could not satisfy it. Since
208
- [#266](https://github.com/firejune/rigc/issues/266) that clause is **one part per
209
- page OR a tiling page**, so the combination is an ordinary build โ€” and it is the
210
- only one that puts the renderer's own rulebook over shared-page sampling. What a
205
+ *Packed atlases* are in that list for what a page may hold, not for being packed:
206
+ under `spine-html`, `A06`'s coverage clause is **one part per page OR a tiling
207
+ page**, so `--pack --profile spine-html` is an ordinary build โ€” and it is the only
208
+ one that puts the renderer's own rulebook over shared-page sampling. What a
211
209
  *tiling* page has to satisfy under that profile is stated where the clause is,
212
- ยง5.2's `A06` row: no two regions on one page overlapping. Rotation is still
213
- refused, and that is a separate clause about rigc's packer never turning a region.
210
+ ยง5.2's `A06` row: no two regions on one page overlapping. Rotation is refused, and
211
+ that is a separate clause about rigc's packer never turning a region.
214
212
 
215
- โš ๏ธ The other half of that sentence โ€” every region wholly inside the page it names
216
- โ€” left this profile in [#694](https://github.com/firejune/rigc/issues/694). It is
217
- **validity**, so no profile switches it off: a rectangle outside its page is
213
+ โš ๏ธ The other half of that clause โ€” every region wholly inside the page it names โ€”
214
+ is **validity**, so no profile switches it off: a rectangle outside its page is
218
215
  broken for every consumer, while two regions over the same texels is something
219
216
  correct, editor-exported data does.
220
217
 
@@ -251,7 +248,7 @@ a gap:
251
248
  - **no rotation.** `rotate: 0` on every region, and it is a fact rather than a
252
249
  field: the runtime transposes `u2/v2` at 90 and not at 270, and
253
250
  `RegionAttachment.computeUVs` assigns a different corner order at 90, so a
254
- rotated pack is one rigc's own `--atlas` substitution cannot read (issue #199).
251
+ rotated pack is one rigc's own `--atlas` substitution cannot read.
255
252
  Rotation buys page area; a page that runs out of room spills to a second page
256
253
  instead.
257
254
  - **no re-ordering of anything the skeleton says.** `skeleton.json` from a packed
@@ -310,7 +307,7 @@ drawing at its own size, which is what an attachment's size means.
310
307
  `scale: 0.5` holds texels half the size of the drawings it was packed from โ€” the
311
308
  line says so, and an attachment's `width` is in world units, which the runtime
312
309
  reads straight out of the skeleton JSON with the atlas nowhere in the expression.
313
- So the scale has to be undone here or never (issue #267). But the packer wrote
310
+ So the scale has to be undone here or never. But the packer wrote
314
311
  `round(drawing ร— scale)`, so a 373-texel region at `scale: 0.5` is consistent with
315
312
  a 745- and a 746-pixel drawing and the file does not say which: an imported size
316
313
  is right to within `0.5 / scale` source pixels, and the build report prints the
@@ -324,19 +321,17 @@ texel count beside it so both numbers are visible:
324
321
  measures the PNG. Reach for `--atlas-in` when the pack is what you were handed, or
325
322
  when drawing through the pack's own texels is the point.
326
323
 
327
- ๐Ÿ“ **What you make beside the art stays at the art's size**
328
- ([#762](https://github.com/firejune/rigc/issues/762)). A depth sheet or a soft
324
+ ๐Ÿ“ **What you make beside the art stays at the art's size.** A depth sheet or a soft
329
325
  mask is read in the drawing's pixels on a `scale:` page as on loose parts โ€” each
330
326
  vertex's texel position over the stated scale โ€” and a mesh fit's overshoot is
331
327
  printed in them, with the texel it was measured on named beside it. So are a
332
- `contour`'s `margin` and `tolerance`
333
- ([#779](https://github.com/firejune/rigc/issues/779)): the trace runs on the
328
+ `contour`'s `margin` and `tolerance`: the trace runs on the
334
329
  texels there are, and the two distances are applied on them as `value ร— scale`
335
330
  texels โ€” the scale the header states, never a measured one (below). What stays
336
331
  in texels is only what is counted off them: a contour's hole count.
337
332
 
338
333
  ๐Ÿšจ **A page that declares a size it does not have is a different thing, and it is
339
- refused** ([#715](https://github.com/firejune/rigc/issues/715)). The common shape
334
+ refused.** The common shape
340
335
  is a pack whose `4096x4096` pages ship as `2048x2048` PNGs with the atlas
341
336
  untouched โ€” every region still stated in 4096-space โ€” and that is **not** what a
342
337
  `scale:` line says. A runtime draws it anyway: `TextureAtlas` computes every
@@ -372,14 +367,14 @@ it hits instead of leaving it to be discovered:
372
367
  size the atlas declares, or repack.
373
368
 
374
369
  ๐Ÿ“ **`explain` does not refuse such a page โ€” it withholds what it would have
375
- measured off it** ([#750](https://github.com/firejune/rigc/issues/750)). It never
370
+ measured off it.** It never
376
371
  gates, a runtime draws the page, and most of its report โ€” bones, slots, timelines
377
372
  โ€” reads no texel at all, so the pack still compiles and the report still prints.
378
- What it no longer prints is a figure taken at the coordinates the atlas states,
373
+ What it does not print is a figure taken at the coordinates the atlas states,
379
374
  because on such a file those are another part of the picture: measured on a pack
380
375
  at half resolution, an authored mesh that covers 100.00% of its art and reaches
381
- 16.00px past it printed **68.49%** and **76.24px**, and a `grid` whose depth sheet
382
- reads 10 of its 12 vertices on undrawn texels printed **12 of 12**. The page is
376
+ 16.00px past it would print **68.49%** and **76.24px**, and a `grid` whose depth sheet
377
+ reads 10 of its 12 vertices on undrawn texels would print **12 of 12**. The page is
383
378
  named once where the report starts, with the ratio `A06` refuses it by, and every
384
379
  withheld figure says so where it would have stood:
385
380
 
@@ -402,12 +397,11 @@ on the **coarser** texels the page really has, so a figure that depends on the
402
397
  grid need not equal the one the pack the page was halved from reads โ€” and a
403
398
  contour's outline is one of them, below.
404
399
 
405
- ๐Ÿ“ **A fit's overshoot is stated in the drawing's pixels on every page**
406
- ([#762](https://github.com/firejune/rigc/issues/762)). It is a distance, and on a
407
- page that declares a `scale:` it is taken on the page's texels โ€” so it used to be
408
- printed in them, under the same `px`: one mesh over one drawing read 16.00px on
409
- the declared-size page, **8.00px** on its `scale: 0.5` restatement and **32.00px**
410
- on a `scale: 2` one. It is now the texel distance over the scale the atlas states,
400
+ ๐Ÿ“ **A fit's overshoot is stated in the drawing's pixels on every page.** It is a
401
+ distance, and on a page that declares a `scale:` it is taken on the page's texels โ€”
402
+ counted in them, one mesh over one drawing reads 16.00px on the declared-size page,
403
+ **8.00px** on its `scale: 0.5` restatement and **32.00px** on a `scale: 2` one. So
404
+ it is printed as the texel distance over the scale the atlas states,
411
405
  which is the unit an attachment's `width` is in and the one you drew in, and the
412
406
  line says which grid it was taken on โ€” a texel of a `scale: 0.5` page is 2.00px of
413
407
  the drawing, so that is the step the figure moves in:
@@ -426,14 +420,14 @@ texels are the drawing, and the line is the one it always was. A `contour`'s hol
426
420
  count is a count of those cells and says `texel(s)` on such a page.
427
421
 
428
422
  ๐Ÿ“ **A `contour`'s `margin` and `tolerance` are the drawing's pixels on every
429
- page** ([#779](https://github.com/firejune/rigc/issues/779)). They are your
423
+ page.** They are your
430
424
  statement, in the unit every other size in the spec is in, so on a `scale:` page
431
425
  the trace applies them as `margin ร— scale` and `tolerance ร— scale` texels, and
432
- `maxVertices` is judged on the outline that asks for. They used to be applied in
433
- texels unconverted, which asked each page a different question. Measured on
434
- `halvedMeshPacks`, one spec โ€” `tolerance: 1.5`, `margin: 2`, `maxVertices: 48`:
426
+ `maxVertices` is judged on the outline that asks for. Applied in texels
427
+ unconverted, they would ask each page a different question. Measured on one pack
428
+ at three scales, one spec โ€” `tolerance: 1.5`, `margin: 2`, `maxVertices: 48`:
435
429
 
436
- | page | applied in texels (before) | applied as the drawing's pixels (now) |
430
+ | page | applied in texels | applied as the drawing's pixels (what rigc does) |
437
431
  | --- | --- | --- |
438
432
  | declared size | 15 vertices | **15** vertices, the same mesh |
439
433
  | `scale: 2` | refused: `simplified to 66 vertices at tolerance 1.5, past the 48` | **15** vertices, the declared outline to 0.00px |
@@ -458,7 +452,7 @@ names the figure the spec states, the texels it was applied as and the scale:
458
452
  ```
459
453
 
460
454
  ๐Ÿšจ **A page that is not a PNG is refused by name, before anything is compiled
461
- against it** ([#732](https://github.com/firejune/rigc/issues/732)). rigc reads PNG
455
+ against it.** rigc reads PNG
462
456
  and nothing else โ€” the size `A06` judges, the alpha `A19` judges, the renderer and
463
457
  the region lift all decode PNG, and rigc links no decoder for any other format โ€”
464
458
  and a file's name is not evidence: a production pack shipped WebP pages called
@@ -483,20 +477,16 @@ a page fails `A06` with the same sentence:
483
477
  measures it like any other. โš ๏ธ `A06` deliberately does not read the width and
484
478
  height a WebP header carries in a fixed field. Doing so would print a size that no
485
479
  reader in this tree can check โ€” the parse would agree only with the forgery it was
486
- written against โ€” about a page that is refused either way; and the variant that
487
- let a size-matching WebP page pass was measured building green under the default
488
- profile and writing a directory whose next `render` refused the page.
480
+ written against โ€” about a page that is refused either way; and a size-matching
481
+ WebP page let through that way builds green under the default profile and writes
482
+ a directory whose next `render` refuses the page.
489
483
 
490
484
  **What `--out` holds afterwards:** `skeleton.json` and a `skeleton.atlas` that is
491
485
  the pack, page paths pointing back at the pack's own PNGs โ€” so `rigc validate
492
486
  <that directory>` reads it green with no flags, exactly as it reads a loose
493
487
  build's. Add `--copy-images` and the pack's page PNGs are copied in beside the
494
488
  skeleton and the page names become their basenames, which is the same directory
495
- with nothing outside it left to resolve. โš ๏ธ Until
496
- [#693](https://github.com/firejune/rigc/issues/693) that flag rebuilt the atlas
497
- from the parts the rig declared instead of from the pack, and a rebuild through
498
- `ingest --art none` declares none: the file written was **zero bytes**, on a build
499
- that printed `PASS` for all four atlas assertions.
489
+ with nothing outside it left to resolve.
500
490
 
501
491
  The emitted `skeleton.atlas` **is** the imported one, verbatim except for its page
502
492
  name lines, which are paths and have to be re-anchored to `--out`. Fields rigc
@@ -505,17 +495,15 @@ because the text passes through by line; regions the rig does not use stay in th
505
495
  file, because a real pack is shared between cuts and an importer that quietly
506
496
  dropped half of one would make `--out` disagree with the pack it was built from.
507
497
 
508
- ๐Ÿ”ธ **The one thing not passed through is the pack's blank lines**
509
- ([#803](https://github.com/firejune/rigc/issues/803)). They are written in the
510
- shape rigc writes its own packs in: none before the first page, exactly one between
511
- two page blocks, one trailing newline. Every non-blank line keeps its bytes and its
512
- order, so a pack already in that shape โ€” every editor export in the example corpus,
513
- and every pack `--pack` writes โ€” is emitted byte for byte as before. What this
514
- changes is a pack from a 3.8-era packer, which begins with a blank line: the
515
- runtime reads a run of blank lines before the first page as nothing, so the pack
516
- loads, and until #803 `build` re-emitted the blank and refused its own emission as
517
- `A07_ATLAS_TEXT_SHAPE: line 1: consecutive blank lines`. A run of blank lines
518
- between two page blocks reads as one to the runtime, and is written as one.
498
+ ๐Ÿ”ธ **The one thing not passed through is the pack's blank lines.** They are
499
+ written in the shape rigc writes its own packs in: none before the first page,
500
+ exactly one between two page blocks, one trailing newline. Every non-blank line
501
+ keeps its bytes and its order, so a pack already in that shape โ€” every editor
502
+ export in the example corpus, and every pack `--pack` writes โ€” is emitted byte for
503
+ byte. A pack from a 3.8-era packer begins with a blank line: the runtime reads a
504
+ run of blank lines before the first page as nothing, so the pack loads, and
505
+ `build` writes it without the blank. A run of blank lines between two page blocks
506
+ reads as one to the runtime, and is written as one.
519
507
 
520
508
  Five things are refused rather than warned about, because each of them otherwise
521
509
  **loads clean and draws wrong** โ€” or, the last, cannot be read back at all:
@@ -525,17 +513,17 @@ Five things are refused rather than warned about, because each of them otherwise
525
513
  | a region name the atlas does not have | `AtlasAttachmentLoader` returns null and the part silently does not draw. The refusal lists the near misses โ€” the usual cause is one character |
526
514
  | a size the spec disagrees with | the same silence `A06` exists for, one link earlier: a quad sized against a region of another size collapses |
527
515
  | a page the atlas names and the disk lacks | nothing to sample; caught on the way in, so the message names the atlas rather than the artifact rigc wrote from it |
528
- | a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does. The gate names the same rectangle, under every profile, for a pack that reaches it without passing through here โ€” `A06`, ยง5.2 ([#694](https://github.com/firejune/rigc/issues/694)) |
529
- | a page file that is not a PNG | nothing in rigc can read it โ€” not the gate, not `render`, not a mesh generator โ€” so a green build over it would certify pixels nobody opened. Named by what its first bytes are, every such page of the pack in one sentence; the gate says the same for a directory that reaches it another way โ€” `A06`, ยง5.2 ([#732](https://github.com/firejune/rigc/issues/732)) |
516
+ | a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does. The gate names the same rectangle, under every profile, for a pack that reaches it without passing through here โ€” `A06`, ยง5.2 |
517
+ | a page file that is not a PNG | nothing in rigc can read it โ€” not the gate, not `render`, not a mesh generator โ€” so a green build over it would certify pixels nobody opened. Named by what its first bytes are, every such page of the pack in one sentence; the gate says the same for a directory that reaches it another way โ€” `A06`, ยง5.2 |
530
518
 
531
519
  One limit, stated rather than discovered:
532
520
 
533
521
  - an **optional state** (a manifest `states:` entry) whose region is not in the
534
- pack is a `DROP`, not a refusal โ€” the same documented absence a missing PNG has
535
- always been, and the line names the atlas rather than a file nobody opened.
522
+ pack is a `DROP`, not a refusal โ€” the same documented absence a missing PNG is,
523
+ and the line names the atlas rather than a file nobody opened.
536
524
 
537
- โญ **The missing PNG it is the same rule as, spelled out**, because it was left
538
- implicit here and the two halves are read together. A `states:` entry whose PNG is
525
+ โญ **The missing PNG it is the same rule as, spelled out**, because the two halves
526
+ are read together. A `states:` entry whose PNG is
539
527
  not on disk is a `DROP` too, and the line names the file rather than the pack:
540
528
 
541
529
  ```bash
@@ -552,11 +540,9 @@ with that part missing, at exit 0 and a green gate. Restore the art, or fix the
552
540
  path the manifest states.
553
541
 
554
542
  ๐Ÿ”‘ **A refused build prints its `DROP` lines too**, under the refusal on stderr,
555
- and did not before [#671](https://github.com/firejune/rigc/issues/671): they were
556
- reported from the compile result and a refusal returns none, so the one output
557
- naming the file was missing from exactly the run that failed over it.
543
+ so the one output naming the file is there on exactly the run that failed over it.
558
544
 
559
- A **turned** region is not one of them, and used to be (issue #570). A pack made
545
+ A **turned** region is not one of them. A pack made
560
546
  by somebody else routinely rotates a region to save space โ€” `rotate: 90`,
561
547
  `rotate: 180`, `rotate: 270`, or the format's older `rotate: true` โ€” and rigc
562
548
  reads all of them: anything that measures a part's pixels sees the same grid it
@@ -582,15 +568,13 @@ means for the numbers beside it, and the pack states none of this:
582
568
 
583
569
  Each direction there is **measured** rather than reasoned about: it comes off
584
570
  `MeshAttachment.computeUVs` in the linked runtime, the one routine there that
585
- says where a region's texels are for all four values, and the selftest derives
586
- the words in that list from the same routine instead of reading them. โš ๏ธ Do not
571
+ says where a region's texels are for all four values. โš ๏ธ Do not
587
572
  take the turn from `TextureAtlas`'s own `u2`/`v2` โ€” those transpose at a quarter
588
573
  turn one way and not at the other, so one of the two pairs describes a rectangle
589
- the page does not have ([#579](https://github.com/firejune/rigc/issues/579)).
574
+ the page does not have.
590
575
 
591
- `build` now prints the page rectangle on the line for a turned region, so the
592
- report carries the rectangle to cut instead of leaving it to be derived
593
- ([#718](https://github.com/firejune/rigc/issues/718)):
576
+ `build` prints the page rectangle on the line for a turned region, so the
577
+ report carries the rectangle to cut instead of leaving it to be derived:
594
578
 
595
579
  ```bash
596
580
  # .. pendulum 105x139 <- ../export/atlas.png @ 710,16 rotate 90, occupies 139x105
@@ -599,7 +583,7 @@ report carries the rectangle to cut instead of leaving it to be derived
599
583
  โš ๏ธ Two limits here are real and neither is about rotation:
600
584
 
601
585
  - `--atlas-in` cannot recover what a `scale:` quantised away (above), turned or not;
602
- - the **renderer** profile still refuses a turned region outright
586
+ - the **renderer** profile refuses a turned region outright
603
587
  (`A06`, `--profile spine-html`), because that profile is about artifacts rigc
604
588
  itself emits and it never packs one turned. Reading a foreign pack and gating
605
589
  one under somebody else's renderer policy are different questions.
@@ -648,7 +632,7 @@ repository builds on every run.
648
632
  | `--art loose` (default) | name an `image` per attachment โ€” `<path or placeholder>.png` โ€” so the rebuild resolves loose PNGs and rigc measures them |
649
633
  | `--art none` | state `width`/`height` only, so the rebuild is `build --atlas-in <pack.atlas>` and every part resolves out of the pack |
650
634
  | `--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 |
651
- | `--stage x,y,w,h` | a setup bounding box to **add** to a skeleton that declares none โ€” without it the absence is carried as `"width": null, "height": null` ([#714](https://github.com/firejune/rigc/issues/714)). 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 ([#626](https://github.com/firejune/rigc/issues/626)) |
635
+ | `--stage x,y,w,h` | a setup bounding box to **add** to a skeleton that declares none โ€” 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 |
652
636
  | `--name <n>` | the rig spec's `name`, which the motion spec's `archetype` must equal (default: the file's basename) |
653
637
 
654
638
  โš ๏ธ **`ingest --images` and `build --images` point opposite ways.** `build --images`
@@ -663,8 +647,8 @@ where the parts are in one convention.
663
647
  one โ€” posing the rig gives the *animated* extent, which is a different number from the
664
648
  editor's setup box. So a file that declares none is **carried as declaring none**: the
665
649
  spec states `"width": null, "height": null` (ยง3.1), the rebuild emits no box, and
666
- nothing is recorded, because the rebuild is the file that was read
667
- ([#714](https://github.com/firejune/rigc/issues/714)). `--stage x,y,w,h` *adds* a box โ€”
650
+ nothing is recorded, because the rebuild is the file that was read.
651
+ `--stage x,y,w,h` *adds* a box โ€”
668
652
  supply it from the project the file came from, or from the editor's own canvas โ€” and
669
653
  is recorded as a `NO_STAGE` judgement. A header stating **half** a stage (an origin
670
654
  with no extent) is a `NO_STAGE` blocker: the spec holds a stage as four fields or none.
@@ -672,27 +656,20 @@ with no extent) is a `NO_STAGE` blocker: the spec holds a stage as four fields o
672
656
  โ›” **The flag is refused beside a box the file states.** Two sources for one value, and
673
657
  the file is the one that was measured โ€” so `ingest` names both boxes and stops rather
674
658
  than writing one of them and saying nothing. Drop the flag, or correct `skeleton` in the
675
- source if its box is wrong ([#626](https://github.com/firejune/rigc/issues/626)). What
676
- it does **not** do is refuse an *omitted origin*: inside a declared extent an omitted
677
- `x`/`y` is `0` โ€” the reading `build` emits and `diff` compares
678
- ([#620](https://github.com/firejune/rigc/issues/620)) โ€” so the written spec states it
679
- and a `LOSS HEADER_ORIGIN` line says the source omitted it and that the rebuild will
680
- spell it ([#622](https://github.com/firejune/rigc/issues/622)).
681
-
682
- โš ๏ธ **This said an editor export "never" carries one until
683
- [#594](https://github.com/firejune/rigc/issues/594) measured the corpus.** All twelve
684
- exports under `examples/` declare `x`, `y`, `width` and `height`, `ingest` takes the
685
- early return on every one of them, and not one needs the flag. What an editor export
686
- *may* do is carry none: a rigc build that declares no stage
687
- ([#578](https://github.com/firejune/rigc/issues/578)) came back from a Spine 4.3.26
688
- round trip with a header of `hash`, `spine`, `images`, `audio` and **no box at all** โ€”
689
- the editor preserves the absence rather than inventing a stage
690
- ([#616](https://github.com/firejune/rigc/issues/616)). So `--stage` is for a file that
691
- really has none, and this repository's corpus holds no example of one; a production
692
- corpus measured for [#714](https://github.com/firejune/rigc/issues/714) holds 48 of 48. It is still the
693
- value that costs least to get wrong: `diff` reports the box as `stage_present` and
694
- `stage_box` ([#578](https://github.com/firejune/rigc/issues/578)) and both are
695
- `(reported)`, so nothing on the ladder consults them.
659
+ source if its box is wrong. What it does **not** do is refuse an *omitted origin*:
660
+ inside a declared extent an omitted `x`/`y` is `0` โ€” the reading `build` emits and
661
+ `diff` compares โ€” so the written spec states it and a `LOSS HEADER_ORIGIN` line says
662
+ the source omitted it and that the rebuild will spell it.
663
+
664
+ โš ๏ธ **All twelve exports under `examples/` declare `x`, `y`, `width` and `height`**,
665
+ `ingest` reads each box straight through, and not one needs the flag. What an
666
+ editor export *may* do is carry none: a rigc build that declares no stage came back
667
+ from a Spine 4.3.26 round trip with a header of `hash`, `spine`, `images`, `audio`
668
+ and **no box at all** โ€” the editor preserves the absence rather than inventing a
669
+ stage. So `--stage` is for a file that really has none; the example corpus holds no
670
+ such file, and production exports do โ€” 48 of 48 measured. It is the value that
671
+ costs least to get wrong: `diff` reports the box as `stage_present` and
672
+ `stage_box` and both are `(reported)`, so nothing on the ladder consults them.
696
673
 
697
674
  โš ๏ธ **The duration is a convention, and it is recorded as one.** Skeleton JSON has no
698
675
  duration field. The largest key time is the only derivable answer and it is what a
@@ -709,24 +686,26 @@ the first:
709
686
  | --- | --- |
710
687
  | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read โ€” `point`, a `sequence` block the parser would read as some other series, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
711
688
  | `JUDGE` | the skeleton cannot answer and somebody has to: the stage, and each animation's duration |
712
- | `LOSS` | the skeleton's spelling and rigc's differ, on purpose, and the line says how: `HEADER_BOOKKEEPING` for a field the spec has no home for, `HEADER_REDERIVED` for the version string, `HEADER_ORIGIN` for an origin the source left to the format and the rebuild writes out (#622) |
689
+ | `LOSS` | the skeleton's spelling and rigc's differ, on purpose, and the line says how: `HEADER_BOOKKEEPING` for a field the spec has no home for, `HEADER_REDERIVED` for the version string, `HEADER_ORIGIN` for an origin the source left to the format and the rebuild writes out |
713
690
 
714
691
  โ›” **It reads one generation of the format, and a file from another one ends loud.**
715
692
  Spine data is locked to the generation that exported it and a mismatch does not
716
693
  throw: 4.3 takes constraints from the top-level `constraints` array alone, so a
717
694
  4.0โ€“4.2 file's `ik`/`transform`/`path`/`physics` arrays load as nothing at all โ€” 1,302
718
695
  shipped skeletons parsed on a 4.3 runtime and loaded 0 of 8,672 constraints
719
- ([#706](https://github.com/firejune/rigc/issues/706) row 1). So `ingest` reads
696
+ (GENERATIONS.md, row 1). So `ingest` reads
720
697
  `skeleton.spine` before it reads a field of the file. A file from another generation is
721
698
  a `BLOCK GENERATION_UNSUPPORTED` naming the generation, the string it was read from,
722
699
  and what a 4.3 reader loses **on that file**: the constraints parked in those arrays
723
- counted by kind, the bones carrying 4.2's `transform` where 4.3 spells `inherit`, and
700
+ counted by kind, the bones carrying 4.0/4.1's `transform` where 4.3 spells `inherit`, and
724
701
  the physics constraints omitting `inertia`/`damping`, whose default is not the same
725
702
  number in the two. A label naming no generation rigc knows โ€” or a header stating none โ€”
726
703
  is a `BLOCK GENERATION_UNKNOWN`, never rounded to the nearest: a catalog that rounded
727
704
  handed 19 skeletons labelled `3.8.99` a 4.2 runtime and every one of them posed as NaN
728
- (row 7). Reading a file with *that generation's own* defaults is #706's item 2 and is
729
- not in this tool โ€” re-export as 4.3, or transcribe by hand ([INGEST.md](INGEST.md) ยง2).
705
+ (row 7). Reading a file with *that generation's own* defaults is not in this tool โ€”
706
+ re-export as 4.3, or transcribe by hand ([INGEST.md](INGEST.md) ยง2).
707
+ [GENERATIONS.md](https://github.com/firejune/rigc/blob/main/docs/GENERATIONS.md) is
708
+ the whole policy, and the editor migration it names.
730
709
 
731
710
  ๐Ÿ“ **Do not delete the `note`.** Both written specs carry one saying the file is
732
711
  decompiled and naming the skeleton it came from. A decompiled spec is
@@ -770,12 +749,12 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
770
749
  `ingest --art none` states sizes and names no image. So the size-only pair
771
750
  `build --atlas-in` gates green is readable here through the same flag, and
772
751
  without it the pair is refused by name at exit 2 rather than posed
773
- ([#697](https://github.com/firejune/rigc/issues/697), ยง5.1). โš ๏ธ `--profile`,
752
+ (ยง5.1). โš ๏ธ `--profile`,
774
753
  `--pack`, `--page-size`, `--padding` and `--copy-images` are `build`'s and are
775
754
  not here: four of them decide what is *written*, and this command writes nothing.
776
755
 
777
756
  ๐Ÿ“ **What it will not measure: the texels of a page that is not its declared
778
- size** ([#750](https://github.com/firejune/rigc/issues/750)). Under
757
+ size.** Under
779
758
  `--atlas-in`, a page whose PNG is not the `size:` its atlas states is named once
780
759
  at the top of the report, and the figures that would have been read off it โ€” an
781
760
  authored mesh's `covers โ€ฆ% of the art, reaching โ€ฆpx past it`, a depth sheet's
@@ -785,7 +764,7 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
785
764
  and the repair: ยง0.2. On a page that **does** declare a `scale:`, the overshoot
786
765
  in that line is the drawing's pixels and the line names the texel it was taken
787
766
  on (`โ€ฆ measured on the page's texels at scale: 0.5 โ€” a texel is 2.00px of the
788
- drawing`, [#762](https://github.com/firejune/rigc/issues/762)).
767
+ drawing`).
789
768
  - **`diff`** compares two skeletons and reports **a ratio per measure** in six
790
769
  sections (bones, slots, attachments, constraints, animations, events). It
791
770
  deliberately does not combine them into a score: a rig with the right skeleton
@@ -951,24 +930,12 @@ one A09 does compare.
951
930
  **R1 โ€” A field is emitted when you declare it, and left out where the parser would
952
931
  read the same value without it.** Omit a field and Spine's default stands; write it
953
932
  and it reaches the file โ€” unless what you wrote **is** that default, in which case
954
- the emitter leaves it out, the way the editor's own exporter does (issue
955
- [#716](https://github.com/firejune/rigc/issues/716)). Writing `x: 0` is still
933
+ the emitter leaves it out, the way the editor's own exporter does. Writing `x: 0` is still
956
934
  legitimate, and your spec still says it: the spec is the record of what you wrote,
957
935
  and the file is what the runtime reads. The two cannot disagree about a value,
958
936
  because a key is left out only where the 4.3 parser loads the same `SkeletonData`
959
937
  without it โ€” exact equality with the float the file would hold, so `x: 1e-45` is
960
- written โ€” and the selftest loads every build both ways to hold that (ยง10.6c).
961
-
962
- โš ๏ธ This rule said *"not 'when it differs from the default'"* until #716, on two
963
- grounds: that a rig may need to say `x: 0` out loud, and that deciding emission from
964
- the value would make the file depend on arithmetic. Neither survived being measured.
965
- What reads the file is the 4.3 parser, which reads the absent key as the same number,
966
- and the editor, whose own export leaves the same key out โ€” so there is nobody to say
967
- it out loud *to*; and the decision is an exact comparison with the parser's fallback,
968
- which is not arithmetic in any sense a reader has to redo. What the
969
- old rule cost was concrete: a rebuild of an editor export restated **2,338** keys the
970
- export leaves to the parser, so `ingest โ†’ build` could never give an editor's file
971
- back.
938
+ written (ยง10.6c).
972
939
 
973
940
  **R2 โ€” The compiler never invents a value.** No defaults guessed from the art, no
974
941
  re-measured plates, no reasonable fallbacks. A missing number is a `CompileError`
@@ -999,8 +966,7 @@ two files put two regions in it and each attachment draws its own (ยง3.4.2), whi
999
966
  two attachments naming the same file share the one region that file made. What
1000
967
  cannot be reconciled is two *different* files whose basenames collide โ€” only one of
1001
968
  them can be region `patch` โ€” so rigc refuses the build and names both paths rather
1002
- than letting one of them silently draw the other's pixels
1003
- ([#555](https://github.com/firejune/rigc/issues/555)).
969
+ than letting one of them silently draw the other's pixels.
1004
970
 
1005
971
  **R6 โ€” A key carries `ease` or `curve`, never both.** A named easing says "this
1006
972
  shape, wherever it is used" and is the recommended path. `curve` is the escape
@@ -1032,19 +998,16 @@ reads best; the emit keys them the way the Spine editor does. This is the one pl
1032
998
  reorders anything you wrote, and it is not cosmetic: a `slider`'s animation is a
1033
999
  **name** in JSON and an **ordinal** in the format's binary half, so an editor that
1034
1000
  re-sorts the object repoints every slider whose animation moved index โ€” silently,
1035
- in a file that still parses and still gates green (ยง3.5.2,
1036
- [#535](https://github.com/firejune/rigc/issues/535)). Nothing else moves: each
1001
+ in a file that still parses and still gates green (ยง3.5.2). Nothing else moves: each
1037
1002
  animation's own body is byte-identical either way, and every other collection is
1038
1003
  emitted in the order you gave it.
1039
1004
 
1040
1005
  โš ๏ธ **What that comparator is, measured rather than inferred.** Five name lists
1041
1006
  went through a licensed 4.3.26 editor as JSON and came back as JSON, and the ten
1042
1007
  answers are in the repository as
1043
- `fixtures/editor-order/probe{1..5}.{in,out}.json`
1044
- ([#728](https://github.com/firejune/rigc/issues/728)). Every row below is a clause
1008
+ `fixtures/editor-order/probe{1..5}.{in,out}.json`. Every row below is a clause
1045
1009
  of the rule with the pair from those files that shows it โ€” nothing here is a
1046
- guess, and the selftest re-derives each row from the files rather than from this
1047
- page:
1010
+ guess:
1048
1011
 
1049
1012
  | The editor | Shown by |
1050
1013
  | --- | --- |
@@ -1059,14 +1022,13 @@ page:
1059
1022
  ๐Ÿ”ธ **One clause of the rule is not from those files.** Before comparing, the
1060
1023
  comparator reads U+3000 IDEOGRAPHIC SPACE as a space and the full-width digits
1061
1024
  U+FF10โ€“U+FF19 as `0`โ€“`9`. No probe carried either; the fold was measured on a
1062
- production skin's slot keys ([#791](https://github.com/firejune/rigc/issues/791),
1063
- ยง10.6b says what was measured and why it is those two classes and not NFKC), and
1025
+ production skin's slot keys (ยง10.6b says what was measured and why it is those
1026
+ two classes and not NFKC), and
1064
1027
  it applies here because R10, R11 and a skin's slot keys are one comparator. So
1065
1028
  `shot๏ผ’` sorts before `shot10`, and a pair an ideographic space decides is
1066
1029
  ordered rather than refused as `separator`.
1067
1030
 
1068
- โญ **A tie is not an ambiguity, and that is what retired most of this rule's
1069
- refusals.** Two names the comparator cannot separate come back in the order the
1031
+ โญ **A tie is not an ambiguity.** Two names the comparator cannot separate come back in the order the
1070
1032
  file gave them, so the order rigc emits for such a pair is **your own declaration
1071
1033
  order** and the editor keeps it. Nothing moves index, so there is nothing to
1072
1034
  refuse: `Turn` beside `turn`, `turn01` beside `turn1`, `1turn` beside `turn`,
@@ -1086,30 +1048,18 @@ So what is left refused is short, and each row is a pair two readings of the
1086
1048
  numbered series that crosses 9 โ†’ 10 is keyed **1, 2, โ€ฆ 9, 10, 11, 12**, which is
1087
1049
  what the editor does with it โ€” and is not what a codepoint sort does.
1088
1050
 
1089
- โœ… **This rule was a quantifier over comparators until #728, and that is what
1090
- changed.** rigc keyed `animations` codepoint-ascending until
1091
- [#543](https://github.com/firejune/rigc/issues/543) and then emitted a member of
1092
- the "natural, case-insensitive" family, refusing every pair the family could
1093
- disagree about. Both refusals were sound and both over-refused by construction,
1094
- because a quantifier stands in for a measurement: a name with a capital, an
1095
- accent, a space or a folder in it was refused rather than emitted in the order the
1096
- editor returns. Measuring the comparator moves no byte on any set the old rule
1097
- accepted; it stops refusing the ones it did.
1098
-
1099
1051
  **R11 โ€” The `skins` array is written with `default` first and the rest in exactly
1100
1052
  the order R10 describes.** A skin is a **name** in the JSON half of the format and
1101
1053
  an **ordinal** in the binary half โ€” `skins[readInt()]` for an attachment timeline,
1102
1054
  `skins[skinIndex]` for a linked mesh โ€” so an editor that writes the array in
1103
1055
  another order repoints every such reference, silently, in a file that still
1104
1056
  parses. Measured: a rig built `default, zulu, mike, alpha` exported
1105
- `default, alpha, mike, zulu`
1106
- ([#541](https://github.com/firejune/rigc/issues/541)).
1057
+ `default, alpha, mike, zulu`.
1107
1058
 
1108
1059
  โš ๏ธ **It is one comparator, and that is measured too.** Two of the five round
1109
1060
  trips carried one name list as **both** collections and both came back in one
1110
- order, which is what retired the wider skin refusal #541 shipped: until #728,
1111
- `Zulu` beside `mike` and `mike10` beside `mike2` built as animation names and were
1112
- refused as skin names. Both build now. `default` is pinned rather than sorted, on
1061
+ order, so `Zulu` beside `mike` and `mike10` beside `mike2` build as skin names
1062
+ exactly as they do as animation names. `default` is pinned rather than sorted, on
1113
1063
  names the same comparator puts ahead of it โ€” `default` before `2`, `default`
1114
1064
  before `A` โ€” so renaming a skin away from `default` makes it an ordinary name that
1115
1065
  sorts like one.
@@ -1118,16 +1068,14 @@ sorts like one.
1118
1068
  the `default` skin may not share a placeholder with a named skin.** rigc derives
1119
1069
  no name. A placeholder several skins fill is emitted under that placeholder in
1120
1070
  each skin, carrying the `name` each entry states and none where it states none โ€”
1121
- which is the shape the editor itself exports (ยง3.4.2). Until
1122
- [#796](https://github.com/firejune/rigc/issues/796) rigc composed
1123
- `"<skin>/<placeholder>"` there, which renamed every such attachment at runtime.
1071
+ which is the shape the editor itself exports (ยง3.4.2).
1124
1072
  What you *do* author is where the shared art lives: a
1125
1073
  placeholder the `default` skin shares with a named skin is a **compile error**,
1126
- because the Spine editor has no representation for it in either spelling (#567,
1127
- measured on 4.3.26 โ€” named, the export re-keys it and the default skin draws
1074
+ because the Spine editor has no representation for it in either spelling (measured
1075
+ on 4.3.26 โ€” named, the export re-keys it and the default skin draws
1128
1076
  nothing; unnamed, the import is refused). Put the shared entry in a named skin โ€”
1129
1077
  `base` โ€” and every filler is a named skin. A placeholder only one skin fills is
1130
- emitted exactly as before, in the default skin or anywhere else.
1078
+ emitted as written, in the default skin or anywhere else.
1131
1079
 
1132
1080
  ---
1133
1081
 
@@ -1141,8 +1089,7 @@ payloads in this guide are written to illustrate a field, never copied out of a
1141
1089
  reference export โ€” an example lifted from one would be handing an authoring agent an
1142
1090
  answer to the rung it is standing on, which is the rule ยง10.6 states and the honesty
1143
1091
  rule in [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) turns on. If a snippet here matches a reference file,
1144
- that is a defect in this guide: report it. (It has happened โ€” 2026-08-23; the incident
1145
- is recorded in `bench/runs/README.md`, *What a run may read*.)
1092
+ that is a defect in this guide: report it.
1146
1093
 
1147
1094
  ### 3.1 `skeleton` โ€” the header
1148
1095
 
@@ -1152,53 +1099,49 @@ is recorded in `bench/runs/README.md`, *What a run may read*.)
1152
1099
  | `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** |
1153
1100
  | `fps` | nonessential editor hint | `SkeletonData.fps` stays 30 |
1154
1101
  | `referenceScale` | 4.2+ physics/scale reference | parser default 100 |
1155
- | `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 (issue #370) |
1156
- | `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 ([#716](https://github.com/firejune/rigc/issues/716)). Anything but a string or `null` is a compile error naming the value |
1102
+ | `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 |
1103
+ | `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 |
1157
1104
 
1158
1105
  `spine` and `hash` are not yours to write: rigc emits its own version label
1159
1106
  (`A16` re-checks it is on the 4.3 line) and inventing a hash would claim an export
1160
1107
  this file did not come from. The editor's import then warns `Data version 4.3.13
1161
1108
  does not match Spine version 4.3.23. The Spine version should match the version
1162
1109
  that exported the data file.` and imports anyway โ€” the label is the runtime rigc
1163
- links, not the editor that will open the file, and the warning is harmless
1164
- (issue #370).
1110
+ links, not the editor that will open the file, and the warning is harmless.
1165
1111
 
1166
1112
  `width`/`height` are what `A14` and `A19` measure against, so a guessed stage is a
1167
1113
  gate measuring against a number nobody wrote down.
1168
1114
 
1169
1115
  โญ **A skeleton may declare no stage, and saying so is not the same as saying
1170
- nothing** (issue #578). Write the pair as `null`:
1116
+ nothing.** Write the pair as `null`:
1171
1117
 
1172
1118
  ```json
1173
1119
  "skeleton": { "width": null, "height": null }
1174
1120
  ```
1175
1121
 
1176
1122
  and the emitted header carries **none** of `x`/`y`/`width`/`height` โ€” which is
1177
- what an export of a skeleton whose stage was never set looks like, and the shape a
1178
- transcriber of one now has something to write. `null` is this spec's spelling for
1179
- a stated absence wherever it has one (`slots[].attachment` is `null` for "show
1180
- nothing"), so nothing new is introduced here but a third value of a field that
1181
- already existed.
1123
+ what an export of a skeleton whose stage was never set looks like. `null` is this
1124
+ spec's spelling for a stated absence wherever it has one (`slots[].attachment` is
1125
+ `null` for "show nothing").
1182
1126
 
1183
1127
  Three readings stay apart, and the middle one is the point of the other two:
1184
1128
 
1185
1129
  | What the spec says | What happens |
1186
1130
  | --- | --- |
1187
- | a number for each | the stage, as before; a manifest `crop` is the fallback |
1131
+ | a number for each | the stage; a manifest `crop` is the fallback |
1188
1132
  | `"width": null, "height": null` | builds, and emits a header with no stage at all โ€” and this **beats** a manifest's `crop`, because a rig spec is where a claim about the skeleton is made |
1189
- | neither | **refused**, exactly as before: `no stage size: โ€ฆ` |
1133
+ | neither | **refused**: `no stage size: โ€ฆ` |
1190
1134
  | one `null`, one number | refused โ€” a stage has both extents or neither, and which half was meant is not derivable |
1191
1135
  | the pair `null` **and** an `x` or `y` | refused โ€” an origin for a box that is not there |
1192
1136
 
1193
- What each tool does without one โ€” every reader of the stage in the tree, measured
1194
- for [#714](https://github.com/firejune/rigc/issues/714), and not one of them puts a
1195
- number where the box would be:
1137
+ What each tool does without one โ€” not one of them puts a number where the box
1138
+ would be:
1196
1139
 
1197
1140
  | Reader | With a stage | Without one |
1198
1141
  | --- | --- | --- |
1199
1142
  | `build` | emits `x`/`y`/`width`/`height` | emits none of them |
1200
1143
  | `A14_NO_FULL_FRAME_MESH` | fails a mesh as big as the stage | **SKIP**, by name |
1201
- | `A19_OVERLAY_PNGS_HAVE_ALPHA` | exempts the base plate a cut manifest names โ€” the part whose window is the crop โ€” and, on a build that names none, the one image that covers the stage | a manifest build exempts the plate it names, exactly as with one ([#770](https://github.com/firejune/rigc/issues/770)). A rig-spec build names no base plate, so an opaque part is refused with *"this skeleton declares no stage size to measure one against, and the build names no base plate"* and the two ways to decide it: a `skeleton` stage the plate covers, or *"build from a cut manifest, whose base plate is the part whose window is the crop"* |
1144
+ | `A19_OVERLAY_PNGS_HAVE_ALPHA` | exempts the base plate a cut manifest names โ€” the part whose window is the crop โ€” and, on a build that names none, the one image that covers the stage | a manifest build exempts the plate it names, exactly as with one. A rig-spec build names no base plate, so an opaque part is refused with *"this skeleton declares no stage size to measure one against, and the build names no base plate"* and the two ways to decide it: a `skeleton` stage the plate covers, or *"build from a cut manifest, whose base plate is the part whose window is the crop"* |
1202
1145
  | `diff` | `stage_present` and `stage_box` | `stage_present` 1/1 when neither side declares one (agreement), `stage_box` 0/0 |
1203
1146
  | `explain` | `stage W x H` | `stage none declared` |
1204
1147
  | `render` | frames the posed extent of every animation | the same frames, plus a line saying the viewport is the posed extent and no stage |
@@ -1210,7 +1153,7 @@ number where the box would be:
1210
1153
  โš ๏ธ A stage-less skeleton is **unmeasured, not certified**: `A14_NO_FULL_FRAME_MESH`
1211
1154
  reports **SKIP** on one, because there is no full frame for a mesh to span. And
1212
1155
  `rigc diff` reports it โ€” `skeleton.stage_present` and `skeleton.stage_box`, in the
1213
- header block at the top of the report โ€” so a stage somebody invented now reads
1156
+ header block at the top of the report โ€” so a stage somebody invented reads
1214
1157
  below 1.000 against a source that has none.
1215
1158
 
1216
1159
  โญ **A stage at `0,0` is not a stage-less one, and an editor export spells it by
@@ -1242,7 +1185,7 @@ it would simply be a second root.
1242
1185
  | `rotation` | degrees, counter-clockwise, y **up** | `0` |
1243
1186
  | `scaleX`, `scaleY` | | `1` |
1244
1187
  | `shearX`, `shearY` | | `0` |
1245
- | `inherit` | `normal` ยท `onlyTranslation` ยท `noRotationOrReflection` ยท `noScale` ยท `noScaleOrReflection`. The first letter's case is free (`NoScale` loads) and the rest is exact: the runtime folds that one letter and nothing else, so `NOSCALE` is refused โ€” it used to compile and load as **no mode at all** ([#733](https://github.com/firejune/rigc/issues/733)). An animation can change it over time: the `inherit` track (ยง4.4) | `normal` |
1188
+ | `inherit` | `normal` ยท `onlyTranslation` ยท `noRotationOrReflection` ยท `noScale` ยท `noScaleOrReflection`. The first letter's case is free (`NoScale` loads) and the rest is exact: the runtime folds that one letter and nothing else, so `NOSCALE` is refused โ€” the runtime would load it as **no mode at all**. An animation can change it over time: the `inherit` track (ยง4.4) | `normal` |
1246
1189
  | `skin` | `BoneData.skinRequired` | `false` |
1247
1190
  | `color` | `rrggbbaa`, editor affordance | โ€” |
1248
1191
  | `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 | โ€” |
@@ -1275,18 +1218,13 @@ Declaring a slot no cut fills is therefore normal โ€” it fixes where that slot s
1275
1218
  whether or not this cut has art for it โ€” and transcribing a foreign skeleton that
1276
1219
  carries an empty slot reproduces it exactly.
1277
1220
 
1278
- โš ๏ธ **This changed with issue #575.** Such a slot used to be *dropped*, with no message, and
1279
- the gate allowed the emitted array to be any subsequence of the rig's. What it cost
1280
- is the index: every slot below the dropped one moved up one place, which is what a
1281
- `drawOrder` key's offsets are counted against and what an index-keyed consumer
1282
- splits on. Two production exports declaring 53 and 61 slots built green at 51 and 57
1283
- and read 0.962 and 0.934 under `diff` against the file they were transcribed from.
1284
- If you have a rig that leaned on the drop, the emitted array simply grows; nothing
1285
- else about it moves.
1221
+ โš ๏ธ **Keeping the empty slot is what keeps the index.** A `drawOrder` key's offsets
1222
+ are counted against the slots array, and so is an index-keyed consumer, so a slot
1223
+ dropped from it would move every slot below it up one place.
1286
1224
 
1287
1225
  ๐Ÿšซ **Naming an attachment on a slot nothing fills is refused by name**: `the setup
1288
1226
  pose shows attachment "x" on slot "y", which no skin and no manifest part fills`.
1289
- That is the half-finished wiring-up the old silence hid โ€” the slot is emitted empty
1227
+ That is half-finished wiring-up โ€” the slot is emitted empty
1290
1228
  and the name resolves to nothing, so either give the slot an attachment or state the
1291
1229
  setup pose as `null`.
1292
1230
 
@@ -1299,10 +1237,10 @@ called `default`.)
1299
1237
 
1300
1238
  A skin can also say which bones and constraints it **switches on**, and that needs
1301
1239
  one more level, so a skin entry has a second spelling โ€” see ยง3.4.1. The short one
1302
- above is unchanged and is what almost every rig wants.
1240
+ above is what almost every rig wants.
1303
1241
 
1304
1242
  ๐Ÿ”ธ **`default` is a name, not a requirement, and rigc writes one exactly when the
1305
- spec has one** ([#801](https://github.com/firejune/rigc/issues/801)). A rig may put
1243
+ spec has one.** A rig may put
1306
1244
  every attachment in named skins and declare no `default` at all, which is what an
1307
1245
  editor export of a multi-skin character gives back: the 4.3.26 editor took a build
1308
1246
  declaring `skins: [default, alt, base]` with `default` **empty** and exported
@@ -1311,9 +1249,7 @@ key โ€” `"default": {}` included, which is written back empty โ€” or when a mani
1311
1249
  part files its states under it, and **none otherwise**. spine-core loads both
1312
1250
  spellings alike (`SkeletonData.defaultSkin` is null or an empty skin, and neither
1313
1251
  fills a slot), and the binary format has no way to hold an empty default skin at
1314
- all. Until #801 rigc wrote an empty `default` into every build whether the spec
1315
- had one or not, so a rebuild of such an export read `attachments.skins 1/2`
1316
- against it. Two things change for a rig with no default skin, and both are
1252
+ all. Two things differ for a rig with no default skin, and both are
1317
1253
  named: a linked mesh that states no `skin` looks for its source in the default
1318
1254
  skin and is refused (the error table in ยง5.1), and `rigc explain` says the
1319
1255
  skeleton declares no default skin above its slot list instead of printing
@@ -1329,11 +1265,7 @@ with **no pages**, and rigc writes `skeleton.atlas` as an **empty file** (zero
1329
1265
  bytes). Four rules then report SKIP by name rather than a pass, because a page is
1330
1266
  their whole subject: `A07_ATLAS_TEXT_SHAPE`, `A06_ATLAS_PAGE_SIZE_MATCHES_PNG`,
1331
1267
  `A17_ATLAS_PAGE_FILES_EXIST` and โ€” under `spine-html` โ€”
1332
- `A19_OVERLAY_PNGS_HAVE_ALPHA` and `A27_REGION_NAME_MATCHES_PAGE_FILENAME`. Until
1333
- [#608](https://github.com/firejune/rigc/issues/608) the same compile wrote one
1334
- newline instead and `A07` refused it with two findings, so a hit-box skeleton โ€” a
1335
- correct rig, whose geometry `A33_VERTEX_ATTACHMENT_GEOMETRY` passes โ€” could not be
1336
- built at all.
1268
+ `A19_OVERLAY_PNGS_HAVE_ALPHA` and `A27_REGION_NAME_MATCHES_PAGE_FILENAME`.
1337
1269
 
1338
1270
  โš ๏ธ **This is not the case where an attachment WANTS a region.** A region or mesh
1339
1271
  attachment that states `width`/`height` and names no `image` still resolves a
@@ -1349,8 +1281,7 @@ there is nothing to draw`. That is the honest division โ€” the rig is valid Spin
1349
1281
  data, and there is no picture of it. `rigc check` says the same thing one step
1350
1282
  on, since it has no frames to compare; `tools/editor_roundtrip.ts` quotes both
1351
1283
  renderers and reports its step 5 as a **SKIP** naming that, and the round trip
1352
- comes back green on the strength of `validate` and `diff`
1353
- ([#621](https://github.com/firejune/rigc/issues/621)). A skin only **one** side
1284
+ comes back green on the strength of `validate` and `diff`. A skin only **one** side
1354
1285
  can draw is the other case entirely, and stays red.
1355
1286
 
1356
1287
  **Region attachment** ([Spine: region attachments](http://esotericsoftware.com/spine-regions)),
@@ -1381,15 +1312,10 @@ region does: stated wins, otherwise the PNG's basename when that differs from th
1381
1312
  attachment's name โ€” its `name`, else the placeholder โ€” otherwise nothing. The parser reads `path` off both with the same line
1382
1313
  (`getValue(map, "path", name)`, `SkeletonJson.ts:541` and `:570`), and `path`
1383
1314
  defaults to the attachment's **name** rather than to the placeholder โ€” so a mesh
1384
- with `image: hair_short.png` under a placeholder called `hair` resolves the region
1385
- `hair`, which no atlas has. Until
1386
- [#577](https://github.com/firejune/rigc/issues/577) a region derived it and an
1387
- authored mesh did not, so that rig **built** and then failed
1388
- `A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: hair` โ€” which is the
1389
- loader's sentence and all the report had. Since
1390
- [#589](https://github.com/firejune/rigc/issues/589) the same miss is named by
1391
- `A08_REGION_NAMES_MATCH_ATTACHMENTS`, with the skin, the slot, the placeholder
1392
- and the attachment's own name beside the path.
1315
+ with `image: hair_short.png` under a placeholder called `hair` and no derived
1316
+ `path` would resolve the region `hair`, which no atlas has. A region that is
1317
+ missing anyway is named by `A08_REGION_NAMES_MATCH_ATTACHMENTS`, with the skin,
1318
+ the slot, the placeholder and the attachment's own name beside the path.
1393
1319
 
1394
1320
  Where each vertex sits on the **art**:
1395
1321
 
@@ -1404,8 +1330,7 @@ of the **top-left** quadrant's colour and not one pixel of any other, and the sa
1404
1330
  quad given `0.5 โ†’ 1` as 3,120 px of the **bottom-right**. Beside it, the runtime's own
1405
1331
  `MeshAttachment.computeUVs`, handed the emitted atlas region, put `(0, 0)` at page
1406
1332
  pixel `(0.00, 0.00)` and `(1, 1)` at `(64.00, 48.00)` โ€” the region's own two
1407
- corners, in the page's y-down pixels. `CUR44` in the selftest holds the sentence
1408
- above against that second measurement.
1333
+ corners, in the page's y-down pixels.
1409
1334
 
1410
1335
  Geometry comes in one of two fields:
1411
1336
 
@@ -1424,19 +1349,19 @@ Geometry comes in one of two fields:
1424
1349
 
1425
1350
  โญ **Weights bind bones by NAME, like everything else in a rig spec.** A bone's
1426
1351
  `parent`, a slot's `bone`, an ik constraint's `bones` and `target` and a draw-order
1427
- key's `slot` all resolve by name and refuse a miss by name, and mesh weights now do
1352
+ key's `slot` all resolve by name and refuse a miss by name, and mesh weights do
1428
1353
  too: an unknown name is a `CompileError` that says which vertex and which name, and
1429
1354
  the compiler resolves the names to indices on emit. So inserting a bone renumbers
1430
1355
  the emitted array and rebinds nothing.
1431
1356
 
1432
- ๐Ÿšจ **The index form is still reachable and it still costs silence.** Spine's own
1357
+ ๐Ÿšจ **The index form is reachable, and it costs silence.** Spine's own
1433
1358
  encoding is a flat run โ€” `boneCount, (boneIndex, bindX, bindY, weight) ร— n, โ€ฆ` โ€”
1434
1359
  where `boneIndex` is a position in the **emitted** bone array, a list the rig spec
1435
1360
  never writes and cannot see. Put one bone ahead of the meshes and every vertex
1436
1361
  rebinds: the file still loads, every index is still in range, every vertex's weights
1437
1362
  still sum to 1, and `A04`, `A20` and `diff` are all quiet, because an index has no
1438
1363
  name to be wrong. (Measured, on the rung 6 transcription: union MAE 3.30 โ†’ 15.09,
1439
- worst mesh-slot drift 0.09 px โ†’ 9.8 px, with a green gate throughout. Issue #45.)
1364
+ worst mesh-slot drift 0.09 px โ†’ 9.8 px, with a green gate throughout.)
1440
1365
  rigc therefore refuses a weighted `vertices` run unless the attachment says
1441
1366
  `"boneIndexing": "raw"` out loud โ€” an opt-in, because what is being opted into is
1442
1367
  the silence.
@@ -1465,11 +1390,9 @@ these messages prints the outline walk, because the walk is the fix:
1465
1390
  | outline vertices that are first but zigzag | `hull vertices must trace the outline in order; the triangles' outline runs 0 โ†’ 1 โ†’ 3 โ†’ 5 โ†’ โ€ฆ โ†’ 4 โ†’ 2, so vertex 3 has to follow vertex 1 in the list, and vertex 2 does. Renumber the vertices along that walk` |
1466
1391
  | triangles with no single outline | `the triangles do not tile the outline: 25 vertices with a 16-vertex outline tile as 32 triangles and there are 33` โ€” an unused vertex or a doubled interior triangle; or `the triangles' outline is not one closed loop: vertex 4 has 4 boundary edges` โ€” a pinch, a hole, or a doubled triangle on the perimeter |
1467
1392
 
1468
- ๐Ÿšจ **A row-major grid is the case this catches**, and until
1469
- [#368](https://github.com/firejune/rigc/issues/368) it was `gallery/portrait`'s: a
1470
- 5ร—5 grid's perimeter is 16 of its 25 vertices, interleaved with the interior, so
1471
- no `hull` can describe that list โ€” and `hull: 0`, which rigc used to write there,
1472
- is what the editor repairs on import by making **every** vertex a hull vertex in
1393
+ ๐Ÿšจ **A row-major grid is the case this catches**: a 5ร—5 grid's perimeter is 16 of
1394
+ its 25 vertices, interleaved with the interior, so no `hull` can describe that
1395
+ list โ€” and `hull: 0` is what the editor repairs on import by making **every** vertex a hull vertex in
1473
1396
  list order and saying so in a WARNING. A two-column strip is the same trap with
1474
1397
  every vertex on the outline: the list zigzags across, the outline runs down one
1475
1398
  side and up the other, and the hull it would declare self-intersects. The fix is
@@ -1489,17 +1412,14 @@ export carries the edges somebody drew โ€” passes through verbatim.
1489
1412
  `width`/`height` are the size of the image the mesh is drawn on. Stated wins;
1490
1413
  omitted, they are the named PNG's measured size (R5), the same number a region
1491
1414
  reads. With neither a size nor an `image`: `a mesh needs width and height โ€” give
1492
- them, or give an "image" and rigc will measure the PNG`. They were written as 0
1493
- before #368 โ€” a size no spec stated.
1415
+ them, or give an "image" and rigc will measure the PNG`.
1494
1416
 
1495
1417
  โš ๏ธ **Authored geometry is not a rigc generator, and the gate says so.** rigc built
1496
1418
  neither its rim nor its rows, so it gets to assume nothing about its topology:
1497
1419
  `A21_MESH_RIM_PINNED` and `A28_RIBBON_ROWS_SHARE_WEIGHTS` **SKIP** on an authored
1498
1420
  mesh with that as the reason, and `A20`'s two generator-policy branches (a mesh here
1499
1421
  is weighted; a generated mesh binds only bones that move it) do not apply to one.
1500
- `A20`'s coherence rules โ€” weights present, in range, summing to 1 โ€” still do. Issue
1501
- #44; before it was fixed, `A21` reported 40 failures on a correct 40-vertex editor
1502
- mesh because an absent `meshKinds` entry read as `ring`.
1422
+ `A20`'s coherence rules โ€” weights present, in range, summing to 1 โ€” still do.
1503
1423
 
1504
1424
  โญ **Coverage is the exception, and it is reported for an authored mesh too.** Those
1505
1425
  rules are about a mesh's **structure** โ€” where its rim is, how its rows pair โ€”
@@ -1524,15 +1444,13 @@ meant to bend a core while its edges stretch โ€” so the figure informs and the
1524
1444
  decision stays with the author. A mesh with no `image` reports nothing, because
1525
1445
  there is nothing to measure it against.
1526
1446
 
1527
- **The silence was worth closing, and that example is where it was found.** The
1528
- ball is a centre vertex plus 8 rim vertices, and the first version placed them
1529
- *on* the silhouette โ€” but an octagon's sides pass `R ยท cos(ฯ€/8)` from its centre,
1530
- so its whole ink outline between the spokes was not going to be drawn, and every
1531
- assertion passed (issue #277). That is why the command above prints 100.00%
1532
- rather than the figure it was filed over:
1447
+ **Why the figure is worth reading.** The ball is a centre vertex plus 8 rim
1448
+ vertices, and an octagon's sides pass `R ยท cos(ฯ€/8)` from its centre โ€” so rim
1449
+ vertices placed *on* a round silhouette leave the ink outline between the spokes
1450
+ undrawn, and every assertion passes.
1533
1451
  [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash)'s
1534
1452
  README carries the inradius arithmetic, both coverage readings, and the rim move
1535
- that settled it.
1453
+ that fixes it.
1536
1454
 
1537
1455
  **Linked mesh** ([Spine: linked meshes](http://esotericsoftware.com/spine-meshes)) โ€”
1538
1456
  a mesh that draws **another mesh's geometry** with **its own art**. It is the type
@@ -1568,7 +1486,7 @@ skeleton rigc did **not** write, where the compiler never sees the spec:
1568
1486
  `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` (ยง5.2) names the attachment, the
1569
1487
  keys and whose geometry is drawn instead, and `ingest` reports one as
1570
1488
  `ATTACHMENT_LINK_GEOMETRY` before dropping it
1571
- ([INGEST ยง2.0](INGEST.md)) โ€” [#710](https://github.com/firejune/rigc/issues/710).
1489
+ ([INGEST ยง2.0](INGEST.md)).
1572
1490
 
1573
1491
  ๐Ÿšซ **A chain is refused, and so is a link to itself.** A `source` that names
1574
1492
  another linked mesh resolves in the order the file was read: measured through
@@ -1669,7 +1587,7 @@ other vertex attachment.
1669
1587
  | `name`, `vertexCount`, `vertices`, `weights`, `boneIndexing`, `color` | as on a bounding box โ€” except that these vertices are knots **and** their handles, which the count rule below is about |
1670
1588
  | `closed` | default **false**. True joins the last knot back to the first |
1671
1589
  | `constantSpeed` | default **true** โ€” note the direction. Leaving it out asks for the expensive-and-correct traversal, in which the runtime re-measures the path every frame and `lengths` is never read. `false` makes the runtime trust the emitted `lengths` instead: cheaper, exact only while the path holds its setup shape, and the reason a deformed path wants the default |
1672
- | `lengths` | **stated or measured** ([#804](https://github.com/firejune/rigc/issues/804)). The cumulative length at the end of each curve, and exactly **`vertexCount / 3`** entries on an open path and a closed one alike โ€” the parser's own allocation, which on an open path is one more than its curves: the last entry is the wrap-around curve's cumulative, which the editor writes and nothing reads. **Stated**, it is emitted as stated (float32, like every number in the file) โ€” which is what `ingest` writes from an export, and the only way to carry the editor's number, because the editor measures the pose the first update gives the path constraint, *constraints applied*, and rigc does not pose. A stated array of the wrong count is refused with both counts, `"lengths" has N entry(ies) where the parser sizes M`, and a non-finite entry or one below its predecessor by index. **Left out**, rigc measures it off the geometry: weighted vertices through their bones' setup world transforms, scale, shear and `inherit` included, on the **unconstrained** setup pose โ€” so a path whose bones a constraint moves at rest gets a different figure from the editor's. Only `constantSpeed: false` reads the field at all. What the numbers are โ€” and why *arc length* is the wrong name for them โ€” is ยง10.6 |
1590
+ | `lengths` | **stated or measured**. The cumulative length at the end of each curve, and exactly **`vertexCount / 3`** entries on an open path and a closed one alike โ€” the parser's own allocation, which on an open path is one more than its curves: the last entry is the wrap-around curve's cumulative, which the editor writes and nothing reads. **Stated**, it is emitted as stated (float32, like every number in the file) โ€” which is what `ingest` writes from an export, and the only way to carry the editor's number, because the editor measures the pose the first update gives the path constraint, *constraints applied*, and rigc does not pose. A stated array of the wrong count is refused with both counts, `"lengths" has N entry(ies) where the parser sizes M`, and a non-finite entry or one below its predecessor by index. **Left out**, rigc measures it off the geometry: weighted vertices through their bones' setup world transforms, scale, shear and `inherit` included, on the **unconstrained** setup pose โ€” so a path whose bones a constraint moves at rest gets a different figure from the editor's. Only `constantSpeed: false` reads the field at all. What the numbers are โ€” and why *arc length* is the wrong name for them โ€” is ยง10.6 |
1673
1591
 
1674
1592
  ๐Ÿšจ **`vertexCount` counts knots AND handles, and it has to be a multiple of 3.**
1675
1593
  The parser hands `vertexCount << 1` to `readVertices` and then walks the result in
@@ -1710,13 +1628,12 @@ mesh counts against that budget, a rig that declares none has a budget of
1710
1628
  beside it: without it `A13_MESH_BUDGET` has nothing to measure against and SKIPs,
1711
1629
  and the `MESH` report line has no budget to print. Authored geometry is exempt in
1712
1630
  the other direction and for the same reason โ€” rigc did not draw it, so leaving it
1713
- unmeasured is the author's call (issue #274).
1631
+ unmeasured is the author's call.
1714
1632
 
1715
1633
  โญ **The no-manifest path centres the part window on its own slot bone.** There is
1716
1634
  no crop to flip against, so `size` (or, for a contour, the PNG's own size) is
1717
1635
  placed with its centre on the bone the slot names โ€” which is also exactly where a
1718
- plain region attachment with no `x`/`y` would have drawn it. Measured on a ring, a
1719
- ribbon and a moved bone by `CT05` in the selftest (issue #1).
1636
+ plain region attachment with no `x`/`y` would have drawn it.
1720
1637
 
1721
1638
  #### `ring` โ€” an aperture that opens inside a pinned seam
1722
1639
 
@@ -1770,13 +1687,9 @@ the centre line all 8 take more weight from the grip on their own side. Splittin
1770
1687
  **moves** authority rather than adding it: every vertex gives its controls the
1771
1688
  same total with one grip, two or three.
1772
1689
 
1773
- โš ๏ธ **This route used to bind only the first name**
1774
- ([#684](https://github.com/firejune/rigc/issues/684)). It passed no angles at all,
1775
- so the split collapsed onto `controls[0]` and the rest of the names reached the
1776
- `MESH` line, the bone list and nothing else โ€” and the gate was green, because a
1777
- bone no vertex binds is in no weight, no sum and no index. `A20_MESH_WEIGHTS_COHERENT`
1778
- now names it, so a ring that declares a grip and does not use it is a failure
1779
- rather than a quiet stiffness.
1690
+ โš ๏ธ **A grip no vertex binds is a failure, not a quiet stiffness.** A bone no
1691
+ vertex binds is in no weight, no sum and no index, so nothing else would notice
1692
+ it; `A20_MESH_WEIGHTS_COHERENT` names it.
1780
1693
 
1781
1694
  **Stated limits, each a named refusal rather than a mesh that loads wrong:**
1782
1695
 
@@ -1853,12 +1766,12 @@ detail of the attachment.
1853
1766
  โญ **Both keys are the fragment.** Drop the two of them into ยง1.1's minimal rig in
1854
1767
  place of its `skins`, rename its `box` bone and slot to `cape`, put a
1855
1768
  transparent-margined `cape.png` in `images/`, and it compiles โ€” `invariants`
1856
- included, because without it the generator is refused (issue #274). The
1769
+ included, because without it the generator is refused. The
1857
1770
  `meshTriangles` figure is invented; pick the one your renderer can afford.
1858
1771
 
1859
1772
  | Field | Meaning |
1860
1773
  | --- | --- |
1861
- | `tolerance` | **required.** Douglas-Peucker tolerance, in the drawing's pixels โ€” on a packed page that declares a `scale:`, applied as `tolerance ร— scale` of its texels (ยง0.2, [#779](https://github.com/firejune/rigc/issues/779)). Bigger spends fewer vertices and cuts more corners |
1774
+ | `tolerance` | **required.** Douglas-Peucker tolerance, in the drawing's pixels โ€” on a packed page that declares a `scale:`, applied as `tolerance ร— scale` of its texels (ยง0.2). Bigger spends fewer vertices and cuts more corners |
1862
1775
  | `margin` | how far the outline is pushed out past the traced silhouette, in the drawing's pixels (`margin ร— scale` texels on a `scale:` page). Default `1` |
1863
1776
  | `maxVertices` | refuse rather than emit more outline vertices than this โ€” judged on the outline the two distances above ask for, so a finer `scale:` page of the same art needs no bigger budget. Default `64` |
1864
1777
  | `alpha` | the alpha at or above which a pixel counts as art, `1`..`255`. Default `1` โ€” any pixel that is not fully transparent |
@@ -1880,9 +1793,7 @@ against the mask it came from**. `build` and `explain` print what it measured:
1880
1793
  ```
1881
1794
 
1882
1795
  The budget in that line is the rig's `invariants.meshTriangles` โ€” `(no budget
1883
- declared)` when it declares none, which is the same distinction `A13` SKIPs on. It
1884
- used to be the literal `80` whatever the rig said, so the line an author reads and
1885
- the assertion that measures could print two different numbers (issue #275). A part
1796
+ declared)` when it declares none, which is the same distinction `A13` SKIPs on. A part
1886
1797
  whose outline encloses a hole says so too, because nothing else in the output
1887
1798
  moves when one appears:
1888
1799
 
@@ -1916,8 +1827,7 @@ That last one is the guarantee: **the emitted triangles cover at least 99.5% of
1916
1827
  the art**, measured by rasterising them back over the mask, and a build that would
1917
1828
  clip the art is refused rather than shipped. It is not 100% because a
1918
1829
  simplification that could never cut a corner would not be one โ€” the figure a
1919
- given part actually measures is in the `build`/`explain` line above, and the
1920
- selftest's fixture measures 100.000%.
1830
+ given part actually measures is in the `build`/`explain` line above.
1921
1831
 
1922
1832
  Self-intersection is refused; **holes are not cut out**; and nothing here does
1923
1833
  interior/Steiner points, so a contour mesh bends only where its outline has
@@ -1947,11 +1857,8 @@ attachment's own `image`, and every vertex is pinned to the slot bone at weight
1947
1857
 
1948
1858
  โญ **Positions, because [FACE ยง4.1](FACE.md) places columns where the drawing
1949
1859
  needs them.** The five above are `gallery/portrait`'s own โ€” dense at the
1950
- silhouette, sparse across the middle, and not reaching the window edge. A
1951
- generator that could only divide evenly would be a step backwards from the table
1952
- it replaces, and it does replace it: the selftest builds that exact mesh from
1953
- those five numbers and requires it to come out identical to the 25 vertex pairs
1954
- and 32 triangles the example shipped by hand.
1860
+ silhouette, sparse across the middle, and not reaching the window edge โ€” a
1861
+ layout an even division cannot state.
1955
1862
 
1956
1863
  ๐Ÿšจ **The reason to generate this at all is the hull.** Spine's `hull` is a
1957
1864
  **count** โ€” the first `hull` entries of the vertex list are the outline โ€” so the
@@ -1993,9 +1900,9 @@ off a greyscale sheet in the part's own pixel grid:
1993
1900
 
1994
1901
  | Field | Meaning |
1995
1902
  | --- | --- |
1996
- | `image` | **required.** The sheet, relative to the rig's `images` directory, and the **same pixel size as this attachment's `image`**. It is not packed into the atlas โ€” it is a measurement rigc reads at compile time, not art anything draws. Under `--atlas-in` that is still the drawing's size, on a page that declares a `scale:` too: the sheet is read at each vertex's texel position over the scale the atlas states, so one sheet serves the loose parts and every pack of them ([#762](https://github.com/firejune/rigc/issues/762)) |
1903
+ | `image` | **required.** The sheet, relative to the rig's `images` directory, and the **same pixel size as this attachment's `image`**. It is not packed into the atlas โ€” it is a measurement rigc reads at compile time, not art anything draws. Under `--atlas-in` that is still the drawing's size, on a page that declares a `scale:` too: the sheet is read at each vertex's texel position over the scale the atlas states, so one sheet serves the loose parts and every pack of them |
1997
1904
  | `near` | **required.** `"white"` or `"black"` โ€” which end of the range is closest to the viewer. Stated rather than defaulted: both conventions are in use, and a sheet read with the wrong one turns the part inside out with every gate still green |
1998
- | `zScale` | **required.** How many world units the map's full range spans, in the attachment's own units โ€” the number `radius` used to carry. 8 bits of level say nothing about scale, so this is authored, never measured |
1905
+ | `zScale` | **required.** How many world units the map's full range spans, in the attachment's own units โ€” the number `radius` carries on a cylinder. 8 bits of level say nothing about scale, so this is authored, never measured |
1999
1906
  | `gamma`, `contrast`, `bias` | the tone curve applied to the nearness, defaults `1` / `1` / `0`. State them when a consumer's own renderer curves the same sheet, so the mesh and that renderer describe one surface |
2000
1907
 
2001
1908
  The order is fixed and it matters, because every step is a place two
@@ -2024,9 +1931,7 @@ The loop this replaces is *pick an angle, build, read the refusal, guess again*.
2024
1931
  #### Is the ceiling describing the form, or the sheet's grain?
2025
1932
 
2026
1933
  The lines under the ceiling answer that, and they are **reports only** โ€” nothing
2027
- in them refuses a build or moves a ceiling
2028
- ([#412](https://github.com/firejune/rigc/issues/412),
2029
- [#448](https://github.com/firejune/rigc/issues/448)).
1934
+ in them refuses a build or moves a ceiling.
2030
1935
 
2031
1936
  The ceiling is the **minimum** of the per-triangle fold angles, and a minimum
2032
1937
  cannot say whether it is the floor of a band or one bad pixel. Measured: a clean
@@ -2110,8 +2015,7 @@ off the screen before it gets there** โ€” fade the slot to alpha 0 (`rgba`, ยง4.
2110
2015
  swap the attachment away, as the far cheek or ear does while the head turns. That
2111
2016
  third one is measured rather than declared: a deform key whose slot draws no
2112
2017
  pixels at that key's own time is passed over by name, with the reason on the
2113
- stats line and in the `DEFORM` block
2114
- ([#401](https://github.com/firejune/rigc/issues/401)). **Alpha exactly 0** โ€” a
2018
+ stats line and in the `DEFORM` block. **Alpha exactly 0** โ€” a
2115
2019
  part faded halfway is still refused, and the alpha is in the message.
2116
2020
 
2117
2021
  ๐Ÿšจ **Land the alpha-0 key *before* the folding key, not on it.** The gate measures
@@ -2141,9 +2045,8 @@ u** โ€” and it reaches zero at `tan t = Aโ‚€/A_yaw`. Three consequences:
2141
2045
 
2142
2046
  โ›” It is a **report and never a refusal**. `A39` owns the refusal, from the
2143
2047
  artifact and through the runtime; a second wall here would be the compiler
2144
- inventing a policy out of a measurement. What holds the two together is a
2145
- control: `TC01` requires this number to be the angle `A39` actually fires at, on
2146
- the triangle `A39` actually names, to 0.01ยฐ.
2048
+ inventing a policy out of a measurement. The two agree: this number is the angle
2049
+ `A39` fires at, on the triangle `A39` names, to 0.01ยฐ.
2147
2050
 
2148
2051
  ๐Ÿšจ **The one that will catch you: the sheet has to cover the mesh, and usually
2149
2052
  it does not.** A contour mesh puts every vertex *on* the silhouette and pushes it
@@ -2170,15 +2073,13 @@ in it, which is what monocular depth estimation produces โ€” covers its whole gr
2170
2073
  by construction, so the coverage refusal has nothing to hold it to and skips.
2171
2074
  That is right: a full-frame sheet is a legitimate statement and rigc has no
2172
2075
  authority to guess an input away. But the defect the refusal exists to catch is
2173
- still reachable in that encoding, so the report counts it instead
2174
- ([#449](https://github.com/firejune/rigc/issues/449)):
2076
+ still reachable in that encoding, so the report counts it instead:
2175
2077
 
2176
2078
  ```
2177
2079
  80 of 189 vertices sample a texel the part image does not draw โ€” their z is the sheet's reading of somewhere the part is not
2178
2080
  ```
2179
2081
 
2180
- โš ๏ธ **The reported range is NOT where this shows up**, and this guide said it was
2181
- until it was measured. A background level is a legitimate depth value, so a map
2082
+ โš ๏ธ **The reported range is NOT where this shows up.** A background level is a legitimate depth value, so a map
2182
2083
  half of which is background reports exactly as full a range as one that is all
2183
2084
  subject: on the measured build, `z=[0, 223.97]` of a stated 224 โ€” healthy โ€” with
2184
2085
  54 % of the mesh reading background. "Covers its whole grid" is true and about
@@ -2227,21 +2128,18 @@ hanging sleeve.
2227
2128
  | Field | Meaning |
2228
2129
  | --- | --- |
2229
2130
  | `bone` | **required.** The bone the region is carried by. It has to already exist โ€” a bone a physics constraint targets is part of the skeleton, not a side effect of a mesh |
2230
- | `mask` | **required.** A greyscale sheet in the part's own pixel grid: the level IS the weight, black still and white fully carried, sampled at each vertex. Alpha is not read โ€” a transparent pixel is black. The grid is the drawing's, as for a depth map: on a `scale:` page the mask is still the art's size ([#762](https://github.com/firejune/rigc/issues/762)) |
2131
+ | `mask` | **required.** A greyscale sheet in the part's own pixel grid: the level IS the weight, black still and white fully carried, sampled at each vertex. Alpha is not read โ€” a transparent pixel is black. The grid is the drawing's, as for a depth map: on a `scale:` page the mask is still the art's size |
2231
2132
 
2232
2133
  The remainder always stays on the slot bone, so every vertex closes at 1 by
2233
2134
  construction rather than by `A20` catching it later. `build` and `explain` report
2234
2135
  the mask's digest, how many vertices were carried outright and how many landed in
2235
2136
  the painted falloff.
2236
2137
 
2237
- ๐Ÿšจ **Why a painted mask and not a depth threshold.** rigc tried the threshold โ€”
2238
- "the near part wobbles" โ€” for exactly one day. It is wrong, and instructively so:
2239
- **softness and prominence are different properties of a drawing.** The most
2240
- prominent thing on a face is the nose, and a nose does not wobble. A threshold
2241
- produced a region that was plausible, gated green and carried the wrong pixels.
2242
- It also claimed something untrue โ€” "no mask painted" โ€” while the renderer this
2243
- was modelled on had a hand-painted spring mask all along. rigc does not get to
2244
- delete an input by guessing it.
2138
+ ๐Ÿšจ **Why a painted mask and not a depth threshold.** A threshold โ€” "the near part
2139
+ wobbles" โ€” is wrong, and instructively so: **softness and prominence are different
2140
+ properties of a drawing.** The most prominent thing on a face is the nose, and a
2141
+ nose does not wobble. A threshold produces a region that is plausible, gates green
2142
+ and carries the wrong pixels. rigc does not get to delete an input by guessing it.
2245
2143
 
2246
2144
  โญ The falloff is painted for the same reason. A `feather` parameter would be
2247
2145
  rigc guessing the shape of something you can simply draw.
@@ -2261,13 +2159,10 @@ bone, must close at 1, and at least one must actually be carried.
2261
2159
  | a mask that is not on disk | `the soft mask "x.png" is not at โ€ฆ` |
2262
2160
 
2263
2161
  โญ **One depth pass buys both, on one part.** A carried mesh has two bones on the
2264
- vertices in the mask's falloff, and a `transform` key (ยง4.12) used to be refused
2265
- on any attachment that did โ€” so the **angle** a raised surface turns through and
2266
- the **impact** a soft one answers had to sit on separate slots. Since
2267
- [#389](https://github.com/firejune/rigc/issues/389) they do not: the model is
2268
- evaluated at each vertex's setup world position and pushed into every influence
2269
- through that bone's own inverse. `SF03` is the case that measures the
2270
- combination. โš ๏ธ Read ยง4.11.1 before you add a mask to a part that already turns:
2162
+ vertices in the mask's falloff, and a `transform` key (ยง4.12) works on it: the
2163
+ model is evaluated at each vertex's setup world position and pushed into every
2164
+ influence through that bone's own inverse, so the **angle** a raised surface turns
2165
+ through and the **impact** a soft one answers sit on one slot. โš ๏ธ Read ยง4.11.1 before you add a mask to a part that already turns:
2271
2166
  past one bone the model's own coordinates are **world** ones, so `radius` and
2272
2167
  `about` change units.
2273
2168
  #### 3.4.1 A skin that switches bones and constraints on
@@ -2374,13 +2269,11 @@ defaults to it, so an image named after it needs no `path`:
2374
2269
  emits `"name": "patch-base"` first and no `path`, and the runtime draws region
2375
2270
  `patch-base` for an attachment it calls `patch-base`, filed under `patch`.
2376
2271
 
2377
- ๐Ÿ”ฌ **Until [#796](https://github.com/firejune/rigc/issues/796) rigc composed a name
2378
- here, and the reason it did was measured wrong.**
2379
- [#541](https://github.com/firejune/rigc/issues/541) read the editor's refusal of a
2380
- four-skin rig โ€” `Multiple attachments have the same name: patch patch` โ€” as "one
2381
- name over several attachments, because a linked mesh resolves its parent by name",
2382
- and [#552](https://github.com/firejune/rigc/issues/552) wrote
2383
- `"<skin>/<placeholder>"` on every contested entry. Both halves fail a measurement:
2272
+ ๐Ÿ”ฌ **rigc composes no name here, and nothing needs one.** The editor's refusal of a
2273
+ four-skin rig โ€” `Multiple attachments have the same name: patch patch` โ€” reads like
2274
+ "one name over several attachments, because a linked mesh resolves its parent by
2275
+ name", and a composed `"<skin>/<placeholder>"` on every contested entry like the
2276
+ repair. Both halves fail a measurement:
2384
2277
 
2385
2278
  - **A linked mesh finds its source by skin, slot and KEY.** Through spine-core
2386
2279
  4.3.13, two skins each fill `C` with a different mesh and no `name`, both load
@@ -2393,18 +2286,17 @@ and [#552](https://github.com/firejune/rigc/issues/552) wrote
2393
2286
  through Spine 4.3.26 with every composed name stripped. It is the file that editor
2394
2287
  exported.
2395
2288
 
2396
- What #541 had bisected was a rig whose **default** skin filled the contested
2397
- placeholder beside the named ones โ€” its smallest refusing variant was `default` plus
2398
- one named skin โ€” and that shape is refused on measurements of its own, below. The
2399
- composition renamed every contested attachment of every multi-skin export at
2400
- runtime, which `slot.attachment.name` shows a consumer and no gate could see; `diff`
2401
- now reads it as `attachments.runtime_name`.
2289
+ What the editor does refuse is a rig whose **default** skin fills the contested
2290
+ placeholder beside the named ones โ€” the smallest refusing variant is `default` plus
2291
+ one named skin โ€” and that shape is refused on measurements of its own, below. A
2292
+ composed name would rename every contested attachment at runtime, which
2293
+ `slot.attachment.name` shows a consumer; `diff` reads it as
2294
+ `attachments.runtime_name`.
2402
2295
 
2403
2296
  ๐Ÿšจ **Every skin that shares a placeholder has to be a named one โ€” the default
2404
2297
  skin may not be among them, and rigc refuses the rig if it is.** That is not a
2405
2298
  style rule; it is the editor's model, and two round trips through Spine
2406
- **4.3.26** established it by ruling out both of the only two spellings there are
2407
- ([#567](https://github.com/firejune/rigc/issues/567)):
2299
+ **4.3.26** established it by ruling out both of the only two spellings there are:
2408
2300
 
2409
2301
  - **Give the default skin's entry a name of its own** (`"name": "default/patch"`)
2410
2302
  and the import succeeds โ€” then the export comes back with that name as the
@@ -2444,28 +2336,16 @@ What to know about it, and nothing to author:
2444
2336
  `const regionImage = region && this.regionImages.get(region.name)`, where
2445
2337
  `region` came off the attachment โ€” which `AtlasAttachmentLoader` resolved
2446
2338
  through `path`. Every published version of that renderer keys the same way.
2447
- โš ๏ธ `A08` used to carry a `--profile spine-html` clause requiring a
2448
- placeholder to be spelled exactly like the region it resolves to, which made
2449
- this shape and a green `spine-html` **mutually exclusive** from
2450
- [#567](https://github.com/firejune/rigc/issues/567) onwards; the first
2451
- production rig with named skins hit it three times. That clause is retired โ€”
2452
- restated as the join the renderer actually performs it was a tautology over
2453
- the resolve check beside it
2454
- ([#574](https://github.com/firejune/rigc/issues/574)).
2455
2339
  - **A stated `name` with no `path` resolves the region the name spells.** `path`
2456
2340
  defaults to the attachment's **name**, not to its placeholder, so an entry naming
2457
2341
  `patch-base` and drawing `patch_a.png` needs a `path` โ€” and gets one, because the
2458
2342
  image basename differs from the name. `A08_REGION_NAMES_MATCH_ATTACHMENTS` names
2459
- the skin, the slot, the placeholder and the path if the region is not there
2460
- ([#589](https://github.com/firejune/rigc/issues/589)).
2343
+ the skin, the slot, the placeholder and the path if the region is not there.
2461
2344
  - **Each skin's art is measured and atlased on its own.** The example above points
2462
2345
  the two skins at two PNGs, so there are two regions, each attachment's `path`
2463
2346
  resolving to the file that attachment named and its `width`/`height` measured off
2464
- that file. Until [#555](https://github.com/firejune/rigc/issues/555) only the
2465
- first skin's PNG was ever opened, and the second skin's art reached neither the
2466
- atlas nor the measurement โ€” so name the two files **distinctly**, because the
2467
- region name is the basename and `a/patch.png` beside `b/patch.png` is refused
2468
- (R5).
2347
+ that file. Name the two files **distinctly**: the region name is the basename,
2348
+ and `a/patch.png` beside `b/patch.png` is refused (R5).
2469
2349
 
2470
2350
  โš ๏ธ **Nothing measured says the editor wants names unique within a slot.**
2471
2351
  `spineboy-pro.json`, which the editor wrote, gives the name `head` to a region in
@@ -2475,7 +2355,7 @@ slot across its named skins. The one refusal the editor was measured making is t
2475
2355
  default-skin share, and that is the rule rigc enforces.
2476
2356
 
2477
2357
  ๐Ÿšจ **Once a rig has named skins, no instrument here can see them until you say
2478
- which one** ([#571](https://github.com/firejune/rigc/issues/571)). `render` and
2358
+ which one.** `render` and
2479
2359
  `check` set no skin unless told to, so every slot resolves through the *default*
2480
2360
  skin alone and the art you just moved into `base`, `zulu` and `mike` draws
2481
2361
  nothing at all. `check` then compares blank against blank and reports a perfect
@@ -2483,8 +2363,7 @@ nothing at all. `check` then compares blank against blank and reports a perfect
2483
2363
  `--skin <name>` to both, once per skin (**ยง9**); `tools/editor_roundtrip.ts`
2484
2364
  loops over every skin the build and the export both declare for the same reason,
2485
2365
  and names a skin only one of them declares as **lost** (or **added**) **by the
2486
- export**, quoting `diff`'s `attachments.skins`, without rendering it on either side
2487
- ([#801](https://github.com/firejune/rigc/issues/801)).
2366
+ export**, quoting `diff`'s `attachments.skins`, without rendering it on either side.
2488
2367
 
2489
2368
  #### 3.4.3 `sequence` โ€” a numbered image series on one attachment
2490
2369
 
@@ -2525,8 +2404,7 @@ The loader's own miss would be `Region not found in atlas: glint_0005 (attachmen
2525
2404
  glint)`, which says neither that the region was a frame nor of which series.
2526
2405
 
2527
2406
  โš ๏ธ **The rest of what is refused**, each a series the parser would load as something
2528
- other than what was written (measured on spine-core 4.3.13,
2529
- [#729](https://github.com/firejune/rigc/issues/729)):
2407
+ other than what was written (measured on spine-core 4.3.13):
2530
2408
 
2531
2409
  - a `setup` at or past `count` โ€” `Sequence.resolveIndex` clamps it to the last frame
2532
2410
  (`setup: 7` on four frames showed frame 4);
@@ -2546,8 +2424,7 @@ frames **may differ in size**: the runtime scales each frame's region into the
2546
2424
  attachment's size (`RegionAttachment.computeUVs`, `width / region.originalWidth`), so a
2547
2425
  stated size is emitted as stated and compared with no frame, on the loose route and
2548
2426
  under `--atlas-in` alike. That is what an editor exports for a series that mixes image
2549
- sizes โ€” the setup frame's size, the other frames as they are
2550
- ([#795](https://github.com/firejune/rigc/issues/795)). Omit them and rigc takes the
2427
+ sizes โ€” the setup frame's size, the other frames as they are. Omit them and rigc takes the
2551
2428
  frames' size โ€” **only when every frame measures the same**; frames of different sizes
2552
2429
  are refused until you state the size, because picking one of them would be the
2553
2430
  compiler choosing a value. โš ๏ธ One region is different: its `width` is its image's size
@@ -2579,11 +2456,7 @@ carrying here:
2579
2456
  finds each from its own group, and a motion spec's `ik` block, `transform`
2580
2457
  block and `path`/`physics`/`slider` tracks each name the kind they mean and
2581
2458
  resolve the same way (ยง4.9, ยง4.12). Two constraints **of one kind** sharing a
2582
- name are refused (ยง5.1), because no timeline could say which was meant. Until
2583
- [#692](https://github.com/firejune/rigc/issues/692) the rig spec kept one
2584
- namespace over the whole array, so a rig the editor exports and the runtime
2585
- plays โ€” an IK chain and the transform constraint that follows it, both carrying
2586
- the chain's name โ€” could not be written down at all.
2459
+ name are refused (ยง5.1), because no timeline could say which was meant.
2587
2460
  - A transform constraint's `properties` names come from a fixed six โ€” `rotate`,
2588
2461
  `x`, `y`, `scaleX`, `scaleY`, `shearY`. rigc refuses anything else by name; in
2589
2462
  raw JSON the parser throws.
@@ -2606,11 +2479,8 @@ carrying here:
2606
2479
  `true` and leaves the rest alone (`Animation.js:2066-2072`). So the flag is one
2607
2480
  constraint's **opt-in to being driven in bulk**, per tuning value, and it does
2608
2481
  nothing on its own. The parser's default for all seven is `false`.
2609
- The motion spec keys that timeline as **`"physics": "*"`** (ยง4.4,
2610
- [#726](https://github.com/firejune/rigc/issues/726)), and rigc writes it under
2611
- the empty name. Until then rigc emitted the flags and could not emit the
2612
- timeline that reads them: every physics track named its constraint and the
2613
- empty name was refused as `keys unknown physics constraint ""`. What rigc does
2482
+ The motion spec keys that timeline as **`"physics": "*"`** (ยง4.4), and rigc
2483
+ writes it under the empty name. What rigc does
2614
2484
  with the flags is **pass them through**, `false` included: measured, a
2615
2485
  constraint stating none emits none, and one stating `"windGlobal": false` emits
2616
2486
  `"windGlobal": false` rather than dropping it the way the motion spec's
@@ -2620,10 +2490,8 @@ carrying here:
2620
2490
  - `"*"` is **reserved** as a physics constraint's name, in the rig spec and in
2621
2491
  ยง4.6's table alike: a track naming it could mean either. `compile` refuses one
2622
2492
  by name.
2623
- - โš ๏ธ `src/rig.ts` called the physics `ScaleYMode` key **`scaleYMode`** until
2624
- issue #545 โ€” the runtime's field name rather than the format's key โ€” and nothing
2625
- read it, so a spec that wrote `scaleYMode` set no mode and said nothing. A rig
2626
- that still writes it is now refused by name, with `scaleY` beside it.
2493
+ - โš ๏ธ **`scaleYMode`** โ€” the runtime's field name rather than the format's key โ€”
2494
+ is refused by name, with `scaleY` beside it.
2627
2495
 
2628
2496
  Every constraint may also carry `skin: true`, which makes it run only under the skin
2629
2497
  that lists it โ€” see ยง3.4.1, and note that the flag alone does nothing.
@@ -2633,7 +2501,7 @@ that is deliberate. Every key on a constraint object is a Spine field the emitte
2633
2501
  writes; a statement to the gate about who turns the dial is what the artifact cannot
2634
2502
  say about itself, so it lives in `invariants.consumerDrivenMix` (ยง3.7), beside
2635
2503
  `deformMayFold` โ€” the one other exemption from a named rule, which names its subject
2636
- the same way ([#784](https://github.com/firejune/rigc/issues/784)).
2504
+ the same way.
2637
2505
 
2638
2506
  #### 3.5.1 `path` โ€” bones that travel along a curve
2639
2507
 
@@ -2714,7 +2582,7 @@ dial, the gate swings.
2714
2582
  timeline left it: a negative time is not clamped to the first frame, it is a time
2715
2583
  before the animation starts, where `Animation.apply` leaves the pose it found
2716
2584
  untouched. That is the same picture as the first frame only when the first frame
2717
- *is* the rest pose. [measured] `PS140` in `selftest.ts`.
2585
+ *is* the rest pose.
2718
2586
 
2719
2587
  | Field | Meaning |
2720
2588
  | --- | --- |
@@ -2739,40 +2607,28 @@ where the message can name both files.
2739
2607
  why R10 exists.** In JSON the reference is a name on both sides. In the format's
2740
2608
  binary half it is an **ordinal** โ€” `constraint.animation = animations[readInt()]`
2741
2609
  (`SkeletonBinary`) โ€” so an editor holding that ordinal writes back whichever
2742
- animation now stands at the position. `gallery/look` went into a licensed editor
2743
- (data version 4.3.26) as `turn, tilt, sweep` with `yaw -> "turn"` and came back
2610
+ animation now stands at the position. `gallery/look` taken into a licensed editor
2611
+ (data version 4.3.26) as `turn, tilt, sweep` with `yaw -> "turn"` comes back
2744
2612
  `sweep, tilt, turn` with **`yaw -> "sweep"`**: a file that parses, gates green and
2745
- applies the wrong animation. โญ Its second slider is what named the mechanism
2746
- rather than a second casualty โ€” `tilt` survived because it sat at index 1 in both
2747
- orderings. rigc now emits animations in the editor's own order so its re-sort
2748
- moves no index ([#535](https://github.com/firejune/rigc/issues/535)); on the same
2749
- rig through the same editor that restored `yaw -> "turn"` and took the
2750
- re-rendered mean absolute error from 10.4655 / 8.4961 / 8.7140 down to
2751
- 0.3035 / 0.0769 / 0.0588. (`look`'s three names are ones a codepoint sort orders
2752
- identically, which is what rigc emitted when that trip was measured.)
2753
-
2754
- โš ๏ธ The editor's comparator is natural and case-insensitive
2755
- ([#539](https://github.com/firejune/rigc/issues/539)) and is now measured in full
2756
- off five stored round trips
2757
- ([#728](https://github.com/firejune/rigc/issues/728)) โ€” so the emit is the
2758
- editor's own order, and only what those files leave open is a compile error.
2759
- **R10** has the rule and the three shapes to avoid.
2760
-
2761
- โœ… **What that repair does not reach is a compile error now, not a hazard.** This
2762
- paragraph used to say that names a codepoint sort and a friendlier one disagree
2763
- about โ€” `Turn` / `turn`, `turn2` / `turn10` โ€” were where *the hazard returns*, and
2764
- that nobody had round-tripped such a pair. Somebody has: `Turn, sweep, wave` came
2765
- back `sweep, Turn, wave` and `turn10, turn2, zoom` came back `turn2, turn10, zoom`
2766
- ([#539](https://github.com/firejune/rigc/issues/539)). โ‡’ rigc no longer leaves
2767
- that to naming discipline โ€” and since
2768
- [#543](https://github.com/firejune/rigc/issues/543) it does better than refusing
2769
- those two, because they are the two sets the editor's answer is **known** for:
2770
- both are emitted in the order it returned. What is still a compile error is the
2771
- set whose order turns on one of the three things the round trips of
2772
- [#728](https://github.com/firejune/rigc/issues/728) leave open, printed with both
2773
- names, which of them decides it, and the rename that settles it. What changed is
2774
- the price of forgetting: a build that stops, rather than a slider that silently
2775
- applies the wrong animation.
2613
+ applies the wrong animation โ€” `tilt` survives only because it sits at index 1 in
2614
+ both orderings. rigc emits animations in the editor's own order so its re-sort
2615
+ moves no index; on the same rig through the same editor that keeps
2616
+ `yaw -> "turn"`, at a re-rendered mean absolute error of 0.3035 / 0.0769 / 0.0588
2617
+ against 10.4655 / 8.4961 / 8.7140 for the order above.
2618
+
2619
+ โš ๏ธ The editor's comparator is natural and case-insensitive, measured off five
2620
+ stored round trips โ€” so the emit is the editor's own order, and only what those
2621
+ round trips leave open is a compile error. **R10** has the rule and the three
2622
+ shapes to avoid.
2623
+
2624
+ โœ… **What that order does not settle is a compile error, not a hazard.** Names a
2625
+ codepoint sort and a friendlier one disagree about โ€” `Turn` / `turn`, `turn2` /
2626
+ `turn10` โ€” are emitted in the order the editor returns for them: measured,
2627
+ `Turn, sweep, wave` comes back `sweep, Turn, wave` and `turn10, turn2, zoom` comes
2628
+ back `turn2, turn10, zoom`. A set whose order turns on one of the three things
2629
+ those round trips leave open is refused, printed with both names, which of them
2630
+ decides it, and the rename that settles it โ€” a build that stops, rather than a
2631
+ slider that silently applies the wrong animation.
2776
2632
 
2777
2633
  โš ๏ธ **The fields of the model you did not choose are refused, not ignored.** The
2778
2634
  parser reads `time` only in the bone-less branch and `property`/`from`/`to`/`scale`/
@@ -2811,8 +2667,8 @@ one wins today. โ›” rigc does **not** set the flag for you. The compiler never
2811
2667
  invents a value that is not in the spec, and a rig whose composition was chosen by
2812
2668
  the tool is one nobody can reason about.
2813
2669
 
2814
- โš ๏ธ **And below full authority it is a weighting rather than a quieter erasure**
2815
- ([#399](https://github.com/firejune/rigc/issues/399)). An additive slider scales
2670
+ โš ๏ธ **And below full authority it is a weighting rather than a quieter erasure.**
2671
+ An additive slider scales
2816
2672
  its whole contribution by its own `mix`, so two of them at any pair of mixes are
2817
2673
  still the sum โ€” which is why `A40` skipping below full authority is right: what
2818
2674
  happens there is a weighting, not the erasure it refuses. A **non-additive**
@@ -2822,8 +2678,7 @@ the pose it found*, so the earlier slider is not erased โ€” it is attenuated by
2822
2678
  part of the way down takes that share of every earlier slider on the target with
2823
2679
  it, on every frame, with the gate green โ€” the second reason to write
2824
2680
  `"additive": true` on **every** slider that shares a target and not only on the
2825
- later one. `PS130` in `selftest.ts` poses both models rather than quoting the
2826
- runtime, and [`docs/FACE.md`](FACE.md) ยง8 is the same rule on a face's two axes.
2681
+ later one. [`docs/FACE.md`](FACE.md) ยง8 is the same rule on a face's two axes.
2827
2682
 
2828
2683
  โš ๏ธ **And `"additive": true` is not always available.** What composes is bone,
2829
2684
  deform, transform-constraint, path `position` and path `mix`, physics
@@ -2831,36 +2686,27 @@ deform, transform-constraint, path `position` and path `mix`, physics
2831
2686
  a slot colour, an attachment swap, a draw order and a sequence, and also an **ik
2832
2687
  constraint's mix**, a path's `spacing`, and every physics timeline except those
2833
2688
  two โ€” so two sliders sharing one of those overwrite each other whatever you
2834
- write. โš ๏ธ The four spelled out here used to read as the whole of the complement
2835
- and they are examples of it; `A40` was never reading a list, which is why it
2836
- refuses the ik case this sentence did not name.
2689
+ write. โš ๏ธ Those are examples of the complement, not the whole of it: `A40` reads
2690
+ no list.
2837
2691
 
2838
- ๐Ÿšจ **That list is not `Timeline.additive`, and this is what it cost to learn.**
2692
+ ๐Ÿšจ **That list is not `Timeline.additive`.**
2839
2693
  The runtime's own flag says a class "supports being applied additively", and on
2840
2694
  two classes it is not what the class does: `PathConstraintMixTimeline` and
2841
2695
  `SliderTimeline` declare themselves non-additive and their `apply` passes the
2842
2696
  `add` argument straight through anyway โ€” every other non-additive timeline either
2843
2697
  hardcodes `false` in the call, zeroes `add` first, or never reads it. So two
2844
2698
  additive sliders keying one path constraint's `mix`, or one slider's `time`,
2845
- **do** compose, as the same sum as everything else above, and `A40` refused both
2846
- by name with a message saying `"additive": true` would not compose them. โœ… **It
2847
- no longer reads the flag: it poses each shared timeline twice with `add` set and
2848
- reads whether the second application accumulated**
2849
- ([#655](https://github.com/firejune/rigc/issues/655)), so the two rigs above pass
2850
- and the message names what the class was measured to do. [measured] `PS143` in
2851
- `selftest.ts` poses all thirty spellings of the motion vocabulary under two
2852
- additive sliders and prints the flag beside the behaviour; `PS145` holds the
2853
- probe's verdict to that same pose on every one of them; `PS140` holds the `time`
2854
- case to a grid.
2855
-
2856
- โญ **The same measurement retired a refusal nothing could have distinguished.**
2857
- Two sliders whose animations both fire **events** were refused as sharing a
2858
- property โ€” and a slider fires no event at all: it applies its animation with
2859
- `firedEvents` null, and `EventTimeline.apply` returns on that. The probe's third
2860
- answer is *this timeline moved no pose at all*, so an events pair, and a physics
2861
- `reset` pair with it, are simply not findings. Nothing else changed: a shared
2862
- slot colour, attachment, draw order, ik mix, path `spacing` or physics property
2863
- is refused exactly as before.
2699
+ **do** compose, as the same sum as everything else above. โœ… **So `A40` does not
2700
+ read the flag: it poses each shared timeline twice with `add` set and reads
2701
+ whether the second application accumulated**, and the message names what the
2702
+ class was measured to do.
2703
+
2704
+ โญ **Two sliders whose animations both fire events are not a finding.** A slider
2705
+ fires no event at all: it applies its animation with `firedEvents` null, and
2706
+ `EventTimeline.apply` returns on that. `A40`'s third answer is *this timeline
2707
+ moved no pose at all*, so an events pair, and a physics `reset` pair with it, are
2708
+ passed. A shared slot colour, attachment, draw order, ik mix, path `spacing` or
2709
+ physics property is refused.
2864
2710
 
2865
2711
  โ‡’ **And the `constraints` array decides twice, for two different reasons.**
2866
2712
  *Overwriting* has a direction: the slider **later in the array** puts its own
@@ -2874,16 +2720,11 @@ earlier ik constraint, the `position` of an earlier path constraint or the `wind
2874
2720
  of an earlier physics constraint writes a value nothing reads again, and what
2875
2721
  that constraint drives is dead at every position of the dial.
2876
2722
 
2877
- โœ… **That second one is a refusal by name since
2878
- [#658](https://github.com/firejune/rigc/issues/658), and it covers every
2879
- constraint kind since [#665](https://github.com/firejune/rigc/issues/665).**
2723
+ โœ… **That second one is a refusal by name, for every constraint kind.**
2880
2724
  `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` names the driving slider, the
2881
2725
  driven constraint and its kind, the property, both `constraints` indices, the
2882
2726
  runtime class whose `update` does the reading and the animation the key sits in,
2883
- and its repair is the reorder. It is a rule of its own rather than a clause of
2884
- `A40` because `A40`'s population is the sliders whose `mix` nothing keys โ€” the
2885
- exclusion *is* the shape of the hole โ€” and not a clause of `A37`, which asks
2886
- whether some animation keys the `mix` above 0 and never which one.
2727
+ and its repair is the reorder.
2887
2728
  โญ **The runtime repairs this for bones and not for constraints**, which is why an
2888
2729
  author cannot reason it out from the bone case: `Slider.sort` clears `sorted` on
2889
2730
  every bone its animation keys so those bones re-sort *after* the slider, while
@@ -2904,14 +2745,10 @@ round. ๐Ÿ”ธ **One spelling is outside the rule and a reorder does not repair it*
2904
2745
  a `physics` `reset` key. It writes no pose and fires only when a frame time is
2905
2746
  crossed, and a slider applies its animation at a single instant โ€” so the key
2906
2747
  never fires in either order, and the SKIP says so rather than advising a move
2907
- that would change nothing. [measured] `PS139`, `PS140`, `PS156`, `PS157`,
2908
- `PS158`, `PS159` and `PS160` in `selftest.ts`.
2748
+ that would change nothing.
2909
2749
 
2910
2750
  Swap the two array entries and the answer swaps with them โ€” it is the
2911
- array that decides, not the flags and not which animation the file names first
2912
- (`PS135` in `selftest.ts` poses four such targets both ways; `PS136` poses the
2913
- three that do compose, and they are the same sum ยง3.5.2 states, over each target's
2914
- own setup value). A40 names this case separately, because the fix is different:
2751
+ array that decides, not the flags and not which animation the file names first. A40 names this case separately, because the fix is different:
2915
2752
  key such a property from one slider only, or move both edits into the single
2916
2753
  animation one slider applies.
2917
2754
 
@@ -2920,8 +2757,7 @@ animation one slider applies.
2920
2757
  **A `property` under `local: false` is read through the world transform, and four
2921
2758
  of those readings are bounded.** A range that names values the reader cannot
2922
2759
  return is dead there: the dial moves, the reading does not follow, and nothing at
2923
- runtime says so. [measured] against `spine-core` 4.3.13, one reader at a time โ€”
2924
- `bench/studies/2026-09-06-readers`:
2760
+ runtime says so. [measured] against `spine-core` 4.3.13, one reader at a time:
2925
2761
 
2926
2762
  | `property` | `"local"` | Reads | Producible floor | Producible ceiling |
2927
2763
  | --- | --- | --- | --- | --- |
@@ -2952,10 +2788,9 @@ runtime says so. [measured] against `spine-core` 4.3.13, one reader at a time
2952
2788
  ๐Ÿšจ One that dips below it is not *dead*, it **folds**: โˆ’2 and +2 read the same
2953
2789
  number, select the same frame and pose the same face, so the axis doubles back
2954
2790
  on itself about the point it should have passed through. Such a range is
2955
- **refused at compile** (ยง3.5.2.2, beside the `rotate` circle). [measured]
2956
- `PS138` sweeps both halves and poses them; `PS151` poses the fold on a dial the
2957
- refusal leaves standing โ€” a legal `0`..`4` window turned below 0 by a consumer
2958
- applies the same time at โˆ’2 and +2 to the bit, while the same mapping read
2791
+ **refused at compile** (ยง3.5.2.2, beside the `rotate` circle). [measured] a
2792
+ legal `0`..`4` window turned below 0 by a consumer applies the same time at โˆ’2
2793
+ and +2 to the bit, while the same mapping read
2959
2794
  `local: true` is on its own signed mapping at every cell.
2960
2795
  - **`shearY` under `local: false` wraps like `rotate` does, and worse.** It is a
2961
2796
  difference of two `atan2` calls, so at any one bone orientation the readable
@@ -2965,8 +2800,8 @@ runtime says so. [measured] against `spine-core` 4.3.13, one reader at a time
2965
2800
  pointing. Prefer `local: true` for a shear axis.
2966
2801
  โš ๏ธ **Nothing refuses a `shearY` range, and that is deliberate.** The reader
2967
2802
  keeps its sign โ€” it does not fold the way the two `scale` readers do โ€” and what
2968
- it does instead is not a fact a rig spec holds: [measured] `PS155` sweeps three
2969
- orientations across their own seams and each wraps by exactly one turn at
2803
+ it does instead is not a fact a rig spec holds: [measured] three orientations
2804
+ swept across their own seams each wrap by exactly one turn at
2970
2805
  `90ยฐ โˆ’ ฮธx` (90ยฐ, 45ยฐ and 150ยฐ for a dial bone at 0ยฐ, 45ยฐ and โˆ’60ยฐ), which any
2971
2806
  animation a consumer writes can move. A range rule here would have to name a
2972
2807
  seam the compiler cannot know, and would pass the case that actually breaks.
@@ -3026,8 +2861,7 @@ examples above each sit within one turn, where a single ยฑ360 gives the same
3026
2861
  answer; past that only the modulo does. [measured] through spine-core, a bone
3027
2862
  parked at **โˆ’500ยฐ** drives the slider to **3.600000 s**, which is exactly where a
3028
2863
  bone parked at **220ยฐ** drives it โ€” so the reading is 220ยฐ, not โˆ’140ยฐ, and a
3029
- refusal naming โˆ’140ยฐ would be naming a value that reader cannot return at all
3030
- (issue [#431](https://github.com/firejune/rigc/issues/431)). The same on the other
2864
+ refusal naming โˆ’140ยฐ would be naming a value that reader cannot return at all. The same on the other
3031
2865
  side: **900ยฐ** drives it to **0.200000 s**, the time a bone at **180ยฐ** selects.
3032
2866
 
3033
2867
  ๐Ÿ“ **The consequence in that message is computed, not described.** Both refusals
@@ -3062,10 +2896,7 @@ lies outside `[0, 360]` โ€” and not the reach from the boundary to the far end.
3062
2896
  two are the same number for a range that *straddles* a boundary, as `300ยฐ..500ยฐ`
3063
2897
  does. A range lying **wholly** outside is told its own width instead: `400ยฐ..500ยฐ`
3064
2898
  reads *"the 100.000ยฐ of the range past 360ยฐ"*, and `-500ยฐ..-300ยฐ` *"the 200.000ยฐ
3065
- of the range below 0ยฐ"* (issue
3066
- [#434](https://github.com/firejune/rigc/issues/434) โ€” both used to print the
3067
- reach, which on the first of those was 140.000ยฐ, wider than the 100ยฐ-wide range
3068
- it was describing).
2899
+ of the range below 0ยฐ"*.
3069
2900
 
3070
2901
  โญ **A range ending exactly on 360ยฐ is legal**, and that is the whole turn: a
3071
2902
  wheel, a turntable, a head that goes all the way round, written `from: 0` with a
@@ -3109,8 +2940,7 @@ the same posed face. The refusal names the arc that is written twice:
3109
2940
  - **A range lying wholly below 0 gets its own sentence** โ€” *"the whole 4.000 of
3110
2941
  this range is below 0, so it reaches none of the animation's 1s"* โ€” and that
3111
2942
  figure is the width **under** the floor, not the reach to the far end, the same
3112
- distinction [#434](https://github.com/firejune/rigc/issues/434) drew for the
3113
- circle.
2943
+ distinction the circle's message draws.
3114
2944
  - **`loop: true` changes nothing here**, unlike the circle: a fold is not a
3115
2945
  clamp, so there is no loop branch in the message. [measured] the same ยฑ1.500
3116
2946
  pair applies 1.875000 s either way.
@@ -3118,14 +2948,14 @@ the same posed face. The refusal names the arc that is written twice:
3118
2948
  negative half drives the *first* half of the animation ([measured] โˆ’2 โ†’
3119
2949
  0.000 s, +2 โ†’ 1.000 s), or moving the range so it does not dip below 0.
3120
2950
 
3121
- [measured] `PS151`โ€“`PS154` in `selftest.ts`. `shearY` has no such rule and
2951
+ `shearY` has no such rule and
3122
2952
  ยง3.5.2.1 says why: that reader keeps its sign and wraps at a seam the bone's own
3123
2953
  orientation places, which a rig spec does not hold.
3124
2954
 
3125
- โš ๏ธ **An artifact can still carry a dead range** โ€” one exported from the editor,
3126
- hand-edited, or built by an older rigc. `A39` reports that from the artifact side
3127
- as a key at a time no dial selects (ยง4.11.4); the compile refusal above is what
3128
- stops a rig spec in this repository from producing one.
2955
+ โš ๏ธ **An artifact can still carry a dead range** โ€” one exported from the editor or
2956
+ hand-edited. `A39` reports that from the artifact side as a key at a time no dial
2957
+ selects (ยง4.11.4); the compile refusal above is what stops a rig spec from
2958
+ producing one.
3129
2959
 
3130
2960
  ๐Ÿ“Œ **A slider's animation is measured in the slider's own frame** โ€” `A39` inverts
3131
2961
  the mapping above and drives the bone to the value it names, rather than playing
@@ -3133,8 +2963,7 @@ the animation on a track while the slider sits at its neutral. ยง4.11.4 is what
3133
2963
  that changes and why it matters at `mix: 1`.
3134
2964
 
3135
2965
  ๐Ÿ”ธ **`scale` is emitted as its float32**, like every other number rigc writes, so
3136
- `1/60` ships as `0.016666668` โ€” 8e-8 relative (it was `0.016667`, 2e-5, on the
3137
- six-decimal grid before issue #716). Invisible in the middle of the range; it
2966
+ `1/60` ships as `0.016666668` โ€” 8e-8 relative. Invisible in the middle of the range; it
3138
2967
  shows at the top of it, where a 60ยฐ turn then applies at 1.00000008 s rather than
3139
2968
  1 s. With `loop: false` that is the last frame and harmless, with
3140
2969
  `loop: true` it wraps to the start. When the range comes from a *measured* ceiling
@@ -3205,7 +3034,7 @@ stops before the gate with `N mesh slot(s) emitted but the rig "X" allows 0`.
3205
3034
  Geometry rigc built is geometry rigc will not ship unmeasured; geometry the author
3206
3035
  drew is exempt, because rigc did not draw it. So `A13`'s **SKIP** means *this rig
3207
3036
  is unmeasured*, not *this budget is inert* โ€” those are two code paths with one
3208
- name, and reading the SKIP as the whole story is what issue #274 was.
3037
+ name.
3209
3038
 
3210
3039
  ๐Ÿ”— **`detached` is a list of forbidden parentages**, and its shape is
3211
3040
  `[{ "bone": โ€ฆ, "notUnder": โ€ฆ, "why"?: โ€ฆ }]`. `A25_DETACHED_BONE_PARENTAGE` walks
@@ -3234,20 +3063,16 @@ is the whole value of it. What the field is for is art that folds on purpose: a
3234
3063
  page turning over, a cloth creasing back on itself, where the reversed winding
3235
3064
  *is* the drawing.
3236
3065
 
3237
- ๐Ÿ“Œ **Nothing in this repository uses it.** `gallery/flex` did, for one leaf whose
3238
- `why` named the issue tracking the defect rather than claiming intent; that
3239
- tracked defect is repaired ([#313](https://github.com/firejune/rigc/issues/313))
3240
- and the entry is gone. โ‡’ An exemption whose `why` reads *"known defect, see
3241
- #N"* is a legitimate use of the field and an honest one, but it is a loan
3242
- against a fix, not a fix โ€” and the thing that made it repayable was A39
3243
- measuring the ceiling the art could actually take.
3066
+ ๐Ÿ“Œ **An exemption whose `why` reads *"known defect, see #N"* is a legitimate use
3067
+ of the field and an honest one**, but it is a loan against a fix, not a fix โ€” and
3068
+ what makes it repayable is A39 measuring the ceiling the art can actually take.
3244
3069
 
3245
3070
  ๐ŸŽฌ **`editorRoundTrip` is the one field here that names a CONSUMER rather than a
3246
3071
  shape.** Write `"editorRoundTrip": true` when this rig is authored to come back
3247
3072
  out of the Spine editor โ€” imported, hand-edited, exported โ€” and
3248
3073
  `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` refuses a physics constraint driving a
3249
3074
  component that editor cannot hold. `true` is the only accepted value; a `false`
3250
- would be a key nothing reads ([#545](https://github.com/firejune/rigc/issues/545)).
3075
+ would be a key nothing reads.
3251
3076
 
3252
3077
  โš ๏ธ **Leaving it out is not a weaker gate, and this is the part worth reading.**
3253
3078
  rigc's output is not wrong here: a physics constraint driving `rotate` is valid
@@ -3260,7 +3085,7 @@ constraint and the component:
3260
3085
  SKIP A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP: the rig "look" does not declare `invariants.editorRoundTrip`, so nothing here is gated against the Spine editor. What is here: physics "whip" drives rotate, and the editor's physics model holds x and y only, so a round trip returns that constraint driving nothing at all (issue #540)
3261
3086
  ```
3262
3087
 
3263
- ๐Ÿ“ **Measured, not inferred** ([#540](https://github.com/firejune/rigc/issues/540)):
3088
+ ๐Ÿ“ **Measured, not inferred:**
3264
3089
  three rigs, twelve constraints, predictions written before the round trip. A lone
3265
3090
  `y` came back and `x` + `y` together came back โ€” the rule is membership, not arity
3266
3091
  โ€” while a lone `rotate`, a lone `scaleX` and a lone `shearX` each came back driving
@@ -3273,16 +3098,13 @@ loss seen from the far side: it is what fires on the file the editor hands **bac
3273
3098
  draws no pixels at that key's own time is already passed over โ€” `A39` measures
3274
3099
  that and says so (ยง4.11, and the `skipped` line in the `DEFORM` block). Declaring
3275
3100
  the slot instead would turn the check off at every angle where the part is fully
3276
- visible too, which is trading a false positive for a blind spot on the same slot
3277
- ([#401](https://github.com/firejune/rigc/issues/401)). โš ๏ธ And land that alpha-0
3278
- key **before** the folding key rather than on it โ€” the frames in between are
3279
- drawn and are gated (ยง4.11.3); this used to be a rule you had to follow and is
3280
- now one the gate keeps
3281
- ([#403](https://github.com/firejune/rigc/issues/403)).
3101
+ visible too, which is trading a false positive for a blind spot on the same slot.
3102
+ โš ๏ธ And land that alpha-0 key **before** the folding key rather than on it โ€” the
3103
+ frames in between are drawn and are gated (ยง4.11.3).
3282
3104
 
3283
3105
  ๐ŸŽ›๏ธ **`consumerDrivenMix` names the ik and transform constraints whose mix the
3284
- CONSUMER sets** โ€” from code, at runtime โ€” rather than any animation in this file
3285
- ([#784](https://github.com/firejune/rigc/issues/784)). It is
3106
+ CONSUMER sets** โ€” from code, at runtime โ€” rather than any animation in this file.
3107
+ It is
3286
3108
  `[{ "constraint": โ€ฆ, "type": "ik" | "transform", "why": โ€ฆ }]`, and it exempts that
3287
3109
  constraint from `A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT` or
3288
3110
  `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` (ยง4.12). A constraint resting muted
@@ -3310,8 +3132,7 @@ spelling of `gallery/look`'s rule that a face angle is a value rather than a tim
3310
3132
  `spec` must be `"rigc-motion/1"`; `archetype` must equal the rig's `name`; `cut` is
3311
3133
  a label for the shot. Always include an `easings` object โ€” an empty one is fine.
3312
3134
 
3313
- โญ **The file is PARSED, not cast** ([`src/motion.ts`](../src/motion.ts), issue
3314
- #307), so a field of the wrong type is refused before any of it is compiled. Every
3135
+ โญ **The file is PARSED, not cast** ([`src/motion.ts`](../src/motion.ts)), so a field of the wrong type is refused before any of it is compiled. Every
3315
3136
  one of those refusals names the file, the key path, what the value actually is and
3316
3137
  the spelling that works โ€” `path/to/motion.json: \`easings."soft"\` is an array of
3317
3138
  3; a named easing is FOUR finite numbers โ€ฆ`. The split with the compile-time
@@ -3321,7 +3142,7 @@ parser; whether a name resolves against the rig, whether a bone is in that group
3321
3142
  whether a key's value has the right number of channels for its property needs
3322
3143
  something this file does not contain, and stays where it can say so.
3323
3144
 
3324
- โš ๏ธ **An unknown key is still ignored**, exactly as it is in a rig spec โ€” a
3145
+ โš ๏ธ **An unknown key is ignored**, exactly as it is in a rig spec โ€” a
3325
3146
  misspelled `"easing"` for `"ease"` plays linear and says nothing. The two formats
3326
3147
  are deliberately consistent here rather than each surprising in its own way.
3327
3148
 
@@ -3332,11 +3153,9 @@ shows. rigc converts them per key into the absolute `(time, value)` control poin
3332
3153
  the JSON actually holds. Writing normalised handles into a raw `curve` instead loads
3333
3154
  without error and plays a different curve.
3334
3155
 
3335
- **Four finite numbers, refused by name if they are not** (#307). `bezierForChannel`
3336
- destructures four handles with no guard of its own, so `[0.42, 0, 0.58]` used to
3337
- emit `"curve": [0.42, 0, 0.58, null]` โ€” a curve with a hole in it, which loads,
3338
- plays, and is not the shape the spec named. A non-numeric handle was the same
3339
- silence one character further in.
3156
+ **Four finite numbers, refused by name if they are not.** Unchecked,
3157
+ `[0.42, 0, 0.58]` would emit `"curve": [0.42, 0, 0.58, null]` โ€” a curve with a
3158
+ hole in it, which loads, plays, and is not the shape the spec named.
3340
3159
 
3341
3160
  ### 4.2 `setup` โ€” the setup pose, per slot
3342
3161
 
@@ -3344,12 +3163,9 @@ silence one character further in.
3344
3163
  channels in 0..1. Both halves are refused by name: an entry that is not that object
3345
3164
  โ€” `"lid_l": null`, or `"lid_l": "plate"` with the attachment name where its wrapper
3346
3165
  belongs โ€” and a colour channel that is not a finite number in 0..1. The second
3347
- spelling is the one worth knowing about, because `.attachment` on a string is
3348
- `undefined`: it used to compile GREEN and **hide the slot**, which is the opposite
3349
- of what was asked (#293). The guard is in the parser rather than the emit path, so
3350
- it also covers a slot the rig declares without attachments and a slot name that
3351
- matches nothing at all โ€” the two corners where the emit-path version stayed silent
3352
- (#307). Declaring a slot's setup pose here **and** on the rig slot is a
3166
+ spelling is the one worth knowing about: unchecked it would **hide the slot**,
3167
+ which is the opposite of what was asked. The check also covers a slot the rig
3168
+ declares without attachments and a slot name that matches nothing at all. Declaring a slot's setup pose here **and** on the rig slot is a
3353
3169
  compile error (R3). Use whichever file owns the decision: a rig that is purely
3354
3170
  structure puts it on the slot; a cut whose overlay mechanism is a decision about
3355
3171
  time puts it here.
@@ -3359,7 +3175,7 @@ time puts it here.
3359
3175
  | Field | Meaning |
3360
3176
  | --- | --- |
3361
3177
  | `duration` | seconds, declared and checked (R7) |
3362
- | `loop` | a **player hint only** โ€” skeleton JSON has no loop field, so this is not emitted and no assertion or diff measure reads it. **Optional**, and 20 of the motion specs in this repository omit it |
3178
+ | `loop` | a **player hint only** โ€” skeleton JSON has no loop field, so this is not emitted and no assertion or diff measure reads it. **Optional** |
3363
3179
  | `note` | free text |
3364
3180
  | `tracks` | the timelines |
3365
3181
  | `drawOrder` | the draw-order timeline โ€” ยง4.7. Not a track: it names no target |
@@ -3383,8 +3199,7 @@ resolves every member against the rig as that. So a `group` is the one target
3383
3199
  where the property picks the family rather than the other way round (ยง4.4's โญ is
3384
3200
  about the three **constraint** families, which are picked by the field), and a
3385
3201
  property that is in none of the three tables is refused naming the group and all
3386
- three vocabularies rather than being read as any of them
3387
- ([#661](https://github.com/firejune/rigc/issues/661); ยง4.4). A group whose members
3202
+ three vocabularies rather than being read as any of them (ยง4.4). A group whose members
3388
3203
  are not all of one family is not refused as such: the first member that is not
3389
3204
  what the property made it is the one named.
3390
3205
 
@@ -3443,60 +3258,22 @@ multipliers where `1` is setup; rotation is in degrees.
3443
3258
  compile error** โ€” `animation "A" bone "B" has no timeline "P" (it has: translate,
3444
3259
  translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate,
3445
3260
  inherit)`, ยง5.1's row. The eleven are the three rows above read as one list, in the order the
3446
- message prints them, and they are the emitter's own dispatch table (`BONE_TRACKS`
3447
- in `src/compile.ts`): `resolveTargets` asks that table which family a track
3448
- belongs to, `compileValueTrack` writes a key out of the shape it finds there, and
3449
- the refusal prints `Object.keys` of the same object โ€” so what you are told a bone
3450
- accepts is what it accepts.
3451
-
3452
- - Until [#656](https://github.com/firejune/rigc/issues/656) it printed no list at
3453
- all: the refusal read `bone "B" cannot take slot property "P"`, which named the
3454
- **slot** family for whatever you had written โ€” `wobble`, `translateX`, `rgb` โ€”
3455
- and told you nothing about what a bone does take. Nothing wrong reached disk
3456
- then either, because the dispatch was already this table; what was missing was
3457
- the way forward.
3458
- - `rgba` and `attachment` are the only two names that sentence was ever right
3459
- about, and for those the redirect survives as a clause beside the list:
3460
- `. "rgba" is a slot timeline โ€” put the name in "slot"`. It is read off
3461
- `SLOT_TRACKS`, so the two halves of the message cannot drift apart. A
3261
+ message prints them, and the refusal prints the same table the compiler dispatches
3262
+ on โ€” so what you are told a bone accepts is what it accepts.
3263
+
3264
+ - For `rgba` and `attachment` the refusal adds a redirect beside the list:
3265
+ `. "rgba" is a slot timeline โ€” put the name in "slot"`. A
3462
3266
  **constraint** property written on a bone track (`mix`, `inertia`, `position`,
3463
3267
  โ€ฆ) never reaches this refusal at all โ€” it is refused first, by the row that
3464
3268
  names the field its constraint's name belongs in (ยง4.12).
3465
- - **Nothing derives this page's copy of the eleven from the table.** What keeps the
3466
- two in step is the control that quotes the message โ€” `RF26` in `selftest.ts` โ€”
3467
- which goes red if the list ever widens without this page moving with it, and
3468
- `RF27` holds the slot clause the same way. The selftest's spelling census
3469
- (`PS144`) reads the eleven off the same message and compares them against the
3470
- eleven it actually poses, both ways, which is what makes the list checkable at
3471
- all: it was stated there too until the refusal had something to state. It was
3472
- ten until [#733](https://github.com/firejune/rigc/issues/733) added `inherit`,
3473
- and that landing is what moved `RF26`, this page and the census together.
3474
3269
 
3475
3270
  โš ๏ธ **A `slot` track's `property` is one of the six above, and anything else is a
3476
3271
  compile error** โ€” `animation "A" slot "X" has no timeline "P" (it has:
3477
- attachment, rgba, rgb, alpha, rgba2, rgb2)`, ยง5.1's row. Until
3478
- [#650](https://github.com/firejune/rigc/issues/650) it was not: the emitter had a
3479
- branch for `attachment` and wrote **everything else** as an rgba timeline under
3480
- the name you gave it, so a track spelled `sequence` compiled, emitted
3481
- `slots.X.sequence` with rgba keys, and was refused one stage later by the gate
3482
- (`A00_ROUNDTRIP_PARSE: threw: Invalid timeline type for a slot`) โ€” while the
3483
- one-channel spelling of the same mistake was refused at compile as `rgba value
3484
- needs 4 channels, got 1`, a message about a key you had not written.
3485
-
3486
- - The six are the emitter's own dispatch table (`SLOT_TRACKS` in
3487
- `src/compile.ts`), in the order `SkeletonJson.readAnimation` switches on
3488
- them โ€” which makes it the format's whole slot switch: `compileTrack` reads it
3489
- to pick its branch, and the refusal prints `Object.keys` of the same object,
3490
- so what you are told a slot accepts is what it accepts.
3491
- - **Nothing derives this page's copy of that list from the table**, and it is
3492
- six names long: no `DQ*`/`RD*`/`CUR*` control reads ยง4.4 (the only gated
3493
- table on this page is ยง3.5.2.1's, held by `RD01`โ€“`RD06`). What keeps the two
3494
- in step is the control that quotes the message โ€” `RF23` in `selftest.ts` โ€”
3495
- which goes red if the accepted list ever widens without this page moving with
3496
- it. It did, on the day `rgba2` was added
3497
- ([#690](https://github.com/firejune/rigc/issues/690)) and again when `rgb`,
3498
- `alpha` and `rgb2` were ([#730](https://github.com/firejune/rigc/issues/730)),
3499
- which is the mechanism working rather than a hole in it.
3272
+ attachment, rgba, rgb, alpha, rgba2, rgb2)`, ยง5.1's row.
3273
+
3274
+ - The six are in the order `SkeletonJson.readAnimation` switches on them โ€” the
3275
+ format's whole slot switch โ€” and the refusal prints the table the compiler
3276
+ dispatches on, so what you are told a slot accepts is what it accepts.
3500
3277
  - ๐ŸŽจ **`rgb`, `alpha` and `rgb2` are the SEPARABLE colour timelines, and they
3501
3278
  are not spellings of `rgba`.** Each poses part of the slot's colour and leaves
3502
3279
  the rest exactly where it was: `rgb` writes the light colour's r g b and never
@@ -3542,7 +3319,7 @@ needs 4 channels, got 1`, a message about a key you had not written.
3542
3319
  - The six are every slot timeline the format has; anything else is refused
3543
3320
  here, and would be refused by the runtime's own reader too (`Invalid timeline
3544
3321
  type for a slot`). `A12_NO_DARK_COLOR` refuses `rgb2` โ€” and `rgba2`, and the
3545
- slot field โ€” in a file under the `spine-html` profile (SPEC_COVERAGE ยง2.1).
3322
+ slot field โ€” in a file under the `spine-html` profile.
3546
3323
  `sequence` is a timeline on an **attachment**, not on a slot โ€” it is the
3547
3324
  family beside `deform`, ยง4.13.
3548
3325
 
@@ -3552,26 +3329,13 @@ and anything else is a compile error** โ€” `animation "A" group "G" has no timel
3552
3329
  โ€ฆ)`, ยง5.1's row. It is the only refusal on this page that prints **three** lists,
3553
3330
  and the reason is ยง4.3's: a group's family is decided by the property, so a
3554
3331
  property no table claims leaves the compiler with no family to answer for. All
3555
- three come from the objects the dispatch reads โ€” `BONE_TRACKS`, `SLOT_TRACKS` and
3556
- `PHYSICS_TRACKS` in `src/compile.ts` โ€” and the refusal is raised in
3557
- `resolveTargets`, after the group's own existence check and before any member is
3558
- resolved against the rig.
3559
-
3560
- - Until [#661](https://github.com/firejune/rigc/issues/661) a group of **bones**
3561
- with a misspelled bone property read `animation "A" targets unknown slot "M"`:
3562
- with no table claiming the property the track fell through to the slot branch,
3563
- and what you were told was that the first member is not a slot โ€” on a file that
3564
- named neither a slot nor that member. A group of **slots** got ยง4.4's slot row
3565
- instead (`slot "M" has no timeline "P" (it has: attachment, rgba, rgba2)`), which is
3566
- true of the member and names one family out of three on a track whose family
3567
- nothing had determined.
3332
+ three are the tables the compiler dispatches on, and the refusal comes after the
3333
+ group's own existence check and before any member is resolved against the rig.
3334
+
3568
3335
  - **A constraint property never reaches it.** `position`, `spacing` and `time` are
3569
3336
  refused first with the field their constraint's name goes in (ยง4.12), and a
3570
3337
  physics timeline spelled correctly is not an error at all โ€” a group of physics
3571
3338
  constraints is how several are tuned in one track.
3572
- - `RF30`โ€“`RF34` in `selftest.ts` quote this message; the three lists are typed
3573
- there rather than read off the tables, so widening any of the three without
3574
- moving this page turns them red.
3575
3339
 
3576
3340
  **A physics constraint's six tuning timelines override ยง4.6's table for the
3577
3341
  length of an animation.** `{ "physics": "hair", "property": "wind", "keys": [โ€ฆ] }`
@@ -3592,21 +3356,19 @@ a delta from the constraint's own setting.
3592
3356
  key states a mass and the pose holds `1 / mass`, so a `mass` key of `0` is an
3593
3357
  infinite inverse mass โ€” every velocity and offset is NaN from the first step.
3594
3358
  - **Four of the seven are bounded, and a key outside its bound is a compile
3595
- error** ([#610](https://github.com/firejune/rigc/issues/610)). `mass` must be
3359
+ error.** `mass` must be
3596
3360
  `> 0`, `damping` must be inside the closed `[0, 1]`, and `mix` and `strength`
3597
3361
  must be `0` or more. `A23_PHYSICS_CONSTRAINT_EFFECTIVE` applies the same four to
3598
3362
  a file rigc did not write, naming the animation, the constraint, the key time
3599
3363
  and the value โ€” so the compiler is where a spec you wrote is refused, and the
3600
3364
  assertion is where an import is.
3601
- - ๐Ÿ”Ž **Each way out of a bound has a basis, and the refusal says which**
3602
- ([#798](https://github.com/firejune/rigc/issues/798)). **arithmetic** means the
3365
+ - ๐Ÿ”Ž **Each way out of a bound has a basis, and the refusal says which.**
3366
+ **arithmetic** means the
3603
3367
  runtime cannot compute the value โ€” an expression in the integrator is
3604
3368
  non-finite at it โ€” and the sentence names the expression and its lines;
3605
3369
  **behavioural** means the runtime runs it, finitely, and the rig runs wrongly,
3606
3370
  so refusing it is rigc's call and the sentence says so and says what the value
3607
- does. Two of the eight ways out are arithmetic. The table is the
3608
- `PHYSICS_POSE_RULES` rows' own `basis`, verbatim, and `CUR100` holds it to them;
3609
- every basis is re-measured through spine-core by `T113` on every run.
3371
+ does. Two of the eight ways out are arithmetic.
3610
3372
 
3611
3373
  | field | at rest | on a key | basis |
3612
3374
  | --- | --- | --- | --- |
@@ -3622,18 +3384,15 @@ a delta from the constraint's own setting.
3622
3384
  finite on every one. Every run-away overflows at some horizon, which is why
3623
3385
  `damping` above 1 is behavioural although it diverges.
3624
3386
  - โš–๏ธ **A negative `mix` is refused on a physics constraint and accepted on a
3625
- transform constraint, and neither is the runtime's arithmetic**
3626
- ([#798](https://github.com/firejune/rigc/issues/798)). [measured] both are a
3387
+ transform constraint, and neither is the runtime's arithmetic.** [measured] both are a
3627
3388
  finite, mirrored scale: a physics constraint resting at `mix` โˆ’0.5 moves its
3628
3389
  bone by exactly the opposite of +0.5 on every step, and a transform constraint
3629
3390
  at `mixRotate` โˆ’0.5 rotates its bone by the negative of +0.5's. What differs is
3630
3391
  the range the runtime documents for each field โ€” `PhysicsConstraintPose.mix` is
3631
3392
  "a percentage (0+)", `TransformConstraintPose.mixRotate` "a percentage
3632
- (unbounded)" โ€” and each rule follows its own. `T114` holds the mirror and both
3633
- documented ranges against the runtime; `T105` holds the transform side's
3634
- acceptance.
3393
+ (unbounded)" โ€” and each rule follows its own.
3635
3394
  - โš ๏ธ **`damping`'s bound depends on the constraint's `fps`, and a rig played at
3636
- 60 fps hides it** ([#748](https://github.com/firejune/rigc/issues/748)). The
3395
+ 60 fps hides it.** The
3637
3396
  decay is `damping ** (60 * step)` with `step` = `1 / fps`, so the exponent is
3638
3397
  exactly 1 at 60 fps, where a negative damping only flips the velocity's sign
3639
3398
  each step and can look like a jiggle settling. Wherever `60 / fps` is not a
@@ -3642,15 +3401,13 @@ a delta from the constraint's own setting.
3642
3401
  stays finite at 60 fps and at 30 (exponent 2), and is NaN within three steps of
3643
3402
  the key at 45 and at 120. Above `1` every velocity grows on every step and
3644
3403
  diverges at every rate. The refusal names the exponent and the rate for this
3645
- reason, and `T101` holds it to the two-rate measurement.
3646
- - โœ… **Both ends of `damping` are inside the bound**
3647
- ([#794](https://github.com/firejune/rigc/issues/794)). `1 ** x` is 1 and
3404
+ reason.
3405
+ - โœ… **Both ends of `damping` are inside the bound.** `1 ** x` is 1 and
3648
3406
  `0 ** x` is 0 for every positive exponent, so neither depends on `fps`: at `1`
3649
3407
  the velocity never decays and the jiggle holds for as long as it runs, and at
3650
3408
  `0` every velocity is zeroed on every step, so the offset follows the bone with
3651
3409
  no overshoot. Both are finite, at rest and keyed โ€” [measured] 120 steps from
3652
- `Physics.reset` at 60, 45 and 30 fps on the generated physics fixture, which
3653
- `T109`โ€“`T111` hold. โš ๏ธ `1` is also what a 4.2 file means by saying nothing:
3410
+ `Physics.reset` at 60, 45 and 30 fps on the generated physics fixture. โš ๏ธ `1` is also what a 4.2 file means by saying nothing:
3654
3411
  4.2's parser reads an omitted `damping` as `1` where 4.3's reads `0.85`
3655
3412
  (ยง4.6), so a rig migrated from 4.2 that keys "the default" keys `1`.
3656
3413
  - ๐Ÿšซ **`inertia`, `wind` and `gravity` are bounded nowhere, and neither is the top
@@ -3666,7 +3423,7 @@ a delta from the constraint's own setting.
3666
3423
  `strength`, 0 takes the restoring term out of the velocity update and leaves
3667
3424
  `damping` and `inertia` applied โ€” the offset is not pulled back *while the key
3668
3425
  holds*, which is "physics released" for that span, and the next key pulls it
3669
- back ([#727](https://github.com/firejune/rigc/issues/727)). At rest the two
3426
+ back. At rest the two
3670
3427
  part ways: a `strength` of 0 is a constraint nothing pulls back, which `A23`
3671
3428
  refuses, and a `mix` of 0 is a constraint muted until an animation keys it
3672
3429
  above 0 โ€” the next bullet. A **negative** setup `strength` is refused too, with
@@ -3674,11 +3431,9 @@ a delta from the constraint's own setting.
3674
3431
  added to the offset instead of taken out, so the offset is **pushed away** and
3675
3432
  grows โ€” [measured] resting at โˆ’100 on the generated physics fixture it grew
3676
3433
  28.35ร— over 0.5 s with no sign change, where resting at 100 it swung back
3677
- through 0. `A23` reads which sentence to print off the `strength` row of
3678
- `PHYSICS_POSE_RULES` (its `outside` arms), and the key's refusal quotes the
3679
- negative arm from the same row, so the two cannot say different things about
3680
- one number ([#748](https://github.com/firejune/rigc/issues/748)).
3681
- - โš ๏ธ **A setup `mix` of `0` is legal when some animation keys it above 0** ([#743](https://github.com/firejune/rigc/issues/743)).
3434
+ through 0. `A23` and the key's refusal read that sentence from one source, so
3435
+ the two cannot say different things about one number.
3436
+ - โš ๏ธ **A setup `mix` of `0` is legal when some animation keys it above 0.**
3682
3437
  A constraint muted *at rest* is a rig whose physics is off until an animation
3683
3438
  switches it on, which is a design rather than the silence `A23` was built for.
3684
3439
  What `A23` refuses is the constraint nothing rescues: muted at setup and keyed
@@ -3690,10 +3445,10 @@ a delta from the constraint's own setting.
3690
3445
  "Keyed above 0" means **any value the timeline poses**, which includes a
3691
3446
  Bezier between two keys of 0 whose handles lie above 0: the runtime
3692
3447
  interpolates through the curve's samples, not between the keys, and such a
3693
- pair moves the bone ([#752](https://github.com/firejune/rigc/issues/752)). The
3448
+ pair moves the bone. The
3694
3449
  same reading is `A36`'s and `A37`'s (ยง4.12).
3695
3450
  - ๐Ÿ“ **What a `strength` key of `0` costs, measured through spine-core** on the
3696
- generated overlay fixture, stepping at 60 fps from `Physics.reset` (#727). With
3451
+ generated overlay fixture, stepping at 60 fps from `Physics.reset`. With
3697
3452
  no wind or gravity the offset coasts to a limit rather than running away โ€” a
3698
3453
  0.5 s release and a 2.0 s release end **0.95 %** apart โ€” and the restoring key
3699
3454
  takes it from 5.5063 back under 0.01 in **54 steps**. With `gravity -40` pulling,
@@ -3705,8 +3460,7 @@ a delta from the constraint's own setting.
3705
3460
  the first sub-step and **still NaN after the restoring key**, and a keyed
3706
3461
  `damping` of `2` was still 6.9e4 two seconds later.
3707
3462
 
3708
- **`"physics": "*"` is the physics timeline that names no constraint**
3709
- ([#726](https://github.com/firejune/rigc/issues/726)). The skeleton file writes it
3463
+ **`"physics": "*"` is the physics timeline that names no constraint.** The skeleton file writes it
3710
3464
  under the empty name โ€” `animations.<a>.physics[""]`, which `SkeletonJson` loads as
3711
3465
  constraint index `-1` without looking anything up โ€” and the runtime then applies it
3712
3466
  to every active physics constraint whose own data declares the keyed property
@@ -3759,14 +3513,7 @@ consumer's, decided by dressing the skeleton rather than by the animation.
3759
3513
  refusal says which skins were searched and what the slot does have (ยง5.1).
3760
3514
  - A rig with **no `default` skin at all** โ€” every attachment in named skins,
3761
3515
  which is the shape an editor export of a multi-skin character gives back โ€” is
3762
- therefore a rig whose attachment keys work. Until
3763
- [#695](https://github.com/firejune/rigc/issues/695) it was not: keys were
3764
- checked against the default skin alone, so a named-skin name was refused as
3765
- unknown, and a rig with no default skin had **every** attachment key refused,
3766
- including ones whose art is in the first named skin. The setup pose resolved
3767
- across skins the whole time (ยง4.2), so the two halves of one slot disagreed โ€”
3768
- `slots[].attachment: "plain"` was accepted and a key naming `plain` on that
3769
- same slot was not.
3516
+ therefore a rig whose attachment keys work.
3770
3517
  - โš ๏ธ **A `deform` track is the other way round and names its skin outright**
3771
3518
  (ยง4.11.5), because the format keys a deform timeline on a `skin/slot/attachment`
3772
3519
  triple and a deform run is geometry for one attachment object. An attachment
@@ -3782,7 +3529,7 @@ consumer's, decided by dressing the skeleton rather than by the animation.
3782
3529
  - **Key times are emitted as float32s like every other number, and never stored
3783
3530
  LATER than you wrote them.** Every emitted number is the shortest decimal naming
3784
3531
  its float32 โ€” the text the editor writes, and the precision the runtime keeps,
3785
- because `spine-core` reads a timeline's frames into a `Float32Array` (issue #716).
3532
+ because `spine-core` reads a timeline's frames into a `Float32Array`.
3786
3533
  A time that already names a float โ€” `0.5`, `0.2`, the editor's `1.4333333` โ€” is
3787
3534
  written as you wrote it. A time that does not โ€” `2/12` computed in doubles, a key
3788
3535
  moved by `lag` or `stagger` โ€” steps to the **largest float not above it**, never
@@ -3792,9 +3539,7 @@ consumer's, decided by dressing the skeleton rather than by the animation.
3792
3539
  *larger* than either, so a key stored there is applied at sample **3** of a 12 fps
3793
3540
  playback and not sample 2 โ€” a whole frame late, with nothing raised. On a
3794
3541
  **stepped** timeline (an attachment timeline always is) that is the wrong picture
3795
- rather than a slightly wrong value: the spineboy run's muzzle flare fired a frame
3796
- late for exactly this, on the six-decimal grid rigc emitted until #716, until the
3797
- run's own frame check caught it (issue #99). Stepping down cannot do that; the
3542
+ rather than a slightly wrong value. Stepping down cannot do that; the
3798
3543
  worst it can do is store a key one float early โ€” 1.5e-8 s at 1/6 s โ€” on the sample
3799
3544
  it was written for. โš ๏ธ What this does **not** protect you from is rounding your
3800
3545
  own times before you write them โ€” write `2/12`, not `0.1667`, and let the compiler
@@ -3808,13 +3553,11 @@ consumer's, decided by dressing the skeleton rather than by the animation.
3808
3553
  and a stepped key there sits above the sample that was meant to see it. On an
3809
3554
  interpolated timeline that costs a few ULPs of value and nothing else. On a
3810
3555
  **stepped** one it is the whole frame โ€” and on the last sample it is the whole
3811
- event, because there is no later sample to catch it. Measured on rung 5's 6.5 s
3812
- shot at 12 fps: **13 of its 78 sample times are affected** (f6, f15, f18, f21, f24,
3556
+ event, because there is no later sample to catch it. Measured on a 6.5 s shot
3557
+ at 12 fps: **13 of its 78 sample times are affected** (f6, f15, f18, f21, f24,
3813
3558
  f27, f60, f63, f66, f69, f72, f75, f78), and an attachment key written at the
3814
3559
  declared duration `6.5` never fired at all against an accumulated
3815
- `6.499999999999994` โ€” which read as a frame-change disagreement the pose series had
3816
- already fixed, and cost that run three builds
3817
- ([`2026-08-26-rung5-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-26-rung5-1/LOOP.md), ยง8). โ‡’ **For a
3560
+ `6.499999999999994`. โ‡’ **For a
3818
3561
  stepped timeline, write a time a little below `T` โ€” `T โˆ’ 1e-6` still works, at
3819
3562
  any magnitude โ€” rather than `T`.** What makes it work is not the size of the step:
3820
3563
  any time below `T` is stored on a float below `T`, because a time that names a
@@ -3831,10 +3574,10 @@ consumer's, decided by dressing the skeleton rather than by the animation.
3831
3574
  step at the duration โ€” 4.8e-7 s at 5 s, 3.8e-6 s at 32 s โ€” which is the most a
3832
3575
  key you put exactly *on* a duration the float cannot hold is stored past it, so
3833
3576
  such a key is fine. R7's frame of slack does not apply in this direction and
3834
- would not see this: rung 6 rounded its key times to 4 dp somewhere upstream, its
3835
- one-frame reveal landed 0.000034 s past a 68/12 s duration, another track was
3836
- already sitting on the declared duration so the animation's *longest* key time
3837
- looked right โ€” and the reveal never appeared. If you want a key on the last
3577
+ would not see this: key times rounded to 4 dp upstream put a one-frame reveal
3578
+ 0.000034 s past a 68/12 s duration while another track sat on the declared
3579
+ duration, so the animation's *longest* key time looked right โ€” and the reveal
3580
+ never appeared. If you want a key on the last
3838
3581
  sample, write the duration's own value; if you want the animation to run longer,
3839
3582
  say so in `duration`.
3840
3583
  - `ease` names an entry of `easings`, or the literal `"stepped"`. Absent = linear โ€”
@@ -3847,7 +3590,7 @@ consumer's, decided by dressing the skeleton rather than by the animation.
3847
3590
  key equals the next key's *as emitted* โ€” a `translate` whose x moves and whose y
3848
3591
  does not is **not** a hold โ€” the curve would run from a value to the same value and
3849
3592
  draw nothing, and the editor writes that segment as `"curve": "stepped"`. So rigc
3850
- does too (ยง10.4, issue #369): the frames are byte-identical either way, and a build
3593
+ does too (ยง10.4): the frames are byte-identical either way, and a build
3851
3594
  stops differing from its own editor export on `diff`'s `animations.curve_kinds`. A
3852
3595
  raw `curve` is **not** rewritten โ€” it states the file's own numbers verbatim, and
3853
3596
  the editor's own exports do carry beziers over holds, so a transcription has to be
@@ -3863,12 +3606,10 @@ consumer's, decided by dressing the skeleton rather than by the animation.
3863
3606
  ### 4.5.1 A group track's per-member values โ€” a map, or a model
3864
3607
 
3865
3608
  โญ **`groups` keys several bones identically, and on a face the whole content of
3866
- the motion is that each part gets a *different* number.** That is the complaint
3867
- [#295](https://github.com/firejune/rigc/issues/295) filed. `gallery/portrait`'s
3868
- held 12ยฐ yaw was **20 tracks**, sixteen of them the same two properties on six
3869
- sibling bones โ€” identical times, identical easings, identical key counts, six
3870
- different values โ€” and exactly **one** of the twenty was a `groups` entry, the
3871
- pair that happened to share `cos t`.
3609
+ the motion is that each part gets a *different* number.** `gallery/portrait`'s
3610
+ held 12ยฐ yaw, written as plain tracks, is the same two properties on six sibling
3611
+ bones โ€” identical times, identical easings, identical key counts, six different
3612
+ values.
3872
3613
 
3873
3614
  Two spellings land, and **which one is right depends on whether the numbers are
3874
3615
  decisions or arithmetic.**
@@ -3884,9 +3625,8 @@ would be for that one member, so nothing about the value shapes in ยง4.4 changes
3884
3625
  { "t": 2.2, "v": [0] } ] }
3885
3626
  ```
3886
3627
 
3887
- A non-map `v` keeps meaning what it always meant โ€” every member gets it โ€” so no
3888
- existing spec changes, and the emitted file is **byte for byte** the one the six
3889
- separate tracks produced. Reach for this when the six numbers are six
3628
+ A non-map `v` means every member gets it, and the emitted file is **byte for
3629
+ byte** the one six separate tracks produce. Reach for this when the six numbers are six
3890
3630
  **judgements**: hand-picked swings on a row of hanging locks, a decided offset
3891
3631
  per part.
3892
3632
 
@@ -3905,9 +3645,8 @@ Same move as ยง4.11.1 on the bone half of the same turn:
3905
3645
 
3906
3646
  โญ **The depth table is the reason this is worth more than the map, and it is not
3907
3647
  the line count.** FACE ยง2's sharp edge is that **`x` is in the file and `z` is
3908
- not** โ€” `grep -c '"z"'` over the worked example's two specs returned `0` and `0`,
3909
- and every depth that produced every number lived only in a README beside them. A
3910
- `derive` key puts them in the file that uses them, and FACE ยง3's whole argument
3648
+ not** โ€” with plain values, every depth that produced every number lives outside
3649
+ the specs. A `derive` key puts them in the file that uses them, and FACE ยง3's whole argument
3911
3650
  is that a **depth** is the decision while a **residual** is not.
3912
3651
 
3913
3652
  **The two kinds, and the property picks the projection:**
@@ -3933,8 +3672,7 @@ a head swing the other way (FACE ยง2). That sign is the one parameter here no
3933
3672
  assertion can check, so the closed form is the arbiter: a part with
3934
3673
  `depth > carried` gets a **negative** residual, and FACE ยง3 makes exactly that
3935
3674
  the nose diagnostic โ€” *if the nose's residual is not negative, the depths are
3936
- wrong*. ([#351](https://github.com/firejune/rigc/issues/351) was this sentence
3937
- missing here and stated backwards in the field reference.)
3675
+ wrong*.
3938
3676
 
3939
3677
  `carried`
3940
3678
  (default 0) is **the depth whose shift a parent bone already applies** โ€” FACE
@@ -3988,8 +3726,8 @@ iris counter-scale is `1/scaleX` of the socket, and it stays two ordinary
3988
3726
  `groups` entries with one shared value each โ€” because a counter-scale belongs to
3989
3727
  the **socket**, not to the part: `spark_l` at local `x = โˆ’11` takes the same
3990
3728
  number as `iris_l` at `0`. A shared value on a group is the correct statement of
3991
- that, and forcing it into a per-member model would have been a worse spec that
3992
- happened to use the new field.
3729
+ that, and forcing it into a per-member model would be a worse spec that merely
3730
+ used the field.
3993
3731
 
3994
3732
  ๐Ÿ”ญ **Deliberately not built:** a per-member `v` on a track that names no group (a
3995
3733
  value map needs members to name); per-member **easings** or key **times** (the
@@ -4037,7 +3775,7 @@ group members (the per-member values of one track, side by side โ€” issue #295)
4037
3775
  that face, because it is the only feature in front of the skull surface โ€” and
4038
3776
  FACE ยง3 makes it the diagnostic: *if the nose's residual is not negative, the
4039
3777
  depths are wrong.* That check is arithmetic rather than a render, and this block
4040
- is where it is now legible.
3778
+ is where it is legible.
4041
3779
 
4042
3780
  **It quotes; it does not re-derive.** Every value is the one the compiler
4043
3781
  emitted, so the block and the artifact cannot disagree โ€” a report that evaluated
@@ -4058,34 +3796,30 @@ mass?, wind?, gravity?, mix?, fps?, limit? }`. These are emitted into the 4.3
4058
3796
  `constraints` array. Seven of them โ€” the six tuning numbers and `mix` โ€” can also
4059
3797
  be **keyed over time** as `tracks` entries naming this constraint (ยง4.4); this
4060
3798
  table is the value at rest, and a timeline overrides it while it plays. `mass: 0` becomes an infinite inverse mass and a `damping` above 1
4061
- never settles โ€” both are `A23`, here and on every timeline key that states them
4062
- ([#610](https://github.com/firejune/rigc/issues/610)); a `damping` below 0
4063
- is refused as well, and whether it is NaN depends on `fps` โ€” ยง4.4
4064
- ([#748](https://github.com/firejune/rigc/issues/748)). `damping` is `[0, 1]`
4065
- with both ends in: `1` never decays and `0` zeroes the velocity on every step,
4066
- both finite at every rate ([#794](https://github.com/firejune/rigc/issues/794)).
3799
+ never settles โ€” both are `A23`, here and on every timeline key that states them;
3800
+ a `damping` below 0 is refused as well, and whether it is NaN depends on `fps` โ€”
3801
+ ยง4.4. `damping` is `[0, 1]` with both ends in: `1` never decays and `0` zeroes the
3802
+ velocity on every step, both finite at every rate.
4067
3803
  โš ๏ธ An omitted `damping` is `0.85` to 4.3's parser (`SkeletonJson.js:308` in
4068
3804
  4.3.13) and `1` to 4.2's (`SkeletonJson.js:242` in 4.2.120), so a constraint
4069
3805
  migrated from 4.2 that stated nothing may arrive stating `1` โ€” which builds. โš ๏ธ `strength: 0` is `A23` **here and not on a key**:
4070
3806
  at rest it is a constraint nothing pulls back, and on a key it is a release somebody
4071
- asked for, which ยง4.4 states with the measurement behind it
4072
- ([#727](https://github.com/firejune/rigc/issues/727)). A negative `strength` is
3807
+ asked for, which ยง4.4 states with the measurement behind it. A negative `strength` is
4073
3808
  `A23` here and on a key, and here its sentence is its own: the offset is pushed
4074
- away and grows ([#748](https://github.com/firejune/rigc/issues/748)). `mix: 0` is the one value
3809
+ away and grows. `mix: 0` is the one value
4075
3810
  here an animation can answer for: it rests the constraint **muted**, which is
4076
3811
  legal, and `A23` names it only when no timeline in any animation keys that `mix`
4077
- above 0 โ€” on a key it is the mute for a span (ยง4.4, [#743](https://github.com/firejune/rigc/issues/743)). None of the
3812
+ above 0 โ€” on a key it is the mute for a span (ยง4.4). None of the
4078
3813
  three is a compile error โ€” a setup value this table states reaches the gate, where the whole
4079
3814
  file can be read; it is a **key** outside its bound that `build` refuses (ยง4.4). Every field but `bone` and `note` must be a finite
4080
- number: a non-number is rounded to `NaN` and emitted as `null`, which the runtime
4081
- reads as **zero**, so `"mass": "heavy"` used to ship a constraint that never
4082
- settles with no word from anybody (#307).
3815
+ number: a non-number would reach the file as `null`, which the runtime reads as
3816
+ **zero**, so `"mass": "heavy"` would ship a constraint that never settles.
4083
3817
 
4084
3818
  `mix` is a player-side `AnimationStateData` config and is **not** emitted into
4085
- skeleton JSON โ€” which is why nothing looked at it until the parser did: it is
3819
+ skeleton JSON, so the parser is the only check it gets: it is
4086
3820
  `{ "default": <seconds>, "pairs"?: [["<from>", "<to>", <seconds>], โ€ฆ] }`, and a
4087
- `default` that is not a number passed the compiler, the gate and the round trip
4088
- before becoming a `NaN` mix duration in the player.
3821
+ `default` that is not a number would pass the gate and the round trip before
3822
+ becoming a `NaN` mix duration in the player.
4089
3823
 
4090
3824
  ### 4.7 `drawOrder` โ€” reordering the slots over time
4091
3825
 
@@ -4228,11 +3962,9 @@ here was measured off a real rig. Copy the shape, not the values.
4228
3962
  the constraint (`SkeletonJson:155`) and once on **every timeline key**
4229
3963
  (`:912`) โ€” so a key that omits `bendPositive` does not inherit the constraint's
4230
3964
  value, it asserts `true`. A rig declaring `bendPositive: false` under a timeline
4231
- that keys only `mix` therefore bent the *other* way for the whole animation,
4232
- with the field still in the file and inert: four builds differing only in those
4233
- values posed one pose, and the gate was green throughout (issue #273). rigc now
4234
- stamps the rig's value onto every emitted key, so the declaration reaches the
4235
- runtime. **Stating a flag on every key still overrides the rig** โ€” the format
3965
+ that keys only `mix` would therefore bend the *other* way for the whole
3966
+ animation, with the field still in the file and inert. So rigc stamps the rig's
3967
+ value onto every emitted key, and the declaration reaches the runtime. **Stating a flag on every key still overrides the rig** โ€” the format
4236
3968
  keys them per key on purpose, a bend that flips partway through is a real thing
4237
3969
  to write, and it is what the editor's own export does.
4238
3970
  - ๐Ÿ” **A spec that came out of `ingest` (ยง0.3) states all three on every key, and
@@ -4488,8 +4220,7 @@ emitted file, measuring the array's length from the attachment rather than assum
4488
4220
  an encoding.
4489
4221
 
4490
4222
  ๐Ÿ“Œ **A run is not required to be an even number of numbers, and it is not required
4491
- to start on an even index.** Two rows stood here saying otherwise until issue
4492
- #576. Nothing in either reader aligns a run to a pair: `SkeletonJson` does
4223
+ to start on an even index.** Nothing in either reader aligns a run to a pair: `SkeletonJson` does
4493
4224
  `Utils.arrayCopy(vertices, 0, deform, offset, vertices.length)` โ€” a raw copy, at
4494
4225
  the raw index the key gives โ€” and `SkeletonBinary` reads a count and a start and
4495
4226
  fills `for (let v = start; v < end; v++)`. So a run may begin and end mid-pair,
@@ -4499,24 +4230,21 @@ the whole delta minus one leading zero.
4499
4230
 
4500
4231
  โญ **An odd run's last number is an x with no y beside it, and that is a
4501
4232
  statement, not an accident.** It moves that vertex in x and leaves its y at the
4502
- setup value, because the parser copies your numbers and touches nothing else. It
4503
- is also the reason the rule could not stay as an authoring convenience: padding a
4504
- `0` to make the run even *changes what plays* wherever that setup y is non-zero,
4505
- so there is no second spelling of such a key โ€” refusing it would mean no
4506
- transcription of that file can be written in this spec at all. `A35` reached the
4507
- same conclusion from the other side in issue #262, and between that fix and this
4508
- one the two halves of rigc disagreed about what the format holds.
4509
-
4510
- โš ๏ธ What you lose with those rows is a typo filter: `offset: 3` written where
4511
- `fromVertex: 3` was meant now compiles. It always half-did โ€” `offset: 4` meant as
4512
- vertex 4 lands on vertex 2 and was never refused โ€” so read the field name twice.
4233
+ setup value, because the parser copies your numbers and touches nothing else.
4234
+ Padding a `0` to make the run even *changes what plays* wherever that setup y is
4235
+ non-zero, so there is no second spelling of such a key โ€” which is why an odd run
4236
+ is accepted rather than refused.
4237
+
4238
+ โš ๏ธ What that costs is a typo filter: `offset: 3` written where `fromVertex: 3` was
4239
+ meant compiles, as `offset: 4` meant as vertex 4 lands on vertex 2 โ€” so read the
4240
+ field name twice.
4513
4241
  `offset` is an index into the deform array; `fromVertex` is a vertex.
4514
4242
 
4515
4243
  ๐Ÿ–ผ๏ธ **Worked examples, and they use a deform for four different things** โ€” all
4516
4244
  four are repository material rather than part of the published package, so the
4517
4245
  links go to GitHub.
4518
4246
  [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash) โ€” a 9-vertex ball squashed about its contact point,
4519
- from the two affine transforms its keys now **state**.
4247
+ from the two affine transforms its keys **state**.
4520
4248
  [`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait) โ€” a 2.5D head turn, where the keys are the
4521
4249
  **projection of a yaw** rather than a squash: two grid meshes whose columns are
4522
4250
  placed to sample a cosine, and a measured account of the angle past which the
@@ -4545,12 +4273,10 @@ build.
4545
4273
  half: the key names a transform, the compiler evaluates it over the attachment's
4546
4274
  own setup geometry, and the emitted file carries the numbers.
4547
4275
 
4548
- The case that filed it ([#294](https://github.com/firejune/rigc/issues/294)):
4549
- `gallery/portrait`'s held 12ยฐ head yaw was **160 hand-written vertex offsets
4550
- across 8 keys**, and not one of them was a judgement โ€” every one is
4551
- `xยท(cos t โˆ’ 1) โˆ’ zยทsin t` at a different column (FACE ยง1). A second angle was a
4552
- second full table, which is why that example's own angle sweep needed a
4553
- throwaway script that never made it into the repository. It is now four lines:
4276
+ `gallery/portrait`'s held 12ยฐ head yaw, written as a table, is **160 vertex
4277
+ offsets across 8 keys**, and not one of them is a judgement โ€” every one is
4278
+ `xยท(cos t โˆ’ 1) โˆ’ zยทsin t` at a different column (FACE ยง1), and a second angle is
4279
+ a second full table. As a model it is four lines:
4554
4280
 
4555
4281
  ```json
4556
4282
  "deform": [
@@ -4591,8 +4317,7 @@ convention:
4591
4317
  **It covers every vertex, always.** A transform is a model of the attachment, not
4592
4318
  an edit of part of it, so it starts at deform index 0 and runs to the end โ€” which
4593
4319
  is why `fromVertex` and `offset` are refused beside it. A model applied to part
4594
- of a run leaves a **step at the run's edge**, and that is one half of the defect
4595
- [#313](https://github.com/firejune/rigc/issues/313) records. If you want a
4320
+ of a run leaves a **step at the run's edge**. If you want a
4596
4321
  partial run, write it.
4597
4322
 
4598
4323
  **There is no `parallax` kind, and the reason is worth stating.** A pure depth
@@ -4619,8 +4344,7 @@ parameters that key states, and what happens between two keys is still the
4619
4344
  timeline's own single 0..1 blend channel. Sweeping an angle is editing one number
4620
4345
  per key.
4621
4346
 
4622
- **It works on a weighted attachment, and reads world coordinates there**
4623
- ([#389](https://github.com/firejune/rigc/issues/389)). On an unweighted
4347
+ **It works on a weighted attachment, and reads world coordinates there.** On an unweighted
4624
4348
  attachment the deform array is one `x, y` per vertex in the slot bone's space,
4625
4349
  and the model is evaluated in that space. The same is true of a weighted
4626
4350
  attachment while every vertex has exactly one bone and they all share it โ€” the
@@ -4660,8 +4384,7 @@ consequences. One kind is the exception and it is an exception with a proof:
4660
4384
  `affine` refuses a determinant at or below zero, and above zero a positive
4661
4385
  determinant means no triangle **can** reverse.
4662
4386
 
4663
- **A model that evaluates to nothing is refused too**
4664
- ([#350](https://github.com/firejune/rigc/issues/350)). Every parameter can be
4387
+ **A model that evaluates to nothing is refused too.** Every parameter can be
4665
4388
  individually legal and the model still come out as a **run of zeros** โ€” a `wave`
4666
4389
  whose wavelength puts every vertex on a zero crossing, a `bend` over a span the
4667
4390
  part barely enters. The key then claims a deformation, emits the identity and
@@ -4714,19 +4437,17 @@ like every other number, so the same spec emits the same bytes and
4714
4437
  `A18_DETERMINISTIC_EMIT` proves it on a second compile. The grid is absolute on
4715
4438
  purpose: a model's identities โ€” a wave sampled on its zero crossings, a whole
4716
4439
  revolution โ€” are exact zeros float64 misses by ~1e-16, and the refusal of a key
4717
- that states a deformation and evaluates to nothing is decided on it (issue #350);
4440
+ that states a deformation and evaluates to nothing is decided on it;
4718
4441
  a float32 alone is relative and has no zero to land on. The runtime then loads
4719
4442
  the numbers into a `Float32Array`, which is equally true of a hand-written table โ€”
4720
- the difference the generator makes is that the numbers now agree with a stated
4443
+ the difference the generator makes is that the numbers agree with a stated
4721
4444
  model instead of with a transcription.
4722
4445
 
4723
- ๐Ÿ”ญ **Both adjacent asks have since landed.**
4724
- [#295](https://github.com/firejune/rigc/issues/295) was the same complaint about
4725
- a different table โ€” the **bone** tracks of the same turn, where the missing
4726
- number is a depth rather than a vertex โ€” and **ยง4.5.1** is that construct: a
4727
- `derive` kind on a group track, under the same rules as this one. The per-key
4728
- `DEFORM` report block ([#316](https://github.com/firejune/rigc/issues/316))
4729
- quotes the model above rather than re-evaluating it โ€” **ยง4.11.2**.
4446
+ ๐Ÿ”ญ **The same construct covers the bone half of the turn.** The **bone** tracks of
4447
+ the same turn, where the missing number is a depth rather than a vertex, take a
4448
+ `derive` kind on a group track, under the same rules as this one โ€” **ยง4.5.1**. The
4449
+ per-key `DEFORM` report block quotes the model above rather than re-evaluating it
4450
+ โ€” **ยง4.11.2**.
4730
4451
 
4731
4452
  ---
4732
4453
 
@@ -4779,7 +4500,7 @@ deform (what each key does to the geometry โ€” figures with names, never a bar;
4779
4500
  ๐Ÿ“Œ **The frame is the posed one, and the denominator is 1.000 by definition.**
4780
4501
  Both sides of every comparison are taken at the key's own time with the animation
4781
4502
  applied โ€” the deformed mesh against **the same posed bones with the deform
4782
- cleared**. Setup bones were tried and are wrong in principle: a weighted mesh's
4503
+ cleared**. Setup bones would be wrong in principle: a weighted mesh's
4783
4504
  offsets are authored in bone space against the pose they land in. So every ratio
4784
4505
  is *the deform's own contribution*, and a `(setup 1.000)` column beside it would
4785
4506
  be printing the definition. It is also what makes a **mirrored** slot bone a
@@ -4863,18 +4584,14 @@ and displaces `x` over row-major quads preserves every signed area exactly.
4863
4584
  ### 4.11.3 The times no key lands on โ€” `A39` between two keys
4864
4585
 
4865
4586
  ๐Ÿšจ **Your keys are not where the runtime is.** It interpolates between them, so a
4866
- deform that is inside its fold angle at *every* key can be past it in between โ€”
4867
- and until issue
4868
- [#403](https://github.com/firejune/rigc/issues/403) nothing looked there. The
4869
- reachable version of that was the fade above: land the alpha-0 key **on** the
4587
+ deform that is inside its fold angle at *every* key can be past it in between.
4588
+ The reachable version of that is the fade above: land the alpha-0 key **on** the
4870
4589
  folding key and every key is honest โ€” that one really does draw nothing โ€” while
4871
4590
  the frames just before it are drawn, nearly folded, and land on no key at all. On
4872
- the turn probe that is **8 reversed triangles at alpha 0.20, gating green**.
4591
+ the turn probe that is **8 reversed triangles at alpha 0.20**.
4873
4592
 
4874
- โ‡’ `A39` now scans every interval between two consecutive deform keys as well, and
4875
- refuses one with its own sentence. No spec this repository ships produces one โ€” the
4876
- rig it was written from is a probe `selftest.ts` generates and nothing else can
4877
- invoke โ€” so the sentence is described here rather than transcribed.
4593
+ โ‡’ `A39` scans every interval between two consecutive deform keys as well, and
4594
+ refuses one with its own sentence.
4878
4595
 
4879
4596
  **What it carries**, in the order it says it: `BETWEEN key <i> (t=โ€ฆs) and key <j>
4880
4597
  (t=โ€ฆs)` where a key refusal puts one index; the time the closed form solved for, and
@@ -4887,8 +4604,6 @@ that same instant, present only where it is not 1; and the ways out โ€” for an
4887
4604
  interpolating span the four the table below gives, the fade one among them only
4888
4605
  where the alpha is not 1, and for a stepped one the key it holds instead, with
4889
4606
  `invariants.deformMayFold` the last resort either way.
4890
- [`src/validate.ts`](../src/validate.ts) builds it, beside the key sentence ยง4.11.2
4891
- quotes.
4892
4607
 
4893
4608
  **What to change when you see it**, in the order worth trying:
4894
4609
 
@@ -4942,8 +4657,7 @@ means one thing: played on track 0. **An animation a `slider` applies (ยง3.5.2)
4942
4657
  never played that way** โ€” spine-core says so in `SkeletonData.findSliderAnimations`
4943
4658
  โ€” and posing it as though it were is a frame no playthrough contains. The dial
4944
4659
  picks the time, so **the key's time and the applied time are the same number by
4945
- construction**; measuring them independently was issue
4946
- [#407](https://github.com/firejune/rigc/issues/407), and it refused a correct rig.
4660
+ construction**, and measuring them independently would refuse a correct rig.
4947
4661
 
4948
4662
  โ‡’ For every deform key of an animation some slider applies, `A39` **inverts that
4949
4663
  slider's own mapping** and drives its bone until the runtime selects this key's
@@ -4954,9 +4668,7 @@ time = to + (value โˆ’ from) ร— scale the slider's mapping (ยง3.5.2)
4954
4668
  value = from + (time โˆ’ to) / scale what A39 sets the dial to
4955
4669
  ```
4956
4670
 
4957
- ๐Ÿ”’ **The `DEFORM` block prints the frame on every key**, because the derivation
4958
- changed and a block that went on printing the same figures under a changed meaning
4959
- would be worse than the red it replaced. `gallery/look`'s `turn` is the animation
4671
+ ๐Ÿ”’ **The `DEFORM` block prints the frame on every key.** `gallery/look`'s `turn` is the animation
4960
4672
  a slider applies, and this is one of its keys:
4961
4673
 
4962
4674
  ```bash
@@ -4976,24 +4688,23 @@ dial and not reached at all through another.
4976
4688
 
4977
4689
  **Three consequences worth knowing before you build a rig like this:**
4978
4690
 
4979
- - โญ **A slider at `mix: 1` in setup no longer forces the older idiom.** Muting at
4980
- setup and keying `slider.<name>.mix` from a playing animation (ยง4.12) is still
4981
- legitimate, but it is no longer the only way to keep `A39` honest about a slot
4982
- the animation itself fades โ€” and it costs `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET`,
4691
+ - โญ **A slider may rest at `mix: 1` in setup.** Muting at setup and keying
4692
+ `slider.<name>.mix` from a playing animation (ยง4.12) is legitimate too, but it
4693
+ is not needed to keep `A39` honest about a slot the animation itself fades โ€”
4694
+ and it costs `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET`,
4983
4695
  which excludes a slider below full authority. A slider muted at setup is **not**
4984
4696
  a way in: `Slider.update` returns before it reads the bone, so its animation
4985
4697
  keeps the track frame.
4986
4698
  - **Two sliders applying one animation are two frames**, and both are measured.
4987
4699
  A fold only one dial can reach is still a fold.
4988
4700
  - โš ๏ธ **A key at a time no dial can select is named, not passed.** The cause is
4989
- [#405](https://github.com/firejune/rigc/issues/405)'s wrap: `FromRotate.value`
4701
+ the wrap: `FromRotate.value`
4990
4702
  under `local: false` is an `atan2` ending `if (value < 0) value += 360`, so
4991
4703
  **`[0, 360]` is the whole of its range** and a mapping needing anything outside
4992
- it selects nothing. ๐Ÿšจ A **rig spec** can no longer ask for one โ€” the compiler
4993
- refuses both ends of that circle (ยง3.5.2), the low one since #405 and the high
4994
- one since [#417](https://github.com/firejune/rigc/issues/417) โ€” but an
4995
- **artifact** can, because it may have come from the editor, from a hand edit or
4996
- from an older rigc, and this is the surface that reads what actually shipped.
4704
+ it selects nothing. ๐Ÿšจ A **rig spec** cannot ask for one โ€” the compiler refuses
4705
+ both ends of that circle (ยง3.5.2) โ€” but an **artifact** can, because it may have
4706
+ come from the editor or from a hand edit, and this is the surface that reads
4707
+ what actually shipped.
4997
4708
  `A39` measures the frame the runtime *does* land on, leaves the key out of
4998
4709
  `deformKeysMeasured`, and names it:
4999
4710
 
@@ -5043,10 +4754,8 @@ the line says so rather than picking one silently.**
5043
4754
  tie. โ›” Neither case is a refusal and neither is guessed past โ€” an ambiguous
5044
4755
  discovery is a thing to report.
5045
4756
 
5046
- **And a `build` says it too, on `A39`'s stats line**
5047
- ([#427](https://github.com/firejune/rigc/issues/427)) โ€” because `explain` is not
5048
- the loop you run, and until this the whole finding lived on a line only `explain`
5049
- prints. Nothing appears on a rig where the two answers agreed, which is every
4757
+ **And a `build` says it too, on `A39`'s stats line** โ€” because `explain` is not
4758
+ the loop you run. Nothing appears on a rig where the two answers agreed, which is every
5050
4759
  `local: true` slider and every gallery example:
5051
4760
 
5052
4761
  ```
@@ -5068,8 +4777,8 @@ deformDialsDisagreed=1 deformDialDisagreed=dial|artifact:knob.x@2.321e-8|reaches
5068
4777
  - A **tie** never carries `probe:`, `reaches:` or `outside:`, and never counts as a
5069
4778
  disagreement. There is one belief there, not two.
5070
4779
 
5071
- **And `explain`'s rollup carries it too, with the breadcrumb its neighbours have**
5072
- ([#440](https://github.com/firejune/rigc/issues/440)). The per-key `frame` lines
4780
+ **And `explain`'s rollup carries it too, with the breadcrumb its neighbours have.**
4781
+ The per-key `frame` lines
5073
4782
  say it once each; the rollup says it once per animation, and ends by naming the
5074
4783
  two readings a `build` prints it under โ€” because they differ by one letter, and a
5075
4784
  breadcrumb naming only one would send a reader to grep for the other:
@@ -5078,10 +4787,8 @@ breadcrumb naming only one would send a reader to grep for the other:
5078
4787
  .. 1 dial(s) the skeleton and the probe disagree about: the skeleton reads knob.x and the probe drives knob.y, 2 key time(s) outside what the skeleton's own field reaches <- A39 counts them as deformDialsDisagreed and spells them out as deformDialDisagreed
5079
4788
  ```
5080
4789
 
5081
- The count after `outside` is the length of the stats line's own `outside:` list,
5082
- and `deformDialsDisagreed` / `deformDialDisagreed` are checked against the keys
5083
- `A39` really printed rather than restated โ€” `DW34`. An agreed dial adds no line
5084
- (`DW35`) and a **tie** is never rolled up as a disagreement (`DW36`).
4790
+ The count after `outside` is the length of the stats line's own `outside:` list.
4791
+ An agreed dial adds no line, and a **tie** is never rolled up as a disagreement.
5085
4792
 
5086
4793
  โ›” **None of it refuses a build**, and the reason is measured rather than chosen.
5087
4794
  The field the survey drives is the largest response the probe found, so its reach
@@ -5109,33 +4816,21 @@ records that, so a slider-applied animation is measured in its slider frames onl
5109
4816
  ยง4.11 opens on the triple: a deform timeline is the only one keyed on
5110
4817
  **skin / slot / attachment**. So the skin is not context around the key, it is a
5111
4818
  third of the key's own address โ€” and every pose `A39` and the `DEFORM` block take
5112
- is now taken with **that skin worn**
5113
- ([#583](https://github.com/firejune/rigc/issues/583)). You do not ask for it and
5114
- there is no flag: the skin comes out of the timeline.
5115
-
5116
- ๐Ÿšจ **It used to wear nothing at all**, which is `spine-core`'s own initial state
5117
- and the same one `check` reports as `no skin set (the default skin alone)`
5118
- (ยง9) โ€” every slot resolved through `SkeletonData.defaultSkin` and nothing else.
5119
- Move a deformed mesh into a named skin, which the format not only allows but keys
5120
- the timeline on, and the slot showed **no attachment**. This is what
5121
- `gallery/squash`'s ball printed with its one mesh moved into a skin `suit`, on
5122
- every one of its five keys, before #583:
5123
-
5124
- ```
5125
- DEFORM bounce suit/ball/ball key 1 t=0.340000 transform affine scale=[0.88, 1.16]
5126
- skipped A39 reads no winding off this key: the slot shows no attachment at all at this
5127
- time, not this mesh, so the runtime applies no deform to it here and draws
5128
- none of it โ€” a triangle that draws no pixels cannot draw them backwards
5129
- ```
5130
-
5131
- โ€” `A39` went **PASS โ†’ SKIP** on a rig whose only edit was which skin one mesh sat
5132
- in, in a sentence that reads as a verdict on that rig. Worn, the same build
5133
- reports every figure the default-skin one does, to the last digit, and `A39` gates
5134
- it: the two `DEFORM` blocks differ in the skin of the triple and in nothing else.
4819
+ is taken with **that skin worn**. You do not ask for it and there is no flag: the
4820
+ skin comes out of the timeline.
4821
+
4822
+ ๐Ÿšจ **Wearing no skin would measure a named-skin mesh as drawing nothing.** That is
4823
+ `spine-core`'s own initial state and the one `check` reports as
4824
+ `no skin set (the default skin alone)` (ยง9): every slot resolves through `SkeletonData.defaultSkin`
4825
+ and nothing else, so a deformed mesh in a named skin โ€” which the format not only
4826
+ allows but keys the timeline on โ€” would show **no attachment**. Worn, a build with
4827
+ its mesh in a named skin reports every figure the default-skin one does, to the
4828
+ last digit, and `A39` gates it: the two `DEFORM` blocks differ in the skin of the
4829
+ triple and in nothing else.
5135
4830
 
5136
4831
  **Two things follow, and one of them is not about art:**
5137
4832
 
5138
- - โš ๏ธ **A "nothing is drawn" sentence now names the skin it was measured in** โ€”
4833
+ - โš ๏ธ **A "nothing is drawn" sentence names the skin it was measured in** โ€”
5139
4834
  `the slot shows attachment "away" at this time, with skin "suit" worn, not this
5140
4835
  mesh โ€ฆ`. It is on the *shows-something-else* branch only, because that is the
5141
4836
  branch a skin decides; an alpha is read off the pose and has no skin in it. The
@@ -5145,12 +4840,8 @@ it: the two `DEFORM` blocks differ in the skin of the triple and in nothing else
5145
4840
  `Skeleton.setSkin` calls `updateCache`, which leaves a `skinRequired` bone
5146
4841
  inactive and a `skinRequired` constraint out of the update cache under any skin
5147
4842
  that does not list it. So a **slider** dressed into the same skin as the mesh
5148
- it drives used to be measured with itself switched off, and failed in two ways
5149
- depending on its `local` flag: a world-read property never moved, so the frame
5150
- was reported as *"played on a track"* โ€” an animation a slider is the only way
5151
- into (ยง4.11.4) โ€” while a local-read one kept its mapping but never left
5152
- `SliderPose.time` 0, so every key came back as *"at a time no dial selects"*.
5153
- Both now reach their own key times.
4843
+ it drives is switched on while that mesh is measured, and reaches its own key
4844
+ times (ยง4.11.4).
5154
4845
 
5155
4846
  โ›” **What it does not do is try every skin.** `Attachment.timelineSlots` lets one
5156
4847
  deform reach a second slot โ€” a linked mesh with `inheritTimelines` โ€” and that copy
@@ -5209,23 +4900,19 @@ so `v` is three numbers and not one.
5209
4900
  constraint on from an animation is the idiom (ยง4.10), so `A36`/`A37` only object to
5210
4901
  all-zero mixes when **no** animation keys that constraint's `mix` **above 0**. If
5211
4902
  you mute one at setup, key it up somewhere. `A23` asks the same question of a
5212
- physics constraint ([#743](https://github.com/firejune/rigc/issues/743)), and the
5213
- three share one reading ([#752](https://github.com/firejune/rigc/issues/752)): the
4903
+ physics constraint, and the three share one reading: the
5214
4904
  key **values** the loaded timeline poses, so a `mix` timeline keying 0 only is not a
5215
4905
  rescue, and a Bezier between two keys of 0 whose handles lie above 0 is. A path
5216
4906
  constraint is switched on by a key posing **any one** of its three mixes above 0,
5217
4907
  because `PathConstraint.update` returns only when all three are 0. [measured] on
5218
4908
  generated fixtures, a path constraint and a slider muted at rest and keyed to 0
5219
- only pose every bone exactly where the same rig with no mix timeline does, and until
5220
- #752 both passed. `A23` alone also counts the unnamed global timeline for every
4909
+ only pose every bone exactly where the same rig with no mix timeline does. `A23` alone also counts the unnamed global timeline for every
5221
4910
  constraint declaring `mixGlobal`, since only the physics family has one. The refusal
5222
4911
  says both halves โ€” `path constraint "P" has mixRotate 0, mixX 0 and mixY 0 at setup
5223
4912
  and none of the 2 animations keys its mix above 0; โ€ฆ` โ€” and names both repairs.
5224
4913
 
5225
- โš ๏ธ **The same question of an `ik` and a `transform` constraint is `A47` and `A48`**
5226
- ([#765](https://github.com/firejune/rigc/issues/765)); until then no assertion asked
5227
- it, and a rig resting either kind muted with nothing keying it gated green with 0
5228
- failures. [measured] on generated fixtures, an ik at `mix` 0 and a transform at
4914
+ โš ๏ธ **The same question of an `ik` and a `transform` constraint is `A47` and `A48`.**
4915
+ [measured] on generated fixtures, an ik at `mix` 0 and a transform at
5229
4916
  every mix 0, each with nothing keying it and each keyed to 0 only, pose every bone
5230
4917
  exactly where the same rig with no constraint does. They read the timelines through
5231
4918
  the same helper as `A23`/`A36`/`A37`, so a lifted Bezier between two keys of 0 is a
@@ -5253,11 +4940,9 @@ its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "reac
5253
4940
  "type": "ik", "why": โ€ฆ }]`. A rig resting at 0 and keyed up by the animation that
5254
4941
  needs it โ€” spineboy's aim โ€” is refused by neither.
5255
4942
 
5256
- ๐ŸŽ›๏ธ **The third door is for a dial the file cannot show turning**
5257
- ([#784](https://github.com/firejune/rigc/issues/784)). A constraint resting muted
5258
- that nothing keys up is either a leftover or a mix a game sets from code, and the two
5259
- export as the same bytes โ€” a production skeleton's rebuild was refused for exactly
5260
- that, over an ik its game switches on at runtime. `invariants.consumerDrivenMix`
4943
+ ๐ŸŽ›๏ธ **The third door is for a dial the file cannot show turning.** A constraint
4944
+ resting muted that nothing keys up is either a leftover or a mix a game sets from
4945
+ code, and the two export as the same bytes. `invariants.consumerDrivenMix`
5261
4946
  (ยง3.7) is the statement the file cannot make, and what it buys is a **SKIP by name,
5262
4947
  never a pass**: the file still shows nothing moving the constraint.
5263
4948
 
@@ -5369,16 +5054,14 @@ first is a **shape** refusal from a spec parser โ€” `parseRigSpec`
5369
5054
  compiled, and ask only what the one file in front of them can answer. Every
5370
5055
  `parseMotionSpec` refusal reads `<file>: \`<key path>\` is <what it actually is>;
5371
5056
  <the spelling that works>`, so its whole wave is **one row per FIELD** rather than
5372
- one per message. (The rig spec's parser predates the convention and its messages
5373
- are prose, so they sit in the second table with everything else.)
5057
+ one per message. (The rig spec parser's messages are prose, so they sit in the
5058
+ second table with everything else.)
5374
5059
 
5375
5060
  ๐Ÿšจ **A key neither format has is refused by name, in both files.** Not a row in
5376
5061
  the table below, because it is not about one field: every object in a rig spec and
5377
5062
  in a motion spec is checked against the keys its shape actually owns, and a key
5378
- outside that set stops the build. Issue #545 is why โ€” before it, such a key was
5379
- never looked at, never mentioned and never emitted, and the build exited 0 with
5380
- every assertion green. Four keys planted into one physics constraint all vanished,
5381
- and no line of output named any of them.
5063
+ outside that set stops the build, because nothing else reads such a key: left
5064
+ alone it would be dropped from the emitted skeleton with every assertion green.
5382
5065
 
5383
5066
  ```
5384
5067
  rigc compile error: rig.json: constraint "ctl" (physics) has 4 keys this compiler
@@ -5414,11 +5097,7 @@ nothing and the required-key message below it is what arrives โ€” `a rig spec ne
5414
5097
  **"does not read"** means the key *is* in your file, spelt some other way, and
5415
5098
  **"needs a โ€ฆ"** / **"is absent"** means it is not in the file at all. Where both
5416
5099
  are true at once โ€” a typo beside a key that really is missing โ€” the typo is named
5417
- first, because it is the one you can fix without re-reading the file. Until
5418
- [#672](https://github.com/firejune/rigc/issues/672) the rig spec's required-key
5419
- checks ran *above* its scan, so `"slot"` for `"slots"` printed the sentence asking
5420
- for an array the file already had, one character off, and never named the stray
5421
- key; the motion spec has always had this order.
5100
+ first, because it is the one you can fix without re-reading the file.
5422
5101
 
5423
5102
  โš ๏ธ One check stays above the scan in both parsers: the `spec` version tag. The
5424
5103
  version is what decides *which* key set applies, so a file declaring a version this
@@ -5434,49 +5113,48 @@ one is listed: a rig spec's root, a motion spec's root, an animation, and a
5434
5113
  the compiler tolerates is a key it cannot distinguish from one you meant it to
5435
5114
  read. (The **cut manifest** is deliberately outside this: it is the record of the
5436
5115
  pipeline that produced the art as much as an input, it carries fields the compiler
5437
- states outright that it does not read โ€” `roi` โ€” and the fixtures in this repository
5438
- already give it `note` and `archetype`. It has no shape parse at all, which is the
5439
- same hole issue #307 closed for the motion spec.)
5116
+ states outright that it does not read โ€” `roi` โ€” and it may carry `note` and
5117
+ `archetype` as well. It has no shape parse at all.)
5440
5118
 
5441
5119
  | Key | Refused when it is not | Why the shape matters |
5442
5120
  | --- | --- | --- |
5443
5121
  | the file itself | a JSON object | the version row below would otherwise report a missing `spec` tag in a file that has no fields at all |
5444
5122
  | `spec` | `"rigc-motion/1"` | ยง4 |
5445
- | `archetype`, `cut` | a non-empty string | `cut` was read by nothing, so a spec that omitted it compiled green and could not say what it was authored for |
5123
+ | `archetype`, `cut` | a non-empty string | a spec with no `cut` cannot say what it was authored for |
5446
5124
  | `note` (at any level) | a string | โ€” |
5447
- | `easings` | an object | it is declared required and nothing asserted it, so an absent table failed at the first key that named an easing instead of at the table |
5448
- | `easings."<name>"` | **four finite numbers** | `bezierForChannel` destructures four handles with no guard: `[0.42, 0, 0.58]` emitted `"curve": [0.42, 0, 0.58, null]` (ยง4.1) |
5449
- | `groups` | an object keyed by group name | an array compiled green with no group defined. Each group's member LIST stays a compile-time refusal โ€” see the rows further down |
5450
- | `setup` | an object keyed by slot name | every `setup?.[slot]` lookup on an array is `undefined`, so the whole table was silently absent |
5451
- | `setup."<slot>"` | an object of `{ attachment?, color? }` | ยง4.2 โ€” `"<slot>": "plate"` reads `.attachment` off a string as `undefined` and **hides the slot** (#293) |
5125
+ | `easings` | an object | an absent table is refused at the table, not at the first key that names an easing |
5126
+ | `easings."<name>"` | **four finite numbers** | `bezierForChannel` destructures four handles with no guard: `[0.42, 0, 0.58]` would emit `"curve": [0.42, 0, 0.58, null]` (ยง4.1) |
5127
+ | `groups` | an object keyed by group name | an array would compile green with no group defined. Each group's member LIST stays a compile-time refusal โ€” see the rows further down |
5128
+ | `setup` | an object keyed by slot name | every `setup?.[slot]` lookup on an array is `undefined`, so the whole table would be silently absent |
5129
+ | `setup."<slot>"` | an object of `{ attachment?, color? }` | ยง4.2 โ€” `"<slot>": "plate"` reads `.attachment` off a string as `undefined` and **hides the slot** |
5452
5130
  | `setup."<slot>".attachment` | a string or `null` | whether a string RESOLVES is still the compile-time row below; whether it is a string is this one |
5453
5131
  | `setup."<slot>".color[i]` | a finite number in 0..1 | `channelHex` clamps with `Math.min`/`Math.max`, which pass `NaN` through, and `NaN.toString(16)` is the text `"NaN"` |
5454
5132
  | `physics` | an object keyed by constraint name | as `setup` |
5455
5133
  | `physics."<name>".bone` | a non-empty string | โ€” |
5456
5134
  | `physics."<name>".<tuning field>` | a finite number | rounded to `NaN` and emitted as `null`, which the runtime reads as zero (ยง4.6) |
5457
5135
  | `mix`, `mix.default`, `mix.pairs[i]` | an object / a number / a `["<from>", "<to>", <seconds>]` triple | not emitted into skeleton JSON, so nothing else ever looks at it (ยง4.6) |
5458
- | `animations` | an object keyed by animation name | an absent one crashed with a raw `TypeError` that named neither input file; an array compiled green with no animations |
5459
- | `animations."<a>"` | an object | crashed with a raw `TypeError` on `anim.tracks` |
5460
- | `animations."<a>".duration` | a finite number โ‰ฅ 0 | R7's check is `Math.abs(compiled โˆ’ declared) > FRAME`, and a comparison against `NaN` is **false** โ€” so the one guard on the field passed hardest exactly when the field was missing |
5136
+ | `animations` | an object keyed by animation name | an absent one would crash with a raw `TypeError` that names neither input file; an array would compile green with no animations |
5137
+ | `animations."<a>"` | an object | it would crash with a raw `TypeError` on `anim.tracks` |
5138
+ | `animations."<a>".duration` | a finite number โ‰ฅ 0 | R7's check is `Math.abs(compiled โˆ’ declared) > FRAME`, and a comparison against `NaN` is **false** โ€” so the one guard on the field would pass hardest exactly when the field is missing |
5461
5139
  | `animations."<a>".loop` | `true` or `false` | optional; absent means the player decides (ยง4.3) |
5462
5140
  | `animations."<a>".tracks` | an array | an animation whose timelines are all in the families beside `tracks` still writes `"tracks": []` |
5463
5141
  | `โ€ฆtracks[i]` | an object | โ€” |
5464
5142
  | `โ€ฆtracks[i].property` | a non-empty string | which properties exist is the compile-time row below (ยง4.4) |
5465
5143
  | `โ€ฆtracks[i].slot`/`.group`/`.bone`/`.physics`/`.path`/`.slider` | a string | whether exactly one is named, and what it resolves to, are compile-time rows below |
5466
- | `โ€ฆtracks[i].lag`, `.stagger` | a finite number | a string is CONCATENATED onto each key time and a boolean adds 1s, and the resulting refusal blamed the key and the duration for a fault in neither |
5144
+ | `โ€ฆtracks[i].lag`, `.stagger` | a finite number | a string is CONCATENATED onto each key time and a boolean adds 1s, and the resulting refusal would blame the key and the duration for a fault in neither |
5467
5145
  | `โ€ฆkeys[j]` | an object | โ€” |
5468
- | `โ€ฆkeys[j].t` | a finite number | **every** key family, including the three that never had this guard: value tracks, slot tracks and `drawOrder` |
5146
+ | `โ€ฆkeys[j].t` | a finite number | **every** key family, value tracks, slot tracks and `drawOrder` included |
5469
5147
  | `โ€ฆkeys[j].ease` | a string | โ€” |
5470
- | `โ€ฆdrawOrder`, `โ€ฆevents`, `โ€ฆik`, `โ€ฆtransform`, `โ€ฆdeform` | an array | an object was refused as `no keys`, which is what an EMPTY array says |
5471
- | `โ€ฆdrawOrder[i].offsets` | an array | a key with no offsets is the format's own "back to the setup draw order", and the test for it was true for `{}` โ€” a complete statement of the draw order, made by accident |
5148
+ | `โ€ฆdrawOrder`, `โ€ฆevents`, `โ€ฆik`, `โ€ฆtransform`, `โ€ฆdeform` | an array | an object would be refused as `no keys`, which is what an EMPTY array says |
5149
+ | `โ€ฆdrawOrder[i].offsets` | an array | a key with no offsets is the format's own "back to the setup draw order", and the test for it is true for `{}` โ€” a complete statement of the draw order, made by accident |
5472
5150
  | `โ€ฆdrawOrder[i].offsets[j]` | `{ slot: string, offset: number }` | whether the slot is emitted, and whether the offset is whole and lands inside the array, are compile-time rows below |
5473
- | `โ€ฆik[i]`, `โ€ฆtransform[i]`, `โ€ฆdeform[i]` | an object | `null` in one of these lists crashed with a raw `TypeError` on `track.constraint` / `track.skin` |
5151
+ | `โ€ฆik[i]`, `โ€ฆtransform[i]`, `โ€ฆdeform[i]` | an object | `null` in one of these lists would crash with a raw `TypeError` on `track.constraint` / `track.skin` |
5474
5152
  | `โ€ฆik[i].constraint`, `โ€ฆtransform[i].constraint` | a non-empty string | 4.3 writes the group as `ik.<constraint>`, so the name is the only target there is |
5475
5153
  | `โ€ฆdeform[i].slot`, `.attachment`, `.skin` | a string (`skin` optional) | โ€” |
5476
5154
  | `โ€ฆsequence` | an array | ยง4.13 โ€” one entry per skin/slot/attachment triple |
5477
5155
  | `โ€ฆsequence[i].slot`, `.attachment`, `.skin` | a string (`skin` optional) | โ€” |
5478
5156
  | `โ€ฆsequence[i].keys[j].mode` | one of the seven โ€” `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` | `SequenceMode[mode]` is `undefined` for anything else and the mode bits store 0: the key loads without a word and plays as `hold` |
5479
- | `โ€ฆsequence[i].keys[j].index` | a whole number โ‰ฅ 0 | stored as `index << 4`, which truncates a fraction (1.5 showed frame 1). Whether it is inside the series' `count` is the compile-time row below |
5157
+ | `โ€ฆsequence[i].keys[j].index` | a whole number โ‰ฅ 0 | stored as `index << 4`, which truncates a fraction (1.5 shows frame 1). Whether it is inside the series' `count` is the compile-time row below |
5480
5158
  | `โ€ฆsequence[i].keys[j].delay` | a finite number โ‰ฅ 0 โ€” and **above 0 wherever the mode advances**, counting a delay carried from the key before | `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so a `loop` at delay 0 shows its first frame throughout. The message reads `` `โ€ฆkeys[j]` plays "loop" at a delay of 0 โ€ฆ which is "hold" spelt as "loop" `` |
5481
5159
 
5482
5160
  The second wave is everything that needed the **other** file, the property table
@@ -5489,13 +5167,13 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5489
5167
  | `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 |
5490
5168
  | `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
5491
5169
  | `two bones are called "X"` | bone names are the join key; rename one |
5492
- | `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 and never was one to fix |
5170
+ | `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 |
5493
5171
  | `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | ยง4.6 โ€” the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
5494
- | `skin "S" lists "X" under "transform", but the rig declares it as a "ik" constraint โ€” a skin looks its constraints up by name AND type, so this one is a miss and the loader throws` | ยง3.4.1 โ€” move the name to the list named after the constraint's own kind. A name the rig declares under **no** kind is the other miss and says so (`skin "S" activates ik constraint "X", which this rig does not declare`). โš ๏ธ A name listed by **several skins** is not an error and no longer was one as of [#725](https://github.com/firejune/rigc/issues/725): the lists are per-skin sets and a bone or constraint in two of them is active under either |
5172
+ | `skin "S" lists "X" under "transform", but the rig declares it as a "ik" constraint โ€” a skin looks its constraints up by name AND type, so this one is a miss and the loader throws` | ยง3.4.1 โ€” move the name to the list named after the constraint's own kind. A name the rig declares under **no** kind is the other miss and says so (`skin "S" activates ik constraint "X", which this rig does not declare`). โš ๏ธ A name listed by **several skins** is not an error: the lists are per-skin sets and a bone or constraint in two of them is active under either |
5495
5173
  | `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
5496
5174
  | `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 โ€” pick one file and declare it there. A slot **nothing** fills is exempt: its setup pose can only be "show nothing" and is not asked for |
5497
5175
  | `the setup pose shows attachment "A" on slot "X", which no skin and no manifest part fills` | ยง3.3 โ€” the slot is emitted empty and nothing was ever going to fill it, so `A` resolves to nothing. Give the slot an attachment (a skin entry or a manifest part), or state the setup pose as `null` |
5498
- | `the setup pose shows attachment "A" on slot "X", and the slot is emitted empty because the one manifest state that fills it has no art: "s" (no PNG at p)` | ยง0.2 โ€” a **different** fault with a similar shape: a manifest part does fill this slot and its art was not where the manifest pointed, so every state of it was dropped. **Restore the file or fix the path** โ€” posing `null` compiles, but it ships the rig with that part missing and the gate green. The path printed is the one rigc tried, which is what tells a deleted file from a misspelt `states:` entry; `all N manifest states that fill it have no art` is the same message where the part has more than one. Under `--atlas-in` the same sentence reads `no region "R" in <atlas>` and asks you to add the region to the pack โ€” no file was opened ([#671](https://github.com/firejune/rigc/issues/671)) |
5176
+ | `the setup pose shows attachment "A" on slot "X", and the slot is emitted empty because the one manifest state that fills it has no art: "s" (no PNG at p)` | ยง0.2 โ€” a **different** fault with a similar shape: a manifest part does fill this slot and its art was not where the manifest pointed, so every state of it was dropped. **Restore the file or fix the path** โ€” posing `null` compiles, but it ships the rig with that part missing and the gate green. The path printed is the one rigc tried, which is what tells a deleted file from a misspelt `states:` entry; `all N manifest states that fill it have no art` is the same message where the part has more than one. Under `--atlas-in` the same sentence reads `no region "R" in <atlas>` and asks you to add the region to the pack โ€” no file was opened |
5499
5177
  | `a region needs width and height โ€” give them, or give an "image" and rigc will measure the PNG` | add `image`, or both sizes |
5500
5178
  | `a mesh needs width and height โ€” give them, or give an "image" and rigc will measure the PNG` | ยง3.4 โ€” the same rule for a mesh |
5501
5179
  | `"type" is null, which is not a name. An attachment's type is one of region, mesh, linkedmesh, โ€ฆ or the key is absent and reads as "region"` | ยง6 โ€” **remove the key**. Absent is the format's own default; present-and-null matches no parser case and the attachment is dropped in silence |
@@ -5505,7 +5183,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5505
5183
  | `a linked mesh states "uvs", "triangles", โ€ฆ, and a linked mesh has no geometry of its own` | ยง3.4 โ€” remove them, or remove `source` and author this as a mesh. The parser returns before `readVertices`, so those keys are read by nothing at all |
5506
5184
  | `"source" is "X", and skin "S" โ€ฆ slot "L" โ€ฆ holds 2: "a", "b"` | ยง3.4 โ€” `source` is the PLACEHOLDER the source is filed under, not its `name`. A clause after the skin and after the slot says whether each was stated or taken from the parser's default โ€” **the default skin** and **this attachment's own slot**, which is the pair that surprises |
5507
5185
  | `"slot" is "X", which the rig does not declare as a slot` / `"skin" is "X", โ€ฆ the rig declares no such skin` | ยง3.4 โ€” a link resolves both by name. Left to the round trip these are the runtime's `Source mesh slot not found` and `Skin not found`, which name neither the attachment nor where it looked |
5508
- | `no "skin" is stated, so the parser looks for the source "X" in the default skin โ€” and this rig declares no default skin` | ยง3.4 โ€” state the `skin` the source is filed under. A link with no `skin` resolves through `SkeletonData.defaultSkin`, which is null in a file with no default skin, and the runtime reads `getAttachment` off it with a `TypeError` naming nothing. Before [#801](https://github.com/firejune/rigc/issues/801) every build carried an empty `default`, so the same link failed at the source instead |
5186
+ | `no "skin" is stated, so the parser looks for the source "X" in the default skin โ€” and this rig declares no default skin` | ยง3.4 โ€” state the `skin` the source is filed under. A link with no `skin` resolves through `SkeletonData.defaultSkin`, which is null in a file with no default skin, and the runtime reads `getAttachment` off it with a `TypeError` naming nothing |
5509
5187
  | `"source" is "X", which is itself a linked mesh, and a chain of them is refused` | ยง3.4 โ€” point `source` at the mesh. A chain resolves in file order and loads nothing at all in one of the two orders, silently |
5510
5188
  | `"source" is "X", which is a "region" attachment and not a mesh` | ยง3.4 โ€” a link takes another MESH's geometry; off any other type the runtime reads `undefined` and says nothing |
5511
5189
  | `โ€ฆ is a boundingbox and states a "sequence". A sequence is a numbered series of atlas regions, and only the 3 kinds that draw a region carry one โ€” region, mesh, linkedmesh โ€ฆ` | ยง3.4.3 โ€” put the series on a region or a mesh, or remove it |
@@ -5523,26 +5201,26 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5523
5201
  | `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` |
5524
5202
  | `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 |
5525
5203
  | `image "X.png" is not on disk at โ€ฆ` | fix the name, or point `--images` at the right directory |
5526
- | `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, [#732](https://github.com/firejune/rigc/issues/732)) |
5527
- | `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, [#750](https://github.com/firejune/rigc/issues/750)) |
5204
+ | `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) |
5205
+ | `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) |
5528
5206
  | `--atlas-in <pack>.atlas: N of its M page(s) cannot be read as PNG, and nothing was compiled against the pack โ€” page "p.png": โ€ฆ` | the same, for every page of the pack at once, before the compile: re-export each named page as PNG under the name the atlas gives it (ยง0.2) |
5529
5207
  | `parts/iris_open.png is 96x64 but slot "iris" declares 96x60` | R5 โ€” a manifest `states:` entry whose art is not the window the part declares. Re-export the PNG, or fix the part's `size`; a quad sized against art of another size is the silence `A06` exists for, and the window is what the quad is built from |
5530
5208
  | `plates/00_stage.png is 256x256 but the manifest window for "stage" is 250x256` | R5 โ€” the same check on the part's unconditional `image`, against the window the crop gives it |
5531
- | `region "00_stage" of <pack>.atlas (declared 256x256 by its offsets) is 256x256 but the manifest window for "stage" is 250x256` | R5 โ€” the row above under `--atlas-in`, and the prefix is the whole point: it says which of the two rigc **measured**, because the remedy differs. A bare path is a loose PNG it opened and you re-export; a `region โ€ฆ of <pack>` was read out of the pack, and you repack or aim the part at another region. This is the size row of ยง0.2's four, with the message it actually prints |
5209
+ | `region "00_stage" of <pack>.atlas (declared 256x256 by its offsets) is 256x256 but the manifest window for "stage" is 250x256` | R5 โ€” the row above under `--atlas-in`, and the prefix is the whole point: it says which of the two rigc **measured**, because the remedy differs. A bare path is a loose PNG it opened and you re-export; a `region โ€ฆ of <pack>` was read out of the pack, and you repack or aim the part at another region. This is the size row of ยง0.2's table, with the message it actually prints |
5532
5210
  | `duplicate region name "X"` | two PNGs share a basename; one part, one page, one name |
5533
5211
  | `"b/X.png" and the art already atlased as region "X" are two different files โ€” โ€ฆ Rename one of the PNGs.` | R5 โ€” the region name is the basename, so only one of the two can hold it. Rename a file (not a placeholder: the placeholder is free to repeat) |
5534
5212
  | `the image "X.png" was never added to the atlas, so there is no region "X" โ€ฆ` | nothing in the spec โ€” every image an attachment names is measured and atlased, one per file whichever skin names it, so this says rigc skipped one. Report it on [the tracker](https://github.com/firejune/rigc/issues). The `#555` the message itself cites is the change that made one-per-file an invariant, not a place to file against |
5535
5213
  | `motion spec names archetype "A" but the rig spec at โ€ฆ is called "B"` | make `archetype` equal the rig's `name` |
5536
5214
  | `animation "A" declares duration Ns but its last key is at Ms` | R7 โ€” fix whichever of the two you meant |
5537
5215
  | `animation "A" slot "X" attachment: key at Ns is Ms past the declared duration Ds` | ยง4.5 โ€” the key is past the end of the animation and nothing will sample it. Move the key onto `duration`, or raise `duration` |
5538
- | `animation "A" slot "X" attachment: attachment "N" is not in slot "X" under any skin (searched: default, alt) โ€” the slot has: plain, trim` | ยง4.4 โ€” the keyed name is in **no** skin, and the two clauses say where the compiler looked and what it would have taken. Fix the spelling, or give some skin a placeholder called `N`. A name only a NAMED skin fills is not this error and never was one to fix โ€” it compiles, and the slot shows nothing under the skins that lack it. Before [#695](https://github.com/firejune/rigc/issues/695) the message read `attachment "N" is not in slot "X"` and was raised against the **default skin alone**, so it fired on correct rigs: any key into named-skin art, and every key in a rig with no default skin. `the slot has no attachments at all` is the same message where nothing fills the slot |
5216
+ | `animation "A" slot "X" attachment: attachment "N" is not in slot "X" under any skin (searched: default, alt) โ€” the slot has: plain, trim` | ยง4.4 โ€” the keyed name is in **no** skin, and the two clauses say where the compiler looked and what it would have taken. Fix the spelling, or give some skin a placeholder called `N`. A name only a NAMED skin fills is not this error โ€” it compiles, and the slot shows nothing under the skins that lack it. `the slot has no attachments at all` is the same message where nothing fills the slot |
5539
5217
  | `animation "A" keys unknown bone "X"` | the track's `bone` is not in the rig |
5540
5218
  | `animation "A" keys physics "*" P, the timeline that names no constraint and drives every physics constraint declaring "PGlobal": true, and none of "C", โ€ฆ does โ€” it would parse and move nothing` | ยง4.4 โ€” set `"PGlobal": true` on the constraints the track is for (ยง3.5), or key one by name. For `reset` it reads *every physics constraint the rig has*, and fires only on a rig with none |
5541
5219
  | `` `animations."A".tracks[i].physics` is the string ""; the empty name is how a skeleton file spells a physics timeline that names no constraint, and a motion spec spells that "*" โ€ฆ `` | ยง4.4 โ€” name one constraint, or write `"*"` |
5542
5220
  | `physics constraint "*": the name is reserved โ€” โ€ฆ` / `` `physics."*"` names a physics constraint "*", and that name is reserved โ€ฆ `` | ยง3.5 โ€” `"*"` is the target of the timeline that names no constraint; rename the constraint |
5543
5221
  | `animation "A": group "G" lists "*", which is not a constraint but the target that names none โ€ฆ` | ยง4.4 โ€” write `"*"` as the track's `physics` field |
5544
5222
  | `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (ยง4.4) |
5545
- | `animation "A" physics constraint "C" mass key at t=โ€ฆ is 0 (massInverse Infinity); must be > 0 โ€” โ€ฆ` | ยง4.4 โ€” a keyed physics value outside its row's bound. The message names the bound and then its basis ([#798](https://github.com/firejune/rigc/issues/798)): where the runtime's arithmetic fails, the expression and its lines (`mass` at 0 is the one here, and `damping` below 0); where the value runs and runs wrongly, that refusing it is rigc's call and what the value does. The bounds: `mass` is `> 0`, `damping` is inside `[0, 1]`, both ends included ([#794](https://github.com/firejune/rigc/issues/794)), `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)). โš ๏ธ Those are the bounds a **key** is held to. A setup `strength` of `0` is refused too, but by `A23` rather than here, and with its own sentence โ€” `physics "C" has strength 0; nothing pulls it back` ([#727](https://github.com/firejune/rigc/issues/727)); a setup `strength` below 0 says the offset is pushed away and grows, the same arm this message quotes for a negative key ([#748](https://github.com/firejune/rigc/issues/748)). A `damping` key's sentence names the exponent `60 * step` and why a negative is NaN at any `fps` where `60 / fps` is not whole (ยง4.4) |
5223
+ | `animation "A" physics constraint "C" mass key at t=โ€ฆ is 0 (massInverse Infinity); must be > 0 โ€” โ€ฆ` | ยง4.4 โ€” a keyed physics value outside its row's bound. The message names the bound and then its basis: where the runtime's arithmetic fails, the expression and its lines (`mass` at 0 is the one here, and `damping` below 0); where the value runs and runs wrongly, that refusing it is rigc's call and what the value does. The bounds: `mass` is `> 0`, `damping` is inside `[0, 1]`, both ends included, `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere. โš ๏ธ Those are the bounds a **key** is held to. A setup `strength` of `0` is refused too, but by `A23` rather than here, and with its own sentence โ€” `physics "C" has strength 0; nothing pulls it back`; a setup `strength` below 0 says the offset is pushed away and grows, the same arm this message quotes for a negative key. A `damping` key's sentence names the exponent `60 * step` and why a negative is NaN at any `fps` where `60 / fps` is not whole (ยง4.4) |
5546
5224
  | `a key carries both a named easing and a raw curve; pick one` | R6 |
5547
5225
  | `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
5548
5226
  | `key times must strictly increase (at t=โ€ฆ)` | including after `lag` and `stagger` |
@@ -5604,19 +5282,19 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5604
5282
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | ยง4.12 โ€” put the name in `"path"` |
5605
5283
  | `rgba value needs 4 channels, got 3` | ยง4.4 โ€” an `rgba` key is `[r, g, b, a]`. It names no animation, slot or key time, and the only input that reaches it is a slot `rgba` key: the setup pose's `color` is refused earlier, by its own row, with the slot named |
5606
5284
  | `rgba2 value needs 7 channels, got 6` | ยง4.4 โ€” an `rgba2` key is `[lr, lg, lb, la, dr, dg, db]`: the light colour with its alpha, then the dark colour **without** one. Six is the commonest way to get it wrong, because the dark half looks like it should take an alpha too โ€” the format has no channel for it, and neither does the runtime's `setFrame`. Like the row above it names no animation or key time; the only input that reaches it is a slot `rgba2` key |
5607
- | `rgb value needs 3 channels, got 4` ยท `alpha value needs 1 channel, got 2` ยท `rgb2 value needs 6 channels, got 7` | ยง4.4 โ€” the separable shapes, refused in the words the two above use: an `rgb` key is `[r, g, b]`, an `alpha` key is `[a]`, an `rgb2` key is `[lr, lg, lb, dr, dg, db]`. Four channels on an `rgb` track is the likeliest way to get it wrong โ€” the `rgba` spelling on the timeline that exists to leave the alpha alone โ€” and seven on `rgb2` is the `rgba2` spelling. [#730](https://github.com/firejune/rigc/issues/730) |
5285
+ | `rgb value needs 3 channels, got 4` ยท `alpha value needs 1 channel, got 2` ยท `rgb2 value needs 6 channels, got 7` | ยง4.4 โ€” the separable shapes, refused in the words the two above use: an `rgb` key is `[r, g, b]`, an `alpha` key is `[a]`, an `rgb2` key is `[lr, lg, lb, dr, dg, db]`. Four channels on an `rgb` track is the likeliest way to get it wrong โ€” the `rgba` spelling on the timeline that exists to leave the alpha alone โ€” and seven on `rgb2` is the `rgba2` spelling |
5608
5286
  | `animation "A" slot "X" alpha: alpha key value must be [a]` | ยง4.4 โ€” an alpha key's `v` is a one-element array like every other one-channel track, even though the file writes it bare (`{ time, value }`). The same row exists for each colour shape with its own spelling (`rgb key value must be [r,g,b]`, โ€ฆ) |
5609
5287
  | `animation "A" slot "X" alpha: key at t=T is V; an alpha is a number from 0 to 1 โ€ฆ` | ยง4.4 โ€” the one colour key stored as a number, so the one nothing clamps on the way out. The runtime clamps the posed alpha only after interpolating, so a key outside 0..1 bends the curve toward a value no pose holds; state the value you mean. 0 and 1 themselves are taken |
5610
5288
  | `animation "A" slot "X": tracks "P" and "Q" both key the slot's alpha โ€” a colour timeline poses its channels at every time โ€ฆ` | ยง4.4 โ€” two colour tracks of one slot that pose a shared channel (`rgba` + `alpha`, `rgba` + `rgb`, `rgba2` + `rgb2`, โ€ฆ). Each poses its channels at every time, its setup value before its first key included, so the later in the file overwrites the other everywhere and one of them is read by nothing. Key each channel once: `rgb` and `alpha` for two halves on their own key times, `rgba` for both together. The channel named is whichever the two share โ€” `light rgb`, `alpha`, `dark colour` |
5611
- | `animation "A" bone "B" has no timeline "P" (it has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate, inherit)` | ยง4.4 โ€” a bone has exactly eleven timelines and `P` is none of them. Fix the spelling โ€” the single-axis ones are lower-case (`translatex`, not `translateX`). A **constraint** property is refused first, by its own row, naming the field its constraint's name goes in. When `P` is a slot timeline the message says so and where to put the name: `. "rgba" is a slot timeline โ€” put the name in "slot"`. Before [#656](https://github.com/firejune/rigc/issues/656) all of them read `bone "B" cannot take slot property "P"`, which named the slot family whatever you had written and listed nothing |
5612
- | `animation "A" bone "B" inherit key at t=T carries an easing ("E"), and this timeline is stepped by the format โ€” its reader builds no curve, so the mode changes AT the key and holds until the next one. Remove it` (and `โ€ฆ carries a curve, โ€ฆ` for a raw `curve`) | ยง4.4 โ€” an `inherit` key is a mode, not a value between two others: the parser's `inherit` branch reads `time` and `inherit` and nothing else, so an easing would be written into a key nobody reads (`A05_CURVE_ARRAY_LENGTH` refuses the same `curve` on a file rigc did not write). `"stepped"` is refused too, for the same reason โ€” the timeline already is. Delete the `ease` or `curve` ([#733](https://github.com/firejune/rigc/issues/733)) |
5613
- | `animation "A" bone "B" inherit key at t=T names mode "M"; known: normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection โ€” the runtime folds the case of the first letter and of nothing else` | ยง4.4 โ€” `M` is not one of the five. The list and the rule are the setup field's (ยง3.2) โ€” one resolver reads both โ€” so `NoScale` compiles (and is written `noScale`, the editor's spelling) while `NOSCALE` and `noscale` do not: the runtime would load either as **no mode**, and the bone would keep whatever world transform it had ([#733](https://github.com/firejune/rigc/issues/733)) |
5614
- | `bone "B" has inherit "M"; known: normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection โ€” the runtime folds the case of the first letter and of nothing else` | ยง3.2 โ€” the setup half of the row above, and the same list. Before [#733](https://github.com/firejune/rigc/issues/733) this check was case-insensitive, which is wider than the runtime: `"NOSCALE"` compiled, gated green, and loaded as no mode, so the bone's world rotation, scale and shear were never computed โ€” every attachment on it collapsed to a point. Fix the spelling |
5615
- | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate, inherit; a slot group has: attachment, rgba, rgb, alpha, rgba2, rgb2; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | ยง4.3, ยง4.4 โ€” a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one. Before [#661](https://github.com/firejune/rigc/issues/661) a group of bones read `animation "A" targets unknown slot "M"` and a group of slots got the slot row below, naming one family out of three |
5616
- | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba, rgb, alpha, rgba2, rgb2)` | ยง4.4 โ€” a slot has exactly six timelines โ€” every one the format has โ€” and `P` is none of them. Fix the spelling; a bone or constraint property written on a slot track is refused by its own row instead. Before [#650](https://github.com/firejune/rigc/issues/650) every other name compiled as an **rgba** timeline called `P`, and what you saw was `A00_ROUNDTRIP_PARSE` on the emitted file โ€” or, for the one-channel spelling, `rgba value needs 4 channels, got 1` |
5617
- | `animation "A" slot "X" rgba2: slot "X" declares no setup "dark", and an "rgba2" timeline poses a slot's dark colour โ€ฆ` โ€” and the same with `rgb2` | ยง3.3, ยง4.4 โ€” the two-colour tint has a setup half and a keyed half, and the keyed half cannot exist without the other. `Slot`'s constructor allocates a dark colour only for a slot whose setup pose declares one, and `RGBA2Timeline` writes it unconditionally โ€” so without the `dark` the file loads, and the first `state.apply` throws `TypeError: null is not an object` in the consumer's process. Give the slot the `dark` it holds at rest, or key `rgba` (for `rgb2`, `rgb`) if only the light colour moves. `RGB2Timeline` was measured to throw the same way before `rgb2` joined the refusal ([#730](https://github.com/firejune/rigc/issues/730)). Raised before the keys are read, with the slot named, for the same reason the row above is |
5618
- | `N pair(s) of animation names have no one order: โ€ฆ "Fx/a" / "fx/b" (folder) โ€” "Fx/a" and "fx/b" sit in the sibling folders "Fx" and "fx", which the comparator leaves in one place โ€ฆ; rename one of the two folders so they differ by more than letter case, spacing or a leading zero` | **R10** โ€” rename until no pair is left. The kind in brackets says which of the three things the five stored round trips leave open decides the pair: `number` (two digit runs that are each one number written twice, pointing opposite ways), `separator` (a whitespace character that is not a space) or `folder` (two sibling folders the comparator cannot separate). rigc keys `animations` in the editor's own comparator, read off `fixtures/editor-order/probe{1..5}.{in,out}.json` ([#728](https://github.com/firejune/rigc/issues/728)) โ€” so a pair those files settle is emitted rather than refused, **including a pair that differs only in case**, whose order is then the one your spec declared. On the three that are left, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
5619
- | `N pair(s) of skin names have no one order: โ€ฆ "Fx/a" / "fx/b" (folder) โ€” โ€ฆ` | **R11** โ€” rename until no pair is left. The same shape and the same three kinds as the row above, because it is the same comparator: two of the five round trips carried one name list as both collections and both came back in one order ([#728](https://github.com/firejune/rigc/issues/728)). โš ๏ธ This row was **wider** than R10's until then โ€” `Zulu`/`mike` and `mike10`/`mike2` built as animation names and were refused as skin names ([#541](https://github.com/firejune/rigc/issues/541)) โ€” and both build now |
5289
+ | `animation "A" bone "B" has no timeline "P" (it has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate, inherit)` | ยง4.4 โ€” a bone has exactly eleven timelines and `P` is none of them. Fix the spelling โ€” the single-axis ones are lower-case (`translatex`, not `translateX`). A **constraint** property is refused first, by its own row, naming the field its constraint's name goes in. When `P` is a slot timeline the message says so and where to put the name: `. "rgba" is a slot timeline โ€” put the name in "slot"` |
5290
+ | `animation "A" bone "B" inherit key at t=T carries an easing ("E"), and this timeline is stepped by the format โ€” its reader builds no curve, so the mode changes AT the key and holds until the next one. Remove it` (and `โ€ฆ carries a curve, โ€ฆ` for a raw `curve`) | ยง4.4 โ€” an `inherit` key is a mode, not a value between two others: the parser's `inherit` branch reads `time` and `inherit` and nothing else, so an easing would be written into a key nobody reads (`A05_CURVE_ARRAY_LENGTH` refuses the same `curve` on a file rigc did not write). `"stepped"` is refused too, for the same reason โ€” the timeline already is. Delete the `ease` or `curve` |
5291
+ | `animation "A" bone "B" inherit key at t=T names mode "M"; known: normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection โ€” the runtime folds the case of the first letter and of nothing else` | ยง4.4 โ€” `M` is not one of the five. The list and the rule are the setup field's (ยง3.2) โ€” one resolver reads both โ€” so `NoScale` compiles (and is written `noScale`, the editor's spelling) while `NOSCALE` and `noscale` do not: the runtime would load either as **no mode**, and the bone would keep whatever world transform it had |
5292
+ | `bone "B" has inherit "M"; known: normal, onlyTranslation, noRotationOrReflection, noScale, noScaleOrReflection โ€” the runtime folds the case of the first letter and of nothing else` | ยง3.2 โ€” the setup half of the row above, and the same list. The check is exactly as wide as the runtime: `"NOSCALE"` would load as no mode, so the bone's world rotation, scale and shear would never be computed โ€” every attachment on it collapses to a point. Fix the spelling |
5293
+ | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate, inherit; a slot group has: attachment, rgba, rgb, alpha, rgba2, rgb2; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | ยง4.3, ยง4.4 โ€” a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one |
5294
+ | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba, rgb, alpha, rgba2, rgb2)` | ยง4.4 โ€” a slot has exactly six timelines โ€” every one the format has โ€” and `P` is none of them. Fix the spelling; a bone or constraint property written on a slot track is refused by its own row instead |
5295
+ | `animation "A" slot "X" rgba2: slot "X" declares no setup "dark", and an "rgba2" timeline poses a slot's dark colour โ€ฆ` โ€” and the same with `rgb2` | ยง3.3, ยง4.4 โ€” the two-colour tint has a setup half and a keyed half, and the keyed half cannot exist without the other. `Slot`'s constructor allocates a dark colour only for a slot whose setup pose declares one, and `RGBA2Timeline` writes it unconditionally โ€” so without the `dark` the file loads, and the first `state.apply` throws `TypeError: null is not an object` in the consumer's process. Give the slot the `dark` it holds at rest, or key `rgba` (for `rgb2`, `rgb`) if only the light colour moves. `RGB2Timeline` is measured to throw the same way. Raised before the keys are read, with the slot named, for the same reason the row above is |
5296
+ | `N pair(s) of animation names have no one order: โ€ฆ "Fx/a" / "fx/b" (folder) โ€” "Fx/a" and "fx/b" sit in the sibling folders "Fx" and "fx", which the comparator leaves in one place โ€ฆ; rename one of the two folders so they differ by more than letter case, spacing or a leading zero` | **R10** โ€” rename until no pair is left. The kind in brackets says which of the three things the five stored round trips leave open decides the pair: `number` (two digit runs that are each one number written twice, pointing opposite ways), `separator` (a whitespace character that is not a space) or `folder` (two sibling folders the comparator cannot separate). rigc keys `animations` in the editor's own comparator, read off `fixtures/editor-order/probe{1..5}.{in,out}.json` โ€” so a pair those files settle is emitted rather than refused, **including a pair that differs only in case**, whose order is then the one your spec declared. On the three that are left, the editor's re-key repoints every slider whose animation moves index |
5297
+ | `N pair(s) of skin names have no one order: โ€ฆ "Fx/a" / "fx/b" (folder) โ€” โ€ฆ` | **R11** โ€” rename until no pair is left. The same shape and the same three kinds as the row above, because it is the same comparator: two of the five round trips carried one name list as both collections and both came back in one order |
5620
5298
  | `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that โ€ฆ Move the default skin's entry for this slot into a named skin โ€” call it "base"` | **R12** โ€” do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and ยง3.4.2 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
5621
5299
  | `skin "S" slot "X" attachment "P": "name" is V, which is not a string. An attachment's name is the runtime's `Attachment.name` โ€ฆ` | ยง3.4 โ€” write the name as a string, or leave the key out and the attachment is named by its placeholder. It is written verbatim and `path` defaults to it, so a number there would reach the atlas as a region name nobody spelled |
5622
5300
 
@@ -5642,12 +5320,7 @@ Both halves of it are measured rather than fixed text: the count is this pair's,
5642
5320
  where the atlas has a **near miss** the sentence prints that too, in `A08`'s own
5643
5321
  clause and `A08`'s own words. When `--atlas-in` **was** given and the region is still
5644
5322
  absent, the second half names the pack instead and asks you to fix the spec's region
5645
- name or point the flag at the pack that has it. Until
5646
- [#697](https://github.com/firejune/rigc/issues/697) there was no rigc sentence at
5647
- all: the report printed in full and the run then died inside `AtlasAttachmentLoader`
5648
- with `Region not found in atlas: shade (attachment: shade)` and a spine-core stack
5649
- trace, at exit 1 โ€” the runtime's message about rigc's internals standing in for
5650
- rigc's message about your two files.
5323
+ name or point the flag at the pack that has it.
5651
5324
 
5652
5325
  ### 5.2 Assertions โ€” the gate
5653
5326
 
@@ -5664,8 +5337,7 @@ The report prints one line per assertion:
5664
5337
  - **PASS** โ€” it ran and held.
5665
5338
  - **SKIP** โ€” it had *nothing to look at*, and the reason says what was missing. A
5666
5339
  skip is never folded into the pass count.
5667
- ๐Ÿšจ **"Nothing to look at" includes a subject the skeleton does not carry**
5668
- ([#580](https://github.com/firejune/rigc/issues/580)). A rule named
5340
+ ๐Ÿšจ **"Nothing to look at" includes a subject the skeleton does not carry.** A rule named
5669
5341
  โŸจsubjectโŸฉ_โŸจpropertyโŸฉ โ€” `A03_REGION_WIDTH_HEIGHT_FINITE`,
5670
5342
  `A04_MESH_TRIANGLES_AND_ENCODING`, `A23_PHYSICS_CONSTRAINT_EFFECTIVE` โ€” walks
5671
5343
  that subject and measures each member, so a skeleton with no region, no mesh or
@@ -5705,59 +5377,59 @@ and with no parse there is nothing for them to look at โ€” so they report `SKIP`
5705
5377
  naming that, *the round trip did not produce a skeleton to measure* or *โ€ฆan atlas
5706
5378
  to measure*, and the summary's `<S>` goes up while `<M>` collapses. A run in that
5707
5379
  state is not a rig that nearly passed; it is a rig that was measured on one rule.
5708
- Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5380
+ Fix A00 and run it again.
5709
5381
 
5710
5382
  | Assertion | Profile | What tripped it, and where to fix it |
5711
5383
  | --- | --- | --- |
5712
- | `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Almost everything else in the report is downstream of this one โ€” fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked ([#568](https://github.com/firejune/rigc/issues/568)). โš ๏ธ **Two rules run before it and can be upstream of it**: `A31_DRAW_ORDER_OFFSETS_RESOLVE`, because a bad draw-order key makes the loader spin rather than return, and `A08_REGION_NAMES_MATCH_ATTACHMENTS`, because a `path` naming no region makes it throw. The round trip is still attempted either way; when the loader refuses a path A08 has already refused, this row **defers** to A08 by name instead of restating the miss in the parser's poorer words ([#589](https://github.com/firejune/rigc/issues/589)) |
5384
+ | `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Almost everything else in the report is downstream of this one โ€” fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked. โš ๏ธ **Two rules run before it and can be upstream of it**: `A31_DRAW_ORDER_OFFSETS_RESOLVE`, because a bad draw-order key makes the loader spin rather than return, and `A08_REGION_NAMES_MATCH_ATTACHMENTS`, because a `path` naming no region makes it throw. The round trip is still attempted either way; when the loader refuses a path A08 has already refused, this row **defers** to A08 by name instead of restating the miss in the parser's poorer words |
5713
5385
  | `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS` | both | a 4.1/4.2-shaped `ik`/`transform`/`path`/`physics`/`slider` array. rigc emits the 4.3 `constraints` array, so this normally means hand-edited JSON |
5714
- | `A02_NO_BONE_TRANSFORM_KEY` | both | a bone uses 4.2's `transform`; rename it `inherit` in the rig spec |
5715
- | `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size โ€” the attachment has no `image` and no `width`/`height`. **SKIP** when the skeleton carries no region attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
5716
- | `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
5717
- | `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel**. **SKIP** when no animation carries a timeline at all ([#580](https://github.com/firejune/rigc/issues/580)). Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one |
5718
- | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both โ—‘ | the atlas `size:` disagrees with the PNG on disk โ€” the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and ยง0.2 has the whole of it ([#715](https://github.com/firejune/rigc/issues/715)): a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names โ€” rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles ([#694](https://github.com/firejune/rigc/issues/694)): a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (ยง0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels โ€” a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling** ([#266](https://github.com/firejune/rigc/issues/266)), and what that message names is the pair that shares texels. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes โ€” one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way โ€” or calls it **truncated** when it begins as a PNG and runs out before its `IEND` (ยง0.2, [#732](https://github.com/firejune/rigc/issues/732)). That clause is validity too: nothing in rigc can read such a page back. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
5719
- | `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. Two sentences are about blank lines and name two different repairs ([#803](https://github.com/firejune/rigc/issues/803)): `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: consecutive blank lines` is two blank lines side by side after the first page โ€” delete one. rigc writes neither: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (ยง0.2), so either sentence 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` |
5720
- | `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. Until [#589](https://github.com/firejune/rigc/issues/589) they were unreachable: the loader threw first and the miss arrived as `A00_ROUNDTRIP_PARSE`. There is no `spine-html` clause here any more โ€” a placeholder is free to differ from the region its `path` names ([#574](https://github.com/firejune/rigc/issues/574)) **SKIP** when no attachment names a region *and* the atlas declares none โ€” both of its subjects at once ([#580](https://github.com/firejune/rigc/issues/580)) |
5386
+ | `A02_NO_BONE_TRANSFORM_KEY` | both | `bone "โ€ฆ" uses "transform", the key 4.0 and 4.1 spelled; 4.2 and 4.3 spell it "inherit"` โ€” rename it `inherit` in the rig spec |
5387
+ | `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size โ€” the attachment has no `image` and no `width`/`height`. **SKIP** when the skeleton carries no region attachment |
5388
+ | `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment |
5389
+ | `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel**. **SKIP** when no animation carries a timeline at all. Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one |
5390
+ | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both โ—‘ | the atlas `size:` disagrees with the PNG on disk โ€” the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and ยง0.2 has the whole of it: a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names โ€” rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles: a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (ยง0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels โ€” a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling**, and what that message names is the pair that shares texels. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes โ€” one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way โ€” or calls it **truncated** when it begins as a PNG and runs out before its `IEND` (ยง0.2). That clause is validity too: nothing in rigc can read such a page back. **SKIP** when the atlas declares no page |
5391
+ | `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` |
5392
+ | `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 |
5721
5393
  | `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 |
5722
- | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale. ๐Ÿฆด **It also poses every bone `inherit` key at its own time** ([#733](https://github.com/firejune/rigc/issues/733)): 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 ([#580](https://github.com/firejune/rigc/issues/580)): the NaN is produced by stepping, and a static rig is never stepped โ€” the same subject `A09` skips on |
5394
+ | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for 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 |
5723
5395
  | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
5724
5396
  | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
5725
- | `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, issue #274) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh ([#580](https://github.com/firejune/rigc/issues/580)). A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
5397
+ | `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 |
5726
5398
  | `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage โ€” a full-frame canvas that can never dirty-skip. **SKIP** when the skeleton declares no stage (ยง3.1): there is no full frame to span, and *unmeasured* must not print the same green as *measured and clear* |
5727
- | `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. **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 ([#568](https://github.com/firejune/rigc/issues/568)) |
5399
+ | `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. **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 |
5728
5400
  | `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`) |
5729
- | `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 ([#580](https://github.com/firejune/rigc/issues/580)) โ€” as it is for `A06`, `A19` and `A27`; see `A07` ([#608](https://github.com/firejune/rigc/issues/608)) |
5401
+ | `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` |
5730
5402
  | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug โ€” report it |
5731
- | `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** ([#777](https://github.com/firejune/rigc/issues/777)); 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. Until #777 the loose route stopped at the header, so the same fully opaque RGBA part passed a loose build and was refused by `--pack` of the same rig. 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 ([#770](https://github.com/firejune/rigc/issues/770)): 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 ([#266](https://github.com/firejune/rigc/issues/266)). โš ๏ธ **That scan states its verdict over the texels it READ, and never over texels that are not on the page** ([#705](https://github.com/firejune/rigc/issues/705)): 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** ([#715](https://github.com/firejune/rigc/issues/715)), and #705's clause 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 the scan came 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 **disappeared**, the scan having found 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. Before #705 the walk was silent about its own reach, so a part nobody could read printed *opaque in every one of its 12x8 texels* over zero of them, which is a refusal pointing at the wrong file: the art it names may be transparent, and the repair is the rectangle in `A06`'s row above. โš ๏ธ **A page file that cannot be read as PNG at all is the same non-measurement for every part on it** ([#732](https://github.com/firejune/rigc/issues/732)): one row per page naming its parts and pointing at `A06`, which names what the file is โ€” where it used to print `threw: cannot decode PNG โ€ฆ: unexpected end of file`, an inflate error about a file that was never a PNG. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
5732
- | `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 ([#684](https://github.com/firejune/rigc/issues/684)). Fix the rig spec's `controls`/`chain`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
5403
+ | `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 |
5404
+ | `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`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
5733
5405
  | `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 |
5734
- | `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 ([#580](https://github.com/firejune/rigc/issues/580)) |
5735
- | `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 ([#748](https://github.com/firejune/rigc/issues/748)) โ€” 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 ([#794](https://github.com/firejune/rigc/issues/794)) โ€” **at rest, and on every physics timeline key** ([#610](https://github.com/firejune/rigc/issues/610), [#743](https://github.com/firejune/rigc/issues/743)). 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 ([#727](https://github.com/firejune/rigc/issues/727)). 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, [#798](https://github.com/firejune/rigc/issues/798)) โ€” 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: until #798 it read "it is muted", of a constraint measured moving its bone by exactly the opposite of a positive mix (`T115`). 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 ([#752](https://github.com/firejune/rigc/issues/752)). `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), [#731](https://github.com/firejune/rigc/issues/731)), so this sentence is met on a file, never on a decompiled rebuild. **SKIP** when the skeleton declares no physics constraint ([#580](https://github.com/firejune/rigc/issues/580)) โ€” the same sentence `A36` and `A37` have always printed for their own constraint types |
5736
- | `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it ([#580](https://github.com/firejune/rigc/issues/580)) |
5406
+ | `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 |
5407
+ | `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 |
5408
+ | `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it |
5737
5409
  | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
5738
- | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table โ€” a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (ยง3.3). **SKIP** when the rig declares no canonical slot order. โš ๏ธ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass ([#580](https://github.com/firejune/rigc/issues/580)): the completeness clause reads it as every declared slot lost and names them, which is the maximal case of what [#575](https://github.com/firejune/rigc/issues/575) filed |
5739
- | `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename. **SKIP** when the atlas declares no region ([#580](https://github.com/firejune/rigc/issues/580)) |
5410
+ | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table โ€” a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (ยง3.3). **SKIP** when the rig declares no canonical slot order. โš ๏ธ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass: the completeness clause reads it as every declared slot lost and names them |
5411
+ | `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename. **SKIP** when the atlas declares no region |
5740
5412
  | `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width. **SKIPs** on authored geometry and on a contour mesh โ€” neither has rows rigc paired |
5741
5413
  | `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
5742
5414
  | `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
5743
5415
  | `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 |
5744
5416
  | `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 |
5745
5417
  | `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 |
5746
- | `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` ([#726](https://github.com/firejune/rigc/issues/726)). 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 |
5747
- | `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, issue #262). **SKIP** when no animation carries a deform timeline |
5748
- | `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, and before [#752](https://github.com/firejune/rigc/issues/752) it passed. 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 |
5749
- | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` **above 0** (ยง3.5.2, ยง4.12) โ€” the same reading as `A36`'s, so a `mix` timeline keying 0 only is no rescue ([#752](https://github.com/firejune/rigc/issues/752)): `slider "S" has mix 0 at setup and none of the 2 animations keys its mix above 0; update() returns on mix 0 โ€” rest it above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no slider |
5418
+ | `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 |
5419
+ | `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 |
5420
+ | `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 |
5421
+ | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` **above 0** (ยง3.5.2, ยง4.12) โ€” the same reading as `A36`'s, so a `mix` timeline keying 0 only is no rescue: `slider "S" has mix 0 at setup and none of the 2 animations keys its mix above 0; update() returns on mix 0 โ€” rest it above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no slider |
5750
5422
  | `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (ยง3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
5751
- | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (ยง4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause โ€” [FACE.md ยง4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (ยง3.7) for a slot that folds on purpose. โš ๏ธ A key whose slot **draws no pixels at that key's own time** โ€” faded to alpha exactly 0, or showing another attachment โ€” is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. โš ๏ธ And the **spans between** consecutive keys are scanned too (ยง4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence โ€” `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=โ€ฆ` โ€” with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. โš ๏ธ And the **frame** it poses in is the one the animation is reached in (ยง4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time โ€” because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` โ€” never refused and never silent. โš ๏ธ And the **skin** it poses in is the one the timeline is keyed on (ยง4.11.5, issue #583), since a deform's address is a `skin / slot / attachment` triple: the pose wears that skin, which also switches on any `skin: true` bone or constraint it activates, and the "nothing is drawn" sentence names the skin it was read under. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
5752
- | `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` โ€” it writes that property outright at `mix: 1` and every earlier slider on it is dead (ยง3.5.2). Also fires when the shared timeline **cannot** be applied additively (a slot colour, an attachment swap, a draw order, an ik mix, a path's `spacing`, most physics properties), where `"additive": true` is not the fix and one of the two has to go. โญ Which of the two it is, is **posed rather than read off `Timeline.additive`**: the shared timeline is applied twice with `add` set and the detail says what it did ([#655](https://github.com/firejune/rigc/issues/655) โ€” two classes declare that flag falsely about themselves, so a path constraint's `mix` and a slider's `time` were refused although they compose). The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, which one wins today, and the class that was posed. Four shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, two sliders on different properties, and a shared timeline that writes **nothing a pose holds** โ€” an `events` timeline fires no event under a slider (`firedEvents` is null), so neither slider has anything there for the other to erase. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
5753
- | `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` | both | a physics constraint driving a component the **Spine editor** cannot hold, on a rig that declared `invariants.editorRoundTrip` (ยง3.7). The editor's physics model holds `x` and `y` only, with no cap on how many at once, so a constraint driving `rotate`, `scaleX` or `shearX` is imported, exported and handed back driving **nothing** โ€” measured over three rigs and twelve constraints with the predictions written first ([#540](https://github.com/firejune/rigc/issues/540)). The detail names the constraint and each component. โš ๏ธ rigc's own output is correct โ€” every runtime plays a rotation jiggle โ€” so this is opt-in and the default is *not* silence: on a rig that declares nothing it **SKIPs**, and the SKIP names the constraint and the component anyway, so an author learns without having asked. Fix by driving the constraint in `x`/`y`, or by dropping the declaration if the rig never goes near the editor. Disjoint from `A23_PHYSICS_CONSTRAINT_EFFECTIVE` by construction: A23 refuses an **empty** driven set, which is what comes back from the editor, and this refuses a non-empty one that will not survive going in. **SKIP** also when the rig declares the editor and carries no physics constraint at all |
5754
- | `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` | both | a slider whose animation keys a property of a constraint **at or before it** in `constraints` (ยง3.5.2) โ€” a slider's `mix` or `time`, an ik or transform mix, a path `position`, `spacing` or `mix`, any physics value. That array is the update order for every kind, and each constraint reads its own applied pose when its turn comes โ€” `Slider.update` takes `mix` as the alpha it applies with and `time` as the time it applies at, `PhysicsConstraint.update` returns on `mix` 0 before reading the rest โ€” so the key lands after the only read of it and `Posed.resetConstrained` discards it before the next frame: what the driven constraint drives is dead at every position of the driving dial, although its pose still holds the number ([#658](https://github.com/firejune/rigc/issues/658), [#665](https://github.com/firejune/rigc/issues/665)). The detail names the slider, the driven constraint with its kind, both array indices, the property, the runtime class whose `update` reads it, and the animation the key sits in. Fix by moving the driver earlier, or by keying that property from a slider that already is. **The two indices equal is the same failure**: a slider cannot key its own `mix` or `time`, and one muted at setup that keys its own `mix` up never applies anything at all โ€” `A37` is silent there, because it asks whether *an* animation keys the mix and not which one. **Two shapes it deliberately leaves out**, both measured: a `physics` `reset` key, which fires on a crossed frame time and so never fires from a slider at all, in either order โ€” the reorder would repair nothing; and a physics timeline naming no constraint, which is every physics constraint declaring that property global and IS refused for the ones already run. Disjoint from `A40` by construction: `A40` asks who writes a shared property last and excludes every slider whose `mix` is keyed, this asks whether anything reads what was written. **SKIP** when the skeleton declares no slider, and when no slider's animation keys a constraint property โ€” that SKIP names any `reset` keys it found โ€” a pass means a driver and a driven were compared |
5755
- | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (ยง3.3) or an `rgba2` / `rgb2` timeline (ยง4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** โ€” it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** โ€” `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` or `rgb2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time โ€” at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298โ€ฆ`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before (a correct file was refused that way until [#771](https://github.com/firejune/rigc/issues/771)), and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. โš ๏ธ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. An `rgb2` key's light colour is compared over its three channels only: the light alpha is not its to state, and that it is left where it was is measured in the selftest (`S85`). **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` or `rgb2` โ€” there is then no two-colour tint to read back |
5756
- | `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | both | a **linked mesh** (ยง3.4) โ€” `type: "linkedmesh"`, or a `type: "mesh"` carrying `source` โ€” that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by **nothing at all** and `setSourceMesh` fills the attachment with the source's arrays instead: the file says one mesh and every runtime draws another, in silence. The detail names the attachment by skin, slot and placeholder, every key it states, the `source` and where the parser looks for it โ€” the two defaults spelled out, because an omitted `skin` is the **default** skin rather than the one the link is written in โ€” and the shape the keys describe beside the shape the attachment loaded. โš ๏ธ **`width`/`height` are not part of this.** `setSourceMesh` overwrites both with the source's, so they are as dead at runtime โ€” but the parser reads them (`:569-570`), the format carries them on a link and rigc emits them, so refusing them would refuse every link rigc writes (ยง3.4). `compile.ts` refuses the same shape outright in a rig rigc builds (ยง5.1); this is that fact held against a skeleton it did not write, and `ingest` reports it as `ATTACHMENT_LINK_GEOMETRY` ([INGEST ยง2.0](INGEST.md)). **SKIP** when no attachment in the skeleton takes its geometry from another โ€” which is almost every skeleton, so a pass here means a link was read ([#710](https://github.com/firejune/rigc/issues/710)) |
5757
- | `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | both | an `rgb` or `alpha` timeline (ยง4.4) the runtime does not hold as the file states it, in one of two shapes that both parse in silence. **A channel keyed twice**: another colour timeline of the same slot in the same animation poses a channel this one poses โ€” `rgba` beside `alpha` is the shape a converter leaves when it writes a separable `rgb` back as `rgba` next to the `alpha` it kept. Every colour timeline poses its channels at every time, the setup value before its first key included, so the one the file states later overwrites the other everywhere; the detail names both timelines, the channel, and which one survives. **A key not posed as written**: the animation is stepped to each key's own time โ€” at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298โ€ฆ`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before (a correct file was refused that way until [#771](https://github.com/firejune/rigc/issues/771)), and the posed r g b (for `rgb`, against the hex, to half a quantisation step) or alpha (for `alpha`, against `value`, whose absence the parser reads as 0) is compared โ€” a colour that is not six hex digits loads as NaN, and a key whose time another key repeats is read by nothing. โš ๏ธ An `rgb` alone written as an `rgba` holding the setup alpha is **not** caught and cannot be from the file: it is a correct `rgba`, and the difference shows only under another track that moves the alpha. The loaded timeline class and the channels a separable timeline leaves alone are measured in the selftest (`S83`โ€“`S85`) rather than here, because against the linked parser neither can come out wrong. The channel table is `SLOT_COLOR_CHANNELS` in `src/timelines.ts`, shared with the compiler's refusal and held to the runtime's own property ids (`S89`). **SKIP** when no animation keys an `rgb` or `alpha` โ€” there is then no separable slot colour to read back |
5758
- | `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | both | a **numbered series** (ยง3.4.3, ยง4.13) that the runtime does not show as the file states it. Every shape below loads without a word, measured on spine-core 4.3.13 ([#729](https://github.com/firejune/rigc/issues/729)). **The block**: a `sequence` with no `count` (`readSequence` reads 0, and the attachment holds no region) or a `setup` at or past `count` (`Sequence.resolveIndex` clamps it to the last frame). **The keys**: a `mode` outside the seven โ€” `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` โ€” loads as `hold`; an `index` that is fractional (`index << 4` truncates it) or past the end (clamped); an advancing mode at an effective delay of 0 (the parser carries a key's `delay` from the key before; `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so it never advances); a timeline on an attachment that carries no block (the parser gives every region a one-region series, so every mode shows it). **The pose**: every key is stepped to mid-frame sample times โ€” enough to wrap every mode, and a `hold` key to its own time as the runtime stores it, a 32-bit float ([#771](https://github.com/firejune/rigc/issues/771)) โ€” and the region the slot shows is held to the frame the file's own statement gives, the arithmetic of `SequenceTimeline.applyToSlot` and the names of `Sequence.getPath` transcribed rather than read off the loaded timeline, so the check is not the runtime agreeing with itself. Before the first key the frame is `setup`. โš ๏ธ A sample where the slot shows another attachment is not compared, because the runtime writes nothing there; a timeline with no comparable sample is counted in `stats.sequenceSamplesUnshown`. `compile.ts` refuses every one of these shapes in a spec (ยง5.1); this is them held against a skeleton it did not write. **SKIP** when no attachment carries a `sequence` block and no animation keys a `sequence` timeline |
5759
- | `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, and before [#765](https://github.com/firejune/rigc/issues/765) it passed. 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` ([#784](https://github.com/firejune/rigc/issues/784)). **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`) |
5760
- | `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 ([#765](https://github.com/firejune/rigc/issues/765)). 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 ([#784](https://github.com/firejune/rigc/issues/784)). **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`) |
5423
+ | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (ยง4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause โ€” [FACE.md ยง4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (ยง3.7) for a slot that folds on purpose. โš ๏ธ A key whose slot **draws no pixels at that key's own time** โ€” faded to alpha exactly 0, or showing another attachment โ€” is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is still refused. โš ๏ธ And the **spans between** consecutive keys are scanned too (ยง4.11.3): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence โ€” `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=โ€ฆ` โ€” with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. โš ๏ธ And the **frame** it poses in is the one the animation is reached in (ยง4.11.4): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time โ€” because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` โ€” never refused and never silent. โš ๏ธ And the **skin** it poses in is the one the timeline is keyed on (ยง4.11.5), since a deform's address is a `skin / slot / attachment` triple: the pose wears that skin, which also switches on any `skin: true` bone or constraint it activates, and the "nothing is drawn" sentence names the skin it was read under. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
5424
+ | `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` โ€” it writes that property outright at `mix: 1` and every earlier slider on it is dead (ยง3.5.2). Also fires when the shared timeline **cannot** be applied additively (a slot colour, an attachment swap, a draw order, an ik mix, a path's `spacing`, most physics properties), where `"additive": true` is not the fix and one of the two has to go. โญ Which of the two it is, is **posed rather than read off `Timeline.additive`**: the shared timeline is applied twice with `add` set and the detail says what it did (two classes declare that flag falsely about themselves: a path constraint's `mix` and a slider's `time` compose although the flag says they do not). The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, which one wins today, and the class that was posed. Four shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, two sliders on different properties, and a shared timeline that writes **nothing a pose holds** โ€” an `events` timeline fires no event under a slider (`firedEvents` is null), so neither slider has anything there for the other to erase. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
5425
+ | `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` | both | a physics constraint driving a component the **Spine editor** cannot hold, on a rig that declared `invariants.editorRoundTrip` (ยง3.7). The editor's physics model holds `x` and `y` only, with no cap on how many at once, so a constraint driving `rotate`, `scaleX` or `shearX` is imported, exported and handed back driving **nothing** โ€” measured over three rigs and twelve constraints with the predictions written first. The detail names the constraint and each component. โš ๏ธ rigc's own output is correct โ€” every runtime plays a rotation jiggle โ€” so this is opt-in and the default is *not* silence: on a rig that declares nothing it **SKIPs**, and the SKIP names the constraint and the component anyway, so an author learns without having asked. Fix by driving the constraint in `x`/`y`, or by dropping the declaration if the rig never goes near the editor. Disjoint from `A23_PHYSICS_CONSTRAINT_EFFECTIVE` by construction: A23 refuses an **empty** driven set, which is what comes back from the editor, and this refuses a non-empty one that will not survive going in. **SKIP** also when the rig declares the editor and carries no physics constraint at all |
5426
+ | `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` | both | a slider whose animation keys a property of a constraint **at or before it** in `constraints` (ยง3.5.2) โ€” a slider's `mix` or `time`, an ik or transform mix, a path `position`, `spacing` or `mix`, any physics value. That array is the update order for every kind, and each constraint reads its own applied pose when its turn comes โ€” `Slider.update` takes `mix` as the alpha it applies with and `time` as the time it applies at, `PhysicsConstraint.update` returns on `mix` 0 before reading the rest โ€” so the key lands after the only read of it and `Posed.resetConstrained` discards it before the next frame: what the driven constraint drives is dead at every position of the driving dial, although its pose still holds the number. The detail names the slider, the driven constraint with its kind, both array indices, the property, the runtime class whose `update` reads it, and the animation the key sits in. Fix by moving the driver earlier, or by keying that property from a slider that already is. **The two indices equal is the same failure**: a slider cannot key its own `mix` or `time`, and one muted at setup that keys its own `mix` up never applies anything at all โ€” `A37` is silent there, because it asks whether *an* animation keys the mix and not which one. **Two shapes it deliberately leaves out**, both measured: a `physics` `reset` key, which fires on a crossed frame time and so never fires from a slider at all, in either order โ€” the reorder would repair nothing; and a physics timeline naming no constraint, which is every physics constraint declaring that property global and IS refused for the ones already run. Disjoint from `A40` by construction: `A40` asks who writes a shared property last and excludes every slider whose `mix` is keyed, this asks whether anything reads what was written. **SKIP** when the skeleton declares no slider, and when no slider's animation keys a constraint property โ€” that SKIP names any `reset` keys it found โ€” a pass means a driver and a driven were compared |
5427
+ | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (ยง3.3) or an `rgba2` / `rgb2` timeline (ยง4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** โ€” it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** โ€” `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` or `rgb2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time โ€” at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298โ€ฆ`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before, and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. โš ๏ธ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. An `rgb2` key's light colour is compared over its three channels only: the light alpha is not its to state, and it is left where it was. **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` or `rgb2` โ€” there is then no two-colour tint to read back |
5428
+ | `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | both | a **linked mesh** (ยง3.4) โ€” `type: "linkedmesh"`, or a `type: "mesh"` carrying `source` โ€” that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by **nothing at all** and `setSourceMesh` fills the attachment with the source's arrays instead: the file says one mesh and every runtime draws another, in silence. The detail names the attachment by skin, slot and placeholder, every key it states, the `source` and where the parser looks for it โ€” the two defaults spelled out, because an omitted `skin` is the **default** skin rather than the one the link is written in โ€” and the shape the keys describe beside the shape the attachment loaded. โš ๏ธ **`width`/`height` are not part of this.** `setSourceMesh` overwrites both with the source's, so they are as dead at runtime โ€” but the parser reads them (`:569-570`), the format carries them on a link and rigc emits them, so refusing them would refuse every link rigc writes (ยง3.4). `compile.ts` refuses the same shape outright in a rig rigc builds (ยง5.1); this is that fact held against a skeleton it did not write, and `ingest` reports it as `ATTACHMENT_LINK_GEOMETRY` ([INGEST ยง2.0](INGEST.md)). **SKIP** when no attachment in the skeleton takes its geometry from another โ€” which is almost every skeleton, so a pass here means a link was read |
5429
+ | `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | both | an `rgb` or `alpha` timeline (ยง4.4) the runtime does not hold as the file states it, in one of two shapes that both parse in silence. **A channel keyed twice**: another colour timeline of the same slot in the same animation poses a channel this one poses โ€” `rgba` beside `alpha` is the shape a converter leaves when it writes a separable `rgb` back as `rgba` next to the `alpha` it kept. Every colour timeline poses its channels at every time, the setup value before its first key included, so the one the file states later overwrites the other everywhere; the detail names both timelines, the channel, and which one survives. **A key not posed as written**: the animation is stepped to each key's own time โ€” at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298โ€ฆ`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before, and the posed r g b (for `rgb`, against the hex, to half a quantisation step) or alpha (for `alpha`, against `value`, whose absence the parser reads as 0) is compared โ€” a colour that is not six hex digits loads as NaN, and a key whose time another key repeats is read by nothing. โš ๏ธ An `rgb` alone written as an `rgba` holding the setup alpha is **not** caught and cannot be from the file: it is a correct `rgba`, and the difference shows only under another track that moves the alpha. The loaded timeline class and the channels a separable timeline leaves alone are not measured here, because against the linked parser neither can come out wrong. The channel table is `SLOT_COLOR_CHANNELS` in `src/timelines.ts`, shared with the compiler's refusal and held to the runtime's own property ids. **SKIP** when no animation keys an `rgb` or `alpha` โ€” there is then no separable slot colour to read back |
5430
+ | `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | both | a **numbered series** (ยง3.4.3, ยง4.13) that the runtime does not show as the file states it. Every shape below loads without a word, measured on spine-core 4.3.13. **The block**: a `sequence` with no `count` (`readSequence` reads 0, and the attachment holds no region) or a `setup` at or past `count` (`Sequence.resolveIndex` clamps it to the last frame). **The keys**: a `mode` outside the seven โ€” `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` โ€” loads as `hold`; an `index` that is fractional (`index << 4` truncates it) or past the end (clamped); an advancing mode at an effective delay of 0 (the parser carries a key's `delay` from the key before; `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so it never advances); a timeline on an attachment that carries no block (the parser gives every region a one-region series, so every mode shows it). **The pose**: every key is stepped to mid-frame sample times โ€” enough to wrap every mode, and a `hold` key to its own time as the runtime stores it, a 32-bit float โ€” and the region the slot shows is held to the frame the file's own statement gives, the arithmetic of `SequenceTimeline.applyToSlot` and the names of `Sequence.getPath` transcribed rather than read off the loaded timeline, so the check is not the runtime agreeing with itself. Before the first key the frame is `setup`. โš ๏ธ A sample where the slot shows another attachment is not compared, because the runtime writes nothing there; a timeline with no comparable sample is counted in `stats.sequenceSamplesUnshown`. `compile.ts` refuses every one of these shapes in a spec (ยง5.1); this is them held against a skeleton it did not write. **SKIP** when no attachment carries a `sequence` block and no animation keys a `sequence` timeline |
5431
+ | `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`) |
5432
+ | `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`) |
5761
5433
 
5762
5434
  `both โ—‘` marks a mixed assertion: its validity half always runs and its policy
5763
5435
  clauses are gated by profile.
@@ -5772,46 +5444,40 @@ own behaviour is worse: an unknown attachment `type` returns `null` and the
5772
5444
  attachment disappears, and a constraint entry with an unrecognised `type` matches no
5773
5445
  case and vanishes.
5774
5446
 
5775
- A deferral carries its reason, and there is one deferred attachment type left:
5776
- **`point` appears nowhere in the benchmark corpus** (SPEC_COVERAGE parts 3-1 and
5777
- 4-2), so it is not on the ladder's critical path. The message says so, because a
5778
- deferral without its reason is a wall rather than a work item. `linkedmesh` stood
5779
- beside it until [#691](https://github.com/firejune/rigc/issues/691) and is now
5780
- emitted โ€” ยง3.4 has its fields.
5447
+ A deferral carries its reason, and there is one deferred attachment type:
5448
+ **`point` appears nowhere in the benchmark corpus**, so it is not on the ladder's
5449
+ critical path. The message says so, because a
5450
+ deferral without its reason is a wall rather than a work item.
5781
5451
 
5782
5452
  โš ๏ธ **A spelling the format does not have is a different refusal and says so.**
5783
5453
  `sequence` is not an attachment type, and a `"type"` that is `null` is not an absent
5784
5454
  one โ€” telling either author that "rigc does not emit it yet" promises work that will
5785
5455
  never be done, on a map the parser would have dropped in silence. Those rows below
5786
- are `CompileError`s, and they name what the format actually defines
5787
- ([#577](https://github.com/firejune/rigc/issues/577)).
5456
+ are `CompileError`s, and they name what the format actually defines.
5788
5457
 
5789
5458
  | You wrote | You get |
5790
5459
  | --- | --- |
5791
5460
  | attachment `type` of `point` | `this attachment is a "point" โ€” a position and an angle with no geometry at all โ€” "x", "y", "rotation" and "color" โ€ฆ. rigc does not emit it yet, deliberately: it emits region, mesh, linkedmesh, boundingbox, clipping, path, and a point appears nowhere in the benchmark corpus โ€ฆ` โ€” the message names the **construct**, not just its type string, and part 1-6 is where the sentence comes from |
5792
- | a mesh carrying `source` (`type: "mesh"` **or** `type: "linkedmesh"`) | **not a refusal any more** โ€” both spellings compile to a linked mesh (ยง3.4, [#691](https://github.com/firejune/rigc/issues/691)). They share one parser branch and `source` is what decides between them (SPEC_COVERAGE part 1-6), so `source` on a mesh is a linked mesh whatever `type` says. It was once refused as *2 keys this compiler does not read: "source", "skin" โ€ฆ fix the spelling or remove it*, whose remedy destroys the construct ([#577](https://github.com/firejune/rigc/issues/577)) |
5461
+ | a mesh carrying `source` (`type: "mesh"` **or** `type: "linkedmesh"`) | **not a refusal** โ€” both spellings compile to a linked mesh (ยง3.4). They share one parser branch and `source` is what decides between them (SPEC_COVERAGE part 1-6), so `source` on a mesh is a linked mesh whatever `type` says |
5793
5462
  | attachment `type` of anything else โ€” `sequence`, a typo | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (region, mesh, linkedmesh, boundingbox, path, point, clipping). โ€ฆ the attachment is dropped from the skeleton without a word` โ€” a **`CompileError`**, not a deferral: rigc is not going to implement a name the format does not have. (`sequence` is a key on a region or a mesh, not a type of its own.) |
5794
- | `"type": null` | `"type" is null, which is not a name. โ€ฆ PRESENT-and-null is not absent: getValue(map, "type", "region") takes the default only when the key is missing, so this map matches no case, readAttachment returns null, and the attachment is dropped from the skeleton without a word. Remove the key, or name a type.` Leaving the key **out** is legal and reads as `region`; writing it as `null` is not the same thing ([#577](https://github.com/firejune/rigc/issues/577)) |
5463
+ | `"type": null` | `"type" is null, which is not a name. โ€ฆ PRESENT-and-null is not absent: getValue(map, "type", "region") takes the default only when the key is missing, so this map matches no case, readAttachment returns null, and the attachment is dropped from the skeleton without a word. Remove the key, or name a type.` Leaving the key **out** is legal and reads as `region`; writing it as `null` is not the same thing |
5795
5464
  | constraint `type` of anything else | `constraint type "X" is not one Spine 4.3 knows. The five are: ik, transform, path, physics, slider.` โ€” all five are emitted, so this is a typo, and a typo is what the parser drops in silence |
5796
5465
  | any key neither format has, anywhere in either file | `<object> has a key this compiler does not read: "x" (did you mean "y"?) โ€ฆ Known here: โ€ฆ` (ยง5.1). Not a deferral either: a key nothing reads is a value you wrote and the emitted skeleton does not contain |
5797
5466
 
5798
5467
  Two more limits that are not errors but will shape what you can attempt:
5799
5468
 
5800
- - **The atlas packer and importer exist and are narrow** (issue #4). `--pack`
5469
+ - **The atlas packer and importer exist and are narrow.** `--pack`
5801
5470
  arranges the parts onto shared pages and `--atlas-in` resolves them against a
5802
5471
  pack somebody else made โ€” **ยง0.1** and **ยง0.2** state what each does and, more
5803
- usefully, what neither does: no trimming, no rotation, no scaling, and no
5804
- `--profile spine-html` over a packed atlas. The default is still **one part per
5805
- page** with `pma: false` and every region covering its whole page, and nothing
5472
+ usefully, what neither does: no trimming, no rotation, no scaling. The default is
5473
+ **one part per page** with `pma: false` and every region covering its whole page, and nothing
5806
5474
  about a build changes unless one of those flags is given.
5807
5475
  - **`drawOrderFolder`** is walked by the validator (so `A05` checks its curves and
5808
5476
  `diff` counts its keys) and cannot be *written*: there is no motion-spec property
5809
- for it. Everything a motion spec **can** key is ยง4.4's track table โ€” which now
5477
+ for it. Everything a motion spec **can** key is ยง4.4's track table โ€” which
5810
5478
  includes the `path` and `slider` groups (ยง4.12) โ€” plus the six families that sit
5811
5479
  beside `tracks`: `drawOrder` (ยง4.7), `events` (ยง4.8), `ik` (ยง4.9), `transform`
5812
- (ยง4.10), `deform` (ยง4.11) and `sequence` (ยง4.13). `sequence` stood beside
5813
- `drawOrderFolder` in this sentence until
5814
- [#729](https://github.com/firejune/rigc/issues/729) made it writable.
5480
+ (ยง4.10), `deform` (ยง4.11) and `sequence` (ยง4.13).
5815
5481
 
5816
5482
  ---
5817
5483
 
@@ -5820,12 +5486,10 @@ Two more limits that are not errors but will shape what you can attempt:
5820
5486
  1. `build --profile <the one you meant>` exits 0 and the report has **no FAIL**.
5821
5487
  Saying nothing means `spine`, so "the one you meant" is a decision either way โ€”
5822
5488
  the report's first line names the profile that judged it.
5823
- ๐Ÿ“Ž The two profiles judge attachment **naming** identically since
5824
- [#574](https://github.com/firejune/rigc/issues/574): `spine-html` adds the
5825
- renderer and archetype rules and has no opinion about how an attachment is
5489
+ ๐Ÿ“Ž The two profiles judge attachment **naming** identically: `spine-html` adds
5490
+ the renderer and archetype rules and has no opinion about how an attachment is
5826
5491
  spelled, so a rig whose named skins share a placeholder (ยง3.4.2) is green
5827
- under either. Before that it was green under exactly one of them, and which
5828
- one was not a property of the rig.
5492
+ under either.
5829
5493
  2. Read the `SKIP` lines. Each one is a check that did *not* run โ€” make sure none of
5830
5494
  them is a check you were relying on. The summary's *measured* figure is the
5831
5495
  shortest version of this step: it is how many of the rules actually looked at
@@ -5839,7 +5503,7 @@ Two more limits that are not errors but will shape what you can attempt:
5839
5503
  of how it reads: if `A00` failed, most of the SKIP lines say the round trip
5840
5504
  denied them a result, and the handful of `PASS` rows beside them were measured
5841
5505
  on the raw text alone. Step 1 already sent you back; do not take anything from
5842
- the rest of that report on the way ([#568](https://github.com/firejune/rigc/issues/568)).
5506
+ the rest of that report on the way.
5843
5507
  3. Read the `PROF` lines. They are where "was this rig held to that rule at all"
5844
5508
  gets answered for everything the profile left out โ€” the renderer policy *and*
5845
5509
  the archetype rules. A green under `spine` has been held to neither; a green
@@ -6028,9 +5692,8 @@ part's own height**, from four independent fits, which is the quantity that had
6028
5692
  agree between them and did.
6029
5693
 
6030
5694
  **A value is easier to get right than a curve.** The three traps above are all
6031
- about measuring a *value*, and both ladder runs so far found that the values came
6032
- out right early: rung 1's key values were exact at every keyframe on the second
6033
- build. What was wrong was the *shape between* them โ€” every accelerating segment
5695
+ about measuring a *value*, and on the ladder the values come out right early:
5696
+ rung 1's key values were exact at every keyframe on the second build. What was wrong was the *shape between* them โ€” every accelerating segment
6034
5697
  had the decelerating curve and vice versa, from one inverted comparison. Nothing
6035
5698
  in a static reading of the file can find that, because both candidates for the
6036
5699
  shape are legal and the file reads fine either way, and a symmetric pair of easings
@@ -6335,8 +5998,7 @@ your candidate under one of its skins (below), `--all-frames` to list
6335
5998
  every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
6336
5999
  per-slot report.
6337
6000
 
6338
- ๐Ÿšจ **What `check` certifies is the DEFAULT skin, unless you pass `--skin`**
6339
- (issue #571). With no `--skin` no skin is set at all, which is `spine-core`'s own
6001
+ ๐Ÿšจ **What `check` certifies is the DEFAULT skin, unless you pass `--skin`.** With no `--skin` no skin is set at all, which is `spine-core`'s own
6340
6002
  initial state: every slot resolves through `SkeletonData.defaultSkin` alone, and a
6341
6003
  slot whose art lives only in a named skin draws **nothing** โ€” on both sides, since
6342
6004
  the reference frames came out of the same renderer. A multi-skin rig checked that
@@ -6376,7 +6038,7 @@ A skin name the candidate does not declare is refused with the ones it does โ€”
6376
6038
  name.** `A39` and the `DEFORM` block pose each key with the skin that key is keyed
6377
6039
  on โ€” a deform timeline's address is a `skin / slot / attachment` triple โ€” so a
6378
6040
  mesh in a named skin is measured in that skin without anything being passed on the
6379
- command line (ยง4.11.5, issue #583). The difference is where the name comes from,
6041
+ command line (ยง4.11.5). The difference is where the name comes from,
6380
6042
  not which command cares: `check` compares two picture sets and neither of them
6381
6043
  records a skin unless you say so, while a deform key knows its own.
6382
6044
 
@@ -6387,8 +6049,7 @@ frames, and spineboy's `@30fps` sets do the same. The frame table still says
6387
6049
  `frames 2 on disk, candidate samples 311, 2 compared` โ€” those are the files โ€” and a
6388
6050
  **`sheet` line under it** carries the other 309: your candidate sampled at the set's
6389
6051
  own rate, rendered into the same box the frames above were at the sheet's own scale,
6390
- and compared tile by tile (issue #36 โ€” the gap a rung-2 run had already prototyped a
6391
- comparator for, in its own working directory, because the tool could not see its shot).
6052
+ and compared tile by tile.
6392
6053
 
6393
6054
  โš ๏ธ **Read it as a series, not as one number** โ€” ยง9.2. And note what it does not
6394
6055
  carry: MAE only. The `ฮ”px` / `ref ฮ”` thresholds are calibrated at frame scale and a
@@ -6417,8 +6078,8 @@ your shot's length is not a whole number of coarse intervals, expect **both**
6417
6078
  duration the coarse sidecar understates, and a terminal pose only the finer set shows
6418
6079
  you.
6419
6080
 
6420
- `--fps <n>` exists for frame sets that have no `frames.json` beside them, which are
6421
- sets rendered before the sidecar existed: it gives the rate those frames were
6081
+ `--fps <n>` exists for frame sets that have no `frames.json` beside them: it gives
6082
+ the rate those frames were
6422
6083
  sampled at, and without it the 12 fps protocol rate is assumed and the report says
6423
6084
  so rather than letting the assumption look like a measurement. Passing `--fps` with
6424
6085
  a value the sidecar contradicts is an error, not an override.
@@ -6435,15 +6096,15 @@ fitting it. Two uses:
6435
6096
  MAE moves by more than a point for a fraction of a pixel, that separation is
6436
6097
  worth more than the absolute number.
6437
6098
 
6438
- There used to be a third โ€” *"you already know your candidate's world coordinates
6439
- match the reference's own, declared in `frames.json`"* โ€” and **`check` now does
6440
- that one for you** (issue #52). Before fitting anything it renders your candidate
6099
+ A third use โ€” *"you already know your candidate's world coordinates match the
6100
+ reference's own, declared in `frames.json`"* โ€” is one **`check` covers for you.** Before
6101
+ fitting anything it renders your candidate
6441
6102
  into the box `frames.json` records and measures where your pixels land. If they
6442
6103
  land on the reference's to within a pixel, that box is yours too, and it is used:
6443
6104
  it is not an *estimate* of where the frames were drawn, it is where they were
6444
6105
  drawn, and the framing line says `frames.json's own box โ€” the candidate measured
6445
6106
  into it`. If they do not, the box is refused and your candidate is framed by its
6446
- own pixels exactly as before โ€” which is the ordinary case, because the reference's
6107
+ own pixels โ€” which is the ordinary case, because the reference's
6447
6108
  origin is in a file you are not allowed to open.
6448
6109
 
6449
6110
  That is worth a paragraph rather than a line because of what it costs when it is
@@ -6458,11 +6119,7 @@ author could tell that from a wrong animation without running the pin by hand.
6458
6119
  Pinning to paper over a **real** framing difference โ€” rather than one of the two
6459
6120
  cases above โ€” is the dishonest use: it makes a genuine mismatch between your
6460
6121
  candidate and the reference disappear from the report instead of showing up as
6461
- `content`/`rms`/`union residual`. That used to be easy to do by accident, because
6462
- the old quad-corner framing could be wrong by more than a pixel for reasons that had
6463
- nothing to do with either side's motion โ€” two honest ladder runs measured it costing
6464
- 30+ points of MAE with no key changed, which is why framing is now fitted to drawn
6465
- pixels rather than quad corners (issue #34, closed by #39; see ยง9.2). Pin to a box
6122
+ `content`/`rms`/`union residual`. Pin to a box
6466
6123
  you can name a reason for, and read the unpinned framing line first when you are not
6467
6124
  sure whether you have one.
6468
6125
 
@@ -6483,7 +6140,7 @@ Why the split falls there, both halves measured on an 8-shot character (147 fram
6483
6140
  that is not in the frames' coordinates puts the pooled correction over the
6484
6141
  one-pixel threshold and the *whole root* falls back to a fit. Per set, the shots
6485
6142
  that qualify read what pinning by hand reads: `idle` **18.77** where a whole-root
6486
- run read 41.59, with not one key different (issue #100).
6143
+ run read 41.59, with not one key different.
6487
6144
  - **Fitting per set is worse, so `check` does not.** `fitFraming` registers extent,
6488
6145
  and extent is not alignment (see the โš ๏ธ in ยง9.2), so on a shot whose silhouette
6489
6146
  genuinely differs one shot's frames do not constrain the fit enough: `hit` reads
@@ -6491,8 +6148,7 @@ Why the split falls there, both halves measured on an 8-shot character (147 fram
6491
6148
  set reads 101.94 against 42.98. More frames is a better-conditioned fit, so the
6492
6149
  fallback is deliberately the shared one.
6493
6150
 
6494
- `--framing shared` measures **every** set in the shared framing, which is what a
6495
- whole-root run used to do. It answers one question and it is a good one โ€” *does a
6151
+ `--framing shared` measures **every** set in the shared framing. It answers one question and it is a good one โ€” *does a
6496
6152
  single box serve every set?* โ€” and it is the wrong number to read as one shot's
6497
6153
  fidelity.
6498
6154
 
@@ -6534,7 +6190,7 @@ and [the ladder's honesty rule](https://github.com/firejune/rigc/blob/main/docs/
6534
6190
  them is a candidate's own reading against rendered frames โ€” the exam question, not the
6535
6191
  answer key โ€” so none of them narrows a reference-side measure, and a guide that censored
6536
6192
  them would be teaching less for no gain in honesty. The criterion is under *The honesty
6537
- rule* in [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) (issue #158); what it *does* seal is a score written
6193
+ rule* in [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md); what it *does* seal is a score written
6538
6194
  over a reference's own count, and no such figure appears here.
6539
6195
 
6540
6196
  ๐Ÿšจ **If you drive the runtime yourself, a bone's local transform lives on
@@ -6547,8 +6203,8 @@ first thing it hits is not a subtlety. **spine-core 4.3 keeps a bone's local
6547
6203
  transform on `bone.pose`, not on the bone**, so `bone.rotation = โ€ฆ` โ€” or `.x`,
6548
6204
  `.y`, `.scaleX` โ€” is neither an error nor a rotation: it adds a property nothing
6549
6205
  reads, and every frame renders as the setup pose. Write `bone.pose.rotation`.
6550
- Rung 8 lost a loop to it at a flat 17.3; driven through `bone.pose`, the same
6551
- poses measured **2.76**.
6206
+ Written on the bone, rung 8's poses read a flat 17.3; driven through `bone.pose`,
6207
+ the same poses measure **2.76**.
6552
6208
 
6553
6209
  ๐Ÿšจ **That trap has a twin one level up: the *setup* transform lives on
6554
6210
  `bone.data.setupPose`, not on `bone.data`.** `BoneData` extends `PosedData`,
@@ -6561,8 +6217,8 @@ trap, because nothing on the path raises: `undefined` propagates to `NaN`, and
6561
6217
  file, the next build reads those as zero, `validate` is green and `check` runs.
6562
6218
  โ‡’ **A `null` in your own placements dump is the signature of having read
6563
6219
  `bone.data` directly** โ€” nothing in this format is ever legitimately null. On
6564
- spineboy it cost the candidate MAE 13.0 โ†’ 114.6 with a green gate throughout
6565
- (measured in a spineboy attempt's own loop log, ยง4.1). Note that the two names are not
6220
+ spineboy it costs the candidate MAE 13.0 โ†’ 114.6 with a green gate throughout.
6221
+ Note that the two names are not
6566
6222
  the same thing: `bone.data.setupPose` is
6567
6223
  the setup transform, while `bone.setupPose()` on a `Bone` is the method that
6568
6224
  resets `bone.pose` back to it.
@@ -6572,9 +6228,7 @@ resets `bone.pose` back to it.
6572
6228
  stores; what gets drawn is that stored quad โ€” `computeWorldVertices` reads
6573
6229
  `getOffsets(pose)`, never the fields โ€” so writing them is, again, neither an
6574
6230
  error nor a move. **Call `attachment.updateSequence()` after every write**, or
6575
- every frame renders the quad it was loaded with. A setup fit on spineboy ran
6576
- 4,500 renders and reported the same number for all of them before this surfaced
6577
- (the same loop log, ยง4.2).
6231
+ every frame renders the quad it was loaded with.
6578
6232
 
6579
6233
  โ‡’ **An MAE that is identical across every pose, and that does not move for any
6580
6234
  parameter you sweep โ€” a bone's local transform, an attachment's offsets โ€” is one
@@ -6751,8 +6405,7 @@ of them.
6751
6405
  ```
6752
6406
 
6753
6407
  **Read the framing block first.** Everything below it is computed on the grid it
6754
- chose, so an error there arrives disguised as motion โ€” which is exactly what
6755
- happened to two honest ladder runs before this was fixed (issue #34).
6408
+ chose, so an error there arrives disguised as motion.
6756
6409
 
6757
6410
  Unless the frames' own box already fits you (above), your candidate is framed **by
6758
6411
  its own drawn pixels**. `check` renders it at the frames' own rate and grid, takes
@@ -6786,9 +6439,8 @@ pass count, then one of
6786
6439
 
6787
6440
  That procedure is blind to the two things it must be blind to. **An invisible
6788
6441
  margin cannot move it**: a region's quad runs past its own artwork wherever the art
6789
- is transparent, and that used to set the scale; now nothing outside the drawing is
6790
- looked at (the selftest proves art padded by 20 px on two sides reports numbers
6791
- identical to the last decimal). **A choice of units cannot move it either**: a rig
6442
+ is transparent, and nothing outside the drawing is looked at: art padded by 20 px
6443
+ on two sides reports numbers identical to the last decimal. **A choice of units cannot move it either**: a rig
6792
6444
  scaled by 2 % renders to the same pixels and reads the same MAE.
6793
6445
 
6794
6446
  The lines, in order:
@@ -6832,11 +6484,11 @@ a difference that would have been cheaper to leave alone. Measured floor: about
6832
6484
  third of a pixel on the ladder's shots. On most that is invisible; on a small
6833
6485
  high-contrast frame it is worth a point or two of MAE โ€” rung 6 measured five, and
6834
6486
  on the spineboy sets a **constant** one or two pixels was worth 10โ€“30 % of the
6835
- figure (issue #146). This is the floor the frames' own box has no share in, which is
6487
+ figure. This is the floor the frames' own box has no share in, which is
6836
6488
  why `check` prefers that box whenever your pixels are measured to land in it;
6837
6489
  `--viewport` is how you stop it in the cases that box does not cover.
6838
6490
 
6839
- โญ **What a fitted framing now does about it: one final whole-pixel pass.** After
6491
+ โญ **What a fitted framing does about it: one final whole-pixel pass.** After
6840
6492
  the fit settles (or cycles), `check` searches every whole-pixel offset within ยฑ2 px
6841
6493
  for the lowest MAE over the reference's own drawn pixels and moves the box to the
6842
6494
  best one, when that is worth at least 1 % of the figure. So a fitted set's numbers
@@ -6869,10 +6521,7 @@ blunter.
6869
6521
 
6870
6522
  โš ๏ธ **Half of that denominator is yours, so do not optimise against it.** The union
6871
6523
  is the pixels *either* side drew, and a large, mostly transparent sprite adds many
6872
- cheap pixels to it โ€” so the *mean falls* on a candidate that got worse. That is not
6873
- hypothetical: spineboy-2's muzzle flare walked its own scale to 13x under a fitting
6874
- loop doing exactly this, and cost every set in that run its framing (issue #119).
6875
- So the line under the MAE divides the same difference by the pixels the
6524
+ cheap pixels to it โ€” so the *mean falls* on a candidate that got worse. So the line under the MAE divides the same difference by the pixels the
6876
6525
  **reference** drew, a denominator nothing you do can grow. Read the union figure to
6877
6526
  compare two builds of your own rig, where both sides cover about the same ground,
6878
6527
  and the reference-denominator figure when you are deciding whether a change made
@@ -6915,7 +6564,7 @@ appear under the MAE:
6915
6564
  is `|MAE โˆ’ above| โ‰ค floor`. So `MAE โˆ’ above` is what the texture explained on these
6916
6565
  frames and `floor` is the most it could ever explain. **A floor near zero is a proof
6917
6566
  that the texture is not your problem** โ€” which is a finding worth having before a
6918
- day of key-hunting, and one the old recipe could not give.
6567
+ day of key-hunting.
6919
6568
 
6920
6569
  Measured on rung 3: **6.13 / 6.01** with the candidate's own full-resolution atlas,
6921
6570
  of which **4.45 and 4.10 is texture** โ€” about **70 %** โ€” leaving **1.68 / 1.91** that
@@ -6930,8 +6579,8 @@ pixel* across stopped being visible at half scale, so a substituted run can repo
6930
6579
  frame-change disagreement the graded run does not have. Read it for the floor, never as
6931
6580
  the verdict.
6932
6581
 
6933
- ๐Ÿšจ **Do not run this with `--atlas`, which is what the recipe used to say and which
6934
- substitutes region *geometry* along with the texture.** `--atlas` names **your own**
6582
+ ๐Ÿšจ **Do not run this with `--atlas`, which substitutes region *geometry* along with
6583
+ the texture.** `--atlas` names **your own**
6935
6584
  atlas for the case where it is not beside your skeleton; point it at a foreign one and
6936
6585
  the skeleton is re-loaded against that atlas. An atlas entry is not only a page and a
6937
6586
  rectangle โ€” it carries **`rotate:`** and the trim offsets that say where the opaque part
@@ -6940,15 +6589,14 @@ your attachments are re-seated and the quads change. Worse, `spine-core` impleme
6940
6589
  region's rotated corner assignment for `rotate: 90` and for nothing else, so a
6941
6590
  270-packed region is sampled from the wrong part of the page outright.
6942
6591
 
6943
- โ‡’ **The tell was unmistakable and it is the reason this is now a flag of its own: the
6944
- number went the wrong way.** A texture floor can only *explain* error, so a diagnostic
6592
+ โ‡’ **The tell is that the number goes the wrong way.** A texture floor can only *explain* error, so a diagnostic
6945
6593
  that sends the MAE **up** on every set has substituted geometry rather than pixels.
6946
- Rung 7 is that case โ€” its pack is `rotate: 270` โ€” and under `--atlas` it read
6947
- 24.93 โ†’ 28.84 and 19.17 โ†’ 27.75, which had to be recorded as **inconclusive**. Under
6594
+ Rung 7 is that case โ€” its pack is `rotate: 270` โ€” and under `--atlas` it reads
6595
+ 24.93 โ†’ 28.84 and 19.17 โ†’ 27.75, which cannot be read as a floor at all. Under
6948
6596
  `--texture-from` the same rung measures a floor of **1.7 MAE** on every set, about 2 %
6949
6597
  of each figure: there *is* a floor, it is small, and the rig is the story after all.
6950
6598
 
6951
- โ‡’ **You no longer have to inspect the atlas for `rotate` or trim before running it.**
6599
+ โ‡’ **You do not have to inspect the atlas for `rotate` or trim before running it.**
6952
6600
  `--texture-from` keeps every world vertex where your own atlas put it and remaps only
6953
6601
  the texture coordinates, through the drawing's own coordinate space, so a rotated or
6954
6602
  trimmed pack lands the same artwork in the same place. If a region of yours is missing
@@ -6987,7 +6635,7 @@ same failure as capturing a guard's expected value from a screen that is already
6987
6635
  broken: the baseline records the defect, and then the *repair* is what looks wrong.
6988
6636
 
6989
6637
  โš–๏ธ **`frames.json`'s own box, and the `declared` line that says whether you got it.**
6990
- Every set now prints one, taken or refused, with the numbers that decided:
6638
+ Every set prints one, taken or refused, with the numbers that decided:
6991
6639
 
6992
6640
  ```
6993
6641
  declared frames.json's own box: TAKEN, coincident โ€” a fit there asks for 0.08 px, under
@@ -7005,14 +6653,14 @@ correction, which is what a different origin or a different unit looks like, and
7005
6653
 
7006
6654
  โš ๏ธ **The test is on extent, and extent is not the same claim as coordinates.** A
7007
6655
  candidate authored in the frames' own world units โ€” one whose setup box lands on the
7008
- reference's to the pixel โ€” used to fail it whenever its union content box differed by
7009
- a few per cent at the extremes, because `fitFraming` registers extent and a box 6 %
7010
- narrower reads as 6 % of scale, which is arithmetically what a units error reads as.
7011
- Rung 7 was refused on **all twelve** of its sets that way, and the fitted framing it
7012
- fell back to cost every one of them 0.28โ€“2.01 MAE against the declared box. That cost
7013
- was real and it was **not** a sign the coordinates were wrong.
7014
-
7015
- โ‡’ **`check` now separates the two by asking whether one similarity can explain the
6656
+ reference's to the pixel โ€” fails an extent test whenever its union content box
6657
+ differs by a few per cent at the extremes, because `fitFraming` registers extent and a
6658
+ box 6 % narrower reads as 6 % of scale, which is arithmetically what a units error
6659
+ reads as. On extent alone, rung 7 is refused on **all twelve** of its sets, and the
6660
+ fitted framing it falls back to costs every one of them 0.28โ€“2.01 MAE against the
6661
+ declared box โ€” a real cost, and **not** a sign the coordinates are wrong.
6662
+
6663
+ โ‡’ **`check` separates the two by asking whether one similarity can explain the
7016
6664
  disagreement at all.** A difference of units or of origin *is* a similarity, so the fit
7017
6665
  absorbs it exactly and leaves a residual near zero โ€” a rig at 2 % different units leaves
7018
6666
  0.27 px rms. A silhouette differing at the extremes is not a similarity, so the residual
@@ -7055,8 +6703,7 @@ frame and wrong only in the relation between two:
7055
6703
  structure as matching. The column says `the reference moves here and yours holds
7056
6704
  still`.
7057
6705
 
7058
- Both of those were found by that run writing its own render-diff by hand. Read this
7059
- line whenever the MAE is flat and something still looks wrong: a flat MAE says the
6706
+ Read this line whenever the MAE is flat and something still looks wrong: a flat MAE says the
7060
6707
  framing and the art agree, and it says nothing at all about whether your shot holds
7061
6708
  and blinks where the reference does.
7062
6709
 
@@ -7075,8 +6722,7 @@ shifts under three world units and comes back, so the two coarse samples land on
7075
6722
  same pose either side of it. So ยง10.3's *key both ends of the hold* has to be applied
7076
6723
  **at each committed rate separately**, and equal values are not enough โ€” the two
7077
6724
  samples have to be **keys**, or a planner reduces through one of them and an
7078
- interpolant inside its own tolerance is not equality. That cost two builds on that
7079
- run, the second of them for exactly that reason. โ‡’ Measure the frames' own
6725
+ interpolant inside its own tolerance is not equality. โ‡’ Measure the frames' own
7080
6726
  frame-to-frame change **once per committed rate**, and where a rate holds, pin both
7081
6727
  ends as keys whenever a finer rate moves between them.
7082
6728
 
@@ -7120,9 +6766,9 @@ There are two matchers and the `how` column says which one answered:
7120
6766
  much wider than its box, and may not contain another of your parts' ink. The last
7121
6767
  is the one a dominant part slips through otherwise โ€” rung 2's course is 81 % of a
7122
6768
  blob that also holds the water, the panel and both rings, so the blob is only
7123
- 1.24x its ink and no wider than its box, and the reported *"course drift 11.2 px"*
7124
- was the distance to a five-part centroid (issue #37). It now falls to the matcher
7125
- below and reads 0.0 px.
6769
+ 1.24x its ink and no wider than its box; on size and width alone, *"course drift
6770
+ 11.2 px"* would be the distance to a five-part centroid. The third condition sends
6771
+ it to the matcher below, which reads 0.0 px.
7126
6772
  - `tmpl 0.62` โ€” the reference merged your slot into a neighbour (they touch, or one
7127
6773
  is drawn over the other), so the fallback rendered **your slot on its own** and
7128
6774
  correlated it against the reference around where you drew it. The number is the
@@ -7133,9 +6779,9 @@ There are two matchers and the `how` column says which one answered:
7133
6779
  template pixel you draw something over cannot match the reference wherever the
7134
6780
  slot really is, so it adds the same residual at every offset โ€” and, because
7135
6781
  sliding the template moves those samples onto other pixels, their gradient
7136
- decides the winner wherever the visible basin is shallow. That is not a
7137
- hypothetical: it is what made four of the seven examples in this repository
7138
- report 0.8โ€“2.2 px against frames rendered from themselves (issue #698).
6782
+ decides the winner wherever the visible basin is shallow. Correlated with those
6783
+ pixels, four of the seven examples in this repository report 0.8โ€“2.2 px against
6784
+ frames rendered from themselves.
7139
6785
 
7140
6786
  โš ๏ธ **Both matchers are capped, and a blank is a real answer.** A part can be
7141
6787
  displaced by about its own size and still be that part in the picture; past that,
@@ -7208,24 +6854,17 @@ never against a number from a page about another rig.** A component match prints
7208
6854
  the other sentence โ€” two centroids have no such bound, only the search radius.
7209
6855
 
7210
6856
  ๐Ÿšจ **The clamp bounds the figure only while the whole-pixel winner is the
7211
- identity, and for a while nothing checked that second half.** Issue #698: four of
7212
- the seven examples in this repository read **0.81, 1.11, 2.13 and 2.21 px** against
7213
- frames rendered from themselves, because the template carried the pixels the
7214
- candidate draws *over itself*. Those match nothing wherever the slot really is, so
7215
- they add a residual at every offset โ€” and sliding the template moves them onto
7216
- other pixels, so their gradient walks the winner off the origin wherever the
7217
- visible basin is shallow. The exhaustive whole-pixel field for `flex`'s backdrop
7218
- had its minimum at `(2, 0)` scoring 7.75 against the identity offset's 8.76.
7219
- Correlating only what shows put all seven back on `(0, 0)`, and `C25`โ€“`C30` of
7220
- the repository's own selftest gate that over every example it ships โ€” so the next
7221
- one is gated by arriving.
7222
-
7223
- ๐Ÿ”ธ The same page said `3.7 px` before issue #678, for an unrelated reason worth
7224
- keeping: the coarse sweep started at `โˆ’radius` and stepped by its stride, so the
7225
- identity offset was on the lattice only when the stride divided the radius, and
7226
- the `ยฑ1` refinement around a winner two pixels out could not reach back to it.
7227
-
7228
- **A second example, and the one the repair was measured on.** `gallery/flex` draws
6857
+ identity.** That is why the template carries only what shows: with the pixels the
6858
+ candidate draws *over itself* in it, four of the seven examples in this repository
6859
+ read **0.81, 1.11, 2.13 and 2.21 px** against frames rendered from themselves.
6860
+ Those pixels match nothing wherever the slot really is, so they add a residual at
6861
+ every offset โ€” and sliding the template moves them onto other pixels, so their
6862
+ gradient walks the winner off the origin wherever the visible basin is shallow. The
6863
+ exhaustive whole-pixel field for `flex`'s backdrop then has its minimum at `(2, 0)`
6864
+ scoring 7.75 against the identity offset's 8.76. Correlating only what shows puts
6865
+ all seven on `(0, 0)`.
6866
+
6867
+ **A second example, and the one that paragraph was measured on.** `gallery/flex` draws
7229
6868
  a banner and a leaf over a full-stage backdrop, so almost every slot of it reaches
7230
6869
  the template matcher:
7231
6870
 
@@ -7245,8 +6884,6 @@ bun cli.ts check --candidate <build> --frames <frames>
7245
6884
  per-frame all 29 adjacent pair(s) change by as much as the reference's own frames do
7246
6885
  ```
7247
6886
 
7248
- That figure was **2.21 px** on the same command before #698, on the same slot.
7249
-
7250
6887
  โญ **And the same frames plus two deliberately wrong builds are what say which column
7251
6888
  answers which question.** Each differs from the build above in exactly one way โ€”
7252
6889
  every easing reversed `(x1,y1,x2,y2) โ†’ (1โˆ’x2,1โˆ’y2,1โˆ’x1,1โˆ’y1)`, and the ball's bottom
@@ -7349,9 +6986,9 @@ about your bone tree rather than about a hole in your figure.
7349
6986
  - **A difference smaller than the render scale.** At rung 3's 0.117 px per unit, a
7350
6987
  key 4 units out moves nothing. Author to the frames' precision and record that
7351
6988
  the rest was not checkable.
7352
- - **Whether a mesh is deformed or merely posed.** Since #27 the rasteriser draws
6989
+ - **Whether a mesh is deformed or merely posed.** The rasteriser draws
7353
6990
  meshes โ€” weighted, deformed, both โ€” so a rung with meshes is measurable. What it
7354
- still cannot tell you is *how* a silhouette got its shape: a hull moved by a
6991
+ cannot tell you is *how* a silhouette got its shape: a hull moved by a
7355
6992
  bone chain and the same hull moved by deform keys render to the same pixels, and
7356
6993
  the frames cannot separate them. Choose on what the rig has to do next, not on
7357
6994
  what the frames appear to say. โš ๏ธ **But `bench` does see it.** Take a disc that
@@ -7385,9 +7022,9 @@ about your bone tree rather than about a hole in your figure.
7385
7022
  was, and the interesting ones are interesting because it left the interval. Rung
7386
7023
  4's ball makes contact entirely inside one twelfth of a second: the sheet puts it
7387
7024
  **40 px below both 12 fps frames that bracket it**, while its x sits within their
7388
- own range. A search whose reach was scaled from those two frames' own step
7389
- therefore looked ยฑ9 px for something 40 px away and reported the interpolation
7390
- back, twice, before the tile was read rather than inferred.
7025
+ own range. A search whose reach is scaled from those two frames' own step
7026
+ therefore looks ยฑ9 px for something 40 px away and reports the interpolation
7027
+ back: read the tile rather than inferring it.
7391
7028
 
7392
7029
  ---
7393
7030
 
@@ -7409,18 +7046,10 @@ frames. Every line is marked with where it comes from:
7409
7046
  - ๐Ÿ“— **stated** โ€” quoted or paraphrased from the page linked in the line.
7410
7047
  - ๐Ÿงฉ **inferred** โ€” this guide's reading of those pages. Spine does not say it.
7411
7048
  - ๐Ÿ”ฌ **observed** โ€” read off the editor's own export of a rigc build, not from a
7412
- page. Two round trips stand behind these: [issue
7413
- #285](https://github.com/firejune/rigc/issues/285) (Spine 4.3.23) and the
7414
- eight-rig trip of 2026-09-16 (Spine **4.3.26**), whose findings are collected in
7415
- ยง10.6. โš ๏ธ This legend said *"used only where rigc now emits the same thing"*,
7416
- which was true while every observation had already been adopted; ยง10.6 then
7417
- carried one that had **not** been โ€” the path `lengths` disagreement โ€” so an
7418
- observation is now marked by where it was read, and each says for itself
7419
- whether rigc agrees with it. โญ That outstanding one has since been adopted
7420
- ([#560](https://github.com/firejune/rigc/issues/560)) and the legend is kept in
7421
- this shape anyway: the reason to mark an observation by its source rather than
7422
- by whether rigc follows it is that the second fact goes stale and the first
7423
- does not.
7049
+ page. Two round trips stand behind these: one through Spine 4.3.23 and an
7050
+ eight-rig trip through Spine **4.3.26**, whose findings are collected in ยง10.6.
7051
+ An observation is marked by where it was read, and each says for itself whether
7052
+ rigc agrees with it.
7424
7053
 
7425
7054
  ### 10.1 Structure
7426
7055
 
@@ -7461,11 +7090,11 @@ a rig whose names miss reads near zero on all of them however well it is built.
7461
7090
  carry each part's own name straight through โ€” PNG basename โ†’ slot โ†’ attachment, and
7462
7091
  the bone that moves it โ€” instead of inventing a scheme of your own.
7463
7092
 
7464
- **Both directions are measured.** The one run whose art shipped a separate PNG per
7465
- body part, each named for the part, applied this deliberately and posted `names`
7466
- measures **an order of magnitude** above anything on the ladder before it โ€” without
7467
- either side seeing the other. Every honest run before it read near zero on the same
7468
- measures. It is the largest single thing any run has got out of a convention.
7093
+ **Both directions are measured.** On the ladder, a run whose art ships a separate
7094
+ PNG per body part, each named for the part, and that carries those names through,
7095
+ posts `names` measures **an order of magnitude** above runs that invent their own โ€”
7096
+ without either side seeing the other. It is the largest single thing a convention
7097
+ has bought on the ladder.
7469
7098
 
7470
7099
  โš ๏ธ **And the other half, which matters just as much: when the art is *not* named
7471
7100
  after the parts, no naming strategy beats any other and the measure is noise.** On a
@@ -7486,16 +7115,13 @@ two animations' 16 and 2 bone-timeline keys all came back sorted, while the 30
7486
7115
  `bones`, 24 `slots` and 3 `constraints` โ€” arrays โ€” came back in the build's own
7487
7116
  order, element for element, and each slider kept its place among them.
7488
7117
 
7489
- ๐Ÿšจ **Not every array: `skins` it re-sorts.** That sentence read *"leaves every
7490
- ARRAY alone"* for two releases, on those three arrays and nothing else, and
7491
- `skins` is the one it was wrong about โ€” see the paragraph at the end of this
7118
+ ๐Ÿšจ **Not every array: `skins` it re-sorts** โ€” see the paragraph at the end of this
7492
7119
  section.
7493
7120
 
7494
7121
  โš ๏ธ **The order it sorts them into is natural and case-insensitive, not
7495
- codepoint.** This paragraph said codepoint until
7496
- [#539](https://github.com/firejune/rigc/issues/539) measured it: `Turn, sweep,
7497
- wave` came back `sweep, Turn, wave` and `turn10, turn2, zoom` came back
7498
- `turn2, turn10, zoom`. The corpus says the same thing and always did โ€” of its 105
7122
+ codepoint.** `Turn, sweep, wave` came back `sweep, Turn, wave` and
7123
+ `turn10, turn2, zoom` came back `turn2, turn10, zoom`. The corpus says the same
7124
+ thing โ€” of its 105
7499
7125
  name-keyed collections, 102 are consistent with a codepoint sort and **3 are
7500
7126
  not**: `spineboy-pro.json` keys `portal-flare9` *before* `portal-flare10`, which
7501
7127
  no codepoint sort produces. Every natural comparator reproduces all 105. (The
@@ -7506,54 +7132,34 @@ one key, deform blocks counted at each of their three levels;
7506
7132
  โ‡’ in rigc: `animations` is emitted sorted (R10), because it is the one
7507
7133
  object measured here whose ORDER is also an index space โ€” every reference into
7508
7134
  the re-sorted *other* objects is by name on both sides, so nothing moves when
7509
- they are re-keyed. โš ๏ธ Since [#716](https://github.com/firejune/rigc/issues/716)
7510
- a skin's `attachments` slot keys are emitted sorted too, for the **text** and
7511
- for no reference: a rebuild of an editor export is the export only if it is the
7512
- same text (ยง10.6b), and 11 of the twelve under `examples/` keyed a skin in draw
7513
- order where the export sorts it. Nothing is refused on that map โ€” a pair the
7135
+ they are re-keyed. โš ๏ธ A skin's `attachments` slot keys are emitted sorted too,
7136
+ for the **text** and for no reference: a rebuild of an editor export is the export
7137
+ only if it is the same text (ยง10.6b). Nothing is refused on that map โ€” a pair the
7514
7138
  comparator leaves open, or a slot name with a `/` in it, keeps the whole map in
7515
- rigc's order instead. rigc emits **that comparator's own order**, which
7516
- [#728](https://github.com/firejune/rigc/issues/728) then measured in full off five
7517
- stored round trips (R10) rather than quantifying over a family. Sorting the
7518
- 105 collections that way reproduces **105 of 105**, the three codepoint cannot
7519
- included, and refuses none of them; the codepoint rule that stood until
7520
- [#543](https://github.com/firejune/rigc/issues/543) reproduced 102 and refused
7521
- those same 3.
7522
-
7523
- โœ… **`events` is re-keyed too, and the references into it survive it.** The same
7524
- session measured it: `zebra, mike, alpha` came back `alpha, mike, zebra`, and the
7525
- firings still resolved **by name** โ€” `0.3 -> mike`, `0.6 -> alpha`, payloads
7526
- intact (#539). So the editor treats `events` and `animations` differently, and
7139
+ rigc's order instead. rigc emits **that comparator's own order**, measured in full
7140
+ off five stored round trips (R10). Sorting the 105 collections that way reproduces
7141
+ **105 of 105**, the three codepoint cannot included, and refuses none of them.
7142
+
7143
+ โœ… **`events` is re-keyed too, and the references into it survive it.** Measured:
7144
+ `zebra, mike, alpha` came back `alpha, mike, zebra`, and the firings still resolved
7145
+ **by name** โ€” `0.3 -> mike`, `0.6 -> alpha`, payloads intact. So the editor treats `events` and `animations` differently, and
7527
7146
  rigc emits events in the order you declare them.
7528
7147
 
7529
- ๐Ÿšจ **`skins` is re-sorted, with `default` pinned first โ€” measured, and it is the
7530
- first array measured to move.** A four-skin rig built `default, zulu, mike, alpha`
7531
- exported `default, alpha, mike, zulu`
7532
- ([#541](https://github.com/firejune/rigc/issues/541)), and the deform timelines
7148
+ ๐Ÿšจ **`skins` is re-sorted, with `default` pinned first โ€” measured.** A four-skin rig
7149
+ built `default, zulu, mike, alpha` exported `default, alpha, mike, zulu`, and the
7150
+ deform timelines
7533
7151
  came back keyed `mike, zulu` rather than `zulu, mike` with it. Note `alpha` sorts
7534
7152
  before `default` under every candidate comparator and still came back second: the
7535
7153
  default skin is **pinned**, not sorted. It matters for the same reason `animations`
7536
7154
  does โ€” `skins` carries ordinals in the binary half, `skins[readInt()]` for an
7537
- attachment timeline and `skins[skinIndex]` for a linked mesh โ€” so this is the
7538
- `animations` defect (#535) in the collection nobody had checked. โ‡’ in rigc: R11.
7539
-
7540
- โœ… **Which comparator it is was the open half of that, and #728 closed it.**
7541
- `alpha, mike, zulu` is the order codepoint, case-folding and natural order all
7542
- produce, so #541's rig refuted nothing and rigc refused any skin-name pair the
7543
- candidates could disagree about. Two of the five round trips carried one name list
7544
- as **both** collections and both came back in one order, so `skins` and
7545
- `animations` share one comparator โ€” measured, not inferred โ€” and the wider skin
7546
- refusal is gone (R11).
7547
-
7548
- โœ… **The two readings this replaces.** A pull request once called `skins` *measured
7549
- preserved*, on the strength of a one-skin rig where a one-element array comes back
7550
- in order whatever the editor does to it;
7551
- [#544](https://github.com/firejune/rigc/issues/544) corrected that to *unmeasured*,
7552
- and added that it could not be measured because the editor refused a four-skin rig
7553
- on import without printing a word. Both readings were the same generalisation โ€” *an
7554
- editor does not move arrays* โ€” off the three arrays that were measured, and the
7555
- "without a word" was rigc's own harness discarding the editor's stderr, which had
7556
- named the cause all along (ยง3.4.2).
7155
+ attachment timeline and `skins[skinIndex]` for a linked mesh โ€” so it carries the
7156
+ same hazard as `animations`. โ‡’ in rigc: R11.
7157
+
7158
+ โœ… **Which comparator it is, is measured too.** `alpha, mike, zulu` is the order
7159
+ codepoint, case-folding and natural order all produce, so that rig alone decides
7160
+ nothing between them. Two of the five round trips carried one name list as
7161
+ **both** collections and both came back in one order, so `skins` and `animations`
7162
+ share one comparator โ€” measured, not inferred (R11).
7557
7163
 
7558
7164
  ### 10.2 Draw order
7559
7165
 
@@ -7894,15 +7500,15 @@ and a character's body bone almost never has them there, which is the very shape
7894
7500
  paragraph above was written from. Turn the parent by ฮด and a child *at the origin*
7895
7501
  back by ฮด and the child is where it was; a child sitting 10 units off swings through
7896
7502
  an arc of that radius first, and the counter-turn only spins it on the spot. The fold
7897
- moves art, so it **changes the picture**. Measured on the second spineboy run, whose
7898
- `hip` carries no attachment and has three children 9โ€“13 units off it: the fold cost
7899
- **3 MAE on every `idle` frame** โ€” mean 23.0 with it against 19.9 without, same search
7900
- โ€” and it was removed. โ‡’ Read the children's offsets before you fold. At the origin,
7503
+ moves art, so it **changes the picture**. Measured on a spineboy run whose `hip`
7504
+ carries no attachment and has three children 9โ€“13 units off it: the fold costs
7505
+ **3 MAE on every `idle` frame** โ€” mean 23.0 with it against 19.9 without, same
7506
+ search. โ‡’ Read the children's offsets before you fold. At the origin,
7901
7507
  fold: it is exact and it is cheap. Off the origin the degeneracy is still there but
7902
7508
  it is **soft**, not exact, and a soft degeneracy is *regularised, not folded* โ€” leave
7903
7509
  the values alone and add a penalty on the gauge direction to the objective instead.
7904
- That run used **2e-5 per squared degree** of hip rotation: invisible at animator-sized
7905
- angles, and still decisive against the +181ยฐ against โˆ’184ยฐ above.
7510
+ **2e-5 per squared degree** of hip rotation is enough there: invisible at
7511
+ animator-sized angles, and still decisive against the +181ยฐ against โˆ’184ยฐ above.
7906
7512
 
7907
7513
  ### 10.4 Curves
7908
7514
 
@@ -7943,16 +7549,15 @@ by fitting each span's **own** handles, and then writes the nearest entry of a n
7943
7549
  table, has bought a key count at one tolerance and shipped it at another. Nothing in
7944
7550
  the loop can see that: the timeline count, the key count, the curve kinds and the
7945
7551
  duration are all unmoved, so `diff` does not shift and the gate stays green, while
7946
- the rendered result changes by a multiple. Rung 8's first version did exactly this
7947
- and went from **1.07 to 4.65 MAE** โ€” four times its own fit's floor. โ‡’ Two passes.
7552
+ the rendered result changes by a multiple. Measured on rung 8, that goes from
7553
+ **1.07 to 4.65 MAE** โ€” four times the fit's own floor. โ‡’ Two passes.
7948
7554
  Pass A fits freely and exists only to *discover* which shapes the shot uses; those
7949
7555
  are clustered into the table; pass B re-plans **every** timeline under the table it
7950
7556
  will actually write. Never fit free handles and substitute the nearest named shape
7951
7557
  after the fact. (The table's size then trades against the key count at a fixed
7952
7558
  tolerance โ€” that shot ran 4 easings/368 keys, 8/314, 12/300, 16/284 โ€” because a
7953
- richer table holds more spans.) This is rung 6's clamp defect in another suit:
7954
- **a constraint that is not enforced where the value is written is not a
7955
- constraint.**
7559
+ richer table holds more spans.) **A constraint that is not enforced where the
7560
+ value is written is not a constraint.**
7956
7561
 
7957
7562
  ๐Ÿงฉ **โ‡’ A span with no interior sample takes the automatic handles, not linear.** Two
7958
7563
  keys on adjacent samples leave pass B nothing to fit โ€” the samples cannot constrain
@@ -7960,9 +7565,9 @@ that span's shape at all โ€” and a planner with nothing to fit leaves `ease` off
7960
7565
  is linear (ยง4.5): the one shape this section says to argue for rather than default to.
7961
7566
  "No information" is not an argument for constant speed. Take instead the tangents the
7962
7567
  keys either side imply โ€” the editor's own **automatic handles**, quoted above โ€” and
7963
- snap *those* to the nearest table entry, exactly as you would a fitted span. Rung 8's
7964
- second version did this for its adjacent pairs and the samples barely moved (**17.34 โ†’
7965
- 17.26 MAE**) while `curve_kinds` changed wholesale: nil in the frames, large in the
7568
+ snap *those* to the nearest table entry, exactly as you would a fitted span. Done for
7569
+ rung 8's adjacent pairs, the samples barely move (**17.34 โ†’ 17.26 MAE**) while
7570
+ `curve_kinds` changes wholesale: nil in the frames, large in the
7966
7571
  structure, which is the signature of a convention rather than a fidelity fix โ€” and
7967
7572
  this section's whole subject.
7968
7573
 
@@ -7984,7 +7589,7 @@ of key does not have a transition, such as slot attachment or event keys"* โ€”
7984
7589
  every key that carried a curve into a segment whose next key holds the same value on
7985
7590
  every channel comes back as `"curve": "stepped"` โ€” 14 keys on the gallery's `nod`
7986
7591
  alone, with every rendered frame byte-identical, because a curve over a flat segment
7987
- draws nothing. rigc emits the same for a **named** easing (ยง4.5, issue #369), so
7592
+ draws nothing. rigc emits the same for a **named** easing (ยง4.5), so
7988
7593
  `diff`'s `animations.curve_kinds` reads 1.000 between a build and its own export
7989
7594
  rather than charging the hold rewrites against the timing. A raw `curve` stays as
7990
7595
  written: it is the file's own numbers, and the reference corpus has the editor itself
@@ -8012,8 +7617,8 @@ without the box and rebuilds the hull on its own.
8012
7617
  the command line the โ‡’ above is the exception rather than the case.** The
8013
7618
  sentence is still true of an export made without the box; what is measured is
8014
7619
  that `-e json` with no export-settings file does not make one. Every field on the
8015
- nonessential list came back present, unchanged and not zero on round trip 6
8016
- (2026-09-16, Spine 4.3.26): the header's `fps` (24 on `fields`, the one rig that
7620
+ nonessential list came back present, unchanged and not zero on the eight-rig
7621
+ round trip (Spine 4.3.26): the header's `fps` (24 on `fields`, the one rig that
8017
7622
  declares it) and `images`; a mesh's `width`/`height` (64/48) and `edges`
8018
7623
  (16 entries, identical); the editor colours of a **bounding box** (`3cff6bff`), a
8019
7624
  **clipping** polygon (`ff3c6bff`) and a **path** (`ff6b3cff`); and a bone's
@@ -8032,10 +7637,10 @@ binary export is performed using default settings"* โ€”
8032
7637
  those defaults**, so the answer above is measured here and documented nowhere.
8033
7638
  โ‡’ In practice: do not plan around fields being dropped. An export you did not
8034
7639
  personally make without the box is an export that has them, and the round trip is
8035
- therefore **richer** than the build rather than poorer โ€” which is why #368's
8036
- hull-and-edges degradation does not return on a second trip (no import warning in
8037
- any of the eight `roundtrip.log`s, and a five-vertex mesh's `hull: 4` came back
8038
- `4` rather than recomputed to `5`). A nonessential-**off** trip would need an
7640
+ therefore **richer** than the build rather than poorer โ€” which is why a second
7641
+ trip does not degrade a mesh's hull and edges: no import warning on any of the
7642
+ eight rigs, and a five-vertex mesh's `hull: 4` came back `4` rather than
7643
+ recomputed to `5`. A nonessential-**off** trip would need an
8039
7644
  export-settings JSON, and โ“ the key name inside that file is not documented
8040
7645
  either.
8041
7646
 
@@ -8045,18 +7650,15 @@ omit them in raw JSON and every UV collapses, in silence. Name an `image`.
8045
7650
 
8046
7651
  โœ… **You need not imitate the exporter's omissions โ€” the emitter does it for you.**
8047
7652
  Spine's exporter drops fields equal to their default, which is why the format page is
8048
- a long list of *"assume 0 if omitted"*, and since
8049
- [#716](https://github.com/firejune/rigc/issues/716) rigc's emitter drops the same
8050
- ones (R1, ยง2; the table is ยง10.6c). So an author may still state a default โ€” `x: 0`
7653
+ a long list of *"assume 0 if omitted"*, and rigc's emitter drops the same ones
7654
+ (R1, ยง2; the table is ยง10.6c). So an author may still state a default โ€” `x: 0`
8051
7655
  in a rig spec is legitimate and stays in the spec โ€” and the file leaves it out.
8052
- โš ๏ธ This paragraph said *"do not imitate the exporter's omissions โ€ฆ rigc deliberately
8053
- does the opposite"* until then. The advice was re-derived rather than kept: **the
8054
- round trip is the reason.** A rebuild of an editor export that writes back every
8055
- default the export left out is a different file from the export in 2,338 places over
8056
- the twelve under `examples/`, and one that leaves them out is the export's own text
8057
- apart from the header's `hash` and `spine` (`IG83`, and [INGEST ยง2.3](INGEST.md)).
7656
+ **The round trip is the reason.** A rebuild of an editor export that writes back
7657
+ every default the export left out is a different file from the export in 2,338
7658
+ places over the twelve under `examples/`, and one that leaves them out is the
7659
+ export's own text apart from the header's `hash` and `spine` ([INGEST ยง2.3](INGEST.md)).
8058
7660
  The habit worth carrying over is still not *omit defaults* โ€” it is *declare only what
8059
- the shot needs*; what changed is that stating a default costs nothing in the file.
7661
+ the shot needs*; stating a default costs nothing in the file.
8060
7662
 
8061
7663
  ### 10.6 What a round trip gives back
8062
7664
 
@@ -8066,8 +7668,8 @@ is a construct that might vanish there, and an agent cannot see that it did.
8066
7668
 
8067
7669
  ๐Ÿ”ฌ The source is one run: eight discriminator rigs, each built to isolate a group
8068
7670
  of fields, compiled by rigc **0.21.0** (emitting 4.3.13), imported into a licensed
8069
- Spine **4.3.26** through the documented CLI and exported back on 2026-09-16, with
8070
- the predictions written down before anything was opened. **Seven of the eight came
7671
+ Spine **4.3.26** through the documented CLI and exported back, with the
7672
+ predictions written down before anything was opened. **Seven of the eight came
8071
7673
  back differing from their build in three header fields and nothing else** โ€”
8072
7674
  `hash` and `audio`, which the editor adds, and `spine`, which it stamps with its
8073
7675
  own version. The eighth is the path rig, and it is the last bullet here.
@@ -8078,8 +7680,8 @@ comes back `48.0`) and **omitted defaults** โ€” the export drops any field equal
8078
7680
  its parser default, so the header loses `x: 0` and `y: 0`, a bone loses `x: 0`, and
8079
7681
  a key at t=0 loses its `"time": 0`. Every name-keyed object is also re-sorted, per
8080
7682
  ยง10.1. None of those is a loss of information, and each is worth knowing before
8081
- you read a `diff`. ๐Ÿ” Since [#716](https://github.com/firejune/rigc/issues/716) the
8082
- bone's `x: 0` and the key's `"time": 0` are left out by rigc as well (ยง10.6c); the
7683
+ you read a `diff`. ๐Ÿ” rigc leaves out the bone's `x: 0` and the key's `"time": 0`
7684
+ as well (ยง10.6c); the
8083
7685
  header's origin is not, because the 4.3 JSON reader has no default for it
8084
7686
  (`skeletonData.x = skeletonMap.x`, `SkeletonJson.js:70`), so an absent origin loads
8085
7687
  as `undefined` where a written one loads as `0` โ€” writing it is exact for both
@@ -8116,11 +7718,9 @@ evidence that the format will carry what you write.
8116
7718
  - ๐Ÿ”ฌ **A path attachment's `closed: true` and `constantSpeed: false` survive**, and
8117
7719
  so do its `position`, `spacing` and three-channel `mix` timelines โ€” all three
8118
7720
  channels of every `mix` key, and all twelve curve numbers on each key that
8119
- carries a curve. โš ๏ธ Its `lengths` did **not**, which is the last bullet โ€” and
8120
- since [#560](https://github.com/firejune/rigc/issues/560) they do on that rig,
8121
- because rigc emits the computation the editor runs rather than one near it.
8122
- What they are computed ON is a second question, which the round trip could not
8123
- ask and #804 did: see the end of this section.
7721
+ carries a curve. โš ๏ธ Its `lengths` came back recomputed โ€” the paragraph after
7722
+ this list says how, and rigc emits that same computation. What they are
7723
+ computed ON is a second question: see the end of this section.
8124
7724
  - ๐Ÿ”ฌ **`physics.mix` and `physics.reset` timelines survive**, the `reset` key
8125
7725
  included โ€” a key that carries a time and no value at all โ€” and so does the
8126
7726
  physics constraint's setup `mix`.
@@ -8132,8 +7732,7 @@ evidence that the format will carry what you write.
8132
7732
  five-vertex mesh declaring `hull: 4` came back `4`, with its `edges`, `color`,
8133
7733
  `uvs` and `triangles` unchanged. Beside it, on the same rig: the header's
8134
7734
  `referenceScale`, a region's `rotation` / `scaleX` / `scaleY` / `color`, and an
8135
- attachment whose `path` differs from its placeholder (the mechanism of
8136
- [#552](https://github.com/firejune/rigc/issues/552)) all survive. A `--pack`
7735
+ attachment whose `path` differs from its placeholder all survive. A `--pack`
8137
7736
  build round-trips too.
8138
7737
 
8139
7738
  ๐Ÿšจ **The one thing that did not come back is a path attachment's `lengths`, and it
@@ -8150,21 +7749,18 @@ under 4.3.26 (`[152.7006, 305.4012, 458.1019, 610.8025]`) and on an open one und
8150
7749
  โš ๏ธ **That last reading settles the model and cannot settle the spelling.** A
8151
7750
  4-sample chord sum agrees with the runtime's forward difference to about **nine
8152
7751
  significant digits** โ€” *below* what float32 can hold, which is why both spellings
8153
- reproduce both exports exactly, and since issue #716 below what rigc's own file
8154
- holds too, because rigc now writes each number as its float32. Under the six fixed
8155
- decimals it wrote until then the file could tell them apart: on both rigs above
8156
- they rounded apart on the **last** curve, where the running total has accumulated
8157
- most โ€” `610.802519` against `610.802520`, `1127.735817` against `1127.735818`. So
8158
- the editor is the evidence for *what* is computed, and only `PathConstraint`
8159
- itself is evidence for *how* โ€” `PS67`/`PS68` compare the transcription with it at
8160
- double precision.
8161
-
8162
- โญ **rigc emits the forward difference itself** since
8163
- [#560](https://github.com/firejune/rigc/issues/560) โ€” `pathCurveLengths` in
7752
+ reproduce both exports exactly, and below what rigc's own file holds too, because
7753
+ rigc writes each number as its float32. At six fixed decimals the two would round
7754
+ apart on the **last** curve of both rigs above, where the running total has
7755
+ accumulated most โ€” `610.802519` against `610.802520`, `1127.735817` against
7756
+ `1127.735818`. So the editor is the evidence for *what* is computed, and only
7757
+ `PathConstraint` itself is evidence for *how*.
7758
+
7759
+ โญ **rigc emits the forward difference itself** โ€” `pathCurveLengths` in
8164
7760
  [`src/compile.ts`](../src/compile.ts) is those runtime lines transcribed, down to
8165
7761
  `Math.sqrt(dx * dx + dy * dy)` rather than `Math.hypot` and `0.16666667` rather
8166
7762
  than `1 / 6`, both of which change the emitted file. All seven entries of the two
8167
- exports above now come back at the precision the editor prints them, so a path rig
7763
+ exports above come back at the precision the editor prints them, so a path rig
8168
7764
  built here and one authored in the editor parameterise identically. โš ๏ธ The
8169
7765
  consequence for you is a vocabulary one: `lengths` is **not** an arc length. It
8170
7766
  sits about 0.5 % below the arc by construction, so a physical quantity โ€” how far a
@@ -8175,31 +7771,26 @@ out of the artifact.
8175
7771
  wrap-around curve even on an open path** โ€” that open path's fourth entry,
8176
7772
  `2136.228`, is the closed-chain cumulative. The parser allocates `vertexCount / 3`
8177
7773
  and copies whatever is there, and `PathConstraint` reads at most
8178
- `lengths[curveCount]`, so the trailing entry is never read โ€” and since
8179
- [#804](https://github.com/firejune/rigc/issues/804) rigc writes it too, measured
8180
- over the closed chain, so the `gallery/ride` build ends on the same `2136.228`.
8181
-
8182
- ๐Ÿšจ **"Every digit the editor printed" was true of two rigs and false in
8183
- production** ([#804](https://github.com/firejune/rigc/issues/804)). Both rigs above
8184
- are unweighted, unscaled and unconstrained, and the computation is only half of
8185
- the number โ€” the other half is the geometry it is fed, and there the editor and
8186
- rigc differed twice:
8187
-
8188
- - **Scale.** rigc blended weighted vertices through setup matrices that ignored
8189
- bone scale, shear and `inherit`: a 50/50-weighted probe path over a bone at
8190
- scale 2 measured 0.752ร— the runtime's own `curves`. The matrices are the
8191
- runtime's now, and an omitted `lengths` measures within 1e-4 of `curves`
8192
- (`PS186`).
7774
+ `lengths[curveCount]`, so the trailing entry is never read โ€” and rigc writes it
7775
+ too, measured over the closed chain, so the `gallery/ride` build ends on the same `2136.228`.
7776
+
7777
+ ๐Ÿšจ **The computation is only half of the number โ€” the other half is the geometry
7778
+ it is fed.** Both rigs above are unweighted, unscaled and unconstrained; on a path
7779
+ that is not, two things decide what an omitted `lengths` measures:
7780
+
7781
+ - **Scale.** rigc blends weighted vertices through the runtime's own setup
7782
+ matrices โ€” bone scale, shear and `inherit` included โ€” so an omitted `lengths`
7783
+ measures within 1e-4 of the runtime's own `curves`.
8193
7784
  - **Constraints.** The editor measures the pose the first update hands the path
8194
7785
  constraint, with every constraint ordered before it applied; a transform
8195
7786
  constraint on the path's slot bone ordered after it moves nothing. rigc does
8196
7787
  not pose, so it cannot reproduce that โ€” an omitted `lengths` is the
8197
- **unconstrained** setup figure (`PS190`), and on a path a constraint moves at
7788
+ **unconstrained** setup figure, and on a path a constraint moves at
8198
7789
  rest that is not the editor's number.
8199
7790
 
8200
7791
  โ‡’ **A stated `lengths` is carried as stated**, and `ingest` writes the source's
8201
7792
  array into the spec, so a rebuilt export is its export on this field. What rigc
8202
- still measures is an array you left out of a spec you wrote.
7793
+ measures is an array you left out of a spec you wrote.
8203
7794
 
8204
7795
  โ‡’ **What this means for you.** `lengths` is the one number in a path rig you
8205
7796
  cannot check by looking: `diff` does not compare it, and `A33` asks only that it
@@ -8209,29 +7800,25 @@ path constraint, against **0.0000** on the other seven. If you are comparing a
8209
7800
  path rig against an editor reference and everything structural agrees while the
8210
7801
  picture does not, this is the first place to look.
8211
7802
 
8212
- โš ๏ธ **What decides whether it moves a pixel is the POSITION mode**, and an earlier
8213
- reading of this paragraph put it on `spacingMode: proportional`, which is the one
8214
- mode it cannot be: proportional spacing scales *with* the total, and that is
7803
+ โš ๏ธ **What decides whether it moves a pixel is the POSITION mode**, not
7804
+ `spacingMode: proportional`, which is the one mode it cannot be: proportional spacing scales *with* the total, and that is
8215
7805
  exactly what cancels. The rig that drifted is `positionMode: fixed`, where an
8216
7806
  absolute `position` is compared against a total that moved. Under
8217
7807
  `positionMode: percent` the position scales with the total too, so a uniform
8218
- change cancels out of both โ€” measured across #560, `gallery/ride` is
8219
- percent/percent and every one of its 74 rendered frames came back **byte
8220
- identical** on an emitted array all three of whose numbers moved. โ‡’ Read a
7808
+ change cancels out of both โ€” measured: `gallery/ride` is percent/percent, and
7809
+ every one of its 74 rendered frames comes back **byte identical** across an
7810
+ emitted array all three of whose numbers change. โ‡’ Read a
8221
7811
  `lengths` disagreement as *certainly wrong data, and visible only under
8222
7812
  `positionMode: fixed`*.
8223
7813
 
8224
7814
  ### 10.6b The order it writes keys in
8225
7815
 
8226
7816
  ๐Ÿ”ฌ **The editor writes every object's keys in one fixed order per kind of
8227
- object**, and since [#716](https://github.com/firejune/rigc/issues/716) rigc
8228
- writes the same. Read off the twelve exports under `examples/`: for each kind,
7817
+ object**, and rigc writes the same. Read off the twelve exports under `examples/`: for each kind,
8229
7818
  every object's key sequence is a subsequence of one order, no two exports
8230
7819
  contradict each other on any kind, and the table below is that order. It lives
8231
7820
  in one place, [`src/keyorder.ts`](../src/keyorder.ts)'s `EDITOR_KEY_ORDER`, and
8232
- the compiler applies it once to the finished skeleton โ€” `CUR83` holds this table
8233
- to that one, and `IG77` holds that one to the exports on every run with a
8234
- corpus.
7821
+ the compiler applies it once to the finished skeleton.
8235
7822
 
8236
7823
  | Kind | Keys, in the order the editor writes them |
8237
7824
  | --- | --- |
@@ -8274,8 +7861,8 @@ says which.**
8274
7861
  - **A pair no export carries together has no measured order**, and the row
8275
7862
  places it the way rigc already emitted it โ€” a transform constraint's
8276
7863
  `rotation` before `x`, an ik key's `time` before `mix`, a physics
8277
- constraint's `x`/`y` before `rotate`. `src/keyorder.ts` lists every such pair.
8278
- Such a pair did not move, and nothing here says the editor agrees.
7864
+ constraint's `x`/`y` before `rotate`. `src/keyorder.ts` lists every such pair,
7865
+ and nothing here says the editor agrees.
8279
7866
  - **A kind with no row keeps rigc's order whole**: a linked mesh, a path or
8280
7867
  point attachment, a sequence, the path and slider constraints, an event
8281
7868
  definition, and the keys of every timeline the twelve do not key with two
@@ -8289,10 +7876,8 @@ says which.**
8289
7876
  `time` โ€” first in every other key kind the editor writes โ€” behind `value`.
8290
7877
 
8291
7878
  ๐Ÿ”ฌ **Two positions and one fold are measured on a production set, not on
8292
- `examples/`** ([#791](https://github.com/firejune/rigc/issues/791)). The twelve
8293
- exports carry no mesh `path` or `color` and no slot name outside ASCII โ€” `IG84`
8294
- and `IG85` count both on every run with a corpus โ€” so the table cannot learn
8295
- either, and what rigc writes for them was read off 42 production exports
7879
+ `examples/`**. The twelve exports carry no mesh `path` or `color` and no slot
7880
+ name outside ASCII, so the table cannot learn either, and what rigc writes for them was read off 42 production exports
8296
7881
  instead:
8297
7882
 
8298
7883
  - **A mesh attachment writes `path` and `color` right after `type`, `path` before `color` โ€” measured on a production set (#791), not on `examples/`.**
@@ -8300,51 +7885,46 @@ instead:
8300
7885
  `color` wrote that second; no mesh carried both. The row does not list them,
8301
7886
  so the position is the constructor's โ€” `meshTextureKeys` in
8302
7887
  [`src/compile.ts`](../src/compile.ts), which every mesh route spreads right
8303
- after `type` (`S104`). A region's `path` and a linked mesh's keys stay where
7888
+ after `type`. A region's `path` and a linked mesh's keys stay where
8304
7889
  they were: neither was in the set, and an analogy is not a measurement.
8305
7890
  - **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** โ€”
8306
7891
  the ideographic space as a space, the full-width digits as `0`โ€“`9`
8307
7892
  (`EDITOR_NAME_FOLD`). One map of 95 slot keys, 7 of them carrying those
8308
7893
  characters, came back in an order this reproduces 95 of 95 and neither a
8309
- codepoint sort nor the comparator without the fold does (`S105`). It is **not**
7894
+ codepoint sort nor the comparator without the fold does. It is **not**
8310
7895
  `normalize('NFKC')`, though NFKC folds both the same way: NFKC, a classifier
8311
7896
  that reads every Unicode digit as a digit and every space separator as a space,
8312
7897
  and a width fold alone all reproduce that map and disagree past it (on `๏ฌ`,
8313
7898
  `ยฒ`, `ูฃ`, `๏ผก`, a no-break space), and what all three agree on is exactly
8314
7899
  these two classes. The comparator is R10's, so animation and skin names are
8315
7900
  compared after the fold too. A pair the fold does not close โ€” a tab, a
8316
- no-break space โ€” still keeps the map in rigc's order, whole (`S106`).
7901
+ no-break space โ€” still keeps the map in rigc's order, whole.
8317
7902
 
8318
7903
  ๐Ÿ”ธ **An object keyed by NAMES is not a field order**, and the table does not
8319
7904
  touch one. A bone's or a slot's timelines, and a transform constraint's
8320
7905
  `properties` and each `to` inside them, are read into the runtime's arrays in the
8321
7906
  order they are keyed โ€” for timelines, the order they are applied in โ€” so their
8322
- order is more than a key's position, and rigc already emits the export's own on
8323
- all twelve. A skin's `attachments` slot keys are the one name-keyed map this
8324
- changed: sorted by the editor's comparator (ยง10.1). The per-slot maps inside are
7907
+ order is more than a key's position, and rigc emits the export's own on all
7908
+ twelve. A skin's `attachments` slot keys are the one name-keyed map rigc sorts: by
7909
+ the editor's comparator (ยง10.1). The per-slot maps inside are
8325
7910
  the rig's own order.
8326
7911
 
8327
- โ‡’ **What this means for you: nothing to write.** No input format changed and no
8328
- key order is yours to state โ€” a rig spec is read by name, and its field order
8329
- still means nothing. What changed is that a canonical-form comparison of an
8330
- export's rebuild against the export no longer finds an object whose keys sit
8331
- elsewhere (`IG76`). What it found next was the defaults the export leaves out
8332
- and rigc wrote, `"name": null`, and the three header keys (`IG78`) โ€” and since
8333
- ยง10.6c it finds the header's `hash` and `spine` and nothing else (`IG83`).
7912
+ โ‡’ **What this means for you: nothing to write.** No key order is yours to state โ€”
7913
+ a rig spec is read by name, and its field order means nothing. A canonical-form
7914
+ comparison of an export's rebuild against the export finds no object whose keys
7915
+ sit elsewhere, and with ยง10.6c it finds the header's `hash` and `spine` and
7916
+ nothing else.
8334
7917
 
8335
7918
  ### 10.6c The keys it leaves out
8336
7919
 
8337
7920
  The editor writes a key only where its value is not the one the parser reads in its
8338
- absence, and since [#716](https://github.com/firejune/rigc/issues/716) rigc's emitter
8339
- does the same, in one pass over the finished skeleton (`withoutParserDefaults` in
7921
+ absence, and rigc's emitter does the same, in one pass over the finished skeleton (`withoutParserDefaults` in
8340
7922
  `src/keyorder.ts`, run just before ยง10.6b's). A key is left out when its value is
8341
7923
  **exactly** the one below โ€” the float the file would hold, not a value near it โ€” so a
8342
7924
  rotation of `1e-45` is written and a rotation of `0` is not. The table is
8343
7925
  `PARSER_DEFAULTS`, and every row is the linked `spine-core` parser's own fallback
8344
- (`getValue(map, key, default)` in `SkeletonJson.js`); the selftest loads an object of
8345
- each kind with the key at that value and without it, and requires the two to be the
8346
- same `SkeletonData` (`S103` on the in-tree builds, `IG82` on the corpus), and deletes
8347
- every key of every in-tree build in turn to find one the table is missing (`S101`):
7926
+ (`getValue(map, key, default)` in `SkeletonJson.js`), and an object of each kind
7927
+ loads to the same `SkeletonData` with the key at that value and without it:
8348
7928
 
8349
7929
  | Kind | Left out when it is |
8350
7930
  | --- | --- |
@@ -8399,14 +7979,14 @@ read an absent `mixY` as that same `0`. *The key before's* is a sequence key's
8399
7979
  (`skeletonData.x = skeletonMap.x`, `SkeletonJson.js:70`), so an absent origin loads
8400
7980
  as `undefined` and a written `0` as `0`: there is no fallback for them to equal, and
8401
7981
  rigc keeps writing the origin of a declared stage. The editor leaves it out
8402
- (ยง10.6's round trip 6), so a rebuild of an export whose stage sits at `0,0` spells
7982
+ (ยง10.6), so a rebuild of an export whose stage sits at `0,0` spells
8403
7983
  two fields the export does not โ€” `LOSS HEADER_ORIGIN` says so on `ingest`.
8404
7984
  - **A transform constraint's `mixY` and `mixScaleY`.** Their fallback is the
8405
7985
  constraint's `mixX` / `mixScaleX` โ€” but the parser reads those only for a property
8406
7986
  the constraint drives, so on one that drives `y` alone the fallback is the pose's
8407
- initial `0`, not `1`. A row there was tried and measured wrong: it left out
8408
- `8-follow-through-pro-ball`'s `"mixY": 1` on two such constraints, they loaded at
8409
- `0`, and `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` refused the rebuild.
7987
+ initial `0`, not `1`. Measured: a row there would leave out
7988
+ `8-follow-through-pro-ball`'s `"mixY": 1` on two such constraints, they would load
7989
+ at `0`, and `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` refuses that rebuild.
8410
7990
  - **A region's `path`, an attachment's `name`, an event key's payload.** Their
8411
7991
  fallbacks are another value โ€” the attachment's own name, its placeholder, the event
8412
7992
  definition's โ€” which rigc writes only where it differs.
@@ -8417,19 +7997,21 @@ read an absent `mixY` as that same `0`. *The key before's* is a sequence key's
8417
7997
  stated.
8418
7998
  - **Every kind with no row** โ€” the keys of a `shearx`, `sheary`, `inherit`, `alpha`,
8419
7999
  `rgb`, `rgb2`, `rgba2`, path `spacing`/`mix`, physics `gravity`/`reset` or slider
8420
- timeline, a linked mesh, a point attachment. No build the selftest loads and no
8421
- export carries an object of those kinds, and a row nothing loads is a claim about
8422
- the parser nobody has checked โ€” where a missing row only costs a key the parser
8423
- reads the same either way. `S101` names such a key the day a build emits one.
8424
-
8425
- โš ๏ธ **Why 4.3's defaults, when #706 row 4 says they are not every generation's.**
8000
+ timeline, a linked mesh, a point attachment. No in-tree build and no export
8001
+ carries an object of those kinds, and a row nothing loads is a claim about the
8002
+ parser nobody has checked โ€” where a missing row only costs a key the parser reads
8003
+ the same either way.
8004
+
8005
+ โš ๏ธ **Why 4.3's defaults, when they are not every generation's** (row 4 of
8006
+ [GENERATIONS.md](https://github.com/firejune/rigc/blob/main/docs/GENERATIONS.md)'s
8007
+ table).
8426
8008
  An omitted physics `inertia` is 0.5 to the 4.3 parser and 1 to the 4.2 one, and that
8427
8009
  is exactly why a key is left out only against the parser rigc links: the file states
8428
8010
  `"spine": "4.3.13"`, and the parser it is gated against is that one. Writing the
8429
8011
  default out would not make the file safe for a 4.2 reader: that reader applies its
8430
8012
  own defaults to every *other* key the file leaves out as well โ€” every one the editor's
8431
- own 4.3 export leaves out, too โ€” and #706 rows 1, 2 and 6 measure what reading one
8432
- generation's data on another generation's runtime does, none of it repaired by one
8013
+ own 4.3 export leaves out, too โ€” and that table's rows 1, 2 and 6 measure what
8014
+ reading one generation's data on another generation's runtime does, none of it repaired by one
8433
8015
  key written out. That misread is refused where it can be seen, on the reading side
8434
8016
  (`GENERATION_UNSUPPORTED` in `ingest`), rather than written for on the emitting one.
8435
8017
 
@@ -8447,7 +8029,7 @@ answer read off the exports:
8447
8029
  - any figure for keys per second, or for how key density scales with frame rate;
8448
8030
  - which curve type any particular example project or studio actually shipped;
8449
8031
  - whether a **corpus** export โ€” one somebody else made, out of the editor's own
8450
- dialog โ€” was made with Nonessential data checked. โš ๏ธ ยง10.5 now answers this for
8032
+ dialog โ€” was made with Nonessential data checked. โš ๏ธ ยง10.5 answers this for
8451
8033
  the **CLI's** `-e json`, where it is measured; that measurement says nothing
8452
8034
  about an export you were handed, and the two must not be read as one;
8453
8035
  - how many bones, slots or timelines a rig of a given size ought to have;
@@ -8480,21 +8062,10 @@ placements become a rig and a motion, what goes between two key poses when nothi
8480
8062
  was given for the in-betweens, and how to spread candidates so a `vote` ballot
8481
8063
  informs.
8482
8064
 
8483
- ๐Ÿ“ **Instrument re-baseline, 2026-09-03 โ€” [#306](https://github.com/firejune/rigc/issues/306),
8484
- and it applies to ยง12 as well.** The objective behind every `residual` in both
8485
- commands now interpolates the frame in **premultiplied** space: a tap that
8486
- straddles a silhouette weights each texel's colour by how much material is there,
8487
- so a part is no longer charged for disagreeing with the ground it is drawn
8488
- against. Nothing about the fields, the coordinate contract or the refusals
8489
- changed, and no threshold moved โ€” `--max-residual`, `--min-visible` and
8490
- `--anchor-residual` are the numbers they were. โš ๏ธ **What did move is the residuals
8491
- themselves**, downward and by small amounts, most on the parts whose edges are in
8492
- the frame. โ‡’ A residual measured before that date and one measured after are not
8493
- the same measurement; re-read a frame rather than comparing across it. The worked
8494
- figures live in [MOTION.md](MOTION.md) ยง6 and
8495
- [BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md) โ€”
8496
- repository material, not shipped in the package โ€” each with its own re-baseline
8497
- note.
8065
+ ๐Ÿ“ **The objective behind every `residual` here and in ยง12 interpolates the frame in
8066
+ premultiplied space**: a tap that straddles a silhouette weights each texel's colour
8067
+ by how much material is there, so a part is not charged for disagreeing with the
8068
+ ground it is drawn against. The worked figures live in [MOTION.md](MOTION.md) ยง6.
8498
8069
 
8499
8070
  ### 11.1 It measures an input, so nothing here is a score
8500
8071
 
@@ -8592,9 +8163,7 @@ holds nothing because it already contains every angle.
8592
8163
  an edge of a `--rotation` window narrower than a full turn, the detail names it:
8593
8164
  `best placement at scale 0.500, the floor of --scale 0.5,2 โ€” the truth may lie below the window`.
8594
8165
  That is the case where the window is the first thing to move rather than the
8595
- frame or the threshold โ€” eleven parts of one frame came back refused at
8596
- `scale=0.500` against art rendered at `0.311` per part pixel, and the message
8597
- said only that the residual was above `--max-residual`. The sentence โ€” with its
8166
+ frame or the threshold. The sentence โ€” with its
8598
8167
  *"may lie below"* โ€” is printed on a refusal only.
8599
8168
  - ๐Ÿ”’ **An accepted placement that stopped on a wall says which wall too**, beside
8600
8169
  the value it holds on the console line and in `walls`:
@@ -8646,7 +8215,7 @@ two things.
8646
8215
  with the `visibleShare` it was computed on. โš ๏ธ **`visibleShare` is a per-frame
8647
8216
  diagnostic and not a summary statistic** โ€” it is measured through the fitted
8648
8217
  placements, so a median or a mean of it is not comparable across fits. ยง12.3 says
8649
- what that costs and [the 2026-09-03 study](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
8218
+ what that costs and [the `visibleShare` study](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
8650
8219
  measures it โ€” repository material, not shipped in the package.
8651
8220
  - **Hierarchy and attachment geometry**, so the search collapses. A child bone
8652
8221
  whose parent is already placed does not have four degrees of freedom: the rig
@@ -8679,7 +8248,7 @@ for the same frame** โ€” `--anchor poseA.json` โ€” and without one it runs that
8679
8248
  internally, over exactly the parts your candidate draws. A part becomes an anchor
8680
8249
  when `pose` came back **unambiguous** with `residual โ‰ค 0.16` and
8681
8250
  `unexplained โ‰ค 0.45`; those two numbers are `--anchor-residual` and a reported
8682
- field, and they are the 2026-09-03 measurement run's own *clean frame* criterion
8251
+ field, and they are a measurement run's own *clean frame* criterion
8683
8252
  rather than a line invented here.
8684
8253
 
8685
8254
  An anchored part fixes its **whole bone** โ€” four numbers read off the picture for
@@ -8712,9 +8281,9 @@ one of those placements reading `"inward"` to say what it rests on.
8712
8281
  โš ๏ธ **Which means the inward step reaches exactly the bones that BRANCH.** A bone
8713
8282
  whose children form a single sub-chain can never be determined, however good the
8714
8283
  anchor below it is: two equations, four unknowns. That is `no-bracket`, and it is
8715
- a fact about your rig's topology rather than about the frame. On the 2026-09-03
8716
- spineboy candidate `torso` is the only bone in the whole rig that branches โ€” and
8717
- it is the bone that recorded 30 `no-anchor` frames, which is why this exists.
8284
+ a fact about your rig's topology rather than about the frame. On a spineboy
8285
+ candidate `torso` is the only bone in the whole rig that branches โ€” and without
8286
+ this step it is the bone that records 30 `no-anchor` frames.
8718
8287
 
8719
8288
  | Field on `bone.inward` | Meaning |
8720
8289
  | --- | --- |
@@ -8772,9 +8341,8 @@ themselves.
8772
8341
  evidence lives on the anchors below it. The bone's own `residual` and
8773
8342
  `visibleShare` say how much of the answer *the frame can independently confirm* โ€”
8774
8343
  and **the visibility floor still refuses it** when the answer is one nothing in
8775
- the picture can check. That is deliberate, and it is the second time this call has
8776
- been made in `chainfit`: exempting a placement nothing searched was tried for
8777
- anchors and reverted, because it prints a part nobody can see as READ. The floor
8344
+ the picture can check. That is deliberate: exempting a placement nothing searched
8345
+ prints a part nobody can see as READ. The floor
8778
8346
  is about what the picture can confirm, not about how the number was arrived at.
8779
8347
 
8780
8348
  ### 12.3 What the report adds to a `pose` report
@@ -8827,15 +8395,14 @@ the answer landed and is *less* steady, not more (p99 0.6588 against 0.5592).
8827
8395
  **The field is reporting a bistable fit faithfully.** What is safe: reading it beside
8828
8396
  its own residual, on its own frame โ€” which is what it exists for. What is safe as a
8829
8397
  corpus statistic: a **maximum**, which saturates. Full method and evidence:
8830
- [`bench/studies/2026-09-03-visibleshare`](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
8831
- ([#323](https://github.com/firejune/rigc/issues/323)).
8398
+ [`bench/studies/2026-09-03-visibleshare`](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md).
8832
8399
 
8833
8400
  And per part:
8834
8401
 
8835
8402
  | Field | Meaning |
8836
8403
  | --- | --- |
8837
8404
  | `role` | `anchor` (taken from the anchor pass, not re-fitted), `chain` (fitted through the rig), `inward` (**determined** from two or more anchored descendants, with nothing searched โ€” ยง12.2b), `unplaced` |
8838
- | โ€” | โญ **A refused ANCHOR is not a contradiction, and it is the most useful row in the table.** The anchor pass judged that placement over the part's *whole* footprint โ€” all `pose` can see, and blind to what covers it โ€” while this instrument has just measured how much of the part is visible at all. Both readings are true. A refused anchor means *the placement may well be right and the confirmation is missing*, and every part whose `anchoredTo` names that bone rests on it. Measured on the 2026-09-03 corpus, `rear-bracer` clears `pose`'s criterion on 81 of 147 frames at a median visible share of **0.1%** โ€” suppressing the refusal there was tried and prints that as READ. โš ๏ธ **That pair of numbers is on the pre-[#306](https://github.com/firejune/rigc/issues/306) objective** and has not been re-derived: the study is the 2026-09-03 run's own, over its own candidate, and re-running it is a run-scale job rather than a docs edit. A four-frame spot check under #306 (`--min-visible 0`, committed `ess` frames `idle/f0000`, `run/f0002`, `walk/f0004`, `aim/f0000`) moved `rear-bracer`'s anchor residual *down* on all four โ€” 0.1564โ†’0.1555, 0.1519โ†’0.1513, 0.1493โ†’0.1488, 0.1537โ†’0.1534 โ€” and flipped its eligibility on none, while `visibleShare` moved materially on one of the four (0.36โ†’0.89). โ‡’ Read the **shape** of the row, not those two digits. ๐Ÿšจ **And that median in particular is one of the ones the 2026-09-03 study found unsafe**: `rear-bracer`'s share swings by **0.87** on `idle/f0001` inside `pose`'s own convergence band when the chain anchors on `pose`'s criterion โ€” the basis this figure was taken on. The count and the shape are the reading; the 0.1 % is not a number |
8405
+ | โ€” | โญ **A refused ANCHOR is not a contradiction, and it is the most useful row in the table.** The anchor pass judged that placement over the part's *whole* footprint โ€” all `pose` can see, and blind to what covers it โ€” while this instrument has just measured how much of the part is visible at all. Both readings are true. A refused anchor means *the placement may well be right and the confirmation is missing*, and every part whose `anchoredTo` names that bone rests on it. Measured over the 147 committed `ess` frames, `rear-bracer` clears `pose`'s criterion on 81 of 147 frames at a median visible share of **0.1%** โ€” suppressing the refusal there prints that as READ. โš ๏ธ **That pair of numbers was taken before the objective interpolated in premultiplied space (ยง11).** A four-frame spot check on the current objective (`--min-visible 0`, committed `ess` frames `idle/f0000`, `run/f0002`, `walk/f0004`, `aim/f0000`) moved `rear-bracer`'s anchor residual *down* on all four โ€” 0.1564โ†’0.1555, 0.1519โ†’0.1513, 0.1493โ†’0.1488, 0.1537โ†’0.1534 โ€” and flipped its eligibility on none, while `visibleShare` moved materially on one of the four (0.36โ†’0.89). โ‡’ Read the **shape** of the row, not those two digits. ๐Ÿšจ **And that median in particular is one of the ones the `visibleShare` study found unsafe**: `rear-bracer`'s share swings by **0.87** on `idle/f0001` inside `pose`'s own convergence band when the chain anchors on `pose`'s criterion โ€” the basis this figure was taken on. The count and the shape are the reading; the 0.1 % is not a number |
8839
8406
  | โ€” | The **other** parts on an anchored bone are refused on their own numbers too, and there they mean something different again: their placement is the **rig's** prediction from that anchor, so their residual is a measurement of the rig (a goggle plate that will not sit on the head it is parented to shows up exactly here) |
8840
8407
  | `bone` | the bone this hangs off: its `parent`, its `setupRotationDeg`, its `depth` in links from the anchor, `anchoredTo`, the `dof` searched, the `window` taken, the other parts `sharedWith` it on that bone, and `carriedBones` |
8841
8408
  | `bone.dof.pivotFree` | your candidate keys a `translate` timeline on this bone, so the arc this answer sits on has a centre the rig itself moves. The placement is still read off pixels; `localRotationDeg` alone will not reproduce it |
@@ -8857,7 +8424,7 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
8857
8424
  | --- | --- |
8858
8425
  | `--anchor <pose.json>` | use this `rigc pose` report instead of running one. Refused together with `--scale` / `--rotation`, which size the internal pass that then does not happen |
8859
8426
  | `--atlas` | **refused by name.** Every other `--candidate` command takes it, so trying it here is reasonable โ€” but the part art comes from `--images` and the skeleton is all this needs of the candidate, so a flag that silently did nothing would be worse than one that says why |
8860
- | `--hinge <min,max>` | the window each child's local rotation is searched over, in Spine degrees about its setup value. Default `-180,180` โ€” **a full turn, on purpose**: one degree of freedom is cheap enough to sweep exhaustively, and ยง11.4's warning about a window that does not contain the truth applies here too. The hinge step `3ยฐ` is a **ceiling** on the step, not the step: a window it does not divide is divided into whole steps no coarser than it, and `search` states the step that division produced, so `--hinge -20,20` prints `hinge -20ยฐโ€“20ยฐ step 2.857ยฐ (15 rungs)` and each chain part's note says it was searched `in 2.857ยฐ steps`. The full turn divides it exactly, so the default walks the same rungs it always did |
8427
+ | `--hinge <min,max>` | the window each child's local rotation is searched over, in Spine degrees about its setup value. Default `-180,180` โ€” **a full turn, on purpose**: one degree of freedom is cheap enough to sweep exhaustively, and ยง11.4's warning about a window that does not contain the truth applies here too. The hinge step `3ยฐ` is a **ceiling** on the step, not the step: a window it does not divide is divided into whole steps no coarser than it, and `search` states the step that division produced, so `--hinge -20,20` prints `hinge -20ยฐโ€“20ยฐ step 2.857ยฐ (15 rungs)` and each chain part's note says it was searched `in 2.857ยฐ steps`. The full turn divides it exactly |
8861
8428
  | `--stretch <ratio>` | also search a uniform bone scale, this ratio either way. Without it, stretch is searched **only where your own animations key a `scale` timeline on that bone** โ€” a rig that never scales a bone is a rig saying that bone does not stretch |
8862
8429
  | `--min-visible <0..1>` | below this visible share a placement is refused `occluded` instead of reported flat (default `0.25`). A reporting threshold, not a pass bar; the placement is still in the JSON. โš ๏ธ **It is not inert, though**: a bone whose frozen share is under this floor gets one *unmasked* look before its visible set is fixed, so the flag also changes **where parts land** and not only which rows are refused. Two runs at different `--min-visible` are two fits, and their shares are not one column |
8863
8430
  | `--max-residual <0..1>` | as ยง11, over the visible pixels (default `0.25`) |