rig-c 0.0.0-stage → 2.21.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.
Files changed (213) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +30 -0
  4. package/NOTICE.md +145 -0
  5. package/README.md +817 -3
  6. package/bin/rigc.cjs +83 -0
  7. package/cli.ts +61 -0
  8. package/cli_core.ts +46 -0
  9. package/docs/AUTHORING.md +9923 -0
  10. package/docs/FACE.md +1948 -0
  11. package/docs/INGEST.md +1488 -0
  12. package/docs/MOTION.md +1241 -0
  13. package/docs/PROMPTING.md +109 -0
  14. package/docs/RIGGING.md +1441 -0
  15. package/docs/SPEC_COVERAGE.md +357 -0
  16. package/package.json +108 -4
  17. package/skills/rigc/SKILL.md +133 -0
  18. package/skills/rigc-face/SKILL.md +60 -0
  19. package/skills/rigc-ingest/SKILL.md +78 -0
  20. package/skills/rigc-motion/SKILL.md +51 -0
  21. package/skills/rigc-rigging/SKILL.md +49 -0
  22. package/src/areaband.ts +159 -0
  23. package/src/assertions/bodies/a01.ts +23 -0
  24. package/src/assertions/bodies/a02.ts +21 -0
  25. package/src/assertions/bodies/a03.ts +27 -0
  26. package/src/assertions/bodies/a04.ts +40 -0
  27. package/src/assertions/bodies/a05.ts +56 -0
  28. package/src/assertions/bodies/a06.ts +245 -0
  29. package/src/assertions/bodies/a07.ts +68 -0
  30. package/src/assertions/bodies/a08.ts +76 -0
  31. package/src/assertions/bodies/a09.ts +82 -0
  32. package/src/assertions/bodies/a10.ts +116 -0
  33. package/src/assertions/bodies/a11.ts +15 -0
  34. package/src/assertions/bodies/a12.ts +30 -0
  35. package/src/assertions/bodies/a13.ts +51 -0
  36. package/src/assertions/bodies/a14.ts +35 -0
  37. package/src/assertions/bodies/a15.ts +97 -0
  38. package/src/assertions/bodies/a16.ts +24 -0
  39. package/src/assertions/bodies/a17.ts +26 -0
  40. package/src/assertions/bodies/a18.ts +62 -0
  41. package/src/assertions/bodies/a19.ts +404 -0
  42. package/src/assertions/bodies/a20.ts +122 -0
  43. package/src/assertions/bodies/a21.ts +190 -0
  44. package/src/assertions/bodies/a22.ts +39 -0
  45. package/src/assertions/bodies/a23.ts +305 -0
  46. package/src/assertions/bodies/a24.ts +68 -0
  47. package/src/assertions/bodies/a25.ts +39 -0
  48. package/src/assertions/bodies/a26.ts +61 -0
  49. package/src/assertions/bodies/a27.ts +33 -0
  50. package/src/assertions/bodies/a28.ts +70 -0
  51. package/src/assertions/bodies/a29.ts +34 -0
  52. package/src/assertions/bodies/a30.ts +50 -0
  53. package/src/assertions/bodies/a31.ts +61 -0
  54. package/src/assertions/bodies/a32.ts +44 -0
  55. package/src/assertions/bodies/a33.ts +110 -0
  56. package/src/assertions/bodies/a34.ts +133 -0
  57. package/src/assertions/bodies/a35.ts +160 -0
  58. package/src/assertions/bodies/a36.ts +81 -0
  59. package/src/assertions/bodies/a37.ts +77 -0
  60. package/src/assertions/bodies/a38.ts +73 -0
  61. package/src/assertions/bodies/a39.ts +303 -0
  62. package/src/assertions/bodies/a40.ts +128 -0
  63. package/src/assertions/bodies/a42.ts +97 -0
  64. package/src/assertions/bodies/a43.ts +181 -0
  65. package/src/assertions/bodies/a44.ts +23 -0
  66. package/src/assertions/bodies/a45.ts +172 -0
  67. package/src/assertions/bodies/a46.ts +224 -0
  68. package/src/assertions/bodies/a47.ts +126 -0
  69. package/src/assertions/bodies/a48.ts +83 -0
  70. package/src/assertions/bodies/a49.ts +81 -0
  71. package/src/assertions/bodies/a50.ts +97 -0
  72. package/src/assertions/constraint_words.ts +169 -0
  73. package/src/assertions/emitted/index.ts +148 -0
  74. package/src/assertions/facts/animated_bones.ts +30 -0
  75. package/src/assertions/facts/animation_durations.ts +37 -0
  76. package/src/assertions/facts/atlas_pages.ts +19 -0
  77. package/src/assertions/facts/atlas_regions.ts +52 -0
  78. package/src/assertions/facts/bone_timelines.ts +37 -0
  79. package/src/assertions/facts/constraint_targets.ts +56 -0
  80. package/src/assertions/facts/constraints.ts +155 -0
  81. package/src/assertions/facts/deform_survey.ts +27 -0
  82. package/src/assertions/facts/event_keys.ts +55 -0
  83. package/src/assertions/facts/linked_meshes.ts +38 -0
  84. package/src/assertions/facts/mesh_attachments.ts +100 -0
  85. package/src/assertions/facts/region_joins.ts +34 -0
  86. package/src/assertions/facts/sequences.ts +85 -0
  87. package/src/assertions/facts/skeleton_roster.ts +45 -0
  88. package/src/assertions/facts/skin_entries.ts +37 -0
  89. package/src/assertions/facts/skin_members.ts +53 -0
  90. package/src/assertions/facts/slider_composition.ts +78 -0
  91. package/src/assertions/facts/slot_colour.ts +43 -0
  92. package/src/assertions/facts/stage.ts +27 -0
  93. package/src/assertions/facts/stage_box.ts +65 -0
  94. package/src/assertions/facts/stepped_poses.ts +74 -0
  95. package/src/assertions/facts/two_colour.ts +52 -0
  96. package/src/assertions/facts/vertex_polygons.ts +53 -0
  97. package/src/assertions/footprints.ts +367 -0
  98. package/src/assertions/harness.ts +109 -0
  99. package/src/assertions/inward_advance.ts +58 -0
  100. package/src/assertions/kinds.ts +105 -0
  101. package/src/assertions/mesh_kinds.ts +56 -0
  102. package/src/assertions/model/animated_bones.ts +38 -0
  103. package/src/assertions/model/animation_durations.ts +57 -0
  104. package/src/assertions/model/atlas_pages.ts +15 -0
  105. package/src/assertions/model/atlas_regions.ts +76 -0
  106. package/src/assertions/model/bone_timelines.ts +58 -0
  107. package/src/assertions/model/constraint_targets.ts +82 -0
  108. package/src/assertions/model/constraints.ts +233 -0
  109. package/src/assertions/model/declared.ts +125 -0
  110. package/src/assertions/model/deform_survey.ts +24 -0
  111. package/src/assertions/model/event_keys.ts +45 -0
  112. package/src/assertions/model/given.ts +45 -0
  113. package/src/assertions/model/index.ts +398 -0
  114. package/src/assertions/model/linked_meshes.ts +24 -0
  115. package/src/assertions/model/mesh_attachments.ts +119 -0
  116. package/src/assertions/model/parse.ts +146 -0
  117. package/src/assertions/model/region_joins.ts +67 -0
  118. package/src/assertions/model/runtime_timelines.ts +78 -0
  119. package/src/assertions/model/sequences.ts +157 -0
  120. package/src/assertions/model/skeleton_roster.ts +23 -0
  121. package/src/assertions/model/skin_entries.ts +69 -0
  122. package/src/assertions/model/skin_members.ts +64 -0
  123. package/src/assertions/model/slider_composition.ts +193 -0
  124. package/src/assertions/model/slot_colour.ts +81 -0
  125. package/src/assertions/model/stage.ts +28 -0
  126. package/src/assertions/model/stage_box.ts +51 -0
  127. package/src/assertions/model/stepped_poses.ts +105 -0
  128. package/src/assertions/model/two_colour.ts +61 -0
  129. package/src/assertions/model/vertex_polygons.ts +72 -0
  130. package/src/assertions/reasons.ts +129 -0
  131. package/src/assertions/region_lookups.ts +61 -0
  132. package/src/assertions/report.ts +189 -0
  133. package/src/assertions/values.ts +39 -0
  134. package/src/atlas.ts +2870 -0
  135. package/src/ballot.ts +866 -0
  136. package/src/bonedist.ts +643 -0
  137. package/src/chainfit.ts +2752 -0
  138. package/src/chains.ts +170 -0
  139. package/src/check.ts +4303 -0
  140. package/src/checkpics.ts +295 -0
  141. package/src/cli/core_commands.ts +1627 -0
  142. package/src/cli/repack.ts +414 -0
  143. package/src/cli/shared.ts +2776 -0
  144. package/src/cli/spine_commands.ts +820 -0
  145. package/src/compile.ts +9414 -0
  146. package/src/core/additive.ts +458 -0
  147. package/src/core/animation.ts +1050 -0
  148. package/src/core/clipping.ts +696 -0
  149. package/src/core/constraints.ts +1876 -0
  150. package/src/core/constraints_path.ts +964 -0
  151. package/src/core/constraints_physics.ts +881 -0
  152. package/src/core/constraints_slider.ts +635 -0
  153. package/src/core/deform.ts +613 -0
  154. package/src/core/draw_order.ts +125 -0
  155. package/src/core/events.ts +135 -0
  156. package/src/core/hooks.ts +249 -0
  157. package/src/core/index.ts +1400 -0
  158. package/src/core/raw.ts +739 -0
  159. package/src/core/skins.ts +129 -0
  160. package/src/core/uvs.ts +469 -0
  161. package/src/core/vertices.ts +490 -0
  162. package/src/core/walk.ts +197 -0
  163. package/src/core/world.ts +289 -0
  164. package/src/correspondence.ts +15 -0
  165. package/src/deformbuild.ts +60 -0
  166. package/src/deformgen.ts +630 -0
  167. package/src/deformmeasure.ts +732 -0
  168. package/src/deformreport.ts +373 -0
  169. package/src/deformstructure.ts +386 -0
  170. package/src/deformsurvey.ts +2162 -0
  171. package/src/depth.ts +784 -0
  172. package/src/diff.ts +2252 -0
  173. package/src/emit.ts +134 -0
  174. package/src/emit_spine.ts +854 -0
  175. package/src/errors.ts +53 -0
  176. package/src/framing.ts +819 -0
  177. package/src/generation.ts +139 -0
  178. package/src/ingest.ts +2293 -0
  179. package/src/json-position.ts +253 -0
  180. package/src/keyorder.ts +587 -0
  181. package/src/keys.ts +486 -0
  182. package/src/ladder.ts +121 -0
  183. package/src/mesh.ts +2382 -0
  184. package/src/meshcompare.ts +1191 -0
  185. package/src/meshquality.ts +2051 -0
  186. package/src/meshrasters.ts +944 -0
  187. package/src/meshreduce.ts +1444 -0
  188. package/src/model.ts +1245 -0
  189. package/src/motion.ts +809 -0
  190. package/src/nonfinite.ts +54 -0
  191. package/src/package_meta.ts +48 -0
  192. package/src/png.ts +297 -0
  193. package/src/pose.ts +2324 -0
  194. package/src/preview.ts +434 -0
  195. package/src/region_joins.ts +54 -0
  196. package/src/render.ts +1013 -0
  197. package/src/render_core.ts +871 -0
  198. package/src/render_shared.ts +2958 -0
  199. package/src/repack.ts +495 -0
  200. package/src/rig.ts +2941 -0
  201. package/src/slots.ts +892 -0
  202. package/src/spine_side.ts +138 -0
  203. package/src/timelines.ts +837 -0
  204. package/src/trackgen.ts +364 -0
  205. package/src/transform.ts +310 -0
  206. package/src/types.ts +1797 -0
  207. package/src/validate.ts +3875 -0
  208. package/tools/contact.ts +126 -0
  209. package/tools/editor_roundtrip.ts +1641 -0
  210. package/tools/font5x7.ts +101 -0
  211. package/tools/measure_contact_depth.ts +105 -0
  212. package/tools/plate.ts +508 -0
  213. package/tools/png_probe.mjs +72 -0
