spine-rigc 0.2.1 โ†’ 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -18,13 +18,33 @@ that can see that, and a run that skips it has verified nothing about the motion
18
18
  - Formats and CLI reference: [README.md](../README.md)
19
19
  - The rig spec's own source-level documentation: [`src/rig.ts`](../src/rig.ts)
20
20
  - The motion spec and emitted shapes: [`src/types.ts`](../src/types.ts)
21
- - What the format holds and rigc covers: [SPEC_COVERAGE.md](SPEC_COVERAGE.md)
21
+ - What the format holds and rigc covers: `docs/SPEC_COVERAGE.md` โ€” ๐Ÿšซ **not an
22
+ authoring input, and deliberately unlinked**: it inventories the benchmark corpus
23
+ skeleton by skeleton, so it is on the ladder run's forbidden list. Named here for a
24
+ maintainer, not offered to a run
22
25
  - Reproducing a shot you were given as pictures: **ยง8**, and read it *before* you
23
- start measuring rather than after; then **ยง9** for the loop that closes it
26
+ start measuring rather than after; **ยง8.1** if the figure has more joints than you
27
+ can measure one at a time; then **ยง9** for the loop that closes it
24
28
  - The conventions an editor user follows without being told โ€” one image per
25
29
  attachment, keying practice, curve kind, draw order โ€” sourced from Spine's own
26
30
  public documentation: **ยง10**
27
31
 
32
+ ๐Ÿ”’ **A ladder run reads this guide in full and does not follow its references out of
33
+ it.** The guide is allowed reading; not everything it cites is. Citations here are
34
+ provenance for a reader of record โ€” the loop that hit a trap, the issue that closed it โ€”
35
+ and following one can arrive at a stored candidate's own spec, at the corpus inventory,
36
+ or at the gate a verdict is read against, none of which a run may open. So: read the
37
+ document, take its numbered sections as the input, and leave its footprints to whoever
38
+ is maintaining it. The rule this states is that an **allowed-reading surface has to be
39
+ closed under reading**; the criterion behind it is under *The honesty rule* in
40
+ [LADDER.md](LADDER.md), and the enumerated allowed and forbidden lists are in
41
+ `bench/runs/README.md`, *What a run may read* โ€” the prompt that starts a run quotes them
42
+ outright, which is the copy that binds.
43
+
44
+ If you were given no brief and no frames โ€” you are rigging somebody's own art rather
45
+ than reproducing a measured shot โ€” none of this applies to you. It is the ladder's
46
+ protocol, not a property of the tool.
47
+
28
48
  ## The vocabulary is Spine's
29
49
 
30
50
  Wherever rigc has no better abstraction it uses **Spine 4.3's own concept, its own
@@ -59,6 +79,7 @@ bun cli.ts check \
59
79
  --frames path/to/reference/frames
60
80
 
61
81
  # read the table โ†’ fix the spec โ†’ build again โ†’ check again
82
+ # โ†ณ read its per-frame column before its MAE โ€” ยง9.2
62
83
  ```
63
84
 
64
85
  `build` compiles, round-trips the result through `@esotericsoftware/spine-core`,
@@ -72,6 +93,18 @@ the animation is the one in the frames, and there is no assertion that could โ€”
72
93
  ยง9. The two run in that order because `check` needs artifacts on disk and `build`
73
94
  only writes them when the gate is green.
74
95
 
96
+ ๐Ÿšจ **Read `check`'s per-frame column before its MAE.** The table's headline figures
97
+ are the MAE and the slot drift, and a reader who came for those will skip the
98
+ `per-frame` line printed under them โ€” but that line is the only thing in this
99
+ toolchain that can see a **hold**, a **loop seam** or a **one-frame event**. Those
100
+ defects are cheap in every single frame and wrong only in the relation between two,
101
+ so an aggregate MAE, `diff` and the gate are all silent on them: a candidate can
102
+ slope a line through a frame pair the reference holds perfectly still across, or
103
+ end a cycle on a pose that is not the pose it began on, without moving a decimal
104
+ anywhere else in the loop. **ยง9.2** documents the column. It is named here because
105
+ ยง0 is where the loop is learned, and a run that opens a report for its chain table
106
+ can come away with the column unread.
107
+
75
108
  What the flags mean:
76
109
 
77
110
  | Flag | Meaning |
@@ -81,7 +114,7 @@ What the flags mean:
81
114
  | `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths are written relative to it |
82
115
  | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory) |
83
116
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
84
- | `--profile` | `spine` = the 18 validity rules ยท `spine-html` = all 32 (**the default**) |
117
+ | `--profile` | `spine` = the 20 validity rules ยท `spine-html` = all 34 (**the default**) |
85
118
 
86
119
  Pick the profile deliberately. `spine-html` adds one renderer's policy and one
87
120
  project's canvas budget, and those rules fire on perfectly correct Spine data
@@ -257,6 +290,14 @@ behind it writes literal `x`/`y` instead.
257
290
  `spec` must be exactly `"rigc-rig/1"`. `name` must be a non-empty string. `bones`
258
291
  must be non-empty. `slots` must be present (it may be empty).
259
292
 
293
+ ๐Ÿšซ **Every example value below is invented.** Names, coordinates, vertex lists and
294
+ payloads in this guide are written to illustrate a field, never copied out of a
295
+ reference export โ€” an example lifted from one would be handing an authoring agent an
296
+ answer to the rung it is standing on, which is the rule ยง10.6 states and the honesty
297
+ rule in [LADDER.md](LADDER.md) turns on. If a snippet here matches a reference file,
298
+ that is a defect in this guide: report it. (It has happened โ€” 2026-08-23; the incident
299
+ is recorded in `bench/runs/README.md`, *What a run may read*.)
300
+
260
301
  ### 3.1 `skeleton` โ€” the header
261
302
 
262
303
  | Field | Spine meaning | Default |
@@ -349,9 +390,9 @@ Geometry comes in one of two fields:
349
390
 
350
391
  ```json
351
392
  "weights": [
352
- [{ "bone": "tail3", "x": 184.91, "y": -2.83, "weight": 0.006 },
353
- { "bone": "tail4", "x": 92.72, "y": -2.83, "weight": 0.994 }],
354
- [{ "bone": "tail4", "x": 84.66, "y": -8.47, "weight": 1 }]
393
+ [{ "bone": "link_a", "x": 40, "y": 0, "weight": 0.25 },
394
+ { "bone": "link_b", "x": -20, "y": 0, "weight": 0.75 }],
395
+ [{ "bone": "link_b", "x": -60, "y": 12, "weight": 1 }]
355
396
  ]
356
397
  ```
357
398
 
@@ -393,6 +434,59 @@ they encode a deformation model rather than a table of numbers, which is why the
393
434
  are code invoked by data. A generator is for a skeleton with **no** manifest; a cut
394
435
  that has one invokes the same builders through the manifest's `mesh` block.
395
436
 
