spine-rigc 0.6.0 → 0.8.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
@@ -70,6 +70,12 @@ can see where Spine stops.
70
70
  ```bash
71
71
  bun install # once
72
72
 
73
+ # …but if what you were HANDED is a picture of the pose rather than numbers, read
74
+ # the numbers out of it first. This one runs BEFORE the loop, not inside it:
75
+ bun cli.ts pose --images path/to/parts --frame path/to/poseA.png --out poseA.json
76
+ # ↳ one entry per part PNG: where it sits, how confident that is, and where two
77
+ # answers are equally good — §11
78
+
73
79
  bun cli.ts build \
74
80
  --rig path/to/my.rig.json \
75
81
  --motion path/to/my.motion.json \
@@ -89,6 +95,11 @@ bun cli.ts check \
89
95
  # …and if nobody gave you frames, LOOK at it instead — neither needs a reference:
90
96
  bun cli.ts render --candidate path/to/spine # PNG frames + a contact sheet grid
91
97
  bun cli.ts preview --candidate path/to/spine # one .html that plays it in Spine's own player
98
+
99
+ # …and where you have several green candidates and no instrument that separates
100
+ # them, ask a human — the one loop step this toolchain cannot run for you:
101
+ bun cli.ts vote --candidate path/to/spine-a --candidate path/to/spine-b # -> ballot.html
102
+ bun cli.ts vote --record vote-<id>.json # -> votes.jsonl
92
103
  ```
93
104
 
94
105
  `build` compiles, round-trips the result through `@esotericsoftware/spine-core`,
@@ -122,16 +133,25 @@ What the flags mean:
122
133
  | `--motion` | the motion spec — time |
123
134
  | `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths are written relative to it |
124
135
  | `--copy-images` | `build` only: also copies every referenced page PNG into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own. Default is unchanged — page paths still point at the source art (issue #217) |
125
- | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory) |
136
+ | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** — every `.png` in it is a part, in name order |
126
137
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
127
- | `--profile` | `spine` = the 20 validity rules (**the default**) · `spine-html` = all 34, opt-in |
128
- | `--candidate` | `check`, `bench`, `render` and `preview` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton |
129
- | `--animation` | `render` and `preview` only: which animation to show. The default is **every** one for `render` and the **first** for `preview`. A name the skeleton does not have is refused, with the ones it does have listed |
138
+ | `--profile` | `spine` = the 22 validity rules (**the default**) · `spine-html` = all 36, opt-in |
139
+ | `--candidate` | `check`, `bench`, `render`, `preview` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` is the one command that takes it more than once** — repeat it 2–4 times, one per pane, labelled A, B, C, D in the order given; everywhere else a repeat is a typo and is refused |
140
+ | `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview`, and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote`, so is a name that only *some* candidates have |
141
+ | `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
142
+ | `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
143
+ | `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
144
+ | `--again` | `vote --record` only: record a second vote on a ballot the ledger already has. Without it a repeat is refused by name rather than doubled |
145
+ | `--frame` | `pose` only: one pose frame — the picture to read part placements out of. One frame per call; several key poses are several calls, and correlating them is yours (§11) |
146
+ | `--scale` | `pose` only: the scale window to search, as **frame pixels per part pixel**, `<min>,<max>` (default `0.5,2`). The report states what it searched, and a window that does not contain the truth does not reliably refuse — §11 |
147
+ | `--rotation` | `pose` only: the rotation window to search, in screen degrees, `<min>,<max>` (default `-180,180`, a full turn). Narrow it when you know the art is upright |
148
+ | `--max-residual` | `pose` only: above this residual a placement is **refused by name** instead of reported flat (default `0.25`). A reporting threshold, not a pass bar — the placement is still in the JSON |
130
149
 
131
150
  `render` also takes `--fps <n>` (the rate it samples at, default 12 — the same
132
151
  protocol rate the reference frames use) and `--max <px>` (the long side of a
133
- frame, default 256). Both commands take `--out`: a directory for `render`
134
- (default `render/`), the `.html` file for `preview` (default `preview.html`).
152
+ frame, default 256). Three commands take `--out`: a directory for `render`
153
+ (default `render/`), the `.html` file for `preview` (default `preview.html`) and
154
+ for `vote` (default `ballot.html`).
135
155
 
