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/README.md +125 -716
- package/cli.ts +118 -7
- package/docs/AUTHORING.md +524 -97
- package/docs/SPEC_COVERAGE.md +27 -12
- package/package.json +4 -1
- package/src/check.ts +552 -29
- package/src/compile.ts +674 -48
- package/src/mesh.ts +832 -3
- package/src/render.ts +300 -7
- package/src/rig.ts +448 -20
- package/src/transform.ts +13 -0
- package/src/types.ts +82 -7
- package/src/validate.ts +347 -38
- package/tools/contact.ts +126 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/png_probe.mjs +72 -0
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
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
that has one invokes the same
|
|
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`
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
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,
|
|
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
|
|
1215
|
-
| `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik` or `
|
|
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
|
-
**
|
|
1232
|
-
parts 3-1 and 4-2), so
|
|
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
|
|
1238
|
-
| constraint `type` of
|
|
1239
|
-
|
|
|
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
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
§4.
|
|
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,
|
|
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
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
a
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
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
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
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
|