spine-rigc 0.5.0 β†’ 0.7.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,268 @@ 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
+ **7. Let someone choose.** Sooner or later you will have two builds that both pass
281
+ the gate and no instrument that can separate them. `vote` puts them in one page
282
+ side by side, labelled `A` and `B` with no paths on screen, and takes an answer
283
+ back:
284
+
285
+ ```bash
286
+ rigc vote --candidate spine-a --candidate spine-b # -> ballot.html, open it and pick one
287
+ rigc vote --record vote-<id>.json # -> checks the answer into votes.jsonl
288
+ ```
289
+
290
+ The voter picks a winner or says "tie / no preference"; the page hands them a
291
+ small JSON file to save; `--record` checks that file against the ballot's own
292
+ hashes and appends one line to an append-only ledger, refusing by name anything
293
+ that does not belong to it. See
294
+ [Letting someone choose](#letting-someone-choose--rigc-vote).
295
+
296
+ **Where to go next.**
297
+
298
+ - πŸ“˜ **[docs/AUTHORING.md](docs/AUTHORING.md)** is the real guide β€” both files
299
+ field by field, the emission rules, every named failure mapped to the file that
300
+ has to change, and Β§8–§9 for reproducing a shot you were given as pictures. It
301
+ ships inside the npm package too, at
302
+ `node_modules/spine-rigc/docs/AUTHORING.md`.
303
+ - `rigc explain --rig buoy.rig.json --motion buoy.motion.json --out spine` prints
304
+ the compiled rig as a table β€” every bone with its resolved parent, the slots in
305
+ draw order, every timeline key by key β€” and writes nothing. It is what to reach
306
+ for when a rig compiles and still looks wrong.
307
+ - 🚨 **A green gate does not mean the animation is right**, and no assertion
308
+ could. If you have reference pictures of the shot,
309
+ `rigc check --candidate spine --frames <dir>` is the half of the loop that can
310
+ see a wrong animation β€” AUTHORING.md Β§9.
311
+ - [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) is the benchmark: the same job, from a brief
312
+ and rendered frames, scored. [docs/PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) is how to run an
313
+ agent through it and score what comes back.
314
+ - πŸ€– **Handing the authoring to an AI agent?**
315
+ [docs/PROMPTING.md](docs/PROMPTING.md) is the operator's page β€” the six prompt
316
+ clauses a measured pilot run paid for, and what you can leave unsaid.
317
+
56
318
  ## The yardstick
57
319
 
58
320
  The measure of whether this works is **Spine's own official example projects** β€”
@@ -193,13 +455,14 @@ the scale for a run.
193
455
 
194
456
  There is no pass mark **in the tool**, for the same reason `diff` has none. The
195
457
  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
458
+ the whole table β€” [docs/GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) states the clauses and
459
+ [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md)'s *Operating rules* derives them β€” and not an
197
460
  exit code either command could produce.
198
461
 
199
462
  ### Benchmark ladder β€” the rungs, and where they stand
200
463
 
201
464
  πŸŽ“ **The ladder is complete, 2026-08-28.** All eight numbered rungs and the
202
- spineboy graduation exam are cleared under gate v2.1, every clause PASS or SKIP:
465
+ spineboy graduation exam are cleared under gate v2.1 and hold under **v2.2**, every clause PASS or SKIP:
203
466
  worst attributable slot drift **5.55 px** against a 6.0 px bar, and **0 of 124**
204
467
  frame-change disagreements. Recompiling the same spec in a different session
205
468
  reproduced every field of the measurement record **to the digit**. The rungs stay
@@ -212,11 +475,11 @@ predecessor's specs under the run protocol's inheritance clause. It is **not**
212
475
  that an agent authors a spineboy-scale rig from the brief alone in one run: the
213
476
  ladder has not demonstrated that, and each row records which of the two it is.
214
477
 
215
- **[docs/LADDER.md](docs/LADDER.md) is the live ledger**: the rung order
478
+ **[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) is the live ledger**: the rung order
216
479
  (blockers β†’ rung 3 first β†’ 1 Β· 2 Β· 4 Β· 5 β†’ 6 β†’ 8 β†’ 7 β†’ spineboy), what each
217
480
  rung gates on, how a rung is scored, the honesty rule that keeps the reference
218
481
  export away from the authoring agent, the operating rules β€” what a pass is, and
219
- the numbered thresholds of the current gate (**gate v2.1**) that decide one β€” and a status table. Run
482
+ 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
220
483
  one with:
221
484
 
222
485
  ```bash
@@ -239,9 +502,93 @@ that every example declares; and **B3**, every example ships a **packed** atlas
239
502
  page) against rigc's one-part-per-page model, which `A06` enforced unconditionally. **B1 and B2 are
240
503
  closed**; B3's validator half is (the packed-atlas clauses live behind `--profile`, above) and its
241
504
  emitter half β€” no packer, no atlas importer β€” is not. Ordered gap list in Part 4 of that document;
242
- live status, and B1's proof, in [docs/LADDER.md](docs/LADDER.md).
505
+ live status, and B1's proof, in [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
506
+
507
+ ## Looking at a rig β€” `rigc render` and `rigc preview`
508
+
509
+ The validator cannot see a wrong pose and says so honestly; `check` can, and needs
510
+ reference frames a first user does not have. That left looking as the one thing
511
+ the package could not do, and these two commands are it. Both take a compiled
512
+ artifact β€” the directory `build --out` wrote β€” and neither needs a reference, a
513
+ clone or a server:
243
514
 
244
- ## Run viewer β€” watching a run instead of reading it
515
+ ```bash
516
+ rigc render --candidate spine [--animation <name>] [--fps 12] [--max 256] [--out render/]
517
+ rigc preview --candidate spine [--animation <name>] [--out preview.html]
518
+ ```
519
+
520
+ `render` writes `render/<animation>/f0000.png…` plus a `contact.png` grid of every
521
+ frame and a `frames.json` sidecar describing the world box they are pictures of β€”
522
+ the same frame-set shape `bench/render_reference.ts` writes and `rigc check`
523
+ reads, drawn by the same rasteriser, so the output is a frame set rather than a
524
+ pile of images. `preview` writes one self-contained `.html` that plays the
525
+ artifact in the official Spine Web Player, with the skeleton, the atlas and every
526
+ page embedded as data URIs; the player is loaded from unpkg rather than copied, so
527
+ the first open needs a network and rigc redistributes nothing Esoteric Software
528
+ owns ([NOTICE.md](NOTICE.md)).
529
+
530
+ They complement each other rather than overlap. `render` is offline, deterministic
531
+ and measurable β€” its pixels are the ones `check` reports on. `preview` is the
532
+ interop proof: what plays there was played by Esoteric's own runtime, not by ours.
533
+
534
+ ### Letting someone choose β€” `rigc vote`
535
+
536
+ Sometimes looking is not enough on its own, because there is more than one
537
+ candidate and no instrument that can separate them: a pose fit with two local
538
+ optima that measure the same, a key density that is a matter of taste, a first
539
+ draft with no reference to compare against. `vote` is the deliberate human gate
540
+ for exactly that residue, and only for that residue.
541
+
542
+ ```bash
543
+ rigc vote --candidate spine-a --candidate spine-b [--animation <name>] [--out ballot.html]
544
+ rigc vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl] [--again]
545
+ ```
546
+
547
+ The first form writes one self-contained `ballot.html`: two to four compiled
548
+ candidates side by side, each in its own official player, looping, with one
549
+ button that restarts them together. The panes are labelled `A`, `B`, `C`, `D` and
550
+ show **no paths** β€” a voter who can see that `B` came out of `experiments/` is not
551
+ comparing pictures any more — so the path→label mapping lives in a manifest
552
+ embedded in the same file and is never rendered. A voter picks a winner or says
553
+ "tie / no preference", optionally writes a sentence, and copies or downloads a
554
+ small JSON result the page prints the filename for.
555
+
556
+ The second form checks that result against the ballot's own manifest and appends
557
+ it to an append-only JSONL ledger. Nothing is trusted: the result carries a
558
+ content **digest** per candidate, and a result whose digests are not this
559
+ ballot's, whose choice is not on it, or whose reason code contradicts its choice
560
+ is refused by a named rule (`V02_CANDIDATE_DIGESTS_ARE_THE_BALLOTS` and friends)
561
+ with nothing appended. A second vote on one ballot needs `--again`.
562
+
563
+ The loop it is built for, in one line: **the agent compiles N candidates that all
564
+ pass the gate β†’ `rigc vote` writes the ballot β†’ a human opens it, watches, and
565
+ votes β†’ `rigc vote --record` checks the answer into `votes.jsonl` β†’ the agent
566
+ reads the ledger and proceeds.** Compile first, vote last: a candidate reaches a
567
+ ballot only because it already validated green, so the human is never asked to
568
+ read JSON, a diff or a spec.
569
+
570
+ Three properties are worth stating because they are what make the ledger usable
571
+ by the next agent rather than by a reader:
572
+
573
+ - **A tie is a recorded outcome, not a missing one.** The ledger distinguishes a
574
+ ballot with a winner, a ballot the human called a tie, and a ballot nobody
575
+ opened. `both-unacceptable` is the tie that means *propose again*, and it is
576
+ unreachable if ties are not recordable.
577
+ - **The winner is a digest, not a label.** `B` means nothing outside one ballot;
578
+ the digest identifies the same pixels anywhere. Every line also carries its
579
+ `coverage` β€” which candidates the vote compared β€” so completeness is
580
+ computable rather than assumed.
581
+ - **Every line carries a reason code** from a closed enumeration, and the
582
+ enumeration is enforced: "tie, because this one is better" is refused.
583
+
584
+ Same player, same posture as `preview`: referenced from a CDN, never vendored,
585
+ and the file contains only your own art ([NOTICE.md](NOTICE.md)).
586
+
587
+ ## Run viewer β€” watching a *run* instead of reading it
588
+
589
+ πŸ”Ž **This is the ladder's instrument, not the way to look at your own rig** β€” that
590
+ is the section above. The viewer is reference-bound and repository-bound, and it
591
+ deliberately never ships.
245
592
 
246
593
  `check.txt` says a candidate's worst frame is f0012 at 56 MAE. The viewer shows
247
594
  you f0012.
@@ -308,8 +655,11 @@ the rest of the repository must not have) and is type-checked on its own with
308
655
  - A **motion spec** (`MotionSpec`, `spec: "rigc-motion/1"`) owns **time**: the rig
309
656
  it was authored against, named easing handles, setup overrides, a physics tuning
310
657
  table, and the animations β€” each with a declared duration, a loop flag, its
311
- tracks, and optionally a `drawOrder` timeline (the one timeline that names no
312
- target, so it sits on the animation rather than in `tracks`).
658
+ tracks, and five timeline families that sit on the animation rather than in `tracks`:
659
+ `drawOrder` and `events`, which name no target at all, and `ik`, `transform`
660
+ and `deform`, whose keys carry named fields instead of one value (an IK mix and
661
+ softness, six transform mixes, a sparse run of vertex offsets) β€” which is also
662
+ where 4.3 writes each of them.
313
663
 
314
664
  **Outputs β€” two files per cut**, written to the cut's `out` directory:
315
665
 
@@ -335,7 +685,7 @@ model (what is pinned, what may move, how authority falls off), and the
335
685
  ### The validator
336
686
 
337
687
  [`src/validate.ts`](src/validate.ts) parses the emitted artifacts with `spine-core`
338
- and then runs 34 named assertions over the loaded skeleton. Each one exists because
688
+ and then runs 36 named assertions over the loaded skeleton. Each one exists because
339
689
  the failure it catches is **silent**: the file loads, animates, and lies.
340
690
 
341
691
  Assertions whose data is absent are reported as **SKIP**, never folded into the pass
@@ -343,7 +693,7 @@ count β€” an assertion with nothing to check has not checked anything.
343
693
 
344
694
  #### Profiles β€” "wrong" versus "not how we do it here"
345
695
 
346
- Not all 34 rules are about Spine. Some are about **spine-html**, the renderer this
696
+ Not all 36 rules are about Spine. Some are about **spine-html**, the renderer this
347
697
  compiler was built to feed, and about one project's frame budget; they fire on real,
348
698
  correct, editor-produced Spine data, because the official example projects carry
349
699
  clipping attachments, unweighted meshes, 116-triangle meshes and packed atlases β€”
@@ -356,8 +706,15 @@ So `validate` and `build` take a `--profile`:
356
706
 
357
707
  | Profile | Runs | For |
358
708
  | --- | --- | --- |
359
- | `spine-html` | all 34 | **the default.** Is this a rig this project can ship? |
360
- | `spine` | the 20 validity rules | Is this valid Spine 4.3 that any runtime plays correctly? |
709
+ | `spine` | the 22 validity rules | **the default.** Is this valid Spine 4.3 that any runtime plays correctly? |
710
+ | `spine-html` | all 36 | Opt-in. Is this a rig *this* project can ship? |
711
+
712
+ `spine` is the default because it is the question this package's output answers:
713
+ the artifact imports into the Spine editor and plays in any 4.3 runtime, and
714
+ that is what the 22 validity rules are about. The other 14 are somebody's policy
715
+ β€” one renderer's, one canvas budget's, one compiler's own formations' β€” and a
716
+ rig arriving from anywhere else has no stake in them. Ask for them with
717
+ `--profile spine-html` when you want them.
361
718
 
362
719
  The **Profile** column below says which is which β€” `both` = validity, `renderer` and
363
720
  `archetype` = `spine-html` only, and **`both β—‘`** = a mixed assertion whose validity
@@ -388,7 +745,7 @@ the renderer policy*.
388
745
  | `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` version label is on the 4.3 line (the parser never checks it) |
389
746
  | `A17_ATLAS_PAGE_FILES_EXIST` | both | every page the atlas declares is a file on disk |
390
747
  | `A18_DETERMINISTIC_EMIT` | both | a second, independent compile of the same inputs is byte-identical. SKIPs when re-gating artifacts already on disk |
391
- | `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 |
748
+ | `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 |
392
749
  | `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 |