136
156
  Pick the profile deliberately, and know which one you got by saying nothing. The
137
157
  default is `spine`: "is this valid Spine 4.3 that any runtime plays correctly",
@@ -152,6 +172,9 @@ bun cli.ts check --candidate path/to/spine --frames path/to/frames
152
172
  bun cli.ts bench 3 --candidate path/to/spine [--frames path/to/frames]
153
173
  bun cli.ts render --candidate path/to/spine [--animation …] [--fps 12] [--max 256]
154
174
  bun cli.ts preview --candidate path/to/spine [--animation …] [--out preview.html]
175
+ bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
176
+ bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
177
+ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
155
178
  ```
156
179
 
157
180
  - **`explain`** is the one to reach for when a rig compiles but looks wrong. It
@@ -185,6 +208,34 @@ bun cli.ts preview --candidate path/to/spine [--animation …] [--out preview.h
185
208
  double-clicking it is also the interop proof — what plays there was played by
186
209
  Esoteric Software's runtime, not by rigc's. The player is loaded from a CDN
187
210
  rather than copied into the file, so the first open needs a network.
211
+ - 📐 **`pose` is the only command here that reads an INPUT rather than a result.**
212
+ Everything else takes a spec or a build and tells you something about it; `pose`
213
+ takes a picture the user already has — a key pose — and reports where each loose
214
+ part PNG sits in it, so you can write those coordinates into a rig and a motion
215
+ **by construction**. Nothing about it grades anything: at that point the pose is
216
+ a given condition, not a target, and the residual it reports is how far to trust
217
+ a placement rather than how good the placement is. Reach for it the moment a
218
+ request arrives as *"make it go from this picture to this one"*. **§11.**
219
+ - 🗳️ **`vote` is `preview` with more than one candidate in it and an answer
220
+ coming back**, and it is the one step of this loop you cannot run yourself.
221
+ Reach for it where the instruments have run out: two builds that `check` and
222
+ `diff` cannot separate, a pose fit with two local optima, a key density that is
223
+ a matter of taste, a first draft with no reference at all. **The intended loop
224
+ is: you compile 2–4 candidates that every one of them passes the gate → `rigc
225
+ vote` writes one `ballot.html` → a human opens it, watches the panes loop, and
226
+ picks a winner or declares a tie → they save the small JSON result the page
227
+ hands them → `rigc vote --record <that file>` checks it against the ballot and
228
+ appends one line to `votes.jsonl` → you read the ledger and proceed.**
229
+ Compile first, vote last: never put a candidate on a ballot that did not build
230
+ green, and never ask the human to read JSON, a diff or a spec — the page holds
231
+ playable pixels and nothing else. The panes are labelled `A`/`B`/`C`/`D` and
232
+ carry no paths, so do not describe the candidates to the voter either. What
233
+ comes back is machine-checkable rather than prose: each line names the winner
234
+ by content **digest** (a label means nothing outside one ballot), carries the
235
+ `coverage` set so you can compute what is still unreviewed, and carries a
236
+ reason code from a closed enumeration. A **tie is a recorded answer**, and
237
+ `both-unacceptable` is the one that means *propose again* rather than *adopt
238
+ either* — check for it before you treat a ballot as settled.
188
239
 
189
240
  ---
190
241
 
@@ -617,6 +668,9 @@ time puts it here.
617
668
  | `tracks` | the timelines |
618
669
  | `drawOrder` | the draw-order timeline — §4.7. Not a track: it names no target |
619
670
  | `events` | the event timeline — §4.8. Not a track, for the same reason |
671
+ | `ik` | IK constraint timelines — §4.9. Not a track: its keys carry named fields, not one `v` |
672
+ | `transform` | transform constraint timelines — §4.10. Same reason |
673
+ | `deform` | deform timelines — §4.11. Same reason |
620
674
 
621
675
  `groups` (`name → [member, …]`) lets one track target several bones or slots at
622
676
  once; `lag` shifts every key of a track, and `stagger` adds a per-member delay in
@@ -627,6 +681,13 @@ member order.
627
681
  A track names **exactly one** of `bone`, `slot`, `group`, `physics`. Two tracks on
628
682
  the same `target.property` is a compile error: merge them.
629
683
 
684
+ Three families are **not** tracks and sit beside `tracks` instead — `ik` (§4.9),
685
+ `transform` (§4.10) and `deform` (§4.11). The reason is the key rather than the
686
+ target: a track's key carries one `v`, and those three carry named fields each
687
+ (two for an IK constraint, six for a transform constraint, a sparse vertex run for
688
+ a deform). Folding them in would make `v` mean four different things depending on
689
+ `property`. 4.3 also writes them as their own groups beside `bones` and `slots`.
690
+
630
691
  | Target | `property` | Key `v` |
631
692
  | --- | --- | --- |
632
693
  | `bone` | `translate`, `scale`, `shear` | `[x, y]` |
@@ -819,6 +880,232 @@ decreasing time builds an `EventTimeline` whose earlier firing is simply
819
880
  unreachable, with a perfectly clean load.
820
881
  `A32_EVENT_KEYS_RESOLVE` checks all three from the other side.
821
882
 
883
+ ### 4.9 `ik` — mixing an IK constraint in over time
884
+
885
+ The rig spec declares the constraint (§3.5); this keys it. 4.3 writes the group as
886
+ `animations.<a>.ik.<constraint>` — **one unnamed timeline per constraint**, so the
887
+ constraint name is the only target there is, and there is no timeline name between
888
+ the two the way a bone has `rotate` and `translate`.
889
+
890
+ ```json
891
+ "animations": {
892
+ "walk": {
893
+ "duration": 0.8666666,
894
+ "loop": true,
895
+ "tracks": [],
896
+ "ik": [
897
+ {
898
+ "constraint": "rear-leg-ik",
899
+ "keys": [
900
+ { "t": 0, "mix": 1, "softness": 0 },
901
+ { "t": 0.2666666, "mix": 1, "softness": 14, "ease": "settle" },
902
+ { "t": 0.5333333, "mix": 0, "softness": 0 },
903
+ { "t": 0.8666666, "mix": 1, "softness": 0 }
904
+ ]
905
+ }
906
+ ]
907
+ }
908
+ }
909
+ ```
910
+
911
+ ⚠️ **Every number above is invented.** They are shaped like a walk cycle — the leg
912
+ under load is solved to the ground and the leg in flight is let go — and nothing
913
+ here was measured off a real rig. Copy the shape, not the values.
914
+
915
+ | Field | Meaning | Absent means |
916
+ | --- | --- | --- |
917
+ | `mix` | 0..1, how much of the solved rotation is applied | `1` |
918
+ | `softness` | distance from full reach at which the bones stop straightening | `0` |
919
+ | `bendPositive` | two-bone bend direction | `true` |
920
+ | `compress` | one-bone IK scales the bone down to reach a close target | `false` |
921
+ | `stretch` | scales the bone up to reach a far target | `false` |
922
+
923
+ - **`mix` and `softness` are the two curve channels, in that order.** A raw
924
+ `curve` is therefore 8 numbers, and the three booleans are stepped by nature —
925
+ nothing interpolates a bend direction.
926
+ - `mix` outside `0..1` is a compile error: `IkConstraintPose.mix` is documented as
927
+ a percentage. A **transform** mix is documented *unbounded*, which is why §4.10
928
+ has no such rule — the asymmetry is the runtime's, not ours.
929
+ - 🚨 **A field stated on one key and not the next is a compile error.** This is the
930
+ refusal worth reading twice, because the format's shape invites the mistake: the
931
+ parser reads every field **fresh per key** with its own default, so a key that
932
+ omits `softness` does not hold the previous key's 14 — it snaps to 0, and
933
+ interpolates down to it on the way. It loads. It plays. It is not what you
934
+ wrote. So state a field on every key of a track or on none of them; writing the
935
+ default out loud is how you opt in.
936
+ - Its last key counts towards the declared duration like any other (R7).
937
+
938
+ rigc refuses four things here:
939
+
940
+ | You wrote | You get |
941
+ | --- | --- |
942
+ | a constraint the rig does not declare | `keys unknown ik constraint "X"; the rig declares ik constraint(s): …` |
943
+ | a `transform` constraint's name | `keys "X" as an ik constraint, but the rig declares it as a "transform" constraint` |
944
+ | `softness` on the first key only | `key 0 names "softness" and key 1 (t=…) does not … it would snap to 0` |
945
+ | `mix: 1.5` | `mix is 1.5, outside 0..1 — the runtime documents it as a percentage 0-1` |
946
+
947
+ The type check matters because the parser resolves a timeline's target by name
948
+ **and** type: `findConstraint(name, IkConstraintData)` misses a transform
949
+ constraint of the same name, returns null, and `readAnimation` throws — in the
950
+ consumer's process, which is late. `A34_CONSTRAINT_TIMELINE_TARGETS` checks the
951
+ same two from the other side, plus one thing the compiler cannot produce and a
952
+ hand-edited file can: an **empty key array**, which the parser skips in silence.
953
+
954
+ ### 4.10 `transform` — turning a muted transform constraint on
955
+
956
+ Same shape as §4.9 and the same absent-means-default rule; six mixes instead of
957
+ two. The idiom this exists for is an aim rig: the constraints are declared with
958
+ `mixRotate: 0` in the rig spec — muted at setup, so the figure is posed by its own
959
+ bones — and the animation that needs them mixes them in.
960
+
961
+ ```json
962
+ "animations": {
963
+ "aim": {
964
+ "duration": 0.3333333,
965
+ "loop": false,
966
+ "tracks": [],
967
+ "transform": [
968
+ { "constraint": "aim-head-transform", "keys": [{ "t": 0, "mixRotate": 1 }] },
969
+ { "constraint": "aim-torso-transform", "keys": [{ "t": 0, "mixRotate": 0.6 }] },
970
+ { "constraint": "aim-front-arm-transform", "keys": [{ "t": 0, "mixRotate": 1 }] }
971
+ ]
972
+ }
973
+ }
974
+ ```
975
+
976
+ ⚠️ **Every number above is invented**, including the 0.6 — a torso that follows the
977
+ aim less than the head is a plausible rig and not a measured one.
978
+
979
+ A single key at `t: 0` is the whole timeline here, and that is not a degenerate
980
+ case: it says "while this animation is playing, this constraint is on", which is
981
+ exactly what a mix that was 0 at setup needs said.
982
+
983
+ | Field | Absent means |
984
+ | --- | --- |
985
+ | `mixRotate` | `1` |
986
+ | `mixX` | `1` |
987
+ | `mixY` | **this key's own `mixX`** — not `1` |
988
+ | `mixScaleX` | `1` |
989
+ | `mixScaleY` | `1` |
990
+ | `mixShearY` | `1` |
991
+
992
+ - **Six curve channels, in the order above.** A raw `curve` is 24 numbers.
993
+ - No range check: every one of these is documented **unbounded**, and an over-mix
994
+ above 1 is a real editor idiom rather than a mistake.
995
+ - ⚠️ A mix is only read by the runtime if the constraint declares the matching
996
+ `properties` mapping (§3.5). Keying `mixScaleY` on a constraint that maps
997
+ rotation only is dead data — legal, loaded, and it moves nothing.
998
+ - The refusals are §4.9's, with `transform` in place of `ik`.
999
+
1000
+ ### 4.11 `deform` — moving an attachment's vertices
1001
+
1002
+ The one timeline whose keys mean something different depending on what they are
1003
+ attached to, and the only one keyed on a **skin / slot / attachment** triple:
1004
+ `animations.<a>.attachments.<skin>.<slot>.<attachment>.deform`.
1005
+
1006
+ ```json
1007
+ "animations": {
1008
+ "hoverboard": {
1009
+ "duration": 1.6666666,
1010
+ "loop": true,
1011
+ "tracks": [],
1012
+ "deform": [
1013
+ {
1014
+ "slot": "board",
1015
+ "attachment": "board",
1016
+ "keys": [
1017
+ { "t": 0 },
1018
+ { "t": 0.5, "fromVertex": 4, "vertices": [0, -7, 0, -7], "ease": "settle" },
1019
+ { "t": 1.1666666, "fromVertex": 4, "vertices": [0, 4, 0, 4], "ease": "settle" },
1020
+ { "t": 1.6666666 }
1021
+ ]
1022
+ }
1023
+ ]
1024
+ }
1025
+ }
1026
+ ```
1027
+
1028
+ ⚠️ **Every number above is invented** — the vertex indices, the offsets and the
1029
+ times alike. A real deform's numbers come from the geometry it is bending, and
1030
+ there is no way to guess which vertices of somebody's board are its middle.
1031
+
1032
+ | Field | Meaning |
1033
+ | --- | --- |
1034
+ | `skin` | which skin the attachment lives in. Absent = `"default"` |
1035
+ | `slot` | the slot |
1036
+ | `attachment` | the attachment's **placeholder** name inside that skin and slot |
1037
+
1038
+ Per key:
1039
+
1040
+ | Field | Meaning |
1041
+ | --- | --- |
1042
+ | `vertices` | the run: `x, y` offset pairs. **Absent** = back to the setup pose |
1043
+ | `fromVertex` | which VERTEX the run starts at — rigc translates it |
1044
+ | `offset` | the same start as a raw index into the deform array. Never with `fromVertex` |
1045
+ | `ease` / `curve` | one channel, and it eases the **blend**, not a coordinate |
1046
+
1047
+ Four things are worth having straight before you write one.
1048
+
1049
+ **A key is a sparse edit, and a key with no `vertices` is the setup pose.** The
1050
+ parser builds an array as long as the attachment's own, copies your run into it at
1051
+ `offset`, and leaves the rest at zero. So a run only has to cover the vertices that
1052
+ move, and the `{ "t": 0 }` and `{ "t": 1.6666666 }` keys above are how a looping
1053
+ deform starts and ends undeformed. That is the format's own encoding for it, not a
1054
+ convention — which is why rigc refuses a start index on such a key: there would be
1055
+ nothing for it to point at.
1056
+
1057
+ **Offsets, not positions.** The numbers are relative to the setup geometry in both
1058
+ encodings. On an unweighted attachment the parser literally adds the setup vertex
1059
+ back on load; on a weighted one zero *is* "unmoved".
1060
+
1061
+ **The array is indexed by vertex on an unweighted attachment and by bone influence
1062
+ on a weighted one.** Its length is `vertices.length` in the first case and
1063
+ `vertices.length / 3 * 2` in the second, because a weighted attachment stores
1064
+ `x, y, weight` per influence. A vertex with three bones on it therefore occupies
1065
+ **three** pairs, each in that bone's own bind space. This is the whole reason
1066
+ `fromVertex` exists and the whole reason it is sometimes refused:
1067
+
1068
+ - on an **unweighted** attachment `fromVertex` always works — vertex *v* is array
1069
+ index `2v`, exactly;
1070
+ - on a **weighted** one it works while every vertex the run covers has exactly one
1071
+ bone on it, and rigc computes the true start index for you (which is *not*
1072
+ `2v` — earlier multi-bone vertices push it along);
1073
+ - on a **multi-bone** vertex it is a compile error, and deliberately so. One
1074
+ `x, y` for such a vertex is not a thing the array can hold: its world offset is
1075
+ `Σ weightᵦ · Mᵦ · offsetᵦ` over its bones, so a single pair only means what you
1076
+ meant if every influencing bone happens to share one world matrix. rigc will not
1077
+ guess. Either key the control bone instead — which is what a rigc-generated ring
1078
+ or ribbon mesh is *for* — or write the bind-space pairs yourself and start the
1079
+ run with `offset`. The refusal tells you the index that vertex starts at.
1080
+
1081
+ **The curve eases the blend.** A deform timeline has exactly one channel and
1082
+ `readCurve` builds it between **0 and 1** — the fraction of the way from this key's
1083
+ geometry to the next one's. So a named `ease` behaves as it does anywhere, and a
1084
+ raw `curve` is 4 numbers whose value axis runs 0..1, not the range of your vertex
1085
+ offsets.
1086
+
1087
+ rigc refuses six things here, and the first is the quietest defect in the whole
1088
+ animation half of the format:
1089
+
1090
+ | You wrote | You get |
1091
+ | --- | --- |
1092
+ | a run that ends past the array | `the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long (4 vertices)` |
1093
+ | an odd `offset` | `offset 3 is odd. The deform array is x, y pairs, so an odd start puts every x of this run on a y` |
1094
+ | an odd number of `vertices` | `"vertices" holds 3 numbers; the deform array is x, y PAIRS` |
1095
+ | `fromVertex` on a multi-bone vertex | `"fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` |
1096
+ | an attachment that is not there | `slot "flat" in skin "default" has no attachment "flatt" (it has: flat)` |
1097
+ | a deform on a region attachment | `a deform timeline keys the vertices of an attachment, and this one is a "region"` |
1098
+
1099
+ The overrun is the one to fear. `readAnimation` copies with
1100
+ `Utils.arrayCopy(vertices, 0, deform, offset, vertices.length)` into a
1101
+ `Float32Array`, and writing past the end of a typed array is a **no-op in
1102
+ JavaScript** — no throw, no warning, no `NaN`. A run one pair too long loses its
1103
+ tail and deforms the rest of the mesh correctly, which looks nearly right, and
1104
+ "nearly right" is the hardest kind of wrong to find.
1105
+ `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` checks all of it from the other side, on the
1106
+ emitted file, measuring the array's length from the attachment rather than assuming
1107
+ an encoding.
1108
+
822
1109
  ---
