spine-rigc 0.23.0 → 0.25.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
@@ -362,16 +362,18 @@ Everything above starts from two spec files you wrote. `rigc ingest` starts from
362
362
  an existing rig is a starting point instead of 250 KB of arrays to retype.
363
363
 
364
364
  ```bash
365
- bun cli.ts ingest hero.json --out specs/ --stage 0,0,1024,768
365
+ bun cli.ts ingest hero.json --out specs/ --stage 0,0,1024,768 --images parts/
366
366
  # .. out /abs/path/specs
367
367
  # .. art loose
368
+ # .. images ../parts/ (the rig spec's own, from /abs/path/specs)
368
369
  # JUDGE DURATION: animation "idle" — skeleton JSON carries no duration; the largest key time (2.667) is used …
369
370
  # LOSS PATH_LENGTHS: skin "default" slot "track" attachment "track" — the source states `lengths`; rigc RE-MEASURES it …
370
371
  # rigc: wrote /abs/path/specs/rig.json
371
372
  # rigc: wrote /abs/path/specs/motion.json
372
373
  # rigc: wrote /abs/path/specs/findings.json
374
+ # rigc: build it with rigc build --rig /abs/path/specs/rig.json --motion /abs/path/specs/motion.json --out <dir>
373
375
 
374
- bun cli.ts build --rig specs/rig.json --motion specs/motion.json --images parts/ --out spine
376
+ bun cli.ts build --rig specs/rig.json --motion specs/motion.json --out spine
375
377
  bun cli.ts diff spine/skeleton.json hero.json # 1.000 on every measure
376
378
  ```
377
379
 
@@ -388,22 +390,43 @@ come out in is in no field of the skeleton and a decompiled spec cannot know it.
388
390
  That is a gate rather than a claim: `bun run selftest` round-trips every rig this
389
391
  repository builds on every run.
390
392
 
391
- **Two flags, for the two things a skeleton does not encode.**
393
+ **The flags are for what a skeleton does not encode**, and nothing else is a flag.
392
394
 
393
395
  | flag | what it decides |
394
396
  | --- | --- |
395
- | `--art loose` (default) | name an `image` per attachment — `<path or placeholder>.png` — so the rebuild is `build --images <dir>` and rigc measures the PNGs |
397
+ | `--art loose` (default) | name an `image` per attachment — `<path or placeholder>.png` — so the rebuild resolves loose PNGs and rigc measures them |
396
398
  | `--art none` | state `width`/`height` only, so the rebuild is `build --atlas-in <pack.atlas>` and every part resolves out of the pack |
