spine-rigc 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -1298,6 +1298,26 @@ emitted as written, in the default skin or anywhere else.
1298
1298
  `spec` must be exactly `"rigc-rig/1"`. `name` must be a non-empty string. `bones`
1299
1299
  must be non-empty. `slots` must be present (it may be empty).
1300
1300
 
1301
+ 🔢 **Every number in a rig spec is finite at float32 precision** — at most
1302
+ ±3.4028234663852886e38 — whatever field it is in, so the ranges the tables below
1303
+ state are ranges *within* that. The skeleton is written as float32, and a number
1304
+ past that line would be emitted as Infinity, which JSON writes as `null`: `1e309`
1305
+ (which JSON reads as Infinity) and `1e308` (a finite double with no float32) are
1306
+ both refused by the field they are in, before anything is compiled (§5.1). The cut
1307
+ manifest and the motion spec are held to the same line.
1308
+
1309
+ 🔤 **Every field below holds one JSON type, and a value of another is refused.**
1310
+ Where a table says a field is a position, an angle, a scale, a mix, a count, a
1311
+ size or an index, it is a JSON **number** — `5`, never `"5"`; a flag is `true` or
1312
+ `false`, never `"true"` or `1`; a name or a path is a string; a list of vertices,
1313
+ uvs or triangles is an array of numbers, typed element by element. A JSON `null`
1314
+ is a value, not an absence, and is accepted only where a table below says what
1315
+ `null` means. The type of every key, shape by shape, is the table `RIG_TYPES` in
1316
+ [`src/rig.ts`](../src/rig.ts) (`MOTION_TYPES` in [`src/motion.ts`](../src/motion.ts),
1317
+ `MANIFEST_TYPES` in [`src/types.ts`](../src/types.ts)), and the refusal names the
1318
+ field, the type it found and the type it required (§5.1) — so that sentence, not a
1319
+ column restated beside each row here, is what to act on.
1320
+
1301
1321
  🚫 **Every example value below is invented.** Names, coordinates, vertex lists and
1302
1322
  payloads in this guide are written to illustrate a field, never copied out of a
1303
1323
  reference export — an example lifted from one would be handing an authoring agent an
@@ -5429,11 +5449,14 @@ corpus inventory, and a ladder run reads this guide without following its refere
5429
5449
  out of it.)
5430
5450
 
5431
5451
  🔢 **A fourth surface is a pose that is not finite**, which `render` and `check`
5432
- refuse before drawing anything, naming the bone or vertex and its value. `validate`
5433
- does not always get there first: a bone `rotation` of `1e309`, in the setup pose or
5434
- in a key, was measured leaving `A10_NO_NAN_AFTER_STEPPING` green, because the bone's
5435
- world position stays finite while its matrix is `NaN`. The sentence, and where to
5436
- look for the number, is under *nothing to draw* in §3.4.
5452
+ refuse before drawing anything, naming the bone or vertex and its value.
5453
+ `A10_NO_NAN_AFTER_STEPPING` reads the same scan, so on a skeleton with an animation
5454
+ `validate` gets there first and in the same words: a bone `rotation` of `1e309`, in
5455
+ the setup pose or in a key, leaves the bone's world position finite and its matrix
5456
+ `NaN`, and A10 names the bone and the term. What it does not see is a skeleton with
5457
+ no animation (A10 SKIPs, so `render` is where that one is named) and a skin other
5458
+ than the default, which A10 does not pose. The sentence, and where to look for the
5459
+ number, is under *nothing to draw* in §3.4.
5437
5460
 
5438
5461
  ### 5.1 Compile errors — before the gate
5439
5462
 
@@ -5558,6 +5581,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
5558
5581
  | `a rig spec needs a non-empty "bones" array` | §3.2 — a rig with no bones has nothing to hang a slot on; every one has at least a root |