393
750
  | `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 |
394
751
  | `A22_MESH_UVS_IN_UNIT_RANGE` | both | every UV lies inside its region |
@@ -403,208 +760,8 @@ the renderer policy*.
403
760
  | `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | every draw-order key resolves to a real permutation: known slots, one entry per slot, each landing inside the slots array, offsets in ascending slot order. The **only assertion that runs before `A00`** β€” descending offsets make `readDrawOrder`'s forward-only cursor spin rather than return, so the round trip is refused by name instead of attempted |
404
761
  | `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 |
405
762
  | `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 |
406
-
407
- ## Install
408
-
409
- rigc runs on [Bun](https://bun.sh). The package ships its TypeScript sources and
410
- Bun runs them, so there is no build step and no `dist/` that can drift from the
411
- repository it was cut from.
412
-
413
- **The npm package is `spine-rigc`; the command it installs is `rigc`.** npm
414
- refuses the name `rigc` as too similar to packages that already exist, so the
415
- project, this repository and the executable keep their name and only the
416
- registry entry is spelled out.
417
-
418
- ```bash
419
- bunx spine-rigc --help # run it without installing
420
- bun add -g spine-rigc # or install the command
421
- bun add -d spine-rigc # or pin it in a project
422
- ```
423
-
424
- `npx spine-rigc` works too, as long as Bun is on `PATH` β€” the executable is a
425
- Bun script, and npm only writes the shim that calls it.
426
-
427
- Installed, the command is `rigc`. The examples below spell it `bun cli.ts`
428
- because they are written from a clone of this repository (`bun install`, then run
429
- the CLI in place); the two are interchangeable β€” `rigc build …` is
430
- `bun cli.ts build …`.
431
-
432
- Two commands are repository workflows rather than package ones: `bench` and
433
- `check` measure against Spine's official example projects and the reference
434
- frames rendered from them, which are fetched rather than redistributed (see
435
- [NOTICE.md](NOTICE.md)). They need a clone and `bun run fetch-examples`, and say
436
- so by name when the corpus is absent.
437
-
438
- ## First rig in ten minutes
439
-
440
- A whole rig, end to end, in a scratch directory: three tiny plates, two JSON
441
- files, one `build`, one `validate`. No clone, no art pipeline, nothing fetched.
442
-
443
- 🚫 **Every value below is invented for this section** β€” a doll that exists
444
- nowhere else in this repository. That is [AUTHORING.md](docs/AUTHORING.md) Β§3's
445
- rule applied here: no example value in these documents is copied out of a
446
- reference export, so nothing you read in a quickstart is an answer to anything
447
- [the ladder](docs/LADDER.md) measures.
448
-
449
- **1. Install the command.**
450
-
451
- ```bash
452
- bun add -g spine-rigc # installs `rigc`
453
- ```
454
-
455
- Or skip the install and prefix every command below with `bunx `, e.g.
456
- `bunx spine-rigc build …`.
457
-
458
- **2. Make a directory and three plates.** rigc measures PNGs rather than trusting
459
- a number you typed (R5), so the art has to exist. These three are solid colours a
460
- few dozen pixels across β€” a hull, a mast and a lamp:
461
-
462
- ```bash
463
- mkdir -p buoy/images && cd buoy
464
- bun -e '
465
- const parts = {
466
- "images/hull.png": "iVBORw0KGgoAAAANSUhEUgAAADgAAAAMCAYAAAA3bX6lAAAAKElEQVR42mOI8bL6P5wxw6gHRz046sFRD456cNSDox4c9eCoBwcrBgDSZ+mdl2OiDgAAAABJRU5ErkJggg==",
467
- "images/mast.png": "iVBORw0KGgoAAAANSUhEUgAAAAgAAAA0CAYAAAC3t3ldAAAAH0lEQVR42mO4dunIf3yYYVTBqIJRBaMKRhWMKhgcCgBGJo4s9YnopgAAAABJRU5ErkJggg==",
468
- "images/lamp.png": "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAHElEQVR42mP4v8HhPzUww6hBowaNGjRq0HAzCADvdrVmFPbc+QAAAABJRU5ErkJggg=="
469
- };
470
- for (const [p, b] of Object.entries(parts)) await Bun.write(p, Buffer.from(b, "base64"));
471
- '
472
- ```
473
-
474
- **3. The rig spec β€” `buoy.rig.json`.** Structure only: bones, the slots array in
475
- draw order, and one skin mapping each slot to a plate.
476
-
477
- ```json
478
- {
479
- "spec": "rigc-rig/1",
480
- "name": "buoy",
481
- "images": "images",
482
- "skeleton": { "width": 200, "height": 200 },
483
- "bones": [
484
- { "name": "root" },
485
- { "name": "hull", "parent": "root", "x": 0, "y": 0 },
486
- { "name": "mast", "parent": "hull", "x": 0, "y": 4 },
487
- { "name": "lamp", "parent": "mast", "x": 0, "y": 52 }
488
- ],
489
- "slots": [
490
- { "name": "mast", "bone": "mast", "attachment": "mast" },
491
- { "name": "hull", "bone": "hull", "attachment": "hull" },
492
- { "name": "lamp", "bone": "lamp", "attachment": "lamp" }
493
- ],
494
- "skins": {
495
- "default": {
496
- "mast": { "mast": { "image": "mast.png", "y": 26 } },
497
- "hull": { "hull": { "image": "hull.png" } },
498
- "lamp": { "lamp": { "image": "lamp.png" } }
499
- }
500
- }
501
- }
502
- ```
503
-
504
- Three things in there are worth naming, because each is a rule rather than a
505
- style: the **slots array is the setup draw order** (R4) β€” index 0 is furthest
506
- back, so the mast is behind the hull; the attachment carries an **`image`
507
- instead of a `width`/`height`** (R5), which is what makes the size in the
508
- skeleton and the size in the atlas incapable of drifting apart; and the mast's
509
- `"y": 26` offsets the plate *within* its slot so the bone sits at the mast's foot
510
- rather than its middle.
511
-
512
- **4. The motion spec β€” `buoy.motion.json`.** Time only, aimed at the rig by name:
513
-
514
- ```json
515
- {
516
- "spec": "rigc-motion/1",
517
- "archetype": "buoy",
518
- "cut": "buoy",
519
- "easings": { "swing": [0.42, 0, 0.58, 1] },
520
- "animations": {
521
- "bob": {
522
- "duration": 2,
523
- "loop": true,
524
- "tracks": [
525
- {
526
- "bone": "hull",
527
- "property": "translatey",
528
- "keys": [
529
- { "t": 0, "v": [0], "ease": "swing" },
530
- { "t": 0.5, "v": [5], "ease": "swing" },
531
- { "t": 1.5, "v": [-5], "ease": "swing" },
532
- { "t": 2, "v": [0] }
533
- ]
534
- },
535
- {
536
- "bone": "mast",
537
- "property": "rotate",
538
- "keys": [
539
- { "t": 0, "v": [-6], "ease": "swing" },
540
- { "t": 1, "v": [6], "ease": "swing" },
541
- { "t": 2, "v": [-6] }
542
- ]
543
- }
544
- ]
545
- }
546
- }
547
- }
548
- ```
549
-
550
- `archetype` must equal the rig's `name`. `duration` is declared and then checked
551
- against what actually compiled (R7). The **last key of each track carries no
552
- easing** β€” there is nothing after it to ease towards, and saying otherwise is a
553
- compile error.
554
-
555
- **5. Build, then re-gate what it wrote.**
556
-
557
- ```bash
558
- rigc build --rig buoy.rig.json --motion buoy.motion.json --images images --out spine
559
- rigc validate spine
560
- ```
561
-
562
- `build` prints every assertion by name, then the shape of what it emitted, then
563
- the two files:
564
-
565
- ```
566
- .. 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
567
- rigc: wrote …/buoy/spine/skeleton.json
568
- rigc: wrote …/buoy/spine/skeleton.atlas
569
- ```
570
-
571
- and `validate` re-reads those artifacts from disk and ends `rigc: green`. That is
572
- a rig. `spine/skeleton.json` is Spine 4.3 skeleton data β€” it loads in a Spine
573
- runtime and it imports into the Spine editor.
574
-
575
- **Try breaking it**, because the validator's messages are the interface here and
576
- they are worth meeting once on purpose. Rename `images/hull.png` to
577
- `images/raft.png`, point the spec's `image` at the new name, and build again:
578
-
579
- ```
580
- FAIL A08_REGION_NAMES_MATCH_ATTACHMENTS: attachment "hull" resolves to region "raft"; v0 requires them identical
581
- rigc: 1 assertion(s) failed β€” nothing written
582
- ```
583
-
584
- Nothing was written. A red run leaves no half-built artifact on disk to mistake
585
- for a result, and there is no flag that changes that.
586
-
587
- **Where to go next.**
588
-
589
- - πŸ“˜ **[docs/AUTHORING.md](docs/AUTHORING.md)** is the real guide β€” both files
590
- field by field, the emission rules, every named failure mapped to the file that
591
- has to change, and Β§8–§9 for reproducing a shot you were given as pictures. It
592
- ships inside the npm package too, at
593
- `node_modules/spine-rigc/docs/AUTHORING.md`.
594
- - `rigc explain --rig buoy.rig.json --motion buoy.motion.json --out spine` prints
595
- the compiled rig as a table β€” every bone with its resolved parent, the slots in
596
- draw order, every timeline key by key β€” and writes nothing. It is what to reach
597
- for when a rig compiles and still looks wrong.
598
- - 🚨 **A green gate does not mean the animation is right**, and no assertion
599
- could. If you have reference pictures of the shot,
600
- `rigc check --candidate spine --frames <dir>` is the half of the loop that can
601
- see a wrong animation β€” AUTHORING.md Β§9.
602
- - [docs/LADDER.md](docs/LADDER.md) is the benchmark: the same job, from a brief
603
- and rendered frames, scored. [docs/PILOT.md](docs/PILOT.md) is how to run an
604
- agent through it and score what comes back.
605
- - πŸ€– **Handing the authoring to an AI agent?**
606
- [docs/PROMPTING.md](docs/PROMPTING.md) is the operator's page β€” the six prompt
607
- clauses a measured pilot run paid for, and what you can leave unsaid.
763
+ | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | every `ik` / `transform` timeline names a constraint of that type and carries at least one key. The name-and-type miss is loud in the parser (`IK Constraint not found`) and this one says which constraints the skeleton *does* have; the empty key array is silent β€” `readAnimation` reads key 0, finds nothing and skips the timeline without a word. SKIPs when no animation carries one |
764
+ | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | every deform key's run lands inside the attachment's own deform array, starts on an even index, holds an even count of finite numbers, and names a skin/slot/attachment triple that resolves. The array is one `x, y` pair per **vertex** on an unweighted attachment and one per **bone influence** on a weighted one, so its length is measured from the attachment rather than assumed. An overlong run is the format's quietest defect: `Utils.arrayCopy` into a `Float32Array` drops everything past the end, so part of the mesh deforms and it looks nearly right. SKIPs when no animation carries a deform timeline |
608
765
 
609
766
  ## Usage
610
767
 
@@ -629,6 +786,11 @@ bun cli.ts build \
629
786
  [--manifest path/to/manifest.json] [--images path/to/images]
630
787
  ```
631
788
 
789
+ By default, atlas page paths point back at the source art wherever it lives β€”
790
+ often outside `--out` β€” so add `--copy-images` when `spine/` itself needs to be
791
+ self-contained (zipped, committed, or handed off on its own): it copies every
792
+ referenced page PNG into `--out` and rewrites the atlas to match.
793
+
632
794
  …or register cuts in a `cuts.json` and build them by name. Every path in the table
633
795
  resolves **relative to the `cuts.json` file itself**, so the table lives with the
634
796
  project that owns the art:
@@ -654,17 +816,28 @@ commands:
654
816
  ```bash
655
817
  bun cli.ts explain --cut my_cut --cuts path/to/cuts.json # the compiled rig as a table
656
818
  bun cli.ts validate path/to/spine # re-gate artifacts already on disk
657
- bun cli.ts validate --profile spine path/to/any/skeleton # spec rules only (see Profiles)
819
+ bun cli.ts validate --profile spine-html path/to/spine # …and this project's policy too (see Profiles)
658
820
  bun cli.ts diff candidate.json reference.json # structural comparison
659
821
  bun cli.ts check --candidate path/to/spine \
660
822
  --frames bench/reference/3-timing-and-spacing # against pictures
661
823
  bun cli.ts bench 3 --candidate path/to/spine # one rung of the ladder
824
+ bun cli.ts render --candidate path/to/spine # PNG frames + a contact sheet
825
+ bun cli.ts preview --candidate path/to/spine # one .html that plays it
826
+ bun cli.ts vote --candidate path/to/a --candidate path/to/b # one .html that asks which
827
+ bun cli.ts vote --record vote-<id>.json # check the answer into votes.jsonl
662
828
  ```
663
829
 
664
830
  `validate` on a bare directory checks what it can see. Adding `--cut`/`--cuts` lets
665
831
  it re-derive the declared durations and the structural expectations too, and the
666
- report says which it had. `build` and `validate` both take `--profile spine` to drop
667
- the renderer and archetype policy; the default stays `spine-html`.
832
+ report says which it had. `build` and `validate` both default to `--profile spine`,
833
+ the 22 validity rules; `--profile spine-html` adds this project's renderer and
834
+ archetype policy on top.
835
+
836
+ `render` and `preview` are the two that need no reference at all β€” see
837
+ [Looking at a rig](#looking-at-a-rig--rigc-render-and-rigc-preview). Run either
838
+ straight after a green `build`, on the same directory `--out` wrote. `vote` is the
839
+ same idea with more than one candidate in the page and an answer coming back β€”
840
+ see [Letting someone choose](#letting-someone-choose--rigc-vote).
668
841
 
669
842
  ## Checks
670
843
 
@@ -675,7 +848,7 @@ bun run selftest # the validator's own negative controls (next section)
675
848
  ```
676
849
 
677
850
  All three run on every push and pull request β€”
678
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Bun runs the sources
851
+ [`.github/workflows/ci.yml`](https://github.com/firejune/rigc/blob/main/.github/workflows/ci.yml). Bun runs the sources
679
852
  directly, so the first two are not on the path of anything; they exist because a
680
853
  convention nothing checks is a convention. `tsconfig.json` is
681
854
  `strict: false` with `strictNullChecks: true` and says in place why the rest is
@@ -695,7 +868,7 @@ for each. Two further edits are *tolerance* controls the gate must let through,
695
868
  because a widened assertion can fail by firing too often as easily as by firing too
696
869
  rarely.
697
870
 
698
- **The rigs it breaks are generated.** [`fixtures/public.ts`](fixtures/public.ts)
871
+ **The rigs it breaks are generated.** [`fixtures/public.ts`](https://github.com/firejune/rigc/blob/main/fixtures/public.ts)
699
872
  writes three synthetic cuts into a temp directory on every run, and between them
700
873
  they carry every structure the assertions have an opinion about β€” region
701
874
  attachments, attachment swaps, rgba fades, a ring mesh on a control bone, a ribbon
@@ -761,22 +934,28 @@ nothing substantive executed exits 2 rather than printing green.
761
934
 
762
935
  ```
763
936
  tsconfig.json type-check config (noEmit); eslint.config.js β€” the no-any gate
764
- cli.ts build / validate / explain / diff / check / bench
937
+ cli.ts build / validate / explain / diff / check / bench / render / preview / vote
765
938
  selftest.ts the validator's own negative controls, and diff's and check's
766
939
  fixtures/ public.ts β€” the three synthetic cuts the selftest breaks
767
940
  src/
768
941
  compile.ts rig + motion spec (+ manifest) -> skeleton JSON + atlas text (pure data assembly)
769
942
  rig.ts the rig spec β€” `spec: "rigc-rig/1"`, the skeleton as data
770
- validate.ts spine-core round trip + the 34 assertions
943
+ validate.ts spine-core round trip + the 36 assertions
771
944
  diff.ts structural comparison of two skeletons, one ratio per measure
772
- render.ts the rasteriser (regions + meshes), shared by the reference renderer and check
945
+ render.ts the rasteriser (regions + meshes), shared by the reference renderer,
946
+ `rigc render` and check
947
+ preview.ts the single-file HTML player page β€” the artifact embedded as data
948
+ URIs, played by the official Spine Web Player (referenced, not vendored)
949
+ ballot.ts the same page with 2–4 candidates in it and a vote coming back β€”
950
+ candidate digests, the ballot manifest, and the refusals that
951
+ stand between a saved vote and the ledger
773
952
  check.ts a candidate against rendered frames β€” pixels and per-slot drift,
774
953
  and it never opens the reference skeleton
775
954
  ladder.ts which example is which rung, and which file in it is the reference
776
955
  timelines.ts the 4.3 timeline catalogue and its walker (shared, pure JSON)
777
956
  mesh.ts ring and ribbon mesh builders, weighted-vertex encoding
778
957
  transform.ts crop pixels (y down) <-> Spine world (y up), world transforms
779
- png.ts PNG header reader (size and colour type, no decode)
958
+ png.ts PNG header reader (size, colour type, tRNS; no pixel decode)
780
959
  errors.ts CompileError, and NotImplementedError for what the format holds
781
960
  and the emitter does not write
782
961
  types.ts manifest, motion spec, and emitted-JSON shapes
@@ -793,7 +972,8 @@ viewer/ the run viewer β€” dev server only, no build (see above)
793
972
  vite.config.ts /api/inventory and /repo/<path>, and the build refusal
794
973
  inventory.ts what is under bench/runs, resolved to URLs
795
974
  main.ts the two panes, the transport, the report
796
- docs/ AUTHORING.md (how to author a rig), LADDER.md (live rung status),
975
+ docs/ AUTHORING.md (how to author a rig), GATE.md (the clause statements
976
+ a candidate is graded against), LADDER.md (live rung status),
797
977
  SPEC_COVERAGE.md (format survey),
798
978
  feature_matrix.{csv,json}
799
979
  .github/ workflows/ β€” ci.yml (the gates) and release.yml (release-please)
@@ -806,14 +986,14 @@ CONTRIBUTING.md how to propose a change; RELEASING.md β€” how a version is cut
806
986
  | --- | --- |
807
987
  | `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 |
808
988
  | `contact.ts` | plate-vs-plate overlap measurement β€” the largest advance that keeps two footprints disjoint |
809
- | `plate.ts` / `png_probe.mjs` | minimal PNG read/write and decode |
989
+ | `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) |
810
990
  | `font5x7.ts` | bitmap labels for diagnostic images and generated plates |
811
991
 
812
992
  ## Contributing
813
993
 
814
- Issues are the ledger; see [CONTRIBUTING.md](CONTRIBUTING.md) for what a change
994
+ Issues are the ledger; see [CONTRIBUTING.md](https://github.com/firejune/rigc/blob/main/CONTRIBUTING.md) for what a change
815
995
  has to clear before it lands. Releases are cut by release-please β€”
816
- [RELEASING.md](RELEASING.md).
996
+ [RELEASING.md](https://github.com/firejune/rigc/blob/main/RELEASING.md).
817
997
 
818
998
  ## Licence
819
999