spine-rigc 0.25.5 → 0.26.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
@@ -160,16 +160,16 @@ What the flags mean:
160
160
  | `--rig` | the rig spec — skeleton structure |
161
161
  | `--motion` | the motion spec — time |
162
162
  | `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths and `skeleton.images` are written relative to it |
163
- | `--copy-images` | `build` only: also copies every referenced page PNG into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own, and points `skeleton.images` at `--out` itself so the editor's import finds the parts beside the skeleton (issue #370; §3.1 says why it is spelled `../<out>/` and not `./`). Default is unchanged — page paths still point at the source art (issue #217) |
163
+ | `--copy-images` | `build` only: also copies every page **the emitted atlas names** into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own, and points `skeleton.images` at `--out` itself so the editor's import finds the parts beside the skeleton (issue #370; §3.1 says why it is spelled `../<out>/` and not `./`). Under `--atlas-in` those pages are the pack's, not one per part (issue #693 — **§0.2**). Default is unchanged — page paths still point at the source art (issue #217) |
164
164
  | `--pack` | `build` only: arrange every part onto **shared** atlas page(s), written into `--out` as real PNGs, instead of one page per part. Lossless — nothing is resampled, trimmed or rotated. Default is unchanged (issue #4) — **§0.1** |
165
165
  | `--page-size` | `build --pack` only: the largest page edge (default `2048`). A ceiling, not the size: page edges are powers of two and the one written is the smallest that holds the pack — **§0.1** |
166
166
  | `--padding` | `build --pack` only: the gutter each region reserves on every side (default `2`), filled by extending the region's own edge pixels outwards. `0` is not a legal-but-tight choice, it is bleed — **§0.1** |
167
- | `--atlas-in` | `build` only: resolve every part against the **regions of a pre-packed `.atlas`** instead of against loose PNGs. Region geometry is read from the file and sizes are descaled by the page's `scale:`; the atlas is re-emitted into `--out`, re-anchored — **§0.2** |
167
+ | `--atlas-in` | `build` and `explain`: resolve every part against the **regions of a pre-packed `.atlas`** instead of against loose PNGs. Region geometry is read from the file and sizes are descaled by the page's `scale:`; `build` re-emits the atlas into `--out`, re-anchored, and `explain` writes nothing and poses through it — **§0.2**. On `explain` it is the flag that makes a **size-only** spec readable at all (`ingest --art none`), because posing resolves every attachment against an atlas; without it that pair is refused by name rather than thrown through ([#697](https://github.com/firejune/rigc/issues/697), §5.1) |
168
168
  | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** — every `.png` in it is a part, in name order. For `chainfit` it is only where each attachment's image name **resolves**: the candidate decides what the parts are, so extra PNGs are unused and a missing name is refused by name (§12.3) |
169
169
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
170
170
  | `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
171
171
  | `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
172
- | `--profile` | `spine` = the 28 validity rules (**the default**) · `spine-html` = all 43, opt-in |
172
+ | `--profile` | `spine` = the 29 validity rules (**the default**) · `spine-html` = all 44, opt-in |
173
173
  | `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` is the one command that takes it more than once** — repeat it 2–4 times, one per pane, labelled A, B, C, D in the order given; everywhere else a repeat is a typo and is refused |
174
174
  | `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview`, and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote`, so is a name that only *some* candidates have |
175
175
  | `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
@@ -208,10 +208,15 @@ part per page" flat and rigc's own pack could not satisfy it. Since
208
208
  [#266](https://github.com/firejune/rigc/issues/266) that clause is **one part per
209
209
  page OR a tiling page**, so the combination is an ordinary build — and it is the
210
210
  only one that puts the renderer's own rulebook over shared-page sampling. What a
211
- *tiling* page has to satisfy is stated where the clause is, §7's `A06` row: every
212
- region wholly inside the page it names, and no two regions on one page
213
- overlapping. Rotation is still refused, and that is a separate clause about
214
- rigc's packer never turning a region.
211
+ *tiling* page has to satisfy under that profile is stated where the clause is,
212
+ §5.2's `A06` row: no two regions on one page overlapping. Rotation is still
213
+ refused, and that is a separate clause about rigc's packer never turning a region.
214
+
215
+ ⚠️ The other half of that sentence — every region wholly inside the page it names
216
+ — left this profile in [#694](https://github.com/firejune/rigc/issues/694). It is
217
+ **validity**, so no profile switches it off: a rectangle outside its page is
218
+ broken for every consumer, while two regions over the same texels is something
219
+ correct, editor-exported data does.
215
220
 
216
221
  ### 0.1 Packing the parts onto shared pages — `--pack`
217
222
 
@@ -319,6 +324,17 @@ texel count beside it so both numbers are visible:
319
324
  measures the PNG. Reach for `--atlas-in` when the pack is what you were handed, or
320
325
  when drawing through the pack's own texels is the point.
321
326
 
327
+ **What `--out` holds afterwards:** `skeleton.json` and a `skeleton.atlas` that is
328
+ the pack, page paths pointing back at the pack's own PNGs — so `rigc validate
329
+ <that directory>` reads it green with no flags, exactly as it reads a loose
330
+ build's. Add `--copy-images` and the pack's page PNGs are copied in beside the
331
+ skeleton and the page names become their basenames, which is the same directory
332
+ with nothing outside it left to resolve. ⚠️ Until
333
+ [#693](https://github.com/firejune/rigc/issues/693) that flag rebuilt the atlas
334
+ from the parts the rig declared instead of from the pack, and a rebuild through
335
+ `ingest --art none` declares none: the file written was **zero bytes**, on a build
336
+ that printed `PASS` for all four atlas assertions.
337
+
322
338
  The emitted `skeleton.atlas` **is** the imported one, verbatim except for its page
323
339
  name lines, which are paths and have to be re-anchored to `--out`. Fields rigc
324
340
  does not re-serialise (`format:`, `repeat:`, and `scale:` itself) survive the trip
@@ -334,7 +350,7 @@ Four things are refused rather than warned about, because each of them otherwise
334
350
  | a region name the atlas does not have | `AtlasAttachmentLoader` returns null and the part silently does not draw. The refusal lists the near misses — the usual cause is one character |
335
351
  | a size the spec disagrees with | the same silence `A06` exists for, one link earlier: a quad sized against a region of another size collapses |
336
352
  | a page the atlas names and the disk lacks | nothing to sample; caught on the way in, so the message names the atlas rather than the artifact rigc wrote from it |
337
- | a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does |
353
+ | a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does. The gate names the same rectangle, under every profile, for a pack that reaches it without passing through here — `A06`, §5.2 ([#694](https://github.com/firejune/rigc/issues/694)) |
338
354
 
339
355
  One limit, stated rather than discovered:
340
356
 
@@ -480,7 +496,7 @@ the first:
480
496
 
481
497
  | gutter | meaning |
482
498
  | --- | --- |
483
- | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `linkedmesh`, `point`, an attachment `sequence`, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
499
+ | `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `point`, an attachment `sequence`, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
484
500
  | `JUDGE` | the skeleton cannot answer and somebody has to: the stage, and each animation's duration |
