spine-rigc 0.8.0 → 0.9.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
@@ -450,6 +450,10 @@ skin's slot key can cost you a slot quietly, so check `explain`'s slot table.
450
450
  uses a named skin — all twelve official example skeletons have exactly one skin,
451
451
  called `default`.)
452
452
 
453
+ A skin can also say which bones and constraints it **switches on**, and that needs
454
+ one more level, so a skin entry has a second spelling — see §3.4.1. The short one
455
+ above is unchanged and is what almost every rig wants.
456
+
453
457
  **Region attachment** ([Spine: region attachments](http://esotericsoftware.com/spine-regions)),
454
458
  the default `type`:
455
459
 
@@ -515,10 +519,93 @@ is weighted; a generated mesh binds only bones that move it) do not apply to one
515
519
  #44; before it was fixed, `A21` reported 40 failures on a correct 40-vertex editor
516
520
  mesh because an absent `meshKinds` entry read as `ring`.
517
521
 
518
- The two generators are `ring` and `ribbon` (see [`src/mesh.ts`](../src/mesh.ts));
519
- they encode a deformation model rather than a table of numbers, which is why they
520
- are code invoked by data. A generator is for a skeleton with **no** manifest; a cut
521
- that has one invokes the same builders through the manifest's `mesh` block.
522
+ The generators are `ring`, `ribbon` and `contour` (see
523
+ [`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
524
+ than a table of numbers, which is why they are code invoked by data. A generator
525
+ is for a skeleton with **no** manifest; a cut that has one invokes the same
526
+ builders through the manifest's `mesh` block.
527
+
528
+ ⭐ **The no-manifest path centres the part window on its own slot bone.** There is
529
+ no crop to flip against, so `size` (or, for a contour, the PNG's own size) is
530
+ placed with its centre on the bone the slot names — which is also exactly where a
531
+ plain region attachment with no `x`/`y` would have drawn it. Measured on a ring, a
532
+ ribbon and a moved bone by `CT05` in the selftest (issue #1).
533
+
534
+ #### `contour` — the mesh is the art's own silhouette
535
+
536
+ **When you need one:** the part's outline is the interesting thing and a
537
+ rectangle of region is the wrong shape — a leaf, a cape, a splash, anything whose
538
+ quad is mostly transparent pixels the renderer still blends. Or you want real
539
+ vertices to push with a `deform` timeline (§4.11) at the places the silhouette
540
+ actually is, instead of at four corners.
541
+
542
+ It takes **no geometry and no size**: the shape is traced off the attachment's own
543
+ `image`, so there is no number here that can disagree with the pixels.
544
+
545
+ ```json
546
+ "cape": {
547
+ "cape": {
548
+ "type": "mesh",
549
+ "image": "cape.png",
550
+ "generator": { "kind": "contour", "tolerance": 1.5, "margin": 2, "maxVertices": 48 }
551
+ }
552
+ }
553
+ ```
554
+
555
+ | Field | Meaning |
556
+ | --- | --- |
557
+ | `tolerance` | **required.** Douglas-Peucker tolerance, in part pixels. Bigger spends fewer vertices and cuts more corners |
558
+ | `margin` | how far the outline is pushed out past the traced silhouette, in pixels. Default `1` |
559
+ | `maxVertices` | refuse rather than emit more outline vertices than this. Default `64` |
560
+ | `alpha` | the alpha at or above which a pixel counts as art, `1`..`255`. Default `1` — any pixel that is not fully transparent |
561
+
562
+ The numbers above are invented, and the pair that matters is `tolerance` and
563
+ `margin`: simplification may bite **`tolerance` pixels into the art**, and the
564
+ margin is what pays that back, so **`margin >= tolerance`** is the setting that
565
+ survives the coverage check below. `alpha` is the lever for art with a long soft
566
+ feather — raise it and the outline hugs the solid core instead of the last
567
+ almost-invisible pixel.
568
+
569
+ **What it does, in order:** trace the alpha mask on the pixel-corner lattice (so
570
+ the raw outline encloses every art pixel *whole*), simplify it, push it out by
571
+ `margin`, clamp it to the part window, ear-clip it, and then **measure the result
572
+ against the mask it came from**. `build` and `explain` print what it measured:
573
+
574
+ ```
575
+ MESH cape contour 15 vertices / 13 triangles (budget 80) bones=[cape] attachments=[cape] covers 100.00% of the art, reaching 3.16px past it
576
+ ```
577
+
578
+ 🚨 **It is geometry, not a deformation model.** Every vertex is pinned to the slot
579
+ bone at weight 1, so a contour mesh at rest draws what the region drew and **no
580
+ bone can bend it**. What you gain over a region is a real outline, real triangles
581
+ and a `hull` other tools can read; what you do not gain is bone-driven motion —
582
+ that is what `ring` is for, or authored `weights` (§3.4) if you have an editor's
583
+ auto-weighting to transcribe. `A21_MESH_RIM_PINNED` therefore reads over the whole
584
+ mesh rather than a rim prefix, and `A28_RIBBON_ROWS_SHARE_WEIGHTS` **SKIPs** with
585
+ "a contour is one silhouette loop" as the reason.
586
+
587
+ **Stated limits, each a named refusal rather than a mesh that loads wrong:**
588
+
589
+ | The art | What you get |
590
+ | --- | --- |
591
+ | every pixel opaque | `every pixel of the 96x64 part reaches alpha 1, so its silhouette IS the part window and a contour mesh of it is a region attachment with extra vertices` |
592
+ | two or more islands | `the art is 2 separate islands and one outline can only enclose the largest (529 of 989 px, 53.49%)` — one outline encloses one region; give each island its own slot |
593
+ | a **hole** (a donut) | **accepted, and the hole is inside the mesh.** Ear clipping has no bridging step, so the outline is the art's *outer* boundary; those pixels draw nothing (their alpha is still 0) and the extra triangles are the whole cost. `explain` reports the coverage either way |
594
+ | a **diagonal pinch** — two parts of the art meeting at one pixel corner | `the alpha silhouette pinches to a single point at pixel corner (2,2) … one outline cannot pass through one point twice` |
595
+ | a neck narrower than `margin` | `the outline crosses itself: edge 0 meets edge 3 after a margin of 3px was pushed out of a silhouette narrower than that` |
596
+ | more outline than `maxVertices` | `the silhouette simplified to 15 vertices at tolerance 1.5, past the 4 this mesh allows` — refused, never silently decimated |
597
+ | a `margin` too small for the `tolerance` | `the mesh covers 91.81% of the art (2936 of 3198 px), under the 99.5% a contour mesh guarantees` |
598
+
599
+ That last one is the guarantee: **the emitted triangles cover at least 99.5% of
600
+ the art**, measured by rasterising them back over the mask, and a build that would
601
+ clip the art is refused rather than shipped. It is not 100% because a
602
+ simplification that could never cut a corner would not be one — the figure a
603
+ given part actually measures is in the `build`/`explain` line above, and the
604
+ selftest's fixture measures 100.000%.
605
+
606
+ Self-intersection is refused; **holes are not cut out**; and nothing here does
607
+ interior/Steiner points, so a contour mesh bends only where its outline has
608
+ vertices.
522
609
 
523
610
  **Bounding box** ([Spine: bounding boxes](http://esotericsoftware.com/spine-bounding-boxes))
524
611
  and **clipping** ([Spine: clipping](http://esotericsoftware.com/spine-clipping))
@@ -573,16 +660,122 @@ everything after this one", and is left alone.
573
660
  was supposed to hide something would not. It is valid Spine and the default
574
661
  `--profile spine` accepts it; the refusal is policy, not validity.
575
662
 
663
+ **Path attachment** ([Spine: paths](http://esotericsoftware.com/spine-paths)) — a
664
+ composite cubic Bezier, and the only attachment whose purpose is to be *followed*.
665
+
666
+ **When you need one:** a path constraint has nowhere to aim without it (§3.5). The
667
+ curve is not drawn — no runtime renders a path — and it deforms with its slot's
668
+ bone like any other vertex attachment, so an animated path is a moving track.
669
+
670
+ ```json
671
+ "track": { "track": { "type": "path", "vertexCount": 9,
672
+ "vertices": [-30, 0, 0, 0, 30, 0, 60, 0, 90, 0,
673
+ 120, 0, 150, 0, 180, 0, 210, 0] } }
674
+ ```
675
+
676
+ That is nine invented points forming a straight line: knots at x = 0, 90 and 180
677
+ with their handles evenly spaced between them, which is the simplest path there is
678
+ and the one worth reading twice.
679
+
680
+ | Field | Meaning |
681
+ | --- | --- |
682
+ | `vertexCount` | **required**, and a multiple of 3 — see below |
683
+ | `vertices` / `weights` | the same two encodings a mesh's geometry uses, with the same by-name default |
684
+ | `closed` | default false. When true the last knot joins the first |
685
+ | `constantSpeed` | default **true**: the runtime re-measures the curve every frame so travel along it is even. `false` makes it trust the emitted `lengths` instead — cheaper, and exact only while the path holds its setup shape |
686
+ | `color` | `rrggbbaa`; the colour the editor draws the curve in |
687
+
688
+ 🚨 **`vertexCount` counts knots AND handles, and the arithmetic is not the one you
689
+ would guess.** The vertices are walked in groups of three, and an **open** path
690
+ drops the first and last point — those two are the outer control handles of the end
691
+ knots, and no curve uses them. So:
692
+
693
+ - an open path of K curves has `vertexCount = 3(K + 1)`, minimum **6** (one curve);
694
+ - a closed one has `vertexCount = 3K`, minimum **3**, because the chain wraps.
695
+
696
+ A count that is not a multiple of 3 does not throw anywhere:
697
+ `Utils.newArray(vertexCount / 3, 0)` takes a fractional size happily, the groups of
698
+ six then straddle the knots, and bones slide along a curve nobody drew. rigc refuses
699
+ it, and `A33_VERTEX_ATTACHMENT_GEOMETRY` refuses it again on the artifact.
700
+
701
+ ⚠️ **`lengths` is not yours to write.** The emitted file carries one — the
702
+ cumulative arc length at the end of each curve, in world units, in the setup pose —
703
+ and rigc **measures** it off the geometry above, exactly as it measures a region's
704
+ `width`/`height` off its PNG (R5). Writing one is a `CompileError`: it is a second
705
+ copy of a number the vertices already determine, a copy that disagrees is only
706
+ visible under `constantSpeed: false`, and there it silently rescales the whole
707
+ traversal. `explain` prints the measurement, so `position: 0.25` on a curve that
708
+ reports `180 long` is a bone at 45 units along it.
709
+
710
+ #### 3.4.1 A skin that switches bones and constraints on
711
+
712
+ **When you need one:** a skin that is more than a change of art — a variant with an
713
+ extra bone (a pauldron, a tail, a hat with its own physics), or one where a
714
+ constraint should only run while that skin is applied.
715
+
716
+ A skin entry has two spellings. The short one is `slotName → placeholder →
717
+ attachment`, above. The long one puts that table under `attachments` and adds the
718
+ lists:
719
+
720
+ ```json
721
+ "skins": {
722
+ "default": {
723
+ "attachments": {
724
+ "torso": { "torso": { "image": "torso.png" } }
725
+ }
726
+ },
727
+ "armoured": {
728
+ "attachments": {
729
+ "torso": { "torso_armoured": { "image": "torso_armoured.png" } }
730
+ },
731
+ "bones": ["pauldron"],
732
+ "ik": ["pauldron-ik"]
733
+ }
734
+ }
735
+ ```
736
+
737
+ | Field | Meaning |
738
+ | --- | --- |
739
+ | `attachments` | the short form's whole table, moved one level down |
740
+ | `bones` | bone names this skin activates |
741
+ | `ik`, `transform`, `path`, `physics`, `slider` | constraint names this skin activates, **still split per type** even though the top-level `constraints` array is unified |
742
+
743
+ ⭐ **The list is half a switch, and the other half is `skin: true` on the member.**
744
+ A bone's `skin: true` and a constraint's `skin: true` set `skinRequired`, and
745
+ `Skeleton.updateCache` starts every `skinRequired` object **inactive**, turning it
746
+ on only for the skin that names it (a listed bone activates its whole ancestor
747
+ chain with it). So the two fields only mean anything together, and either one alone
748
+ is dead data in silence:
749
+
750
+ - listed **without** `skin: true` — the bone poses under every skin, the constraint
751
+ runs under every skin, and the list changes nothing at all;
752
+ - `skin: true` **listed nowhere** — the bone never poses and the constraint never
753
+ runs, under any skin there is.
754
+
755
+ rigc refuses both halves by name, and `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` checks
756
+ the artifact for them. A bone or constraint belongs to **one** skin; two skins
757
+ naming the same one is also refused.
758
+
759
+ ⚠️ **The two spellings are told apart by these seven keys** — `attachments`,
760
+ `bones`, `ik`, `transform`, `path`, `physics`, `slider` — so a skin that uses any of
761
+ them is the long form and every one of its keys must be one of them. A slot left
762
+ beside them is refused rather than ignored (an ignored slot is an attachment that
763
+ vanishes), and a rig with a *slot* of one of those names is refused too, because
764
+ there the two forms are genuinely ambiguous. Rename the slot.
765
+
576
766
  ### 3.5 `constraints` — 4.3's single typed array
577
767
 
578
768
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
579
769
  discriminator. The 4.1/4.2 shape (top-level `ik`/`transform`/`path`/`physics`
580
770
  arrays) still loads clean and **the constraints simply vanish** — that is `A01`.
581
771
 
582
- rigc emits `ik` ([IK constraints](http://esotericsoftware.com/spine-ik-constraints)),
583
- `transform` ([transform constraints](http://esotericsoftware.com/spine-transform-constraints))
584
- and `physics` ([physics constraints](http://esotericsoftware.com/spine-physics-constraints)).
585
- Field lists are in [`src/rig.ts`](../src/rig.ts); three traps worth carrying here:
772
+ rigc emits all five: `ik`
773
+ ([IK constraints](http://esotericsoftware.com/spine-ik-constraints)), `transform`
774
+ ([transform constraints](http://esotericsoftware.com/spine-transform-constraints)),
775
+ `path` ([path constraints](http://esotericsoftware.com/spine-path-constraints)),
776
+ `physics` ([physics constraints](http://esotericsoftware.com/spine-physics-constraints))
777
+ and `slider`. Field lists are in [`src/rig.ts`](../src/rig.ts); three traps worth
778
+ carrying here:
586
779
 
587
780
  - A transform constraint's `properties` names come from a fixed six — `rotate`,
588
781
  `x`, `y`, `scaleX`, `scaleY`, `shearY`. rigc refuses anything else by name; in
@@ -593,6 +786,106 @@ Field lists are in [`src/rig.ts`](../src/rig.ts); three traps worth carrying her
593
786
  them parses cleanly and does nothing at all. rigc refuses it up front, and `A23`
594
787
  catches it from the other side.
595
788
 
789
+ Every constraint may also carry `skin: true`, which makes it run only under the skin
790
+ that lists it — see §3.4.1, and note that the flag alone does nothing.
791
+
792
+ #### 3.5.1 `path` — bones that travel along a curve
793
+
794
+ **When you need one:** anything that moves *along* something rather than around a
795
+ joint — a cart on a track, a fish in a current, a chain of links wrapping a pulley.
796
+ One `position` key moves the whole train, and the shape of the motion lives in the
797
+ curve instead of in the keys.
798
+
799
+ ```json
800
+ { "name": "ride", "type": "path", "bones": ["cart"], "slot": "track",
801
+ "positionMode": "percent", "spacingMode": "percent", "rotateMode": "tangent",
802
+ "position": 0.25 }
803
+ ```
804
+
805
+ | Field | Meaning |
806
+ | --- | --- |
807
+ | `bones` | at least one, in the order they ride the path |
808
+ | `slot` | **required.** The slot whose path attachment they follow (§3.4) |
809
+ | `positionMode` | default `"Percent"`: `position` is a fraction of the arc length. `"Fixed"` makes it world units |
810
+ | `spacingMode` | default `"Length"` — `Length`, `Fixed`, `Percent` or `Proportional` |
811
+ | `rotateMode` | default `"Tangent"`: each bone turns to the curve's tangent where it sits. `"Chain"`, `"ChainScale"` |
812
+ | `rotation` | default 0. Degrees added after the path's own rotation |
813
+ | `position` | default 0. Where the first bone sits |
814
+ | `spacing` | default 0. The gap between bones, in the unit `spacingMode` chose |
815
+ | `mixRotate`, `mixX` | default 1. `mixY` defaults to the same entry's `mixX` |
816
+
817
+ With the nine-point path of §3.4 — 180 units long — that constraint puts `cart` at
818
+ exactly `x = 45`, and a `position` timeline walking 0 → 0.5 walks it 0 → 90.
819
+
820
+ 🚨 **The slot has to be able to show a path.** `PathConstraint.update` opens with
821
+ `if (!(attachment instanceof PathAttachment)) return`, so a constraint aimed at a
822
+ slot that never shows one loads perfectly, reports every mix it was given, and moves
823
+ nothing at all — there is no error anywhere in that. rigc refuses a slot with no
824
+ path attachment in any skin, and `A36_PATH_CONSTRAINT_EFFECTIVE` refuses it again on
825
+ the artifact.
826
+
827
+ ⚠️ **The three mode names are checked, and only the first letter's case is free.**
828
+ The parser resolves them with `type[name[0].toUpperCase() + name.slice(1)]`, so
829
+ `"percent"` and `"Percent"` both work and `"PERCENT"` resolves to **`undefined`** —
830
+ which is then assigned without a word, and the constraint runs some *other* mode. An
831
+ unresolved `spacingMode` fails the `=== Length` test and spaces bones as though
832
+ `Fixed` had been asked for; an unresolved `rotateMode` is neither `Tangent` nor
833
+ `ChainScale`, so bones follow the curve and never turn along it.
834
+
835
+ #### 3.5.2 `slider` — a value that drives an animation
836
+
837
+ **When you need one:** a pose that has to be driven by a value instead of by time —
838
+ a dial that opens a door, a blend shape on a face, a suspension that compresses as
839
+ the wheel rises. A slider is the only constraint that applies an **animation**: the
840
+ animation is under its control while it is on, and `mix` is how much of it lands.
841
+
842
+ It comes in two shapes, and `bone` is the switch.
843
+
844
+ **Property-driven** — a bone's transform property maps to a time:
845
+
846
+ ```json
847
+ { "name": "knob", "type": "slider", "animation": "gate-swing",
848
+ "bone": "dial", "property": "rotate", "from": 0, "to": 0, "scale": 0.011111 }
849
+ ```
850
+
851
+ `time = to + (value - from) * scale`, so at `scale: 0.011111` (≈ 1/90) a dial turned
852
+ 90° applies `gate-swing` at one second — which is the whole mechanism: turn the
853
+ dial, the gate swings.
854
+
855
+ **Time-driven** — no bone, and the time is the slider's own pose value, keyed by an
856
+ `animations.<a>.slider.<name>.time` timeline (§4.12):
857
+
858
+ ```json
859
+ { "name": "reveal", "type": "slider", "animation": "curtain", "time": 0 }
860
+ ```
861
+
862
+ | Field | Meaning |
863
+ | --- | --- |
864
+ | `animation` | **required.** An animation the **motion spec** declares |
865
+ | `mix` | default 1. The slider's authority over what its animation keys |
866
+ | `additive` | default false. Add to the current pose instead of overwriting it |
867
+ | `loop` | default false. Repeat past the duration instead of holding the last frame |
868
+ | `bone` | the driving bone. Its presence switches the model |
869
+ | `property` | required with a bone: one of `rotate`, `x`, `y`, `scaleX`, `scaleY`, `shearY` |
870
+ | `from`, `to`, `scale` | the mapping above. Defaults 0, 0, 1 |
871
+ | `max` | default 0. Nonessential: the top of the editor's slider range |
872
+ | `local` | default false. Read the bone's local transform instead of its world one |
873
+ | `time` | the bone-less form's setup time |
874
+
875
+ ⭐ **`animation` is the one field in a rig spec that points at the motion spec.** It
876
+ is the mirror of `events`, where the rig declares a name the motion spec fires. The
877
+ parser resolves it in a **second pass** over the constraints array, after the
878
+ animations are read, and a miss throws `Slider animation not found`; rigc refuses it
879
+ where the message can name both files.
880
+
881
+ ⚠️ **The fields of the model you did not choose are refused, not ignored.** The
882
+ parser reads `time` only in the bone-less branch and `property`/`from`/`to`/`scale`/
883
+ `max`/`local` only in the other, so the losing half would be data no runtime ever
884
+ looks at. Two more shapes `A37_SLIDER_CONSTRAINT_EFFECTIVE` catches from the
885
+ artifact side: an animation with no timelines (the slider runs and the skeleton
886
+ never changes), and `loop: true` on a zero-length animation — looping computes
887
+ `duration + (time % duration)`, so the animation is applied at **NaN**.
888
+
596
889
  ### 3.6 `events` — names the animation can fire
597
890
 
598
891
  **When you need one:** something outside the skeleton has to happen on a
@@ -678,8 +971,14 @@ member order.
678
971
 
679
972
  ### 4.4 `tracks` — one target, one property
680
973
 
681
- A track names **exactly one** of `bone`, `slot`, `group`, `physics`. Two tracks on
682
- the same `target.property` is a compile error: merge them.
974
+ A track names **exactly one** of `bone`, `slot`, `group`, `physics`, `path`,
975
+ `slider`. Two tracks on the same `target.property` is a compile error: merge them.
976
+
977
+ ⭐ **The target field picks the family, not the property.** All three constraint
978
+ families have a timeline called `mix`, so `{ "physics": "hair", "property": "mix" }`
979
+ and `{ "path": "ride", "property": "mix" }` are different timelines with the same
980
+ property name — which is why the constraint's name goes in a field named after its
981
+ type rather than in a shared `constraint` key.
683
982
 
684
983
  Three families are **not** tracks and sit beside `tracks` instead — `ik` (§4.9),
685
984
  `transform` (§4.10) and `deform` (§4.11). The reason is the key rather than the
@@ -696,6 +995,10 @@ a deform). Folding them in would make `v` mean four different things depending o
696
995
  | `slot` | `attachment` | the attachment name, or `null` for "show nothing" |
697
996
  | `physics` | `mix` | `[mix]`, 0..1 — the constraint's authority |
698
997
  | `physics` | `reset` | `null` — the key *is* the event |
998
+ | `path` | `position`, `spacing` | `[value]` — see §4.12 |
999
+ | `path` | `mix` | `[mixRotate, mixX, mixY]` — one timeline, three channels |
1000
+ | `slider` | `time` | `[seconds]` — where in its animation the slider sits |
1001
+ | `slider` | `mix` | `[mix]` — the slider's authority |
699
1002
 
700
1003
  Translate values are **relative to the bone's setup position**; scale values are
701
1004
  multipliers where `1` is setup; rotation is in degrees.
@@ -1108,6 +1411,50 @@ an encoding.
1108
1411
 
1109
1412
  ---
1110
1413
 
1414
+ ### 4.12 `path` and `slider` timelines — tracks, not their own groups
1415
+
1416
+ Unlike `ik` and `transform`, these two are ordinary `tracks` entries: the format
1417
+ writes them as `animations.<a>.path.<constraint>.<timeline>` — a constraint name,
1418
+ then a timeline name under it — which is the same shape as `physics`, and a key that
1419
+ carries one `v` fits it.
1420
+
1421
+ ```json
1422
+ "tracks": [
1423
+ { "path": "ride", "property": "position", "keys": [
1424
+ { "t": 0, "v": [0], "ease": "smooth" }, { "t": 2, "v": [1] } ] },
1425
+ { "path": "ride", "property": "mix", "keys": [
1426
+ { "t": 0, "v": [0, 0, 0] }, { "t": 0.4, "v": [1, 1, 1] } ] },
1427
+ { "slider": "knob", "property": "mix", "keys": [
1428
+ { "t": 0, "v": [0] }, { "t": 0.5, "v": [1] } ] }
1429
+ ]
1430
+ ```
1431
+
1432
+ Those numbers are invented, and the first track is the one to read: `position` 0 → 1
1433
+ under `positionMode: "percent"` walks the constrained bones from one end of the
1434
+ curve to the other over two seconds. On the 180-long path of §3.4 the bone passes
1435
+ through exactly `x = 90` at the halfway point of a linear key pair, which is how you
1436
+ tell a working traversal from a plausible one.
1437
+
1438
+ | Group | `property` | Channels | Note |
1439
+ | --- | --- | --- | --- |
1440
+ | `path` | `position` | 1 | a fraction of the arc length, or world units under `positionMode: "fixed"` |
1441
+ | `path` | `spacing` | 1 | in the unit `spacingMode` chose |
1442
+ | `path` | `mix` | **3** | `[mixRotate, mixX, mixY]` in one key, so a raw `curve` is 12 numbers |
1443
+ | `slider` | `time` | 1 | the bone-less slider's own time. A slider WITH a bone takes its time from the bone and this timeline is not what drives it |
1444
+ | `slider` | `mix` | 1 | mix 0 makes `update()` return, so this is the on/off switch |
1445
+
1446
+ ⚠️ **A `path.mix` key states all three mixes.** In the file `mixY` defaults to the
1447
+ same key's `mixX`, and every field has a per-key default rather than carrying the
1448
+ previous key's value forward — the same trap as §4.9's. rigc writes all three out,
1449
+ so `v` is three numbers and not one.
1450
+
1451
+ ⚠️ **A muted constraint with no timeline is a finding, not an idiom.** Turning a
1452
+ constraint on from an animation is the idiom (§4.10), so `A36`/`A37` only object to
1453
+ all-zero mixes when **no** animation keys that constraint's `mix`. If you mute one
1454
+ at setup, key it somewhere.
1455
+
1456
+ ---
1457
+
1111
1458
  ## 5. Reading a failure
1112
1459
 
1113
1460
  Failures arrive in two layers, and they read differently.
@@ -1152,6 +1499,19 @@ the frequent ones, verbatim:
1152
1499
  | `deform … (t=…): offset 3 is odd` | §4.11 — the deform array is `x, y` pairs, so a run starts on an even index |
1153
1500
  | `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` |
1154
1501
  | `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
1502
+ | `vertexCount is N, which is not a multiple of 3` | §3.4 — a path's vertices are knots and handles read in groups of three: `3(K + 1)` open, `3K` closed |
1503
+ | `vertexCount is N and an open path needs at least 6` | §3.4 — an open path drops its first and last point, so it needs six for one curve |
1504
+ | `"lengths" is not authored — rigc measures the setup arc length of each curve` | §3.4 — delete the array; it is a measurement of the vertices above it |
1505
+ | `rig constraint "X": slot "Y" has no path attachment in any skin` | §3.5.1 — give that slot a `"type": "path"` attachment, or aim the constraint at the slot that has one |
1506
+ | `rig constraint "X": rotateMode is "CHAINSCALE"; known: Tangent, Chain, ChainScale` | §3.5.1 — only the first letter's case is free; anything else resolves to `undefined` in the parser |
1507
+ | `rig constraint "X": applies animation "Y", which the motion spec does not declare (it declares: …)` | §3.5.2 — fix the slider's `animation`, or add it to the motion spec |
1508
+ | `rig constraint "X": declares both a "bone" and "time"` | §3.5.2 — `bone` picks the model and `time` belongs to the other one |
1509
+ | `rig constraint "X": declares "property" but no "bone"` | §3.5.2 — name the driving bone, or key `slider.<name>.time` instead |
1510
+ | `skin "S" activates bone "B", but that bone does not declare \`"skin": true\`` | §3.4.1 — the list and the flag are one switch; add the flag or drop the list |
1511
+ | `bone "B" declares \`"skin": true\` but no skin activates it` | §3.4.1 — the other half: list it in the skin it belongs to, or drop the flag |
1512
+ | `skin "S": uses the long form … and also has a key "X"` | §3.4.1 — move the slot inside `attachments` |
1513
+ | `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 |
1514
+ | `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
1155
1515
 
1156
1516
  ### 5.2 Assertions — the gate
1157
1517
 
@@ -1199,21 +1559,24 @@ The report prints one line per assertion:
1199
1559
  | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
1200
1560
  | `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 |
1201
1561
  | `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 |
1202
- | `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, or a ribbon's entry row, is not pinned to its anchor bone at weight 1 |
1562
+ | `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 |
1203
1563
  | `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count |
1204
1564
  | `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 |
1205
1565
  | `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 |
1206
1566
  | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
1207
1567
  | `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not a subsequence of the rig's slot table — a slot is out of order, or is not in the table at all |
1208
1568
  | `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename |
1209
- | `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width |
1569
+ | `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 |
1210
1570
  | `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
1211
1571
  | `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
1212
1572
  | `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 |
1213
1573
  | `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 |
1214
- | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box or clipping polygon whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index, or a clipping `end` naming a slot the skeleton does not have (§3.4). **SKIP** when the skeleton carries neither type |
1215
- | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik` or `transform` timeline names a constraint the skeleton does not declare, names one of the other type, or carries no keys at all (§4.9, §4.10). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. **SKIP** when no animation carries one |
1574
+ | `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 |
1575
+ | `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 |
1216
1576
  | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, starts on an odd index, holds an odd count of numbers or a non-finite one, 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. **SKIP** when no animation carries a deform timeline |
1577
+ | `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 |
1578
+ | `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 |
1579
+ | `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` |
1217
1580
 
1218
1581
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
1219
1582
  clauses are gated by profile.
@@ -1228,15 +1591,16 @@ worse: an unknown attachment `type` returns `null` and the attachment disappears
1228
1591
  and a constraint entry with an unrecognised `type` matches no case and vanishes.
1229
1592
 
1230
1593
  Each is deferred for a stated reason, and the reason is the same one in every row:
1231
- **not one of these types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
1232
- parts 3-1 and 4-2), so none of them is on the ladder's critical path. The message
1594
+ **neither of these types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
1595
+ parts 3-1 and 4-2), so neither is on the ladder's critical path. The message
1233
1596
  says so, because a deferral without its reason is a wall rather than a work item.
1234
1597
 
1235
1598
  | You wrote | You get |
1236
1599
  | --- | --- |
1237
- | attachment `type` of `point`, `path`, `linkedmesh` | `attachment type "X" is in the Spine 4.3 format and rigc does not emit it yet. Implemented: region, mesh, boundingbox, clipping. point, path and linkedmesh are deliberately deferred: not one of them appears anywhere in the benchmark corpus …` |
1238
- | constraint `type` of `path` or `slider` | `constraint type "X" … Implemented: ik, transform, physics.` |
1239
- | mesh `generator.kind` of `contour` | `the "contour" generator would triangulate a part's own alpha mask, and src/mesh.ts has no triangulator` |
1600
+ | attachment `type` of `point` or `linkedmesh` | `attachment type "X" is in the Spine 4.3 format and rigc does not emit it yet. Implemented: region, mesh, boundingbox, clipping, path. point and linkedmesh are deliberately deferred: neither appears anywhere in the benchmark corpus …` |
1601
+ | 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 |
1602
+ | 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 |
1603
+ | 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 |
1240
1604
 
1241
1605
  Two more limits that are not errors but will shape what you can attempt:
1242
1606
 
@@ -1244,13 +1608,12 @@ Two more limits that are not errors but will shape what you can attempt:
1244
1608
  region covers its whole page, `pma: false`. To reproduce a skeleton whose art
1245
1609
  ships as a packed atlas you either supply loose PNGs and let rigc build its own
1246
1610
  atlas, or hand the packed atlas to `validate`/`bench` alongside the candidate.
1247
- - **`sequence` timelines, `drawOrderFolder` and `slider` / `path` constraint
1248
- timelines** are walked by the validator (so `A05` checks their curves and `diff`
1249
- counts their keys) and cannot be *written*: there is no motion-spec property for
1250
- any of them. The two constraint groups are blocked one level down — rigc emits no
1251
- `path` or `slider` constraint to key. Everything a motion spec **can** key is
1252
- §4.4's track table plus the five families that sit beside `tracks`: `drawOrder`
1253
- (§4.7), `events` (§4.8), `ik` (§4.9), `transform` (§4.10) and `deform` (§4.11).
1611
+ - **`sequence` timelines and `drawOrderFolder`** are walked by the validator (so
1612
+ `A05` checks their curves and `diff` counts their keys) and cannot be *written*:
1613
+ there is no motion-spec property for either. Everything a motion spec **can** key
1614
+ is §4.4's track table — which now includes the `path` and `slider` groups (§4.12)
1615
+ — plus the five families that sit beside `tracks`: `drawOrder` (§4.7), `events`
1616
+ (§4.8), `ik` (§4.9), `transform` (§4.10) and `deform` (§4.11).
1254
1617
 
1255
1618
  ---
1256
1619
 
@@ -1742,7 +2105,10 @@ which checks every animation of that shot, framed per set — see the scope note
1742
2105
  below) or **one animation directory** inside it. Everything else is optional:
