spine-rigc 0.4.0 β†’ 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="assets/banner.svg" alt="rigc - Rig compiler for Spine" width="100%" />
2
+ <img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/banner.svg" alt="rigc - Rig compiler for Spine" width="100%" />
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -53,6 +53,252 @@ weights. Every one of those loads clean, plays, and is wrong. rigc's answer is t
53
53
  make the failure legible: compile from a spec, round-trip through the real parser,
54
54
  run a list of named assertions, and **write nothing unless all of them are green.**
55
55
 
56
+ ## Install
57
+
58
+ πŸ“¦ **rigc measures loose PNGs directly β€” one atlas page per image.** It is not an
59
+ atlas packer: packing several regions onto one page is tracked as
60
+ [issue #4](https://github.com/firejune/rigc/issues/4), not something the tool does
61
+ today.
62
+
63
+ rigc runs on [Bun](https://bun.sh). The package ships its TypeScript sources and
64
+ Bun runs them, so there is no build step and no `dist/` that can drift from the
65
+ repository it was cut from.
66
+
67
+ **The npm package is `spine-rigc`; the command it installs is `rigc`.** npm
68
+ refuses the name `rigc` as too similar to packages that already exist, so the
69
+ project, this repository and the executable keep their name and only the
70
+ registry entry is spelled out.
71
+
72
+ ```bash
73
+ bunx spine-rigc --help # run it without installing
74
+ bun add -g spine-rigc # or install the command
75
+ bun add -d spine-rigc # or pin it in a project
76
+ ```
77
+
78
+ `npx spine-rigc` works too, as long as Bun is on `PATH` β€” the executable is a
79
+ Bun script, and npm only writes the shim that calls it.
80
+
81
+ Installed, the command is `rigc`. The examples below spell it `bun cli.ts`
82
+ because they are written from a clone of this repository (`bun install`, then run
83
+ the CLI in place); the two are interchangeable β€” `rigc build …` is
84
+ `bun cli.ts build …`.
85
+
86
+ Two commands are repository workflows rather than package ones: `bench` and
87
+ `check` measure against Spine's official example projects and the reference
88
+ frames rendered from them, which are fetched rather than redistributed (see
89
+ [NOTICE.md](NOTICE.md)). They need a clone and `bun run fetch-examples`, and say
90
+ so by name when the corpus is absent.
91
+
92
+ ## First rig in ten minutes
93
+
94
+ A whole rig, end to end, in a scratch directory: three tiny plates, two JSON
95
+ files, one `build`, one `validate`. No clone, no art pipeline, nothing fetched.
96
+
97
+ 🚫 **Every value below is invented for this section** β€” a doll that exists
98
+ nowhere else in this repository. That is [AUTHORING.md](docs/AUTHORING.md) Β§3's
99
+ rule applied here: no example value in these documents is copied out of a
100
+ reference export, so nothing you read in a quickstart is an answer to anything
101
+ [the ladder](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) measures.
102
+
103
+ **1. Install the command.**
104
+
105
+ ```bash
106
+ bun add -g spine-rigc # installs `rigc`
107
+ ```
108
+
109
+ Or skip the install and prefix every command below with `bunx `, e.g.
110
+ `bunx spine-rigc build …`.
111
+
112
+ **2. Make a directory and three plates.** rigc measures PNGs rather than trusting
113
+ a number you typed (R5), so the art has to exist. These three are solid colours a
114
+ few dozen pixels across β€” a hull, a mast and a lamp:
115
+
116
+ ```bash
117
+ mkdir -p buoy/images && cd buoy
118
+ bun -e '
119
+ const parts = {
120
+ "images/hull.png": "iVBORw0KGgoAAAANSUhEUgAAADgAAAAMCAYAAAA3bX6lAAAAKElEQVR42mOI8bL6P5wxw6gHRz046sFRD456cNSDox4c9eCoBwcrBgDSZ+mdl2OiDgAAAABJRU5ErkJggg==",
121
+ "images/mast.png": "iVBORw0KGgoAAAANSUhEUgAAAAgAAAA0CAYAAAC3t3ldAAAAH0lEQVR42mO4dunIf3yYYVTBqIJRBaMKRhWMKhgcCgBGJo4s9YnopgAAAABJRU5ErkJggg==",
122
+ "images/lamp.png": "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAHElEQVR42mP4v8HhPzUww6hBowaNGjRq0HAzCADvdrVmFPbc+QAAAABJRU5ErkJggg=="
123
+ };
124
+ for (const [p, b] of Object.entries(parts)) await Bun.write(p, Buffer.from(b, "base64"));
125
+ '
126
+ ```
127
+
128
+ **3. The rig spec β€” `buoy.rig.json`.** Structure only: bones, the slots array in
129
+ draw order, and one skin mapping each slot to a plate.
130
+
131
+ ```json
132
+ {
133
+ "spec": "rigc-rig/1",
134
+ "name": "buoy",
135
+ "images": "images",
136
+ "skeleton": { "width": 200, "height": 200 },
137
+ "bones": [
138
+ { "name": "root" },
139
+ { "name": "hull", "parent": "root", "x": 0, "y": 0 },
140
+ { "name": "mast", "parent": "hull", "x": 0, "y": 4 },
141
+ { "name": "lamp", "parent": "mast", "x": 0, "y": 52 }
142
+ ],
143
+ "slots": [
144
+ { "name": "mast", "bone": "mast", "attachment": "mast" },
145
+ { "name": "hull", "bone": "hull", "attachment": "hull" },
146
+ { "name": "lamp", "bone": "lamp", "attachment": "lamp" }
147
+ ],
148
+ "skins": {
149
+ "default": {
150
+ "mast": { "mast": { "image": "mast.png", "y": 26 } },
151
+ "hull": { "hull": { "image": "hull.png" } },
152
+ "lamp": { "lamp": { "image": "lamp.png" } }
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ Three things in there are worth naming, because each is a rule rather than a
159
+ style: the **slots array is the setup draw order** (R4) β€” index 0 is furthest
160
+ back, so the mast is behind the hull; the attachment carries an **`image`
161
+ instead of a `width`/`height`** (R5), which is what makes the size in the
162
+ skeleton and the size in the atlas incapable of drifting apart; and the mast's
163
+ `"y": 26` offsets the plate *within* its slot so the bone sits at the mast's foot
164
+ rather than its middle.
165
+
166
+ **4. The motion spec β€” `buoy.motion.json`.** Time only, aimed at the rig by name:
167
+
168
+ ```json
169
+ {
170
+ "spec": "rigc-motion/1",
171
+ "archetype": "buoy",
172
+ "cut": "buoy",
173
+ "easings": { "swing": [0.42, 0, 0.58, 1] },
174
+ "animations": {
175
+ "bob": {
176
+ "duration": 2,
177
+ "loop": true,
178
+ "tracks": [
179
+ {
180
+ "bone": "hull",
181
+ "property": "translatey",
182
+ "keys": [
183
+ { "t": 0, "v": [0], "ease": "swing" },
184
+ { "t": 0.5, "v": [5], "ease": "swing" },
185
+ { "t": 1.5, "v": [-5], "ease": "swing" },
186
+ { "t": 2, "v": [0] }
187
+ ]
188
+ },
189
+ {
190
+ "bone": "mast",
191
+ "property": "rotate",
192
+ "keys": [
193
+ { "t": 0, "v": [-6], "ease": "swing" },
194
+ { "t": 1, "v": [6], "ease": "swing" },
195
+ { "t": 2, "v": [-6] }
196
+ ]
197
+ }
198
+ ]
199
+ }
200
+ }
201
+ }
202
+ ```
203
+
204
+ `archetype` must equal the rig's `name`. `duration` is declared and then checked
205
+ against what actually compiled (R7). The **last key of each track carries no
206
+ easing** β€” there is nothing after it to ease towards, and saying otherwise is a
207
+ compile error.
208
+
209
+ **5. Build, then re-gate what it wrote.**
210
+
211
+ ```bash
212
+ rigc build --rig buoy.rig.json --motion buoy.motion.json --images images --out spine
213
+ rigc validate spine
214
+ ```
215
+
216
+ `build` prints every assertion by name, then the shape of what it emitted, then
217
+ the two files:
218
+
219
+ ```
220
+ .. pages=3 regions=3 bones=4 slots=3 animations=1 version=4.3.13 regionAttachments=3 meshAttachments=0 physicsConstraints=0 rig=buoy profile=spine
221
+ rigc: wrote …/buoy/spine/skeleton.json
222
+ rigc: wrote …/buoy/spine/skeleton.atlas
223
+ ```
224
+
225
+ `profile=spine` is the rulebook that judged it: *is this valid Spine 4.3 that any
226
+ runtime plays correctly?* That is the default, and the [Profiles](#profiles--wrong-versus-not-how-we-do-it-here)
227
+ section below is where the other one lives. `validate` then re-reads those
228
+ artifacts from disk and ends `rigc: green`. That is a rig. `spine/skeleton.json`
229
+ is Spine 4.3 skeleton data β€” it loads in a Spine runtime and it imports into the
230
+ Spine editor.
231
+
232
+ **Try breaking it**, because the validator's messages are the interface here and
233
+ they are worth meeting once on purpose. With `spine/` built, rename
234
+ `images/hull.png` to `images/raft.png` and re-run `rigc validate spine`:
235
+
236
+ ```
237
+ FAIL A17_ATLAS_PAGE_FILES_EXIST: page "../images/hull.png" is not on disk at …/images/hull.png
238
+ rigc: 1 assertion(s) failed
239
+ ```
240
+
241
+ Put the name back and it is green again. The same gate runs inside `build`, and
242
+ a FAIL there stops it **before it writes** β€” a red build leaves no half-built
243
+ artifact on disk to mistake for a result, and there is no flag that changes that.
244
+
245
+ **6. See what you built.**
246
+
247
+ 🚨 **Green is a claim about validity and about nothing else.** A rig whose head
248
+ sits visibly off its torso passes every assertion, loads in `spine-core` and steps
249
+ numerically clean β€” the offsets are the ones your spec asked for, and no
250
+ assertion can know you did not mean them. The only remedy is looking, and both
251
+ commands below need nothing you do not already have: no reference frames, no
252
+ second package, no server.
253
+
254
+ ```bash
255
+ rigc render --candidate spine # PNG frames + a contact sheet, in render/
256
+ rigc preview --candidate spine # one .html file that plays it: preview.html
257
+ ```
258
+
259
+ **`render`** samples every animation at 12 fps and writes `render/<animation>/f0000.png…`
260
+ with a `contact.png` beside them β€” every frame of the shot as one labelled grid,
261
+ which is the picture to open first, because spacing is a comparison *across*
262
+ frames. It draws with rigc's own rasteriser (the one `check` measures with), so
263
+ it needs no browser and no network, and the `frames.json` it leaves beside the
264
+ directories makes the result a frame set like any other β€” the world box every
265
+ frame is a picture of. `--animation <name>` narrows it to one, `--fps` and
266
+ `--max` change the rate and the frame size.
267
+
268
+ **`preview`** writes a single self-contained `.html`: your skeleton, your atlas
269
+ and every page's PNG bytes are embedded in it as data URIs, and it plays them in
270
+ the **official [Spine Web Player](https://esotericsoftware.com/spine-player)**.
271
+ Double-click it, or attach it to a message β€” the file carries the whole artifact.
272
+ It is also the strongest interop statement in this repository: a rig that plays
273
+ there has been played by Esoteric Software's own runtime rather than by ours.
274
+
275
+ > βš–οΈ The player itself is **referenced, not embedded** β€” the page loads it from
276
+ > unpkg, so the first open needs a network, and rigc redistributes nothing
277
+ > Esoteric Software owns (see [NOTICE.md](NOTICE.md)). Everything the player
278
+ > draws is inside your file.
279
+
280
+ **Where to go next.**
281
+
282
+ - πŸ“˜ **[docs/AUTHORING.md](docs/AUTHORING.md)** is the real guide β€” both files
283
+ field by field, the emission rules, every named failure mapped to the file that
284
+ has to change, and Β§8–§9 for reproducing a shot you were given as pictures. It
285
+ ships inside the npm package too, at
286
+ `node_modules/spine-rigc/docs/AUTHORING.md`.
287
+ - `rigc explain --rig buoy.rig.json --motion buoy.motion.json --out spine` prints
288
+ the compiled rig as a table β€” every bone with its resolved parent, the slots in
289
+ draw order, every timeline key by key β€” and writes nothing. It is what to reach
290
+ for when a rig compiles and still looks wrong.
291
+ - 🚨 **A green gate does not mean the animation is right**, and no assertion
292
+ could. If you have reference pictures of the shot,
293
+ `rigc check --candidate spine --frames <dir>` is the half of the loop that can
294
+ see a wrong animation β€” AUTHORING.md Β§9.
295
+ - [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) is the benchmark: the same job, from a brief
296
+ and rendered frames, scored. [docs/PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) is how to run an
297
+ agent through it and score what comes back.
298
+ - πŸ€– **Handing the authoring to an AI agent?**
299
+ [docs/PROMPTING.md](docs/PROMPTING.md) is the operator's page β€” the six prompt
300
+ clauses a measured pilot run paid for, and what you can leave unsaid.
301
+
56
302
  ## The yardstick
57
303
 
58
304
  The measure of whether this works is **Spine's own official example projects** β€”
@@ -193,16 +439,31 @@ the scale for a run.
193
439
 
194
440
  There is no pass mark **in the tool**, for the same reason `diff` has none. The
195
441
  ladder's pass definition and its thresholds are a document read by a person over
196
- the whole table β€” [docs/LADDER.md](docs/LADDER.md)'s *Operating rules* β€” and not an
442
+ the whole table β€” [docs/GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) states the clauses and
443
+ [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md)'s *Operating rules* derives them β€” and not an
197
444
  exit code either command could produce.
198
445
 
199
446
  ### Benchmark ladder β€” the rungs, and where they stand
200
447
 
201
- **[docs/LADDER.md](docs/LADDER.md) is the live ledger**: the rung order
448
+ πŸŽ“ **The ladder is complete, 2026-08-28.** All eight numbered rungs and the
449
+ spineboy graduation exam are cleared under gate v2.1 and hold under **v2.2**, every clause PASS or SKIP:
450
+ worst attributable slot drift **5.55 px** against a 6.0 px bar, and **0 of 124**
451
+ frame-change disagreements. Recompiling the same spec in a different session
452
+ reproduced every field of the measurement record **to the digit**. The rungs stay
453
+ in place as regression gates.
454
+
455
+ ⚠️ **What that certifies, stated exactly.** That **the tool, the guide and the
456
+ protocol reach the bar across a bounded series of honest attempts, each residual
457
+ diagnosed and fixed** β€” spineboy took five, and the last inherited its
458
+ predecessor's specs under the run protocol's inheritance clause. It is **not**
459
+ that an agent authors a spineboy-scale rig from the brief alone in one run: the
460
+ ladder has not demonstrated that, and each row records which of the two it is.
461
+
462
+ **[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) is the live ledger**: the rung order
202
463
  (blockers β†’ rung 3 first β†’ 1 Β· 2 Β· 4 Β· 5 β†’ 6 β†’ 8 β†’ 7 β†’ spineboy), what each
203
464
  rung gates on, how a rung is scored, the honesty rule that keeps the reference
204
465
  export away from the authoring agent, the operating rules β€” what a pass is, and
205
- the numbered thresholds of the current gate (**gate v2**) that decide one β€” and a status table. Run
466
+ the numbered thresholds of the current gate (**gate v2.2**, stated in [docs/GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md)) that decide one β€” and a status table. Run
206
467
  one with:
207
468
 
208
469
  ```bash
@@ -225,9 +486,40 @@ that every example declares; and **B3**, every example ships a **packed** atlas
225
486
  page) against rigc's one-part-per-page model, which `A06` enforced unconditionally. **B1 and B2 are
226
487
  closed**; B3's validator half is (the packed-atlas clauses live behind `--profile`, above) and its
227
488
  emitter half β€” no packer, no atlas importer β€” is not. Ordered gap list in Part 4 of that document;
228
- live status, and B1's proof, in [docs/LADDER.md](docs/LADDER.md).
489
+ live status, and B1's proof, in [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
490
+
491
+ ## Looking at a rig β€” `rigc render` and `rigc preview`
492
+
493
+ The validator cannot see a wrong pose and says so honestly; `check` can, and needs
494
+ reference frames a first user does not have. That left looking as the one thing
495
+ the package could not do, and these two commands are it. Both take a compiled
496
+ artifact β€” the directory `build --out` wrote β€” and neither needs a reference, a
497
+ clone or a server:
229
498
 
230
- ## Run viewer β€” watching a run instead of reading it
499
+ ```bash
500
+ rigc render --candidate spine [--animation <name>] [--fps 12] [--max 256] [--out render/]
501
+ rigc preview --candidate spine [--animation <name>] [--out preview.html]
502
+ ```
503
+
504
+ `render` writes `render/<animation>/f0000.png…` plus a `contact.png` grid of every
505
+ frame and a `frames.json` sidecar describing the world box they are pictures of β€”
506
+ the same frame-set shape `bench/render_reference.ts` writes and `rigc check`
507
+ reads, drawn by the same rasteriser, so the output is a frame set rather than a
508
+ pile of images. `preview` writes one self-contained `.html` that plays the
509
+ artifact in the official Spine Web Player, with the skeleton, the atlas and every
510
+ page embedded as data URIs; the player is loaded from unpkg rather than copied, so
511
+ the first open needs a network and rigc redistributes nothing Esoteric Software
512
+ owns ([NOTICE.md](NOTICE.md)).
513
+
514
+ They complement each other rather than overlap. `render` is offline, deterministic
515
+ and measurable β€” its pixels are the ones `check` reports on. `preview` is the
516
+ interop proof: what plays there was played by Esoteric's own runtime, not by ours.
517
+
518
+ ## Run viewer β€” watching a *run* instead of reading it
519
+
520
+ πŸ”Ž **This is the ladder's instrument, not the way to look at your own rig** β€” that
521
+ is the section above. The viewer is reference-bound and repository-bound, and it
522
+ deliberately never ships.
231
523
 
232
524
  `check.txt` says a candidate's worst frame is f0012 at 56 MAE. The viewer shows
233
525
  you f0012.
@@ -342,8 +634,15 @@ So `validate` and `build` take a `--profile`:
342
634
 
343
635
  | Profile | Runs | For |
344
636
  | --- | --- | --- |
345
- | `spine-html` | all 34 | **the default.** Is this a rig this project can ship? |
346
- | `spine` | the 20 validity rules | Is this valid Spine 4.3 that any runtime plays correctly? |
637
+ | `spine` | the 20 validity rules | **the default.** Is this valid Spine 4.3 that any runtime plays correctly? |
638
+ | `spine-html` | all 34 | Opt-in. Is this a rig *this* project can ship? |
639
+
640
+ `spine` is the default because it is the question this package's output answers:
641
+ the artifact imports into the Spine editor and plays in any 4.3 runtime, and
642
+ that is what the 20 validity rules are about. The other 14 are somebody's policy
643
+ β€” one renderer's, one canvas budget's, one compiler's own formations' β€” and a
644
+ rig arriving from anywhere else has no stake in them. Ask for them with
645
+ `--profile spine-html` when you want them.
347
646
 
348
647
  The **Profile** column below says which is which β€” `both` = validity, `renderer` and
349
648
  `archetype` = `spine-html` only, and **`both β—‘`** = a mixed assertion whose validity
@@ -374,7 +673,7 @@ the renderer policy*.
374
673
  | `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` version label is on the 4.3 line (the parser never checks it) |
375
674
  | `A17_ATLAS_PAGE_FILES_EXIST` | both | every page the atlas declares is a file on disk |
376
675
  | `A18_DETERMINISTIC_EMIT` | both | a second, independent compile of the same inputs is byte-identical. SKIPs when re-gating artifacts already on disk |
377
- | `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | every overlay page carries an alpha channel; only the base plate β€” identified structurally as the region covering the stage β€” may be opaque |
676
+ | `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | every overlay part image can be transparent somewhere β€” an alpha channel (colour type 4 or 6) **or** a `tRNS` chunk, which is where indexed and greyscale PNGs keep theirs. Only the base plate β€” identified structurally as the region covering the stage β€” may be opaque |
378
677
  | `A20_MESH_WEIGHTS_COHERENT` | both β—‘ | every weighted vertex has at least one bone, no negative weight, bone indices in range, and each vertex's weights sum to 1. `spine-html` also requires that a mesh be weighted at all and that no binding sit at weight 0 |
379
678
  | `A21_MESH_RIM_PINNED` | archetype | a ring mesh's rim vertices are pinned to the anchor bone and its hull is a real ring; a ribbon's entry row stays put. **SKIPs on authored geometry** β€” rigc did not place its rim |
380
679
  | `A22_MESH_UVS_IN_UNIT_RANGE` | both | every UV lies inside its region |
@@ -390,208 +689,6 @@ the renderer policy*.
390
689
  | `A32_EVENT_KEYS_RESOLVE` | both | every event key fires an event the skeleton declares, no key sits earlier in time than the one before it, and `volume`/`balance` appear only on an event with an `audio` path. Only the first of those is loud in the parser; the other two load clean and drop the firing or the value in silence. SKIPs when no animation carries an event timeline |
391
690
  | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | every bounding box and clipping polygon states a `vertexCount` that agrees with its vertex array, its weighted run decodes to that many vertices with bone indices in range, and a clipping `end` names a slot that exists. All three load clean: a missing count reads as zero and empties the polygon, and a missing end slot makes the clip run to the bottom of the draw order. SKIPs when the skeleton carries neither type |
392
691
 
393
- ## Install
394
-
395
- rigc runs on [Bun](https://bun.sh). The package ships its TypeScript sources and
396
- Bun runs them, so there is no build step and no `dist/` that can drift from the
397
- repository it was cut from.
398
-
399
- **The npm package is `spine-rigc`; the command it installs is `rigc`.** npm
400
- refuses the name `rigc` as too similar to packages that already exist, so the
401
- project, this repository and the executable keep their name and only the
402
- registry entry is spelled out.
403
-
404
- ```bash
405
- bunx spine-rigc --help # run it without installing
406
- bun add -g spine-rigc # or install the command
407
- bun add -d spine-rigc # or pin it in a project
408
- ```
409
-
410
- `npx spine-rigc` works too, as long as Bun is on `PATH` β€” the executable is a
411
- Bun script, and npm only writes the shim that calls it.
412
-
413
- Installed, the command is `rigc`. The examples below spell it `bun cli.ts`
414
- because they are written from a clone of this repository (`bun install`, then run
415
- the CLI in place); the two are interchangeable β€” `rigc build …` is
416
- `bun cli.ts build …`.
417
-
418
- Two commands are repository workflows rather than package ones: `bench` and
419
- `check` measure against Spine's official example projects and the reference
420
- frames rendered from them, which are fetched rather than redistributed (see
421
- [NOTICE.md](NOTICE.md)). They need a clone and `bun run fetch-examples`, and say
422
- so by name when the corpus is absent.
423
-
424
- ## First rig in ten minutes
425
-
426
- A whole rig, end to end, in a scratch directory: three tiny plates, two JSON
427
- files, one `build`, one `validate`. No clone, no art pipeline, nothing fetched.
428
-
429
- 🚫 **Every value below is invented for this section** β€” a doll that exists
430
- nowhere else in this repository. That is [AUTHORING.md](docs/AUTHORING.md) Β§3's
431
- rule applied here: no example value in these documents is copied out of a
432
- reference export, so nothing you read in a quickstart is an answer to anything
433
- [the ladder](docs/LADDER.md) measures.
434
-
435
- **1. Install the command.**
436
-
437
- ```bash
438
- bun add -g spine-rigc # installs `rigc`
439
- ```
440
-
441
- Or skip the install and prefix every command below with `bunx `, e.g.
442
- `bunx spine-rigc build …`.
443
-
444
- **2. Make a directory and three plates.** rigc measures PNGs rather than trusting
445
- a number you typed (R5), so the art has to exist. These three are solid colours a
446
- few dozen pixels across β€” a hull, a mast and a lamp:
447
-
448
- ```bash
449
- mkdir -p buoy/images && cd buoy
450
- bun -e '
451
- const parts = {
452
- "images/hull.png": "iVBORw0KGgoAAAANSUhEUgAAADgAAAAMCAYAAAA3bX6lAAAAKElEQVR42mOI8bL6P5wxw6gHRz046sFRD456cNSDox4c9eCoBwcrBgDSZ+mdl2OiDgAAAABJRU5ErkJggg==",
453
- "images/mast.png": "iVBORw0KGgoAAAANSUhEUgAAAAgAAAA0CAYAAAC3t3ldAAAAH0lEQVR42mO4dunIf3yYYVTBqIJRBaMKRhWMKhgcCgBGJo4s9YnopgAAAABJRU5ErkJggg==",
454
- "images/lamp.png": "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAHElEQVR42mP4v8HhPzUww6hBowaNGjRq0HAzCADvdrVmFPbc+QAAAABJRU5ErkJggg=="
455
- };
456
- for (const [p, b] of Object.entries(parts)) await Bun.write(p, Buffer.from(b, "base64"));
457
- '
458
- ```
459
-
460
- **3. The rig spec β€” `buoy.rig.json`.** Structure only: bones, the slots array in
461
- draw order, and one skin mapping each slot to a plate.
462
-
463
- ```json
464
- {
465
- "spec": "rigc-rig/1",
466
- "name": "buoy",
467
- "images": "images",
468
- "skeleton": { "width": 200, "height": 200 },
469
- "bones": [
470
- { "name": "root" },
471
- { "name": "hull", "parent": "root", "x": 0, "y": 0 },
472
- { "name": "mast", "parent": "hull", "x": 0, "y": 4 },
473
- { "name": "lamp", "parent": "mast", "x": 0, "y": 52 }
474
- ],
475
- "slots": [
476
- { "name": "mast", "bone": "mast", "attachment": "mast" },
477
- { "name": "hull", "bone": "hull", "attachment": "hull" },
478
- { "name": "lamp", "bone": "lamp", "attachment": "lamp" }
479
- ],
480
- "skins": {
481
- "default": {
482
- "mast": { "mast": { "image": "mast.png", "y": 26 } },
483
- "hull": { "hull": { "image": "hull.png" } },
484
- "lamp": { "lamp": { "image": "lamp.png" } }
485
- }
486
- }
487
- }
488
- ```
489
-
490
- Three things in there are worth naming, because each is a rule rather than a
491
- style: the **slots array is the setup draw order** (R4) β€” index 0 is furthest
492
- back, so the mast is behind the hull; the attachment carries an **`image`
493
- instead of a `width`/`height`** (R5), which is what makes the size in the
494
- skeleton and the size in the atlas incapable of drifting apart; and the mast's
495
- `"y": 26` offsets the plate *within* its slot so the bone sits at the mast's foot
496
- rather than its middle.
497
-
498
- **4. The motion spec β€” `buoy.motion.json`.** Time only, aimed at the rig by name:
499
-
500
- ```json
501
- {
502
- "spec": "rigc-motion/1",
503
- "archetype": "buoy",
504
- "cut": "buoy",
505
- "easings": { "swing": [0.42, 0, 0.58, 1] },
506
- "animations": {
507
- "bob": {
508
- "duration": 2,
509
- "loop": true,
510
- "tracks": [
511
- {
512
- "bone": "hull",
513
- "property": "translatey",
514
- "keys": [
515
- { "t": 0, "v": [0], "ease": "swing" },
516
- { "t": 0.5, "v": [5], "ease": "swing" },
517
- { "t": 1.5, "v": [-5], "ease": "swing" },
518
- { "t": 2, "v": [0] }
519
- ]
520
- },
521
- {
522
- "bone": "mast",
523
- "property": "rotate",
524
- "keys": [
525
- { "t": 0, "v": [-6], "ease": "swing" },
526
- { "t": 1, "v": [6], "ease": "swing" },
527
- { "t": 2, "v": [-6] }
528
- ]
529
- }
530
- ]
531
- }
532
- }
533
- }
534
- ```
535
-
536
- `archetype` must equal the rig's `name`. `duration` is declared and then checked
537
- against what actually compiled (R7). The **last key of each track carries no
538
- easing** β€” there is nothing after it to ease towards, and saying otherwise is a
539
- compile error.
540
-
541
- **5. Build, then re-gate what it wrote.**
542
-
543
- ```bash
544
- rigc build --rig buoy.rig.json --motion buoy.motion.json --images images --out spine
545
- rigc validate spine
546
- ```
547
-
548
- `build` prints every assertion by name, then the shape of what it emitted, then
549
- the two files:
550
-
551
- ```
552
- .. pages=3 regions=3 bones=4 slots=3 animations=1 version=4.3.13 regionAttachments=3 meshAttachments=0 physicsConstraints=0 rig=buoy profile=spine-html
553
- rigc: wrote …/buoy/spine/skeleton.json
554
- rigc: wrote …/buoy/spine/skeleton.atlas
555
- ```
556
-
557
- and `validate` re-reads those artifacts from disk and ends `rigc: green`. That is
558
- a rig. `spine/skeleton.json` is Spine 4.3 skeleton data β€” it loads in a Spine
559
- runtime and it imports into the Spine editor.
560
-
561
- **Try breaking it**, because the validator's messages are the interface here and
562
- they are worth meeting once on purpose. Rename `images/hull.png` to
563
- `images/raft.png`, point the spec's `image` at the new name, and build again:
564
-
565
- ```
566
- FAIL A08_REGION_NAMES_MATCH_ATTACHMENTS: attachment "hull" resolves to region "raft"; v0 requires them identical
567
- rigc: 1 assertion(s) failed β€” nothing written
568
- ```
569
-
570
- Nothing was written. A red run leaves no half-built artifact on disk to mistake
571
- for a result, and there is no flag that changes that.
572
-
573
- **Where to go next.**
574
-
575
- - πŸ“˜ **[docs/AUTHORING.md](docs/AUTHORING.md)** is the real guide β€” both files
576
- field by field, the emission rules, every named failure mapped to the file that
577
- has to change, and Β§8–§9 for reproducing a shot you were given as pictures. It
578
- ships inside the npm package too, at
579
- `node_modules/spine-rigc/docs/AUTHORING.md`.
580
- - `rigc explain --rig buoy.rig.json --motion buoy.motion.json --out spine` prints
581
- the compiled rig as a table β€” every bone with its resolved parent, the slots in
582
- draw order, every timeline key by key β€” and writes nothing. It is what to reach
583
- for when a rig compiles and still looks wrong.
584
- - 🚨 **A green gate does not mean the animation is right**, and no assertion
585
- could. If you have reference pictures of the shot,
586
- `rigc check --candidate spine --frames <dir>` is the half of the loop that can
587
- see a wrong animation β€” AUTHORING.md Β§9.
588
- - [docs/LADDER.md](docs/LADDER.md) is the benchmark: the same job, from a brief
589
- and rendered frames, scored. [docs/PILOT.md](docs/PILOT.md) is how to run an
590
- agent through it and score what comes back.
591
- - πŸ€– **Handing the authoring to an AI agent?**
592
- [docs/PROMPTING.md](docs/PROMPTING.md) is the operator's page β€” the six prompt
593
- clauses a measured pilot run paid for, and what you can leave unsaid.
594
-
595
692
  ## Usage
596
693
 
597
694
  πŸ“˜ **Writing a spec? Read [docs/AUTHORING.md](docs/AUTHORING.md) first.** It is the
@@ -615,6 +712,11 @@ bun cli.ts build \
615
712
  [--manifest path/to/manifest.json] [--images path/to/images]
616
713
  ```
617
714
 
715
+ By default, atlas page paths point back at the source art wherever it lives β€”
716
+ often outside `--out` β€” so add `--copy-images` when `spine/` itself needs to be
717
+ self-contained (zipped, committed, or handed off on its own): it copies every
718
+ referenced page PNG into `--out` and rewrites the atlas to match.
719
+
618
720
  …or register cuts in a `cuts.json` and build them by name. Every path in the table
619
721
  resolves **relative to the `cuts.json` file itself**, so the table lives with the
620
722
  project that owns the art:
@@ -640,17 +742,24 @@ commands:
640
742
  ```bash
641
743
  bun cli.ts explain --cut my_cut --cuts path/to/cuts.json # the compiled rig as a table
642
744
  bun cli.ts validate path/to/spine # re-gate artifacts already on disk
643
- bun cli.ts validate --profile spine path/to/any/skeleton # spec rules only (see Profiles)
745
+ bun cli.ts validate --profile spine-html path/to/spine # …and this project's policy too (see Profiles)
644
746
  bun cli.ts diff candidate.json reference.json # structural comparison
645
747
  bun cli.ts check --candidate path/to/spine \
646
748
  --frames bench/reference/3-timing-and-spacing # against pictures
647
749
  bun cli.ts bench 3 --candidate path/to/spine # one rung of the ladder
750
+ bun cli.ts render --candidate path/to/spine # PNG frames + a contact sheet
751
+ bun cli.ts preview --candidate path/to/spine # one .html that plays it
648
752
  ```
649
753
 
650
754
  `validate` on a bare directory checks what it can see. Adding `--cut`/`--cuts` lets
651
755
  it re-derive the declared durations and the structural expectations too, and the
652
- report says which it had. `build` and `validate` both take `--profile spine` to drop
653
- the renderer and archetype policy; the default stays `spine-html`.
756
+ report says which it had. `build` and `validate` both default to `--profile spine`,
757
+ the 20 validity rules; `--profile spine-html` adds this project's renderer and
758
+ archetype policy on top.
759
+
760
+ `render` and `preview` are the two that need no reference at all β€” see
761
+ [Looking at a rig](#looking-at-a-rig--rigc-render-and-rigc-preview). Run either
762
+ straight after a green `build`, on the same directory `--out` wrote.
654
763
 
655
764
  ## Checks
656
765
 
@@ -661,7 +770,7 @@ bun run selftest # the validator's own negative controls (next section)
661
770
  ```
662
771
 
663
772
  All three run on every push and pull request β€”
664
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Bun runs the sources
773
+ [`.github/workflows/ci.yml`](https://github.com/firejune/rigc/blob/main/.github/workflows/ci.yml). Bun runs the sources
665
774
  directly, so the first two are not on the path of anything; they exist because a
666
775
  convention nothing checks is a convention. `tsconfig.json` is
667
776
  `strict: false` with `strictNullChecks: true` and says in place why the rest is
@@ -681,7 +790,7 @@ for each. Two further edits are *tolerance* controls the gate must let through,
681
790
  because a widened assertion can fail by firing too often as easily as by firing too
682
791
  rarely.
683
792
 
684
- **The rigs it breaks are generated.** [`fixtures/public.ts`](fixtures/public.ts)
793
+ **The rigs it breaks are generated.** [`fixtures/public.ts`](https://github.com/firejune/rigc/blob/main/fixtures/public.ts)
685
794
  writes three synthetic cuts into a temp directory on every run, and between them
686
795
  they carry every structure the assertions have an opinion about β€” region
687
796
  attachments, attachment swaps, rgba fades, a ring mesh on a control bone, a ribbon
@@ -747,7 +856,7 @@ nothing substantive executed exits 2 rather than printing green.
747
856
 
748
857
  ```
749
858
  tsconfig.json type-check config (noEmit); eslint.config.js β€” the no-any gate
750
- cli.ts build / validate / explain / diff / check / bench
859
+ cli.ts build / validate / explain / diff / check / bench / render / preview
751
860
  selftest.ts the validator's own negative controls, and diff's and check's
752
861
  fixtures/ public.ts β€” the three synthetic cuts the selftest breaks
753
862
  src/
@@ -755,14 +864,17 @@ src/
755
864
  rig.ts the rig spec β€” `spec: "rigc-rig/1"`, the skeleton as data
756
865
  validate.ts spine-core round trip + the 34 assertions
757
866
  diff.ts structural comparison of two skeletons, one ratio per measure
758
- render.ts the rasteriser (regions + meshes), shared by the reference renderer and check
867
+ render.ts the rasteriser (regions + meshes), shared by the reference renderer,
868
+ `rigc render` and check
869
+ preview.ts the single-file HTML player page β€” the artifact embedded as data
870
+ URIs, played by the official Spine Web Player (referenced, not vendored)
759
871
  check.ts a candidate against rendered frames β€” pixels and per-slot drift,
760
872
  and it never opens the reference skeleton
761
873
  ladder.ts which example is which rung, and which file in it is the reference
762
874
  timelines.ts the 4.3 timeline catalogue and its walker (shared, pure JSON)
763
875
  mesh.ts ring and ribbon mesh builders, weighted-vertex encoding
764
876
  transform.ts crop pixels (y down) <-> Spine world (y up), world transforms
765
- png.ts PNG header reader (size and colour type, no decode)
877
+ png.ts PNG header reader (size, colour type, tRNS; no pixel decode)
766
878
  errors.ts CompileError, and NotImplementedError for what the format holds
767
879
  and the emitter does not write
768
880
  types.ts manifest, motion spec, and emitted-JSON shapes
@@ -779,7 +891,8 @@ viewer/ the run viewer β€” dev server only, no build (see above)
779
891
  vite.config.ts /api/inventory and /repo/<path>, and the build refusal
780
892
  inventory.ts what is under bench/runs, resolved to URLs
781
893
  main.ts the two panes, the transport, the report
782
- docs/ AUTHORING.md (how to author a rig), LADDER.md (live rung status),
894
+ docs/ AUTHORING.md (how to author a rig), GATE.md (the clause statements
895
+ a candidate is graded against), LADDER.md (live rung status),
783
896
  SPEC_COVERAGE.md (format survey),
784
897
  feature_matrix.{csv,json}
785
898
  .github/ workflows/ β€” ci.yml (the gates) and release.yml (release-please)
@@ -792,14 +905,14 @@ CONTRIBUTING.md how to propose a change; RELEASING.md β€” how a version is cut
792
905
  | --- | --- |
793
906
  | `measure_contact_depth.ts` | measures a cut's contact depth from its plates, with the two-sided proof it has to satisfy. Both slot names are required: which plate is the mass and which is the occluder is a fact about one cut, and a default would measure the wrong pair and still print a number |
794
907
  | `contact.ts` | plate-vs-plate overlap measurement β€” the largest advance that keeps two footprints disjoint |
795
- | `plate.ts` / `png_probe.mjs` | minimal PNG read/write and decode |
908
+ | `plate.ts` / `png_probe.mjs` | minimal PNG read/write and decode. The writer emits colour type 6 only; the reader takes every colour type and bit depth PNG allows except interlaced, expanding indexed palettes (`PLTE` + `tRNS`) and greyscale to RGBA β€” because the gate accepts that art, so the renderer has to as well (issue #226) |
796
909
  | `font5x7.ts` | bitmap labels for diagnostic images and generated plates |
797
910
 
798
911
  ## Contributing
799
912
 
800
- Issues are the ledger; see [CONTRIBUTING.md](CONTRIBUTING.md) for what a change
913
+ Issues are the ledger; see [CONTRIBUTING.md](https://github.com/firejune/rigc/blob/main/CONTRIBUTING.md) for what a change
801
914
  has to clear before it lands. Releases are cut by release-please β€”
802
- [RELEASING.md](RELEASING.md).
915
+ [RELEASING.md](https://github.com/firejune/rigc/blob/main/RELEASING.md).
803
916
 
804
917
  ## Licence
805
918