package/docs/INGEST.md ADDED
@@ -0,0 +1,1488 @@
1
+ # Working with a skeleton you did not author
2
+
3
+ **Read this when what you were handed is already a skeleton.** It is written for an
4
+ agent holding a `skeleton.json` — plus its `.atlas` and page images — that came out
5
+ of the Spine editor or another tool, and that has been asked to work *with* it:
6
+ understand it, answer a complaint rigc raised about it, re-express it as rigc specs,
7
+ normalise or re-pivot it, or extend it with motion it does not have.
8
+
9
+ [AUTHORING.md](AUTHORING.md) is the file formats, the failure map and the CLI — read
10
+ it first and keep it open; this page never restates a field it documents.
11
+ [MOTION.md](MOTION.md) is what goes *between* two poses. This page is the third
12
+ question neither of them answers: **what the toolchain will and will not do with
13
+ somebody else's file, and what the honest routes through it are.**
14
+
15
+ 🚨 **The two numbers this page produces are not grades, and they measure different
16
+ things.** `validate`'s red says *this file breaks a stated rule* — a fact about the
17
+ file, not about your work, and sometimes (§3.2) a fact about the rule. `diff`'s
18
+ ratios say *how much of a particular reference's structure your candidate
19
+ reproduces* — so deliberately extending or renaming a foreign skeleton **lowers
20
+ them**, by design (§4.2, §4.3). Neither one has a pass bar, and this page does not
21
+ invent one.
22
+
23
+ - Formats and the CLI reference: [README.md](../README.md)
24
+ - The rig spec, field by field: **AUTHORING §3**, and [`src/rig.ts`](../src/rig.ts)
25
+ - The motion spec, field by field: **AUTHORING §4**
26
+ - Named failures, and the file each one points at: **AUTHORING §5–§6**
27
+ - The coordinate contract, in one place: **AUTHORING §11.2** and
28
+ [`src/transform.ts`](../src/transform.ts)
29
+ - What the editor does when nobody tells it otherwise: **AUTHORING §10**
30
+ - Why the re-pivot in **§4.1** is shaped the way it is, and the child-bone row it
31
+ warns about worked on a bone that actually has one: [RIGGING.md](RIGGING.md) §3.
32
+ Its §2 is how to tell whether the pivot you are moving *to* is identified at all
33
+ - What the Spine 4.3 format holds, field by field:
34
+ [SPEC_COVERAGE.md](SPEC_COVERAGE.md)
35
+ - If you are the *person operating* an agent rather than the agent:
36
+ [PROMPTING.md](PROMPTING.md)
37
+
38
+ ---
39
+
40
+ ## 0. What the toolchain can do with a file you were handed
41
+
42
+ Two facts decide everything below, and they pull in opposite directions:
43
+
44
+ 1. **rigc reads compiled skeleton JSON in more places than you would guess.**
45
+ `validate`, `render`, `preview`, `vote`, `check`, `diff` and `ingest` all take a `skeleton.json` path directly, and none of them needs a rig
46
+ spec to do it.
47
+ 2. **rigc cannot EDIT one.** There is no command that opens a skeleton and
48
+ changes it. The only thing that produces a skeleton is `build`, and `build`'s
49
+ input is a rig spec plus a motion spec.
50
+ ⇒ **Every route that ends in a changed file goes through the specs** — and
51
+ there are two ways to get them: write them (§2, transcription) or have
52
+ `ingest` write them for you from the file itself (§2.0).
53
+
54
+ ### 0.1 The table
55
+
56
+ Every row was executed against the fetched example corpus before it was written down.
57
+ `3-timing-and-spacing` and `spineboy` are the two skeletons this page uses; both carry
58
+ an upstream `license.txt` (Appendix, and [NOTICE.md](../NOTICE.md)).
59
+
60
+ | Command | Takes a foreign `skeleton.json`? | What it needs, and what it gives back |
61
+ | --- | --- | --- |
62
+ | **`validate <skeleton.json>`** | ✅ **yes — this is its foreign-data form** | the `.json`, plus one `.atlas` beside it or named with `--atlas`. Runs the assertions and prints `PASS`/`FAIL`/`SKIP`/`PROF` per rule, naming the profile that judged it. §1.1 |
63
+ | **`render --candidate <skeleton.json>`** | ✅ **yes** | PNG frames plus a contact sheet, per animation. ⭐ **It does not gate** — it draws a file `validate` refuses, which is how you tell a red about the file from a red about the rule (§3.2) |
64
+ | **`preview --candidate <skeleton.json>`** | ✅ **yes** | one self-contained `.html` that plays it in the official Spine Web Player. Needs a network the first time it is opened ([NOTICE.md](../NOTICE.md)) |
65
+ | **`vote --candidate <a> --candidate <b>`** | ✅ **yes, on either side** | a ballot page. Pairing a foreign export against your own transcription is a legitimate ballot, and the panes carry no paths |
66
+ | **`check --candidate <skeleton.json> --frames <dir>`** | ✅ **yes** | ⭐ it reads **frames and never a reference skeleton**, so a foreign export enters this one *twice over*: as the candidate, or — via `render` — as the source of the frames. §1.4 |
67
+ | **`diff <candidate.json> <reference.json>`** | ✅ **yes, both sides** | 54 structural measures over bones, slots, attachments, constraints, animations and events. ⛔ **Blind to every coordinate** — §1.3 |
68
+ | **`ingest <skeleton.json> --out <dir>`** | ✅ **yes — and it is the only reader that WRITES specs** | the `.json` alone; no atlas, no art, no project file. Out come `rig.json`, `motion.json` and a findings report, such that `build`ing them reproduces the skeleton it read **byte for byte — for a skeleton rigc emitted**. ⚠️ For an editor export the claim is identity in canonical form apart from `hash` and `spine`, which §2.3 states in full. The seventh reader, and the one that ends §2's hand work — §2.0 and §5 |
69
+ | **`pose --images <dir> --frame <png>`** | ⛔ **not the skeleton** | loose part PNGs and one picture. A packed atlas page is not loose parts, and pointing it at one produces a confident answer about nothing — §5 |
70
+ | **`explain --rig … --motion … --out …`** | ⛔ **no** | rig spec + motion spec. It explains **what you wrote**, which makes it a transcription instrument rather than a reading one — §1.5 |
71
+ | **`build --rig … --motion … --images …`** | ⛔ **no** | specs in, skeleton out. The only writer in the toolchain, and the reason §2 exists |
72
+ | **`build … --atlas-in <file.atlas>`** | ⛔ not the skeleton — **but yes, the foreign *atlas*** | resolves your parts against the regions of a pack somebody else made, joining on region name. The one place a foreign *file* becomes an input to `build`. It applies the page's `scale:`, so an imported size is the drawing's rather than the pack's — §2.3 |
73
+ | **`build … --pack`** | ⛔ **no** | puts your own loose parts onto shared pages instead of one page per part, losslessly. Not foreign input, but it is what makes a transcription's atlas comparable in shape to an export's — §2.3 |
74
+ | **`bench <rung> --candidate …`** | ✅ mechanically | it is the *benchmark's* instrument: it knows which corpus example is which rung and measures against that one. Not an ingest instrument, and named here only so nobody reaches for it. Use `diff` and `check` directly |
75
+
76
+ ### 0.2 Two path shapes, and why the directory form is not yours
77
+
78
+ `validate`, `render`, `preview`, `check`, `vote` and `bench` all accept either a
79
+ **directory** or a **`.json` file**. The directory form resolves to `skeleton.json` +
80
+ `skeleton.atlas` — **rigc's own output names**. A foreign export directory is named
81
+ after whatever the editor called the project, so the directory form finds nothing:
82
+
83
+ ```bash
84
+ rigc validate examples/3-timing-and-spacing/export
85
+ ```
86
+
87
+ ```
88
+ rigc validate …/examples/3-timing-and-spacing/export/skeleton.json
89
+ .. atlas …/examples/3-timing-and-spacing/export/skeleton.atlas
90
+ …
91
+ ENOENT: no such file or directory, open '…/examples/3-timing-and-spacing/export/skeleton.json'
92
+ ```
93
+
94
+ ⚠️ **That one comes back as a stack trace, exit 1, rather than a named refusal.** It
95
+ is the only complaint on this page that does — everything in §3.1 is a sentence. Do
96
+ not read the trace as *rigc cannot open this export*; read the two `..` lines above
97
+ it, which name the two files it went looking for. ⇒ **Point every command at the
98
+ `.json` itself.** Every command line on this page does.
99
+
100
+ The atlas then resolves by looking beside the skeleton, and that lookup refuses
101
+ rather than guesses:
102
+
103
+ | What is beside the `.json` | What happens |
104
+ | --- | --- |
105
+ | exactly one `.atlas` | it is used, and the report names it |
106
+ | no `.atlas` | ⛔ `no .atlas beside …; name one with --atlas <path>` |
107
+ | two or more | ⛔ refuses and lists them — see below |
108
+ | any of the above, with `--atlas <path>` | `--atlas` wins outright |
109
+
110
+ ```bash
111
+ rigc validate examples/spineboy/export/spineboy-pro.json
112
+ ```
113
+
114
+ ```
115
+ rigc: 2 atlases beside …/examples/spineboy/export/spineboy-pro.json (spineboy-run.atlas,
116
+ spineboy.atlas); name the right one with --atlas <path> — guessing by filename is how
117
+ an attachment quietly resolves against the wrong page
118
+ ```
119
+
120
+ ⭐ **This refusal is worth understanding rather than working around, because the
121
+ obvious heuristic is wrong on this very file.** `spineboy-ess` shares a longer
122
+ filename prefix with `spineboy-run.atlas` than with the `spineboy.atlas` it actually
123
+ uses. Pick by prefix and every attachment resolves against a page that does not
124
+ contain it — and §3.2 shows what that looks like when you get it wrong, which is a
125
+ failure two rules downstream of the choice.
126
+
127
+ ---
128
+
129
+ ## 1. Reading it
130
+
131
+ ### 1.1 `validate` — what the file is, and what rigc objects to
132
+
133
+ ```bash
134
+ rigc validate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
135
+ ```
136
+
137
+ ```
138
+ rigc validate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
139
+ .. atlas …/examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas
140
+ .. profile spine — 8 renderer-policy and 8 archetype assertion(s) do not apply
141
+ PASS A07_ATLAS_TEXT_SHAPE
142
+ PASS A00_ROUNDTRIP_PARSE
143
+ PASS A16_SKELETON_VERSION_4_3
144
+ …
145
+ SKIP A31_DRAW_ORDER_OFFSETS_RESOLVE: no animation carries a drawOrder timeline
146
+ SKIP A32_EVENT_KEYS_RESOLVE: no animation carries an event timeline
147
+ SKIP A34_CONSTRAINT_TIMELINE_TARGETS: no animation carries a constraint timeline
148
+ SKIP A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: no animation carries a deform timeline
149
+ SKIP A04_MESH_TRIANGLES_AND_ENCODING: the skeleton carries no mesh attachment
150
+ SKIP A33_VERTEX_ATTACHMENT_GEOMETRY: the skeleton carries no bounding box, clipping attachment or path
151
+ SKIP A20_MESH_WEIGHTS_COHERENT: the skeleton carries no mesh attachment
152
+ SKIP A22_MESH_UVS_IN_UNIT_RANGE: the skeleton carries no mesh attachment
153
+ SKIP A23_PHYSICS_CONSTRAINT_EFFECTIVE: the skeleton declares no physics constraint
154
+ …
155
+ PROF A12_NO_DARK_COLOR: renderer rule, not in profile "spine"
156
+ PROF A21_MESH_RIM_PINNED: archetype rule, not in profile "spine"
157
+ …
158
+ rigc: green
159
+ ```
160
+
161
+ Read it as three separate statements, because they answer three different questions:
162
+
163
+ - **`PASS` / `FAIL`** — the rule ran, and this is its verdict.
164
+ - **`SKIP`** — the rule ran and had nothing to measure, and the line says what was
165
+ absent. A `SKIP` is never a pass, and on foreign data the `SKIP` list is also a
166
+ **free inventory of what the skeleton does not contain**. The nine lines above tell
167
+ you, without your having opened the JSON, that this export has no draw-order
168
+ timeline, no event timeline, no constraint timeline, no deform timeline, no mesh
169
+ attachment, no physics constraint, and no bounding box, clipping attachment or
170
+ path. ⭐ What passes over an empty list is the other kind of rule — `A01`, `A02`,
171
+ `A11`, `A12`, `A14` ask *how many of this does the file carry*, and **zero is the
172
+ answer**.
173
+ - **`PROF`** — the rule was excluded by the profile before its body ran. §3.3.
174
+
175
+ ⚠️ **A green here is a statement about validity and nothing else.** It does not say
176
+ the skeleton is the one you were meant to be given, that its animations are the ones
177
+ their names suggest, or that anything in it is where the art wants it. The gate cannot
178
+ see a wrong animation (AUTHORING §0), and it certainly cannot see a wrong *file*.
179
+
180
+ ### 1.2 `render` and `preview` — look at it
181
+
182
+ ```bash
183
+ rigc render --candidate examples/spineboy/export/spineboy-pro.json \
184
+ --atlas examples/spineboy/export/spineboy.atlas \
185
+ --animation walk --fps 8 --max 200 --out render/sb
186
+ ```
187
+
188
+ ```
189
+ rigc render
190
+ .. skeleton …/examples/spineboy/export/spineboy-pro.json
191
+ .. atlas …/examples/spineboy/export/spineboy.atlas
192
+ .. poser spine-core — no skeleton.model.json beside …/examples/spineboy/export/spineboy-pro.json — a Spine export, not a rigc build
193
+ .. 200x186px at 8 fps, 1 set(s) -> …/render/sb
194
+ .. walk 9 frame(s), 1.000s + contact.png -> …/render/sb/walk@8fps
195
+ rigc: wrote …/render/sb/frames.json
196
+ ```
197
+
198
+ Three properties of this command matter more for foreign data than for your own:
199
+
200
+ - ⭐ **It does not gate.** That same `spineboy-pro.json` fails two assertions under the
201
+ default profile (§3.2) and renders all nine `walk` frames anyway. ⇒ **A red
202
+ `validate` is not a reason to stop looking**, and looking is often what tells you
203
+ whether the red matters.
204
+ - **Omitting `--animation` renders every animation**, each into its own directory with
205
+ its own contact sheet. On a file you were handed, that is the cheapest complete
206
+ inventory there is — one image per animation, with spacing visible across the grid.
207
+ - **The frame size is fitted to the skeleton's own extent**, so `--max` is a cap on the
208
+ longest side and not a canvas. A skeleton whose world box is 24 units wide renders
209
+ 24 px wide however large you set it; read the size on the `..` line before concluding
210
+ a render came out empty.
211
+
212
+ `preview` writes one HTML file that plays the same data in the official Spine Web
213
+ Player. On foreign data it is also the **interop proof**: if that page plays it, a
214
+ Spine runtime plays it, whatever rigc's own rasteriser or validator thinks.
215
+
216
+ ### 1.3 `diff` — and the two things it cannot see
217
+
218
+ `diff` takes two compiled skeletons and reports 54 measures in eight groups, plus two
219
+ blocks that report and gate nothing: the `(reported)` measures beside `attachments`
220
+ and `animations`, and the `skeleton` header block at the top, which measures the header's setup-pose
221
+ bounding box.
222
+ A ninth group of six joins them when something has paired the two sides'
223
+ animations — `--as <candidate>=<reference>`, or one animation each side, which pairs by
224
+ position (§1.3.1). Both sides may be foreign; the interesting pairing during ingest is
225
+ **your transcription against the export it came from**:
226
+
227
+ ```bash
228
+ rigc diff work/t3/skeleton.json \
229
+ examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
230
+ ```
231
+
232
+ ```
233
+ rigc diff
234
+ candidate …/work/t3/skeleton.json
235
+ reference …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
236
+ .. bones=3/3 slots=2/2 skins=1/1 attachments=2/2 constraints=0/0 animations=2/2 events=0/0 (candidate/reference)
237
+
238
+ skeleton (reported) (no mean) over 2 measures — the header's setup-pose bounding box, which no reading of the frames could decide
239
+ 1.000 bounds_present 1/1 both headers carry a setup-pose bounding box, or neither does — …
240
+ 0.250 bounds_box 1/4 the header carries the same box (x, y, width, height — the extent as stated, an omitted origin as the 0 it means) — …
241
+
242
+ bones mean 1.000 over 8 measures
243
+ 1.000 count 3/3 how many bones
244
+ 1.000 names 3/3 the bone names themselves
245
+ 1.000 parent_by_name 3/3 each bone hangs off the same parent
246
+ 1.000 order 3/3 the bones are declared in the same order
247
+ 1.000 length_present 3/3 a setup `length` is present or absent alike
248
+ 1.000 inherit_present 3/3 a setup `inherit` is present or absent alike
249
+ 1.000 depth_histogram 3/3 NAME-AGNOSTIC: as many bones at each depth
250
+ 1.000 degree_sequence 3/3 NAME-AGNOSTIC: as many bones with each child count
251
+
252
+ bones (name-agnostic) mean 1.000 over 5 measures — the same two skeletons compared with names thrown away
253
+ …
254
+ animations mean 1.000 over 11 measures
255
+ 1.000 count 2/2 how many animations
256
+ 1.000 names 2/2 the animation names
257
+ 1.000 duration 2/2 each animation runs as long (last key time, within one frame)
258
+ 1.000 timeline_kinds 8/8 the same timelines exist
259
+ 1.000 key_counts 69/69 those timelines carry as many keys
260
+ 1.000 curve_kinds 69/69 as many linear / stepped / bezier keys
261
+ …
262
+ ```
263
+
264
+ Each pair is **matched / total**, where the total is the larger of the two sides. So a
265
+ count of `2/3` means one side has three of something and only two were matched — and
266
+ it does not say *which* side has three. The `..` line above is where you read that.
267
+
268
+ ⛔ **`diff` is blind to every coordinate a bone, an attachment or a key carries.** No
269
+ measure reads a bone's `x`/`y`/`rotation`, an attachment's offset, or a key's value —
270
+ only *presence*, *names*, *counts*, *order* and *kinds*. §4.1 moves a pivot 236.5
271
+ units and every one of the 54 measures still reads **1.000**. ⇒ Never take a green
272
+ `diff` as evidence that a geometric edit did not land, and never take it as evidence
273
+ that one did.
274
+
275
+ ⚠️ **`diff` has no value-level measure, and the reason is an input rather than a
276
+ policy** (§2.3 says what a value comparison covers): comparing values means reading
277
+ both files through `spine-core`, and a skeleton whose attachments carry a `sequence`
278
+ cannot be parsed without the atlas that resolves it — so the measure takes two
279
+ skeletons **and two packs**, which `rigc diff <a.json> <b.json>` does not have.
280
+
281
+ ⚠️ **The one exception is the header's own box**, and it is an exception to the
282
+ sentence and not to the rule: `skeleton.bounds_box` compares four world numbers — the
283
+ setup-pose bounding box each writer put in its header (rigc's is spine-core's
284
+ `getBounds` on the header's 1e-6 grid at float32 since issue #907; the editor's is its own arithmetic, up to
285
+ 0.0071 units from `getBounds` on the twelve examples) — exactly, and the block they
286
+ sit in gates nothing. The measure was called `stage_box` until #907, when `build`
287
+ stopped writing the stage there.
288
+
289
+ ⭐ **Declared is not the same as written down, and for the origin it is the
290
+ difference between a true reading and a false finding.** The editor omits a header
291
+ field at its default, so a box sitting at `0,0` exports as a `width` and a `height`
292
+ and no `x`/`y` at all — there is no other spelling for it. The measure reads that
293
+ omission as the `0` it means. The extent is still read exactly as stated: it is what
294
+ decides whether there is a box at all, so a missing `width` is an absent box rather
295
+ than a box of width zero.
296
+
297
+ ⛔ **And its ratios are not a score.** [`src/diff.ts`](../src/diff.ts) says so in the
298
+ type itself (*"Unweighted mean of the measures below. NOT a quality score"*), and the
299
+ report repeats it at the foot. It measures *agreement with a particular reference*,
300
+ which is the right question during transcription and the wrong question the moment the
301
+ task is to change something (§4.2, §4.3).
302
+
303
+ ⭐ **The name-agnostic groups are the ones to reach for when names are what differs.**
304
+ `bones` and `slots` each get a second pass with names thrown away — depth histogram,
305
+ degree sequence, shape histogram, declaration order of shapes, and for slots the
306
+ attachment types and bone-binding shapes by draw-order position. That is how you tell
307
+ *"the same rig with a different vocabulary"* from *"a different rig"*, and §4.2 is the
308
+ recipe built on it.
309
+
310
+ #### 1.3.1 Animations differ by name too — `--as`
311
+
312
+ `bones` and `slots` are matched name-agnostically by their own shape. Animations have
313
+ none: the candidate's `take01` and the reference's `arcs` are the same shot only
314
+ because somebody says they are. Two things say it —
315
+
316
+ ```bash
317
+ rigc diff work/t6/skeleton.json examples/6-arcs/export/6-arcs-pro.json --as take01=arcs
318
+ ```
319
+
320
+ — and, with no flag, **one animation each side**, which pairs by position because there
321
+ is exactly one reading of which shot is which. `--as` is repeatable, one pair each, and
322
+ the candidate's name goes on the left, as it does in `bonedist`'s correspondence file.
323
+
324
+ With the pairing in hand the `animations` section reports two figures like the other
325
+ two sections, the second over the paired shots — `duration`, `timeline_kinds`,
326
+ `key_counts`, `curve_kinds`, `draw_order`, `deform` — and the heading says which pairing
327
+ it used:
328
+
329
+ ```
330
+ animations mean 0.182 over 11 measures
331
+ 1.000 count 1/1 how many animations
332
+ 0.000 names 0/2 the animation names
333
+ …
334
+ animations (name-agnostic) mean 1.000 over 6 measures — the same two skeletons compared with names thrown away, paired by position: take01=arcs, the one animation each side carries
335
+ ```
336
+
337
+ Read that pair exactly as you read `bones`'s: **1.000 beside `names` 0.000** says the
338
+ shot is right and its name is yours. ⛔ `names` never moves into the second block, and
339
+ with two shots on each side and no `--as`, the block is **absent** rather than paired by
340
+ declaration order — a candidate that declares its two shots the other way round would
341
+ then read 0.000 across it and the report would be calling a guess a measurement.
342
+
343
+ ⚠️ An `--as` naming an animation a side does not have is **refused** with what that side
344
+ does have, and so is one that pairs the same animation twice. Neither is dropped
345
+ quietly: a typo that measured less than you asked for is a report about a pairing you
346
+ did not state.
347
+
348
+ ### 1.4 `check` — the instrument that does see coordinates
349
+
350
+ ```bash
351
+ rigc check --candidate <a compiled skeleton> --frames <a rendered frame set>
352
+ ```
353
+
354
+ `check` never opens a reference skeleton. It renders the candidate onto the frames'
355
+ own pixel grid, fits it there by its own drawn pixels, and compares. For ingest that
356
+ gives you a loop nothing else in the toolchain provides, in two steps:
357
+
358
+ ```bash
359
+ # 1. turn the foreign export into a reference frame set
360
+ rigc render --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
361
+ --fps 12 --max 256 --out ref3
362
+
363
+ # 2. measure anything at all against it
364
+ rigc check --candidate work/t3 --frames ref3 \
365
+ --texture-from examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas
366
+ ```
367
+
368
+ 📐 **Establish the positive control first.** Point `check` at the same export the
369
+ frames came from, and it has to read zero. It does:
370
+
371
+ ```bash
372
+ rigc check --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
373
+ --frames ref3/light
374
+ ```
375
+
376
+ **No run reproduces this:** abridged — the head's `atlas`, `frames`, `skin`, `scope`, `reference`, `content` and `in units` lines and its two ⚠️ notes are cut, so the `⤷ fit` line the run prints under `content` stands under `framed to`; the section's `frames` line and every `⤷` note are cut; and so is everything the run prints after `per-frame`
377
+
378
+ ```
379
+ rigc check
380
+ candidate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
381
+ framed to 256x116px 0.117628 px/unit world x[-573.3 .. 1603.0] y[-81.2 .. 908.9] (frames.json's own box — the candidate measured into it)
382
+ ⤷ fit x1.000000 offset +0.00, +0.00 px rms 0.00 px over 84 edge(s) union residual +0.00 x +0.00 px aspect +0.00% (declared, 1 pass(es), settled)
383
+ declared frames.json's own box: TAKEN, coincident — a fit there asks for 0.00 px, under the 1 px that separates a candidate in the frames' coordinates from one in its own, over 21 frame(s).
384
+
385
+ ── light — candidate animation "light", 12 fps ──
386
+ MAE mean 0.00 worst 0.00 (exact: none of the 21 compared frame(s) differs from the reference) (0..255 over the union alpha; over the whole frame, mean 0.00)
387
+ slot drift worst 0.4 px "pendulum" at f0012
388
+ per-frame all 20 adjacent pair(s) change by as much as the reference's own frames do
389
+ ```
390
+
391
+ Two things to take from the control, both of which you need before reading any real
392
+ number: the **framing** resolved to the frames' own box at 0.00 px, so the MAE is a
393
+ comparison of pictures rather than of framings; and **slot drift still reads 0.4 px at
394
+ MAE 0.00**, which is that instrument's own floor rather than a difference.
395
+
396
+ ⚠️ **`--texture-from` is not optional on ingest work, and the reason is structural.**
397
+ rigc's default atlas is **one region per page at the art's own resolution**; the editor
398
+ packs many regions onto one page, often at a reduced `scale:`. Two builds of
399
+ geometrically identical data therefore sample differently-scaled texels in every frame,
400
+ and that difference is a constant no key can move. `check` says so unprompted, and
401
+ `--texture-from <the atlas the frames were rendered through>` measures it: same
402
+ geometry, swapped texels. §2.3 is what the resulting number means, and which atlas your
403
+ build should be using in the first place.
404
+
405
+ ### 1.5 `explain` — for the specs you write, not the file you were given
406
+
407
+ 📌 **`explain` does not read a compiled skeleton, and it is worth knowing that up
408
+ front** so you do not go looking for a reading tool that is not there:
409
+
410
+ ```bash
411
+ rigc explain examples/spineboy/export/spineboy-pro.json
412
+ ```
413
+
414
+ ```
415
+ rigc: give either --cut <name> --cuts <cuts.json>, or --rig/--motion/--out
416
+ ```
417
+
418
+ It takes `--rig`, `--motion`, `--out`, and optionally `--manifest`, `--images` and
419
+ `--atlas-in` — `build`'s spec-reading flags, and not the ones that decide what `build`
420
+ *writes* (`--pack`, `--page-size`, `--padding`, `--page-edges`, `--copy-images`) or the one that gates
421
+ (`--profile`). It prints the resolved account
422
+ of **your** two spec files, and never gates. Which makes it a §2 instrument rather
423
+ than a §1 one — the thing you run to compare what you transcribed against the export
424
+ you transcribed it from, by eye:
425
+
426
+ ```bash
427
+ rigc explain --rig bench/transcriptions/3-timing-and-spacing/3-timing-and-spacing-ess.rig.json \
428
+ --motion bench/transcriptions/3-timing-and-spacing/3-timing-and-spacing-ess.motion.json \
429
+ --out work/x3
430
+ ```
431
+
432
+ ```
433
+ stage 945.1005 x 815.4317 (spine 4.3.13)
434
+
435
+ bones (spine world: y up)
436
+ root parent=- x=0 y=0
437
+ square parent=root x=380.7311 y=78.8596
438
+ bone parent=root x=204.753 y=708.0989 rotation=180
439
+
440
+ slots (array order IS the draw order)
441
+ pendulum bone=bone setup=pendulum color=ffffffff attachments=[pendulum]
442
+ square bone=square setup=square color=ffffffff attachments=[square]
443
+
444
+ animations
445
+ heavy declared=5.333333s loop=true
446
+ bone.rotate 20 key(s)
447
+ t=0 value=0 bezier[4]
448
+ t=0.233333 value=3.31738 bezier[4]
449
+ …
450
+ ```
451
+
452
+ ⚠️ **`--out` is required and nothing is written to it.** The flag is shared with
453
+ `build`'s parser; `explain` prints and exits. Passing a directory that does not exist
454
+ is fine — it is not created.
455
+
456
+ ---
457
+
458
+ ## 2. Getting specs out of a skeleton
459
+
460
+ Everything in §1 reads. To **change** anything you need specs. There are two routes
461
+ to them and you should almost always take the first.
462
+
463
+ ### 2.0 `ingest` — let the tool write them
464
+
465
+ ```bash
466
+ rigc ingest examples/spineboy/export/spineboy-ess.json --out specs/ --art none
467
+ rigc build --rig specs/rig.json --motion specs/motion.json --atlas-in examples/spineboy/export/spineboy.atlas --out spine
468
+ rigc diff spine/skeleton.json examples/spineboy/export/spineboy-ess.json
469
+ ```
470
+
471
+ `ingest` reads the skeleton — **only** the skeleton — and writes `rig.json`,
472
+ `motion.json` and `findings.json`. The contract is an equality rather than a
473
+ rulebook: `build(ingest(x))` is `x`, byte for byte on `skeleton.json`, and the
474
+ atlas comes back with the same region blocks (as a multiset — the page order is in
475
+ no field of the file).
476
+
477
+ 📊 **For an editor export the claim is weaker, and measured.** Every
478
+ `examples/*/export/*.json` ingested with `--art none`, rebuilt through the pack
479
+ beside it, and `diff`ed against the file it was read from: **12 of 12 come back with 0
480
+ blockers and 1.000 on all 54 ratio-bearing measures and all 6 reported ones** — and on
481
+ all **nine value measures** too, over **195,363** compared values. Byte
482
+ identity is not the claim there and the reason is the input, not the round trip — §2.3
483
+ has the pass line, and what the value measures do and do not reach. ⚠️ Which pack is "the one beside it" is
484
+ resolved rather than guessed, for §0.2's reason: `spineboy/export` holds two, and
485
+ `spineboy-run.atlas` covers neither skeleton in it.
486
+
487
+ **What it will not do is invent.** Everything the spec format cannot hold is a
488
+ finding with a code — `BLOCK` for a construct the rebuild will be missing, `JUDGE`
489
+ for what a skeleton does not carry and the rebuild has to state (the two values
490
+ below, and whether a muted constraint is the consumer's), `LOSS` wherever the source's spelling and
491
+ rigc's differ on purpose (a number rigc re-derives, a field the spec has no home for, or
492
+ a default the source left to the format and the rebuild writes out). A blocker exits
493
+ non-zero and still writes both files. **Every code it can print has a row at the end
494
+ of this section**, with its gutter, its effect on the exit code and what to do.
495
+
496
+ ⛔ **And it reads one generation.** Spine data is locked to the generation that
497
+ exported it, and a mismatch is silent rather than loud: 4.3 takes constraints from the
498
+ top-level `constraints` array alone, so a 4.0–4.2 file's `ik`/`transform`/`path`/
499
+ `physics` arrays load as nothing at all. So `ingest` reads
500
+ `skeleton.spine` before it reads a field of the file, and a file from another
501
+ generation is a blocker naming that generation and counting, **on that file**, what a
502
+ 4.3 reader loses by it. Reading such a file with *that generation's own* defaults is a
503
+ different job, and it is not in this tool, which is why the finding points
504
+ at the policy rather than implying the file was read.
505
+
506
+ **Two values are not in a skeleton**, so `ingest` asks rather than guesses:
507
+
508
+ - **the stage** (`skeleton.width`/`height`) — a file that declares none is **carried as
509
+ declaring none**: the rig spec states `"width": null, "height": null` (§2.1 step 3's
510
+ spelling), the rebuild emits a header with none of `x`/`y`/`width`/`height`, and it
511
+ is the file that was read, byte for byte. No finding is recorded, because nothing
512
+ was lost and nobody decided anything. `--stage x,y,w,h` is how a caller *adds* a box
513
+ to such a file, and that is a `NO_STAGE` **judgement**. All twelve exports in the
514
+ fetched corpus carry a box, and `ingest` reads it straight through into the rebuild's
515
+ stage. A **rigc build**'s stage is read instead from the `skeleton.model.json` beside it,
516
+ when that document's `spine.sha256` is the skeleton's digest — its header is the setup-pose
517
+ bounding box — and an export's from its header box. 🔁 What an export's header carries is its setup-pose bounding box; it is the
518
+ only box the file has, so it becomes the rebuild's stage, and the rebuild's own header
519
+ is computed again — `build` writes the setup-pose bounding box there, never the stage
520
+ (issue #907). The stage cannot be *derived*: it is the working area the art was
521
+ painted in, and no pose of the rig states it. 🔸 **Half a stage is a `NO_STAGE` blocker**: an origin with no
522
+ extent, or one extent without the other, declares no stage and is not the absence
523
+ either, and the rig spec holds a stage as four fields or none. It is also the value that costs least to get wrong: `diff`
524
+ reports the header's box as two measures of its own (`bounds_present`, `bounds_box`) and they are
525
+ `(reported)`, so no score reads them and an absurd box is green nearly everywhere.
526
+ ⛔ **The flag is refused beside a box the file states** — two sources for one value,
527
+ both named, and the file is the record of what was measured;
528
+ 📦 **`--stage-box <slot>` reads the stage a rig carried in its Spine files** (issue
529
+ #1168): a rig whose spec states `skeleton.stageBox` ([AUTHORING §3.1](AUTHORING.md))
530
+ ships its stage as a bounding box in that slot, and a caller who has only
531
+ `skeleton.json`, the atlas and its pages names the slot. The box's four corners are
532
+ read as the stage in place of the header's box, and the rebuilt spec asks for the same
533
+ box rather than transcribing it, so `build` writes it again from the stage — a rigc
534
+ build carrying one rebuilds byte for byte. Only a box `build` could write back is read:
535
+ a slot the skeleton lacks, a slot on a bone other than an unmoved root, a slot holding
536
+ anything but one bounding box in the `default` skin, a box not four unweighted corners
537
+ of one axis-aligned rectangle — each is refused by name, as is `--stage` beside the
538
+ flag, and a model document beside the skeleton stating another stage. A box `build`
539
+ spells differently (its corners in another order, the editor's `color`) is the lossy
540
+ `STAGE_BOX_REWRITTEN`. Without the flag no slot is read as the stage because of its
541
+ name, and the box is transcribed as the attachment it is;
542
+ - **each animation's duration** — the format has no such field. The largest key time
543
+ is used, stated in the motion spec's `note`, and recorded as a finding per
544
+ animation. Edit it if you know the real number.
545
+
546
+ 🎛️ **And one statement is not in a skeleton either: who turns a muted constraint on.**
547
+ An ik or transform constraint
548
+ resting at 0 on every mix it reads, that no animation keys above 0, is either a
549
+ leftover that moves nothing or a dial a game sets from code — and the two export as
550
+ the same bytes. `build` refuses the shape by name (`A47`/`A48`), so without a
551
+ statement the rebuild of a file whose game switches that ik on at runtime is refused.
552
+ So `ingest` reads it the way under which the file is correct: it writes the
553
+ constraint into the rig spec's `invariants.consumerDrivenMix` ([AUTHORING
554
+ §3.7](AUTHORING.md)), with a `why` saying the entry is `ingest`'s reading, and prints a
555
+ `CONSUMER_DRIVEN_MIX` **judgement** naming it. The rebuild is the same bytes — the
556
+ declaration is a statement to the gate, never emitted — and it gates green, with the
557
+ constraint SKIPped by name rather than measured. It is the only field of `invariants`
558
+ `ingest` ever writes, and the rig spec's `note` says so where it is present.
559
+ ⚠️ **None of the twelve exports carries the shape**: every constraint they rest muted
560
+ is keyed up by an animation, spineboy's aim rig being the idiom, so `ingest` prints no
561
+ such line and writes no declaration for any of them.
562
+
563
+ And two flags for what the skeleton also does not encode: `--art loose` (the default)
564
+ names an `image` per attachment resolved against loose PNGs, `--art none` states
565
+ `width`/`height` for `build --atlas-in` — and for `explain --atlas-in`, which is the
566
+ same pack read for a report rather than for an artifact: `explain` **poses** the rig
567
+ to print its `DEFORM` block, a pose resolves every attachment against an atlas, and a
568
+ size-only spec carries none of its own, so without the flag that pair is refused by
569
+ name rather than posed; and
570
+ under `loose`, `--images <dir>` writes
571
+ the rig spec's own images directory relative to `--out`, so the rebuild is a plain
572
+ `build --rig … --motion … --out …` rather than one carrying `--images` forever. It is
573
+ refused together with `--art none`, which writes no `image` for a directory to be the
574
+ base of. [AUTHORING §0.3](AUTHORING.md) is the loop in full.
575
+
576
+ 📝 Both written specs carry a `note` saying they are decompiled and naming the file
577
+ they came from. Leave it there — §2.4 is why.
578
+
579
+ #### Every finding code, and what to do about it
580
+
581
+ A finding line reads `<gutter> <CODE>: <where> — <detail>`, and the code is the part
582
+ that is the same on every run. This table is the whole set: which gutter it prints
583
+ under, whether it changes the exit code, what it means, and what to do about it.
584
+ **Nothing here refuses the file** — a construct the spec format cannot hold is recorded
585
+ rather than rejected — so an exit of 1 means *a blocker was recorded*, and both specs
586
+ are on disk either way. (The one thing `ingest` does refuse outright is an option that
587
+ contradicts the file, which is not a finding: `--stage` beside a box the skeleton
588
+ declares — and, under `--stage-box <slot>`, a slot that holds no box `build` could
589
+ write back, `--stage` beside it, or a model document beside it stating another
590
+ stage (§2.0's stage box, below).)
591
+
592
+ | code | gutter | exit | what it means | what to do |
593
+ | --- | --- | --- | --- | --- |
594
+ | `ANIMATION_GROUP` | `BLOCK` | 1 | the animation carries a group the motion spec has no home for. The detail names the ten it does carry. `drawOrderFolder` is the group to know about: the runtime reads it and builds a timeline from it, and no export in this corpus carries one | transcribe that group by hand (§2), or accept that the rebuild does not carry it |
595
+ | `ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so on the one type left it reads `ATTACHMENT_POINT`. rigc emits region, mesh, linkedmesh, boundingbox, clipping and path, and `point` is the one deferred type | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what a deferred type would carry |
596
+ | `ATTACHMENT_LINK_GEOMETRY` | `LOSS` | 0 | a **linked mesh** that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by nothing at all and the attachment draws the geometry its `source` names; the rig spec has no home for them either, because `build` refuses geometry on a link by name. The detail lists the keys and the source | nothing. The rebuild is the mesh the runtime was already drawing — and if those keys were the geometry you meant, take `source` off and author it as a mesh of its own. `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` is the same fact at the gate |
597
+ | `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | a `sequence` block — a numbered image series — that the rig spec cannot say **as written**. A well-formed block on a region, mesh or linked mesh is carried field for field, with no `image` on the loose route (the frames `<path><number>` are the art), and a `sequence` timeline with it; what is left here is a block the parser reads into a series other than the one written — no `count` (0 regions), a `setup` past the end (clamped), a fraction — or one on a `boundingbox`, `clipping` or `path`, where the parser never reads it. The detail quotes the block and says which | the rebuild draws the single region the attachment names. Fix the block in the source — a `count` is the usual one — and ingest again |
598
+ | `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline that is neither `deform` nor `sequence`. Both are carried, and `readAnimation` tests an attachment timeline for exactly those two names and ignores anything else (`SkeletonJson.js:1147-1201`) — so what reaches this line is a name outside the format, which no player plays either | fix the timeline's name in the source, or accept that the rebuild does not carry it |
599
+ | `BONE_FIELD` | `BLOCK` | 1 | a bone field with no rig-spec field, so it is dropped. A 4.0/4.1 export spelling `transform` where 4.3 spells `inherit` lands here; so does a misspelling | check the name against AUTHORING §3 first — a typo and an unsupported field read exactly the same |
600
+ | `BONE_TIMELINE` | `BLOCK` | 1 | a bone timeline the motion spec has no track for. The detail names the eleven it has, read off the table. Every bone timeline the runtime plays has a track, `inherit` included — the eleventh case of the runtime's own bone switch, a stepped mode per key — so this is reachable only for a name the **parser** throws on too (`Invalid timeline type for a bone`), the position `PHYSICS_TIMELINE` is in | check the spelling; there is no bone timeline left for the rebuild to be missing |
601
+ | `CONSTRAINT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a constraint, with its type named beside it | as `BONE_FIELD` |
602
+ | `CONSTRAINT_KEY_RESTATED` | `LOSS` | 0 | an `ik` or `transform` track whose keys do not all state the same fields. The motion spec takes one field set per track, so a field **any** key states is written on **every** key of the spec at the value the parser would have read there — and the line is printed only for a value the **file** will still carry. The emitter leaves a value out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so a restated default is the source's own text again and says nothing; what is left is a value the table has no row for, or a transform key's `mixY` the source left out beside a `mixX` that is not 1, which the emitter keeps because that is where the editor writes it (§10.6c's *only at 1*). Measured on the twelve exports: 0 tracks print it | nothing. Same values, and where this prints, a larger file — the rebuild plays what the source plays |
603
+ | `CONSUMER_DRIVEN_MIX` | `JUDGE` | 0 | an `ik` or `transform` constraint resting at 0 on every mix it reads — an ik's `mix`; a transform's mixes for the `to` properties it declares — that no animation keys above 0, reading every key the way `A47`/`A48` do: an omitted mix is the parser's 1 and a Bezier handle above 0 lifts a 0 → 0 pair. Nothing in the file ever switches it on, and the file cannot say whether that is a leftover or a mix a game sets from code, so the rig spec **declares** it in `invariants.consumerDrivenMix` and the rebuild's gate SKIPs it by name. It is a judgement for `DURATION`'s reason: a statement the skeleton does not carry, made and printed — and not a `LOSS`, because the rebuilt skeleton is the source's bytes. A transform that declares no `to` at all is not a candidate: `A48` refuses it with its own sentence, which no declaration answers | nothing, if a game drives that mix. If it is a leftover, delete the entry and rest a mix it reads above 0 — or remove the constraint — and the gate measures it again |
604
+ | `CONSTRAINT_TYPE` | `BLOCK` | 1 | a constraint whose `type` is none rigc knows, so the whole constraint is dropped rather than approximated | the rebuild has no such constraint; check the spelling before assuming the type is unsupported |
605
+ | `DURATION` | `JUDGE` | 0 | skeleton JSON has no duration field at all. The largest key time is used, which is what a runtime plays to — and wrong for an animation that holds its last pose past its last key | if you know the real number, edit `duration` in the motion spec. It costs nothing: the declared duration is checked against the compiled keys |
606
+ | `GENERATION_UNKNOWN` | `BLOCK` | 1 | `skeleton.spine` names no generation rigc knows, or the header states none at all. A version is read as its LEADING `major.minor` token — a down-export writes `4.0-from-4.1.24`, which is 4.0 data from a 4.1 editor — and it is never rounded to the nearest generation: rounding `3.8.99` up hands 3.8 data to a 4.2 runtime, and it poses as NaN | check the string against the file you were handed. A real generation rigc does not list is worth reporting, with the string beside it |
607
+ | `GENERATION_UNSUPPORTED` | `BLOCK` | 1 | the file is Spine data from another generation and this reader reads 4.3. The detail names the generation, the string it was read from, and what a 4.3 reader loses on **this** file: constraints parked in the top-level `ik` / `transform` / `path` / `physics` / `slider` arrays 4.3 folded into `constraints` and this reader never opens, bones carrying `transform` where 4.3 spells `inherit`, and physics constraints omitting `inertia` / `damping`, whose default is not the same number in 4.2 as in 4.3 | re-export the file as 4.3 from an editor of its own generation, or transcribe it by hand (§2). Reading it with **that generation's** defaults is not in this tool |
608
+ | `HEADER_BOOKKEEPING` | `LOSS` | 0 | a header field the editor writes and the rig spec has no home for — `hash`, the editor's project hash, which is a value about a file rigc did not write. Dropped, and nothing reads it back. `audio` is not on this line: the rig spec states it ([AUTHORING §3.1](AUTHORING.md)) and `ingest` carries it, `null` included | nothing. It is one of the two declared exceptions §2.3's pass line is stated apart from |
609
+ | `HEADER_ORIGIN` | `LOSS` | 0 | the source declares an extent and omits `x`/`y`. Inside a declared extent an omitted origin **is** 0, so the spec states it — and the rebuild then spells two fields the source did not | nothing. Same box, different bytes — which is why byte identity is not the claim for an export that takes this branch |
610
+ | `HEADER_REDERIVED` | `LOSS` | 0 | `skeleton.spine`: the rebuild writes the version of the runtime rigc links. The line says whether that is the same string the source states | nothing — but read the line: a 4.2 export rebuilds as 4.3 in that one field, and a source from another generation raises `GENERATION_UNSUPPORTED` beside it, which is the blocker about the DATA rather than about the string |
611
+ | `IK_KEY_FIELD` | `BLOCK` | 1 | a key field on an `ik` timeline that is not part of its shape | check the spelling; an unknown field is dropped from the rebuilt track |
612
+ | `NO_STAGE` | `BLOCK` `JUDGE` | 1 | the skeleton declares no stage, and one of two things follows. A **judgement** — exit 0 — when `--stage x,y,w,h` supplied a box, because nothing measured the box you gave it. A **blocker** when the header states **half** a stage — an origin with no extent, or one extent without the other — which the rig spec cannot hold; the detail names the fields it states. A header with **none** of the four is not a finding at all: it is carried as `"width": null, "height": null` and rebuilds byte for byte | for the judgement, nothing if the box came from the project the file came from. For the blocker, supply the box with `--stage`, or take the stray field(s) out of the source and the absence is carried. It cannot be derived: posing the rig gives the animated extent, which is a different number |
613
+ | `PATH_TIMELINE` | `BLOCK` | 1 | a path-constraint timeline the motion spec has no track for — it carries position, spacing and mix | transcribe it, or accept that the rebuild plays nothing there |
614
+ | `PHYSICS_DRIVES_NOTHING` | `LOSS` | 0 | a physics constraint none of whose `x`, `y`, `rotate`, `scaleX`, `shearX` is above 0 — absent, or stated at 0 or below. `PhysicsConstraint.update` applies a component only above 0 (`PhysicsConstraint.js:112`), so it moves no bone, and `build` refuses exactly that shape by name at `A23_PHYSICS_CONSTRAINT_EFFECTIVE`. The rig spec **omits** it, together with every timeline keyed to it (a track naming it would be an unknown constraint to the rebuild, refused at compile) and its place on any skin's `physics` list; the detail names each, and the values it did state. Measured on a generated rig through spine-core, posing the source with and without such a constraint differs by **0** on every bone world value — and by at most 9e-8 when it sits on the root, which is the runtime's `modifyWorld` recomputing a local transform it had no reason to, not a component. ⚠️ **One thing does move:** a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation, and the detail says which animation and both lengths | nothing, if it was meant to do nothing. If it was meant to jiggle, the file never said so: give it the component it should drive and it is carried like any other. Where the detail names a shortened animation and the length matters to whatever loops it, key something at the length it had |
615
+ | `PHYSICS_GLOBAL_REACHES_NOTHING` | `LOSS` | 0 | a physics timeline keyed under the **empty** name — the one that names no constraint, which the runtime applies to every physics constraint declaring that property global (`"strengthGlobal": true` for `strength`; `reset` resets every physics constraint and asks no flag) — in a file where no physics constraint the rebuild carries declares it. The motion spec spells that timeline `"physics": "*"` and `build` refuses one that reaches nobody by name, so the rig spec **omits** it: in the source it walked every constraint and wrote into none. A constraint `PHYSICS_DRIVES_NOTHING` omitted counts as not carried — it was the only thing such a timeline could reach, and it moved no bone. ⚠️ As with that row, a duration is the last key an animation has left, so an omitted timeline that held the last key shortens the rebuilt animation and the detail says both lengths. An unnamed timeline that **does** reach a constraint is not a finding at all: it is carried as `"*"` and rebuilt under the empty name byte for byte | nothing, if it was meant to do nothing. If it was meant to drive the constraints, the file never said which: set `"<property>Global": true` on them in the rig spec and key it as `"physics": "*"` |
616
+ | `PHYSICS_TIMELINE` | `BLOCK` | 1 | the same for a physics constraint, whose eight the motion spec carries in full — so this is reachable only for a name the **parser** falls through too | as `PATH_TIMELINE` |
617
+ | `SLIDER_TIMELINE` | `BLOCK` | 1 | the same for a slider, which carries time and mix | as `PATH_TIMELINE` |
618
+ | `SLOT_FIELD` | `BLOCK` | 1 | as `BONE_FIELD`, on a slot | as `BONE_FIELD` |
619
+ | `SLOT_TIMELINE` | `BLOCK` | 1 | a slot timeline the motion spec has no track for. The spec has a track for all six the format has, so what still reaches this line is a name **outside** the format — `sequence` written on a slot rather than an attachment is the likeliest — and the detail says so: the runtime's own reader throws `Invalid timeline type for a slot` on it, so no player loads that file either. The detail names the tracks the spec does have and, when the format has any it lacks, those too, **both read off the tables**. `rgb` and `alpha` are carried under their own names and on their own key times, never folded into one `rgba` — that would state each channel at the other's key times, a value nobody keyed | fix the timeline's name, or accept that the rebuild plays nothing there |
620
+ | `SPEC_REFUSED` | `BLOCK` | 1 | the specs were written and **rigc's own parser refuses one of them** — the detail carries that refusal word for word, after the file and the spec it is about. It is the one finding that is not about a single construct: it is whatever `parseRigSpec` or `parseMotionSpec` names, from a shape the format holds and the spec cannot say (a constraint that is `skinRequired` under no skin) to a defect in this decompiler | read the quoted sentence against the skeleton: it names the object. Both specs are on disk for exactly that, and `build` will refuse them until the shape has a spelling — [AUTHORING §5.1](AUTHORING.md) is the list of what a parser says |
621
+ | `STAGE_BOX_REWRITTEN` | `LOSS` | 0 | under `--stage-box <slot>`: the bounding box read as the stage states its corners in another order than `build` writes them (bottom-left first, counter-clockwise), or a `color` — the editor writes one on every box it exports. The rebuilt spec asks for the box (`skeleton.stageBox`), and `build` writes it from the stage, so the rebuild carries the same rectangle spelled `build`'s way. The detail names what changes | nothing. The stage is the same four numbers; only the box's spelling moves |
622
+ | `TIMELINE_FIELD` | `BLOCK` | 1 | a key field on a bone, path, physics or slider timeline that is not part of that timeline's shape. On an `inherit` key that includes a `curve`: the parser reads `time` and `inherit` there and nothing else, and `build` refuses a curve on that track by name | check the spelling; the field is dropped from the rebuilt key |
623
+ | `TIMELINE_KEY_RESTATED` | `LOSS` | 0 | an editor omits a channel that equals the parser's default, and the motion spec's `v` is positional, so the omission is written out at that default **in the spec** — and the line is printed only where the **file** will carry it too. The emitter leaves a channel out wherever it is the one the parser reads without it ([AUTHORING §10.6c](AUTHORING.md)), so on every key kind with a row the rebuild is the source's own text and nothing is said — measured on the twelve exports, 0 tracks print it. What still prints it is a timeline whose keys have no row (`shearx`, `alpha`, path `spacing`, …), and an `inherit` key: one that omits the mode is written as `normal` — the parser's default — and one spelled with a capital first letter (`NoScale`) as the editor's `noScale`, the same mode either way | nothing. The same values the runtime reads, spelled out — a larger file and the same animation |
624
+ | `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
625
+
626
+ ⚠️ **An attachment's `name` has no row, because nothing about it is lost.** `ingest`
627
+ carries a stated `name` verbatim — equal to its key or not, exactly as the source spells
628
+ it — and writes none where the source states none, and `compile` emits exactly what the
629
+ spec states.
630
+
631
+ ### Transcription — the route that made a foreign skeleton yours
632
+
633
+ ⚠️ **The rest of §2 is transcription by hand, and the reading it produces is the
634
+ right one** — it is what an author does *after*
635
+ `ingest`, and it is what to fall back on for the constructs `ingest` reports as
636
+ blockers. The numbers come out of the JSON into a rig spec and a motion spec by hand,
637
+ and `build` emits a new skeleton from those.
638
+
639
+ What you get for it is that the file becomes editable by declaration — a pivot move
640
+ is two numbers in a spec (§4.1) and a new animation is an added block (§4.3), rather
641
+ than a hand-edit of emitted JSON with nothing checking it. That is what
642
+ `ingest` hands you in one command; the sections below are how to read and change what
643
+ it hands you, and every rule in them applies to a spec `ingest` wrote.
644
+
645
+ A file from another Spine generation is the one case where transcription is not the
646
+ first fallback: migrate it through the editor of its own generation first, as
647
+ [GENERATIONS.md](https://github.com/firejune/rigc/blob/main/docs/GENERATIONS.md) §4
648
+ states.
649
+
650
+ ### 2.1 The workflow
651
+
652
+ 1. **Read the skeleton first, with `validate` and `render`.** The `SKIP` list is your
653
+ feature inventory (§1.1); the contact sheets are what the animations actually do.
654
+ Knowing there is no deform timeline before you start is worth more than discovering
655
+ it in the eleventh hour of transcribing one.
656
+ 2. **Get the loose art, at the size the export declares.** rigc measures PNGs rather
657
+ than trusting a size you typed (AUTHORING R5), so the art has to *be* the right
658
+ size. Take the target from the export's own attachments —
659
+ `3-timing-and-spacing` declares `"width": 745, "height": 212` for `pendulum`, and
660
+ the loose `pendulum.png` beside it is exactly 745×212. ⚠️ Do **not** take it from
661
+ the atlas region bounds: that page carries `scale: 0.5`, so `pendulum`'s bounds read
662
+ `373, 106`. Two numbers for one part, and the attachment's is the one in world
663
+ units. `--atlas-in` does that division for you (§2.3), but it can only land
664
+ within the pack's own rounding — by hand, off the attachment, it is exact.
665
+ 3. **Transcribe the rig spec: header, bones, slots, skins.** Bones parents-first; the
666
+ `slots` array *is* the draw order (AUTHORING R4), so its order is data you are
667
+ copying and not a detail. Leave `invariants` out entirely — it describes rigc's own
668
+ formations, and an absent field makes an archetype assertion `SKIP`, never pass
669
+ (AUTHORING §3.7).
670
+
671
+ 📌 **Transcribe the export's empty slots too** — the ones no skin fills anywhere.
672
+ Such a slot still holds an index in the array, and everything below it is counted
673
+ from that index. Write it as `{ "name": …, "bone": … }` with no `attachment`, or
674
+ with `"attachment": null` if you prefer to say it out loud; either way it comes
675
+ back. If a transcription's `slots.count` is under 1.000, a missing empty slot is
676
+ the first thing to check.
677
+
678
+ ⚠️ **No skeleton in `examples/` has one**: all twelve exports fill every slot they declare from some skin. What they
679
+ *do* carry is the neighbouring shape — a slot a skin DOES fill whose setup pose
680
+ shows nothing (34 of `spineboy-pro`'s 52 slots). Both are written the same way in
681
+ the file: `attachment` simply absent.
682
+
683
+ ⚠️ **If the export's `skeleton` block carries no `x`/`y`/`width`/`height`, write
684
+ `"width": null, "height": null` and do not invent one** — which is also what
685
+ `rigc ingest` writes for such a file. A made-up stage is a number no gate refuses;
686
+ `A14` and `A19` measure against the stage you state, and `diff`'s header block
687
+ reports `skeleton.bounds_present` and `skeleton.bounds_box` — your build's bounding
688
+ box, which you do not author, against the source's. Copy the four numbers when they are there;
689
+ state the absence when they are not.
690
+ 4. **`explain`, then `build`.** `explain` first, because it prints what you wrote in a
691
+ shape you can compare against the export by eye (§1.5) and it never gates. Then
692
+ `build` under `--profile spine`.
693
+ 5. **Transcribe the motion spec, one animation at a time**, and `build` after each.
694
+ 6. **Close it with `diff` and `check`.** `diff` for structure; `check` against frames
695
+ rendered from the export for geometry; `--texture-from` to attribute the floor.
696
+
697
+ ### 2.2 One feature family at a time
698
+
699
+ 📌 **Transcribe by *kind*, not by animation.** All the bones, then all the slots, then
700
+ all the attachments, then one timeline kind across every animation. Two reasons, and
701
+ the second is the one that costs a day:
702
+
703
+ - A whole animation touches every feature the format has, so *"animation 1 of 6 done"*
704
+ means you have hit every unsolved problem at once and solved none of them cleanly.
705
+ - **`build` is all-or-nothing and emits only after green** (AUTHORING §0). A partial
706
+ transcription of one kind still builds; a half-transcribed animation may not build at
707
+ all, and then you are debugging your own incomplete work rather than the format.
708
+
709
+ ⚠️ **When a kind turns out not to be expressible, stop and say so — that is a finding,
710
+ not a blocker to route around.** [SURVEY_2026-08-22.md](https://github.com/firejune/rigc/blob/main/docs/SURVEY_2026-08-22.md) is the
711
+ per-skeleton survey of exactly this. ⇒ Check the survey for your feature before
712
+ concluding either way, and if it is genuinely absent, the shape of the answer is *"this export
713
+ uses X, which the motion spec cannot say"* with a pointer — not a silent
714
+ approximation.
715
+
716
+ 📎 **An editor export's curves usually need the raw `curve` escape hatch.** A named
717
+ easing is one curve reused; an export carries a different bezier per key per channel,
718
+ which no name can say. The motion spec's raw form takes absolute `(time, value)`
719
+ control points verbatim (AUTHORING §4.5). Named easings stay the right default for
720
+ motion you are *authoring* — this is the one case the escape hatch exists for, and the
721
+ 3-timing transcription's own `note` says so.
722
+
723
+ ### 2.3 What "byte-identical" can and cannot mean
724
+
725
+ State the ambition in the right units, because three different things get called
726
+ "identical", and the third is reachable only in a form worth stating exactly.
727
+
728
+ | Ambition | Reachable? | What it costs, and what it proves |
729
+ | --- | --- | --- |
730
+ | **Structural agreement** — same bones, slots, attachments, timelines, key counts, curve kinds | ✅ yes, and `diff` measures it | the 3-timing transcription reads **1.000 on all 54 measures**. Aim here first |
731
+ | **Geometric agreement** — the same drawn pixels, allowing for the atlas | ✅ yes, and `check` measures it | see below |
732
+ | **Byte-identical JSON** | ✅ **for a rebuild, in canonical form, apart from `hash`, `spine` and the header's box** — the pass line below | a **rebuild** of an editor export (`ingest`, then `build`) is the export. A **transcription** by hand is not held to it: what differs there is what a person chose to write, not the emitter |
733
+
734
+ ⭐ **The pass line of the byte round trip, stated once:** `build(ingest(x))` of an
735
+ editor export `x` is **identical to `x` in canonical form, apart from `hash` and
736
+ `spine`**, and from the header's box (below) — canonical form being `JSON.stringify(JSON.parse(text), null, 2)` of each
737
+ file, which keeps every number as parsed, every key in its order and every omitted key
738
+ omitted, and drops only whitespace and the exponent's spelling (an export setting, not
739
+ a property of the rig). It holds on all twelve exports under `examples/`; for a
740
+ skeleton **rigc** emitted, `build(ingest(x))` is byte-identical outright.
741
+
742
+ The two **declared exceptions** are the header keys the editor writes and the rig spec
743
+ has no field for, by design, and each has its finding:
744
+
745
+ | Header key | Example, rebuild vs export | Why it is an exception |
746
+ | --- | --- | --- |
747
+ | `hash` | `undefined` vs `"VFWbaK2UoCM"` | the editor's project hash — a value about a file rigc did not write, so a spec that carried it would be claiming an export it did not come from. `HEADER_BOOKKEEPING` |
748
+ | `spine` | `"4.3.13"` vs `"4.3.75-beta"` | the version of the runtime rigc links, stamped by design and re-checked by `A16`. `HEADER_REDERIVED` |
749
+
750
+ 🔁 **The header's box is the third difference, and it is not a key the spec lacks** (issue
751
+ #907). The rig spec carries the export's `x`, `y`, `width`, `height` — as the rebuild's
752
+ stage — and the rebuild does not copy them back: its header carries the setup-pose bounding
753
+ box `build` computes, which is spine-core's `getBounds` over the export on the header's grid
754
+ (the rebuild draws the export's vertices to the bit, so its box is the export's box), where
755
+ the editor wrote its own arithmetic, at most 0.0071 units away on the twelve —
756
+ `spineboy-pro`'s rebuild writes `-188.63641, -7.936074, 418.453, 686.19934` against the
757
+ editor's `-188.6338, -7.939564, 418.4499, 686.2023`. The suite holds each rebuild's box to
758
+ `getBounds` of its source at tolerance 0 (`IG97`) and reads every other byte against a copy
759
+ of the export carrying that box.
760
+
761
+ 🔢 **The rest of the header, and every omitted default, comes back as the export
762
+ spells it.** The **header's `audio`** is a stated value like `images`, and the rig
763
+ spec carries it ([AUTHORING §3.1](AUTHORING.md)). The emitter leaves out exactly the
764
+ keys the parser reads the same way without them ([AUTHORING §10.6c](AUTHORING.md)).
765
+ One shape is left and it is deliberate: a box at the origin, which the editor writes as
766
+ `width`/`height` with no `x`/`y` and rigc spells whole. The 4.3 JSON reader has no
767
+ default for the origin — an absent one loads as `undefined`, a written `0` as `0` — so
768
+ it is not a key the parser reads the same way without it, and the row is not in the
769
+ table; `LOSS HEADER_ORIGIN` names it. No file in the fetched corpus takes that
770
+ branch: all twelve declare an origin away from 0.
771
+
772
+ 🔢 **Numbers are spelled as the editor spells them** — each as the shortest decimal
773
+ naming its float32 — and `ingest` carries every number as the double it parsed. The
774
+ whitespace of an export is not compared at all — it is an export setting,
775
+ pretty-printed in the examples and one line from the command line.
776
+
777
+ ⇒ **`diff` at 1.000 is a statement about structure** — counts, names, parentage,
778
+ order, timeline kinds, key counts, curve kinds — and **not the values inside the
779
+ keys**, which is why a moved value is invisible to it.
780
+
781
+ ⭐ **The values are measured by a second comparison rather than by `diff`.**
782
+ Structure at 1.000 is silent about the numbers inside it: a decompiler that halved
783
+ every rotation, dropped every bone's `length` or mirrored every vertex would read
784
+ 1.000 on all 54 measures and on every `(reported)` one. So the example round trip is
785
+ also compared **value by value**,
786
+ with the format's defaults taken from the parser rather than from a table — both files
787
+ are read through `spine-core` and the parsed forms are compared path by path, under a
788
+ tolerance that is the sum of the 1e-6 grid rigc's closed-form models are evaluated on
789
+ — the one absolute grid it emits on — and one float32 step of the runtime's
790
+ storage. Nine measures — here is the `6-arcs` export's, wrapped to fit this
791
+ page:
792
+
793
+ ```
794
+ values: 9/9 measure(s) at 1.000 over 13977 compared value(s); skeleton 1.000 ·
795
+ bones 1.000 · slots 1.000 · attachments 1.000 · constraints 1.000 · events 1.000 ·
796
+ key_times 1.000 · key_values 1.000 · curves 1.000
797
+ ```
798
+
799
+ Over the twelve exports that is **195,363 values** compared, and all twelve read
800
+ 1.000 on all nine.
801
+
802
+ `docs/BENCHMARK.md`'s *The nine value measures* is the full statement. What it still
803
+ does **not** cover, in the same breath:
804
+
805
+ | Still uncovered | Why |
806
+ | --- | --- |
807
+ | `spine` and `hash` | the rig spec has no field for either, by design, and `ingest` reports both as findings — the declared exceptions above |
808
+ | anything below one float32 step | the parser stores frames, curves and vertices in a `Float32Array`, so a difference it cannot represent is invisible to any reading of the parsed form |
809
+ | a Bezier's handles *as written* | the parser samples them into the curve, so a moved handle arrives as moved samples rather than as the handle it was |
810
+ | how the file is **spelled** | nothing, on an editor export, but the declared exceptions: the rebuild spells every number as the export does, writes every object's keys in the export's order ([AUTHORING §10.6b](AUTHORING.md)) and leaves out every key the export leaves to the parser ([AUTHORING §10.6c](AUTHORING.md)). The value measure cannot see spelling — it compares what the parser loaded — and the pass line above is what can |
811
+ | how it **looks** | that is `check`, and `--texture-from` is how its figure is attributed |
812
+
813
+ The geometric row needs a real number, because a naive reading of `check` makes an
814
+ exact transcription look wrong. Here is the 3-timing transcription against frames
815
+ rendered from the export it was transcribed from:
816
+
817
+ ```
818
+ ── heavy — candidate animation "heavy", 12 fps ──
819
+ declared frames.json's own box: TAKEN, coincident — a fit there asks for 0.04 px, under the 1 px that separates a candidate in the frames' coordinates from one in its own, over 65 frame(s).
820
+ MAE mean 6.42 worst 7.13 at f0064 (0..255 over the union alpha; over the whole frame, mean 0.27)
821
+ ⭐ texture floor 6.42 above it 0.00 (over the reference's own pixels, 6.42 and 0.00)
822
+ slot drift worst 0.7 px "pendulum" at f0017
823
+ per-frame all 64 adjacent pair(s) change by as much as the reference's own frames do
824
+ ```
825
+
826
+ ⭐ **`above it 0.00` is the whole result.** The MAE is 6.42 and **100 % of it is
827
+ texture**: the same geometry sampled through the reference's own atlas reads zero. The
828
+ transcription is geometrically exact, and the 6.42 is one-region-per-page at full
829
+ resolution meeting a 512×128 page declared at `scale: 0.5`. ⇒ **On ingest work, read
830
+ `above it` before you read the MAE.** Without `--texture-from` there is no way to tell
831
+ 6.42-that-is-all-texture from 6.42-that-is-all-rig, and the report warns you of exactly
832
+ that rather than leaving you to find out.
833
+
834
+ **Which atlas your build should use, then.** `build` has three atlas routes, and the
835
+ choice is an ingest decision rather than a detail. All three rows below are the same
836
+ specs, `--frames ref3/light`, against frames rendered from the export — one set, so
837
+ the figures compare:
838
+
839
+ | Route | What the emitted attachments say | `check --frames ref3/light` |
840
+ | --- | --- | --- |
841
+ | **default** — one region per page, pointing at the loose PNGs | `pendulum 745x212`, `square 159x159` | `in units … x1.0001`; MAE **6.36**, texture floor 6.36, **above it 0.00** |
842
+ | **`--pack`** — the same loose parts onto shared pages | `pendulum 745x212`, `square 159x159` | `in units … x1.0001`; MAE **6.36**, texture floor 6.36, **above it 0.00** |
843
+ | **`--atlas-in <the export's own atlas>`** | `pendulum 746x212`, `square 160x160` | `in units candidate 1053.5 x 808.2 reference 1053.5 x 808.7 x0.9997`; MAE **2.03**, texture floor 0.00, **above it 2.03** |
844
+
845
+ 📌 **The first two are exact and indistinguishable**, and `--pack` is the one to reach
846
+ for when the deliverable is meant to look like an export: MaxRects onto shared pages,
847
+ byte-for-byte region copies, nothing resampled or rotated (AUTHORING §0.1). It does not
848
+ close the 6.36 — nothing that samples full-resolution texels can, against frames drawn
849
+ from a half-resolution pack — so the floor stays, `--texture-from` stays the way to
850
+ attribute it, and *packing does not change what the figure means.*
851
+
852
+ ⭐ **The third row is the mirror image of the first two, and reading it wrong is the
853
+ easy mistake.** Its MAE is the *lowest* of the three because it draws through the
854
+ reference's own texels — floor 0.00 — so what is left is geometry, and 2.03 of geometry
855
+ is the ONE PIXEL the pack cannot give back. `--atlas-in` divides a region's size by the
856
+ page's `scale:` (nine of the ten corpus atlases declare one — eight at 0.5, one at 0.4,
857
+ every file but `spineboy-run.atlas`), and the packer wrote `round(drawing × scale)`, so
858
+ a 373-texel region at `scale: 0.5` is consistent with both a 745- and a 746-pixel
859
+ drawing. rigc states 746, the export says 745, and putting the pixel back by hand takes
860
+ the same build to `x1.0000` / MAE **0.00** — which is how the residual is known to be
861
+ the rounding and nothing else.
862
+
863
+ ⇒ **The routes differ in what their MAE is MADE OF rather than in whether they are
864
+ right.** Loose art or `--pack` gives exact geometry through coarser texels; `--atlas-in`
865
+ gives the reference's texels through geometry good to half a texel. Read `above it`
866
+ before the MAE either way, and read the `in units` line first — it is the line that
867
+ catches a whole-figure scale error, and it is the only one that does.
868
+
869
+ ### 2.4 The worked precedent, and what to take from it
870
+
871
+ Three transcriptions of official Spine exports live in this repository under
872
+ [`bench/transcriptions/`](https://github.com/firejune/rigc/tree/main/bench/transcriptions/)
873
+ — `3-timing-and-spacing` (3 bones, 2 slots, 2 animations, 69 keys, every curve raw),
874
+ `6-arcs` (weighted meshes, mesh `edges`, four 4.3 transform constraints) and
875
+ `spineboy` in both `ess` and `pro`. They are **worked examples you are meant to read**,
876
+ and the rig spec's own `note` field is the thing to read first:
877
+
878
+ > *"Mechanical transcription of Spine's official 3-timing-and-spacing `ess` export …
879
+ > written to prove that a rig spec can express a foreign skeleton at all. It is NOT an
880
+ > authored rig: the numbers were copied out of the reference, so it says nothing about
881
+ > whether an agent could produce them."*
882
+
883
+ ⭐ **Copy that habit, not just the technique.** A transcription's `note` should say what
884
+ it is, what it is not, and where its numbers came from — because the file otherwise
885
+ looks exactly like an authored rig and will be read as one by whoever opens it next.
886
+ The `images` line in those specs points out of the repository into the gitignored
887
+ `examples/` directory for the same reason: the art is fetched, not redistributed
888
+ ([NOTICE.md](../NOTICE.md)).
889
+
890
+ ⚠️ `bench/` does not ship in the npm package, so from an installed copy those files are
891
+ the link above rather than something on disk. Nor does `scripts/fetch-examples.sh` —
892
+ the example corpus is a repository-checkout facility, and every command line on this
893
+ page was run from one.
894
+
895
+ ---
896
+
897
+ ## 3. Complaints, and what each one means
898
+
899
+ Foreign data meets rigc's refusals in two waves: argument handling, before any rule
900
+ runs, and then the assertions.
901
+
902
+ ### 3.1 Before the assertions
903
+
904
+ These three exit 2 with one sentence and no report. Together with §0.2's stack trace
905
+ they are the whole set an ingest task realistically meets.
906
+
907
+ | Complaint | What it means | What to do |
908
+ | --- | --- | --- |
909
+ | `N atlases beside <file> (…); name the right one with --atlas <path>` | the export directory holds more than one atlas and rigc will not guess | look at which regions each atlas declares, and name the one that covers the skeleton's attachments. §0.2 says why the filename heuristic is wrong here |
910
+ | `no .atlas beside <file>; name one with --atlas <path>` | you were handed the skeleton without its atlas, or copied one file out of a directory | get the atlas. Nothing downstream works without it — the attachments resolve through it |
911
+ | `<file> is neither a directory nor a .json skeleton` | the path is a `.skel`, a `.spine`, or anything else | §5 — rigc reads JSON only |
912
+ | *(exit 1, a stack trace ending in `ENOENT: … /skeleton.json`)* | you passed a directory and it is not a rigc output directory | point at the `.json` itself. §0.2 |
913
+
914
+ ### 3.2 Assertions a real export fails
915
+
916
+ 📊 **All twelve skeletons in the fetched corpus come back green** under the default
917
+ profile with the right atlas named. That is the baseline, and it is the honest headline:
918
+ **a correct editor export passes.** The two sections below are worth reading in full
919
+ because they mean opposite things — the first is a wrong input, and the second is data
920
+ that looks wrong and is not.
921
+
922
+ **`A00_ROUNDTRIP_PARSE` — the atlas does not cover the skeleton.**
923
+
924
+ ```bash
925
+ rigc validate examples/spineboy/export/spineboy-ess.json \
926
+ --atlas examples/spineboy/export/spineboy-run.atlas
927
+ ```
928
+
929
+ ```
930
+ FAIL A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: eye-indifferent (attachment: eye-indifferent)
931
+ rigc: 1 assertion(s) failed
932
+ ```
933
+
934
+ **What it means:** the atlas you named is a real atlas and a valid one — it is just not
935
+ this skeleton's. `spineboy-run.atlas` packs only what the `run` animation needs. ⇒
936
+ **Read this as an atlas-choice failure, not as a broken skeleton.** It is the
937
+ downstream shape of guessing at §0.2's refusal, and it is precise about the cost: one
938
+ named attachment, so you can tell "wrong atlas" from "the export is missing a region"
939
+ by whether the missing names are a *coherent subset*. Fix by naming the right atlas —
940
+ `spineboy-ess.json` is green against `spineboy.atlas`.
941
+
942
+ **`A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` — a trimmed deform run is not a defect.**
943
+
944
+ ```bash
945
+ rigc validate examples/spineboy/export/spineboy-pro.json \
946
+ --atlas examples/spineboy/export/spineboy.atlas
947
+ ```
948
+
949
+ ```
950
+ PASS A00_ROUNDTRIP_PARSE
951
+ PASS A10_NO_NAN_AFTER_STEPPING
952
+ …
953
+ PASS A35_DEFORM_KEYS_FIT_THE_ATTACHMENT
954
+ rigc: green
955
+ ```
956
+
957
+ `hoverboard-board` is an unweighted mesh with 148 floats; the key carries `offset: 1`
958
+ and 147 values, covering `1..148` — the whole array minus a leading zero the editor
959
+ trimmed. A trim can land on a y component, so an odd offset or an odd-length run is what
960
+ a trimmed run looks like, and Spine's own parser copies the run in at the raw index with
961
+ no alignment requirement anywhere. A35 refuses what does break — a run that does not fit
962
+ inside the deform array, non-finite values, an empty key array, an attachment missing
963
+ from the skin. The over-long run in particular is refused, and it is the quietest
964
+ defect the format has.
965
+
966
+ 🚨 **A validity rule stricter than the runtime does not look like a bug — it looks like
967
+ a finding about somebody's file.** A `FAIL` on foreign data has three possible meanings
968
+ and the message alone does not separate them:
969
+
970
+ 1. **the data is broken** — fix the data;
971
+ 2. **the input was wrong** — wrong atlas, missing page, truncated file (§3.1, and
972
+ `A00` above);
973
+ 3. **the rule is stricter than the runtime** — fix the rule, or file it.
974
+
975
+ 🎛️ **`A47` / `A48` on a foreign export are a fourth reading, and neither side is
976
+ wrong.** An ik or transform resting muted that no animation keys up moves nothing *in
977
+ the file*, and `validate <file>` has no rig spec and so no way to be told a game turns
978
+ it on from code — it refuses with three doors, the third being that statement. The
979
+ rebuild route makes it for you: `ingest` declares the constraint consumer-driven and
980
+ prints a `CONSUMER_DRIVEN_MIX` judgement (§2.0), and `build` then SKIPs it by name.
981
+
982
+ ⇒ Before changing anybody's export because rigc objected, check case 3: does the file
983
+ **parse** (`A00`), **step without NaN** (`A10`), and **render**? If all three, the
984
+ runtime is content and the burden is on the rule. Reporting that is a better answer
985
+ than a quietly edited export.
986
+
987
+ ### 3.3 Profile choice, and the sixteen rules that will not fire
988
+
989
+ `--profile spine` is the default and answers *"is this valid Spine 4.3 that any
990
+ runtime plays correctly?"* — and it is the right profile for foreign data, because the
991
+ other one is this project's own renderer and archetype policy.
992
+
993
+ **Sixteen assertions do not run under `spine`, and they come back `PROF`, not
994
+ `SKIP`:**
995
+
996
+ | Excluded as | Rules |
997
+ | --- | --- |
998
+ | **renderer policy** (8) | `A11_NO_CLIPPING_ATTACHMENTS`, `A12_NO_DARK_COLOR`, `A13_MESH_BUDGET`, `A14_NO_FULL_FRAME_MESH`, `A15_IDLE_NO_MESH_BONE_KEYS`, `A19_OVERLAY_PNGS_HAVE_ALPHA`, `A27_REGION_NAME_MATCHES_PAGE_FILENAME`, `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` |
999
+ | **archetype policy** (8) | `A21_MESH_RIM_PINNED`, `A24_AXIS_SPACE_STROKE`, `A25_DETACHED_BONE_PARENTAGE`, `A26_SLOT_DRAW_ORDER`, `A28_RIBBON_ROWS_SHARE_WEIGHTS`, `A29_STROKE_WITHIN_CONTACT_DEPTH`, `A30_STROKE_WITHIN_CAP_CONTAINMENT`, `A39_DEFORM_KEEPS_TRIANGLE_WINDING` |
1000
+
1001
+ Two further rules — **`A06`** and **`A20`** — are *mixed*: their validity clauses run
1002
+ in both profiles and their policy clauses only under `spine-html`. `A06`'s
1003
+ size-vs-PNG check is validity; rotation and premultiplied alpha are policy (two
1004
+ regions over the same texels was a policy clause of `A06` until it became `A49`). `A20`'s weight coherence is validity; requiring a mesh to be
1005
+ weighted at all is policy.
1006
+
1007
+ ⚠️ **`--profile spine-html` on foreign data produces a wall of failures that mean
1008
+ nothing about the file.** Same `spineboy-pro.json`, same atlas, one flag changed — the
1009
+ run ends `rigc: 13 assertion(s) failed`, and this is the tally with one real message
1010
+ per rule — `A06` takes a page that is one part covering it exactly *or a tiling of
1011
+ regions*, so a packed atlas passes both profiles and the wall is policy only:
1012
+
1013
+ | Count | Rule | One of its messages |
1014
+ | --- | --- | --- |
1015
+ | **10** | `A15_IDLE_NO_MESH_BONE_KEYS` | `idle keys bone "front-shoulder", which drives a mesh — meshes never idle-skip` |
1016
+ | **2** | `A20_MESH_WEIGHTS_COHERENT` | `mesh "front-shin" is unweighted; the ring tier drives meshes by bones` |
1017
+ | **1** | `A11_NO_CLIPPING_ATTACHMENTS` | `1 clipping attachment(s); the renderer skips them silently` |
1018
+
1019
+ Every one of those is a correct statement about a correct file: a bone *does* key a
1020
+ mesh, a mesh *is* unweighted, a clipping attachment *is* present.
1021
+ ⇒ **Do not run `spine-html` against
1022
+ somebody's export unless they asked whether it satisfies this project's renderer
1023
+ policy**, which is a different question from whether their file is valid.
1024
+
1025
+ ⚠️ **And do not read the absence of `SKIP` lines as thoroughness.** Under `spine` a
1026
+ foreign skeleton typically produces *no* archetype `SKIP` at all — the profile excludes
1027
+ those rules before their bodies could notice the missing `invariants` block, so they
1028
+ arrive as `PROF`. The `PROF` list is where *"was this held to that rule at all"* gets
1029
+ answered (AUTHORING §7).
1030
+
1031
+ ---
1032
+
1033
+ ## 4. Recipes
1034
+
1035
+ Each of these starts from a transcription (§2), and none is an edit to emitted JSON —
1036
+ an edit to emitted JSON is a change nothing in the toolchain checked. All three were
1037
+ run from copies of the stored 3-timing transcription:
1038
+
1039
+ ```bash
1040
+ T=bench/transcriptions/3-timing-and-spacing
1041
+ mkdir -p work/repivot
1042
+ cp $T/3-timing-and-spacing-ess.rig.json work/repivot/repivot.rig.json
1043
+ cp $T/3-timing-and-spacing-ess.motion.json work/repivot/repivot.motion.json
1044
+ ```
1045
+
1046
+ ⚠️ A copied spec's own `images` path is relative to where the spec was, so it breaks
1047
+ on the copy. `--images` overrides it, relative to your working directory, and every
1048
+ `build` below passes it.
1049
+
1050
+ ### 4.1 Moving a pivot without moving the art
1051
+
1052
+ The ask: *"the arm should swing from its middle, not its end — don't change the
1053
+ drawing."* This is the recipe the rest of the section is measured against, because its
1054
+ correctness criterion is exact and checkable without rendering anything.
1055
+
1056
+ **What changes in the file:**
1057
+
1058
+ | Object | Change | Why |
1059
+ | --- | --- | --- |
1060
+ | **the bone** | its `x`/`y` move to the new pivot, expressed in its **parent's** local axes | the bone's origin *is* the pivot |
1061
+ | **its attachments** | offsets move by the same vector expressed in the **bone's own** axes, with the opposite sign | an attachment offset is the art's centre relative to the bone origin; the bone origin just moved, so this cancels it |
1062
+ | **its child bones** | every child's `x`/`y` needs the same opposite correction | a child's offset is in this bone's local space, so moving the origin moved every child with it |
1063
+ | **the timelines** | ⛔ **nothing** | `rotate` keys are angles about the origin, and the origin is what you changed. This is the entire point of the edit |
1064
+
1065
+ ⚠️ **The child-bone row is the one that gets forgotten**, and it fails quietly: the
1066
+ re-pivoted bone's own art lands correctly and everything hanging off it is displaced by
1067
+ exactly the vector you moved. If the bone has children, correct them in the same edit,
1068
+ or the fix looks half-right in a way no assertion will mention.
1069
+
1070
+ **Worked.** 3-timing's `bone` sits at `x: 204.753, y: 708.0989` with `rotation: 180`
1071
+ and `length: 473`; the `pendulum` attachment is at bone-local
1072
+ `x: 316.79, y: 0.4815389`. Move the pivot half the bone's length along the bone's own
1073
+ +x axis, `d = 236.5`: the bone's rotation is 180° in its parent's frame, so
1074
+ `R(180)·(d, 0) = (-d, 0)`, and this bone has no children.
1075
+
1076
+ ```diff
1077
+ { "name": "bone", "parent": "root", "length": 473,
1078
+ - "x": 204.753, "y": 708.0989, "rotation": 180 }
1079
+ + "x": -31.747, "y": 708.0989, "rotation": 180 }
1080
+
1081
+ "pendulum": { "pendulum": { "image": "pendulum.png",
1082
+ - "x": 316.79, "y": 0.4815389, "rotation": -179.81934 } }
1083
+ + "x": 80.29, "y": 0.4815389, "rotation": -179.81934 } }
1084
+ ```
1085
+
1086
+ ```bash
1087
+ rigc build --rig work/repivot/repivot.rig.json \
1088
+ --motion work/repivot/repivot.motion.json \
1089
+ --images examples/3-timing-and-spacing/images \
1090
+ --out work/t3b
1091
+ ```
1092
+
1093
+ **Verify the invariant first, arithmetically.** The art's centre in world coordinates
1094
+ is `bone(x, y) + R(bone.rotation) · att(x, y)`. Computed from the two built skeletons,
1095
+ before and after:
1096
+
1097
+ ```
1098
+ original bone [204.753, 708.0989] att [316.79, 0.4816] -> art centre [-112.0370, 707.6174]
1099
+ re-pivot bone [-31.747, 708.0989] att [ 80.29, 0.4816] -> art centre [-112.0370, 707.6174]
1100
+ art centre moved by 2.842e-14 units
1101
+ ```
1102
+
1103
+ ⭐ **That is the criterion.** If the art's world position at the setup pose moves by
1104
+ more than floating-point noise, the compensation is wrong, and no amount of looking at
1105
+ frames will tell you which of the two numbers to blame.
1106
+
1107
+ **Then confirm the movement did change**, which is the half a pose cannot show. The
1108
+ same displacement evaluated across the bone's own rotation:
1109
+
1110
+ | `rotate` | original art centre | re-pivot art centre | apart |
1111
+ | --- | --- | --- | --- |
1112
+ | 0° | `[-112.04, 707.62]` | `[-112.04, 707.62]` | **0.00 units** |
1113
+ | 15° | `[-101.12, 625.64]` | `[-109.18, 686.85]` | 61.74 |
1114
+ | 45° | `[ -18.91, 483.75]` | `[ -88.18, 650.98]` | 181.01 |
1115
+ | 90° | `[ 205.23, 391.31]` | `[ -31.27, 627.81]` | 334.46 |
1116
+ | 180° | `[ 521.54, 708.57]` | `[ 48.54, 708.58]` | 473.00 |
1117
+
1118
+ **What the instruments say about it** — and this pair is why §1.3 carries its warning:
1119
+
1120
+ - **`diff` sees nothing.** `rigc diff work/t3b/skeleton.json <the export>` reads
1121
+ **1.000 on all 54 measures**: same bones, names, parents, order, slots, draw order,
1122
+ attachments, animations, timelines, key counts, curve kinds. All true, and all silent
1123
+ about a 236.5-unit move.
1124
+ - **`check` sees it loudly**, with the framing pinned so the comparison is of pictures
1125
+ and not of framings:
1126
+
1127
+ ```bash
1128
+ rigc check --candidate work/t3b --frames ref3/heavy \
1129
+ --viewport -573.3,-81.2,2176.3,990.1 \
1130
+ --texture-from examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas \
1131
+ --all-frames
1132
+ ```
1133
+
1134
+ ```
1135
+ MAE mean 106.91 worst 116.67 at f0012 (0..255 over the union alpha; over the whole frame, mean 7.15)
1136
+ ⭐ texture floor 3.62 above it 105.79 (over the reference's own pixels, 1.95 and 92.51)
1137
+ slot drift worst 42.4 px "pendulum" at f0028
1138
+ chain slots worst slot drift mean MAE in it share
1139
+ square 1/1 0.4 px "square" f0000 0.3 px 10.20 3.2%
1140
+ bone 1/1 42.4 px "pendulum" f0028 31.3 px 125.70 77.3%
1141
+ (unattributed) — — — — 19.5%
1142
+
1143
+ frame MAE union px Δpx ref Δ worst slot drift how slots note
1144
+ f0000 5.98 1246 — — square 0.4 component 2/2
1145
+ f0001 6.18 1245 19 40 square 0.4 component 2/2
1146
+ f0002 19.58 1280 456 520 pendulum 0.7 component 2/2
1147
+ f0003 46.11 1412 702 815 pendulum 2.1 component 2/2
1148
+ f0004 75.88 1608 808 939 pendulum 4.3 component 2/2
1149
+ f0005 93.32 1776 996 1125 pendulum 8.0 component 2/2
1150
+ ```
1151
+
1152
+ ⭐ **Read the per-frame column before the mean.** `f0000` is **5.98** — the
1153
+ transcription's own texture floor, i.e. *no difference at all* — and it climbs
1154
+ monotonically from `f0002` as the bone rotates. That is the pivot's whole signature:
1155
+ **invisible in the pose, and everything in the movement**, which is what MOTION §3.9
1156
+ argues from the authoring side and what this measures from the ingest side. The
1157
+ `chains` table puts 77.3 % of the difference on the `bone` chain and 3.2 % on
1158
+ `square`, which is the edit's own blast radius.
1159
+
1160
+ ⚠️ **Pin `--viewport` for a re-pivot check.** Drop the flag and the same run reads
1161
+ `MAE mean 120.65` with `slot drift worst 30.3 px "pendulum" at f0000` — and that drift
1162
+ at frame 0 is an artefact, because the re-pivot changed the skeleton's **world extent**,
1163
+ so `frames.json`'s box came back `REFUSED, coordinates` and the framing was fitted
1164
+ instead:
1165
+
1166
+ ```
1167
+ framed to 256x116px 0.100498 px/unit world x[-908.6 .. 1638.7] y[-74.6 .. 1079.7] (fitted to the candidate's own drawn pixels)
1168
+ declared frames.json's own box: REFUSED, coordinates — a fit there asks for 31.48 px, past the 11.74 px the extent-spread tolerance reaches — a different origin or a different unit, over 65 frame(s).
1169
+ ```
1170
+
1171
+ ⇒ On any edit that changes the extent, take the viewport from the reference render's
1172
+ own `world x[…] y[…]` line, or read `check`'s framing lines before its figures.
1173
+
1174
+ ### 4.2 Renaming, and the name-agnostic mindset
1175
+
1176
+ The ask: *"give everything our project's names."* Mechanically a rename pass over the
1177
+ rig spec; the discipline is in what you check afterwards.
1178
+
1179
+ **Everything in a rig spec resolves by name, and a miss is refused by name.** A bone's
1180
+ `parent`, a slot's `bone`, a constraint's `bones` and `target`, a draw-order key's
1181
+ `slot`, an authored mesh's vertex `weights` — and, across the two files, the motion
1182
+ spec's `archetype` against the rig spec's `name`. That last one is the first refusal a
1183
+ rename produces, before anything else has a chance to go wrong: a `rigc compile error`
1184
+ naming the motion spec's path, the `archetype` that spec states, the path of the rig spec
1185
+ it was handed, and the `name` that rig actually carries — both sides of the mismatch in
1186
+ one sentence. [`src/compile.ts`](../src/compile.ts) builds it; the renamed copies this
1187
+ section works on are not committed, for the reason the Appendix gives, so the figure is
1188
+ described here rather than transcribed off one.
1189
+
1190
+ ⭐ **A rename is therefore mostly safe by construction, and its failures arrive as
1191
+ sentences naming both sides.** That is the reason to do it in the specs rather than in
1192
+ emitted JSON, where the same mistake is a silently unresolved reference.
1193
+
1194
+ **Then check it with the name-agnostic groups**, because after a rename the name-keyed
1195
+ measures are *supposed* to disagree. Renaming `bone`→`arm`, `square`→`block`, the two
1196
+ slots to `arm-art`/`block-art` and their attachments to match:
1197
+
1198
+ ```
1199
+ bones mean 0.567 over 8 measures
1200
+ 1.000 count 3/3 how many bones
1201
+ 0.200 names 1/5 the bone names themselves
1202
+ 0.333 parent_by_name 1/3 each bone hangs off the same parent
1203
+ 0.333 order 1/3 the bones are declared in the same order
1204
+ 0.333 length_present 1/3 a setup `length` is present or absent alike
1205
+ 0.333 inherit_present 1/3 a setup `inherit` is present or absent alike
1206
+ 1.000 depth_histogram 3/3 NAME-AGNOSTIC: as many bones at each depth
1207
+ 1.000 degree_sequence 3/3 NAME-AGNOSTIC: as many bones with each child count
1208
+
1209
+ bones (name-agnostic) mean 1.000 over 5 measures — the same two skeletons compared with names thrown away
1210
+ 1.000 count 3/3 how many bones
1211
+ 1.000 depth_histogram 3/3 as many bones at each depth
1212
+ 1.000 degree_sequence 3/3 as many bones with each child count
1213
+ 1.000 shape_histogram 3/3 as many bones of each depth-and-child-count shape (`d1c3` = one hop down, three children)
1214
+ 1.000 order_shape 3/3 the bones are declared in the same order of shapes
1215
+
1216
+ slots mean 0.143 over 7 measures
1217
+ 1.000 count 2/2 how many slots
1218
+ 0.000 names 0/4 the slot names themselves
1219
+ 0.000 order 0/2 the slots array IS the draw order, so its order is data
1220
+ 0.000 bone 0/2 each slot is bound to the same bone
1221
+ 0.000 attachment 0/2 each slot shows the same setup attachment
1222
+ 0.000 blend 0/2 each slot uses the same blend mode
1223
+ 0.000 color_present 0/2 a tint is present or absent alike
1224
+
1225
+ slots (name-agnostic) mean 1.000 over 4 measures — the same two skeletons compared with names thrown away
1226
+ 1.000 count 2/2 how many slots
1227
+ 1.000 attachment_types_by_position 2/2 the same kind of attachment sits at each position in the draw order
1228
+ 1.000 bone_binding_shape 2/2 as many slots hang off a bone of each shape (`?` = no such bone is declared)
1229
+ 1.000 order_shape 2/2 the draw order is the same order of `<attachment type>@<bone shape>`
1230
+
1231
+ attachments mean 0.818 over 11 measures
1232
+ 1.000 skins 1/1 the skin names
1233
+ 1.000 count 2/2 how many attachments
1234
+ 0.000 names 0/4 skin/slot/attachment keys
1235
+ 1.000 type_counts 2/2 as many of each attachment type
1236
+ …
1237
+ 0.000 refs 0/2 each attachment resolves the same region, clipping end and linked-mesh source — 2 on one side only, which `attachments.names` names
1238
+ 1.000 skin_members 1/1 each skin activates the same skin-required bones and constraints
1239
+ …
1240
+ animations mean 0.909 over 11 measures
1241
+ …
1242
+ 0.000 targets 0/8 each timeline keys the same bone, slot, constraint or attachment — …
1243
+ ```
1244
+
1245
+ Three things to read out of that, in order:
1246
+
1247
+ - **The name-keyed collapse is the task, not a defect.** `bones` 0.567, `slots` 0.143,
1248
+ `attachments` 0.818, `animations` 0.909. Note that several *non*-name measures fall with them —
1249
+ `slots.bone`, `slots.blend`, `bones.parent_by_name` — because they are keyed **by**
1250
+ the name that changed. They are not saying the binding changed.
1251
+ - 🚨 **`bones (name-agnostic)` and `slots (name-agnostic)` must stay 1.000.** They
1252
+ measure the rig with the vocabulary thrown away, so a rename that changed only names
1253
+ leaves them untouched. **A drop there is a structural mistake wearing a rename's
1254
+ clothes**, and it is the only assertion this recipe really has.
1255
+ - **`animations` moves on one measure only, `targets`**, because animation *names*
1256
+ were not part of the ask but every timeline keys a renamed bone — `targets` is keyed
1257
+ by the name that changed, as `slots.bone` is. Every other `animations` measure stays
1258
+ 1.000. Under the same edit `bones.names` reads `1/5` — `root` survived, and the total
1259
+ is the union of both vocabularies.
1260
+
1261
+ ⛔ And remember §1.3: `diff` reads no coordinates either way. A rename that also moved
1262
+ something is invisible to every measure in that report. Pair it with a `check` against
1263
+ frames rendered from the original.
1264
+
1265
+ ⚠️ **Do not rename toward what a rule seems to want.** `A27`'s
1266
+ region-name-matches-page-filename is `spine-html` policy (§3.3): under the default
1267
+ profile it does not fire, and renaming somebody's attachments to satisfy a policy they
1268
+ never opted into is a change with no benefit to them.
1269
+
1270
+ ### 4.3 Extending a foreign skeleton with a new animation
1271
+
1272
+ The ask: *"add a `nudge` to this."* There is no append — `build` re-emits the whole
1273
+ skeleton — so the extension is an added block in the motion spec of a transcription
1274
+ that already round-trips.
1275
+
1276
+ 1. **Get the transcription to structural agreement first** (§2.3), and record the
1277
+ figure. Extending an unfinished transcription mixes two kinds of difference into
1278
+ every measurement after it.
1279
+ 2. **Add the animation to the motion spec.** Named easings here, not raw curves —
1280
+ §2.2's escape hatch is for reproducing an export's own beziers, and this movement
1281
+ has no export behind it. What goes *between* the poses is [MOTION.md](MOTION.md);
1282
+ this page stops at the mechanics.
1283
+ 3. **`build`, and read the count line.**
1284
+
1285
+ ```bash
1286
+ rigc build --rig work/extend/extend.rig.json \
1287
+ --motion work/extend/extend.motion.json \
1288
+ --images examples/3-timing-and-spacing/images \
1289
+ --out work/t3c
1290
+ ```
1291
+
1292
+ ```
1293
+ .. pages=2 regions=2 bones=3 slots=2 animations=3 version=4.3.13 regionAttachments=2 meshAttachments=0 physicsConstraints=0 rig=3-timing-and-spacing-ess profile=spine
1294
+ ```
1295
+
1296
+ `animations=3` where the export had 2. That line is the cheapest confirmation the
1297
+ block landed at all.
1298
+ 4. **Read `diff` knowing what it is about to say.**
1299
+
1300
+ ```
1301
+ animations mean 0.821 over 11 measures
1302
+ 0.667 count 2/3 how many animations
1303
+ 0.667 names 2/3 the animation names
1304
+ 0.667 duration 2/3 each animation runs as long (last key time, within one frame)
1305
+ 0.889 timeline_kinds 8/9 the same timelines exist
1306
+ 0.958 key_counts 69/72 those timelines carry as many keys
1307
+ 0.958 curve_kinds 69/72 as many linear / stepped / bezier keys
1308
+ 1.000 event_keys 0/0 as many event firings — neither side has any
1309
+ 0.667 draw_order 2/3 a draw-order timeline is present or absent alike
1310
+ 0.667 deform 2/3 a deform timeline is present or absent alike
1311
+ 0.889 targets 8/9 each timeline keys the same bone, slot, constraint or attachment — …
1312
+ 1.000 keyed_names 0/0 each key names the same attachment, draw-order slot or event — neither side has any
1313
+ ```
1314
+
1315
+ 🚨 **Every one of those got worse, and that is the correct result.** The
1316
+ `animations` section went 1.000 → 0.821 because the candidate now has something the
1317
+ reference does not. `diff` measures agreement with a reference; you were asked to
1318
+ *disagree* with it, in one specific way. ⇒ **Check that the drop is confined to the
1319
+ `animations` section and is the size the addition explains** — one animation of
1320
+ three, three keys of seventy-two — and that `bones`, `slots`, `attachments` and
1321
+ `constraints` are all still 1.000. That last part is the real assertion here: *the
1322
+ extension changed nothing it was not supposed to change.*
1323
+ 5. **Look at it, then ask.** `render --animation nudge` and open the contact sheet;
1324
+ `vote` the foreign original against your extended build when the question is whether
1325
+ the new movement belongs beside the old ones. Nothing in this toolchain can answer
1326
+ that, and MOTION §4–§5 is how to shape the ballot so the answer informs.
1327
+
1328
+ ---
1329
+
1330
+ ## 5. Non-goals — stated, so nobody proposes them as gaps
1331
+
1332
+ 🚫 **rigc does not read editor project files.** A `.spine` is the editor's own project
1333
+ format, not skeleton data, and no rigc command opens one. The path form refuses it by
1334
+ name:
1335
+
1336
+ ```
1337
+ rigc: …/hero.spine is neither a directory nor a .json skeleton
1338
+ ```
1339
+
1340
+ The route from a project file to rigc is the one the editor already provides: export
1341
+ it, and start from the export. (The official examples' project files are public
1342
+ domain — [NOTICE.md](../NOTICE.md) — and `scripts/fetch-examples.sh` does not download
1343
+ them, because nothing here could use one.)
1344
+
1345
+ 🚫 **Binary `.skel` is not read, and the honest statement has two halves.** rigc's
1346
+ dependency *can* read it and rigc *does not*:
1347
+
1348
+ - `@esotericsoftware/spine-core@4.3.13` exports `SkeletonBinary`, whose
1349
+ `readSkeletonData` is the binary reader.
1350
+ - **rigc's own source names it only in comments — no import, no call.** Every read path goes through
1351
+ `JSON.parse` and `SkeletonJson`, and the path resolver requires a `.json` extension —
1352
+ so a `.skel` is refused by the same sentence a `.spine` is, one layer before any
1353
+ format question arises.
1354
+
1355
+ ⇒ Binary support is *reachable* rather than *present*: a plumbing job on a dependency
1356
+ that already has the reader, not a parser to write. But it is not there, and nothing on
1357
+ this page works on a `.skel` today. Re-export as JSON.
1358
+
1359
+ ✅ **A skeleton-to-spec decompiler exists: `rigc ingest` (§2.0), and it decides
1360
+ nothing the skeleton does not state.** A bone's setup transform — the pivot — is in
1361
+ the skeleton in full, so it is copied with no decision. `ingest` chooses **no**
1362
+ generator: a generator is a *model* (`src/rig.ts`: *"they encode a deformation model …
1363
+ and a model is not a table of numbers"*), the skeleton holds geometry, and geometry is
1364
+ what the rig spec's authored form takes — so a generator-built mesh comes back as
1365
+ authored geometry and rebuilds **byte-identical**. And it writes no `invariants`: an
1366
+ absent field makes an archetype assertion SKIP, never pass (§2.1 step 3), so a
1367
+ decompiled spec carries no intent and says so instead of certifying something nobody
1368
+ measured. Where the skeleton has no value — the stage — `ingest` writes a stated
1369
+ absence (§2.0). ⚠️ Not to be confused with the *atlas* importer below, which is a
1370
+ different direction and also exists.
1371
+
1372
+ ⚠️ **What `ingest` is not.** It reads skeleton JSON and writes two spec files.
1373
+ It does not read a `.spine` project or a binary `.skel` (the two entries above), it
1374
+ does not read the atlas or the art, it does not **edit** a skeleton,
1375
+ and it makes no claim about whether an agent could have *produced* the numbers it
1376
+ copied — only that the spec can carry them and `build` reproduces the file from them.
1377
+
1378
+ ✅ **A packer and an importer both exist, so do not report them as gaps.**
1379
+ `build --pack` writes shared pages losslessly and `build --atlas-in` resolves against a
1380
+ pack somebody else made (AUTHORING §0.1–§0.2). One-region-per-page is the **default**,
1381
+ not the only shape. `--atlas-in` applies the page's `scale:`, so an imported pack states
1382
+ the drawing's size rather than the pack's — to within the pack's own rounding, which is
1383
+ §2.3's row.
1384
+
1385
+ 🚫 **No CLI unpacker, so `pose` needs loose art.** `pose` reads *loose part PNGs*
1386
+ against one picture. A foreign export hands you a packed page instead, and pointing
1387
+ `pose` at one is worse than useless — it treats the whole page as a single part and
1388
+ answers confidently:
1389
+
1390
+ ```bash setup
1391
+ rigc render --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
1392
+ --fps 4 --max 900 --out ref-big
1393
+ mkdir -p packed-only
1394
+ cp examples/3-timing-and-spacing/export/3-timing-and-spacing.png packed-only/
1395
+ rigc pose --images packed-only --frame 'ref-big/light@4fps/f0002.png'
1396
+ ```
1397
+
1398
+ ```
1399
+ rigc pose
1400
+ .. frame …/ref-big/light@4fps/f0002.png (900x409)
1401
+ .. ground rgb(232, 232, 232) over 100% of the border ring
1402
+ .. parts …/packed-only (1 png)
1403
+ .. search scale 0.5–2 in 7 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
1404
+ PLACE 3-timing-and-spacing.png x= 324.3 y= 188.3 rot= -91.6° scale=0.630 residual=0.2085 unexplained= 30%
1405
+ found on a 14x7 anchor grid, step 4 at 16x reduction
1406
+ ```
1407
+
1408
+ ⚠️ **`residual=0.2085` is *under* the default 0.25 refusal bar**, so nothing refused
1409
+ it, and `PLACE` rather than `AMBIG` means nothing flagged it either. With the same frame
1410
+ and the two real loose parts, the answer is what it should be:
1411
+
1412
+ ```bash
1413
+ rigc pose --images examples/3-timing-and-spacing/images \
1414
+ --frame 'ref-big/light@4fps/f0002.png' --scale 0.3,0.6
1415
+ ```
1416
+
1417
+ ```
1418
+ rigc pose
1419
+ .. frame …/ref-big/light@4fps/f0002.png (900x409)
1420
+ .. ground rgb(232, 232, 232) over 100% of the border ring
1421
+ .. parts …/examples/3-timing-and-spacing/images (2 png)
1422
+ .. search scale 0.3–0.6 in 4 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
1423
+ PLACE pendulum.png x= 319.3 y= 214.6 rot= -88.9° scale=0.411 residual=0.0428 unexplained= 7%
1424
+ found on a 28x13 anchor grid, step 2 at 16x reduction
1425
+ PLACE square.png x= 437.1 y= 343.1 rot= 0.0° scale=0.410 residual=0.0334 unexplained= 4%
1426
+ found on a 113x51 anchor grid, step 2 at 4x reduction
1427
+ ```
1428
+
1429
+ ⇒ **Check what is in `--images` before trusting a `pose` report on ingest work.** One
1430
+ PNG where you expected several is the tell, and the `.. parts` line prints the
1431
+ count. AUTHORING §11.4 is the rest of what that command cannot see.
1432
+
1433
+ ⭐ **And on ingest work you usually have the thing `pose` is missing.** A skeleton you
1434
+ are transcribing IS a compiled candidate, so the parts `pose` refuses because
1435
+ something is drawn over them are readable through its own draw order and hierarchy —
1436
+ `rigc chainfit --candidate <that skeleton> --images <dir> --frame <png>`, AUTHORING
1437
+ §12. It is the natural second pass here: `pose` reads the trunk of a foreign figure,
1438
+ `chainfit` reads the limbs it hides, and both report placements rather than grades.
1439
+
1440
+ 📎 To be exact about what is missing: rigc *can* lift a region's drawing back off a
1441
+ page — `extractRegion` does it, and the contour mesh generator uses it under
1442
+ `--atlas-in` — so what is absent is a **command**, not the capability. That includes a
1443
+ region the pack **turned** (`rotate: 90`, `180`, `270`, or the
1444
+ older `rotate: true`), which a foreign pack routinely is and rigc's own never is: the
1445
+ lift transcribes `MeshAttachment.computeUVs`, the one routine in spine-core that
1446
+ states where a turned region's texels are, so what a generator measures does not
1447
+ depend on how the art was delivered (AUTHORING §0.2).
1448
+
1449
+ 🚫 **No `validate --fix`, and no normalisation pass.** Every recipe in §4 is a change
1450
+ you state in a spec and rebuild. A tool that rewrote somebody's export in place would
1451
+ be making decisions on their behalf with nowhere to say it had — and, per §3.2, some of
1452
+ those decisions would be wrong about the rule rather than about the file.
1453
+
1454
+ 🚫 **`diff` will not be given coordinate measures to make it a geometry check.**
1455
+ `check` is the geometry instrument, and it works by rendering, which is the only way to
1456
+ compare two rigs that may be authored in different coordinate systems at different
1457
+ scales. A coordinate diff between two skeletons would be arithmetic on numbers that do
1458
+ not compare — `check`'s own report says the two world boxes *"are different coordinate
1459
+ systems and do not compare; the pixel grid does."*
1460
+
1461
+ ---
1462
+
1463
+ ## Appendix — the corpus this page was verified against
1464
+
1465
+ Every command line above was run from a checkout with `bun run fetch-examples`
1466
+ completed. Two skeletons carry all of it:
1467
+
1468
+ | Example | Files used | Licence |
1469
+ | --- | --- | --- |
1470
+ | **`3-timing-and-spacing`** | `export/3-timing-and-spacing-ess.json`; `export/3-timing-and-spacing.atlas` (one 512×128 page, `scale: 0.5`, 2 regions); `images/pendulum.png` (745×212); `images/square.png` (159×159) | `license.txt` present, © 2021-2025 Esoteric Software |
1471
+ | **`spineboy`** | `export/spineboy-pro.json`, `export/spineboy-ess.json`, `export/spineboy.atlas`, `export/spineboy-run.atlas` | `license.txt` present, © 2013 Esoteric Software LLC |
1472
+
1473
+ The working directories the commands write into — `ref3/`, `ref-big/`, `render/`,
1474
+ `work/`, `packed-only/` — are throwaway and none is committed.
1475
+
1476
+ ⚠️ **`7-anticipation` ships no `license.txt` upstream**, so the redistribution grant its
1477
+ siblings carry does not exist for it. No excerpt, figure or image on this page comes
1478
+ from it; the only places it appears at all are the two corpus-wide tallies — §3.2's
1479
+ *twelve of twelve skeletons* and §2.3's *nine of ten atlases* — both of which count
1480
+ every directory the fetch produced.
1481
+ `scripts/fetch-examples.sh` prints a warning naming it; [NOTICE.md](../NOTICE.md) has
1482
+ the per-directory terms.
1483
+
1484
+ The transcription these recipes start from is
1485
+ [`bench/transcriptions/3-timing-and-spacing/`](https://github.com/firejune/rigc/tree/main/bench/transcriptions/3-timing-and-spacing/),
1486
+ unmodified. The re-pivoted, renamed and extended variants in §4 were built from copies
1487
+ of it; none is committed, because each is an illustration of an edit rather than a
1488
+ transcription of anything.