spine-rigc 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/INGEST.md ADDED
@@ -0,0 +1,1117 @@
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
+ 🔓 **Nothing in this repository's benchmark protocol applies to you.** No reading is
16
+ forbidden, no reference is sealed, no attempt is scored, and no rung is being
17
+ attempted. Those rules exist to keep one *measured experiment* honest; you are
18
+ working on somebody's own data. Where this page points at a stored transcription it
19
+ is pointing at **worked precedent you are meant to read**, not at a candidate you are
20
+ not allowed to see. AUTHORING's own exemption line says the same thing from the
21
+ authoring side.
22
+
23
+ 🚨 **The two numbers this page produces are not grades, and they measure different
24
+ things.** `validate`'s red says *this file breaks a stated rule* — a fact about the
25
+ file, not about your work, and sometimes (§3.2) a fact about the rule. `diff`'s
26
+ ratios say *how much of a particular reference's structure your candidate
27
+ reproduces* — so deliberately extending or renaming a foreign skeleton **lowers
28
+ them**, by design (§4.2, §4.3). Neither one has a pass bar, and this page does not
29
+ invent one.
30
+
31
+ - Formats and the CLI reference: [README.md](../README.md)
32
+ - The rig spec, field by field: **AUTHORING §3**, and [`src/rig.ts`](../src/rig.ts)
33
+ - The motion spec, field by field: **AUTHORING §4**
34
+ - Named failures, and the file each one points at: **AUTHORING §5–§6**
35
+ - The coordinate contract, in one place: **AUTHORING §11.2** and
36
+ [`src/transform.ts`](../src/transform.ts)
37
+ - What the editor does when nobody tells it otherwise: **AUTHORING §10**
38
+ - What the format holds and what rigc covers, skeleton by skeleton:
39
+ [SPEC_COVERAGE.md](SPEC_COVERAGE.md)
40
+ - If you are the *person operating* an agent rather than the agent:
41
+ [PROMPTING.md](PROMPTING.md)
42
+
43
+ ---
44
+
45
+ ## 0. What the toolchain can do with a file you were handed
46
+
47
+ Two facts decide everything below, and they pull in opposite directions:
48
+
49
+ 1. **rigc reads compiled skeleton JSON in more places than you would guess.**
50
+ `validate`, `render`, `preview`, `vote`, `check` and `diff` all take a
51
+ `skeleton.json` path directly, and none of them needs a rig spec to do it.
52
+ 2. **rigc cannot write one back.** There is no command that edits a skeleton, no
53
+ importer, and no route from skeleton JSON to specs. The only thing that produces a
54
+ skeleton is `build`, and `build`'s input is a rig spec plus a motion spec.
55
+ ⇒ **Every route that ends in a changed file goes through transcription** (§2).
56
+
57
+ ### 0.1 The table
58
+
59
+ Every row was executed against the fetched example corpus before it was written down.
60
+ `3-timing-and-spacing` and `spineboy` are the two skeletons this page uses; both carry
61
+ an upstream `license.txt` (Appendix, and [NOTICE.md](../NOTICE.md)).
62
+
63
+ | Command | Takes a foreign `skeleton.json`? | What it needs, and what it gives back |
64
+ | --- | --- | --- |
65
+ | **`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 |
66
+ | **`render --candidate <skeleton.json>`** | ✅ **yes** | PNG frames plus a contact sheet, per animation. ⭐ **It does not gate** — it drew all seven frames of `spineboy-pro`'s `hoverboard` while `validate` was failing that same file, which is how the rule rather than the file was found to be wrong (§3.2) |
67
+ | **`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)) |
68
+ | **`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 |
69
+ | **`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 |
70
+ | **`diff <candidate.json> <reference.json>`** | ✅ **yes, both sides** | 49 structural measures over bones, slots, attachments, constraints, animations and events. ⛔ **Blind to every coordinate** — §1.3 |
71
+ | **`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 |
72
+ | **`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 |
73
+ | **`build --rig … --motion … --images …`** | ⛔ **no** | specs in, skeleton out. The only writer in the toolchain, and the reason §2 exists |
74
+ | **`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 |
75
+ | **`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 |
76
+ | **`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 |
77
+
78
+ ### 0.2 Two path shapes, and why the directory form is not yours
79
+
80
+ `validate`, `render`, `preview`, `check`, `vote` and `bench` all accept either a
81
+ **directory** or a **`.json` file**. The directory form resolves to `skeleton.json` +
82
+ `skeleton.atlas` — **rigc's own output names**. A foreign export directory is named
83
+ after whatever the editor called the project, so the directory form finds nothing:
84
+
85
+ ```bash
86
+ rigc validate examples/3-timing-and-spacing/export
87
+ ```
88
+
89
+ ```
90
+ rigc validate …/examples/3-timing-and-spacing/export/skeleton.json
91
+ .. atlas …/examples/3-timing-and-spacing/export/skeleton.atlas
92
+ …
93
+ ENOENT: no such file or directory, open '…/examples/3-timing-and-spacing/export/skeleton.json'
94
+ ```
95
+
96
+ ⚠️ **That one comes back as a stack trace, exit 1, rather than a named refusal.** It
97
+ is the only complaint on this page that does — everything in §3.1 is a sentence. Do
98
+ not read the trace as *rigc cannot open this export*; read the two `..` lines above
99
+ it, which name the two files it went looking for. ⇒ **Point every command at the
100
+ `.json` itself.** Every command line on this page does.
101
+
102
+ The atlas then resolves by looking beside the skeleton, and that lookup refuses
103
+ rather than guesses:
104
+
105
+ | What is beside the `.json` | What happens |
106
+ | --- | --- |
107
+ | exactly one `.atlas` | it is used, and the report names it |
108
+ | no `.atlas` | ⛔ `no .atlas beside …; name one with --atlas <path>` |
109
+ | two or more | ⛔ refuses and lists them — see below |
110
+ | any of the above, with `--atlas <path>` | `--atlas` wins outright |
111
+
112
+ ```bash
113
+ rigc validate examples/spineboy/export/spineboy-pro.json
114
+ ```
115
+
116
+ ```
117
+ rigc: 2 atlases beside …/examples/spineboy/export/spineboy-pro.json (spineboy-run.atlas,
118
+ spineboy.atlas); name the right one with --atlas <path> — guessing by filename is how
119
+ an attachment quietly resolves against the wrong page
120
+ ```
121
+
122
+ ⭐ **This refusal is worth understanding rather than working around, because the
123
+ obvious heuristic is wrong on this very file.** `spineboy-ess` shares a longer
124
+ filename prefix with `spineboy-run.atlas` than with the `spineboy.atlas` it actually
125
+ uses. Pick by prefix and every attachment resolves against a page that does not
126
+ contain it — and §3.2 shows what that looks like when you get it wrong, which is a
127
+ failure two rules downstream of the choice.
128
+
129
+ ---
130
+
131
+ ## 1. Reading it
132
+
133
+ ### 1.1 `validate` — what the file is, and what rigc objects to
134
+
135
+ ```bash
136
+ rigc validate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
137
+ ```
138
+
139
+ ```
140
+ rigc validate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
141
+ .. atlas …/examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas
142
+ .. profile spine — 7 renderer-policy and 7 archetype assertion(s) do not apply
143
+ PASS A07_ATLAS_TEXT_SHAPE
144
+ PASS A00_ROUNDTRIP_PARSE
145
+ PASS A16_SKELETON_VERSION_4_3
146
+ …
147
+ SKIP A31_DRAW_ORDER_OFFSETS_RESOLVE: no animation carries a drawOrder timeline
148
+ SKIP A32_EVENT_KEYS_RESOLVE: no animation carries an event timeline
149
+ SKIP A34_CONSTRAINT_TIMELINE_TARGETS: no animation carries a constraint timeline
150
+ SKIP A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: no animation carries a deform timeline
151
+ SKIP A33_VERTEX_ATTACHMENT_GEOMETRY: the skeleton carries no bounding box, clipping attachment or path
152
+ …
153
+ PROF A12_NO_DARK_COLOR: renderer rule, not in profile "spine"
154
+ PROF A21_MESH_RIM_PINNED: archetype rule, not in profile "spine"
155
+ …
156
+ rigc: green
157
+ ```
158
+
159
+ Read it as three separate statements, because they answer three different questions:
160
+
161
+ - **`PASS` / `FAIL`** — the rule ran, and this is its verdict.
162
+ - **`SKIP`** — the rule ran and had nothing to measure, and the line says what was
163
+ absent. A `SKIP` is never a pass, and on foreign data the `SKIP` list is also a
164
+ **free inventory of what the skeleton does not contain**. The five lines above tell
165
+ you, without your having opened the JSON, that this export has no draw-order
166
+ timeline, no event timeline, no constraint timeline, no deform timeline, and no
167
+ bounding box, clipping attachment or path.
168
+ - **`PROF`** — the rule was excluded by the profile before its body ran. §3.3.
169
+
170
+ ⚠️ **A green here is a statement about validity and nothing else.** It does not say
171
+ the skeleton is the one you were meant to be given, that its animations are the ones
172
+ their names suggest, or that anything in it is where the art wants it. The gate cannot
173
+ see a wrong animation (AUTHORING §0), and it certainly cannot see a wrong *file*.
174
+
175
+ ### 1.2 `render` and `preview` — look at it
176
+
177
+ ```bash
178
+ rigc render --candidate examples/spineboy/export/spineboy-pro.json \
179
+ --atlas examples/spineboy/export/spineboy.atlas \
180
+ --animation walk --fps 8 --max 200 --out render/sb
181
+ ```
182
+
183
+ ```
184
+ rigc render
185
+ .. skeleton …/examples/spineboy/export/spineboy-pro.json
186
+ .. atlas …/examples/spineboy/export/spineboy.atlas
187
+ .. 200x186px at 8 fps, 1 set(s) -> …/render/sb
188
+ .. walk 9 frame(s), 1.000s + contact.png -> …/render/sb/walk@8fps
189
+ rigc: wrote …/render/sb/frames.json
190
+ ```
191
+
192
+ Three properties of this command matter more for foreign data than for your own:
193
+
194
+ - ⭐ **It does not gate.** That same `spineboy-pro.json` fails two assertions under the
195
+ default profile (§3.2) and renders all nine `walk` frames anyway. ⇒ **A red
196
+ `validate` is not a reason to stop looking**, and looking is often what tells you
197
+ whether the red matters.
198
+ - **Omitting `--animation` renders every animation**, each into its own directory with
199
+ its own contact sheet. On a file you were handed, that is the cheapest complete
200
+ inventory there is — one image per animation, with spacing visible across the grid.
201
+ - **The frame size is fitted to the skeleton's own extent**, so `--max` is a cap on the
202
+ longest side and not a canvas. A skeleton whose world box is 24 units wide renders
203
+ 24 px wide however large you set it; read the size on the `..` line before concluding
204
+ a render came out empty.
205
+
206
+ `preview` writes one HTML file that plays the same data in the official Spine Web
207
+ Player. On foreign data it is also the **interop proof**: if that page plays it, a
208
+ Spine runtime plays it, whatever rigc's own rasteriser or validator thinks.
209
+
210
+ ### 1.3 `diff` — and the two things it cannot see
211
+
212
+ `diff` takes two compiled skeletons and reports 49 measures in eight groups. Both
213
+ sides may be foreign; the interesting pairing during ingest is **your transcription
214
+ against the export it came from**:
215
+
216
+ ```bash
217
+ rigc diff work/t3/skeleton.json \
218
+ examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
219
+ ```
220
+
221
+ ```
222
+ rigc diff
223
+ candidate …/work/t3/skeleton.json
224
+ reference …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
225
+ .. bones=3/3 slots=2/2 skins=1/1 attachments=2/2 constraints=0/0 animations=2/2 events=0/0 (candidate/reference)
226
+
227
+ bones mean 1.000 over 8 measures
228
+ 1.000 count 3/3 how many bones
229
+ 1.000 names 3/3 the bone names themselves
230
+ 1.000 parent_by_name 3/3 each bone hangs off the same parent
231
+ 1.000 order 3/3 the bones are declared in the same order
232
+ 1.000 length_present 3/3 a setup `length` is present or absent alike
233
+ 1.000 inherit_present 3/3 a setup `inherit` is present or absent alike
234
+ 1.000 depth_histogram 3/3 NAME-AGNOSTIC: as many bones at each depth
235
+ 1.000 degree_sequence 3/3 NAME-AGNOSTIC: as many bones with each child count
236
+
237
+ bones (name-agnostic) mean 1.000 over 5 measures — the same two skeletons compared with names thrown away
238
+ …
239
+ animations mean 1.000 over 9 measures
240
+ 1.000 count 2/2 how many animations
241
+ 1.000 names 2/2 the animation names
242
+ 1.000 duration 2/2 each animation runs as long (last key time, within one frame)
243
+ 1.000 timeline_kinds 8/8 the same timelines exist
244
+ 1.000 key_counts 69/69 those timelines carry as many keys
245
+ 1.000 curve_kinds 69/69 as many linear / stepped / bezier keys
246
+ …
247
+ ```
248
+
249
+ Each pair is **matched / total**, where the total is the larger of the two sides. So a
250
+ count of `2/3` means one side has three of something and only two were matched — and
251
+ it does not say *which* side has three. The `..` line above is where you read that.
252
+
253
+ ⛔ **`diff` is blind to every coordinate.** No measure reads a bone's
254
+ `x`/`y`/`rotation`, an attachment's offset, or a key's value — only *presence*,
255
+ *names*, *counts*, *order* and *kinds*. §4.1 moves a pivot 236.5 units and every one
256
+ of the 49 measures still reads **1.000**. ⇒ Never take a green `diff` as evidence that
257
+ a geometric edit did not land, and never take it as evidence that one did.
258
+
259
+ ⛔ **And its ratios are not a score.** [`src/diff.ts`](../src/diff.ts) says so in the
260
+ type itself (*"Unweighted mean of the measures below. NOT a quality score"*), and the
261
+ report repeats it at the foot. It measures *agreement with a particular reference*,
262
+ which is the right question during transcription and the wrong question the moment the
263
+ task is to change something (§4.2, §4.3).
264
+
265
+ ⭐ **The name-agnostic groups are the ones to reach for when names are what differs.**
266
+ `bones` and `slots` each get a second pass with names thrown away — depth histogram,
267
+ degree sequence, shape histogram, declaration order of shapes, and for slots the
268
+ attachment types and bone-binding shapes by draw-order position. That is how you tell
269
+ *"the same rig with a different vocabulary"* from *"a different rig"*, and §4.2 is the
270
+ recipe built on it.
271
+
272
+ ### 1.4 `check` — the instrument that does see coordinates
273
+
274
+ ```bash
275
+ rigc check --candidate <a compiled skeleton> --frames <a rendered frame set>
276
+ ```
277
+
278
+ `check` never opens a reference skeleton. It renders the candidate onto the frames'
279
+ own pixel grid, fits it there by its own drawn pixels, and compares. For ingest that
280
+ gives you a loop nothing else in the toolchain provides, in two steps:
281
+
282
+ ```bash
283
+ # 1. turn the foreign export into a reference frame set
284
+ rigc render --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
285
+ --fps 12 --max 256 --out ref3
286
+
287
+ # 2. measure anything at all against it
288
+ rigc check --candidate work/t3 --frames ref3 \
289
+ --texture-from examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas
290
+ ```
291
+
292
+ 📐 **Establish the positive control first.** Point `check` at the same export the
293
+ frames came from, and it has to read zero. It does:
294
+
295
+ ```bash
296
+ rigc check --candidate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json \
297
+ --frames ref3/light
298
+ ```
299
+
300
+ ```
301
+ rigc check
302
+ candidate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
303
+ 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)
304
+ ⤷ 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)
305
+ 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).
306
+
307
+ ── light — candidate animation "light", 12 fps ──
308
+ MAE mean 0.00 worst 0.00 at f00-1 (0..255 over the union alpha; over the whole frame, mean 0.00)
309
+ slot drift worst 0.5 px "pendulum" at f0003
310
+ per-frame all 20 adjacent pair(s) change by as much as the reference's own frames do
311
+ ```
312
+
313
+ Two things to take from the control, both of which you need before reading any real
314
+ number: the **framing** resolved to the frames' own box at 0.00 px, so the MAE is a
315
+ comparison of pictures rather than of framings; and **slot drift still reads 0.5 px at
316
+ MAE 0.00**, which is that instrument's own floor rather than a difference.
317
+
318
+ ⚠️ **`--texture-from` is not optional on ingest work, and the reason is structural.**
319
+ rigc's default atlas is **one region per page at the art's own resolution**; the editor
320
+ packs many regions onto one page, often at a reduced `scale:`. Two builds of
321
+ geometrically identical data therefore sample differently-scaled texels in every frame,
322
+ and that difference is a constant no key can move. `check` says so unprompted, and
323
+ `--texture-from <the atlas the frames were rendered through>` measures it: same
324
+ geometry, swapped texels. §2.3 is what the resulting number means, and which atlas your
325
+ build should be using in the first place.
326
+
327
+ ### 1.5 `explain` — for the specs you write, not the file you were given
328
+
329
+ 📌 **`explain` does not read a compiled skeleton, and it is worth knowing that up
330
+ front** so you do not go looking for a reading tool that is not there:
331
+
332
+ ```bash
333
+ rigc explain examples/spineboy/export/spineboy-pro.json
334
+ ```
335
+
336
+ ```
337
+ rigc: give either --cut <name> --cuts <cuts.json>, or --rig/--motion/--out
338
+ ```
339
+
340
+ It takes the same arguments as `build` minus `--profile`, prints the resolved account
341
+ of **your** two spec files, and never gates. Which makes it a §2 instrument rather
342
+ than a §1 one — the thing you run to compare what you transcribed against the export
343
+ you transcribed it from, by eye:
344
+
345
+ ```bash
346
+ rigc explain --rig bench/transcriptions/3-timing-and-spacing/3-timing-and-spacing-ess.rig.json \
347
+ --motion bench/transcriptions/3-timing-and-spacing/3-timing-and-spacing-ess.motion.json \
348
+ --out work/x3
349
+ ```
350
+
351
+ ```
352
+ stage 945.1005 x 815.4317 (spine 4.3.13)
353
+
354
+ bones (spine world: y up)
355
+ root parent=- x=0 y=0
356
+ square parent=root x=380.7311 y=78.8596
357
+ bone parent=root x=204.753 y=708.0989 rotation=180
358
+
359
+ slots (array order IS the draw order)
360
+ pendulum bone=bone setup=pendulum color=ffffffff attachments=[pendulum]
361
+ square bone=square setup=square color=ffffffff attachments=[square]
362
+
363
+ animations
364
+ heavy declared=5.333333s loop=true
365
+ bone.rotate 20 key(s)
366
+ t=0 value=0 bezier[4]
367
+ t=0.233333 value=3.31738 bezier[4]
368
+ …
369
+ ```
370
+
371
+ ⚠️ **`--out` is required and nothing is written to it.** The flag is shared with
372
+ `build`'s parser; `explain` prints and exits. Passing a directory that does not exist
373
+ is fine — it is not created.
374
+
375
+ ---
376
+
377
+ ## 2. Transcription — the route that makes a foreign skeleton yours
378
+
379
+ Everything in §1 reads. To **change** anything you need specs, and getting specs out
380
+ of a skeleton is a job rigc does not do for you: the numbers come out of the JSON by
381
+ hand, into a rig spec and a motion spec, and `build` emits a new skeleton from those.
382
+
383
+ ⚠️ **Say this to the user before starting, because it is the part that surprises.**
384
+ Transcription is not a conversion step you run; it *is* the work. What you get for it
385
+ is that the file becomes editable by declaration — after transcription a pivot move is
386
+ two numbers in a spec (§4.1) and a new animation is an added block (§4.3), where
387
+ before it was a hand-edit of emitted JSON with nothing checking it.
388
+
389
+ ### 2.1 The workflow
390
+
391
+ 1. **Read the skeleton first, with `validate` and `render`.** The `SKIP` list is your
392
+ feature inventory (§1.1); the contact sheets are what the animations actually do.
393
+ Knowing there is no deform timeline before you start is worth more than discovering
394
+ it in the eleventh hour of transcribing one.
395
+ 2. **Get the loose art, at the size the export declares.** rigc measures PNGs rather
396
+ than trusting a size you typed (AUTHORING R5), so the art has to *be* the right
397
+ size. Take the target from the export's own attachments —
398
+ `3-timing-and-spacing` declares `"width": 745, "height": 212` for `pendulum`, and
399
+ the loose `pendulum.png` beside it is exactly 745×212. ⚠️ Do **not** take it from
400
+ the atlas region bounds: that page carries `scale: 0.5`, so `pendulum`'s bounds read
401
+ `373, 106`. Two numbers for one part, and the attachment's is the one in world
402
+ units. `--atlas-in` now does that division for you (§2.3), but it can only land
403
+ within the pack's own rounding — by hand, off the attachment, it is exact.
404
+ 3. **Transcribe the rig spec: header, bones, slots, skins.** Bones parents-first; the
405
+ `slots` array *is* the draw order (AUTHORING R4), so its order is data you are
406
+ copying and not a detail. Leave `invariants` out entirely — it describes rigc's own
407
+ formations, and an absent field makes an archetype assertion `SKIP`, never pass
408
+ (AUTHORING §3.7).
409
+ 4. **`explain`, then `build`.** `explain` first, because it prints what you wrote in a
410
+ shape you can compare against the export by eye (§1.5) and it never gates. Then
411
+ `build` under `--profile spine`.
412
+ 5. **Transcribe the motion spec, one animation at a time**, and `build` after each.
413
+ 6. **Close it with `diff` and `check`.** `diff` for structure; `check` against frames
414
+ rendered from the export for geometry; `--texture-from` to attribute the floor.
415
+
416
+ ### 2.2 One feature family at a time
417
+
418
+ 📌 **Transcribe by *kind*, not by animation.** All the bones, then all the slots, then
419
+ all the attachments, then one timeline kind across every animation. Two reasons, and
420
+ the second is the one that costs a day:
421
+
422
+ - A whole animation touches every feature the format has, so *"animation 1 of 6 done"*
423
+ means you have hit every unsolved problem at once and solved none of them cleanly.
424
+ - **`build` is all-or-nothing and emits only after green** (AUTHORING §0). A partial
425
+ transcription of one kind still builds; a half-transcribed animation may not build at
426
+ all, and then you are debugging your own incomplete work rather than the format.
427
+
428
+ ⚠️ **When a kind turns out not to be expressible, stop and say so — that is a finding,
429
+ not a blocker to route around.** [SPEC_COVERAGE.md](SPEC_COVERAGE.md) is the
430
+ per-skeleton survey of exactly this, and it records both directions honestly: rung 6's
431
+ row for weighted meshes reads *"⚠️ **This was wrong.**"* over a struck-out prediction
432
+ that they were inexpressible. ⇒ Check the survey for your feature before concluding
433
+ either way, and if it is genuinely absent, the shape of the answer is *"this export
434
+ uses X, which the motion spec cannot say"* with a pointer — not a silent
435
+ approximation.
436
+
437
+ 📎 **An editor export's curves usually need the raw `curve` escape hatch.** A named
438
+ easing is one curve reused; an export carries a different bezier per key per channel,
439
+ which no name can say. The motion spec's raw form takes absolute `(time, value)`
440
+ control points verbatim (AUTHORING §4.5). Named easings stay the right default for
441
+ motion you are *authoring* — this is the one case the escape hatch exists for, and the
442
+ 3-timing transcription's own `note` says so.
443
+
444
+ ### 2.3 What "byte-identical" can and cannot mean
445
+
446
+ State the ambition in the right units, because three different things get called
447
+ "identical" and only two are reachable.
448
+
449
+ | Ambition | Reachable? | What it costs, and what it proves |
450
+ | --- | --- | --- |
451
+ | **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 49 measures**. Aim here first |
452
+ | **Geometric agreement** — the same drawn pixels, allowing for the atlas | ✅ yes, and `check` measures it | see below |
453
+ | **Byte-identical JSON** | ⛔ **no, and not because of the geometry** | rigc writes defaults explicitly where the editor omits them, and the editor writes bookkeeping rigc has no field for. SPEC_COVERAGE records the count on rung 6: a field-by-field comparison against the reference export leaves **49 differences, every one benign** — 39 explicit defaults, 3 editor bookkeeping keys, 1 runtime version string, and 6 bone `icon` values, which was the only thing the rig spec could not say at all |
454
+
455
+ The geometric row needs a real number, because a naive reading of `check` makes an
456
+ exact transcription look wrong. Here is the 3-timing transcription against frames
457
+ rendered from the export it was transcribed from:
458
+
459
+ ```
460
+ ── heavy — candidate animation "heavy", 12 fps ──
461
+ 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).
462
+ MAE mean 6.42 worst 7.13 at f0064 (0..255 over the union alpha; over the whole frame, mean 0.27)
463
+ ⭐ texture floor 6.42 above it 0.00 (over the reference's own pixels, 6.42 and 0.00)
464
+ slot drift worst 0.7 px "pendulum" at f0017
465
+ per-frame all 64 adjacent pair(s) change by as much as the reference's own frames do
466
+ ```
467
+
468
+ ⭐ **`above it 0.00` is the whole result.** The MAE is 6.42 and **100 % of it is
469
+ texture**: the same geometry sampled through the reference's own atlas reads zero. The
470
+ transcription is geometrically exact, and the 6.42 is one-region-per-page at full
471
+ resolution meeting a 512×128 page declared at `scale: 0.5`. ⇒ **On ingest work, read
472
+ `above it` before you read the MAE.** Without `--texture-from` there is no way to tell
473
+ 6.42-that-is-all-texture from 6.42-that-is-all-rig, and the report warns you of exactly
474
+ that rather than leaving you to find out.
475
+
476
+ **Which atlas your build should use, then.** `build` has three atlas routes, and the
477
+ choice is an ingest decision rather than a detail. All three rows below are the same
478
+ specs, `--frames ref3/light`, against frames rendered from the export — one set, so
479
+ the figures compare:
480
+
481
+ | Route | What the emitted attachments say | `check --frames ref3/light` |
482
+ | --- | --- | --- |
483
+ | **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** |
484
+ | **`--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** |
485
+ | **`--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** |
486
+
487
+ 📌 **The first two are exact and indistinguishable**, and `--pack` is the one to reach
488
+ for when the deliverable is meant to look like an export: MaxRects onto shared pages,
489
+ byte-for-byte region copies, nothing resampled or rotated (AUTHORING §0.1). It does not
490
+ close the 6.36 — nothing that samples full-resolution texels can, against frames drawn
491
+ from a half-resolution pack — so the floor stays, `--texture-from` stays the way to
492
+ attribute it, and *packing does not change what the figure means.*
493
+
494
+ ⭐ **The third row is the mirror image of the first two, and reading it wrong is the
495
+ easy mistake.** Its MAE is the *lowest* of the three because it draws through the
496
+ reference's own texels — floor 0.00 — so what is left is geometry, and 2.03 of geometry
497
+ is the ONE PIXEL the pack cannot give back. `--atlas-in` divides a region's size by the
498
+ page's `scale:` (nine of the ten corpus atlases declare one — eight at 0.5, one at 0.4,
499
+ every file but `spineboy-run.atlas`), and the packer wrote `round(drawing × scale)`, so
500
+ a 373-texel region at `scale: 0.5` is consistent with both a 745- and a 746-pixel
501
+ drawing. rigc states 746, the export says 745, and putting the pixel back by hand takes
502
+ the same build to `x1.0000` / MAE **0.00** — which is how the residual is known to be
503
+ the rounding and nothing else.
504
+
505
+ ⇒ **The routes now differ in what their MAE is MADE OF rather than in whether they are
506
+ right.** Loose art or `--pack` gives exact geometry through coarser texels; `--atlas-in`
507
+ gives the reference's texels through geometry good to half a texel. Read `above it`
508
+ before the MAE either way, and read the `in units` line first — it is the line that
509
+ catches a whole-figure scale error, and it is the only one that does.
510
+
511
+ > 🕰️ **This row used to read `pendulum 373x106`, `square 80x80`, `x0.8092`, MAE
512
+ > 124.97 — the pack's texel counts taken as world sizes, so every attachment came out
513
+ > at half size, green, with nothing in the report saying so.** Found while writing this
514
+ > page and fixed as [issue #267](https://github.com/firejune/rigc/issues/267). The
515
+ > control that isolated it is now a selftest: import a pack with **no** `scale:` line
516
+ > (rigc's own `--pack` output writes none) and the skeleton is byte-identical to the
517
+ > loose build.
518
+
519
+ ### 2.4 The worked precedent, and what to take from it
520
+
521
+ Three transcriptions of official Spine exports live in this repository under
522
+ [`bench/transcriptions/`](https://github.com/firejune/rigc/tree/main/bench/transcriptions/)
523
+ — `3-timing-and-spacing` (3 bones, 2 slots, 2 animations, 69 keys, every curve raw),
524
+ `6-arcs` (weighted meshes, mesh `edges`, four 4.3 transform constraints) and
525
+ `spineboy` in both `ess` and `pro`. They are **worked examples you are meant to read**,
526
+ and the rig spec's own `note` field is the thing to read first:
527
+
528
+ > *"Mechanical transcription of Spine's official 3-timing-and-spacing `ess` export …
529
+ > written to prove that a rig spec can express a foreign skeleton at all. It is NOT an
530
+ > authored rig: the numbers were copied out of the reference, so it says nothing about
531
+ > whether an agent could produce them."*
532
+
533
+ ⭐ **Copy that habit, not just the technique.** A transcription's `note` should say what
534
+ it is, what it is not, and where its numbers came from — because the file otherwise
535
+ looks exactly like an authored rig and will be read as one by whoever opens it next.
536
+ The `images` line in those specs points out of the repository into the gitignored
537
+ `examples/` directory for the same reason: the art is fetched, not redistributed
538
+ ([NOTICE.md](../NOTICE.md)).
539
+
540
+ ⚠️ `bench/` does not ship in the npm package, so from an installed copy those files are
541
+ the link above rather than something on disk. Nor does `scripts/fetch-examples.sh` —
542
+ the example corpus is a repository-checkout facility, and every command line on this
543
+ page was run from one.
544
+
545
+ ---
546
+
547
+ ## 3. Complaints, and what each one means
548
+
549
+ Foreign data meets rigc's refusals in two waves: argument handling, before any rule
550
+ runs, and then the assertions.
551
+
552
+ ### 3.1 Before the assertions
553
+
554
+ These three exit 2 with one sentence and no report. Together with §0.2's stack trace
555
+ they are the whole set an ingest task realistically meets.
556
+
557
+ | Complaint | What it means | What to do |
558
+ | --- | --- | --- |
559
+ | `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 |
560
+ | `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 |
561
+ | `<file> is neither a directory nor a .json skeleton` | the path is a `.skel`, a `.spine`, or anything else | §5 — rigc reads JSON only |
562
+ | *(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 |
563
+
564
+ ### 3.2 Assertions a real export fails
565
+
566
+ 📊 **All twelve skeletons in the fetched corpus come back green** under the default
567
+ profile with the right atlas named. That is the baseline, and it is the honest headline:
568
+ **a correct editor export passes.** Getting there took one rule fixed, and the two
569
+ sections below are worth reading in full because the failures they describe mean
570
+ opposite things — the first is still reachable, and the second was the rule's fault.
571
+
572
+ **`A00_ROUNDTRIP_PARSE` — the atlas does not cover the skeleton.**
573
+
574
+ ```bash
575
+ rigc validate examples/spineboy/export/spineboy-ess.json \
576
+ --atlas examples/spineboy/export/spineboy-run.atlas
577
+ ```
578
+
579
+ ```
580
+ FAIL A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: eye-indifferent (attachment: eye-indifferent)
581
+ rigc: 1 assertion(s) failed
582
+ ```
583
+
584
+ **What it means:** the atlas you named is a real atlas and a valid one — it is just not
585
+ this skeleton's. `spineboy-run.atlas` packs only what the `run` animation needs. ⇒
586
+ **Read this as an atlas-choice failure, not as a broken skeleton.** It is the
587
+ downstream shape of guessing at §0.2's refusal, and it is precise about the cost: one
588
+ named attachment, so you can tell "wrong atlas" from "the export is missing a region"
589
+ by whether the missing names are a *coherent subset*. Fix by naming the right atlas —
590
+ `spineboy-ess.json` is green against `spineboy.atlas`.
591
+
592
+ **`A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` — and this one was the rule's fault.**
593
+
594
+ ```bash
595
+ rigc validate examples/spineboy/export/spineboy-pro.json \
596
+ --atlas examples/spineboy/export/spineboy.atlas
597
+ ```
598
+
599
+ ```
600
+ PASS A00_ROUNDTRIP_PARSE
601
+ PASS A10_NO_NAN_AFTER_STEPPING
602
+ …
603
+ PASS A35_DEFORM_KEYS_FIT_THE_ATTACHMENT
604
+ rigc: green
605
+ ```
606
+
607
+ **Why it is worth a section anyway.** That line used to be two `FAIL`s:
608
+
609
+ ```
610
+ FAIL A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: … key 1: offset 1 is odd, so the run's x values land on y slots and back again
611
+ FAIL A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: … key 1: the run holds 147 numbers and the deform array is x, y pairs
612
+ ```
613
+
614
+ and nothing was wrong with the data. `hoverboard-board` is an unweighted mesh with 148
615
+ floats; the key carries `offset: 1` and 147 values, covering `1..148` — the whole array
616
+ minus a leading zero the editor trimmed. A trim can land on a y component, so an odd
617
+ offset is what a trimmed run looks like, and Spine's own parser copies the run in at the
618
+ raw index with no alignment requirement anywhere. The proof was in the same report:
619
+ **`A00` and `A10` both PASSed on that file**, and `render` drew the `hoverboard`
620
+ animation.
621
+
622
+ ⭐ **The lesson survives the fix, and it is the reason to read this.** A validity rule
623
+ stricter than the runtime does not look like a bug — it looks like a finding about
624
+ somebody's file, and the honest reading of that message (*"your x values land on y
625
+ slots"*) sends an agent to change correct data. Fixed as
626
+ [issue #262](https://github.com/firejune/rigc/issues/262): the two parity clauses are
627
+ gone, and the remaining A35 clauses — the run fitting inside the deform array, finite
628
+ values, a non-empty key array, the attachment existing in the skin — are correct and
629
+ catch real breakage. The over-long run in particular is still refused, and still the
630
+ quietest defect the format has.
631
+
632
+ 🚨 **The general lesson matters more than the specific bug.** A `FAIL` on foreign data
633
+ has three possible meanings and the message alone does not separate them:
634
+
635
+ 1. **the data is broken** — fix the data;
636
+ 2. **the input was wrong** — wrong atlas, missing page, truncated file (§3.1, and
637
+ `A00` above);
638
+ 3. **the rule is stricter than the runtime** — fix the rule, or file it.
639
+
640
+ ⇒ Before changing anybody's export because rigc objected, check case 3: does the file
641
+ **parse** (`A00`), **step without NaN** (`A10`), and **render**? If all three, the
642
+ runtime is content and the burden is on the rule. Reporting that is a better answer
643
+ than a quietly edited export.
644
+
645
+ ### 3.3 Profile choice, and the fourteen rules that will not fire
646
+
647
+ `--profile spine` is the default and answers *"is this valid Spine 4.3 that any
648
+ runtime plays correctly?"* — and it is the right profile for foreign data, because the
649
+ other one is this project's own renderer and archetype policy.
650
+
651
+ **Fourteen assertions do not run under `spine`, and they come back `PROF`, not
652
+ `SKIP`:**
653
+
654
+ | Excluded as | Rules |
655
+ | --- | --- |
656
+ | **renderer policy** (7) | `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` |
657
+ | **archetype policy** (7) | `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` |
658
+
659
+ Three further rules — **`A06`**, **`A08`** and **`A20`** — are *mixed*: their validity
660
+ clauses run in both profiles and their policy clauses only under `spine-html`. `A06`'s
661
+ size-vs-PNG check is validity; one-part-per-page coverage, rotation and premultiplied
662
+ alpha are policy. `A08`'s attachment→region join is validity; requiring the two names
663
+ to be *identical* is policy. `A20`'s weight coherence is validity; requiring a mesh to
664
+ be weighted at all is policy.
665
+
666
+ ⚠️ **`--profile spine-html` on foreign data produces a wall of failures that mean
667
+ nothing about the file.** Same `spineboy-pro.json`, same atlas, one flag changed — the
668
+ run ends `rigc: 53 assertion(s) failed`, and this is the tally with one real message
669
+ per rule:
670
+
671
+ | Count | Rule | One of its messages |
672
+ | --- | --- | --- |
673
+ | **40** | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | `region "crosshair" has UVs (0.181640625,0.06640625)-(0.2255859375,0.2421875); one part per page must cover the page exactly` |
674
+ | **10** | `A15_IDLE_NO_MESH_BONE_KEYS` | `idle keys bone "front-shoulder", which drives a mesh — meshes never idle-skip` |
675
+ | **2** | `A20_MESH_WEIGHTS_COHERENT` | `mesh "hoverboard-board" is unweighted; the ring tier drives meshes by bones` |
676
+ | **1** | `A11_NO_CLIPPING_ATTACHMENTS` | `1 clipping attachment(s); the renderer skips them silently` |
677
+
678
+ Every one of those is a correct statement about a correct file: the atlas *is* packed,
679
+ a bone *does* key a mesh, a mesh *is* unweighted, a clipping attachment *is* present.
680
+ (The tally was 55 before §3.2's A35 was fixed, and that is the one entry that was *not*
681
+ a correct statement — which is why it belonged in a different section from these.)
682
+ And it is not a big-skeleton problem — `3-timing-and-spacing`, with two regions on one
683
+ page, fails `A06` twice for the same reason. ⇒ **Do not run `spine-html` against
684
+ somebody's export unless they asked whether it satisfies this project's renderer
685
+ policy**, which is a different question from whether their file is valid.
686
+
687
+ ⚠️ **And do not read the absence of `SKIP` lines as thoroughness.** Under `spine` a
688
+ foreign skeleton typically produces *no* archetype `SKIP` at all — the profile excludes
689
+ those rules before their bodies could notice the missing `invariants` block, so they
690
+ arrive as `PROF`. The `PROF` list is where *"was this held to that rule at all"* gets
691
+ answered (AUTHORING §7).
692
+
693
+ ---
694
+
695
+ ## 4. Recipes
696
+
697
+ Each of these starts from a transcription (§2), and none is an edit to emitted JSON —
698
+ an edit to emitted JSON is a change nothing in the toolchain checked. All three were
699
+ run from copies of the stored 3-timing transcription:
700
+
701
+ ```bash
702
+ T=bench/transcriptions/3-timing-and-spacing
703
+ mkdir -p work/repivot
704
+ cp $T/3-timing-and-spacing-ess.rig.json work/repivot/repivot.rig.json
705
+ cp $T/3-timing-and-spacing-ess.motion.json work/repivot/repivot.motion.json
706
+ ```
707
+
708
+ ⚠️ A copied spec's own `images` path is relative to where the spec was, so it breaks
709
+ on the copy. `--images` overrides it, relative to your working directory, and every
710
+ `build` below passes it.
711
+
712
+ ### 4.1 Moving a pivot without moving the art
713
+
714
+ The ask: *"the arm should swing from its middle, not its end — don't change the
715
+ drawing."* This is the recipe the rest of the section is measured against, because its
716
+ correctness criterion is exact and checkable without rendering anything.
717
+
718
+ **What changes in the file:**
719
+
720
+ | Object | Change | Why |
721
+ | --- | --- | --- |
722
+ | **the bone** | its `x`/`y` move to the new pivot, expressed in its **parent's** local axes | the bone's origin *is* the pivot |
723
+ | **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 |
724
+ | **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 |
725
+ | **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 |
726
+
727
+ ⚠️ **The child-bone row is the one that gets forgotten**, and it fails quietly: the
728
+ re-pivoted bone's own art lands correctly and everything hanging off it is displaced by
729
+ exactly the vector you moved. If the bone has children, correct them in the same edit,
730
+ or the fix looks half-right in a way no assertion will mention.
731
+
732
+ **Worked.** 3-timing's `bone` sits at `x: 204.753, y: 708.0989` with `rotation: 180`
733
+ and `length: 473`; the `pendulum` attachment is at bone-local
734
+ `x: 316.79, y: 0.4815389`. Move the pivot half the bone's length along the bone's own
735
+ +x axis, `d = 236.5`: the bone's rotation is 180° in its parent's frame, so
736
+ `R(180)·(d, 0) = (-d, 0)`, and this bone has no children.
737
+
738
+ ```diff
739
+ { "name": "bone", "parent": "root", "length": 473,
740
+ - "x": 204.753, "y": 708.0989, "rotation": 180 }
741
+ + "x": -31.747, "y": 708.0989, "rotation": 180 }
742
+
743
+ "pendulum": { "pendulum": { "image": "pendulum.png",
744
+ - "x": 316.79, "y": 0.4815389, "rotation": -179.81934 } }
745
+ + "x": 80.29, "y": 0.4815389, "rotation": -179.81934 } }
746
+ ```
747
+
748
+ ```bash
749
+ rigc build --rig work/repivot/repivot.rig.json \
750
+ --motion work/repivot/repivot.motion.json \
751
+ --images examples/3-timing-and-spacing/images \
752
+ --out work/t3b
753
+ ```
754
+
755
+ **Verify the invariant first, arithmetically.** The art's centre in world coordinates
756
+ is `bone(x, y) + R(bone.rotation) · att(x, y)`. Computed from the two built skeletons,
757
+ before and after:
758
+
759
+ ```
760
+ original bone [204.753, 708.0989] att [316.79, 0.4816] -> art centre [-112.0370, 707.6174]
761
+ re-pivot bone [-31.747, 708.0989] att [ 80.29, 0.4816] -> art centre [-112.0370, 707.6174]
762
+ art centre moved by 2.842e-14 units
763
+ ```
764
+
765
+ ⭐ **That is the criterion.** If the art's world position at the setup pose moves by
766
+ more than floating-point noise, the compensation is wrong, and no amount of looking at
767
+ frames will tell you which of the two numbers to blame.
768
+
769
+ **Then confirm the movement did change**, which is the half a pose cannot show. The
770
+ same displacement evaluated across the bone's own rotation:
771
+
772
+ | `rotate` | original art centre | re-pivot art centre | apart |
773
+ | --- | --- | --- | --- |
774
+ | 0° | `[-112.04, 707.62]` | `[-112.04, 707.62]` | **0.00 units** |
775
+ | 15° | `[-101.12, 625.64]` | `[-109.18, 686.85]` | 61.74 |
776
+ | 45° | `[ -18.91, 483.75]` | `[ -88.18, 650.98]` | 181.01 |
777
+ | 90° | `[ 205.23, 391.31]` | `[ -31.27, 627.81]` | 334.46 |
778
+ | 180° | `[ 521.54, 708.57]` | `[ 48.54, 708.58]` | 473.00 |
779
+
780
+ **What the instruments say about it** — and this pair is why §1.3 carries its warning:
781
+
782
+ - **`diff` sees nothing.** `rigc diff work/t3b/skeleton.json <the export>` reads
783
+ **1.000 on all 49 measures**: same bones, names, parents, order, slots, draw order,
784
+ attachments, animations, timelines, key counts, curve kinds. All true, and all silent
785
+ about a 236.5-unit move.
786
+ - **`check` sees it loudly**, with the framing pinned so the comparison is of pictures
787
+ and not of framings:
788
+
789
+ ```bash
790
+ rigc check --candidate work/t3b --frames ref3/heavy \
791
+ --viewport -573.3,-81.2,2176.3,990.1 \
792
+ --texture-from examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas \
793
+ --all-frames
794
+ ```
795
+
796
+ ```
797
+ MAE mean 106.91 worst 116.67 at f0012 (0..255 over the union alpha; over the whole frame, mean 7.15)
798
+ ⭐ texture floor 3.62 above it 105.79 (over the reference's own pixels, 1.95 and 92.51)
799
+ slot drift worst 42.4 px "pendulum" at f0028
800
+ chain slots worst slot drift mean MAE in it share
801
+ square 1/1 0.4 px "square" f0000 0.3 px 10.20 3.2%
802
+ bone 1/1 42.4 px "pendulum" f0028 31.3 px 125.70 77.3%
803
+ (unattributed) — — — — 19.5%
804
+
805
+ frame MAE union px Δpx ref Δ worst slot drift how slots note
806
+ f0000 5.98 1246 — — square 0.4 component 2/2
807
+ f0001 6.18 1245 19 40 square 0.4 component 2/2
808
+ f0002 19.58 1280 456 520 pendulum 0.7 component 2/2
809
+ f0003 46.11 1412 702 815 pendulum 2.1 component 2/2
810
+ f0004 75.88 1608 808 939 pendulum 4.3 component 2/2
811
+ f0005 93.32 1776 996 1125 pendulum 8.0 component 2/2
812
+ ```
813
+
814
+ ⭐ **Read the per-frame column before the mean.** `f0000` is **5.98** — the
815
+ transcription's own texture floor, i.e. *no difference at all* — and it climbs
816
+ monotonically from `f0002` as the bone rotates. That is the pivot's whole signature:
817
+ **invisible in the pose, and everything in the movement**, which is what MOTION §3.9
818
+ argues from the authoring side and what this measures from the ingest side. The
819
+ `chains` table puts 77.3 % of the difference on the `bone` chain and 3.2 % on
820
+ `square`, which is the edit's own blast radius.
821
+
822
+ ⚠️ **Pin `--viewport` for a re-pivot check.** Drop the flag and the same run reads
823
+ `MAE mean 120.65` with `slot drift worst 30.3 px "pendulum" at f0000` — and that drift
824
+ at frame 0 is an artefact, because the re-pivot changed the skeleton's **world extent**,
825
+ so `frames.json`'s box came back `REFUSED, coordinates` and the framing was fitted
826
+ instead:
827
+
828
+ ```
829
+ 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)
830
+ 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).
831
+ ```
832
+
833
+ ⇒ On any edit that changes the extent, take the viewport from the reference render's
834
+ own `world x[…] y[…]` line, or read `check`'s framing lines before its figures.
835
+
836
+ ### 4.2 Renaming, and the name-agnostic mindset
837
+
838
+ The ask: *"give everything our project's names."* Mechanically a rename pass over the
839
+ rig spec; the discipline is in what you check afterwards.
840
+
841
+ **Everything in a rig spec resolves by name, and a miss is refused by name.** A bone's
842
+ `parent`, a slot's `bone`, a constraint's `bones` and `target`, a draw-order key's
843
+ `slot`, an authored mesh's vertex `weights` — and, across the two files, the motion
844
+ spec's `archetype` against the rig spec's `name`. That last one is the first refusal a
845
+ rename produces, before anything else has a chance to go wrong:
846
+
847
+ ```
848
+ rigc compile error: …/work/renamed/pendulum-rig.motion.json: motion spec names
849
+ archetype "3-timing-and-spacing-ess" but the rig spec at
850
+ …/work/renamed/pendulum-rig.rig.json is called "pendulum-rig"
851
+ ```
852
+
853
+ ⭐ **A rename is therefore mostly safe by construction, and its failures arrive as
854
+ sentences naming both sides.** That is the reason to do it in the specs rather than in
855
+ emitted JSON, where the same mistake is a silently unresolved reference.
856
+
857
+ **Then check it with the name-agnostic groups**, because after a rename the name-keyed
858
+ measures are *supposed* to disagree. Renaming `bone`→`arm`, `square`→`block`, the two
859
+ slots to `arm-art`/`block-art` and their attachments to match:
860
+
861
+ ```
862
+ bones mean 0.567 over 8 measures
863
+ 1.000 count 3/3 how many bones
864
+ 0.200 names 1/5 the bone names themselves
865
+ 0.333 parent_by_name 1/3 each bone hangs off the same parent
866
+ 0.333 order 1/3 the bones are declared in the same order
867
+ 0.333 length_present 1/3 a setup `length` is present or absent alike
868
+ 0.333 inherit_present 1/3 a setup `inherit` is present or absent alike
869
+ 1.000 depth_histogram 3/3 NAME-AGNOSTIC: as many bones at each depth
870
+ 1.000 degree_sequence 3/3 NAME-AGNOSTIC: as many bones with each child count
871
+
872
+ bones (name-agnostic) mean 1.000 over 5 measures — the same two skeletons compared with names thrown away
873
+ 1.000 count 3/3 how many bones
874
+ 1.000 depth_histogram 3/3 as many bones at each depth
875
+ 1.000 degree_sequence 3/3 as many bones with each child count
876
+ 1.000 shape_histogram 3/3 as many bones of each depth-and-child-count shape (`d1c3` = one hop down, three children)
877
+ 1.000 order_shape 3/3 the bones are declared in the same order of shapes
878
+
879
+ slots mean 0.143 over 7 measures
880
+ 1.000 count 2/2 how many slots
881
+ 0.000 names 0/4 the slot names themselves
882
+ 0.000 order 0/2 the slots array IS the draw order, so its order is data
883
+ 0.000 bone 0/2 each slot is bound to the same bone
884
+ 0.000 attachment 0/2 each slot shows the same setup attachment
885
+ 0.000 blend 0/2 each slot uses the same blend mode
886
+ 0.000 color_present 0/2 a tint is present or absent alike
887
+
888
+ slots (name-agnostic) mean 1.000 over 4 measures — the same two skeletons compared with names thrown away
889
+ 1.000 count 2/2 how many slots
890
+ 1.000 attachment_types_by_position 2/2 the same kind of attachment sits at each position in the draw order
891
+ 1.000 bone_binding_shape 2/2 as many slots hang off a bone of each shape (`?` = no such bone is declared)
892
+ 1.000 order_shape 2/2 the draw order is the same order of `<attachment type>@<bone shape>`
893
+
894
+ attachments mean 0.889 over 9 measures
895
+ 1.000 skins 1/1 the skin names
896
+ 1.000 count 2/2 how many attachments
897
+ 0.000 names 0/4 skin/slot/attachment keys
898
+ 1.000 type_counts 2/2 as many of each attachment type
899
+ …
900
+ animations mean 1.000 over 9 measures
901
+ ```
902
+
903
+ Three things to read out of that, in order:
904
+
905
+ - **The name-keyed collapse is the task, not a defect.** `bones` 0.567, `slots` 0.143,
906
+ `attachments` 0.889. Note that several *non*-name measures fall with them —
907
+ `slots.bone`, `slots.blend`, `bones.parent_by_name` — because they are keyed **by**
908
+ the name that changed. They are not saying the binding changed.
909
+ - 🚨 **`bones (name-agnostic)` and `slots (name-agnostic)` must stay 1.000.** They
910
+ measure the rig with the vocabulary thrown away, so a rename that changed only names
911
+ leaves them untouched. **A drop there is a structural mistake wearing a rename's
912
+ clothes**, and it is the only assertion this recipe really has.
913
+ - **`animations` stays 1.000** because animation *names* were not part of the ask.
914
+ Under the same edit `bones.names` reads `1/5` — `root` survived, and the total is the
915
+ union of both vocabularies.
916
+
917
+ ⛔ And remember §1.3: `diff` reads no coordinates either way. A rename that also moved
918
+ something is invisible to every measure in that report. Pair it with a `check` against
919
+ frames rendered from the original.
920
+
921
+ ⚠️ **Do not rename toward what a rule seems to want.** `A08`'s name-identity clause and
922
+ `A27`'s region-name-matches-page-filename are both `spine-html` policy (§3.3): under
923
+ the default profile they do not fire, and renaming somebody's attachments to satisfy a
924
+ policy they never opted into is a change with no benefit to them.
925
+
926
+ ### 4.3 Extending a foreign skeleton with a new animation
927
+
928
+ The ask: *"add a `nudge` to this."* There is no append — `build` re-emits the whole
929
+ skeleton — so the extension is an added block in the motion spec of a transcription
930
+ that already round-trips.
931
+
932
+ 1. **Get the transcription to structural agreement first** (§2.3), and record the
933
+ figure. Extending an unfinished transcription mixes two kinds of difference into
934
+ every measurement after it.
935
+ 2. **Add the animation to the motion spec.** Named easings here, not raw curves —
936
+ §2.2's escape hatch is for reproducing an export's own beziers, and this movement
937
+ has no export behind it. What goes *between* the poses is [MOTION.md](MOTION.md);
938
+ this page stops at the mechanics.
939
+ 3. **`build`, and read the count line.**
940
+
941
+ ```bash
942
+ rigc build --rig work/extend/extend.rig.json \
943
+ --motion work/extend/extend.motion.json \
944
+ --images examples/3-timing-and-spacing/images \
945
+ --out work/t3c
946
+ ```
947
+
948
+ ```
949
+ .. 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
950
+ ```
951
+
952
+ `animations=3` where the export had 2. That line is the cheapest confirmation the
953
+ block landed at all.
954
+ 4. **Read `diff` knowing what it is about to say.**
955
+
956
+ ```
957
+ animations mean 0.793 over 9 measures
958
+ 0.667 count 2/3 how many animations
959
+ 0.667 names 2/3 the animation names
960
+ 0.667 duration 2/3 each animation runs as long (last key time, within one frame)
961
+ 0.889 timeline_kinds 8/9 the same timelines exist
962
+ 0.958 key_counts 69/72 those timelines carry as many keys
963
+ 0.958 curve_kinds 69/72 as many linear / stepped / bezier keys
964
+ 1.000 event_keys 0/0 as many event firings — neither side has any
965
+ 0.667 draw_order 2/3 a draw-order timeline is present or absent alike
966
+ 0.667 deform 2/3 a deform timeline is present or absent alike
967
+ ```
968
+
969
+ 🚨 **Every one of those got worse, and that is the correct result.** The
970
+ `animations` section went 1.000 → 0.793 because the candidate now has something the
971
+ reference does not. `diff` measures agreement with a reference; you were asked to
972
+ *disagree* with it, in one specific way. ⇒ **Check that the drop is confined to the
973
+ `animations` section and is the size the addition explains** — one animation of
974
+ three, three keys of seventy-two — and that `bones`, `slots`, `attachments` and
975
+ `constraints` are all still 1.000. That last part is the real assertion here: *the
976
+ extension changed nothing it was not supposed to change.*
977
+ 5. **Look at it, then ask.** `render --animation nudge` and open the contact sheet;
978
+ `vote` the foreign original against your extended build when the question is whether
979
+ the new movement belongs beside the old ones. Nothing in this toolchain can answer
980
+ that, and MOTION §4–§5 is how to shape the ballot so the answer informs.
981
+
982
+ ---
983
+
984
+ ## 5. Non-goals — stated, so nobody proposes them as gaps
985
+
986
+ 🚫 **rigc does not read editor project files.** A `.spine` is the editor's own project
987
+ format, not skeleton data, and no rigc command opens one. The path form refuses it by
988
+ name:
989
+
990
+ ```
991
+ rigc: …/hero.spine is neither a directory nor a .json skeleton
992
+ ```
993
+
994
+ The route from a project file to rigc is the one the editor already provides: export
995
+ it, and start from the export. (The official examples' project files are public
996
+ domain — [NOTICE.md](../NOTICE.md) — and `scripts/fetch-examples.sh` does not download
997
+ them, because nothing here could use one.)
998
+
999
+ 🚫 **Binary `.skel` is not read, and the honest statement has two halves.** rigc's
1000
+ dependency *can* read it and rigc *does not*:
1001
+
1002
+ - `@esotericsoftware/spine-core@4.3.13` exports `SkeletonBinary`, whose
1003
+ `readSkeletonData` is the binary reader.
1004
+ - **rigc's own source references it zero times.** Every read path goes through
1005
+ `JSON.parse` and `SkeletonJson`, and the path resolver requires a `.json` extension —
1006
+ so a `.skel` is refused by the same sentence a `.spine` is, one layer before any
1007
+ format question arises.
1008
+
1009
+ ⇒ Binary support is *reachable* rather than *present*: a plumbing job on a dependency
1010
+ that already has the reader, not a parser to write. But it is not there, and nothing on
1011
+ this page works on a `.skel` today. Re-export as JSON.
1012
+
1013
+ 🚫 **No skeleton-to-spec decompiler.** Nothing turns skeleton JSON back into a rig spec
1014
+ and a motion spec. §2 is hand work, and that is the current state rather than a
1015
+ temporary one: a decompiler would have to invent the things the spec format exists to
1016
+ make explicit — which pivot, which generator, which invariant — and the compiler's own
1017
+ rule is that it never invents a value that is not in the spec. ⚠️ Not to be confused
1018
+ with the *atlas* importer below, which is a different direction and does exist.
1019
+
1020
+ ✅ **A packer and an importer both exist now, so do not report them as gaps.** This
1021
+ non-goal used to read *"rigc emits one region per page and cannot do otherwise"*, and
1022
+ [issue #4](https://github.com/firejune/rigc/issues/4) closed it: `build --pack` writes
1023
+ shared pages losslessly and `build --atlas-in` resolves against a pack somebody else
1024
+ made (AUTHORING §0.1–§0.2). One-region-per-page is now the **default**, not the only
1025
+ shape. `--atlas-in` applies the page's `scale:`, so an imported pack states the
1026
+ drawing's size rather than the pack's — to within the pack's own rounding, which is
1027
+ §2.3's row and the whole of [issue #267](https://github.com/firejune/rigc/issues/267).
1028
+
1029
+ 🚫 **No CLI unpacker, so `pose` needs loose art.** `pose` reads *loose part PNGs*
1030
+ against one picture. A foreign export hands you a packed page instead, and pointing
1031
+ `pose` at one is worse than useless — it treats the whole page as a single part and
1032
+ answers confidently:
1033
+
1034
+ ```bash
1035
+ mkdir -p packed-only
1036
+ cp examples/3-timing-and-spacing/export/3-timing-and-spacing.png packed-only/
1037
+ rigc pose --images packed-only --frame 'ref-big/light@4fps/f0002.png'
1038
+ ```
1039
+
1040
+ ```
1041
+ rigc pose
1042
+ .. frame …/ref-big/light@4fps/f0002.png (900x409)
1043
+ .. ground rgb(232, 232, 232) over 100% of the border ring
1044
+ .. parts …/packed-only (1 png)
1045
+ .. search scale 0.5–2 in 7 step(s) · rotation -180°–180° step 15° · refuse above residual 0.25
1046
+ PLACE 3-timing-and-spacing.png x= 324.1 y= 188.0 rot= -91.4° scale=0.632 residual=0.2078 unexplained= 29%
1047
+ found on a 7x3 anchor grid, step 4 at 32x reduction
1048
+ ```
1049
+
1050
+ ⚠️ **`residual=0.2078` is *under* the default 0.25 refusal bar**, so nothing refused
1051
+ it, and `PLACE` rather than `AMBIG` means nothing flagged it either. With the same frame
1052
+ and the two real loose parts, the answer is what it should be:
1053
+
1054
+ ```bash
1055
+ rigc pose --images examples/3-timing-and-spacing/images \
1056
+ --frame 'ref-big/light@4fps/f0002.png' --scale 0.3,0.6
1057
+ ```
1058
+
1059
+ ```
1060
+ .. parts …/examples/3-timing-and-spacing/images (2 png)
1061
+ .. search scale 0.3–0.6 in 4 step(s) · rotation -180°–180° step 15° · refuse above residual 0.25
1062
+ PLACE pendulum.png x= 319.3 y= 214.5 rot= -88.9° scale=0.412 residual=0.0410 unexplained= 7%
1063
+ found on a 12x5 anchor grid, step 5 at 16x reduction
1064
+ PLACE square.png x= 437.2 y= 343.1 rot= -0.1° scale=0.410 residual=0.0330 unexplained= 4%
1065
+ found on a 57x26 anchor grid, step 4 at 4x reduction
1066
+ ```
1067
+
1068
+ ⇒ **Check what is in `--images` before trusting a `pose` report on ingest work.** One
1069
+ PNG where you expected several is the tell, and the `.. parts` line prints the
1070
+ count. AUTHORING §11.4 is the rest of what that command cannot see.
1071
+
1072
+ 📎 To be exact about what is missing: rigc *can* lift a region's drawing back off a
1073
+ page — `extractRegion` does it, and the contour mesh generator uses it under
1074
+ `--atlas-in` — so what is absent is a **command**, not the capability. It refuses by
1075
+ name on a region packed `rotate: 90` (AUTHORING §0.2), which a foreign pack can be and
1076
+ rigc's own never is.
1077
+
1078
+ 🚫 **No `validate --fix`, and no normalisation pass.** Every recipe in §4 is a change
1079
+ you state in a spec and rebuild. A tool that rewrote somebody's export in place would
1080
+ be making decisions on their behalf with nowhere to say it had — and, per §3.2, some of
1081
+ those decisions would be wrong about the rule rather than about the file.
1082
+
1083
+ 🚫 **`diff` will not be given coordinate measures to make it a geometry check.**
1084
+ `check` is the geometry instrument, and it works by rendering, which is the only way to
1085
+ compare two rigs that may be authored in different coordinate systems at different
1086
+ scales. A coordinate diff between two skeletons would be arithmetic on numbers that do
1087
+ not compare — `check`'s own report says the two world boxes *"are different coordinate
1088
+ systems and do not compare; the pixel grid does."*
1089
+
1090
+ ---
1091
+
1092
+ ## Appendix — the corpus this page was verified against
1093
+
1094
+ Every command line above was run from a checkout with `bun run fetch-examples`
1095
+ completed. Two skeletons carry all of it:
1096
+
1097
+ | Example | Files used | Licence |
1098
+ | --- | --- | --- |
1099
+ | **`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 |
1100
+ | **`spineboy`** | `export/spineboy-pro.json`, `export/spineboy-ess.json`, `export/spineboy.atlas`, `export/spineboy-run.atlas` | `license.txt` present, © 2013 Esoteric Software LLC |
1101
+
1102
+ The working directories the commands write into — `ref3/`, `ref-big/`, `render/`,
1103
+ `work/`, `packed-only/` — are throwaway and none is committed.
1104
+
1105
+ ⚠️ **`7-anticipation` ships no `license.txt` upstream**, so the redistribution grant its
1106
+ siblings carry does not exist for it. No excerpt, figure or image on this page comes
1107
+ from it; the only places it appears at all are the two corpus-wide tallies — §3.2's
1108
+ *twelve of twelve skeletons* and §2.3's *nine of ten atlases* — both of which count
1109
+ every directory the fetch produced.
1110
+ `scripts/fetch-examples.sh` prints a warning naming it; [NOTICE.md](../NOTICE.md) has
1111
+ the per-directory terms.
1112
+
1113
+ The transcription these recipes start from is
1114
+ [`bench/transcriptions/3-timing-and-spacing/`](https://github.com/firejune/rigc/tree/main/bench/transcriptions/3-timing-and-spacing/),
1115
+ unmodified. The re-pivoted, renamed and extended variants in §4 were built from copies
1116
+ of it; none is committed, because each is an illustration of an edit rather than a
1117
+ transcription of anything.