823
1110
 
824
1111
  ## 5. Reading a failure
@@ -857,6 +1144,14 @@ the frequent ones, verbatim:
857
1144
  | `vertexCount N wants M unweighted numbers and "vertices" holds K` | fix the count or the array; they decide the encoding between them |
858
1145
  | `end names slot "X", which this rig does not declare` | fix the clipping attachment's `end`, or add the slot |
859
1146
  | `bone "X" takes its position from …, which needs a cut manifest` | R8 — pass `--manifest`, or write literal `x`/`y` |
1147
+ | `animation "A" keys unknown ik constraint "X"; the rig declares ik constraint(s): …` | §4.9 — fix the name, or declare the constraint in the rig spec |
1148
+ | `animation "A" keys "X" as an ik constraint, but the rig declares it as a "transform" constraint` | §4.9 — a timeline's target resolves by name AND type; put the entry under the right group |
1149
+ | `ik constraint "X": key 0 names "softness" and key 1 (t=…) does not` | §4.9 — every key is read with its own default, so state the field on every key or on none |
1150
+ | `ik constraint "X" (t=…): mix is 1.5, outside 0..1` | §4.9 — an IK mix is a percentage; a transform mix is unbounded |
1151
+ | `deform …: the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long` | §4.11 — shorten the run or move its start; the parser would drop the tail in silence |
1152
+ | `deform … (t=…): offset 3 is odd` | §4.11 — the deform array is `x, y` pairs, so a run starts on an even index |
1153
+ | `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
+ | `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
860
1155
 