1743
2106
  `--atlas` when the candidate's atlas is not beside its skeleton, `--as <name>` when
1744
2107
  your animation is called something the frame directory is not, `--framing shared`
1745
- to fit one framing across every set instead of one each, `--all-frames` to list
2108
+ to fit one framing across every set instead of one each,
2109
+ `--texture-from <atlas>` to attribute how much of the MAE is texture resampling
2110
+ rather than the rig (**§9.2**'s atlas floor — and note that it is *not* `--atlas`,
2111
+ which re-seats your geometry on that atlas's packing), `--all-frames` to list
1746
2112
  every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
1747
2113
  per-slot report.
1748
2114
 
@@ -2096,11 +2462,14 @@ of them.
2096
2462
  ⤷ fit x0.999256 offset +0.05, -0.02 px rms 0.42 px over 344 edge(s) union residual -0.27 x +0.17 px aspect -0.30% (derived, 4 pass(es), settled)
2097
2463
  ⭐ MAE-refined by -1, +1 px: 54.31 → 48.47 over the reference's own pixels (10.7% of the figure). …
2098
2464
  in units candidate 1995.3 x 809.7 reference 1995.3 x 809.9 x0.9999
2465
+ declared frames.json's own box: REFUSED, coordinates — a fit there asks for … px, past the … px the
2466
+ extent-spread tolerance reaches — a different origin or a different unit, over 86 frame(s).
2099
2467
 
2100
2468
  ── heavy — candidate animation "heavy", 12 fps ──
2101
2469
  frames 65 on disk, candidate samples 65, 65 compared
2102
2470
  MAE mean 23.10 worst 43.36 at f0029 (0..255 over the union alpha; …)
2103
2471
  ⤷ over the REFERENCE's own drawn pixels, mean 23.90 — the union figure compares two builds …
2472
+ (a ⭐ texture floor line joins these two when --texture-from is given — see below)
2104
2473
  slot drift worst 2.1 px "pendulum" at f0029
2105
2474
  per-frame 1 of 64 adjacent pair(s) change by a different amount than the reference does; worst
2106
2475
  f0018, yours moved 0 px where the reference moved 374
@@ -2134,6 +2503,11 @@ pass count, then one of
2134
2503
  The fit beside it is what a fit would still ask for, and on a shot whose
2135
2504
  silhouette differs anywhere that is not zero; it is the fit's floor, not your
2136
2505
  keys.
2506
+ - `extent-spread` — the frames' own box was kept even though your pixels did *not*
2507
+ land in it to within a pixel, because the correction a fit asks for is small
2508
+ relative to the shot and the fit admits it cannot explain it (see the `declared`
2509
+ line and the ⚖️ below). What is left over is a **silhouette** difference at the
2510
+ extremes and is a finding about your own outline, not about the box.
2137
2511
  - `cycling` — the correction fell into a repeating orbit instead of converging.
2138
2512
  **More passes cannot help**: the fit has no fixed point here. Read the `⚠️` line
2139
2513
  under it, which says whether the two content boxes agree anyway (the fit's own
@@ -2176,6 +2550,11 @@ The lines, in order:
2176
2550
  - `in units` — the same two boxes in world units. The framing absorbs a pure scale
2177
2551
  on purpose, so this is the only place one shows; it compares only if you measured
2178
2552
  the shot in the frames' own units.
2553
+ - `declared` — whether `frames.json`'s own box was taken for this set and on which
2554
+ clause, with the correction a fit there asks for and what that fit could not
2555
+ explain. It is printed whether the box was taken or refused, because *"refused,
2556
+ and by this much, on this ground"* is the part an author has to act on. The ⚖️
2557
+ passage below is what the three answers mean.
2179
2558
 
2180
2559
  ⚠️ **The framing is fitted to extent, and extent is not the same as alignment.**
2181
2560
  When your silhouette genuinely differs somewhere — a limb that overreaches, a part
@@ -2234,55 +2613,76 @@ much ink as the reference does gets `⚠️ overdraw` beside those two numbers,
2234
2613
  both pixel counts, because at that point the first figure is cheap for a reason
2235
2614
  that has nothing to do with your keys.
2236
2615
 
2237
- 🚨 **Part of your MAE is the texture, not the animation, and nothing in the report
2238
- says so.** The reference frames are rendered through the example's **own packed
2239
- atlas**, and a packed atlas may carry a `scale:` line — the ladder has one at
2240
- `scale: 0.5`, whose 745x212 part is packed at 373x106. rigc has no packer (**§6**), so
2241
- a candidate built from the loose PNGs samples a texture at twice that resolution and
2242
- resamples every edge differently. The pixels are the same shape in the same place; they
2243
- are filtered from a different source, and the difference lands on the outline of every
2244
- part in every frame. It is a constant, it is invisible to `content`, `rms` and the
2245
- `±2 px` refinement — a resampling difference is not an offset — and **no key you write
2246
- can move it**.
2247
-
2248
- ⇒ **When the MAE is flat across the whole set and the drift is already at the floor,
2249
- check the atlas before you look for keys.** The example's `.atlas` is an allowed input
2250
- in its own right (`bench/runs/README.md`, *What a run may read*, item 4), and one line
2251
- of it answers the question. To size the floor, re-run `check` once with
2252
- `--atlas <the example's own .atlas>`: same skeleton, same keys, the reference's own
2253
- texture. Measured on rung 3, MAE **6.13 / 6.01** with the candidate's own
2254
- full-resolution atlas against **2.25 / 2.30** with the supplied one — **two thirds of
2255
- the figure was the texture**, and the run that did not know it would have spent its
2256
- whole budget hunting a rig that was already right.
2257
-
2258
- ⚠️ Two things about that diagnostic. It is a **diagnostic and not a better number**:
2259
- the artifact `bench` validates ships its own atlas, so the first figure is the one
2260
- that belongs in a run's record and the second is the explanation of where it went.
2261
- And the coarser texture **loses** resolution the finer one has — on the same rung a
2262
- pair the reference moves *one pixel* across stopped being visible at half scale, so
2263
- the diagnostic run reported a frame-change disagreement the graded run does not have.
2264
- Read it for the floor, never as the verdict.
2265
-
2266
- 🚨 **And a third, which decides whether the recipe measures anything at all: `--atlas`
2267
- substitutes region *geometry* as well as texture.** The diagnostic's logic is *"same
2268
- skeleton, same keys, the reference's own texture"* — but an atlas entry is not only a
2269
- page and a rectangle. It also carries how the region was packed: **`rotate`**, and the
2270
- trim offsets that say where the opaque part sits inside the original image. Swap the
2271
- atlas and your attachments are re-seated on those, so the quads change too.
2272
-
2273
- ⇒ **The recipe measures a floor only when the substitution is *"same quads, coarser
2274
- texture"*.** Where the supplied atlas packs its regions **rotated or trimmed** and your
2275
- attachments were measured off the loose PNGs, it is not — and the tell is unmistakable:
2276
- **the number goes the wrong way.** A texture floor can only *explain* error, so a
2277
- diagnostic that sends the MAE **up** on every set has substituted geometry, not just
2278
- pixels, and the run's own atlas was the more faithful of the two.
2279
-
2280
- ⚠️ **Then the honest verdict is *inconclusive*, not *no floor*.** Both readings stay
2281
- open — there may be a texture floor this diagnostic cannot isolate — so record the
2282
- figures, say the substitution changed the quads, and do **not** convert a failed
2283
- diagnostic into a claim about the shot. ⇒ Check the atlas's own entries for `rotate` and
2284
- for trim before you run it; that is one look at a text file, and it tells you in advance
2285
- whether the number you are about to take will mean anything.
2616
+ 🚨 **Part of your MAE is the texture, not the animation.** The reference frames are
2617
+ rendered through the example's **own packed atlas**, and a packed atlas may carry a
2618
+ `scale:` line — the ladder has one at `scale: 0.5`, whose 745x212 part is packed at
2619
+ 373x106. rigc has no packer (**§6**), so a candidate built from the loose PNGs samples
2620
+ a texture at twice that resolution and resamples every edge differently. The pixels are
2621
+ the same shape in the same place; they are filtered from a different source, and the
2622
+ difference lands on the outline of every part in every frame. It is a constant, it is
2623
+ invisible to `content`, `rms` and the `±2 px` refinement — a resampling difference is
2624
+ not an offset — and **no key you write can move it**.
2625
+
2626
+ ⇒ **`check` attributes it when you tell it where the frames' own atlas is.** Pass
2627
+ `--texture-from <the example's own .atlas>` (its `.atlas` is an allowed input in its
2628
+ own right — `bench/runs/README.md`, *What a run may read*, item 4) and two figures
2629
+ appear under the MAE:
2630
+
2631
+ ```
2632
+ MAE mean 6.13 worst 7.51 at f0021 (0..255 over the union alpha; …)
2633
+ ⤷ over the REFERENCE's own drawn pixels, mean 6.13 — …
2634
+ ⭐ texture floor 5.84 above it 1.68 (over the reference's own pixels, 5.83 and 1.67)
2635
+ ⤷ … the texture accounts for 4.45 of this set's MAE (72.6% of the figure above) and
2636
+ cannot account for more than 5.84 — |MAE − above| ≤ floor …
2637
+ ```
2638
+
2639
+ - `texture floor` is your **own geometry through their texels** measured against your
2640
+ own render: same pose, same quads, same rasteriser, so there is no rig content in it
2641
+ at all.
2642
+ - `above it` is the MAE with that difference taken out.
2643
+ - ⭐ **They bound rather than subtract.** Absolute errors do not add, so
2644
+ `MAE = floor + above` is false; what is true, per pixel and therefore of the means,
2645
+ is `|MAE − above| ≤ floor`. So `MAE − above` is what the texture explained on these
2646
+ frames and `floor` is the most it could ever explain. **A floor near zero is a proof
2647
+ that the texture is not your problem** — which is a finding worth having before a
2648
+ day of key-hunting, and one the old recipe could not give.
2649
+
2650
+ Measured on rung 3: **6.13 / 6.01** with the candidate's own full-resolution atlas,
2651
+ of which **4.45 and 4.10 is texture** — about **70 %** — leaving **1.68 / 1.91** that
2652
+ is the rig. A run that did not know that would have spent its whole budget hunting a
2653
+ rig that was already right.
2654
+
2655
+ ⚠️ Two things about it. It is a **diagnostic and not a better number**: the artifact
2656
+ `bench` validates ships its own atlas, so the MAE is the figure that belongs in a run's
2657
+ record and the floor is the account of where part of it went. And the coarser texture
2658
+ **loses** resolution the finer one has — on rung 3 a pair the reference moves *one
2659
+ pixel* across stopped being visible at half scale, so a substituted run can report a
2660
+ frame-change disagreement the graded run does not have. Read it for the floor, never as
2661
+ the verdict.
2662
+
2663
+ 🚨 **Do not run this with `--atlas`, which is what the recipe used to say and which
2664
+ substitutes region *geometry* along with the texture.** `--atlas` names **your own**
2665
+ atlas for the case where it is not beside your skeleton; point it at a foreign one and
2666
+ the skeleton is re-loaded against that atlas. An atlas entry is not only a page and a
2667
+ rectangle — it carries **`rotate:`** and the trim offsets that say where the opaque part
2668
+ sits inside the original image, and a region attachment's quad is derived from those, so
2669
+ your attachments are re-seated and the quads change. Worse, `spine-core` implements a
2670
+ region's rotated corner assignment for `rotate: 90` and for nothing else, so a
2671
+ 270-packed region is sampled from the wrong part of the page outright.
2672
+
2673
+ ⇒ **The tell was unmistakable and it is the reason this is now a flag of its own: the
2674
+ number went the wrong way.** A texture floor can only *explain* error, so a diagnostic
2675
+ that sends the MAE **up** on every set has substituted geometry rather than pixels.
2676
+ Rung 7 is that case — its pack is `rotate: 270` — and under `--atlas` it read
2677
+ 24.93 → 28.84 and 19.17 → 27.75, which had to be recorded as **inconclusive**. Under
2678
+ `--texture-from` the same rung measures a floor of **1.7 MAE** on every set, about 2 %
2679
+ of each figure: there *is* a floor, it is small, and the rig is the story after all.
2680
+
2681
+ ⇒ **You no longer have to inspect the atlas for `rotate` or trim before running it.**
2682
+ `--texture-from` keeps every world vertex where your own atlas put it and remaps only
2683
+ the texture coordinates, through the drawing's own coordinate space, so a rotated or
2684
+ trimmed pack lands the same artwork in the same place. If a region of yours is missing
2685
+ from the substituting atlas the report names it and says the floor is a mixture.
2286
2686
 
2287
2687
  🚨 **The precondition the advice above does not state: a floor measured with another
2288
2688
  part misplaced is not a floor.** Measuring at the rest pose is right — it is the one
@@ -2316,27 +2716,54 @@ the floor is an upper bound on the error rather than a floor under it. ⚠️ Th
2316
2716
  same failure as capturing a guard's expected value from a screen that is already
2317
2717
  broken: the baseline records the defect, and then the *repair* is what looks wrong.
2318
2718
 
2319
- ⚖️ **`frames.json`'s own box can be refused for a reason that is not a coordinate
2320
- error, and there is an honest answer.** The test is on **extent**: a candidate authored
2321
- in the frames' own world units — one whose setup box lands on the reference's to the
2322
- pixel — still fails it if its union content box differs by a few pixels at the
2323
- extremes, because one part reaching somewhere nothing in the frames reaches is enough.
2324
- `check` then fits its own box, and on a multi-shot root the fitted framing costs every
2325
- set some MAE against the declared one — the same order as the shared-versus-per-set gap
2326
- the `--framing` flag's own help quotes, and easily more than a round of fitting buys.
2327
- That cost is real and it is **not** a sign you got the coordinates wrong.
2328
-
2329
- ⇒ **Report both, label which is which, and say what separates them.** Run `check`
2330
- unaided — that is the figure the artifact produces on its own and the one that belongs
2331
- in a run's record — then run it once more with `--viewport` on the declared box and keep
2332
- that output as a **named diagnostic file** beside the first. The gap between them is the
2333
- framing; what is left is the keys, which is the only reason to want the second number.
2334
- 🚫 **The pinned run is never the record.** `--viewport` is a claim about your own
2335
- coordinates and `check` says so above every figure it prints under one: *nothing checks
2336
- it*. And do not chase the refusal by shrinking a part to fit the box — that trades a
2337
- framing cost for a wrong silhouette, which is worse in every column that matters.
2338
- Instead read the `content` line's own advice: it names how much wider and shorter you
2339
- cover, and **which part reaches too far is a drift question**, not a framing one.
2719
+ ⚖️ **`frames.json`'s own box, and the `declared` line that says whether you got it.**
2720
+ Every set now prints one, taken or refused, with the numbers that decided:
2721
+
2722
+ ```
2723
+ declared frames.json's own box: TAKEN, coincident — a fit there asks for 0.08 px, under
2724
+ the 1 px that separates a candidate in the frames' coordinates from one in its
2725
+ own, over 65 frame(s).
2726
+ ```
2727
+
2728
+ Two clauses take the box. **`coincident`** is the plain one: rendering into that box
2729
+ put your pixels on the reference's, so you are authored in the frames' own
2730
+ coordinates — measured, not assumed. **`extent-spread`** is the tolerance for the case
2731
+ below, and when it engages the report names it and gives both of its numbers. A
2732
+ **`coordinates`** refusal means one scale and one offset account for the whole
2733
+ correction, which is what a different origin or a different unit looks like, and
2734
+ `check` frames you by a fit instead — where it is blind to your units by design.
2735
+
2736
+ ⚠️ **The test is on extent, and extent is not the same claim as coordinates.** A
2737
+ candidate authored in the frames' own world units — one whose setup box lands on the
2738
+ reference's to the pixel — used to fail it whenever its union content box differed by
2739
+ a few per cent at the extremes, because `fitFraming` registers extent and a box 6 %
2740
+ narrower reads as 6 % of scale, which is arithmetically what a units error reads as.
2741
+ Rung 7 was refused on **all twelve** of its sets that way, and the fitted framing it
2742
+ fell back to cost every one of them 0.28–2.01 MAE against the declared box. That cost
2743
+ was real and it was **not** a sign the coordinates were wrong.
2744
+
2745
+ ⇒ **`check` now separates the two by asking whether one similarity can explain the
2746
+ disagreement at all.** A difference of units or of origin *is* a similarity, so the fit
2747
+ absorbs it exactly and leaves a residual near zero — a rig at 2 % different units leaves
2748
+ 0.27 px rms. A silhouette differing at the extremes is not a similarity, so the residual
2749
+ stays the size of the disagreement — rung 7 leaves 3.32–16.85 px. So the box is taken
2750
+ when the correction reaches no further than **5 % of the reference's own content box**
2751
+ *and* the fit leaves **more than a pixel rms** it cannot explain. Both halves matter:
2752
+ the residual test on its own also takes a rig 300 units away, whose content box is
2753
+ truncated by the box it is being probed in and whose fit is therefore meaningless.
2754
+
2755
+ ⇒ **When the box is still refused, report both figures and label which is which.** Run
2756
+ `check` unaided — that is the figure the artifact produces on its own and the one that
2757
+ belongs in a run's record — then run it once more with `--viewport` on the declared box
2758
+ and keep that output as a **named diagnostic file** beside the first. The gap between
2759
+ them is the framing; what is left is the keys, which is the only reason to want the
2760
+ second number. 🚫 **The pinned run is never the record.** `--viewport` is a claim about
2761
+ your own coordinates and `check` says so above every figure it prints under one:
2762
+ *nothing checks it*. And do not chase a refusal by shrinking a part to fit the box —
2763
+ that trades a framing cost for a wrong silhouette, which is worse in every column that
2764
+ matters. Read the `content` line's own advice instead: it names how much wider and
2765
+ shorter you cover, and **which part reaches too far is a drift question**, not a
2766
+ framing one.
2340
2767
 
2341
2768
  **`Δpx` and `ref Δ`** are the two columns that do **not** compare you against the
2342
2769
  reference. They compare each side against **itself one frame earlier**: how many