spine-rigc 0.3.0 โ†’ 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,7 +18,10 @@ 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
26
  start measuring rather than after; **ยง8.1** if the figure has more joints than you
24
27
  can measure one at a time; then **ยง9** for the loop that closes it
@@ -26,6 +29,22 @@ that can see that, and a run that skips it has verified nothing about the motion
26
29
  attachment, keying practice, curve kind, draw order โ€” sourced from Spine's own
27
30
  public documentation: **ยง10**
28
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
+
29
48
  ## The vocabulary is Spine's
30
49
 
31
50
  Wherever rigc has no better abstraction it uses **Spine 4.3's own concept, its own
@@ -60,6 +79,7 @@ bun cli.ts check \
60
79
  --frames path/to/reference/frames
61
80
 
62
81
  # read the table โ†’ fix the spec โ†’ build again โ†’ check again
82
+ # โ†ณ read its per-frame column before its MAE โ€” ยง9.2
63
83
  ```
64
84
 
65
85
  `build` compiles, round-trips the result through `@esotericsoftware/spine-core`,
@@ -73,6 +93,18 @@ the animation is the one in the frames, and there is no assertion that could โ€”
73
93
  ยง9. The two run in that order because `check` needs artifacts on disk and `build`
74
94
  only writes them when the gate is green.
75
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
+
76
108
  What the flags mean:
77
109
 
78
110
  | Flag | Meaning |
@@ -263,8 +295,8 @@ payloads in this guide are written to illustrate a field, never copied out of a
263
295
  reference export โ€” an example lifted from one would be handing an authoring agent an
264
296
  answer to the rung it is standing on, which is the rule ยง10.6 states and the honesty
265
297
  rule in [LADDER.md](LADDER.md) turns on. If a snippet here matches a reference file,
266
- that is a defect in this guide: report it. (It has happened โ€” 2026-08-23, see
267
- [`bench/runs/README.md`](../bench/runs/README.md), *What a run may read*.)
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*.)
268
300
 
269
301
  ### 3.1 `skeleton` โ€” the header
270
302
 
@@ -603,6 +635,26 @@ stepped.
603
635
  written for. โš ๏ธ What this does **not** protect you from is rounding your own
604
636
  times before you write them โ€” write `2/12`, not `0.1667`, and let the compiler
605
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.
606
658
  - **No key may land past the animation's `duration`.** Nothing that plays the
607
659
  animation for the duration it declares ever reaches such a key, so it is a
608
660
  compile error โ€” checked on **every timeline**, not just on the latest key in the
@@ -956,17 +1008,83 @@ frames did not show the rest, and rendering like-for-like settled three more of
956
1008
  โ€” worth a measurable drop in window MAE, a convention the gate cannot see and the
957
1009
  measures can.
958
1010
 
959
- โš ๏ธ **The same test knows when to stay silent, and you have to let it.** Run on
960
- another shot, two orders came out **0.8 % apart over the whole shot and pointing
961
- opposite ways** โ€” a gap well inside the objective's own scatter, which
962
- is correctly *no answer*. A difference that small is not a quiet vote for the
963
- winner; it means the frames do not decide this edge, and you ship it on reasoning
964
- and say in the log that is what you did.
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.
965
1064
 
966
1065
  And the general form of all three: **when a reading implies a key, look for a second
967
1066
  way to get the same number before you author it.** A wrong measurement costs one
968
1067
  spurious key; a wrong measurement you believed costs the shape of the whole shot.
969
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
+
970
1088
  **A value is easier to get right than a curve.** The three traps above are all
971
1089
  about measuring a *value*, and both ladder runs so far found that the values came
972
1090
  out right early: rung 1's key values were exact at every keyframe on the second
@@ -1060,6 +1178,38 @@ the other two. Where a chain ends in something whose position you can actually s
1060
1178
  a hand, a foot, a held prop โ€” scan the two links above it as a **pair**, over the
1061
1179
  grid. That is the product of two ranges on a handful of chains, not on every bone.
1062
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
+
1063
1213
  **Re-fit the setup pose against frames drawn from every shot, not against one.** Every
1064
1214
  animation is measured from the setup pose, so an error in it is an error in all of
1065
1215
  them โ€” and it is exactly the error one frame cannot show you. Fit an attachment's
@@ -1153,19 +1303,21 @@ to fit one framing across every set instead of one each, `--all-frames` to list
1153
1303
  every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
1154
1304
  per-slot report.
1155
1305
 
1156
- โš ๏ธ **A frame set may be contact-sheets-only.** `check` only reads `fNNNN.png`
1157
- files โ€” a committed reference set that ships a contact sheet plus a couple of
1158
- stills (rung 2's does: `f0000.png` and `f0310.png` per animation, the rest folded
1159
- into `contact.png` so a 311-frame shot does not commit 311 near-duplicate PNGs)
1160
- reports `frames 2 on disk, candidate samples 311, 2 compared` and means it: `check`
1161
- compared exactly the committed stills, not the shot. That is not a defect to author
1162
- around โ€” the frame count line says so rather than pretending a fuller comparison
1163
- happened โ€” but it does mean a clean `check` table on a contact-sheet-only set says
1164
- nothing about the frames between the stills. Whole-shot fidelity against a contact
1165
- sheet needs a tile-wise comparison against the sheet's own grid, which `check` does
1166
- not do yet (issue #36);
1167
- [`bench/runs/2026-08-23-rung2-2/sheetcheck.ts`](../bench/runs/2026-08-23-rung2-2/sheetcheck.ts)
1168
- is a working prototype, built in-run for exactly this gap.
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.
1169
1321
 
1170
1322
  `--fps <n>` exists for frame sets that have no `frames.json` beside them, which are
1171
1323
  sets rendered before the sidecar existed: it gives the rate those frames were
@@ -1222,8 +1374,10 @@ question first: *do this set's own drawn pixels land in the box `frames.json`
1222
1374
  records?* The sets that do are measured in that box, which is exact โ€” it is not an
1223
1375
  estimate of where the frames were drawn, it is where they were drawn โ€” and nothing
1224
1376
  another set does can move them. The sets that do not are measured in **one shared
1225
- framing** fitted across every set, printed as the header's `shared box` line. Each
1226
- set says which it got on its own `framed to` line.
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.
1227
1381
 
1228
1382
  Why the split falls there, both halves measured on an 8-shot character (147 frames):
1229
1383
 
@@ -1278,6 +1432,13 @@ your run ceasing to be an honest authoring run. It is a loop, in the way `build`
1278
1432
  a loop. `bench` and `diff` against a rung's export are not โ€” they read the answer,
1279
1433
  and [the ladder's honesty rule](LADDER.md) makes them a finish line you reach once.
1280
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
+
1281
1442
  ๐Ÿšจ **If you drive the runtime yourself, a bone's local transform lives on
1282
1443
  `bone.pose`.** A shot whose poses have to be *fitted* rather than read sends you
1283
1444
  past `check` and into your own render loop over
@@ -1303,8 +1464,8 @@ file, the next build reads those as zero, `validate` is green and `check` runs.
1303
1464
  โ‡’ **A `null` in your own placements dump is the signature of having read
1304
1465
  `bone.data` directly** โ€” nothing in this format is ever legitimately null. On
1305
1466
  spineboy it cost the candidate MAE 13.0 โ†’ 114.6 with a green gate throughout
1306
- ([`bench/runs/2026-08-23-spineboy-2/LOOP.md`](../bench/runs/2026-08-23-spineboy-2/LOOP.md),
1307
- ยง4.1). Note that the two names are not the same thing: `bone.data.setupPose` is
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
1308
1469
  the setup transform, while `bone.setupPose()` on a `Bone` is the method that
1309
1470
  resets `bone.pose` back to it.
1310
1471
 
@@ -1315,8 +1476,7 @@ stores; what gets drawn is that stored quad โ€” `computeWorldVertices` reads
1315
1476
  error nor a move. **Call `attachment.updateSequence()` after every write**, or
1316
1477
  every frame renders the quad it was loaded with. A setup fit on spineboy ran
1317
1478
  4,500 renders and reported the same number for all of them before this surfaced
1318
- ([`bench/runs/2026-08-23-spineboy-2/LOOP.md`](../bench/runs/2026-08-23-spineboy-2/LOOP.md),
1319
- ยง4.2).
1479
+ (the same loop log, ยง4.2).
1320
1480
 
1321
1481
  โ‡’ **An MAE that is identical across every pose, and that does not move for any
1322
1482
  parameter you sweep โ€” a bone's local transform, an attachment's offsets โ€” is one
@@ -1331,6 +1491,7 @@ cannot.
1331
1491
  reference 256x116px 0.117628 px/unit world x[-573.3 .. 1603.0] y[-81.2 .. 908.9] (frames.json)
1332
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))
1333
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). โ€ฆ
1334
1495
  in units candidate 1995.3 x 809.7 reference 1995.3 x 809.9 x0.9999
1335
1496
 
1336
1497
  โ”€โ”€ heavy โ€” candidate animation "heavy", 12 fps โ”€โ”€
@@ -1340,6 +1501,8 @@ cannot.
1340
1501
  slot drift worst 2.1 px "pendulum" at f0029
1341
1502
  per-frame 1 of 64 adjacent pair(s) change by a different amount than the reference does; worst
1342
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
1343
1506
 
1344
1507
  the 9 frames worth reading โ€” worst by MAE, plus every frame whose own change disagrees, in index order
1345
1508
  frame MAE union px ฮ”px ref ฮ” worst slot drift how slots note
@@ -1401,6 +1564,12 @@ The lines, in order:
1401
1564
  does not, after the fit. This is the number that says *"something reaches
1402
1565
  somewhere nothing in the frames does, or is a different size"*, and a warning
1403
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.
1404
1573
  - `in units` โ€” the same two boxes in world units. The framing absorbs a pure scale
1405
1574
  on purpose, so this is the only place one shows; it compares only if you measured
1406
1575
  the shot in the frames' own units.
@@ -1411,10 +1580,31 @@ that is a little large โ€” the best fit of the two extents is not quite the best
1411
1580
  alignment of the two pictures, and the fit spends a fraction of a pixel absorbing
1412
1581
  a difference that would have been cheaper to leave alone. Measured floor: about a
1413
1582
  third of a pixel on the ladder's shots. On most that is invisible; on a small
1414
- high-contrast frame it is worth a point or two of MAE โ€” rung 6 measured five. This
1415
- is the floor the frames' own box has no share in, which is why `check` prefers that
1416
- box whenever your pixels are measured to land in it; `--viewport` is how you stop
1417
- 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.
1418
1608
 
1419
1609
  **MAE** is the mean absolute RGB difference, 0..255, over the pixels either side
1420
1610
  covers โ€” the *union alpha*. It is not scored against a threshold, any more than a
@@ -1441,6 +1631,35 @@ much ink as the reference does gets `โš ๏ธ overdraw` beside those two numbers,
1441
1631
  both pixel counts, because at that point the first figure is cheap for a reason
1442
1632
  that has nothing to do with your keys.
1443
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
+
1444
1663
  **`ฮ”px` and `ref ฮ”`** are the two columns that do **not** compare you against the
1445
1664
  reference. They compare each side against **itself one frame earlier**: how many
1446
1665
  pixels of your own frame moved since your own previous frame, and the same for the
@@ -1466,12 +1685,46 @@ line whenever the MAE is flat and something still looks wrong: a flat MAE says t
1466
1685
  framing and the art agree, and it says nothing at all about whether your shot holds
1467
1686
  and blinks where the reference does.
1468
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
+
1469
1703
  โš ๏ธ Only between **adjacent** frames. A set that ships stills rather than every frame
1470
1704
  โ€” rung 2's contact-sheet sets โ€” reports `no two compared frames are adjacent`, and
1471
1705
  means it: the difference between two frames 310 apart is not a frame-to-frame delta.
1472
1706
  A disagreement needs one side to hold *exactly* still while the other moves, or one
1473
1707
  side to move four times the other and at least two dozen pixels more; below that the
1474
- 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.
1475
1728
 
1476
1729
  **Slot drift** is what you act on. For each of your slots, `check` measures where
1477
1730
  it landed and how far that is from where the reference put it. That names the part,
@@ -1481,8 +1734,15 @@ sentence you can take straight back to a key.
1481
1734
  There are two matchers and the `how` column says which one answered:
1482
1735
 
1483
1736
  - `component` โ€” your slot sits on a connected component of the reference frame that
1484
- is its own size. The drift is the distance between the two centroids, and it is
1485
- 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.
1486
1746
  - `tmpl 0.62` โ€” the reference merged your slot into a neighbour (they touch, or one
1487
1747
  is drawn over the other), so the fallback rendered **your slot on its own** and
1488
1748
  correlated it against the reference around where you drew it. The number is the
@@ -1508,19 +1768,19 @@ a part you have not authored, or one you have put somewhere else entirely.
1508
1768
  ```