5559
5582
  | `a rig spec needs a "slots" array (it may be empty; its ORDER is the draw order)` | §3.3 — write `[]` for a rig that draws nothing. ⚠️ These three arrive only when the key is really absent: **misspelt**, it is the unknown-key refusal above, naming what you wrote |
5560
5583
  | `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
5584
+ | `<file>: <where> is a string "5"; a number is required` (also `is a boolean true`, `is an array [1]`, `is null`, `is a number 7; a string is required`, `…; an array of numbers is required`, `…; a boolean (true or false) is required`, `…; a string or null is required`) | §3 / §4 — write the value as the JSON type its field holds: a number as a JSON number (`5`, not `"5"`), a flag as `true`/`false`, a name as a string. Every field of the three input files has one type, and a value of another is refused by the field before any reader does arithmetic on it — which it used to: `"x": "5"` built as `5`, `true` as `1` and `[1]` as `1`, and a `null` built too, all green. An array is typed element by element, so the refusal names the index (`weights[3][1].x`, `vertices[0]`). `null` is refused as `null` except where the field states it means something (`skeleton.width`/`height`, `skeleton.audio`, a slot's and a setup entry's `attachment`, a deform key's `vertices`, the manifest's `image`, `states`, `contact_depth` and `cap_containment_ceiling`). `<where>` is the rig spec's wording (`bone "hip" x`, `skin "default" slot "s" attachment "a" rotation`, `constraint "aim" mix`), the dotted path in the cut manifest (`crop.h`, `parts[2].offset[0]`), and the motion spec's own path (`` `animations."walk".ik[0].keys[0].mix` ``). In the rig spec and the manifest it runs right after the key scan and before every other check, so a field check that used to name a type (a mesh name that is not a string, a `lengths` entry spelt as a string) now reads as this row; in the motion spec it runs after that file's own field checks, which keep their sentences. A closed set of names (`blend`, `inherit`, a constraint's `type`, a generator's `kind`) keeps its own refusal, which lists the names that exist |
5585
+ | `<file>: <where> is Infinity; a number in this file is finite at float32 precision, at most ±3.4028234663852886e+38, because the skeleton is written as float32 — …` (also `is -Infinity`, `is 1e+308`, and `is NaN` from a library caller) | §3 — state the number you meant, inside float32's range. One walk over the whole file, run right after the key scan and before any other rig-spec check, so a range rule never prints `NaN`. `<where>` is every number in the rig spec — a bone (`bone "hip" x`), a slot, a skin's attachment down to its arrays (`skin "default" slot "s" attachment "a" weights[3][1].x`), a constraint (`constraint "aim" mixRotate`), an event, `skeleton`, `invariants.meshTriangles` — and the dotted path in the cut manifest (`crop.h`) and in the motion spec (`` `animations.idle.tracks[0].keys[1].v[0]` ``, after the motion spec's own field checks, which keep their sentences). ⚠️ It sees numbers only: `"x": "NaN"` is a string, and the row above refuses it as one |
5561
5586
  | `two bones are called "X"` | bone names are the join key; rename one |
5562
5587
  | `two ik constraints are called "X" — a constraint resolves by name AND type (\`SkeletonData.findConstraint\`), so names are unique PER KIND: an ik and a transform constraint may share one, two of a kind may not` | §3.5 — rename one of the two. The kind in the sentence is the pair's own, so `two transform constraints are called "X"` is the same refusal on another kind; a name shared **across** kinds is not this error |
5563
5588
  | `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | §4.6 — the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
@@ -5783,7 +5808,7 @@ Fix A00 and run it again.
5783
5808
  | `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` |
5784
5809
  | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. There is no `spine-html` clause here — a placeholder is free to differ from the region its `path` names **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once |
5785
5810
  | `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
5786
- | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked **SKIP** when the skeleton carries no animation: the NaN is produced by stepping, and a static rig is never stepped — the same subject `A09` skips on |
5811
+ | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked **SKIP** when the skeleton carries no animation: the NaN is produced by stepping, and a static rig is never stepped — the same subject `A09` skips on |
5787
5812
  | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
5788
5813
  | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
5789
5814
  | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
@@ -8840,8 +8865,8 @@ Found within 2 px of the truth, out of 15 trials per cell (free search, default
8840
8865
 
8841
8866
  | texture level (mean) | 24 px | 32 px | 48 px | 64 px | 96 px | 128 px | 192 px |
8842
8867
  | --- | --- | --- | --- | --- | --- | --- | --- |
8843
- | `native` (detail 3.49) | 13/15 (2 amb) | 15/15 | 15/15 | 15/15 | 13/15 (1 amb, 1 wrong) | 14/15 (1 wrong) | 14/15 (1 wrong) |
8844
- | `blur1` (detail 1.58) | 12/15 (3 amb) | 12/15 (2 amb, 1 wrong) | 13/15 (2 amb) | 12/15 (3 amb) | 12/15 (2 amb, 1 wrong) | 11/15 (1 amb, 3 wrong) | 11/15 (2 amb, 2 wrong) |
8868
+ | `native` (detail 3.49) | 13/15 (2 amb) | 15/15 | 15/15 | 15/15 | 14/15 (1 wrong) | 14/15 (1 wrong) | 14/15 (1 wrong) |
8869
+ | `blur1` (detail 1.58) | 12/15 (3 amb) | 12/15 (3 amb) | 13/15 (2 amb) | 12/15 (3 amb) | 12/15 (2 amb, 1 wrong) | 11/15 (1 amb, 3 wrong) | 11/15 (2 amb, 2 wrong) |
8845
8870
  | `blur2` (detail 0.59) | 9/15 (5 amb, 1 wrong) | 9/15 (4 amb, 2 wrong) | 9/15 (4 amb, 2 wrong) | 8/15 (3 amb, 4 wrong) | 7/15 (3 amb, 5 wrong) | 6/15 (4 amb, 5 wrong) | 7/15 (3 amb, 5 wrong) |
8846
8871
  | `flat` (detail 0.00) | 0/15 (14 amb, 1 wrong) | 0/15 (14 amb, 1 wrong) | 0/15 (13 amb, 2 wrong) | 0/15 (11 amb, 4 wrong) | 0/15 (13 amb, 2 wrong) | 0/15 (12 amb, 3 wrong) | 0/15 (12 amb, 3 wrong) |
8847
8872
 
@@ -8850,14 +8875,14 @@ The same trials, grouped by `detail`:
8850
8875
  | detail | found | ambiguous | wrong | refused |
8851
8876
  | --- | --- | --- | --- | --- |
8852
8877
  | under 0.25 | 8/119 | 89 | 22 | 0 |
8853
- | 0.25–0.5 | 1/3 | 1 | 1 | 0 |
8854
- | 0.5–0.75 | 48/81 | 14 | 19 | 0 |
8878
+ | 0.25–0.5 | 0/3 | 2 | 1 | 0 |
8879
+ | 0.5–0.75 | 49/81 | 13 | 19 | 0 |
8855
8880
  | 0.75–1 | 17/34 | 14 | 3 | 0 |
8856
8881
  | 1–1.5 | 25/25 | 0 | 0 | 0 |
8857
- | 1.5–2 | 34/48 | 12 | 2 | 0 |
8882
+ | 1.5–2 | 34/48 | 13 | 1 | 0 |
8858
8883
  | 2–3 | 40/44 | 1 | 3 | 0 |
8859
8884
  | 3–4 | 34/34 | 0 | 0 | 0 |
8860
- | 4 and over | 30/32 | 2 | 0 | 0 |
8885
+ | 4 and over | 31/32 | 1 | 0 | 0 |
8861
8886
 
8862
8887
  **Method.** The truth set is `examples/spineboy`: its setup pose and one frame of
8863
8888
  `walk` (a third of the way in). For every trial the figure is rendered twice with one
@@ -8879,8 +8904,8 @@ part has a low per-pixel gradient and still places — and size alone has no
8879
8904
  measurable effect across 24–192 px at any level.
8880
8905
 
8881
8906
  **The edge**, derived by `deriveFloor` as the largest rung of a fixed ladder under
8882
- which at most a tenth of the trials were found: **`detail` 0.5**. Under it, **7.4%**
8883
- of the trials were found (9/122); at or over it, **76.5%** (228/298). A part is below
8907
+ which at most a tenth of the trials were found: **`detail` 0.5**. Under it, **6.6%**
8908
+ of the trials were found (8/122); at or over it, **77.2%** (230/298). A part is below
8884
8909
  the floor when its `detail` is under 0.5, or when its own longest side is under 24 px,
8885
8910
  the smallest size measured (there, nothing measured says `pose` can place it).
8886
8911
 
@@ -8911,16 +8936,44 @@ blurred part now reported with a second placement at the same spot and another
8911
8936
  scale, one of them with the truth itself as its best answer), for 43% more
8912
8937
  refinement samples and the coarse pass untouched.
8913
8938
 
8914
- ⚠️ **What this is and is not.** The grid is not a reliability line: five native cells
8915
- place 14 or 15 of 15 and the trials at `detail` 3 and over place 64 of 66, but the
8916
- goggles still place 12 of 42, and a threshold read off two dozen parts of one figure
8939
+ Since issue #886 the stopped polish tries **a quarter rung** (2^(1/12)) beside the
8940
+ half and keeps the better re-fit. The case was not a grid trial but §6's stationary
8941
+ post in [MOTION.md](MOTION.md): under `--scale 0.85,1.2` it came back at scale
8942
+ 0.917, residual 0.0649, where the old placement at 0.975 still scores 0.0589. The
8943
+ half rung stepped from 0.917 to 1.029, where the best position scores 0.0980 — a part
8944
+ that fills its image pays for every pixel pushed past the figure, so above its truth
8945
+ the objective is a cliff, and half a rung crossed the basin onto it. With both, the
8946
+ post places at 0.971 (0.0593, 0.1 px from the old placement), and the grid goes from
8947
+ 237 to 238 found and 8 to 6 misses (2 gained, 1 lost) for 16% more user CPU. The
8948
+ quarter alone found 234, losing the native arms #877 was made for. The one lost is a
8949
+ margin reading again: the blurred 32 px front shin (walk) now reports its truth as its
8950
+ best (0.07 px, 0.00968), with its old 0.51 px answer 0.0045 above it.
8951
+
8952
+ Issue #892 asked whether the coarse field, which keeps **one rotation per anchor
8953
+ cell**, loses the truth more often than on that post, and the answer is no. The
8954
+ tool now records, per trial, the coarse seed nearest the truth, the rotation the
8955
+ field kept for its cell and the field's own score there at the truth's rotation.
8956
+ Within 2.4 px of the truth that seed is a half turn off on 2 of the 238 found
8957
+ trials (a mouth at 48 px, both found anyway) and on none of the 6 misses, and
8958
+ every failed trial whose nearest seed is a quarter or half turn off but one is a
8959
+ flat part, none of them a miss. On the post itself, the upright rung scores 0.305
8960
+ at that cell against the half turn's 0.168, so a second rotation per cell would
8961
+ need a band 1.8 times wide to carry it. Re-gridding the rotation at the coarse level
8962
+ instead of the next one took the grid to 236 found (7 gained, 9 lost), and
8963
+ suppressing minima only within a rotation family took it to 231 (2 gained, 10
8964
+ lost). Both lose trials far from the truth, so neither shipped and the field is
8965
+ unchanged.
8966
+
8967
+ ⚠️ **What this is and is not.** The grid is not a reliability line: six native cells
8968
+ place 14 or 15 of 15 and the trials at `detail` 3 and over place 65 of 66, but the
8969
+ goggles still place 13 of 42, and a threshold read off two dozen parts of one figure
8917
8970
  would be a claim about that figure. What the grid can separate is **whose** failure a
8918
8971
  failure is. `bun tools/pose_floor.ts` prints the residual at the truth for every
8919
8972
  failed trial and marks it **missed** when the truth scores better than everything
8920
8973
  reported and nothing reported is on it — a failure the search could fix. The other
8921
8974
  failures are the objective preferring another placement (a blurred part shrunk into
8922
8975
  its own region scores better than its truth) or the truth reported and tied, and no
8923
- search can fix those. After #877, 8 of the 183 failed trials are misses, and 7 of
8976
+ search can fix those. After #886, 6 of the 182 failed trials are misses, and 5 of
8924
8977
  those sit within 3.1 px of the truth rather than on another hill; the `--from`
8925
8978
  listing prints, for each, where the refinement left the truth level by level. All this was
8926
8979
  measured on **ideal cuts**, the frame's own pixels, where a residual at the truth is
@@ -8935,7 +8988,10 @@ print the table, derive the edge and list the misses per cell. `PO22` in
8935
8988
  no miss at all, and `PO23` places the 48 px fist #865 was made for; `PO28` and
8936
8989
  `PO29` place the four trials #877 was made for and read where the shin survived the
8937
8990
  cutoff, and `PO26` holds every polish to strictly descending residuals, read through
8938
- the same trace.
8991
+ the same trace. `PO31` places §6's post at the old placement from the page's own
8992
+ input bytes, and `PO32` holds it to taking the quarter-rung escape and reading the same twice.
8993
+ `PO33` holds the coarse row the tool reads rotations from to the field each seed was
8994
+ picked from.
8939
8995
 
8940
8996
  ## 12. Reading the half of that picture `pose` refuses — `rigc chainfit`
8941
8997
 
package/docs/INGEST.md CHANGED
@@ -370,6 +370,8 @@ rigc check --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing
370
370
  --frames ref3/light
371
371
  ```
372
372
 
373
+ **No run reproduces this:** abridged — the head's `atlas`, `frames`, `skin`, `scope`, `reference`, `content` and `in units` lines and its two ⚠️ notes are cut, so the `⤷ fit` line the run prints under `content` stands under `framed to`; the section's `frames` line and every `⤷` note are cut; and so is everything the run prints after `per-frame`
374
+
373
375
  ```
374
376
  rigc check
375
377
  candidate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
@@ -378,14 +380,14 @@ rigc check
378
380
  declared frames.json's own box: TAKEN, coincident — a fit there asks for 0.00 px, under the 1 px that separates a candidate in the frames' coordinates from one in its own, over 21 frame(s).
379
381
 
380
382
  ── light — candidate animation "light", 12 fps ──
381
- MAE mean 0.00 worst 0.00 at f00-1 (0..255 over the union alpha; over the whole frame, mean 0.00)
382
- slot drift worst 0.5 px "pendulum" at f0003
383
+ MAE mean 0.00 worst 0.00 (exact: none of the 21 compared frame(s) differs from the reference) (0..255 over the union alpha; over the whole frame, mean 0.00)
384
+ slot drift worst 0.4 px "pendulum" at f0012
383
385
  per-frame all 20 adjacent pair(s) change by as much as the reference's own frames do
384
386
  ```
385
387
 
386
388
  Two things to take from the control, both of which you need before reading any real
387
389
  number: the **framing** resolved to the frames' own box at 0.00 px, so the MAE is a
388
- comparison of pictures rather than of framings; and **slot drift still reads 0.5 px at
390
+ comparison of pictures rather than of framings; and **slot drift still reads 0.4 px at
389
391
  MAE 0.00**, which is that instrument's own floor rather than a difference.
390
392
 
391
393
  ⚠️ **`--texture-from` is not optional on ingest work, and the reason is structural.**
@@ -1339,7 +1341,9 @@ against one picture. A foreign export hands you a packed page instead, and point
1339
1341
  `pose` at one is worse than useless — it treats the whole page as a single part and
1340
1342
  answers confidently:
1341
1343
 
1342
- ```bash
1344
+ ```bash setup
1345
+ rigc render --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
1346
+ --fps 4 --max 900 --out ref-big
1343
1347
  mkdir -p packed-only
1344
1348
  cp examples/3-timing-and-spacing/export/3-timing-and-spacing.png packed-only/
1345
1349
  rigc pose --images packed-only --frame 'ref-big/light@4fps/f0002.png'
@@ -1350,12 +1354,12 @@ rigc pose
1350
1354
  .. frame …/ref-big/light@4fps/f0002.png (900x409)
1351
1355
  .. ground rgb(232, 232, 232) over 100% of the border ring
1352
1356
  .. parts …/packed-only (1 png)
1353
- .. search scale 0.5–2 in 7 step(s) · rotation -180°–180° step 15° · refuse above residual 0.25
1354
- PLACE 3-timing-and-spacing.png x= 324.3 y= 188.2 rot= -91.4° scale=0.629 residual=0.2083 unexplained= 30%
1355
- found on a 7x3 anchor grid, step 4 at 32x reduction
1357
+ .. search scale 0.5–2 in 7 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
1358
+ PLACE 3-timing-and-spacing.png x= 324.3 y= 188.3 rot= -91.6° scale=0.630 residual=0.2085 unexplained= 30%
1359
+ found on a 14x7 anchor grid, step 4 at 16x reduction
1356
1360
  ```
1357
1361
 
1358
- ⚠️ **`residual=0.2083` is *under* the default 0.25 refusal bar**, so nothing refused
1362
+ ⚠️ **`residual=0.2085` is *under* the default 0.25 refusal bar**, so nothing refused
1359
1363
  it, and `PLACE` rather than `AMBIG` means nothing flagged it either. With the same frame
1360
1364
  and the two real loose parts, the answer is what it should be:
1361
1365
 
@@ -1365,12 +1369,15 @@ rigc pose --images examples/3-timing-and-spacing/images \
1365
1369
  ```
1366
1370
 
1367
1371
  ```
1372
+ rigc pose
1373
+ .. frame …/ref-big/light@4fps/f0002.png (900x409)
1374
+ .. ground rgb(232, 232, 232) over 100% of the border ring
1368
1375
  .. parts …/examples/3-timing-and-spacing/images (2 png)
1369
- .. search scale 0.3–0.6 in 4 step(s) · rotation -180°–180° step 15° · refuse above residual 0.25
1370
- PLACE pendulum.png x= 319.3 y= 214.5 rot= -88.9° scale=0.412 residual=0.0425 unexplained= 7%
1371
- found on a 12x5 anchor grid, step 5 at 16x reduction
1372
- PLACE square.png x= 437.2 y= 343.1 rot= -0.1° scale=0.410 residual=0.0331 unexplained= 4%
1373
- found on a 57x26 anchor grid, step 4 at 4x reduction
1376
+ .. search scale 0.3–0.6 in 4 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
1377
+ PLACE pendulum.png x= 319.3 y= 214.6 rot= -88.9° scale=0.411 residual=0.0428 unexplained= 7%
1378
+ found on a 28x13 anchor grid, step 2 at 16x reduction
1379
+ PLACE square.png x= 437.1 y= 343.1 rot= 0.0° scale=0.410 residual=0.0334 unexplained= 4%
1380
+ found on a 113x51 anchor grid, step 2 at 4x reduction
1374
1381
  ```
1375
1382
 
1376
1383
  ⇒ **Check what is in `--images` before trusting a `pose` report on ingest work.** One