397
- | `--stage x,y,w,h` | the setup bounding box. **Required for an editor export**, which carries none |
399
+ | `--images <dir>` | **write** the rig spec's own `images` directory, spelled relative to `--out`, so the rebuild is a plain `build --rig … --motion … --out …`. Without it the field is left out and every `image` resolves against `--out` itself, which holds the specs and no art — so every rebuild has to repeat `build --images <dir>`. Refused together with `--art none`, which writes no `image` for it to be the base of |
400
+ | `--stage x,y,w,h` | the setup bounding box, **for a skeleton that declares none**. An editor export *may* be one; every export under `examples/` carries a box and `ingest` reads it straight through |
398
401
  | `--name <n>` | the rig spec's `name`, which the motion spec's `archetype` must equal (default: the file's basename) |
399
402
 
400
- 🚨 **The stage is the one value `ingest` will not guess.** rigc always emits
401
- `skeleton.width`/`height` and an editor export never does, so a foreign file needs
402
- `--stage`; without it the missing box is a **blocker**, named. It is not derivable —
403
- posing the rig gives the *animated* extent, which is a different number from the
404
- editor's setup box — and it is the value that costs least to get wrong, because no
405
- measure `diff` reports reads the skeleton header at all. Supply it from the project
406
- the file came from, or from the editor's own canvas.
403
+ ⚠️ **`ingest --images` and `build --images` point opposite ways.** `build --images`
404
+ *overrides* the rig spec's own directory for one invocation; `ingest --images`
405
+ *writes* it, once, so no invocation needs the override. They share a name because
406
+ they name the same field — and `ingest` spells the value with the same function
407
+ `build` spells `skeleton.images` with, so a spec and the skeleton it came from say
408
+ where the parts are in one convention.
409
+
410
+ 🚨 **The stage is one of the two values `ingest` will not guess.** A skeleton JSON
411
+ *need not* carry `skeleton.width`/`height`, and when it does not rigc cannot derive
412
+ one — posing the rig gives the *animated* extent, which is a different number from the
413
+ editor's setup box. So a file that declares none is a **blocker**, named, unless
414
+ `--stage x,y,w,h` supplies it; supply it from the project the file came from, or from
415
+ the editor's own canvas.
416
+
417
+ ⚠️ **This said an editor export "never" carries one until
418
+ [#594](https://github.com/firejune/rigc/issues/594) measured the corpus.** All twelve
419
+ exports under `examples/` declare `x`, `y`, `width` and `height`, `ingest` takes the
420
+ early return on every one of them, and not one needs the flag. What an editor export
421
+ *may* do is carry none: a rigc build that declares no stage
422
+ ([#578](https://github.com/firejune/rigc/issues/578)) came back from a Spine 4.3.26
423
+ round trip with a header of `hash`, `spine`, `images`, `audio` and **no box at all** —
424
+ the editor preserves the absence rather than inventing a stage
425
+ ([#616](https://github.com/firejune/rigc/issues/616)). So `--stage` is for a file that
426
+ really has none, and this repository's corpus holds no example of one. It is still the
427
+ value that costs least to get wrong: `diff` reports the box as `stage_present` and
428
+ `stage_box` ([#578](https://github.com/firejune/rigc/issues/578)) and both are
429
+ `(reported)`, so nothing on the ladder consults them.
407
430
 
408
431
  ⚠️ **The duration is a convention, and it is recorded as one.** Skeleton JSON has no
409
432
  duration field. The largest key time is the only derivable answer and it is what a
@@ -812,7 +835,17 @@ Three readings stay apart, and the middle one is the point of the other two:
812
835
  reports **SKIP** on one, because there is no full frame for a mesh to span. And
813
836
  `rigc diff` reports it — `skeleton.stage_present` and `skeleton.stage_box`, in the
814
837
  header block at the top of the report — so a stage somebody invented now reads
815
- below 1.000 against a source that has none. Both are reported and gate nothing, for
838
+ below 1.000 against a source that has none.
839
+
840
+ ⭐ **A stage at `0,0` is not a stage-less one, and an editor export spells it by
841
+ saying nothing.** The editor omits a header field that is at its default, so a
842
+ skeleton whose box sits at the origin exports as a `width` and a `height` with no
843
+ `x`/`y`; `stage_box` reads that omission as the `0` it means, and its line says so
844
+ — *the extent as stated, an omitted origin as the 0 it means*. So `4/4` on a build
845
+ of yours against an export of that same build is the right answer rather than a
846
+ tolerance, and an origin that really did move still reads below 1.000. The extent
847
+ is the half that is read exactly as stated: omit a `width` and you have declared no
848
+ stage, which `stage_present` is the measure of. Both are reported and gate nothing, for
816
849
  the reason every reported measure is: no reading of the rendered frames could have
817
850
  decided a setup-pose bounding box. The measure inventory that says so lives in
818
851
  [BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md), which
@@ -892,6 +925,38 @@ A skin can also say which bones and constraints it **switches on**, and that nee
892
925
  one more level, so a skin entry has a second spelling — see §3.4.1. The short one
893
926
  above is unchanged and is what almost every rig wants.
894
927
 
928
+ 🔸 **A skin may fill no slot with anything that needs art, and that build is
929
+ green.** The atlas is built out of what the skins reference, so a rig whose skins
930
+ name no `image` — an empty `default`, or one carrying only a `boundingbox`, a
931
+ `clipping` polygon or a `path`, none of which has a page — compiles to an atlas
932
+ with **no pages**, and rigc writes `skeleton.atlas` as an **empty file** (zero
933
+ bytes). Four rules then report SKIP by name rather than a pass, because a page is
934
+ their whole subject: `A07_ATLAS_TEXT_SHAPE`, `A06_ATLAS_PAGE_SIZE_MATCHES_PNG`,
935
+ `A17_ATLAS_PAGE_FILES_EXIST` and — under `spine-html` —
936
+ `A19_OVERLAY_PNGS_HAVE_ALPHA` and `A27_REGION_NAME_MATCHES_PAGE_FILENAME`. Until
937
+ [#608](https://github.com/firejune/rigc/issues/608) the same compile wrote one
938
+ newline instead and `A07` refused it with two findings, so a hit-box skeleton — a
939
+ correct rig, whose geometry `A33_VERTEX_ATTACHMENT_GEOMETRY` passes — could not be
940
+ built at all.
941
+
942
+ ⚠️ **This is not the case where an attachment WANTS a region.** A region or mesh
943
+ attachment that states `width`/`height` and names no `image` still resolves a
944
+ region by `path`, and with no atlas to supply it that is
945
+ `A08_REGION_NAMES_MATCH_ATTACHMENTS` naming the skin, the slot, the placeholder
946
+ and the path — which is what a spec written by `ingest --art none` does when it is
947
+ built without `--atlas-in` (§0.2, §0.3). The empty atlas is legal; an attachment
948
+ pointing into it is not.
949
+
950
+ ⚠️ **`rigc render` still refuses such a build**, by name and before it draws
951
+ anything: `… posed no drawable attachment in any animation or in its setup pose —
952
+ there is nothing to draw`. That is the honest division — the rig is valid Spine
953
+ data, and there is no picture of it. `rigc check` says the same thing one step
954
+ on, since it has no frames to compare; `tools/editor_roundtrip.ts` quotes both
955
+ renderers and reports its step 5 as a **SKIP** naming that, and the round trip
956
+ comes back green on the strength of `validate` and `diff`
957
+ ([#621](https://github.com/firejune/rigc/issues/621)). A skin only **one** side
958
+ can draw is the other case entirely, and stays red.
959
+
895
960
  **Region attachment** ([Spine: region attachments](http://esotericsoftware.com/spine-regions)),
896
961
  the default `type`:
897
962
 
@@ -921,7 +986,11 @@ with `image: hair_short.png` under a placeholder called `hair` resolves the regi
921
986
  `hair`, which no atlas has. Until
922
987
  [#577](https://github.com/firejune/rigc/issues/577) a region derived it and an
923
988
  authored mesh did not, so that rig **built** and then failed
924
- `A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: hair`.
989
+ `A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: hair` — which is the
990
+ loader's sentence and all the report had. Since
991
+ [#589](https://github.com/firejune/rigc/issues/589) the same miss is named by
992
+ `A08_REGION_NAMES_MATCH_ATTACHMENTS`, with the skin, the slot, the placeholder
993
+ and the attachment's own name beside the path.
925
994
 
926
995
  Geometry comes in one of two fields:
927
996
 
@@ -1673,8 +1742,11 @@ What to know about it, and nothing to author:
1673
1742
  ([#574](https://github.com/firejune/rigc/issues/574)).
1674
1743
  - **`path` is restated, and it has to be.** `path` defaults to the attachment's
1675
1744
  **name**, not to its placeholder, so an entry given a name and no path would
1676
- resolve its texture at `zulu/patch` and find no such region. `A00_ROUNDTRIP_PARSE`
1677
- says so in the parser's own words if it is ever dropped.
1745
+ resolve its texture at `zulu/patch` and find no such region.
1746
+ `A08_REGION_NAMES_MATCH_ATTACHMENTS` says so if it is ever dropped, naming the
1747
+ skin, the slot, the placeholder and the path
1748
+ ([#589](https://github.com/firejune/rigc/issues/589)); `A00_ROUNDTRIP_PARSE`
1749
+ reported it in the parser's own words until then, and now defers to A08.
1678
1750
  - **Only contested placeholders are touched.** One skin filling a placeholder, or
1679
1751
  two skins filling a slot under *different* placeholders, emit exactly what they
1680
1752
  always did — every rig in this repository is byte-identical across the change.
@@ -2317,7 +2389,8 @@ a deform). Folding them in would make `v` mean four different things depending o
2317
2389
  | `bone` | `translatex`, `translatey`, `scalex`, `scaley`, `shearx`, `sheary`, `rotate` | `[value]` |
2318
2390
  | `slot` | `rgba` | `[r, g, b, a]` in 0..1 |
2319
2391
  | `slot` | `attachment` | the attachment name, or `null` for "show nothing" |
2320
- | `physics` | `mix` | `[mix]`, 0..1 — the constraint's authority |
2392
+ | `physics` | `inertia`, `strength`, `damping`, `mass`, `wind`, `gravity` | `[value]` — the constraint's own tuning, keyed over time |
2393
+ | `physics` | `mix` | `[mix]`, **0 or more** — the constraint's authority |
2321
2394
  | `physics` | `reset` | `null` — the key *is* the event |
2322
2395
  | `path` | `position`, `spacing` | `[value]` — see §4.12 |
2323
2396
  | `path` | `mix` | `[mixRotate, mixX, mixY]` — one timeline, three channels |
@@ -2327,6 +2400,42 @@ a deform). Folding them in would make `v` mean four different things depending o
2327
2400
  Translate values are **relative to the bone's setup position**; scale values are
2328
2401
  multipliers where `1` is setup; rotation is in degrees.
2329
2402
 
2403
+ **A physics constraint's six tuning timelines override §4.6's table for the
2404
+ length of an animation.** `{ "physics": "hair", "property": "wind", "keys": […] }`
2405
+ is a wind that rises and falls; `damping` is how fast the jiggle settles,
2406
+ `strength` how hard it is pulled back, `inertia` how much of the bone's motion it
2407
+ carries, `mass` the weight it swings with, `gravity` the constant pull. They are
2408
+ the same field names §4.6 uses at rest, and one key states the whole value — not
2409
+ a delta from the constraint's own setting.
2410
+
2411
+ - ⚠️ **The per-key default is 0 on all six, and 1 on `mix` — not the
2412
+ constraint's default.** The parser opens every physics key at 0 and only
2413
+ `mix` reassigns it, so a key that omits its number reads 0 for `damping`, not
2414
+ the 0.85 §4.6 gives a constraint that states none. rigc never omits a channel,
2415
+ so this bites only when you compare an emitted file against an editor export,
2416
+ or when you read an export by hand: a `damping` key with no `value` in
2417
+ somebody else's file means **0**.
2418
+ - ⚠️ `mass` is the one whose keyed number is not what the runtime stores. The
2419
+ key states a mass and the pose holds `1 / mass`, so a `mass` key of `0` is an
2420
+ infinite inverse mass — the constraint stops moving.
2421
+ - **Four of the seven are bounded, and a key outside its bound is a compile
2422
+ error** ([#610](https://github.com/firejune/rigc/issues/610)). `mass` and
2423
+ `strength` must be `> 0`, `damping` must be strictly inside `(0, 1)`, and `mix`
2424
+ must be `0` or more. `A23_PHYSICS_CONSTRAINT_EFFECTIVE` applies the same four to
2425
+ a file rigc did not write, naming the animation, the constraint, the key time
2426
+ and the value — so the compiler is where a spec you wrote is refused, and the
2427
+ assertion is where an import is.
2428
+ - 🚫 **`inertia`, `wind` and `gravity` are bounded nowhere, and neither is the top
2429
+ of `mix`.** The runtime documents no range for the first three, and
2430
+ `PhysicsConstraintPose` documents `mix` as "a percentage (0+)" — so a negative
2431
+ wind is the other direction and a `mix` of `1.5` is an over-mix. Both are real
2432
+ and both compile.
2433
+ - ⚠️ **A `mix` key of exactly `0` is legal where a setup `mix` of `0` is not**, and
2434
+ the difference is not an inconsistency. `PhysicsConstraint.update` opens with
2435
+ `if (mix === 0) return;`, so muting a constraint for a stretch of an animation is
2436
+ what a mix timeline is for; a constraint muted *at rest* does nothing at all
2437
+ unless some animation rescues it, which is the silence `A23` was built for.
2438
+
2330
2439
  On a track that names a `group`, one key's `v` may instead be a **map keyed by
2331
2440
  member name**, whose entries are each exactly the `v` above — or a `derive`
2332
2441
  model the compiler evaluates per member. §4.5.1.
@@ -2612,8 +2721,11 @@ key times. One lag, one place.
2612
2721
 
2613
2722
  `name → { bone, x?, y?, rotate?, scaleX?, shearX?, inertia?, strength?, damping?,
2614
2723
  mass?, wind?, gravity?, mix?, fps?, limit? }`. These are emitted into the 4.3
2615
- `constraints` array. `mass: 0` becomes an infinite inverse mass and `damping ≥ 1`
2616
- never settles — both are `A23`. Every field but `bone` and `note` must be a finite
2724
+ `constraints` array. Seven of them — the six tuning numbers and `mix` — can also
2725
+ be **keyed over time** as `tracks` entries naming this constraint (§4.4); this
2726
+ table is the value at rest, and a timeline overrides it while it plays. `mass: 0` becomes an infinite inverse mass and `damping ≥ 1`
2727
+ never settles — both are `A23`, here and on every timeline key that states them
2728
+ ([#610](https://github.com/firejune/rigc/issues/610)). Every field but `bone` and `note` must be a finite
2617
2729
  number: a non-number is rounded to `NaN` and emitted as `null`, which the runtime
2618
2730
  reads as **zero**, so `"mass": "heavy"` used to ship a constraint that never
2619
2731
  settles with no word from anybody (#307).
@@ -3307,6 +3419,13 @@ keys were passed over and how many reversed triangles nothing gated:
3307
3419
  8 reversed triangle(s) nothing gates <- A39 counts them as deformKeysNotDrawn
3308
3420
  ```
3309
3421
 
3422
+ ⭐ **On the "shows another attachment" half, that sentence also names the skin the
3423
+ pose was taken in** — `the slot shows attachment "away" at this time, with skin
3424
+ "suit" worn, not this mesh …` — because what a slot shows is resolved through the
3425
+ worn skin first and `defaultSkin` second, so the reading is only meaningful beside
3426
+ the dress it was taken in (§4.11.5). The alpha half carries no such clause: an
3427
+ alpha is read off the pose and no skin is in it.
3428
+
3310
3429
  🚫 **What it does not print, and why — `coverage`.** A deform **cannot move
3311
3430
  coverage.** That figure is rasterised from the attachment's **uvs** against the
3312
3431
  part's alpha, and a deform moves positions and never uvs, so it is identical at
@@ -3582,6 +3701,70 @@ records that, so a slider-applied animation is measured in its slider frames onl
3582
3701
 
3583
3702
  ---
3584
3703
 
3704
+ ### 4.11.5 Which skin a deform key is measured in — the one it is keyed on
3705
+
3706
+ §4.11 opens on the triple: a deform timeline is the only one keyed on
3707
+ **skin / slot / attachment**. So the skin is not context around the key, it is a
3708
+ third of the key's own address — and every pose `A39` and the `DEFORM` block take
3709
+ is now taken with **that skin worn**
3710
+ ([#583](https://github.com/firejune/rigc/issues/583)). You do not ask for it and
3711
+ there is no flag: the skin comes out of the timeline.
3712
+
3713
+ 🚨 **It used to wear nothing at all**, which is `spine-core`'s own initial state
3714
+ and the same one `check` reports as `no skin set (the default skin alone)`
3715
+ (§9) — every slot resolved through `SkeletonData.defaultSkin` and nothing else.
3716
+ Move a deformed mesh into a named skin, which the format not only allows but keys
3717
+ the timeline on, and the slot showed **no attachment**. This is what
3718
+ `gallery/squash`'s ball printed with its one mesh moved into a skin `suit`, on
3719
+ every one of its five keys, before #583:
3720
+
3721
+ ```
3722
+ DEFORM bounce suit/ball/ball key 1 t=0.340000 transform affine scale=[0.88, 1.16]
3723
+ skipped A39 reads no winding off this key: the slot shows no attachment at all at this
3724
+ time, not this mesh, so the runtime applies no deform to it here and draws
3725
+ none of it — a triangle that draws no pixels cannot draw them backwards
3726
+ ```
3727
+
3728
+ — `A39` went **PASS → SKIP** on a rig whose only edit was which skin one mesh sat
3729
+ in, in a sentence that reads as a verdict on that rig. Worn, the same build
3730
+ reports every figure the default-skin one does, to the last digit, and `A39` gates
3731
+ it: the two `DEFORM` blocks differ in the skin of the triple and in nothing else.
3732
+
3733
+ **Two things follow, and one of them is not about art:**
3734
+
3735
+ - ⚠️ **A "nothing is drawn" sentence now names the skin it was measured in** —
3736
+ `the slot shows attachment "away" at this time, with skin "suit" worn, not this
3737
+ mesh …`. It is on the *shows-something-else* branch only, because that is the
3738
+ branch a skin decides; an alpha is read off the pose and has no skin in it. The
3739
+ clause is what separates "this slot is empty" from "this slot is empty in the
3740
+ dress this mesh lives in", and only the second is a measurement.
3741
+ - ⭐ **A `skin: true` bone or constraint (§3.4.1) is switched on too.**
3742
+ `Skeleton.setSkin` calls `updateCache`, which leaves a `skinRequired` bone
3743
+ inactive and a `skinRequired` constraint out of the update cache under any skin
3744
+ that does not list it. So a **slider** dressed into the same skin as the mesh
3745
+ it drives used to be measured with itself switched off, and failed in two ways
3746
+ depending on its `local` flag: a world-read property never moved, so the frame
3747
+ was reported as *"played on a track"* — an animation a slider is the only way
3748
+ into (§4.11.4) — while a local-read one kept its mapping but never left
3749
+ `SliderPose.time` 0, so every key came back as *"at a time no dial selects"*.
3750
+ Both now reach their own key times.
3751
+
3752
+ ⛔ **What it does not do is try every skin.** `Attachment.timelineSlots` lets one
3753
+ deform reach a second slot — a linked mesh with `inheritTimelines` — and that copy
3754
+ may live in a skin of its own; `setSkin` dresses the whole skeleton at once, so
3755
+ under the timeline's own skin that copy resolves to nothing and is reported as
3756
+ drawing nothing, which is exactly what the runtime does with that same skin on.
3757
+ Posing the key again under some other skin would be rigc choosing which dress the
3758
+ character is wearing, and which skin is worn is yours. What it owes you instead is
3759
+ the skin in the sentence, so the reading is never mistaken for a verdict.
3760
+
3761
+ 📌 **`check` is the other half of the same question and it is not automatic**:
3762
+ a rig whose art lives in named skins is rendered and checked **once per skin**,
3763
+ with `--skin` on both sides (§9). The difference is where the name comes from —
3764
+ a deform timeline carries its own skin and a frame set does not.
3765
+
3766
+ ---
3767
+
3585
3768
  ### 4.12 `path` and `slider` timelines — tracks, not their own groups
3586
3769
 
3587
3770
  Unlike `ik` and `transform`, these two are ordinary `tracks` entries: the format
@@ -3740,6 +3923,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
3740
3923
  | `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` |
3741
3924
  | `animation "A" keys unknown bone "X"` | the track's `bone` is not in the rig |
3742
3925
  | `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (§4.4) |
3926
+ | `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)) |
3743
3927
  | `a key carries both a named easing and a raw curve; pick one` | R6 |
3744
3928
  | `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
3745
3929
  | `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
@@ -3816,6 +4000,18 @@ The report prints one line per assertion:
3816
4000
  - **PASS** — it ran and held.
3817
4001
  - **SKIP** — it had *nothing to look at*, and the reason says what was missing. A
3818
4002
  skip is never folded into the pass count.
4003
+ 🚨 **"Nothing to look at" includes a subject the skeleton does not carry**
4004
+ ([#580](https://github.com/firejune/rigc/issues/580)). A rule named
4005
+ ⟨subject⟩_⟨property⟩ — `A03_REGION_WIDTH_HEIGHT_FINITE`,
4006
+ `A04_MESH_TRIANGLES_AND_ENCODING`, `A23_PHYSICS_CONSTRAINT_EFFECTIVE` — walks
4007
+ that subject and measures each member, so a skeleton with no region, no mesh or
4008
+ no physics constraint gives it nothing and it **SKIPs**, naming the subject. A
4009
+ rule named NO_⟨construct⟩ — `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS`,
4010
+ `A02_NO_BONE_TRANSFORM_KEY`, `A11_NO_CLIPPING_ATTACHMENTS`,
4011
+ `A12_NO_DARK_COLOR`, `A14_NO_FULL_FRAME_MESH` — asks how many of a thing the
4012
+ artifact carries, and **zero is the answer**, so it **PASSes**. ⇒ On a foreign
4013
+ export the SKIP list is a free inventory of what the skeleton does not contain,
4014
+ and the PASS list never contains a rule that looked at nothing.
3819
4015
  - **PROF** — the profile you chose does not carry that kind of rule. Two kinds sit
3820
4016
  outside `spine`, not one: the renderer rules **and** the archetype rules. The
3821
4017
  exclusion is checked before the assertion's body runs, so an archetype rule with
@@ -3849,46 +4045,46 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
3849
4045
 
3850
4046
  | Assertion | Profile | What tripped it, and where to fix it |
3851
4047
  | --- | --- | --- |
3852
- | `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Everything else in the report is downstream of this one — fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked ([#568](https://github.com/firejune/rigc/issues/568)) |
4048
+ | `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Almost everything else in the report is downstream of this one — fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked ([#568](https://github.com/firejune/rigc/issues/568)). ⚠️ **Two rules run before it and can be upstream of it**: `A31_DRAW_ORDER_OFFSETS_RESOLVE`, because a bad draw-order key makes the loader spin rather than return, and `A08_REGION_NAMES_MATCH_ATTACHMENTS`, because a `path` naming no region makes it throw. The round trip is still attempted either way; when the loader refuses a path A08 has already refused, this row **defers** to A08 by name instead of restating the miss in the parser's poorer words ([#589](https://github.com/firejune/rigc/issues/589)) |
3853
4049
  | `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS` | both | a 4.1/4.2-shaped `ik`/`transform`/`path`/`physics`/`slider` array. rigc emits the 4.3 `constraints` array, so this normally means hand-edited JSON |
3854
4050
  | `A02_NO_BONE_TRANSFORM_KEY` | both | a bone uses 4.2's `transform`; rename it `inherit` in the rig spec |
3855
- | `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height` |
3856
- | `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) |
3857
- | `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** |
3858
- | `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 |
3859
- | `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 |
3860
- | `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | an atlas region name carrying stray whitespace. rigc writes the atlas, so this means a hand-edited file, and `A07` names the same line with its line number. ⚠️ A `path` that resolves to **no** region never reaches here: `AtlasAttachmentLoader` throws `Region not found in atlas: <path> (attachment: <name>)` while the skeleton is still loading, so it arrives 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)) |
4051
+ | `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)) |
4052
+ | `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)) |
4053
+ | `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 |
4054
+ | `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)) |
4055
+ | `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` |
4056
+ | `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)) |
3861
4057
  | `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 |
3862
- | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale |
4058
+ | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale. **SKIP** when the skeleton carries no animation ([#580](https://github.com/firejune/rigc/issues/580)): the NaN is produced by stepping, and a static rig is never stepped — the same subject `A09` skips on |
3863
4059
  | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
3864
4060
  | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
3865
- | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7, issue #274) |
4061
+ | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7, issue #274) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh ([#580](https://github.com/firejune/rigc/issues/580)). A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
3866
4062
  | `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip. **SKIP** when the skeleton declares no stage (§3.1): there is no full frame to span, and *unmeasured* must not print the same green as *measured and clear* |
3867
4063
  | `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. **SKIP** when there is no `idle` animation, or when the one there is carries no bone timeline — a rule whose subject does not exist is unmeasured and not satisfied ([#568](https://github.com/firejune/rigc/issues/568)) |
3868
4064
  | `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
3869
- | `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out` |
4065
+ | `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)) |
3870
4066
  | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
3871
- | `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)) |
3872
- | `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 |
4067
+ | `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)) |
4068
+ | `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)) |
3873
4069
  | `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 |
3874
- | `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count |
3875
- | `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 |
3876
- | `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed |
4070
+ | `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)) |
4071
+ | `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 |
4072
+ | `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it ([#580](https://github.com/firejune/rigc/issues/580)) |
3877
4073
  | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
3878
- | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3) |
3879
- | `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename |
4074
+ | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3). **SKIP** when the rig declares no canonical slot order. ⚠️ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass ([#580](https://github.com/firejune/rigc/issues/580)): the completeness clause reads it as every declared slot lost and names them, which is the maximal case of what [#575](https://github.com/firejune/rigc/issues/575) filed |
4075
+ | `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename. **SKIP** when the atlas declares no region ([#580](https://github.com/firejune/rigc/issues/580)) |
3880
4076
  | `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width. **SKIPs** on authored geometry and on a contour mesh — neither has rows rigc paired |
3881
4077
  | `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
3882
4078
  | `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
3883
4079
  | `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted |
3884
4080
  | `A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
3885
4081
  | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index, a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
3886
- | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. **SKIP** when no animation carries one |
4082
+ | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path`, `physics` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.4, §4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. **SKIP** when no animation carries one |
3887
4083
  | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11, issue #262). **SKIP** when no animation carries a deform timeline |
3888
4084
  | `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying its `mix` (§3.5.1). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. **SKIP** when the skeleton declares no path constraint |
3889
4085
  | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` (§3.5.2). **SKIP** when the skeleton declares no slider |
3890
4086
  | `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
3891
- | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
4087
+ | `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. ⚠️ And the **skin** it poses in is the one the timeline is keyed on (§4.11.5, issue #583), since a deform's address is a `skin / slot / attachment` triple: the pose wears that skin, which also switches on any `skin: true` bone or constraint it activates, and the "nothing is drawn" sentence names the skin it was read under. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
3892
4088
  | `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 additive (a slot colour, an attachment swap, a draw order, a sequence), where `"additive": true` is not the fix and one of the two has to go. The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, and which one wins today. Three 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, and two sliders on different properties. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
3893
4089
  | `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 |
3894
4090
 
@@ -4503,6 +4699,14 @@ remember:
4503
4699
  A skin name the candidate does not declare is refused with the ones it does —
4504
4700
  `the candidate declares no skin "path"; it declares [default, patch, torn]`.
4505
4701
 
4702
+ 📌 **Deform measurement needs no such flag, because the timeline carries the
4703
+ name.** `A39` and the `DEFORM` block pose each key with the skin that key is keyed
4704
+ on — a deform timeline's address is a `skin / slot / attachment` triple — so a
4705
+ mesh in a named skin is measured in that skin without anything being passed on the
4706
+ command line (§4.11.5, issue #583). The difference is where the name comes from,
4707
+ not which command cares: `check` compares two picture sets and neither of them
4708
+ records a skin unless you say so, while a deform key knows its own.
4709
+
4506
4710
  ⭐ **A frame set may ship a contact sheet instead of every frame, and the sheet is
4507
4711
  compared too.** A long shot does not commit 311 near-duplicate PNGs: rung 2's sets
4508
4712
  ship `f0000.png` and `f0310.png` plus a `contact.png` holding all 311 sampled