437
+ **Bounding box** ([Spine: bounding boxes](http://esotericsoftware.com/spine-bounding-boxes))
438
+ and **clipping** ([Spine: clipping](http://esotericsoftware.com/spine-clipping))
439
+ attachments โ€” a polygon, and nothing else.
440
+
441
+ **When you need one:** a *bounding box* is a shape the game hit-tests against โ€” a
442
+ hurt box, a pick region, a trigger volume โ€” that follows the skeleton and draws
443
+ nothing. A *clipping* attachment is a **mask**: everything drawn from the slot
444
+ carrying it up to and including `end` is clipped to the polygon, so a window, a
445
+ portal or a wipe is one attachment rather than a second set of art.
446
+
447
+ ```json
448
+ "hitbox_a": { "hitbox_a": { "type": "boundingbox", "vertexCount": 4,
449
+ "vertices": [-30, -10, 30, -10, 30, 50, -30, 50] } },
450
+ "mask_a": { "mask_a": { "type": "clipping", "end": "box", "vertexCount": 3,
451
+ "vertices": [0, 0, 200, 0, 0, 160],
452
+ "color": "ff00ffff" } }
453
+ ```
454
+
455
+ Both polygons above are invented โ€” an axis-aligned rectangle and a right triangle,
456
+ in round numbers, so that nothing here can be mistaken for a shape measured off a
457
+ reference. A real one is measured off your own art (ยง8) or drawn to the volume the
458
+ game needs.
459
+
460
+ | Field | Meaning |
461
+ | --- | --- |
462
+ | `vertexCount` | **required.** How many vertices the polygon has, stated outright โ€” see the warning below |
463
+ | `vertices` / `weights` | the same two encodings a mesh's geometry uses, with the same by-name default and the same `"boneIndexing": "raw"` opt-in |
464
+ | `color` | `rrggbbaa`; the colour the editor draws the outline in |
465
+ | `end` | clipping only. The **last** slot the clip applies to, by name |
466
+ | `convex`, `inverse` | clipping only, 4.3, both default false |
467
+
468
+ ๐Ÿšจ **`vertexCount` has no parser default and rigc will not infer one.** A mesh gets
469
+ its count from `uvs.length`; a polygon has no uvs, and the parser reads
470
+ `map.vertexCount << 1` as the number of coordinates to expect. With the field
471
+ absent that is `undefined << 1` = **0**, so the coordinate array is decoded as a
472
+ *weighted* run โ€” bone counts and weights read out of your x/y pairs โ€” and the
473
+ attachment ends up holding nothing. It loads. Neither type draws a pixel, so
474
+ nothing downstream notices. rigc requires the count and cross-checks it against
475
+ whichever encoding you used; `A33_VERTEX_ATTACHMENT_GEOMETRY` checks it again on
476
+ the artifact.
477
+
478
+ โš ๏ธ **A clipping `end` that names nothing is not an error to Spine.**
479
+ `skeletonData.findSlot` returns `null` on a miss and the parser assigns that null
480
+ without a word, so the clip never ends โ€” it runs to the bottom of the draw order
481
+ and takes every slot below it out of the frame. rigc refuses a name the rig does
482
+ not declare. Omitting `end` entirely is the format's own way of saying "clip
483
+ everything after this one", and is left alone.
484
+
485
+ ๐Ÿšซ Under the default `spine-html` profile a clipping attachment is refused by
486
+ `A11_NO_CLIPPING_ATTACHMENTS` โ€” that renderer skips them silently, so a mask that
487
+ was supposed to hide something would not. It is valid Spine and `--profile spine`
488
+ accepts it; the refusal is policy, not validity.
489
+
396
490
  ### 3.5 `constraints` โ€” 4.3's single typed array
397
491
 
398
492
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
@@ -413,7 +507,38 @@ Field lists are in [`src/rig.ts`](../src/rig.ts); three traps worth carrying her
413
507
  them parses cleanly and does nothing at all. rigc refuses it up front, and `A23`
414
508
  catches it from the other side.
415
509
 
416
- ### 3.6 `invariants` โ€” what the artifact cannot say about itself
510
+ ### 3.6 `events` โ€” names the animation can fire
511
+
512
+ **When you need one:** something outside the skeleton has to happen on a
513
+ particular frame โ€” a footstep sound, a spawn, a hit window opening. Spine
514
+ [events](http://esotericsoftware.com/spine-events) carry no rendering effect at
515
+ all; they are a named signal the game listens for, with an optional payload.
516
+
517
+ The **declaration** lives here, in the rig spec, because the name is structure.
518
+ The **firings** live in the motion spec (ยง4.8), because when they happen is time.
519
+
520
+ ```json
521
+ "events": {
522
+ "cue_a": {},
523
+ "cue_b": { "audio": "cue_b.ogg", "volume": 0.8, "string": "line-01" }
524
+ }
525
+ ```
526
+
527
+ An object keyed by event name โ€” the one top-level collection in the format that
528
+ is not an array. Every field is optional and each is the payload a firing
529
+ **inherits** when it does not override it: `int` (0), `float` (0), `string`
530
+ (`""`), `audio` (none), `volume` and `balance`.
531
+
532
+ - **An empty object is the normal case, not a stub.** Most events carry no payload:
533
+ the name *is* the signal, and the declaration exists so the firings in ยง4.8 have
534
+ something to resolve against. Write `{}` and move on.
535
+ - โš ๏ธ `volume` and `balance` are read **only when `audio` is set**. Without an
536
+ audio path the parser drops them without a word, so rigc refuses that pairing
537
+ rather than emitting two numbers no runtime will read.
538
+ - An event that nothing fires is legitimate: a skeleton may declare the vocabulary
539
+ its game listens for and key only some of it in any one animation.
540
+
541
+ ### 3.7 `invariants` โ€” what the artifact cannot say about itself
417
542
 
418
543
  Optional, and only meaningful for rigc's own formations: `meshSlots` and
419
544
  `meshTriangles` (the two halves of the mesh budget `A13` measures against),
@@ -456,6 +581,7 @@ time puts it here.
456
581
  | `note` | free text |
457
582
  | `tracks` | the timelines |
458
583
  | `drawOrder` | the draw-order timeline โ€” ยง4.7. Not a track: it names no target |
584
+ | `events` | the event timeline โ€” ยง4.8. Not a track, for the same reason |
459
585
 
460
586
  `groups` (`name โ†’ [member, โ€ฆ]`) lets one track target several bones or slots at
461
587
  once; `lag` shifts every key of a track, and `stagger` adds a per-member delay in
@@ -496,6 +622,39 @@ stepped.
496
622
  Seconds, not frames: nothing requires a key to land on any frame grid, and a
497
623
  reference rendered at some rate says nothing about where its keys are. Put keys
498
624
  where the motion changes.
625
+ - **Key times are quantised onto a 1e-6 s grid by rounding DOWN, never to
626
+ nearest.** A key time is a position against the sample grid a player will step,
627
+ and the two directions of a half-step error are not the same size. `2/12 s` and
628
+ `5/30 s` are both 0.16666666โ€ฆ; `0.166667` is *larger* than either, so a key
629
+ emitted there is applied at sample **3** of a 12 fps playback and not sample 2 โ€”
630
+ a whole frame late, with nothing raised. On a **stepped** timeline (an attachment
631
+ timeline always is) that is the wrong picture rather than a slightly wrong value:
632
+ the spineboy run's muzzle flare fired a frame late for exactly this until the
633
+ run's own frame check caught it (issue #99). Rounding down cannot do that; the
634
+ worst it can do is put a key a millionth of a second early, on the sample it was
635
+ written for. โš ๏ธ What this does **not** protect you from is rounding your own
636
+ times before you write them โ€” write `2/12`, not `0.1667`, and let the compiler
637
+ do the quantising.
638
+ - ๐Ÿšจ **Nor does it protect a stepped key whose time is ALREADY on the 1e-6 grid.**
639
+ Rounding down leaves such a time exactly where you wrote it, and the sampler does
640
+ not arrive there: a player โ€” and `sampleAnimation`, and therefore `check` โ€” reaches
641
+ sample *i* by accumulating `1/fps` *i* times, which for many *i* lands a few ULPs
642
+ **below** `i/fps`. `2/12` is saved by the rule above precisely because it is *not*
643
+ on the grid; `0.25`, `0.5`, `0.75`, `1` and every other multiple of `0.25 s` is, and
644
+ a stepped key there sits above the sample that was meant to see it. On an
645
+ interpolated timeline that costs a few ULPs of value and nothing else. On a
646
+ **stepped** one it is the whole frame โ€” and on the last sample it is the whole
647
+ event, because there is no later sample to catch it. Measured on rung 5's 6.5 s
648
+ shot at 12 fps: **13 of its 78 sample times are affected** (f6, f15, f18, f21, f24,
649
+ f27, f60, f63, f66, f69, f72, f75, f78), and an attachment key written at the
650
+ declared duration `6.5` never fired at all against an accumulated
651
+ `6.499999999999994` โ€” which read as a frame-change disagreement the pose series had
652
+ already fixed, and cost that run three builds
653
+ ([`2026-08-26-rung5-1`](../bench/runs/2026-08-26-rung5-1/LOOP.md), ยง8). โ‡’ **For a
654
+ stepped timeline, write `T โˆ’ 1e-6` rather than `T`.** One grid step early cannot
655
+ reach the previous sample โ€” 83,333 ยตs away at 12 fps โ€” and is always seen by the
656
+ sample it was written for; one ULP late loses the frame. This is the same asymmetry
657
+ the rule above turns on, one grid step further in.
499
658
  - **No key may land past the animation's `duration`.** Nothing that plays the
500
659
  animation for the duration it declares ever reaches such a key, so it is a
501
660
  compile error โ€” checked on **every timeline**, not just on the latest key in the
@@ -578,6 +737,53 @@ the emitted file is the parser's requirement rather than a decision of yours, so
578
737
  you state the set of moves and rigc writes them in the order the parser needs.
579
738
  `A31_DRAW_ORDER_OFFSETS_RESOLVE` checks all four from the other side.
580
739
 
740
+ ### 4.8 `events` โ€” firing a declared event
741
+
742
+ The second timeline that names no target, and it sits on the animation for the
743
+ same reason `drawOrder` does: 4.3 writes it as `animations.<a>.events`, beside
744
+ `bones` and `slots`.
745
+
746
+ ```json
747
+ "animations": {
748
+ "walk": {
749
+ "duration": 1.0666666,
750
+ "loop": true,
751
+ "events": [
752
+ { "t": 0, "name": "cue_a" },
753
+ { "t": 0.5333333, "name": "cue_a", "int": 2 }
754
+ ],
755
+ "tracks": []
756
+ }
757
+ }
758
+ ```
759
+
760
+ - `name` must be a key of the rig spec's `events` block (ยง3.6). A miss is a
761
+ compile error here; in raw JSON the parser **throws** `Event not found` in the
762
+ consumer's process, which is late.
763
+ - `int`, `float` and `string` override the declaration's payload **for this firing
764
+ only**. Omit them and the firing inherits the declared defaults, which is what
765
+ the editor writes.
766
+ - `volume` and `balance` are accepted only on an event that declares `audio`, for
767
+ the same reason as ยง3.6.
768
+ - Event keys are **instantaneous** and carry no `ease` or `curve`.
769
+ - Its last key counts towards the declared duration like any other (R7).
770
+
771
+ rigc refuses three things here, and only the first is loud in the parser:
772
+
773
+ | You wrote | You get |
774
+ | --- | --- |
775
+ | a name the rig spec does not declare | `event "X" is not declared in the rig spec's "events" block; declared: โ€ฆ` |
776
+ | a key time earlier than the key before it | `key times must not go backwards (at t=0.25, after t=0.5)` |
777
+ | `volume`/`balance` on an event with no `audio` | `volume is set but event "X" declares no "audio"` |
778
+
779
+ The ordering rule is **non-decreasing**, not strictly increasing: two different
780
+ events on one frame is an ordinary thing to want, and unlike a value track there
781
+ is no contradiction in it. What is refused is going *backwards* โ€”
782
+ `readAnimation` fills frame `i` from key `i` in array order and never sorts, so a
783
+ decreasing time builds an `EventTimeline` whose earlier firing is simply
784
+ unreachable, with a perfectly clean load.
785
+ `A32_EVENT_KEYS_RESOLVE` checks all three from the other side.
786
+
581
787
  ---
582
788
 
583
789
  ## 5. Reading a failure
@@ -609,6 +815,12 @@ the frequent ones, verbatim:
609
815
  | `animation "A" has two tracks on X.property; merge them into one track` | one timeline per target property |
610
816
  | `no stage size: give the rig spec a \`skeleton.width\`/\`skeleton.height\`` | ยง3.1 |
611
817
  | `drawOrder at t=โ€ฆ: slot "X" is not one this rig emits` / `is offset twice in one key` / `puts it at N, outside the โ€ฆ emitted slots` | ยง4.7 |
818
+ | `events at t=โ€ฆ: event "X" is not declared in the rig spec's "events" block` | declare it in the rig spec (ยง3.6), or fix the name |
819
+ | `events: key times must not go backwards` | put the firings in time order (ยง4.8) |
820
+ | `events at t=โ€ฆ: volume is set but event "X" declares no "audio"` | drop `volume`/`balance`, or give the event an audio path |
821
+ | `vertexCount is undefined; a polygon needs at least 3 vertices, stated outright` | give the bounding box or clipping attachment a `vertexCount` (ยง3.4) |
822
+ | `vertexCount N wants M unweighted numbers and "vertices" holds K` | fix the count or the array; they decide the encoding between them |
823
+ | `end names slot "X", which this rig does not declare` | fix the clipping attachment's `end`, or add the slot |
612
824
  | `bone "X" takes its position from โ€ฆ, which needs a cut manifest` | R8 โ€” pass `--manifest`, or write literal `x`/`y` |
613
825
 
614
826
  ### 5.2 Assertions โ€” the gate
@@ -668,6 +880,8 @@ The report prints one line per assertion:
668
880
  | `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
669
881
  | `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
670
882
  | `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 |
883
+ | `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 |
884
+ | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box or clipping polygon 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, or a clipping `end` naming a slot the skeleton does not have (ยง3.4). **SKIP** when the skeleton carries neither type |
671
885
 
672
886
  `both โ—‘` marks a mixed assertion: its validity half always runs and its policy
673
887
  clauses are gated by profile.
@@ -681,9 +895,14 @@ a **`NotImplementedError` naming the field**, because the parser's own behaviour
681
895
  worse: an unknown attachment `type` returns `null` and the attachment disappears,
682
896
  and a constraint entry with an unrecognised `type` matches no case and vanishes.
683
897
 
898
+ Each is deferred for a stated reason, and the reason is the same one in every row:
899
+ **not one of these types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
900
+ parts 3-1 and 4-2), so none of them is on the ladder's critical path. The message
901
+ says so, because a deferral without its reason is a wall rather than a work item.
902
+
684
903
  | You wrote | You get |
685
904
  | --- | --- |
686
- | attachment `type` of `boundingbox`, `point`, `clipping`, `path`, `linkedmesh` | `attachment type "X" is in the Spine 4.3 format and rigc does not emit it yet. Implemented: region, mesh.` |
905
+ | attachment `type` of `point`, `path`, `linkedmesh` | `attachment type "X" is in the Spine 4.3 format and rigc does not emit it yet. Implemented: region, mesh, boundingbox, clipping. point, path and linkedmesh are deliberately deferred: not one of them appears anywhere in the benchmark corpus โ€ฆ` |
687
906
  | constraint `type` of `path` or `slider` | `constraint type "X" โ€ฆ Implemented: ik, transform, physics.` |
688
907
  | mesh `generator.kind` of `contour` | `the "contour" generator would triangulate a part's own alpha mask, and src/mesh.ts has no triangulator` |
689
908
 
@@ -774,10 +993,98 @@ where one part's **interior detail** โ€” a marking, a highlight, anything not on
774
993
  outline โ€” lies inside the other part's area, and see which survives. Then write the
775
994
  slots in that order (R4), because there is no other place in the file to say it.
776
995
 
996
+ **That reads the reference only, so it settles the edges the reference happens to
997
+ show an interior detail on, and no more. There is a second test, and it decides more
998
+ of them: render your own candidate both ways and measure the same feature on both
999
+ sides.** Build the pair, render each back at the frames' own scale, and compare
1000
+ like with like. A part whose unoccluded size you can compute is a ruler โ€” composite
1001
+ it alone, then read how much of it survives in the reference and how much survives
1002
+ in each build. If a part that measures 110 px on its own reads 100 px in the
1003
+ reference, the build that also reads 100 px is covering it the way the reference
1004
+ does and the build that reads 108 px is not, and that is the order. It costs two
1005
+ builds and it reaches edges the frames never show a marking on: on the ladder a brief
1006
+ has settled a single edge of a chain from interior detail and said outright that the
1007
+ frames did not show the rest, and rendering like-for-like settled three more of them
1008
+ โ€” worth a measurable drop in window MAE, a convention the gate cannot see and the
1009
+ measures can.
1010
+
1011
+ **Score that comparison over the pixels where the two builds differ. A whole-shot
1012
+ figure is the wrong feature.** Two builds that differ only in slot order are
1013
+ **bit-identical everywhere the two slots do not overlap**, so a whole-shot MAE
1014
+ divides the evidence by the whole figure and by every frame that carries none of
1015
+ it. What survives that division sits inside the objective's own scatter โ€” real
1016
+ hypotheses and a deliberately reversed control alike land in there, pointing
1017
+ whichever way the noise does, and what has been condemned is the statistic and not
1018
+ the edge. โ‡’ Take the pixels where the two renders differ **at all** and score both
1019
+ builds over exactly that set. Nothing outside it can contribute, so the dilution is
1020
+ gone by construction, and the reading needs no knowledge of which parts are involved
1021
+ โ€” it is the same mechanical test on any structural pair. Read a frame-by-frame tally
1022
+ beside the figure too, because an edge the frames really decide wins shot after shot
1023
+ rather than on a couple of them.
1024
+
1025
+ **Calibrate the band with a control on an edge the brief has already settled by
1026
+ measurement.** Run the same test on that edge, read how far apart the two builds
1027
+ come out over the pixels that decide it, and treat that separation as the scale a
1028
+ real answer is measured against. On the deciding pixels a settled edge separates by
1029
+ a wide margin where the whole-shot figure had it inside its own noise โ€” which is
1030
+ what lets an edge the frames show no interior detail on stop being unanswerable and
1031
+ start being an edge the null-result rule below has no business firing on.
1032
+
1033
+ โš ๏ธ **A control that fails may be a wrong control โ€” read the per-frame rows before
1034
+ you condemn the hypothesis.** A control is a **build**, and a build differs from
1035
+ base in everything the change implies, not only in the thing you meant to change:
1036
+ send one part behind another and it goes behind everything drawn between them too,
1037
+ so what you actually ran is one reversed edge plus several asserted ones. The
1038
+ aggregate will not say so, and it can favour the variant while the per-frame rows
1039
+ give base *every one* of the frames that carry most of the deciding pixels. That
1040
+ split โ€” an aggregate one way, a consistent per-frame tally the other โ€” is the
1041
+ signature of a control that asserts more than one thing, and reading the aggregate
1042
+ alone condemns an edge the brief settles by measurement. The rule the run protocol
1043
+ carries from the other side is the same one: a control that returns an impossible
1044
+ number has told you something, so read the number rather than the pass or fail.
1045
+
1046
+ โš ๏ธ **A render-back sweep whose spread is inside the objective's own scatter is
1047
+ *no answer*, not a weak one.** Rendering candidates back and keeping the best
1048
+ number is not a draw-order trick โ€” it is how any structural choice the frames
1049
+ might decide gets decided, a scale, an offset, an attachment kind, an order โ€” and
1050
+ every one of those sweeps can come back null. **Estimate the objective's own
1051
+ scatter first, or the sweep is not readable at all** โ€” a spread smaller than that
1052
+ is noise wearing a decimal point. Two orders on one ladder shot came out **0.8 %
1053
+ apart over the whole shot and pointing opposite ways**, and a later run swept
1054
+ **three** structural choices and landed inside that scatter on every one of them.
1055
+ A difference that small is not a quiet vote for the winner; it means the frames do
1056
+ not decide this, and there are two honest ways on:
1057
+
1058
+ - **find a second, independent way to get the number** โ€” often by measuring the
1059
+ *art* instead of the render, which needs no build at all. Two of those three
1060
+ sweeps were settled that way.
1061
+ - **or ship it on reasoning, and say in the log that is what you did.** The third
1062
+ one was. What makes that honest is the record โ€” a number that arrived as an
1063
+ argument must not later be read as a measurement.
1064
+
777
1065
  And the general form of all three: **when a reading implies a key, look for a second
778
1066
  way to get the same number before you author it.** A wrong measurement costs one
779
1067
  spurious key; a wrong measurement you believed costs the shape of the whole shot.
780
1068
 
1069
+ โš ๏ธ **A part that grows about a pivot that is not its own centre reads as a part
1070
+ that moves.** Fit a scale about the region centre when the reference scales it about
1071
+ somewhere else, and the residual stays *plausible* while the fitted centre walks
1072
+ along with the fitted scale โ€” so the series looks like a translate you measured
1073
+ rather than a pivot you did not model. The tell is that shape and nothing else: **a
1074
+ per-frame centre that moves monotonically with the fitted scale is an unmodelled
1075
+ pivot.** Read it as motion and you author a translate timeline the reference does
1076
+ not have; read it as a pivot and it is an attachment offset (ยง3.4's `x`/`y`) with
1077
+ the bone's own scale carrying both the size and the centre drift โ€” *one* keyed
1078
+ property, which is also what an editor rig has. Recover the offset by sweeping it
1079
+ against the frames where the part is unoccluded and taking the minimum; the minimum
1080
+ is sharp, and it is a structural constant rather than a per-frame value, so a
1081
+ handful of frames settle it. Measured on a shot with four such parts: MAE **3.13 โ†’
1082
+ 1.95** with not one key value re-measured, and those parts' chains from 10โ€“13
1083
+ `MAE in it` down to 1.3โ€“8.8. What made it believable rather than a lucky fit is the
1084
+ second trap's own cross-check โ€” the four offsets came out the same **fraction of each
1085
+ part's own height**, from four independent fits, which is the quantity that had to
1086
+ agree between them and did.
1087
+
781
1088
  **A value is easier to get right than a curve.** The three traps above are all
782
1089
  about measuring a *value*, and both ladder runs so far found that the values came
783
1090
  out right early: rung 1's key values were exact at every keyframe on the second
@@ -797,16 +1104,188 @@ linear, stepped or bezier and never compares two handle shapes; `check` measures
797
1104
  rendered result, so it can tell you a curve is *wrong* without telling you what it
798
1105
  should have been. What does not follow is that omitting `ease` abstains. It authors
799
1106
  constant speed on every span โ€” the one shape a hand-animated reference almost never
800
- has (ยง10.4) โ€” and the ladder has measured both sides of that bet. Rung 6 keyed
801
- everything linear for exactly the reasoning above and scored `curve_kinds` **34/539**,
802
- its single largest structural gap. Rung 3's second attempt โ€” same brief, same frames โ€”
803
- applied ยง10.4's rule instead and went 41/69 โ†’ 49/69, with `key_counts` rising beside
804
- it and every other section figure unchanged. โ‡’ Take the curve *kind* from what the motion does โ€” starts, stops,
805
- accelerates, settles, falls โ€” rather than from how far apart the keys are; take its
1107
+ has (ยง10.4) โ€” and the ladder has measured both sides of that bet. A run that keyed
1108
+ everything linear for exactly the reasoning above scored under a **sixteenth** of its
1109
+ `curve_kinds` measure, and that was its single largest structural gap. A second
1110
+ attempt at another rung โ€” same brief, same frames, same model as its own first, with
1111
+ ยง10.4's rule added and nothing else โ€” lifted `curve_kinds` by a fifth, with
1112
+ `key_counts` rising beside it and every other section figure unchanged. โ‡’ Take the
1113
+ curve *kind* from what the motion does โ€” starts, stops, accelerates, settles,
1114
+ falls โ€” rather than from how far apart the keys are; take its
806
1115
  shape from ยง10.4's automatic-handle advice and a small reused `easings` table; and
807
1116
  leave `check` to catch the one thing no static reading can, an easing applied the
808
1117
  wrong way round.
809
1118
 
1119
+ ### 8.1 Getting a pose for a figure with a dozen joints
1120
+
1121
+ Everything above measures a *part*, and ยง9 checks a pose you already have. Between
1122
+ them sits the question neither answers โ€” where do a dozen bones go on this frame โ€”
1123
+ and on a character that gap is most of the run. What follows is not Spine's and no
1124
+ public page has an opinion about it, but it decides whether the search converges at
1125
+ all, so it sits here rather than being rediscovered once per figure.
1126
+
1127
+ **Fit the rendered composite, never a part on its own.** The first trap above tells
1128
+ you to measure each part on pixels that can only be that part. On a figure with limbs
1129
+ there are no such pixels: an arm crosses a torso, one leg crosses the other, a held
1130
+ prop is drawn over both, and **every frame is a frame where parts are touching** โ€” the
1131
+ trap with no way out of it. So the objective is the whole picture. Render your
1132
+ candidate through the same rasteriser that drew the reference, into the frames' own
1133
+ viewport, and minimise the difference over the bones' local transforms; that is ยง8's
1134
+ *"look for a second way to get the same number"* applied to a whole pose at once, and
1135
+ it is the render loop **ยง9.1** sends you into. Read ยง9.1's warning about where a
1136
+ bone's local transform lives *before* you write the first sweep โ€” a fitter that is
1137
+ posing nothing reports a flat number and looks like a bad objective.
1138
+
1139
+ โš ๏ธ **Fitting one part at a time in sequence is the same mistake wearing a
1140
+ schedule.** A near arm solved against the composite while the far arm is still
1141
+ wrong is being scored on a picture the other arm is spoiling, and the minimum it
1142
+ walks to is not its own. The knobs come down together, coarse first โ€” the next two
1143
+ rules are how.
1144
+
1145
+ **Compare at a reduced resolution first. At full resolution the objective is flat
1146
+ over the range a joint has to travel.** Sweep one bone alone across the width of the
1147
+ figure against a single frame and watch the number: at full resolution it can wander
1148
+ inside a few percent for the whole sweep with no slope anywhere in it, because a
1149
+ limb fifty pixels from where it belongs overlaps the reference no better than one a
1150
+ hundred pixels away โ€” both are *no overlap*, and the difference between them is
1151
+ aliasing. A coordinate search sees noise, reports no improvement, and leaves the frame
1152
+ at a pose that shares almost nothing with the picture. โ‡’ **Box-average both sides
1153
+ before comparing them and run the search coarse to fine.** The coarsest level places
1154
+ the body, the next the limbs, the last two the pixels. The same sweep at the coarse
1155
+ level has a slope on it, because at that block size the two figures still overlap and
1156
+ the number knows which way to go.
1157
+
1158
+ โš ๏ธ **The coarsest level is for the body and nothing else.** A block big enough to
1159
+ give the whole figure a gradient is a block a shin is one cell of, and one cell
1160
+ cannot say which way a shin points. Place the root and the torso there, then decide
1161
+ each limb at a level whose cells are smaller than the part that level is moving.
1162
+
1163
+ **Scan each knob's whole plausible range. Do not line-search out from where it sits.**
1164
+ A search that steps out from the current value cannot bring an arm 60ยฐ round, because
1165
+ the first step overlaps the reference no better than standing still did โ€” so it
1166
+ reports no improvement and stops, correctly, on the objective it was given. A figure
1167
+ whose legs have folded under it is that failure with a picture attached: a real local
1168
+ minimum, sat in for as long as you care to iterate. โ‡’ For each knob, evaluate the
1169
+ whole range that bone can plausibly take โ€” a few dozen samples across it, which is
1170
+ nothing beside the frames you are fitting โ€” take the best, and refine only after
1171
+ that. The cost is linear in the number of knobs, and it is the difference between a
1172
+ fit that converges and a fit that reports success on a folded figure.
1173
+
1174
+ โš ๏ธ **Some knobs only decide together.** A part hanging three rotations below a
1175
+ shoulder is placed by none of them alone: each single-knob scan finds its own best
1176
+ while the part is still nowhere near, because every value of that knob is wrong given
1177
+ the other two. Where a chain ends in something whose position you can actually see โ€”
1178
+ a hand, a foot, a held prop โ€” scan the two links above it as a **pair**, over the
1179
+ grid. That is the product of two ranges on a handful of chains, not on every bone.
1180
+
1181
+ โš ๏ธ **Two whole chains can share a minimum, and no paired scan reaches that one.**
1182
+ The case above is two knobs in one chain. The harder one is two *chains* sitting in
1183
+ the same pixels: an arm and the prop it holds lying across the part of the frame the
1184
+ reference fills with the legs. Every leg knob that would carry a leg there finds the
1185
+ pixels already inked and reports no improvement โ€” correctly, on the objective it was
1186
+ given โ€” and the frame keeps its limbs in a **different configuration** rather than a
1187
+ slightly wrong one. Pairing cannot help here, because the two knobs are in different
1188
+ chains and pairing every chain with every other is the whole product.
1189
+
1190
+ **What reaches it is cheap: more than one start, screened coarsely.** Assemble a
1191
+ handful of candidate poses for the frame โ€” the incumbent, the two neighbouring
1192
+ frames' solutions, a few poses spread across the shot, the setup pose โ€” run all of
1193
+ them through the **coarse levels only**, and take the best two through the full
1194
+ schedule. **Keep the incumbent among the candidates**, so a frame can only improve
1195
+ on what it already had. The cost is a multiple of the coarse pass rather than of the
1196
+ fit, and the neighbour seed below is one start out of that set rather than a rule of
1197
+ its own. โš ๏ธ It also measures how far from converged a single-start fit can be while
1198
+ reporting success: repeat the *identical* search on one frame from different starts
1199
+ and the numbers walk down, step after step. That is not a tolerance being tightened,
1200
+ it is a different basin each time โ€” so a search that stopped improving is evidence
1201
+ about the start it was given and about nothing else.
1202
+
1203
+ **Cross-shot starts, for a configuration a shot cannot reach from its own frames.**
1204
+ Where every pose in one shot holds the prop low and the reference holds it out
1205
+ level, every start drawn from that shot's own frames is on the wrong side of the
1206
+ same two-chain minimum, and multi-start inside the shot barely moves the number.
1207
+ These shots are states of one character, so a configuration this shot never visits
1208
+ may be sitting in another one โ€” take the start from there. โ‡’ Borrow **only the bones
1209
+ of the chain in question**, never a whole foreign pose: a foreign pose puts the legs
1210
+ where this shot never goes, and the rest of the search then spends itself fighting
1211
+ what the borrow brought with it.
1212
+
1213
+ **Re-fit the setup pose against frames drawn from every shot, not against one.** Every
1214
+ animation is measured from the setup pose, so an error in it is an error in all of
1215
+ them โ€” and it is exactly the error one frame cannot show you. Fit an attachment's
1216
+ offset against a single frame and that frame's own rotations absorb whatever you got
1217
+ wrong: the picture comes out right, the offset is wrong, and every other shot pays
1218
+ for it. Across a spread it cannot hide, because a wrong offset would have to be
1219
+ absorbed by a *different* rotation in each frame and no one value of the offset does
1220
+ that. โ‡’ Fit the setup pose against one clear frame to get near, then re-fit it
1221
+ against a handful of frames drawn from **every** animation at once, and hold it fixed
1222
+ while the per-frame poses are fitted. It is the spread that identifies it โ€” a
1223
+ sequence of single-frame fits, one per shot, is not the same thing.
1224
+
1225
+ **Seed each frame's search from its neighbour's solution โ€” as one start among the
1226
+ full-range scans, never instead of them.** Adjacent frames are adjacent poses, so the
1227
+ answer next door is a better first guess than the middle of any range, and it costs one
1228
+ extra evaluation per knob to try it. What it must not do is *replace* the scans: the
1229
+ neighbouring frame's pose is precisely where a line search would have started, and a
1230
+ limb 60ยฐ out in one frame stays 60ยฐ out in the next for the reason the rule above
1231
+ gives โ€” stepping away from it overlaps no better, so the whole series inherits one
1232
+ frame's local minimum and looks stable while it does. โ‡’ Scan the whole range, add the
1233
+ neighbour's value to the starts, take the best of them. Fit outward from a frame you
1234
+ trust in both directions rather than only forward, so a bad frame seeds its neighbour
1235
+ and not every frame behind it.
1236
+
1237
+ โš ๏ธ **Then measure the adjacency drift, because a fit that lost a limb teleports.**
1238
+ Fitted frame by frame with nothing tying the frames together, a leg has two answers
1239
+ wherever the other leg is near it, and no single frame's number prefers the right one:
1240
+ some frames land on the wrong leg, every one of them cheap, and the series jumps back
1241
+ and forth between the two. That defect is invisible in any per-frame figure and loud in
1242
+ the relation between two โ€” the reading ยง9.2's `ฮ”px` and `ref ฮ”` columns already make
1243
+ for the whole figure, and the chain table localises: a chain whose worst slot drift on
1244
+ one frame runs many times its own mean across the set did not travel that far, it was
1245
+ lost and refound somewhere else. **A limb that moves much further between two adjacent
1246
+ frames than the reference's own frame-to-frame change is a fit that lost it, not a limb
1247
+ that moved.** Read it per chain rather than per figure โ€” one leg swapped for its twin
1248
+ is a small share of a whole-frame delta and vanishes into it.
1249
+
1250
+ **Two near-identical parts need one calibrated separator, decided once and pinned.** A
1251
+ front limb and a rear one are often the same drawing twice, differing by a tint or by
1252
+ nothing at all; a search scoring a whole composite cannot tell which of the two it just
1253
+ placed, because exchanging them costs almost nothing on the frames where they overlap.
1254
+ Left to the per-frame fit, that assignment is re-decided on every frame โ€” the teleport
1255
+ above, arriving by a second route. โ‡’ Settle it the way ยง8 settles a draw-order edge:
1256
+ build both hypotheses, render each at the frames' own scale, and compare like with like
1257
+ โ€” but calibrate on the frames where the two parts are *unambiguous*, the ones where
1258
+ they are far apart or only one of them is drawn, and read the separation there, where
1259
+ it is a real gap rather than a rounding difference. Then **pin the assignment for the
1260
+ run** and let no per-frame search reopen it. โš ๏ธ The same test knows when to stay silent
1261
+ here too: two hypotheses that come out inside the objective's own scatter mean the
1262
+ frames do not decide this, and you ship it on reasoning and say in the log that is what
1263
+ you did.
1264
+
1265
+ **Spend each iteration on the worst chain, and stop re-fitting the ones already at the
1266
+ floor.** ยง9.2's chain table is this loop's work queue: it gives every limb a worst slot
1267
+ drift, an error per pixel, and a share of the set's error. โ‡’ Take the next iteration to
1268
+ the worst **per-pixel** chain rather than the largest share โ€” the share confounds
1269
+ *wrong* with *big*, as ยง9.2 says beside the table, and the chain holding most of a
1270
+ run's error is routinely the one that simply covers most of the figure. Then freeze the
1271
+ chains that have converged. Re-fitting them spends the budget the broken chain needed,
1272
+ and it is not merely wasteful: chains share parents, so a search free to move a
1273
+ converged limb's ancestors will walk it back off the floor to buy a fraction of a point
1274
+ somewhere else. โš ๏ธ **A blank where a drift should be is the loudest row in the table,
1275
+ not a quiet one.** The matcher refuses to name a distance past the part's own size
1276
+ (ยง9.2), so a limb far enough out reports no match rather than a large number โ€” read
1277
+ that beside a high figure per pixel as the strongest signal the table has.
1278
+
1279
+ **What comes out is a pose per frame, and a pose per frame is not a key.** Two things
1280
+ decide what survives the reduction, and **ยง10.3** states both: declare one tolerance
1281
+ in pixels at the end of what each bone swings rather than a figure in degrees, and
1282
+ deal with the gauges โ€” the directions the pixels cannot see โ€” *before* the series
1283
+ becomes keys, because a fitter will have wandered along every one of them; fold the
1284
+ exact ones out and penalise the rest. Then close
1285
+ the loop with **ยง9**: the fit's own number says how near this pose is to this frame,
1286
+ and only `check` says whether the shot is the shot. **ยง9.3** is the list of what even
1287
+ that cannot see.
1288
+
810
1289
  ---
811
1290
 
812
1291
  ## 9. Checking against the frames โ€” `rigc check`
@@ -816,25 +1295,29 @@ bun cli.ts check --candidate path/to/spine --frames path/to/reference/frames
816
1295
  ```
817
1296
 
818
1297
  `--frames` takes either a **skeleton root** (the directory holding `frames.json`,
819
- which checks every animation of that shot) or **one animation directory** inside
820
- it. Everything else is optional: `--atlas` when the candidate's atlas is not beside
821
- its skeleton, `--as <name>` when your animation is called something the frame
822
- directory is not, `--all-frames` to list every frame instead of the worst by MAE,
823
- `--json <out>` for the whole per-frame, per-slot report.
824
-
825
- โš ๏ธ **A frame set may be contact-sheets-only.** `check` only reads `fNNNN.png`
826
- files โ€” a committed reference set that ships a contact sheet plus a couple of
827
- stills (rung 2's does: `f0000.png` and `f0310.png` per animation, the rest folded
828
- into `contact.png` so a 311-frame shot does not commit 311 near-duplicate PNGs)
829
- reports `frames 2 on disk, candidate samples 311, 2 compared` and means it: `check`
830
- compared exactly the committed stills, not the shot. That is not a defect to author
831
- around โ€” the frame count line says so rather than pretending a fuller comparison
832
- happened โ€” but it does mean a clean `check` table on a contact-sheet-only set says
833
- nothing about the frames between the stills. Whole-shot fidelity against a contact
834
- sheet needs a tile-wise comparison against the sheet's own grid, which `check` does
835
- not do yet (issue #36);
836
- [`bench/runs/2026-08-23-rung2-2/sheetcheck.ts`](../bench/runs/2026-08-23-rung2-2/sheetcheck.ts)
837
- is a working prototype, built in-run for exactly this gap.
1298
+ which checks every animation of that shot, framed per set โ€” see the scope note
1299
+ below) or **one animation directory** inside it. Everything else is optional:
1300
+ `--atlas` when the candidate's atlas is not beside its skeleton, `--as <name>` when
1301
+ your animation is called something the frame directory is not, `--framing shared`
1302
+ to fit one framing across every set instead of one each, `--all-frames` to list
1303
+ every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
1304
+ per-slot report.
1305
+
1306
+ โญ **A frame set may ship a contact sheet instead of every frame, and the sheet is
1307
+ compared too.** A long shot does not commit 311 near-duplicate PNGs: rung 2's sets
1308
+ ship `f0000.png` and `f0310.png` plus a `contact.png` holding all 311 sampled
1309
+ frames, and spineboy's `@30fps` sets do the same. The frame table still says
1310
+ `frames 2 on disk, candidate samples 311, 2 compared` โ€” those are the files โ€” and a
1311
+ **`sheet` line under it** carries the other 309: your candidate sampled at the set's
1312
+ own rate, rendered into the same box the frames above were at the sheet's own scale,
1313
+ and compared tile by tile (issue #36 โ€” the gap a rung-2 run had already prototyped a
1314
+ comparator for, in its own working directory, because the tool could not see its shot).
1315
+
1316
+ โš ๏ธ **Read it as a series, not as one number** โ€” ยง9.2. And note what it does not
1317
+ carry: MAE only. The `ฮ”px` / `ref ฮ”` thresholds are calibrated at frame scale and a
1318
+ tile has a fraction of a frame's pixels, so the per-frame change measure stays on
1319
+ the committed stills, where it reports `no two compared frames are adjacent` and
1320
+ means it.
838
1321
 
839
1322
  `--fps <n>` exists for frame sets that have no `frames.json` beside them, which are
840
1323
  sets rendered before the sidecar existed: it gives the rate those frames were
@@ -885,10 +1368,49 @@ pixels rather than quad corners (issue #34, closed by #39; see ยง9.2). Pin to a
885
1368
  you can name a reason for, and read the unpinned framing line first when you are not
886
1369
  sure whether you have one.
887
1370
 
888
- โš ๏ธ **The framing is over the frames you compare.** `--frames <root>` fits one
889
- framing across every set under it; `--frames <root>/<one-set>` fits one to that set
890
- alone. Both are right and they are not the same number, so compare like with like
891
- across builds.
1371
+ โš ๏ธ **The framing is over the frames you compare, and `check` decides it PER SET.**
1372
+ Point it at a skeleton root and each animation directory under it is asked its own
1373
+ question first: *do this set's own drawn pixels land in the box `frames.json`
1374
+ records?* The sets that do are measured in that box, which is exact โ€” it is not an
1375
+ estimate of where the frames were drawn, it is where they were drawn โ€” and nothing
1376
+ another set does can move them. The sets that do not are measured in **one shared
1377
+ framing** fitted across every set, printed as the header's `shared box` line, plus
1378
+ their own whole-pixel MAE refinement off it (ยง9.2) โ€” the fit is shared because more
1379
+ frames condition it better; the constant offset it still leaves is per set, and
1380
+ measured per set. Each set says which it got on its own `framed to` line.
1381
+
1382
+ Why the split falls there, both halves measured on an 8-shot character (147 frames):
1383
+
1384
+ - **Deciding the declared box per set is worth 15โ€“25 MAE.** Over the union, one shot
1385
+ that is not in the frames' coordinates puts the pooled correction over the
1386
+ one-pixel threshold and the *whole root* falls back to a fit. Per set, the shots
1387
+ that qualify read what pinning by hand reads: `idle` **18.77** where a whole-root
1388
+ run read 41.59, with not one key different (issue #100).
1389
+ - **Fitting per set is worse, so `check` does not.** `fitFraming` registers extent,
1390
+ and extent is not alignment (see the โš ๏ธ in ยง9.2), so on a shot whose silhouette
1391
+ genuinely differs one shot's frames do not constrain the fit enough: `hit` reads
1392
+ **92.36** fitted on its own against 60.59 in the shared fit, and a two-frame stills
1393
+ set reads 101.94 against 42.98. More frames is a better-conditioned fit, so the
1394
+ fallback is deliberately the shared one.
1395
+
1396
+ `--framing shared` measures **every** set in the shared framing, which is what a
1397
+ whole-root run used to do. It answers one question and it is a good one โ€” *does a
1398
+ single box serve every set?* โ€” and it is the wrong number to read as one shot's
1399
+ fidelity.
1400
+
1401
+ โš ๏ธ The two are different measurements and their absolute numbers are not comparable.
1402
+ The `scope` line at the top of the report says which one you got. `--viewport` pins
1403
+ one box for the whole run whichever scope you ask for, because a pin is a claim
1404
+ about your candidate's own coordinates and those do not change between shots.
1405
+
1406
+ โš ๏ธ **`--frames <root>/<one-set>` is a third number for a set that cannot take the
1407
+ declared box**, and it is the least constrained of the three: the fit has only that
1408
+ set's frames to work from, where a root run's shared fit has every frame in the
1409
+ skeleton. Measured on the same character, `hit` reads 60.50 at the root and 92.41
1410
+ pointed at its own directory. A set that DOES take the declared box reads the same
1411
+ either way โ€” that box is not fitted to anything โ€” so pointing at one directory is
1412
+ exact for a shot you authored in the frames' coordinates and a rough estimate for
1413
+ one you did not.
892
1414
 
893
1415
  ### 9.1 Why this exists
894
1416
 
@@ -910,6 +1432,58 @@ your run ceasing to be an honest authoring run. It is a loop, in the way `build`
910
1432
  a loop. `bench` and `diff` against a rung's export are not โ€” they read the answer,
911
1433
  and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach once.
912
1434
 
1435
+ ๐Ÿ“Œ **That is also why the MAE figures quoted through this section stay.** Every one of
1436
+ them is a candidate's own reading against rendered frames โ€” the exam question, not the
1437
+ answer key โ€” so none of them narrows a reference-side measure, and a guide that censored
1438
+ them would be teaching less for no gain in honesty. The criterion is under *The honesty
1439
+ rule* in [LADDER.md](LADDER.md) (issue #158); what it *does* seal is a score written
1440
+ over a reference's own count, and no such figure appears here.
1441
+
1442
+ ๐Ÿšจ **If you drive the runtime yourself, a bone's local transform lives on
1443
+ `bone.pose`.** A shot whose poses have to be *fitted* rather than read sends you
1444
+ past `check` and into your own render loop over
1445
+ `@esotericsoftware/spine-core` โ€” that is ยง8's *"look for a second way to get the
1446
+ same number"* applied to a whole pose, and it is a legitimate thing to build โ€” ยง8.1
1447
+ is how that search is set up so it converges. The
1448
+ first thing it hits is not a subtlety. **spine-core 4.3 keeps a bone's local
1449
+ transform on `bone.pose`, not on the bone**, so `bone.rotation = โ€ฆ` โ€” or `.x`,
1450
+ `.y`, `.scaleX` โ€” is neither an error nor a rotation: it adds a property nothing
1451
+ reads, and every frame renders as the setup pose. Write `bone.pose.rotation`.
1452
+ Rung 8 lost a loop to it at a flat 17.3; driven through `bone.pose`, the same
1453
+ poses measured **2.76**.
1454
+
1455
+ ๐Ÿšจ **That trap has a twin one level up: the *setup* transform lives on
1456
+ `bone.data.setupPose`, not on `bone.data`.** `BoneData` extends `PosedData`,
1457
+ which carries the whole setup transform on a `setupPose` object, so
1458
+ `bone.data.rotation` โ€” or `.x`, `.y`, `.scaleX` โ€” is `undefined`, and
1459
+ `bone.data.rotation + delta` is `NaN`. Read and write it as
1460
+ `bone.data.setupPose.rotation`. This one is worse to spot than the `bone.pose`
1461
+ trap, because nothing on the path raises: `undefined` propagates to `NaN`, and
1462
+ `NaN` serialises to `null`, so a fit writes `"px": null` into its own placements
1463
+ file, the next build reads those as zero, `validate` is green and `check` runs.
1464
+ โ‡’ **A `null` in your own placements dump is the signature of having read
1465
+ `bone.data` directly** โ€” nothing in this format is ever legitimately null. On
1466
+ spineboy it cost the candidate MAE 13.0 โ†’ 114.6 with a green gate throughout
1467
+ (measured in a spineboy attempt's own loop log, ยง4.1). Note that the two names are not
1468
+ the same thing: `bone.data.setupPose` is
1469
+ the setup transform, while `bone.setupPose()` on a `Bone` is the method that
1470
+ resets `bone.pose` back to it.
1471
+
1472
+ ๐Ÿšจ **A region attachment's own offsets are cached.** `attachment.x`, `.y`,
1473
+ `.rotation`, `.scaleX/.scaleY` are inputs to a quad spine-core computes once and
1474
+ stores; what gets drawn is that stored quad โ€” `computeWorldVertices` reads
1475
+ `getOffsets(pose)`, never the fields โ€” so writing them is, again, neither an
1476
+ error nor a move. **Call `attachment.updateSequence()` after every write**, or
1477
+ every frame renders the quad it was loaded with. A setup fit on spineboy ran
1478
+ 4,500 renders and reported the same number for all of them before this surfaced
1479
+ (the same loop log, ยง4.2).
1480
+
1481
+ โ‡’ **An MAE that is identical across every pose, and that does not move for any
1482
+ parameter you sweep โ€” a bone's local transform, an attachment's offsets โ€” is one
1483
+ of these inert writes and not a wrong animation.** The parameter you swept was
1484
+ never read; a wrong rig moves the number, a write to a field nothing reads
1485
+ cannot.
1486
+
913
1487
  ### 9.2 Reading the table
914
1488
 
915
1489
  ```
@@ -917,14 +1491,18 @@ and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach on
917
1491
  reference 256x116px 0.117628 px/unit world x[-573.3 .. 1603.0] y[-81.2 .. 908.9] (frames.json)
918
1492
  content candidate 234.6x95.5px at (11.3, 11.5) reference 234.7x95.3px at (11.2, 11.7) (union over 86 frame(s))
919
1493
  โคท fit x0.999256 offset +0.05, -0.02 px rms 0.42 px over 344 edge(s) union residual -0.27 x +0.17 px aspect -0.30% (derived, 4 pass(es), settled)
1494
+ โญ MAE-refined by -1, +1 px: 54.31 โ†’ 48.47 over the reference's own pixels (10.7% of the figure). โ€ฆ
920
1495
  in units candidate 1995.3 x 809.7 reference 1995.3 x 809.9 x0.9999
921
1496
 
922
1497
  โ”€โ”€ heavy โ€” candidate animation "heavy", 12 fps โ”€โ”€
923
1498
  frames 65 on disk, candidate samples 65, 65 compared
924
1499
  MAE mean 23.10 worst 43.36 at f0029 (0..255 over the union alpha; โ€ฆ)
1500
+ โคท over the REFERENCE's own drawn pixels, mean 23.90 โ€” the union figure compares two builds โ€ฆ
925
1501
  slot drift worst 2.1 px "pendulum" at f0029
926
1502
  per-frame 1 of 64 adjacent pair(s) change by a different amount than the reference does; worst
927
1503
  f0018, yours moved 0 px where the reference moved 374
1504
+ sheet 311 of 311 tile(s) of contact.png at 64x57px in 8 column(s) MAE mean 4.30 worst 4.76 at f0047
1505
+ โคท worst 8: f0047=4.8 f0048=4.7 f0045=4.7 f0039=4.7 f0149=4.7 f0044=4.6 f0046=4.6 f0043=4.6
928
1506
 
929
1507
  the 9 frames worth reading โ€” worst by MAE, plus every frame whose own change disagrees, in index order
930
1508
  frame MAE union px ฮ”px ref ฮ” worst slot drift how slots note
@@ -986,6 +1564,12 @@ The lines, in order:
986
1564
  does not, after the fit. This is the number that says *"something reaches
987
1565
  somewhere nothing in the frames does, or is a different size"*, and a warning
988
1566
  spells it out past a pixel.
1567
+ - the **MAE-refined** line, which is the last thing that happens to the box and the
1568
+ paragraph below is what it is for. On a **fitted** framing it says what constant
1569
+ whole-pixel offset was taken out and what that was worth (`โญ`), or that the
1570
+ search ran and the identity won. On a box that is not an estimate โ€” the frames'
1571
+ own, or one you pinned โ€” it never moves anything, and if it finds a constant there
1572
+ it says so as a **finding about your rig** rather than about the framing.
989
1573
  - `in units` โ€” the same two boxes in world units. The framing absorbs a pure scale
990
1574
  on purpose, so this is the only place one shows; it compares only if you measured
991
1575
  the shot in the frames' own units.
@@ -996,10 +1580,31 @@ that is a little large โ€” the best fit of the two extents is not quite the best
996
1580
  alignment of the two pictures, and the fit spends a fraction of a pixel absorbing
997
1581
  a difference that would have been cheaper to leave alone. Measured floor: about a
998
1582
  third of a pixel on the ladder's shots. On most that is invisible; on a small
999
- high-contrast frame it is worth a point or two of MAE โ€” rung 6 measured five. This
1000
- is the floor the frames' own box has no share in, which is why `check` prefers that
1001
- box whenever your pixels are measured to land in it; `--viewport` is how you stop
1002
- it in the cases that box does not cover.
1583
+ high-contrast frame it is worth a point or two of MAE โ€” rung 6 measured five, and
1584
+ on the spineboy sets a **constant** one or two pixels was worth 10โ€“30 % of the
1585
+ figure (issue #146). This is the floor the frames' own box has no share in, which is
1586
+ why `check` prefers that box whenever your pixels are measured to land in it;
1587
+ `--viewport` is how you stop it in the cases that box does not cover.
1588
+
1589
+ โญ **What a fitted framing now does about it: one final whole-pixel pass.** After
1590
+ the fit settles (or cycles), `check` searches every whole-pixel offset within ยฑ2 px
1591
+ for the lowest MAE over the reference's own drawn pixels and moves the box to the
1592
+ best one, when that is worth at least 1 % of the figure. So a fitted set's numbers
1593
+ are what is left **after** the best constant offset has been removed, rather than a
1594
+ constant offset read as motion โ€” and the line says which offset and what it bought,
1595
+ in both directions, so nothing is quietly absorbed.
1596
+
1597
+ Two things to know when you read it:
1598
+
1599
+ - โš ๏ธ **A large refinement on a set whose drift is also large is not necessarily
1600
+ framing.** The pass removes the best *constant*, and when one part carries much of
1601
+ the shot's ink a constant can absorb part of that part's own displacement. Read
1602
+ the offset beside the chain table: a big offset with a flat drift table is the
1603
+ fit's floor; a big offset with one limb far out is that limb.
1604
+ - On a box that is not an estimate the pass declines and says why. `frames.json`'s
1605
+ own box is where the frames were drawn, so a constant pixel *there* is your
1606
+ figure sitting a pixel off inside the right box โ€” a thing to fix, and the report
1607
+ refuses to frame it away. A pinned box is your claim, and nothing overrules it.
1003
1608
 
1004
1609
  **MAE** is the mean absolute RGB difference, 0..255, over the pixels either side
1005
1610
  covers โ€” the *union alpha*. It is not scored against a threshold, any more than a
@@ -1012,6 +1617,49 @@ because an ad-hoc re-render check naturally computes that one, and on every set
1012
1617
  measured so far it comes out ten to twenty-five times smaller and correspondingly
1013
1618
  blunter.
1014
1619
 
1620
+ โš ๏ธ **Half of that denominator is yours, so do not optimise against it.** The union
1621
+ is the pixels *either* side drew, and a large, mostly transparent sprite adds many
1622
+ cheap pixels to it โ€” so the *mean falls* on a candidate that got worse. That is not
1623
+ hypothetical: spineboy-2's muzzle flare walked its own scale to 13x under a fitting
1624
+ loop doing exactly this, and cost every set in that run its framing (issue #119).
1625
+ So the line under the MAE divides the same difference by the pixels the
1626
+ **reference** drew, a denominator nothing you do can grow. Read the union figure to
1627
+ compare two builds of your own rig, where both sides cover about the same ground,
1628
+ and the reference-denominator figure when you are deciding whether a change made
1629
+ the shot better; it is not bounded by 255. A set that draws more than half again as
1630
+ much ink as the reference does gets `โš ๏ธ overdraw` beside those two numbers, with
1631
+ both pixel counts, because at that point the first figure is cheap for a reason
1632
+ that has nothing to do with your keys.
1633
+
1634
+ ๐Ÿšจ **Part of your MAE is the texture, not the animation, and nothing in the report
1635
+ says so.** The reference frames are rendered through the example's **own packed
1636
+ atlas**, and a packed atlas may carry a `scale:` line โ€” the ladder has one at
1637
+ `scale: 0.5`, whose 745x212 part is packed at 373x106. rigc has no packer (**ยง6**), so
1638
+ a candidate built from the loose PNGs samples a texture at twice that resolution and
1639
+ resamples every edge differently. The pixels are the same shape in the same place; they
1640
+ are filtered from a different source, and the difference lands on the outline of every
1641
+ part in every frame. It is a constant, it is invisible to `content`, `rms` and the
1642
+ `ยฑ2 px` refinement โ€” a resampling difference is not an offset โ€” and **no key you write
1643
+ can move it**.
1644
+
1645
+ โ‡’ **When the MAE is flat across the whole set and the drift is already at the floor,
1646
+ check the atlas before you look for keys.** The example's `.atlas` is an allowed input
1647
+ in its own right (`bench/runs/README.md`, *What a run may read*, item 4), and one line
1648
+ of it answers the question. To size the floor, re-run `check` once with
1649
+ `--atlas <the example's own .atlas>`: same skeleton, same keys, the reference's own
1650
+ texture. Measured on rung 3, MAE **6.13 / 6.01** with the candidate's own
1651
+ full-resolution atlas against **2.25 / 2.30** with the supplied one โ€” **two thirds of
1652
+ the figure was the texture**, and the run that did not know it would have spent its
1653
+ whole budget hunting a rig that was already right.
1654
+
1655
+ โš ๏ธ Two things about that diagnostic. It is a **diagnostic and not a better number**:
1656
+ the artifact `bench` validates ships its own atlas, so the first figure is the one
1657
+ that belongs in a run's record and the second is the explanation of where it went.
1658
+ And the coarser texture **loses** resolution the finer one has โ€” on the same rung a
1659
+ pair the reference moves *one pixel* across stopped being visible at half scale, so
1660
+ the diagnostic run reported a frame-change disagreement the graded run does not have.
1661
+ Read it for the floor, never as the verdict.
1662
+
1015
1663
  **`ฮ”px` and `ref ฮ”`** are the two columns that do **not** compare you against the
1016
1664
  reference. They compare each side against **itself one frame earlier**: how many
1017
1665
  pixels of your own frame moved since your own previous frame, and the same for the
@@ -1037,12 +1685,46 @@ line whenever the MAE is flat and something still looks wrong: a flat MAE says t
1037
1685
  framing and the art agree, and it says nothing at all about whether your shot holds
1038
1686
  and blinks where the reference does.
1039
1687
 
1688
+ โš ๏ธ **Each set is compared against ITSELF, so on a shot committed at two rates a hold
1689
+ can exist in one set and not the other.** The coarse set samples every other frame of
1690
+ the fine one, so a pair the coarse set holds across is a constraint between samples
1691
+ **2k and 2k+2** of the fine series โ€” and 2k+1 between them is free to move, and does.
1692
+ One ladder shot has a pair whose whole-frame change at the coarse rate is **exactly
1693
+ 0** while both fine-rate pairs inside that same span change by 48 px: the subject
1694
+ shifts under three world units and comes back, so the two coarse samples land on the
1695
+ same pose either side of it. So ยง10.3's *key both ends of the hold* has to be applied
1696
+ **at each committed rate separately**, and equal values are not enough โ€” the two
1697
+ samples have to be **keys**, or a planner reduces through one of them and an
1698
+ interpolant inside its own tolerance is not equality. That cost two builds on that
1699
+ run, the second of them for exactly that reason. โ‡’ Measure the frames' own
1700
+ frame-to-frame change **once per committed rate**, and where a rate holds, pin both
1701
+ ends as keys whenever a finer rate moves between them.
1702
+
1040
1703
  โš ๏ธ Only between **adjacent** frames. A set that ships stills rather than every frame
1041
1704
  โ€” rung 2's contact-sheet sets โ€” reports `no two compared frames are adjacent`, and
1042
1705
  means it: the difference between two frames 310 apart is not a frame-to-frame delta.
1043
1706
  A disagreement needs one side to hold *exactly* still while the other moves, or one
1044
1707
  side to move four times the other and at least two dozen pixels more; below that the
1045
- two rasterisations differ by their own last bit and the column says nothing.
1708
+ two rasterisations differ by their own last bit and the column says nothing. Such a
1709
+ set gets the `sheet` line instead, which is MAE over every sampled frame and not a
1710
+ change measure โ€” the two thresholds above are pixel counts at frame scale, and a
1711
+ tile has a fraction of a frame's pixels.
1712
+
1713
+ **The `sheet` line is the whole shot**, on the sets that commit a couple of stills
1714
+ and fold every sampled frame into one `contact.png`. It says how many tiles were
1715
+ compared out of how many the sheet holds, the grid it measured off the sheet itself,
1716
+ the mean and worst tile, and the worst eight by MAE. Read the **series** rather than
1717
+ the mean, exactly as with the frame table: flat across the shot is framing or art,
1718
+ a spike is timing at that moment โ€” rung 2's four shots read 4.30โ€“4.41 flat over
1719
+ 1,244 tiles, which is what says their trajectories, ring rates and attachment swaps
1720
+ land where and when they should. Two things to know:
1721
+
1722
+ - it is measured in the **same box** the frame table was, at the sheet's scale. For
1723
+ a stills-plus-sheet set that box was decided on the stills, so a set whose framing
1724
+ is a fit carries that fit into these numbers as well;
1725
+ - a sheet whose dimensions are not a grid of this set's frame count at these frames'
1726
+ aspect is **refused by name** rather than read wrong โ€” the note names the file, and
1727
+ the answer is to re-render the set.
1046
1728
 
1047
1729
  **Slot drift** is what you act on. For each of your slots, `check` measures where
1048
1730
  it landed and how far that is from where the reference put it. That names the part,
@@ -1052,8 +1734,15 @@ sentence you can take straight back to a key.
1052
1734
  There are two matchers and the `how` column says which one answered:
1053
1735
 
1054
1736
  - `component` โ€” your slot sits on a connected component of the reference frame that
1055
- is its own size. The drift is the distance between the two centroids, and it is
1056
- the strongest answer available.
1737
+ is its own size **and holds nothing else you drew**. The drift is the distance
1738
+ between the two centroids, and it is the strongest answer available. All three
1739
+ conditions are checked: a blob may not be much bigger than the slot, may not be
1740
+ much wider than its box, and may not contain another of your parts' ink. The last
1741
+ is the one a dominant part slips through otherwise โ€” rung 2's course is 81 % of a
1742
+ blob that also holds the water, the panel and both rings, so the blob is only
1743
+ 1.24x its ink and no wider than its box, and the reported *"course drift 11.2 px"*
1744
+ was the distance to a five-part centroid (issue #37). It now falls to the matcher
1745
+ below and reads 0.0 px.
1057
1746
  - `tmpl 0.62` โ€” the reference merged your slot into a neighbour (they touch, or one
1058
1747
  is drawn over the other), so the fallback rendered **your slot on its own** and
1059
1748
  correlated it against the reference around where you drew it. The number is the
@@ -1074,6 +1763,39 @@ summary line carries the same denominator. `N reference component(s) no slot
1074
1763
  reaches` means the reference frame contains something none of your slots overlaps:
1075
1764
  a part you have not authored, or one you have put somewhere else entirely.
1076
1765
 
1766
+ **The `chains` block is the same two measures on the unit you actually repair.**
1767
+
1768
+ ```
1769
+ chains 6 from the candidate's own bone tree โ€” the roster is at the foot of the report
1770
+ chain slots worst slot drift mean MAE in it share
1771
+ crest 5/5 3.0 px "lantern" f0006 2.4 px 28.40 31.5%
1772
+ prop-arm 6/6 2.0 px "prop" f0002 1.5 px 44.90 33.0%
1773
+ near-strut 3/3 15.0 px "strut-tip" f0004 7.0 px 51.30 8.5%
1774
+ ```
1775
+
1776
+ `check` cuts **your own** bone tree at every branch point โ€” a chain runs from a
1777
+ root or a fork down to the next fork; a single-bone chain that is itself a fork
1778
+ folds into its parent, so a `hub` that branches three ways joins the trunk rather
1779
+ than becoming a row of its own; and each chain is named after the first bone in it
1780
+ that carries a slot. On a biped that lands on the parts you would name (`crest`,
1781
+ `near-strut` and `prop-arm` above); on a serial figure with no fork it is one chain,
1782
+ and the slot rows under it still say which link moved. Which bones and slots went
1783
+ where is printed as a roster at the foot of the report, so it is never a guess. `MAE
1784
+ share` divides the difference over the **reference's** own drawn pixels โ€” the
1785
+ denominator from the line above, which nothing you draw can grow โ€” and splits it by
1786
+ giving each of those pixels to the chain whose ink is nearest, so the shares
1787
+ partition the set and no chain can look better by drawing more: growing its ink
1788
+ only pulls more of the reference's pixels, and their error, into it. Read `MAE in
1789
+ it` beside the share, because the share alone confounds *wrong* with *big* โ€” a head
1790
+ and its features cover a lot of a figure and can carry a third of the error at a
1791
+ below-average figure per pixel. Reference ink further from your ink than the part's
1792
+ own size is left `(unattributed)` rather than charged to a neighbour, and a chain
1793
+ reading 0 % on `0/3` slots drawn is missing, not clean. The rollup at the foot gives
1794
+ each chain one line across every set โ€” the sentence a run's README quotes instead of
1795
+ a per-shot list. **ยง8.1** is how to act on it: the next iteration goes to the worst
1796
+ chain by error per pixel, and a chain already at the floor is frozen rather than
1797
+ re-fitted.
1798
+
1077
1799
  ### 9.3 What it still cannot see
1078
1800
 
1079
1801
  - **Anything a frame does not contain.** Bone `length`, the setup `inherit` mode,
@@ -1086,15 +1808,40 @@ a part you have not authored, or one you have put somewhere else entirely.
1086
1808
  still cannot tell you is *how* a silhouette got its shape: a hull moved by a
1087
1809
  bone chain and the same hull moved by deform keys render to the same pixels, and
1088
1810
  the frames cannot separate them. Choose on what the rig has to do next, not on
1089
- what the frames appear to say.
1811
+ what the frames appear to say. โš ๏ธ **But `bench` does see it.** Take a disc that
1812
+ squashes as it travels: a region on a bone that scales, and a grid mesh weighted
1813
+ 1.0 to that same bone, draw the same pixels on every frame โ€” and they are
1814
+ different files. One has a region and one bone; the other has a hull, a
1815
+ triangulation and a weight per vertex, and a rig that meshes one part usually
1816
+ meshes its neighbour and carries the bones to drive them. `attachments.type_counts`,
1817
+ `attachments.mesh_weighted`, `attachments.region_size` and the bone count that
1818
+ comes with the choice all move on that decision, while `animations.deform` moves
1819
+ for neither, because neither one deforms with keys โ€” the whole difference is which
1820
+ machinery renders identical pixels. That is not an argument for guessing: the
1821
+ frames cannot choose, and this guide will not tell you which way any reference
1822
+ went. It is an argument for **writing down which way you went and why at the
1823
+ moment you decide it**, rather than meeting the decision again in the measures
1824
+ after the run is over.
1090
1825
  - **Which of two explanations is right.** A slot 3 px low every frame and a slot
1091
1826
  3 px low at one frame have the same drift and opposite causes. The table gives
1092
1827
  you the frame index; ยง8's rule still applies โ€” look for a second way to get the
1093
1828
  number before you author the key.
1094
- - **What happens between two committed frames.** `ฮ”px` compares adjacent frames and
1095
- a set that ships stills has none, so a shot that is right at every committed frame
1096
- and wrong between them reads clean. That is the same gap `--frames` on a
1097
- contact-sheet set already has, and it is why the frame-count line is printed.
1829
+ - **What happens between two committed frames** โ€” *unless the set ships a sheet.*
1830
+ `ฮ”px` compares adjacent frames and a set that ships stills has none, so a shot
1831
+ that is right at every committed frame and wrong between them reads clean in that
1832
+ column. That is the same gap `--frames` on a contact-sheet set already has, and it
1833
+ is why the frame-count line is printed. โญ What closes it is the **`sheet` line**:
1834
+ on a set rendered at a higher rate than the frames on disk, every sampled frame is
1835
+ compared, so the samples between two committed ones are measured there and
1836
+ nowhere else.
1837
+ ๐Ÿšจ **And when you go looking for one of them, do not assume it lies between its
1838
+ neighbours.** A half-frame is not an interpolation โ€” it is where the shot actually
1839
+ was, and the interesting ones are interesting because it left the interval. Rung
1840
+ 4's ball makes contact entirely inside one twelfth of a second: the sheet puts it
1841
+ **40 px below both 12 fps frames that bracket it**, while its x sits within their
1842
+ own range. A search whose reach was scaled from those two frames' own step
1843
+ therefore looked ยฑ9 px for something 40 px away and reported the interpolation
1844
+ back, twice, before the tile was read rather than inferred.
1098
1845
 
1099
1846
  ---
1100
1847
 
@@ -1143,6 +1890,31 @@ attachment name"* โ€” [Images](http://esotericsoftware.com/spine-images). โ‡’ in
1143
1890
  keep the placeholder equal to the PNG's basename and no `path` is written (R5). A
1144
1891
  `path` in the emitted file means the two disagreed.
1145
1892
 
1893
+ ๐Ÿงฉ **โ‡’ When the art is named after the parts, the art's names are the rig's names โ€”
1894
+ and this is the largest lever ยง10 has.** *One image โ†’ one slot โ†’ one attachment,
1895
+ named after the image* reads as a structural rule. It is also, and mostly, a
1896
+ **naming** rule, and naming is what whole sections of the measures are made of:
1897
+ five of `bones`'s eight name-matched measures (`names`, `parent_by_name`, `order`,
1898
+ `length_present`, `inherit_present`) and every `slots` measure but the count are
1899
+ scored over the names the two sides **share** ([`src/diff.ts`](../src/diff.ts)), so
1900
+ a rig whose names miss reads near zero on all of them however well it is built. โ‡’
1901
+ carry each part's own name straight through โ€” PNG basename โ†’ slot โ†’ attachment, and
1902
+ the bone that moves it โ€” instead of inventing a scheme of your own.
1903
+
1904
+ **Both directions are measured.** The one run whose art shipped a separate PNG per
1905
+ body part, each named for the part, applied this deliberately and posted `names`
1906
+ measures **an order of magnitude** above anything on the ladder before it โ€” without
1907
+ either side seeing the other. Every honest run before it read near zero on the same
1908
+ measures. It is the largest single thing any run has got out of a convention.
1909
+
1910
+ โš ๏ธ **And the other half, which matters just as much: when the art is *not* named
1911
+ after the parts, no naming strategy beats any other and the measure is noise.** On a
1912
+ shot whose two PNGs are called things like `square` and `pendulum`, the names carry
1913
+ nothing a rig could inherit, every candidate name is as good as every other, and the
1914
+ name measures are reporting the honesty rule's own price rather than a defect in
1915
+ your rig. Do not spend a loop hunting for better names there, and do not read the
1916
+ low figure as a miss โ€” say in the log that the art did not carry them.
1917
+
1146
1918
  ๐Ÿ“— **Housekeeping the format fixes for you.** The default skin *"always has the name
1147
1919
  `default`"* and *"bones are ordered so that the parent always comes before a child
1148
1920
  bone"* โ€” [JSON format](http://esotericsoftware.com/spine-json-format). ยง3.4.
@@ -1200,7 +1972,79 @@ all the time"*, and Clean Up *"deletes all unnecessary keys โ€ฆ keying the same
1200
1972
  multiple times in a row, keying the same values as the setup pose"*, because *"often
1201
1973
  it is convenient to set keys liberally when designing an animation, then use Clean
1202
1974
  Up afterward"* โ€” [Keys](http://esotericsoftware.com/spine-keys). โ‡’ a shipped export
1203
- is dense, but it does not repeat a value.
1975
+ is dense, and what it does not carry is a key its own neighbours already imply.
1976
+
1977
+ ๐Ÿงฉ **โ‡’ A hold still needs a key at both ends, and two equal values are not a
1978
+ repeat.** Clean Up's *"keying the same value multiple times in a row"* is about
1979
+ **three or more** โ€” a run of keys whose interior ones their neighbours imply. Two
1980
+ keys of equal value imply nothing: they are the only way to say *nothing moves here*
1981
+ on an interpolated timeline, and deleting either one ramps the value through the
1982
+ hold. Stillness is a thing a shot does, sometimes for a twelfth of a second and
1983
+ sometimes for nine, and it is authored, not omitted. โ‡’ Key the start of a hold and
1984
+ key its end, at the same value; drop the ones in between. This is ยง9.2's *"held pose
1985
+ that is not held"* from the other side, and the same place catches it โ€” a sloped
1986
+ hold shows up in `check`'s per-frame column and nowhere else, because it is cheap in
1987
+ every single frame and wrong only in the relation between two.
1988
+
1989
+ โš ๏ธ **The key reducer has to key the plateau, because a plateau is neither an end nor
1990
+ a turn.** A planner that forces the series ends and every change of direction โ€”
1991
+ which is exactly what the rule above asks for โ€” will still author a slope straight
1992
+ through a run of still frames: not one sample in the middle of a hold is an end or
1993
+ a turning point, and a greedy span stays inside its own per-bone tolerance the whole
1994
+ way across. **A tolerance is not a hold.** Slow motion inside the tolerance is a
1995
+ tolerance question; stillness is a thing the shot *does*, and it survives the
1996
+ reduction only if the reduction is told to keep it. โ‡’ Force **both ends of every run
1997
+ of equal values** as keys in their own right โ€” a third kind of forced index beside
1998
+ the series ends and the turning points โ€” and test on **exact** equality, so that a
1999
+ merely near-still span is deliberately not swept up with it. This is worth doing
2000
+ before you have any evidence you need it: a run whose *poses* are all right can fail
2001
+ this and see nothing wrong anywhere else, because `validate` has no opinion on it,
2002
+ `diff` never looks at a rendered frame, and an aggregate MAE is cheap in every
2003
+ single frame and wrong only in the relation between two.
2004
+
2005
+ โš ๏ธ **And a tolerance is not a *slow span* either โ€” a key tolerance needs a relative
2006
+ floor beside its absolute one.** The rule above rescues exact stillness by forcing it,
2007
+ and deliberately leaves near-stillness to the tolerance. But **ยง9.2's per-frame column
2008
+ compares *changes*, not positions**, so on a span the shot barely moves across, an
2009
+ error well inside an absolute tolerance is most of the motion. Measured, on rung 3: the
2010
+ reference moves **0.109 px** between two frames; a greedy span deviated **0.098 px**
2011
+ there, legal under a 0.30 px tolerance and legal again under 0.15 px โ€” and the column
2012
+ read **259 px against the reference's 40**, a six-fold disagreement authored by a span
2013
+ that was, at every keyframe, exactly right. The same tolerance that is generous on the
2014
+ fast part of the shot is a 90 % error on the slow part, because one figure in pixels
2015
+ cannot be both.
2016
+
2017
+ โ‡’ Cap each span's deviation at **the smaller of the absolute tolerance and the
2018
+ smallest single-frame move inside that span**. It is one line in the planner, it costs
2019
+ a handful of keys, and it is the difference between a reduction that is accurate and
2020
+ one that is accurate *in proportion to what is happening*.
2021
+
2022
+ โญ **Then stop trusting the floor and close the loop on the frames, because a floor is
2023
+ a heuristic and the column is a measurement.** The floor above cut rung 3's
2024
+ disagreements from three to one and could not reach the last: **sample your own planned
2025
+ curves at the frames' own rate, render them, compare every adjacent pair against the
2026
+ reference's own change, force the offending frames as keys, and re-plan** โ€” repeating
2027
+ until no pair is out of band. It terminates quickly (that shot needed one extra round
2028
+ on one animation and none on the other), it needs no build, and it is the only part of
2029
+ key planning that is verified rather than argued. โš ๏ธ The band is worth reading before
2030
+ you aim at it: `check` calls a pair a disagreement when one side is **exactly** still
2031
+ and the other is not, or when one side moves **four times** the other **and** at least
2032
+ **24 px** more (`src/check.ts`). So the wide middle of a shot is nearly free and the
2033
+ whole difficulty is the pairs where the reference barely moves โ€” including, on that
2034
+ rung, one pair it moves a single pixel across, which no MAE and no drift figure in this
2035
+ toolchain can see.
2036
+
2037
+ ๐Ÿšจ **But check that the shot holds at all first, because applying this to one that
2038
+ never does manufactures the defect it prevents.** The rule is about a *shot*, and a
2039
+ snap-to-still step in a planner does not cost nothing when there is nothing to snap:
2040
+ it will find some run of samples inside the fitter's own resolution and flatten it.
2041
+ Rung 4's shot has **no** still span โ€” not one adjacent pair of its 155 reference
2042
+ frames is pixel-identical, and even its last two differ, because the chain is still
2043
+ settling at the final frame โ€” and the snap duly flattened the tail and put a
2044
+ *"yours moved 0 px where the reference moved 28"* into the per-frame column: ยง9.2's
2045
+ held-pose defect arriving from the opposite direction. โ‡’ Difference every adjacent
2046
+ pair of frames once, before the planner runs. It is one pass over the set, and it
2047
+ tells you whether this paragraph applies to you at all.
1204
2048
 
1205
2049
  ๐Ÿ“— **Add a key when a curve cannot carry the shape.** *"If a curve is not smooth
1206
2050
  enough, it is easily remedied by adding another key"*, and the **Bounce** handle
@@ -1217,6 +2061,89 @@ minimal one-key-per-pose spec produces.
1217
2061
  one.** Reaching for a target density is guessing. The frames are the only thing that
1218
2062
  can say where the motion turns; ยง8 is how to read them.
1219
2063
 
2064
+ โš ๏ธ **Two rules for a run that *fits* a pose series rather than reading it off the
2065
+ frames.** Neither is Spine's โ€” no public page has an opinion about a fitter โ€” but
2066
+ both decide where the keys above actually land, so they sit beside them. Both were
2067
+ paid for on the ladder. (**ยง8.1** is how the series gets fitted in the first place.)
2068
+
2069
+ **A key tolerance on a rotation is not a number of degrees.** The same angular error
2070
+ costs a different number of pixels at every level of a hierarchy, because everything
2071
+ below the bone comes with it. Measured on rung 8's four-link chain: **a quarter of a
2072
+ degree on the last link moves the chain's end 0.15 px, and the same quarter degree
2073
+ on the plate the chain hangs from moves it 0.69 px.** One figure in degrees applied
2074
+ per property therefore keys the far end of the chain roughly four times too loosely
2075
+ while over-keying the near end โ€” it is not one tolerance at all. โ‡’ Declare **one**
2076
+ tolerance, **in pixels at the end of what the bone swings**, and convert it per bone
2077
+ by that bone's lever arm. With that, the whole trade reads as one curve and you can
2078
+ pick a point on it deliberately: that shot measured 0.6 px โ†’ 259 keys โ†’ 1.619 window
2079
+ MAE, 0.3 px โ†’ 300 โ†’ 1.402, 0.15 px โ†’ 377 โ†’ 1.305.
2080
+
2081
+ โš ๏ธ **Compute what a skipped sample costs before you declare that tolerance โ€” on a
2082
+ fast subject the shot's own speed has already pinned it.** Skipping one sample means
2083
+ spanning it linearly, and the chord through its two neighbours sits at their mean, so
2084
+ the deviation at the sample skipped is **half** the series' second difference there:
2085
+ `|f(nโˆ’1) โˆ’ 2ยทf(n) + f(n+1)| / 2`. That is an identity, not an approximation โ€” check
2086
+ it on `f(n) = nยฒ`, whose second difference is 2: the chord reads `nยฒ + 1` where the
2087
+ curve reads `nยฒ`, and the deviation is 1. So second-difference the fitted series,
2088
+ halve it, and read that number against the tolerance you were about to declare. If it
2089
+ is the larger of the two, the fitter keys nearly every frame, and no tolerance below
2090
+ it changes that โ€” the key density is then a fact about the subject, not a choice you
2091
+ made. One ladder run is that case: its subject's median frame-to-frame second
2092
+ difference measured 6.4 px, so a span that skips one sample deviates about 3.2 px,
2093
+ ten times the 0.3 px tolerance the run declared, and the tolerance would have to be
2094
+ loosened past that 3.2 px before a span could afford to skip anything at all. What
2095
+ the trade bought there was accuracy and never sparsity โ€” 0.6 px โ†’ 439 keys โ†’ 18.22
2096
+ MAE at 12 fps, 0.45 px โ†’ 482 โ†’ 17.54, 0.3 px โ†’ 521 โ†’ 17.26. โ‡’ Do the arithmetic
2097
+ first. It tells you which of the two situations you are in: picking a point on the
2098
+ curve above, or discovering the point the shot has already put you on โ€” and the
2099
+ second one is not a failure to reach the density this section asks for, it is what
2100
+ that density is here.
2101
+
2102
+ ๐Ÿšจ **There is a third situation, and it is the one where nothing in the loop can
2103
+ help you: a tolerance under the accuracy of whatever produced the series.** The
2104
+ arithmetic above is about the *subject*; this is about the *estimator*. A fitted
2105
+ series is only as good as its objective's basin, and a tolerance below that width
2106
+ buys keys that encode the fitter's wander โ€” and **`check` cannot see that it
2107
+ happened**, because two candidates that are both inside tolerance render the same
2108
+ pixels. `diff`'s `key_counts` sees it and `diff` is the finish line, so a run gets
2109
+ one shot at the number. Rung 4's is the recorded case: it declared 0.28 px, and its
2110
+ own objective's basin on the shortest lever in the chain measured **ยฑ1.5ยฐ**, which
2111
+ is ยฑ0.5 px there โ€” so the declared figure was under the noise, and it shipped about
2112
+ three times the reference's key count (`key_counts` 421/1339) with every other
2113
+ animation measure at or near 1.000. โ‡’ **Measure the basin before you declare the
2114
+ tolerance**, which costs nothing and needs no reference: scan each knob around its
2115
+ converged value and read how far it moves before the objective does. Then declare a
2116
+ tolerance at or above the widest of them, and record both numbers.
2117
+
2118
+ **A rig's parameters are not identified by its pixels โ€” remove the gauges before you
2119
+ key.** A bone that carries no attachment is an exact gauge: turn it by ฮด, turn its
2120
+ children back by ฮด, and **not one pixel changes**. Anything optimising against pixels
2121
+ is free to wander along that direction, and it does. On the spineboy run the figure's
2122
+ topmost body bone carries no art and every moving bone sits under it, and a
2123
+ coordinate descent walked it to **+181ยฐ** against its child's **โˆ’184ยฐ** โ€” a pose
2124
+ whose picture is right on every frame and whose key series spins the whole figure
2125
+ through a full turn between two of them. The rendered result is correct and the
2126
+ authored rig is nonsense, and no amount of further fitting finds it, because every
2127
+ point on the gauge orbit has identical error. โ‡’ Fold each gauge out *before* the
2128
+ series becomes keys โ€” for a rotation gauge, take the median of the values along the
2129
+ chain and fold it back. The same shape exists wherever a transform is unobservable: a
2130
+ bone with no art, a slot-less parent chain, a uniform scale split across two bones.
2131
+
2132
+ โš ๏ธ **That fold is exact only when the gauge bone's children sit at its own origin** โ€”
2133
+ and a character's body bone almost never has them there, which is the very shape the
2134
+ paragraph above was written from. Turn the parent by ฮด and a child *at the origin*
2135
+ back by ฮด and the child is where it was; a child sitting 10 units off swings through
2136
+ an arc of that radius first, and the counter-turn only spins it on the spot. The fold
2137
+ moves art, so it **changes the picture**. Measured on the second spineboy run, whose
2138
+ `hip` carries no attachment and has three children 9โ€“13 units off it: the fold cost
2139
+ **3 MAE on every `idle` frame** โ€” mean 23.0 with it against 19.9 without, same search
2140
+ โ€” and it was removed. โ‡’ Read the children's offsets before you fold. At the origin,
2141
+ fold: it is exact and it is cheap. Off the origin the degeneracy is still there but
2142
+ it is **soft**, not exact, and a soft degeneracy is *regularised, not folded* โ€” leave
2143
+ the values alone and add a penalty on the gauge direction to the objective instead.
2144
+ That run used **2e-5 per squared degree** of hip rotation: invisible at animator-sized
2145
+ angles, and still decisive against the +181ยฐ against โˆ’184ยฐ above.
2146
+
1220
2147
  ### 10.4 Curves
1221
2148
 
1222
2149
  ๐Ÿ“— **Linear is what a *new* key gets, and it does not survive contact with a curve.**
@@ -1250,6 +2177,35 @@ and **Ease in** (*"the value changes more slowly near the next key"*) โ€”
1250
2177
  across the file, is how an editor rig reads. Raw `curve` is R6's escape hatch โ€” one
1251
2178
  key needing a shape no other key has โ€” not the normal way to write a curve.
1252
2179
 
2180
+ ๐Ÿšจ **The table is a constraint on where the keys go, not a formatting step applied
2181
+ afterwards โ€” it has to exist while the keys are chosen.** A run that plans its keys
2182
+ by fitting each span's **own** handles, and then writes the nearest entry of a named
2183
+ table, has bought a key count at one tolerance and shipped it at another. Nothing in
2184
+ the loop can see that: the timeline count, the key count, the curve kinds and the
2185
+ duration are all unmoved, so `diff` does not shift and the gate stays green, while
2186
+ the rendered result changes by a multiple. Rung 8's first version did exactly this
2187
+ and went from **1.07 to 4.65 MAE** โ€” four times its own fit's floor. โ‡’ Two passes.
2188
+ Pass A fits freely and exists only to *discover* which shapes the shot uses; those
2189
+ are clustered into the table; pass B re-plans **every** timeline under the table it
2190
+ will actually write. Never fit free handles and substitute the nearest named shape
2191
+ after the fact. (The table's size then trades against the key count at a fixed
2192
+ tolerance โ€” that shot ran 4 easings/368 keys, 8/314, 12/300, 16/284 โ€” because a
2193
+ richer table holds more spans.) This is rung 6's clamp defect in another suit:
2194
+ **a constraint that is not enforced where the value is written is not a
2195
+ constraint.**
2196
+
2197
+ ๐Ÿงฉ **โ‡’ A span with no interior sample takes the automatic handles, not linear.** Two
2198
+ keys on adjacent samples leave pass B nothing to fit โ€” the samples cannot constrain
2199
+ that span's shape at all โ€” and a planner with nothing to fit leaves `ease` off, which
2200
+ is linear (ยง4.5): the one shape this section says to argue for rather than default to.
2201
+ "No information" is not an argument for constant speed. Take instead the tangents the
2202
+ keys either side imply โ€” the editor's own **automatic handles**, quoted above โ€” and
2203
+ snap *those* to the nearest table entry, exactly as you would a fitted span. Rung 8's
2204
+ second version did this for its adjacent pairs and the samples barely moved (**17.34 โ†’
2205
+ 17.26 MAE**) while `curve_kinds` changed wholesale: nil in the frames, large in the
2206
+ structure, which is the signature of a convention rather than a fidelity fix โ€” and
2207
+ this section's whole subject.
2208
+
1253
2209
  ๐Ÿ“— **Handles are normalised, and that is the shape an `easings` entry takes.** For a
1254
2210
  Bezier key, *"the X axis is from 0 to 1 and represents the percent of time between
1255
2211
  the two keyframes. The Y axis is from 0 to 1 and represents the percent of the