485
501
  | `LOSS` | the skeleton's spelling and rigc's differ, on purpose, and the line says how. A path attachment's `lengths` is the one that matters — it is `PathConstraint`'s own four-sample measurement rather than an arc length (#560), so a transcribed one would freeze whatever produced the source. The header ones are cheaper: `HEADER_BOOKKEEPING` for a field the spec has no home for, `HEADER_REDERIVED` for the version string, `HEADER_ORIGIN` for an origin the source left to the format and the rebuild writes out (#622) |
486
502
 
@@ -518,6 +534,17 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
518
534
  extremes, how far each vertex moved and whether the winding survived
519
535
  (**§4.11.2**). It takes no `--profile`, it never gates, and it does not write
520
536
  anything — so the figures are readable on a build the gate is refusing.
537
+
538
+ ⭐ **It reads what `ingest` writes, given the pack** — `--atlas-in <pack.atlas>`,
539
+ with `build`'s meaning (§0.2). That matters more here than it looks: printing the
540
+ `DEFORM` block means **posing** the rig, a pose resolves *every* attachment
541
+ against the atlas whether or not anything deforms it, and a spec written by
542
+ `ingest --art none` states sizes and names no image. So the size-only pair
543
+ `build --atlas-in` gates green is readable here through the same flag, and
544
+ without it the pair is refused by name at exit 2 rather than posed
545
+ ([#697](https://github.com/firejune/rigc/issues/697), §5.1). ⚠️ `--profile`,
546
+ `--pack`, `--page-size`, `--padding` and `--copy-images` are `build`'s and are
547
+ not here: four of them decide what is *written*, and this command writes nothing.
521
548
  - **`diff`** compares two skeletons and reports **a ratio per measure** in six
522
549
  sections (bones, slots, attachments, constraints, animations, events). It
523
550
  deliberately does not combine them into a score: a rig with the right skeleton
@@ -922,7 +949,7 @@ and the inheritance silently falls back to Normal — assertion `A02` refuses it
922
949
  | `bone` | required; must be a bone this rig declares | — |
923
950
  | `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) — **except** on a slot nothing fills, where it can only be `null` and may be left out |
924
951
  | `color` | `rrggbbaa` tint | opaque white |
925
- | `dark` | two-colour tint, `rrggbb` | — (🚫 `A12` under `spine-html`) |
952
+ | `dark` | two-colour tint, `rrggbb`. The **setup** half; §4.4's `rgba2` track keys it over time and requires it | — (🚫 `A12` under `spine-html`) |
926
953
  | `blend` | `normal` · `additive` · `multiply` · `screen` | `normal` |
927
954
 
928
955
  ✅ **Every slot you declare is emitted, in this order.** A slot nothing fills — no
@@ -964,6 +991,12 @@ A skin can also say which bones and constraints it **switches on**, and that nee
964
991
  one more level, so a skin entry has a second spelling — see §3.4.1. The short one
965
992
  above is unchanged and is what almost every rig wants.
966
993
 
994
+ 🔸 **`default` is a name, not a requirement.** A rig may put every attachment in
995
+ named skins and declare no `default` at all, which is what an editor export of a
996
+ multi-skin character gives back; rigc still emits a `default` skin, empty, because
997
+ it always does. An animation keying such a slot resolves its attachment names
998
+ against every skin there is — §4.4 states that rule and §5.1 the one refusal left.
999
+
967
1000
  🔸 **A skin may fill no slot with anything that needs art, and that build is
968
1001
  green.** The atlas is built out of what the skins reference, so a rig whose skins
969
1002
  name no `image` — an empty `default`, or one carrying only a `boundingbox`, a
@@ -1158,6 +1191,60 @@ rather than the figure it was filed over:
1158
1191
  README carries the inradius arithmetic, both coverage readings, and the rim move
1159
1192
  that settled it.
1160
1193
 
1194
+ **Linked mesh** ([Spine: linked meshes](http://esotericsoftware.com/spine-meshes)) —
1195
+ a mesh that draws **another mesh's geometry** with **its own art**. It is the type
1196
+ a skin variant uses: one triangulation and one set of weights, several outfits over
1197
+ it. Say `type: "linkedmesh"`, or put `source` on a `type: "mesh"` — the format has
1198
+ both spellings, they share one parser branch, and `source` is what decides between
1199
+ them (`SkeletonJson.ts:568-569`, `:582`), so rigc reads them the same way.
1200
+
1201
+ | Field | Meaning |
1202
+ | --- | --- |
1203
+ | `source` | **required.** The **placeholder** of the mesh whose geometry this one draws — the key it is filed under in its skin, not its `name`. A miss is refused naming the skin, the slot and what that slot holds |
1204
+ | `slot` | the slot the source lives in. Default: **this attachment's own slot**. Resolved by name against the rig's slots |
1205
+ | `skin` | the skin the source lives in. Default: **`default`** — the default skin, *not* the skin this link is written in. Resolved by name |
1206
+ | `timelines` | default **`true`**: the link plays the source's `deform` keys. `false` makes it its own timeline target, so only keys written against the link move it |
1207
+ | `image`, `path`, `width`, `height`, `color` | exactly as on a mesh — the link resolves **its own** region, which is the point of the type |
1208
+
1209
+ ```json
1210
+ "skins": {
1211
+ "base": { "cloak": { "cloak": { "type": "mesh", "image": "cloak_red.png", "uvs": [], "triangles": [], "weights": [] } } },
1212
+ "winter": { "cloak": { "cloak": { "type": "linkedmesh", "image": "cloak_blue.png", "source": "cloak", "skin": "base" } } }
1213
+ }
1214
+ ```
1215
+
1216
+ 🚨 **A linked mesh states no geometry of its own, and every geometry key on one is
1217
+ refused by name.** `uvs`, `triangles`, `vertices`, `weights`, `boneIndexing`,
1218
+ `hull`, `edges` and `generator` are read by **nothing**: the parser returns from
1219
+ the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`). Measured on
1220
+ a forged skeleton — a link declaring 5 uvs, 3 triangles, `hull: 5` and
1221
+ `edges: [0, 2]` beside a 4-vertex source loaded with the **source's** 8-long
1222
+ `worldVerticesLength`, 6 triangles, `hullLength` 8 and 10 edges. Nothing the author
1223
+ wrote reached anything and nothing said so.
1224
+
1225
+ 🚫 **A chain is refused, and so is a link to itself.** A `source` that names
1226
+ another linked mesh resolves in the order the file was read: measured through
1227
+ spine-core, the chained link loaded the full geometry with the source declared
1228
+ first, and `worldVerticesLength` **0**, 0 triangles and a 0x0 size with the two
1229
+ keys swapped in the same file — silently, both ways. A construct whose meaning
1230
+ depends on JSON key order is one rigc will not write. Point `source` at the mesh.
1231
+
1232
+ ⚠️ **`width`/`height` are emitted and the gate cannot see them.** The runtime
1233
+ overwrites both with the source's when it resolves the link
1234
+ (`MeshAttachment.setSourceMesh`; measured: a link stating `99x77` beside a 32x32
1235
+ source loads as 32x32). They are written because the editor reads them off the
1236
+ file and because the spec stated them — R1 — and no assertion can check them.
1237
+
1238
+ 🔸 **`A21_MESH_RIM_PINNED` and `A28_RIBBON_ROWS_SHARE_WEIGHTS` leave a link out**,
1239
+ for the same reason they leave authored geometry out: the rim and the rows it draws
1240
+ are its source's and are measured there. `A21` drops it from the set it measures and
1241
+ **SKIPs by name** — naming the link and its source — when that leaves nothing;
1242
+ `A28` passes over it. `A04`, `A20` and `A22` read a link exactly as they read any
1243
+ other mesh, because after the round trip it **is** the source's triangles, weights
1244
+ and uvs. `A13_MESH_BUDGET` counts it as a mesh of its own: the runtime draws it as
1245
+ one, so a link in a second slot is a second mesh slot against
1246
+ `invariants.meshSlots`.
1247
+
1161
1248
  The generators are `ring`, `ribbon`, `contour` and `grid` (see
1162
1249
  [`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
1163
1250
  than a table of numbers, which is why they are code invoked by data. The last two
@@ -1213,7 +1300,7 @@ ends, so the deformation dies into the pinned rim instead of creasing against it
1213
1300
  | `inner` | **required.** Where the moving ring sits between the centre (`0`) and the hull (`1`), strictly inside that interval. No default: a ring with no number here is refused, not centred |
1214
1301
  | `size` | **required.** The part window, `[w, h]` in pixels, which the UVs and the emitted `width`/`height` are taken from |
1215
1302
  | `bias` | optional; absent means authority is radial only. `{ "axis_deg": <screen degrees, y down>, "ramp": [d0, d1] }` — a line through `center` at that angle, with control authority 0 on its negative side and 1 on its positive side, smoothstepped across the signed distances `d0 < d1`. It is what lets a mouth open downward with the upper lip left pinned |
1216
- | `controls` | **required.** The control bones, by name, at least one. Each must be a bone the rig declares |
1303
+ | `controls` | **required.** The control bones, by name, at least one. Each must be a bone the rig declares. **More than one splits the ring by angle**, and the angle of each is measured from where the rig put that bone relative to `center` — so the split is a consequence of the skeleton and never a number you write here |
1217
1304
 
1218
1305
  ⚠️ **`size` is stated here, not measured.** A `contour` and a `grid` take the
1219
1306
  window off the attachment's own `image`; a `ring` and a `ribbon` are built from
@@ -1223,17 +1310,24 @@ drawing — measured: a 240x240 part declared `"size": [64, 64]` builds green un
1223
1310
  `--profile spine-html`. That is R1 rather than a gap: the compiler emits the
1224
1311
  number the spec states and does not re-measure a plate to overrule it.
1225
1312
 
1226
- 🚨 **On this route the FIRST control bone is the only one that moves the mesh.**
1227
- Splitting a ring's authority between several grips needs each bone's angle about
1228
- the aperture centre, and rigc measures that from where the rig put the bone — on
1229
- the **manifest** route, which is the one that has a crop to measure in. A rig spec
1230
- that lists two gets the single-bone geometry instead: `controls[0]` takes the
1231
- whole of the control authority, the second name is still printed on the `MESH`
1232
- line, and the gate is green. Measured on a two-control ring — 25 vertices, 40
1233
- triangles, report line `bones=[box, grip_a, grip_b]`, and the emitted weighted run
1234
- binds two bone indices, the slot bone and `grip_a`. Until that is closed, write
1235
- one `controls` entry on this route and reach for the manifest when a ring needs
1236
- several grips.
1313
+ ⭐ **Several grips split the ring by where the rig put them, on this route as on
1314
+ the manifest one.** Each control bone's angle about `center` is measured from its
1315
+ own rest position — the window is centred on the slot bone (the ⭐ above), so a
1316
+ bone sitting below the aperture owns the arc below it — and authority is
1317
+ smoothstepped between the two bones either side of a vertex, so no grip creases
1318
+ against the next. Measured on a two-control ring whose grips sit 12px above and
1319
+ below the centre: 25 vertices, both grips bound, and of the 8 shared vertices off
1320
+ the centre line all 8 take more weight from the grip on their own side. Splitting
1321
+ **moves** authority rather than adding it: every vertex gives its controls the
1322
+ same total with one grip, two or three.
1323
+
1324
+ ⚠️ **This route used to bind only the first name**
1325
+ ([#684](https://github.com/firejune/rigc/issues/684)). It passed no angles at all,
1326
+ so the split collapsed onto `controls[0]` and the rest of the names reached the
1327
+ `MESH` line, the bone list and nothing else — and the gate was green, because a
1328
+ bone no vertex binds is in no weight, no sum and no index. `A20_MESH_WEIGHTS_COHERENT`
1329
+ now names it, so a ring that declares a grip and does not use it is a failure
1330
+ rather than a quiet stiffness.
1237
1331
 
1238
1332
  **Stated limits, each a named refusal rather than a mesh that loads wrong:**
1239
1333
 
@@ -1245,6 +1339,8 @@ several grips.
1245
1339
  | a hull the centre cannot see all of | `hull is not star-shaped about the aperture centre; the inner ring would fold` — the inner ring is the hull scaled toward `center`, so an edge hidden from it crosses the rim and renders as folded meat |
1246
1340
  | a `bias` ramp that does not increase | `bias ramp must increase, got [16, 4]` |
1247
1341
  | a control bone the rig does not declare | `mesh bone "nobody" is not in the rig's bone list` |
1342
+ | two or more `controls`, one of them ON `center` | `control bone "iris_aperture" sits on the aperture centre, so it has no radial direction` — the position a lone control is supposed to occupy is the one position a split cannot use. With a single control it is never asked, so this refusal cannot fire on one |
1343
+ | two `controls` at the same angle about `center` | `two control bones share the angle 90 degrees about the aperture centre` — further out is not elsewhere: the arc between them is empty and one of them would bind nothing |
1248
1344
 
1249
1345
  #### `ribbon` — a strip of cross rows riding a bone chain
1250
1346
 
@@ -1931,6 +2027,20 @@ rigc emits all five: `ik`
1931
2027
  and `slider`. Field lists are in [`src/rig.ts`](../src/rig.ts); the traps worth
1932
2028
  carrying here:
1933
2029
 
2030
+ - 🔑 **A constraint name is unique per KIND, not across the array.** Spine
2031
+ resolves one with `SkeletonData.findConstraint(name, type)`, which tests
2032
+ `constraint instanceof type` **before** it compares the name, and every
2033
+ resolution in the format goes through it: a timeline group, a skin's member
2034
+ list (§3.4.1), a slider's animation. So an `ik` constraint and a `transform`
2035
+ constraint may both be called `leg` — the editor exports both, the runtime
2036
+ finds each from its own group, and a motion spec's `ik` block, `transform`
2037
+ block and `path`/`physics`/`slider` tracks each name the kind they mean and
2038
+ resolve the same way (§4.9, §4.12). Two constraints **of one kind** sharing a
2039
+ name are refused (§5.1), because no timeline could say which was meant. Until
2040
+ [#692](https://github.com/firejune/rigc/issues/692) the rig spec kept one
2041
+ namespace over the whole array, so a rig the editor exports and the runtime
2042
+ plays — an IK chain and the transform constraint that follows it, both carrying
2043
+ the chain's name — could not be written down at all.
1934
2044
  - A transform constraint's `properties` names come from a fixed six — `rotate`,
1935
2045
  `x`, `y`, `scaleX`, `scaleY`, `shearY`. rigc refuses anything else by name; in
1936
2046
  raw JSON the parser throws.
@@ -2722,6 +2832,7 @@ a deform). Folding them in would make `v` mean four different things depending o
2722
2832
  | `bone` | `translate`, `scale`, `shear` | `[x, y]` |
2723
2833
  | `bone` | `translatex`, `translatey`, `scalex`, `scaley`, `shearx`, `sheary`, `rotate` | `[value]` |
2724
2834
  | `slot` | `rgba` | `[r, g, b, a]` in 0..1 |
2835
+ | `slot` | `rgba2` | `[lr, lg, lb, la, dr, dg, db]` in 0..1 — the two-colour tint, light then dark, **seven** channels. The slot must declare a setup `dark` (§3.3) |
2725
2836
  | `slot` | `attachment` | the attachment name, or `null` for "show nothing" |
2726
2837
  | `physics` | `inertia`, `strength`, `damping`, `mass`, `wind`, `gravity` | `[value]` — the constraint's own tuning, keyed over time |
2727
2838
  | `physics` | `mix` | `[mix]`, **0 or more** — the constraint's authority |
@@ -2765,9 +2876,9 @@ accepts is what it accepts.
2765
2876
  it actually poses, both ways, which is what makes the list checkable at all: it
2766
2877
  was stated there too until the refusal had something to state.
2767
2878
 
2768
- ⚠️ **A `slot` track's `property` is one of the two above, and anything else is a
2879
+ ⚠️ **A `slot` track's `property` is one of the three above, and anything else is a
2769
2880
  compile error** — `animation "A" slot "X" has no timeline "P" (it has:
2770
- attachment, rgba)`, §5.1's row. Until
2881
+ attachment, rgba, rgba2)`, §5.1's row. Until
2771
2882
  [#650](https://github.com/firejune/rigc/issues/650) it was not: the emitter had a
2772
2883
  branch for `attachment` and wrote **everything else** as an rgba timeline under
2773
2884
  the name you gave it, so a track spelled `sequence` compiled, emitted
@@ -2776,21 +2887,33 @@ the name you gave it, so a track spelled `sequence` compiled, emitted
2776
2887
  one-channel spelling of the same mistake was refused at compile as `rgba value
2777
2888
  needs 4 channels, got 1`, a message about a key you had not written.
2778
2889
 
2779
- - The pair is the emitter's own dispatch table (`SLOT_TRACKS` in
2890
+ - The three are the emitter's own dispatch table (`SLOT_TRACKS` in
2780
2891
  `src/compile.ts`): `compileTrack` reads it to pick its branch, and the refusal
2781
2892
  prints `Object.keys` of the same object, so what you are told a slot accepts
2782
2893
  is what it accepts.
2783
- - **Nothing derives this page's copy of that pair from the table**, and the list
2784
- is two names long: no `DQ*`/`RD*`/`CUR*` control reads §4.4 (the only gated
2894
+ - **Nothing derives this page's copy of that list from the table**, and it is
2895
+ three names long: no `DQ*`/`RD*`/`CUR*` control reads §4.4 (the only gated
2785
2896
  table on this page is §3.5.2.1's, held by `RD01`–`RD06`). What keeps the two
2786
2897
  in step is the control that quotes the message — `RF23` in `selftest.ts` —
2787
- which goes red if the accepted pair ever widens without this page moving with
2788
- it.
2789
- - The format has four more slot timelines (`rgb`, `alpha`, `rgba2`, `rgb2`) and
2790
- rigc emits none of them, so their names are refused here too; `A12_NO_DARK_COLOR`
2791
- refuses the last two in a file rigc did not write (SPEC_COVERAGE §2.1).
2792
- `sequence` is a timeline on an **attachment**, not on a slot, and rigc does
2793
- not emit that either.
2898
+ which goes red if the accepted list ever widens without this page moving with
2899
+ it. It did, on the day `rgba2` was added
2900
+ ([#690](https://github.com/firejune/rigc/issues/690)), which is the mechanism
2901
+ working rather than a hole in it.
2902
+ - 🎨 **`rgba2` keys the two-colour tint, and the slot has to own one first.** A
2903
+ track `{ "slot": "X", "property": "rgba2" }` on a slot whose rig spec declares
2904
+ no `dark` (§3.3) is a compile error with the slot named, and it is not a
2905
+ formality: the runtime allocates a slot's dark colour only when its setup pose
2906
+ has one, so a file keying it without one parses cleanly and then throws in the
2907
+ player the first time the animation is applied. `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN`
2908
+ (§5.2) holds the same pairing on a skeleton rigc did not write. 🚫 Both halves
2909
+ of the two-colour tint are refused under `--profile spine-html`, whose renderer
2910
+ ignores them; the default `spine` profile that `build` runs reports `A12` as
2911
+ `PROF` and never applies it.
2912
+ - The format has three more slot timelines (`rgb`, `alpha`, `rgb2`) and rigc
2913
+ emits none of them, so their names are refused here too; `A12_NO_DARK_COLOR`
2914
+ refuses `rgb2` — and `rgba2`, and the slot field — in a file under that
2915
+ renderer's profile (SPEC_COVERAGE §2.1). `sequence` is a timeline on an
2916
+ **attachment**, not on a slot, and rigc does not emit that either.
2794
2917
 
2795
2918
  ⚠️ **A `group` track's `property` is one of those two lists or the physics one,
2796
2919
  and anything else is a compile error** — `animation "A" group "G" has no timeline
@@ -2808,7 +2931,7 @@ resolved against the rig.
2808
2931
  with no table claiming the property the track fell through to the slot branch,
2809
2932
  and what you were told was that the first member is not a slot — on a file that
2810
2933
  named neither a slot nor that member. A group of **slots** got §4.4's slot row
2811
- instead (`slot "M" has no timeline "P" (it has: attachment, rgba)`), which is
2934
+ instead (`slot "M" has no timeline "P" (it has: attachment, rgba, rgba2)`), which is
2812
2935
  true of the member and names one family out of three on a track whose family
2813
2936
  nothing had determined.
2814
2937
  - **A constraint property never reaches it.** `position`, `spacing` and `time` are
@@ -2871,6 +2994,36 @@ different curves. The same applies to `scale`/`scalex`/`scaley` and
2871
2994
  An `attachment` key carries no easing — attachment timelines are inherently
2872
2995
  stepped.
2873
2996
 
2997
+ 🔑 **An attachment key resolves against every skin, not against `default` alone.**
2998
+ A slot's `attachment` timeline carries a name and **no skin** — the format has no
2999
+ field for one — so the names a key may use are the union of every skin's
3000
+ placeholders for that slot, the default skin's included.
3001
+ `Skeleton.getAttachment` resolves the keyed name at run time through the skin the
3002
+ skeleton is **wearing** and, failing that, through `defaultSkin` (spine-core
3003
+ 4.3.13 `Skeleton.js:335-346`), so which skin's art a key lands on is the
3004
+ consumer's, decided by dressing the skeleton rather than by the animation.
3005
+
3006
+ - **A name some skins fill and others do not is accepted, and that is the
3007
+ format's own semantics** rather than a hole in the check: under a skin that
3008
+ lacks it the slot shows nothing, which is exactly what a `null` key says and a
3009
+ thing a rig may well mean. What is refused is a name **no** skin holds, and the
3010
+ refusal says which skins were searched and what the slot does have (§5.1).
3011
+ - A rig with **no `default` skin at all** — every attachment in named skins,
3012
+ which is the shape an editor export of a multi-skin character gives back — is
3013
+ therefore a rig whose attachment keys work. Until
3014
+ [#695](https://github.com/firejune/rigc/issues/695) it was not: keys were
3015
+ checked against the default skin alone, so a named-skin name was refused as
3016
+ unknown, and a rig with no default skin had **every** attachment key refused,
3017
+ including ones whose art is in the first named skin. The setup pose resolved
3018
+ across skins the whole time (§4.2), so the two halves of one slot disagreed —
3019
+ `slots[].attachment: "plain"` was accepted and a key naming `plain` on that
3020
+ same slot was not.
3021
+ - ⚠️ **A `deform` track is the other way round and names its skin outright**
3022
+ (§4.11.5), because the format keys a deform timeline on a `skin/slot/attachment`
3023
+ triple and a deform run is geometry for one attachment object. An attachment
3024
+ timeline has no such field, which is why this one is a union and that one is a
3025
+ lookup.
3026
+
2874
3027
  ### 4.5 `keys` — times, values, curves
2875
3028
 
2876
3029
  - `t` is in seconds and **must strictly increase** after `lag`/`stagger` are added.
@@ -3441,6 +3594,35 @@ Per key:
3441
3594
  | `offset` | the same start as a raw index into the deform array. Never with `fromVertex` |
3442
3595
  | `ease` / `curve` | one channel, and it eases the **blend**, not a coordinate |
3443
3596
 
3597
+ **The target is any attachment that has a vertex array** — a mesh, a bounding
3598
+ box, a clipping polygon, or a **path**. The array a key edits is that
3599
+ attachment's own, so everything below about runs, start indices and the two
3600
+ encodings reads the same whichever it is. A region attachment has no vertex array
3601
+ and is refused by name.
3602
+
3603
+ ⭐ **A path's vertices are its control points** — knots and their Bezier handles
3604
+ alike, in the order §3.5.1 lists them — so a run covers them in that order and a
3605
+ `fromVertex` counts them the same way. Its deform array is `vertexCount * 2` long
3606
+ unweighted, and one pair per influence weighted, exactly as a mesh's is. What is
3607
+ and is not measured on one:
3608
+
3609
+ - `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` measures the run against that length, as
3610
+ it does for every other target.
3611
+ - `A39_DEFORM_KEEPS_TRIANGLE_WINDING` reports **SKIP**, naming the slot and
3612
+ saying the attachment has no triangles. A path is knots and handles; there is
3613
+ no winding to keep, and reporting a pass for a measurement that did not happen
3614
+ is the one thing an assertion here may not do.
3615
+ - **`lengths` is not re-measured, and cannot be.** It is a field of the
3616
+ attachment (§3.5.1) and the format has nowhere to put a per-key one, so the
3617
+ array every exporter writes — rigc's included — is the **setup** measurement.
3618
+ `PathConstraint.computeWorldPositions` reads it only when `constantSpeed` is
3619
+ `false`; under the parser's default, `true`, it re-measures the curve from the
3620
+ posed vertices every frame. So a path constraint follows the deformed curve as
3621
+ written, and under `constantSpeed: false` it traverses the deformed curve at
3622
+ the **setup** spacing. That is the format's behaviour rather than rigc's
3623
+ choice, which is why it is stated here instead of refused: an editor export of
3624
+ the same rig does the same thing.
3625
+
3444
3626
  Four things are worth having straight before you write one.
3445
3627
 
3446
3628
  **A key is a sparse edit, and a key with no `vertices` is the setup pose.** The
@@ -3494,6 +3676,7 @@ half of the format:
3494
3676
  | `fromVertex` on a multi-bone vertex | `"fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` |
3495
3677
  | an attachment that is not there | `slot "flat" in skin "default" has no attachment "flatt" (it has: flat)` |
3496
3678
  | a deform on a region attachment | `a deform timeline keys the vertices of an attachment, and this one is a "region"` |
3679
+ | a run past a path's control points | `this attachment's deform array is 18 long (9 vertices)` — the same bound as any other target, counted in the control points §3.5.1 declares |
3497
3680
  | `transform` beside a `vertices` run | `the key carries both a "transform" and a "vertices" run, and they are two answers to one question` |
3498
3681
  | `transform` with `fromVertex` or `offset` | `A transform is a model of the whole attachment and is evaluated over all 25 of its vertices, so it always starts at deform index 0` |
3499
3682
  | a `transform` on an attachment whose weights do not close at 1 | `this one has a vertex the arithmetic cannot place — vertex 1's 2 weights sum to 0.9000 rather than 1` |
@@ -4371,6 +4554,9 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4371
4554
  | `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 |
4372
4555
  | `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
4373
4556
  | `two bones are called "X"` | bone names are the join key; rename one |
4557
+ | `two ik constraints are called "X" — a constraint resolves by name AND type (\`SkeletonData.findConstraint\`), so names are unique PER KIND: an ik and a transform constraint may share one, two of a kind may not` | §3.5 — rename one of the two. The kind in the sentence is the pair's own, so `two transform constraints are called "X"` is the same refusal on another kind; a name shared **across** kinds is not this error and never was one to fix |
4558
+ | `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 |
4559
+ | `skin "S" activates ik constraint "X", which skin "T" already activates; a constraint belongs to one skin` | §3.4.1 — a constraint runs under one skin or under all of them. The kind is in the sentence because `ik` "X" and `transform` "X" are two constraints, and each may belong to a different skin |
4374
4560
  | `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
4375
4561
  | `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 — pick one file and declare it there. A slot **nothing** fills is exempt: its setup pose can only be "show nothing" and is not asked for |
4376
4562
  | `the setup pose shows attachment "A" on slot "X", which no skin and no manifest part fills` | §3.3 — the slot is emitted empty and nothing was ever going to fill it, so `A` resolves to nothing. Give the slot an attachment (a skin entry or a manifest part), or state the setup pose as `null` |
@@ -4379,7 +4565,14 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4379
4565
  | `a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG` | §3.4 — the same rule for a mesh |
4380
4566
  | `"type" is null, which is not a name. An attachment's type is one of region, mesh, linkedmesh, … or the key is absent and reads as "region"` | §6 — **remove the key**. Absent is the format's own default; present-and-null matches no parser case and the attachment is dropped in silence |
4381
4567
  | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (…)` | §6 — a name the format does not have. Not a deferral, and not something rigc will grow: fix the spelling (`sequence` is a key on a region or a mesh, not a type) |
4382
- | `this attachment is a "linkedmesh"` / `"point"` … `rigc does not emit it yet` | §6 — a construct the format has and rigc does not write. The message says what it would carry; SPEC_COVERAGE part 1-6 is the row it reads from |
4568
+ | `this attachment is a "point" … rigc does not emit it yet` | §6 — a construct the format has and rigc does not write. The message says what it would carry; SPEC_COVERAGE part 1-6 is the row it reads from |
4569
+ | `a linked mesh needs "source" — the PLACEHOLDER of the mesh whose geometry it draws …` | §3.4 — `source` is what MAKES a mesh linked, and the parser falsy-tests it, so an absent or empty one is read as an ordinary mesh and throws on the `uvs` a link has not got |
4570
+ | `a linked mesh states "uvs", "triangles", …, and a linked mesh has no geometry of its own` | §3.4 — remove them, or remove `source` and author this as a mesh. The parser returns before `readVertices`, so those keys are read by nothing at all |
4571
+ | `"source" is "X", and skin "S" … slot "L" … holds 2: "a", "b"` | §3.4 — `source` is the PLACEHOLDER the source is filed under, not its `name`. A clause after the skin and after the slot says whether each was stated or taken from the parser's default — **the default skin** and **this attachment's own slot**, which is the pair that surprises |
4572
+ | `"slot" is "X", which the rig does not declare as a slot` / `"skin" is "X", … the rig declares no such skin` | §3.4 — a link resolves both by name. Left to the round trip these are the runtime's `Source mesh slot not found` and `Skin not found`, which name neither the attachment nor where it looked |
4573
+ | `"source" is "X", which is itself a linked mesh, and a chain of them is refused` | §3.4 — point `source` at the mesh. A chain resolves in file order and loads nothing at all in one of the two orders, silently |
4574
+ | `"source" is "X", which is a "region" attachment and not a mesh` | §3.4 — a link takes another MESH's geometry; off any other type the runtime reads `undefined` and says nothing |
4575
+ | `a linked mesh needs width and height — give them, or give an "image" and rigc will measure the PNG` | §3.4 — the mesh rule, on a link. Its art is its own |
4383
4576
  | `hull N disagrees with the triangles, whose outline has K vertices (0 → …)` | §3.4 — delete `hull`, or state K |
4384
4577
  | `hull vertices must come first; vertex i is on the boundary and vertex j is not. The triangles' outline runs …: list those K vertices first, in that order, then the M interior vertices` | §3.4 — renumber the vertices: the printed walk first, then the interior |
4385
4578
  | `hull vertices must trace the outline in order; the triangles' outline runs …, so vertex a has to follow vertex b in the list, and vertex c does` | §3.4 — renumber along the printed walk |
@@ -4395,6 +4588,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4395
4588
  | `motion spec names archetype "A" but the rig spec at … is called "B"` | make `archetype` equal the rig's `name` |
4396
4589
  | `animation "A" declares duration Ns but its last key is at Ms` | R7 — fix whichever of the two you meant |
4397
4590
  | `animation "A" slot "X" attachment: key at Ns is Ms past the declared duration Ds` | §4.5 — the key is past the end of the animation and nothing will sample it. Move the key onto `duration`, or raise `duration` |
4591
+ | `animation "A" slot "X" attachment: attachment "N" is not in slot "X" under any skin (searched: default, alt) — the slot has: plain, trim` | §4.4 — the keyed name is in **no** skin, and the two clauses say where the compiler looked and what it would have taken. Fix the spelling, or give some skin a placeholder called `N`. A name only a NAMED skin fills is not this error and never was one to fix — it compiles, and the slot shows nothing under the skins that lack it. Before [#695](https://github.com/firejune/rigc/issues/695) the message read `attachment "N" is not in slot "X"` and was raised against the **default skin alone**, so it fired on correct rigs: any key into named-skin art, and every key in a rig with no default skin. `the slot has no attachments at all` is the same message where nothing fills the slot |
4398
4592
  | `animation "A" keys unknown bone "X"` | the track's `bone` is not in the rig |
4399
4593
  | `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (§4.4) |
4400
4594
  | `animation "A" physics constraint "C" mass key at t=… is 0 (massInverse Infinity); must be > 0 — …` | §4.4 — a keyed physics value the runtime cannot use. The message names the bound and the `PhysicsConstraint.js` lines that make it one: `mass` and `strength` are `> 0`, `damping` is inside `(0, 1)`, `mix` is `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)) |
@@ -4430,7 +4624,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4430
4624
  | `animation "A" keys "X" as an ik constraint, but the rig declares it as a "transform" constraint` | §4.9 — a timeline's target resolves by name AND type; put the entry under the right group |
4431
4625
  | `ik constraint "X": key 0 names "softness" and key 1 (t=…) does not` | §4.9 — every key is read with its own default, so state the field on every key or on none |
4432
4626
  | `ik constraint "X" (t=…): mix is 1.5, outside 0..1` | §4.9 — an IK mix is a percentage; a transform mix is unbounded |
4433
- | `deform …: the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long` | §4.11 — shorten the run or move its start; the parser would drop the tail in silence |
4627
+ | `deform …: the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long (4 vertices)` | §4.11 — shorten the run or move its start; the parser would drop the tail in silence. The count in brackets is the **target's own**: a mesh's or a path's vertices, or a weighted attachment's bone influences |
4434
4628
  | `deform …: "fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` | §4.11 — key the control bone, or write bind-space pairs and start with `offset` |
4435
4629
  | `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
4436
4630
  | `deform … (t=…): the key carries both a "transform" and a "vertices" run` | §4.11.1 — a model and a table are two answers to one question; drop one |
@@ -4457,14 +4651,45 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4457
4651
  | `animation "A" keys "X" as a path constraint, but the rig declares it as a "slider"` | §4.12 — a timeline group resolves by name AND type; use the field named after the constraint's own type |
4458
4652
  | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
4459
4653
  | `rgba value needs 4 channels, got 3` | §4.4 — an `rgba` key is `[r, g, b, a]`. It names no animation, slot or key time, and the only input that reaches it is a slot `rgba` key: the setup pose's `color` is refused earlier, by its own row, with the slot named |
4654
+ | `rgba2 value needs 7 channels, got 6` | §4.4 — an `rgba2` key is `[lr, lg, lb, la, dr, dg, db]`: the light colour with its alpha, then the dark colour **without** one. Six is the commonest way to get it wrong, because the dark half looks like it should take an alpha too — the format has no channel for it, and neither does the runtime's `setFrame`. Like the row above it names no animation or key time; the only input that reaches it is a slot `rgba2` key |
4460
4655
  | `animation "A" bone "B" has no timeline "P" (it has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate)` | §4.4 — a bone has exactly ten timelines and `P` is none of them. Fix the spelling — the single-axis ones are lower-case (`translatex`, not `translateX`). A **constraint** property is refused first, by its own row, naming the field its constraint's name goes in. When `P` is a slot timeline the message says so and where to put the name: `. "rgba" is a slot timeline — put the name in "slot"`. Before [#656](https://github.com/firejune/rigc/issues/656) all of them read `bone "B" cannot take slot property "P"`, which named the slot family whatever you had written and listed nothing |
4461
- | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate; a slot group has: attachment, rgba; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | §4.3, §4.4 — a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one. Before [#661](https://github.com/firejune/rigc/issues/661) a group of bones read `animation "A" targets unknown slot "M"` and a group of slots got the slot row below, naming one family out of three |
4462
- | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba)` | §4.4 — a slot has exactly two timelines and `P` is neither. Fix the spelling; a bone or constraint property written on a slot track is refused by its own row instead. Before [#650](https://github.com/firejune/rigc/issues/650) every other name compiled as an **rgba** timeline called `P`, and what you saw was `A00_ROUNDTRIP_PARSE` on the emitted file — or, for the one-channel spelling, `rgba value needs 4 channels, got 1` |
4656
+ | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate; a slot group has: attachment, rgba, rgba2; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | §4.3, §4.4 — a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one. Before [#661](https://github.com/firejune/rigc/issues/661) a group of bones read `animation "A" targets unknown slot "M"` and a group of slots got the slot row below, naming one family out of three |
4657
+ | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba, rgba2)` | §4.4 — a slot has exactly three timelines and `P` is none of them. Fix the spelling; a bone or constraint property written on a slot track is refused by its own row instead. Before [#650](https://github.com/firejune/rigc/issues/650) every other name compiled as an **rgba** timeline called `P`, and what you saw was `A00_ROUNDTRIP_PARSE` on the emitted file — or, for the one-channel spelling, `rgba value needs 4 channels, got 1` |
4658
+ | `animation "A" slot "X" rgba2: slot "X" declares no setup "dark", and an "rgba2" timeline poses a slot's dark colour …` | §3.3, §4.4 — the two-colour tint has a setup half and a keyed half, and the keyed half cannot exist without the other. `Slot`'s constructor allocates a dark colour only for a slot whose setup pose declares one, and `RGBA2Timeline` writes it unconditionally — so without the `dark` the file loads, and the first `state.apply` throws `TypeError: null is not an object` in the consumer's process. Give the slot the `dark` it holds at rest, or key `rgba` if only the light colour moves. Raised before the keys are read, with the slot named, for the same reason the row above is |
4463
4659
  | `N pair(s) of animation names have no one order: … "turn" / "Turn" (case) — they are one name in two cases, and which of them the editor puts first is not measured; rename one of them so they differ by more than letter case` | **R10** — rename until no pair is left. The kind in brackets says which of the editor comparator's four UNMEASURED choices decides the pair: `case` (a pure case tie), `number` (one number written two ways, or a run of digits against a word) or `separator` (make the first character that differs a letter or a digit). rigc keys `animations` in the editor's own comparator — natural and case-insensitive ([#539](https://github.com/firejune/rigc/issues/539), [#543](https://github.com/firejune/rigc/issues/543)) — so a pair that comparator settles is emitted rather than refused, and only the four choices nobody has measured are a compile error; on those, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
4464
4660
  | `N pair(s) of skin names have no one order: … "Zulu" / "mike" (case) — folded to one case "Zulu" and "mike" order the other way round, so whether the editor folds SKIN names decides this pair` | **R11** — rename until no pair is left. The same shape as the row above with a **wider** family: #539 measured the editor's comparator for animation names and thereby ruled codepoint out, and nothing has ruled anything out for skin names, so a pair the candidates could disagree about is refused even where the animation rule would emit it. `Zulu`/`mike` and `mike10`/`mike2` build as animation names and are refused as skin names ([#541](https://github.com/firejune/rigc/issues/541)) |
4465
4661
  | `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that … Move the default skin's entry for this slot into a named skin — call it "base"` | **R12** — do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and §3.4.2 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
4466
4662
  | `N attachment name collision(s): a placeholder that more than one skin fills is emitted with the name "<skin>/<placeholder>" … slot "patch": skin "base" placeholder "zulu/patch" and skin "zulu" placeholder "patch" would both be named "zulu/patch"` | **R12** — rename the placeholder or the skin. rigc composes an attachment name for every placeholder more than one skin fills (§3.4.2), and this fires when a composed name is one another entry in the same slot already answers to — including a plain name in the default skin, which composed nothing. Both sites are named; either rename ends it |
4467
4663
 
4664
+ ⚠️ **One refusal in this section is not a `CompileError`, and it is `explain`'s.**
4665
+ `explain` prints the `DEFORM` block by **posing** the rig, and a pose resolves every
4666
+ attachment against the atlas — so it needs the art `build` needs, reached the same
4667
+ two ways (§0.2). A pair whose art it cannot resolve is refused **before a line of
4668
+ the report**, at **exit 2**, in rigc's own sentence:
4669
+
4670
+ ```
4671
+ rigc explain: skin "default" slot "lamp" placeholder "shade": attachment "shade"
4672
+ wants region "shade", which this build's atlas does not have (it declares no region
4673
+ at all). `explain` poses the rig to measure its deform keys and a pose resolves
4674
+ every attachment against the atlas, so there is nothing to pose it against. Art
4675
+ reaches a compile two ways and this run took neither: `--atlas-in <pack.atlas>`
4676
+ resolves the parts against a pack somebody already made, and an "image" per
4677
+ attachment resolves them as loose PNGs under `--images <dir>` — a spec that states
4678
+ a size and names no image is what `ingest --art none` writes, and `--atlas-in` is
4679
+ what reads it. 1 of 1 attachment lookup(s) here resolve to no region.
4680
+ ```
4681
+
4682
+ Both halves of it are measured rather than fixed text: the count is this pair's, and
4683
+ where the atlas has a **near miss** the sentence prints that too, in `A08`'s own
4684
+ clause and `A08`'s own words. When `--atlas-in` **was** given and the region is still
4685
+ absent, the second half names the pack instead and asks you to fix the spec's region
4686
+ name or point the flag at the pack that has it. Until
4687
+ [#697](https://github.com/firejune/rigc/issues/697) there was no rigc sentence at
4688
+ all: the report printed in full and the run then died inside `AtlasAttachmentLoader`
4689
+ with `Region not found in atlas: shade (attachment: shade)` and a spine-core stack
4690
+ trace, at exit 1 — the runtime's message about rigc's internals standing in for
4691
+ rigc's message about your two files.
4692
+
4468
4693
  ### 5.2 Assertions — the gate
4469
4694
 
4470
4695
  The report prints one line per assertion:
@@ -4531,7 +4756,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
4531
4756
  | `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height`. **SKIP** when the skeleton carries no region attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
4532
4757
  | `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
4533
4758
  | `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel**. **SKIP** when no animation carries a timeline at all ([#580](https://github.com/firejune/rigc/issues/580)). Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one |
4534
- | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk. Under `spine-html` also: `pma`, rotation, and a page that is neither **one part covering it exactly** (the unpacked convention) nor a **tiling** — a page whose regions all sit inside it and none of which overlap ([#266](https://github.com/firejune/rigc/issues/266)). A packed atlas therefore gates under this profile; what the message names is the region that runs off its page, or the pair that shares texels. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
4759
+ | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk, **or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles ([#694](https://github.com/firejune/rigc/issues/694)): a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels — a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling** ([#266](https://github.com/firejune/rigc/issues/266)), and what that message names is the pair that shares texels. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
4535
4760
  | `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. ⚠️ 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` |
4536
4761
  | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. Until [#589](https://github.com/firejune/rigc/issues/589) they were unreachable: the loader threw first and the miss arrived as `A00_ROUNDTRIP_PARSE`. There is no `spine-html` clause here any more — a placeholder is free to differ from the region its `path` names ([#574](https://github.com/firejune/rigc/issues/574)) **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once ([#580](https://github.com/firejune/rigc/issues/580)) |
4537
4762
  | `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 |
@@ -4545,7 +4770,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
4545
4770
  | `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) — as it is for `A06`, `A19` and `A27`; see `A07` ([#608](https://github.com/firejune/rigc/issues/608)) |
4546
4771
  | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
4547
4772
  | `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the full-stage base plate may be opaque. Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art ([#266](https://github.com/firejune/rigc/issues/266)) **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
4548
- | `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, or a binding at weight 0. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
4773
+ | `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not ([#684](https://github.com/firejune/rigc/issues/684)). Fix the rig spec's `controls`/`chain`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
4549
4774
  | `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1 |
4550
4775
  | `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
4551
4776
  | `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, is muted by `mix: 0`, has `mass: 0`, has `strength: 0`, or has `damping` outside `(0, 1)` so it never settles — **at rest, and on every physics timeline key** ([#610](https://github.com/firejune/rigc/issues/610)). The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. One difference between the two arms, and the runtime is the reason for it: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. **SKIP** when the skeleton declares no physics constraint ([#580](https://github.com/firejune/rigc/issues/580)) — the same sentence `A36` and `A37` have always printed for their own constraint types |
@@ -4568,6 +4793,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
4568
4793
  | `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be applied additively (a slot colour, an attachment swap, a draw order, an ik mix, a path's `spacing`, most physics properties), where `"additive": true` is not the fix and one of the two has to go. ⭐ Which of the two it is, is **posed rather than read off `Timeline.additive`**: the shared timeline is applied twice with `add` set and the detail says what it did ([#655](https://github.com/firejune/rigc/issues/655) — two classes declare that flag falsely about themselves, so a path constraint's `mix` and a slider's `time` were refused although they compose). The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, which one wins today, and the class that was posed. Four shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, two sliders on different properties, and a shared timeline that writes **nothing a pose holds** — an `events` timeline fires no event under a slider (`firedEvents` is null), so neither slider has anything there for the other to erase. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
4569
4794
  | `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` | both | a physics constraint driving a component the **Spine editor** cannot hold, on a rig that declared `invariants.editorRoundTrip` (§3.7). The editor's physics model holds `x` and `y` only, with no cap on how many at once, so a constraint driving `rotate`, `scaleX` or `shearX` is imported, exported and handed back driving **nothing** — measured over three rigs and twelve constraints with the predictions written first ([#540](https://github.com/firejune/rigc/issues/540)). The detail names the constraint and each component. ⚠️ rigc's own output is correct — every runtime plays a rotation jiggle — so this is opt-in and the default is *not* silence: on a rig that declares nothing it **SKIPs**, and the SKIP names the constraint and the component anyway, so an author learns without having asked. Fix by driving the constraint in `x`/`y`, or by dropping the declaration if the rig never goes near the editor. Disjoint from `A23_PHYSICS_CONSTRAINT_EFFECTIVE` by construction: A23 refuses an **empty** driven set, which is what comes back from the editor, and this refuses a non-empty one that will not survive going in. **SKIP** also when the rig declares the editor and carries no physics constraint at all |
4570
4795
  | `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` | both | a slider whose animation keys a property of a constraint **at or before it** in `constraints` (§3.5.2) — a slider's `mix` or `time`, an ik or transform mix, a path `position`, `spacing` or `mix`, any physics value. That array is the update order for every kind, and each constraint reads its own applied pose when its turn comes — `Slider.update` takes `mix` as the alpha it applies with and `time` as the time it applies at, `PhysicsConstraint.update` returns on `mix` 0 before reading the rest — so the key lands after the only read of it and `Posed.resetConstrained` discards it before the next frame: what the driven constraint drives is dead at every position of the driving dial, although its pose still holds the number ([#658](https://github.com/firejune/rigc/issues/658), [#665](https://github.com/firejune/rigc/issues/665)). The detail names the slider, the driven constraint with its kind, both array indices, the property, the runtime class whose `update` reads it, and the animation the key sits in. Fix by moving the driver earlier, or by keying that property from a slider that already is. **The two indices equal is the same failure**: a slider cannot key its own `mix` or `time`, and one muted at setup that keys its own `mix` up never applies anything at all — `A37` is silent there, because it asks whether *an* animation keys the mix and not which one. **Two shapes it deliberately leaves out**, both measured: a `physics` `reset` key, which fires on a crossed frame time and so never fires from a slider at all, in either order — the reorder would repair nothing; and a physics timeline naming no constraint, which is every physics constraint declaring that property global and IS refused for the ones already run. Disjoint from `A40` by construction: `A40` asks who writes a shared property last and excludes every slider whose `mix` is keyed, this asks whether anything reads what was written. **SKIP** when the skeleton declares no slider, and when no slider's animation keys a constraint property — that SKIP names any `reset` keys it found — a pass means a driver and a driven were compared |
4796
+ | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (§3.3) or an `rgba2` timeline (§4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** — it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** — `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. ⚠️ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` — there is then no two-colour tint to read back |
4571
4797
 
4572
4798
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
4573
4799
  clauses are gated by profile.
@@ -4582,10 +4808,12 @@ own behaviour is worse: an unknown attachment `type` returns `null` and the
4582
4808
  attachment disappears, and a constraint entry with an unrecognised `type` matches no
4583
4809
  case and vanishes.
4584
4810
 
4585
- A deferral carries its reason, and the reason is the same one in every deferred row:
4586
- **neither of those types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
4587
- parts 3-1 and 4-2), so neither is on the ladder's critical path. The message
4588
- says so, because a deferral without its reason is a wall rather than a work item.
4811
+ A deferral carries its reason, and there is one deferred attachment type left:
4812
+ **`point` appears nowhere in the benchmark corpus** (SPEC_COVERAGE parts 3-1 and
4813
+ 4-2), so it is not on the ladder's critical path. The message says so, because a
4814
+ deferral without its reason is a wall rather than a work item. `linkedmesh` stood
4815
+ beside it until [#691](https://github.com/firejune/rigc/issues/691) and is now
4816
+ emitted — §3.4 has its fields.
4589
4817
 
4590
4818
  ⚠️ **A spelling the format does not have is a different refusal and says so.**
4591
4819
  `sequence` is not an attachment type, and a `"type"` that is `null` is not an absent
@@ -4596,13 +4824,12 @@ are `CompileError`s, and they name what the format actually defines
4596
4824
 
4597
4825
  | You wrote | You get |
4598
4826
  | --- | --- |
4599
- | attachment `type` of `point` or `linkedmesh` | `this attachment is a "linkedmesh" — a mesh that takes its geometry from another mesh instead of stating any — a region/mesh head, then "source" …. rigc does not emit it yet, deliberately: it emits region, mesh, boundingbox, clipping, path, and neither a point nor a linked mesh appears anywhere in the benchmark corpus …` — the message names the **construct**, not just its type string, and part 1-6 is where the sentence comes from |
4600
- | a mesh carrying `source` (`type: "mesh"` **or** `type: "linkedmesh"`) | the same refusal, prefixed `(a mesh carrying "source" is one)`. The two spellings share one parser branch and `source` is what decides between them (SPEC_COVERAGE part 1-6), so `source` on a mesh is a linked mesh whatever `type` says. It used to be refused as *2 keys this compiler does not read: "source", "skin" … fix the spelling or remove it*, whose remedy destroys the construct ([#577](https://github.com/firejune/rigc/issues/577)) |
4827
+ | attachment `type` of `point` | `this attachment is a "point" — a position and an angle with no geometry at all — "x", "y", "rotation" and "color" …. rigc does not emit it yet, deliberately: it emits region, mesh, linkedmesh, boundingbox, clipping, path, and a point appears nowhere in the benchmark corpus …` — the message names the **construct**, not just its type string, and part 1-6 is where the sentence comes from |
4828
+ | a mesh carrying `source` (`type: "mesh"` **or** `type: "linkedmesh"`) | **not a refusal any more** — both spellings compile to a linked mesh (§3.4, [#691](https://github.com/firejune/rigc/issues/691)). They share one parser branch and `source` is what decides between them (SPEC_COVERAGE part 1-6), so `source` on a mesh is a linked mesh whatever `type` says. It was once refused as *2 keys this compiler does not read: "source", "skin" … fix the spelling or remove it*, whose remedy destroys the construct ([#577](https://github.com/firejune/rigc/issues/577)) |
4601
4829
  | attachment `type` of anything else — `sequence`, a typo | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (region, mesh, linkedmesh, boundingbox, path, point, clipping). … the attachment is dropped from the skeleton without a word` — a **`CompileError`**, not a deferral: rigc is not going to implement a name the format does not have. (`sequence` is a key on a region or a mesh, not a type of its own.) |
4602
4830
  | `"type": null` | `"type" is null, which is not a name. … PRESENT-and-null is not absent: getValue(map, "type", "region") takes the default only when the key is missing, so this map matches no case, readAttachment returns null, and the attachment is dropped from the skeleton without a word. Remove the key, or name a type.` Leaving the key **out** is legal and reads as `region`; writing it as `null` is not the same thing ([#577](https://github.com/firejune/rigc/issues/577)) |
4603
4831
  | constraint `type` of anything else | `constraint type "X" is not one Spine 4.3 knows. The five are: ik, transform, path, physics, slider.` — all five are emitted, so this is a typo, and a typo is what the parser drops in silence |
4604
4832
  | a path attachment's `lengths` | `"lengths" is not authored — rigc measures the setup arc length of each curve off the geometry` (§3.4). Not a deferral: a second copy of a number the vertices already fix |
4605
- | a `deform` timeline on a path attachment | `a path attachment does have a vertex array, and rigc does not key it yet` — the format allows it and an animated track is a real idiom, but a deformed path invalidates the `lengths` a `constantSpeed: false` traversal reads. Move the curve by posing the bones its vertices are bound to |
4606
4833
  | any key neither format has, anywhere in either file | `<object> has a key this compiler does not read: "x" (did you mean "y"?) … Known here: …` (§5.1). Not a deferral either: a key nothing reads is a value you wrote and the emitted skeleton does not contain |
4607
4834
 
4608
4835
  Two more limits that are not errors but will shape what you can attempt:
@@ -5937,6 +6164,13 @@ There are two matchers and the `how` column says which one answered:
5937
6164
  confidence: how much better the winning position was than the best rival inside
5938
6165
  the search window. This is what gives a shot like a chain of touching links any
5939
6166
  drift at all — under connected components alone, every frame of it is ambiguous.
6167
+ ⭐ **Only the pixels of it your own composite lets show are correlated**: a
6168
+ template pixel you draw something over cannot match the reference wherever the
6169
+ slot really is, so it adds the same residual at every offset — and, because
6170
+ sliding the template moves those samples onto other pixels, their gradient
6171
+ decides the winner wherever the visible basin is shallow. That is not a
6172
+ hypothetical: it is what made four of the seven examples in this repository
6173
+ report 0.8–2.2 px against frames rendered from themselves (issue #698).
5940
6174
 
5941
6175
  ⚠️ **Both matchers are capped, and a blank is a real answer.** A part can be
5942
6176
  displaced by about its own size and still be that part in the picture; past that,
@@ -5946,6 +6180,23 @@ the 47 px course as its drift. The bar rises with the distance being claimed: a
5946
6180
  peak sitting where you already drew the slot only has to confirm it, a peak
5947
6181
  claiming the part moved most of a radius has to be distinctive to be believed.
5948
6182
 
6183
+ ⚠️ **And a slot you cover completely has no drift to report at all.** If every
6184
+ pixel a slot draws is painted over by something later in your own draw order,
6185
+ none of its ink reaches the picture, so there is nothing to correlate — `check`
6186
+ says so by name and counts it out, rather than correlating hidden pixels against
6187
+ whatever is on top of them:
6188
+
6189
+ ```
6190
+ its drift is not measurable — every one of the 2116 px this 48x54 px slot draws is
6191
+ covered by something the candidate draws over it, so none of its own ink reaches the
6192
+ picture to be correlated against
6193
+ ```
6194
+
6195
+ That is a fact about the shot and not an error: a part behind another part is
6196
+ still where the rig put it, and the answer to *where did it land* is that this
6197
+ run cannot say. Read it beside the `slots` column, which counts it as
6198
+ unattributed.
6199
+
5949
6200
  The `slots` column is how many of the slots you drew got an answer at all, and the
5950
6201
  summary line carries the same denominator. `N reference component(s) no slot
5951
6202
  reaches` means the reference frame contains something none of your slots overlaps:
@@ -5969,25 +6220,67 @@ bun cli.ts check --candidate <build> --frames <frames>
5969
6220
  frames 11 on disk, candidate samples 11, 11 compared
5970
6221
  MAE mean 0.00 worst 0.00 (exact: none of the 11 compared frame(s) differs from the reference) (0..255 over the union alpha; over the whole frame, mean 0.00)
5971
6222
  ⤷ over the REFERENCE's own drawn pixels, mean 0.00 — the union figure compares two builds of the same rig; this one is the one to optimise against, because the union is yours to grow.
5972
- slot drift worst 0.4 px "arm_b" at f0007
6223
+ slot drift worst 0.5 px "plate" at f0004
6224
+ ⤷ bounded by 0.71 px — the correlation put this slot where the candidate drew it, so the whole figure is the sub-pixel step, clamped to 0.5 px on each axis
5973
6225
  per-frame all 10 adjacent pair(s) change by as much as the reference's own frames do
5974
6226
  ```
5975
6227
 
5976
6228
  ⚠️ **The MAE floor is zero and the slot-drift floor is not**, and the second half of
5977
6229
  that is the instrument's own arithmetic rather than anything about your rig. Both
5978
6230
  sides are the same pixels, so every frame's MAE is exactly 0 — which is why the line
5979
- says `(exact)` instead of naming a frame. The drift is a **correlation**, and its
5980
- last step fits a parabola through three whole-pixel residuals and takes its vertex,
5981
- clamped to half a pixel on each axis; so an identity run can report up to
5982
- `hypot(0.5, 0.5) = 0.71 px` and no more. ⭐ **It is not zero because the template is
5983
- your slot drawn *alone* and the reference is the composite**: wherever a neighbour
5984
- covers part of the slot, the residual surface around the true minimum is asymmetric
5985
- and the parabola's vertex sits a fraction of a pixel off it. That fraction is the
5986
- floor, it is per slot, and it is bounded — **a drift above 0.71 px on an identity run
5987
- is a defect in `check`, not a property of it.** This example read 3.7 px until issue
5988
- #678: the coarse sweep started at `−radius` and stepped by its stride, so the
5989
- identity offset was on the lattice only when the stride divided the radius, and the
5990
- `±1` refinement around a winner two pixels out could not reach back to it.
6231
+ says `(exact)` instead of naming a frame. The drift is a **correlation**: it finds
6232
+ the whole-pixel offset that matches best and then fits a parabola through three
6233
+ whole-pixel residuals for the fraction, clamped to half a pixel on each axis.
6234
+
6235
+ 🔑 **So the bound has two halves, and the run states both of them for you.** The
6236
+ `⤷ bounded by` line is not a constant off this page: it is `hypot(|dx| + 0.5,
6237
+ |dy| + 0.5)` for that match's own whole-pixel winner `(dx, dy)`. A winner at the
6238
+ offset you drew — which is the answer on every frame of a correct rig — bounds the
6239
+ whole figure at `hypot(0.5, 0.5) = 0.71 px`, and the line says the figure is the
6240
+ sub-pixel step and nothing else. A winner one pixel out bounds it at 1.58 px and
6241
+ says the correlation moved the part. ⇒ **Read a drift against the line under it,
6242
+ never against a number from a page about another rig.** A component match prints
6243
+ the other sentence — two centroids have no such bound, only the search radius.
6244
+
6245
+ 🚨 **The clamp bounds the figure only while the whole-pixel winner is the
6246
+ identity, and for a while nothing checked that second half.** Issue #698: four of
6247
+ the seven examples in this repository read **0.81, 1.11, 2.13 and 2.21 px** against
6248
+ frames rendered from themselves, because the template carried the pixels the
6249
+ candidate draws *over itself*. Those match nothing wherever the slot really is, so
6250
+ they add a residual at every offset — and sliding the template moves them onto
6251
+ other pixels, so their gradient walks the winner off the origin wherever the
6252
+ visible basin is shallow. The exhaustive whole-pixel field for `flex`'s backdrop
6253
+ had its minimum at `(2, 0)` scoring 7.75 against the identity offset's 8.76.
6254
+ Correlating only what shows put all seven back on `(0, 0)`, and `C25`–`C30` of
6255
+ the repository's own selftest gate that over every example it ships — so the next
6256
+ one is gated by arriving.
6257
+
6258
+ 🔸 The same page said `3.7 px` before issue #678, for an unrelated reason worth
6259
+ keeping: the coarse sweep started at `−radius` and stepped by its stride, so the
6260
+ identity offset was on the lattice only when the stride divided the radius, and
6261
+ the `±1` refinement around a winner two pixels out could not reach back to it.
6262
+
6263
+ **A second example, and the one the repair was measured on.** `gallery/flex` draws
6264
+ a banner and a leaf over a full-stage backdrop, so almost every slot of it reaches
6265
+ the template matcher:
6266
+
6267
+ ```bash
6268
+ bun cli.ts build --rig gallery/flex/rig.json --motion gallery/flex/motion.json --out <build>
6269
+ bun cli.ts render --candidate <build> --animation wave --fps 12 --out <frames>
6270
+ bun cli.ts check --candidate <build> --frames <frames>
6271
+ ```
6272
+
6273
+ ```
6274
+ ── wave — candidate animation "wave", 12 fps ──
6275
+ frames 30 on disk, candidate samples 30, 30 compared
6276
+ MAE mean 0.00 worst 0.00 (exact: none of the 30 compared frame(s) differs from the reference) (0..255 over the union alpha; over the whole frame, mean 0.00)
6277
+ ⤷ over the REFERENCE's own drawn pixels, mean 0.00 — the union figure compares two builds of the same rig; this one is the one to optimise against, because the union is yours to grow.
6278
+ slot drift worst 0.2 px "plate" at f0009
6279
+ ⤷ bounded by 0.71 px — the correlation put this slot where the candidate drew it, so the whole figure is the sub-pixel step, clamped to 0.5 px on each axis
6280
+ per-frame all 29 adjacent pair(s) change by as much as the reference's own frames do
6281
+ ```
6282
+
6283
+ That figure was **2.21 px** on the same command before #698, on the same slot.
5991
6284
 
5992
6285
  ⭐ **And the same frames plus two deliberately wrong builds are what say which column
5993
6286
  answers which question.** Each differs from the build above in exactly one way —
@@ -6029,6 +6322,15 @@ samples, so the column is correctly silent on it. ⇒ Read the pair together: a
6029
6322
  MAE with `per-frame` silent is a pose in the wrong place or at the wrong moment; a
6030
6323
  loud MAE with `per-frame` firing is a **speed**, which is a curve.
6031
6324
 
6325
+ 🔸 Neither block above reprints its own `⤷ bounded by` line, for the same reason
6326
+ both carry the declaration: the spec edits are not in the tree, so those two lines
6327
+ would be hand-written rather than taken. What the gate measures on them is stated
6328
+ instead — the worst match of each sits at whole-pixel `(0, −31)` and `(0, −32)`,
6329
+ which bounds them at **31.50 px** and **32.50 px**. That is the contrast worth
6330
+ carrying away: a correct rig's drift is bounded by the clamp because its winner is
6331
+ the identity, and these two are bounded by nothing of the kind because a part
6332
+ really moved.
6333
+
6032
6334
  **The `chains` block is the same two measures on the unit you actually repair.**
6033
6335
 
6034
6336
  ```