861
1156
  ### 5.2 Assertions — the gate
862
1157
 
@@ -917,6 +1212,8 @@ The report prints one line per assertion:
917
1212
  | `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 |
918
1213
  | `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 |
919
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 |
1216
+ | `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 |
920
1217
 
921
1218
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
922
1219
  clauses are gated by profile.
@@ -947,10 +1244,13 @@ Two more limits that are not errors but will shape what you can attempt:
947
1244
  region covers its whole page, `pma: false`. To reproduce a skeleton whose art
948
1245
  ships as a packed atlas you either supply loose PNGs and let rigc build its own
949
1246
  atlas, or hand the packed atlas to `validate`/`bench` alongside the candidate.
950
- - **Sequences, `drawOrderFolder`, event timelines and deform timelines** are walked
951
- by the validator but are not motion-spec properties: the track table in §4.4 is
952
- the complete list of what a *track* can key, and §4.7's `drawOrder` is the only
953
- timeline outside it.
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).
954
1254
 
955
1255
  ---
956
1256
 
@@ -2772,3 +3072,119 @@ answer read off the exports:
2772
3072
 
2773
3073
  If one of those turns out to matter, it belongs in the run's `log.md` as something
2774
3074
  the frames had to teach you — not here.
3075
+
3076
+ ---
3077
+
3078
+ ## 11. Reading a pose you were given — `rigc pose`
3079
+
3080
+ ```bash
3081
+ bun cli.ts pose --images path/to/parts --frame path/to/poseA.png [--out pose.json]
3082
+ ```
3083
+
3084
+ Every other command in this guide takes something you wrote and tells you about
3085
+ it. This one runs the other way: it takes a **picture the user already has** and
3086
+ reports, for each loose part PNG, where that part sits in it — `x`, `y`,
3087
+ `rotationDeg`, `scale` — plus a residual saying how well the placed part explains
3088
+ the frame's pixels underneath it.
3089
+
3090
+ Reach for it when a request arrives as *"make it go from this picture to this
3091
+ one"*. Read both frames, write the two key poses into the rig and the motion from
3092
+ the numbers it gives you, and spend your loops on the part no instrument can
3093
+ measure: the movement between them.
3094
+
3095
+ 📘 **The recipe that consumes this report is [MOTION.md](MOTION.md)** — how these
3096
+ placements become a rig and a motion, what goes between two key poses when nothing
3097
+ was given for the in-betweens, and how to spread candidates so a `vote` ballot
3098
+ informs.
3099
+
3100
+ ### 11.1 It measures an input, so nothing here is a score
3101
+
3102
+ 🚨 **No number in this report has a pass bar, and none of them is a grade.** The
3103
+ distinction is not modesty, it decides how to read the output. `check` and `bench`
3104
+ compare a build against a reference, so their numbers mean *how close*. A pose
3105
+ frame is not a reference — it is a **given condition**, and once the spec states
3106
+ those coordinates there is nothing left to be close to. The residual exists so you
3107
+ know **how far to trust each placement** and **where two answers are equally
3108
+ good**.
3109
+
3110
+ That is why the command's output is shaped as help rather than as a verdict:
3111
+
3112
+ - a placement that matches nowhere is **refused by name** and its best guess is
3113
+ still printed, because a refusal tells you not to trust a number rather than
3114
+ hiding it;
3115
+ - two near-equal optima are reported as **both**, flagged `ambiguous`, never
3116
+ silently resolved;
3117
+ - a part whose rotation genuinely does not matter is reported as having a **free
3118
+ degree of freedom**, not as a failure.
3119
+
3120
+ ### 11.2 The coordinate contract
3121
+
3122
+ Placements are in the **frame's own pixels: y down, origin top-left** — the same
3123
+ convention a cut manifest uses, and the `space` field of every report repeats it.
3124
+
3125
+ - `x`, `y` — where the part image's own centre, `(width / 2, height / 2)`, lands.
3126
+ - `rotationDeg` — **screen** degrees, positive turning **clockwise** on screen.
3127
+ - `scale` — uniform, as **frame pixels per part pixel**.
3128
+
3129
+ Reconstruct a part pixel `p` as `centre + scale · R(rotationDeg) · (p − (width/2,
3130
+ height/2))`. To get to Spine's y-up, counter-clockwise world, use the two
3131
+ conversions that already exist and open-code neither:
3132
+ `screenToSpineDegrees(rotationDeg)` and `cropToSpineY(y, frameHeight)`
3133
+ ([`src/transform.ts`](../src/transform.ts)).
3134
+
3135
+ ### 11.3 The fields, and what each one is for
3136
+
3137
+ Per part:
3138
+
3139
+ | Field | Meaning |
3140
+ | --- | --- |
3141
+ | `part`, `path`, `width`, `height` | the PNG, by the name every message uses |
3142
+ | `placement` | the best placement found — `null` **only** for `empty-part` and `larger-than-canvas`, where nothing was searched |
3143
+ | `alternates` | other optima worth reporting, best first. Non-empty means the answer was not unique |
3144
+ | `ambiguous` | at least one alternate is inside the ambiguity margin. **Choose with something this instrument cannot see** — anatomy, the other frame, or `rigc vote` |
3145
+ | `rotationFree` | the part is self-similar under rotation, so `rotationDeg` is a placeholder and the value is yours |
3146
+ | `rotationSelfSimilarity` | the number `rotationFree` is a threshold on. A part just over the line is worth a look |
3147
+ | `refusal` | `{ reason, detail }` or `null`. Reasons: `no-match`, `larger-than-canvas`, `empty-part` |
3148
+ | `coarse` | the grid this part was actually searched on. A handful of cells means the part is small relative to the frame and the first pass had little to go on |
3149
+ | `notes` | the same facts in prose, in the order they were found |
3150
+
3151
+ Per placement:
3152
+
3153
+ | Field | Meaning |
3154
+ | --- | --- |
3155
+ | `residual` | alpha-weighted mean absolute colour error over the part's own footprint, `0..1`. **Lower is better explained** — that is all it means |
3156
+ | `unexplained` | the share of the part's material that disagrees with the frame at this placement. **Read this next to the residual** — see §11.4 |
3157
+ | `offCanvas` | the share of the part's material that falls outside the frame at this placement |
3158
+ | `footprint` | frame pixels of material the placement accounts for; the tie-break between two equal residuals |
3159
+ | `bbox` | the axis-aligned box the placed part occupies, in frame pixels |
3160
+
3161
+ The report also carries `frame.background` (how the picture's empty space was
3162
+ identified — a flat colour, transparency, or `unknown`), `search` (every window
3163
+ and threshold that was applied), and `caveats`.
3164
+
3165
+ ### 11.4 What it cannot see — read this before using the numbers
3166
+
3167
+ - ⚠️ **Residuals degrade under occlusion, and there is no depth solver here.** A
3168
+ part drawn *behind* another has the occluder's pixels where its own should be, so
3169
+ its residual rises **at the correct placement**. `unexplained` separates the two
3170
+ readings: a middling residual with a high `unexplained` usually means *right
3171
+ place, seen through something else*, not *wrong place*. The only robustness in
3172
+ the objective is that it is measured on the part's **own alpha footprint** — a
3173
+ part is never charged for pixels of the picture it does not claim. Weigh
3174
+ accordingly; do not treat either number as a verdict.
3175
+ - ⚠️ **A search window that does not contain the truth does not reliably refuse.**
3176
+ A part shrunk inside the region it came from still explains those pixels, so the
3177
+ answer is the best placement available *inside* `--scale` / `--rotation` and its
3178
+ residual can look reasonable. This is why the window is a reported field: if the
3179
+ numbers surprise you, check `search` before you trust them.
3180
+ - ⚠️ **A frame whose border has no dominant colour reports `background.unknown`.**
3181
+ Every pixel then counts as material, the silhouette signal is gone, and the
3182
+ residual is colour agreement alone. The report says so rather than being quietly
3183
+ weaker.
3184
+ - **An `ambiguous` part is genuinely ambiguous.** Two identical limbs, a part that
3185
+ fits its own silhouette at more than one angle, and a shape whose interior is one
3186
+ flat colour all look like this. The instrument has run out; that is what
3187
+ `rigc vote` is for.
3188
+ - **One frame per call.** Several key poses are several calls, and correlating A
3189
+ with B — which placement of a repeated part belongs to which limb, across two
3190
+ frames — is the authoring job, not this tool's.