1509
1769
  chains 6 from the candidate's own bone tree โ€” the roster is at the foot of the report
1510
1770
  chain slots worst slot drift mean MAE in it share
1511
- neck 5/5 3.0 px "goggles" f0004 2.3 px 30.37 24.3%
1512
- rear-upper-arm 6/6 2.2 px "gun" f0000 1.4 px 46.26 34.6%
1513
- front-thigh 3/3 16.3 px "front-shin" f0003 7.2 px 48.18 9.0%
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%
1514
1774
  ```
1515
1775
 
1516
1776
  `check` cuts **your own** bone tree at every branch point โ€” a chain runs from a
1517
1777
  root or a fork down to the next fork; a single-bone chain that is itself a fork
1518
- folds into its parent, so a `torso` that branches three ways joins the trunk rather
1778
+ folds into its parent, so a `hub` that branches three ways joins the trunk rather
1519
1779
  than becoming a row of its own; and each chain is named after the first bone in it
1520
- that carries a slot. On a biped that lands on the parts you would name (`neck`,
1521
- `front-thigh`, the gun arm); on a serial figure with no fork it is one chain, and
1522
- the slot rows under it still say which link moved. Which bones and slots went where
1523
- is printed as a roster at the foot of the report, so it is never a guess. `MAE
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
1524
1784
  share` divides the difference over the **reference's** own drawn pixels โ€” the
1525
1785
  denominator from the line above, which nothing you draw can grow โ€” and splits it by
1526
1786
  giving each of those pixels to the chain whose ink is nearest, so the shares
@@ -1566,10 +1826,22 @@ re-fitted.
1566
1826
  3 px low at one frame have the same drift and opposite causes. The table gives
1567
1827
  you the frame index; ยง8's rule still applies โ€” look for a second way to get the
1568
1828
  number before you author the key.
1569
- - **What happens between two committed frames.** `ฮ”px` compares adjacent frames and
1570
- a set that ships stills has none, so a shot that is right at every committed frame
1571
- and wrong between them reads clean. That is the same gap `--frames` on a
1572
- 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.
1573
1845
 
1574
1846
  ---
1575
1847
 
@@ -1714,6 +1986,66 @@ that is not held"* from the other side, and the same place catches it โ€” a slop
1714
1986
  hold shows up in `check`'s per-frame column and nowhere else, because it is cheap in
1715
1987
  every single frame and wrong only in the relation between two.
1716
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.
2048
+
1717
2049
  ๐Ÿ“— **Add a key when a curve cannot carry the shape.** *"If a curve is not smooth
1718
2050
  enough, it is easily remedied by adding another key"*, and the **Bounce** handle
1719
2051
  preset exists for *"changing directions abruptly, such as when a ball bounces"* โ€”
@@ -1746,6 +2078,43 @@ by that bone's lever arm. With that, the whole trade reads as one curve and you
1746
2078
  pick a point on it deliberately: that shot measured 0.6 px โ†’ 259 keys โ†’ 1.619 window
1747
2079
  MAE, 0.3 px โ†’ 300 โ†’ 1.402, 0.15 px โ†’ 377 โ†’ 1.305.
1748
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
+
1749
2118
  **A rig's parameters are not identified by its pixels โ€” remove the gauges before you
1750
2119
  key.** A bone that carries no attachment is an exact gauge: turn it by ฮด, turn its
1751
2120
  children back by ฮด, and **not one pixel changes**. Anything optimising against pixels