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/NOTICE.md +21 -0
- package/README.md +409 -229
- package/bin/rigc.cjs +40 -0
- package/cli.ts +916 -56
- package/docs/AUTHORING.md +345 -27
- package/docs/PROMPTING.md +106 -0
- package/docs/SPEC_COVERAGE.md +16 -14
- package/package.json +4 -2
- package/src/ballot.ts +869 -0
- package/src/compile.ts +796 -137
- package/src/emit.ts +88 -0
- package/src/json-position.ts +253 -0
- package/src/png.ts +72 -7
- package/src/preview.ts +243 -0
- package/src/render.ts +58 -0
- package/src/types.ts +171 -0
- package/src/validate.ts +287 -14
- package/tools/plate.ts +164 -21
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/
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
312
|
-
|
|
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
|
|
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
|
|
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
|
|
360
|
-
| `spine` |
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
|
667
|
-
the
|
|
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
|
|
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
|
|
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
|
|
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),
|
|
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
|
|