spine-rigc 0.8.0 → 0.9.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 +125 -716
- package/cli.ts +118 -7
- package/docs/AUTHORING.md +524 -97
- package/docs/SPEC_COVERAGE.md +27 -12
- package/package.json +4 -1
- package/src/check.ts +552 -29
- package/src/compile.ts +674 -48
- package/src/mesh.ts +832 -3
- package/src/render.ts +300 -7
- package/src/rig.ts +448 -20
- package/src/transform.ts +13 -0
- package/src/types.ts +82 -7
- package/src/validate.ts +347 -38
- package/tools/contact.ts +126 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/png_probe.mjs +72 -0
package/README.md
CHANGED
|
@@ -12,6 +12,32 @@
|
|
|
12
12
|
verified by a `spine-core` round-trip. Built so AI agents can author rigs and check
|
|
13
13
|
their own work.
|
|
14
14
|
|
|
15
|
+
## What you get
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/rigc-demo.gif" alt="Loose part PNGs assembling themselves into a character that breathes, blinks and waves" width="600" />
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center"><em>Fourteen hand-drawn part PNGs, one rig spec, one motion spec — the assembly,
|
|
22
|
+
the breathing and the wave are all rigc-compiled Spine animations, rendered with
|
|
23
|
+
<code>rigc render</code>.</em></p>
|
|
24
|
+
|
|
25
|
+
Loose part PNGs and two small JSON files in; **Spine 4.3 skeleton data out** — a
|
|
26
|
+
`skeleton.json` and a `skeleton.atlas` that load in any Spine runtime and **import
|
|
27
|
+
into the Spine editor**. Nothing is written unless a round-trip through Spine's own
|
|
28
|
+
parser and a list of named assertions all come back green.
|
|
29
|
+
|
|
30
|
+
| You have | You run | You get |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| part PNGs, a rig spec and a motion spec | `rigc build` | `skeleton.json` + `skeleton.atlas` — or a failure named by rule, and **nothing on disk** |
|
|
33
|
+
| a compiled rig | `rigc render` | every animation as PNG frames, plus one labelled contact sheet of the whole shot |
|
|
34
|
+
| a compiled rig | `rigc preview` | one self-contained `.html` that plays it in Spine's own web player |
|
|
35
|
+
| two to four compiled rigs | `rigc vote` | one ballot page a human picks from, and the answer checked into a ledger |
|
|
36
|
+
| a picture of a key pose | `rigc pose` | where each loose part PNG sits in it, in spec coordinates — the movement between two poses is then yours to key ([docs/MOTION.md](docs/MOTION.md)) |
|
|
37
|
+
|
|
38
|
+
Everything in that table needs Bun and this package: no clone, no reference art, no
|
|
39
|
+
art pipeline, no server.
|
|
40
|
+
|
|
15
41
|
## What rigc is, and what it is not
|
|
16
42
|
|
|
17
43
|
rigc emits **Spine's own skeleton data format**. That is the whole positioning, and
|
|
@@ -223,8 +249,8 @@ rigc: wrote …/buoy/spine/skeleton.atlas
|
|
|
223
249
|
```
|
|
224
250
|
|
|
225
251
|
`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
|
|
252
|
+
runtime plays correctly?* That is the default, and the [Profiles](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md#profiles--wrong-versus-not-how-we-do-it-here)
|
|
253
|
+
section of the benchmark dossier is where the other one lives. `validate` then re-reads those
|
|
228
254
|
artifacts from disk and ends `rigc: green`. That is a rig. `spine/skeleton.json`
|
|
229
255
|
is Spine 4.3 skeleton data — it loads in a Spine runtime and it imports into the
|
|
230
256
|
Spine editor.
|
|
@@ -291,7 +317,7 @@ The voter picks a winner or says "tie / no preference"; the page hands them a
|
|
|
291
317
|
small JSON file to save; `--record` checks that file against the ballot's own
|
|
292
318
|
hashes and appends one line to an append-only ledger, refusing by name anything
|
|
293
319
|
that does not belong to it. See
|
|
294
|
-
[Letting someone choose](#letting-someone-choose--rigc-vote).
|
|
320
|
+
[Letting someone choose](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md#letting-someone-choose--rigc-vote).
|
|
295
321
|
|
|
296
322
|
**Where to go next.**
|
|
297
323
|
|
|
@@ -315,159 +341,105 @@ that does not belong to it. See
|
|
|
315
341
|
[docs/PROMPTING.md](docs/PROMPTING.md) is the operator's page — the six prompt
|
|
316
342
|
clauses a measured pilot run paid for, and what you can leave unsaid.
|
|
317
343
|
|
|
318
|
-
##
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
`
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
344
|
+
## See what you built, and let someone choose
|
|
345
|
+
|
|
346
|
+
Steps 6 and 7 above are the three commands that need nothing but a compiled rig — no
|
|
347
|
+
reference frames, no second package, no server. **`render`** writes every frame as a
|
|
348
|
+
PNG plus one contact-sheet grid of the whole shot; **`preview`** writes one
|
|
349
|
+
self-contained `.html` that plays it in Spine's own web player; **`vote`** puts two to
|
|
350
|
+
four candidates in one page and takes a human's answer back. Reach for them the moment
|
|
351
|
+
a rig compiles green, because green says nothing at all about the picture.
|
|
352
|
+
|
|
353
|
+
Three properties of `vote` are worth stating, because they are what make its ledger
|
|
354
|
+
usable by the next agent rather than by a reader: **a tie is a recorded outcome, not a
|
|
355
|
+
missing one** — `both-unacceptable` is the tie that means *propose again*, and it is
|
|
356
|
+
unreachable if ties are not recordable; **the winner is a digest, not a label**, since
|
|
357
|
+
`B` means nothing outside one ballot while a digest identifies the same pixels
|
|
358
|
+
anywhere; and **every line carries a reason code** from a closed enumeration that is
|
|
359
|
+
enforced, so *"tie, because this one is better"* is refused.
|
|
360
|
+
|
|
361
|
+
🎞️ **Authoring the movement those pages show you** — key poses, in-betweening, and how
|
|
362
|
+
to spread candidates so a ballot informs — is [docs/MOTION.md](docs/MOTION.md).
|
|
363
|
+
|
|
364
|
+
📐 **`rigc pose --images parts/ --frame poseA.png` runs the other way.** Every command
|
|
365
|
+
above takes something you authored and tells you about it; this one takes a **picture
|
|
366
|
+
the user already has** — one key pose — and reports where each loose part PNG sits in
|
|
367
|
+
it, so those coordinates go into the rig and the motion **by construction** and the
|
|
368
|
+
effort goes into the part no instrument can measure: the movement between two poses.
|
|
369
|
+
A part that matches nowhere is refused by name, two near-equal placements are reported
|
|
370
|
+
as both, and nothing it prints is a score. Fields, the coordinate contract and the
|
|
371
|
+
limits: [AUTHORING.md §11](docs/AUTHORING.md).
|
|
372
|
+
|
|
373
|
+
## Commands
|
|
374
|
+
|
|
375
|
+
Every command takes its paths explicitly. `rigc <command> --help` prints its flags, and
|
|
376
|
+
[AUTHORING.md §0](docs/AUTHORING.md) is the same list with what each flag means, which
|
|
377
|
+
commands take it and what its default is.
|
|
378
|
+
|
|
379
|
+
| Command | Does |
|
|
380
|
+
| --- | --- |
|
|
381
|
+
| `build --rig … --motion … --out …` | compiles, gates, and **writes only if the gate is green**. `--images <dir>` says where the rig spec's `image` names resolve, `--manifest` adds measured art, and `--copy-images` copies every page PNG into `--out` so the directory is self-contained |
|
|
382
|
+
| `validate <dir>` | re-gates artifacts already on disk |
|
|
383
|
+
| `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
|
|
384
|
+
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/` |
|
|
385
|
+
| `preview --candidate <dir>` | one self-contained `.html` that plays it |
|
|
386
|
+
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
387
|
+
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
|
388
|
+
| `diff <candidate.json> <reference.json>` | structural comparison of two skeletons, one ratio per measure and deliberately no combined score |
|
|
389
|
+
| `check --candidate <dir> --frames <dir>` | the candidate against reference pictures — the only instrument here that can see a *wrong animation* |
|
|
390
|
+
| `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
|
|
391
|
+
|
|
392
|
+
`diff`, `check` and `bench` measure against something you were given; the first two
|
|
393
|
+
work on any frames you have, and `bench` is a repository workflow that needs a clone
|
|
394
|
+
and `bun run fetch-examples`. The reasoning behind all three is in
|
|
395
|
+
[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
|
|
396
|
+
|
|
397
|
+
`build` and `validate` both default to `--profile spine` — the 25 validity rules, which
|
|
398
|
+
ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
|
|
399
|
+
adds all 39: the other 14 are one renderer's policy and one canvas budget's, and they
|
|
400
|
+
fire on perfectly correct editor-produced Spine data, so reach for that profile when
|
|
401
|
+
you are shipping into *that* project rather than to be thorough. A report always names
|
|
402
|
+
the profile it ran and lists what that profile left out.
|
|
403
|
+
|
|
404
|
+
Several cuts can also be registered in a `cuts.json` and built by name
|
|
405
|
+
(`build --cut my_cut --cuts path/to/cuts.json`); every path in that table resolves
|
|
406
|
+
relative to the `cuts.json` file itself, so the table lives with the project that owns
|
|
407
|
+
the art. Its shape is under
|
|
408
|
+
[Usage](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md#usage).
|
|
409
|
+
|
|
410
|
+
## Documentation
|
|
411
|
+
|
|
412
|
+
| Document | For |
|
|
413
|
+
| --- | --- |
|
|
414
|
+
| 📘 **[docs/AUTHORING.md](docs/AUTHORING.md)** | **the format guide, and the one to read before writing a spec.** Both input files field by field with a complete minimal example each, every field with its Spine meaning, the rules that decide what is emitted, the build → read the report → fix → repeat loop, the map from every named failure to the file that has to change, and the features rigc refuses by name so you do not spend a loop discovering them. It travels **inside the npm package**, at `node_modules/spine-rigc/docs/AUTHORING.md` |
|
|
415
|
+
| 🎞️ **[docs/MOTION.md](docs/MOTION.md)** | **the key-pose recipe.** How to get two poses, what a pair of poses does and does not fix, the in-betweening rules and where each comes from, and how to spread candidates so a ballot informs. Ships in the package too |
|
|
416
|
+
| 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
|
|
417
|
+
| 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
|
|
418
|
+
| 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 36 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
|
|
419
|
+
| 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
|
|
332
420
|
|
|
333
|
-
|
|
334
|
-
bun cli.ts diff candidate.json reference.json [--json report.json]
|
|
335
|
-
```
|
|
421
|
+
## Why you can trust the output
|
|
336
422
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
with the right skeleton and the wrong timing apart from a rig with the right
|
|
341
|
-
timing and the wrong skeleton, and those are opposite diagnoses.
|
|
342
|
-
|
|
343
|
-
Three properties the measures are built to have:
|
|
344
|
-
|
|
345
|
-
- **`diff X X` is 1.000 on every measure.** A comparison tool that cannot
|
|
346
|
-
recognise identity is reporting noise, and noise looks like a small honest gap.
|
|
347
|
-
The selftest asserts it.
|
|
348
|
-
- **A difference moves as few measures as possible.** Reordering two slots moves
|
|
349
|
-
`slots.order` and nothing else — not the slot-to-bone bindings, not the setup
|
|
350
|
-
attachments — so the report says *where* a rig is wrong, not just *how much*.
|
|
351
|
-
Each selftest case names the exact set of measures its edit may disturb.
|
|
352
|
-
- **Name-agnostic figures sit beside name-matched ones.** A candidate that builds
|
|
353
|
-
the right tree under its own bone names scores 0 on `bones.parent_by_name` and
|
|
354
|
-
1.000 on `bones.depth_histogram` and `bones.degree_sequence`. Reporting only the
|
|
355
|
-
first calls a correct rig a total failure; reporting only the second calls any
|
|
356
|
-
14-bone tree a match. That holds at the section level too: `bones` and `slots`
|
|
357
|
-
are the two sections whose measures are mostly name-keyed, so each reports **two
|
|
358
|
-
figures**, and the pair is the finding —
|
|
359
|
-
|
|
360
|
-
```
|
|
361
|
-
bones 0.567 (name-matched) · 1.000 (name-agnostic)
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
reads *"the tree is right and the vocabulary is different"*, which the single
|
|
365
|
-
mean on its own could not say. They are two comparisons with their own measure
|
|
366
|
-
sets rather than two halves of one; the name-matched figure is unchanged, so
|
|
367
|
-
older reports stay comparable. `sections[].nameAgnostic` in the JSON lists them.
|
|
368
|
-
|
|
369
|
-
An assertion or measure with nothing to compare reports its `total` as 0 and says
|
|
370
|
-
so, exactly as the validator's SKIP does — a vacuous 1.000 that looks earned is
|
|
371
|
-
the same false green in a different costume.
|
|
372
|
-
|
|
373
|
-
### Checking a rig against the pictures — `rigc check`
|
|
423
|
+
rigc is measured against **Spine's own official example projects** — the
|
|
424
|
+
`1-weight-and-mass` … `8-follow-through` series as a difficulty ladder, with spineboy
|
|
425
|
+
as the graduation exam.
|
|
374
426
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
⭐ **Neither the gate nor `diff` can see a wrong animation.** The gate checks
|
|
380
|
-
validity: it parses the skeleton, steps every animation and refuses anything
|
|
381
|
-
degenerate, and it has no opinion about whether the animation is the one that was
|
|
382
|
-
asked for. `diff` checks structure: a reversed easing is the same timeline, the
|
|
383
|
-
same key count and the same curve kind. Three honest ladder runs have now produced
|
|
384
|
-
**zero** validator FAILs between them, and one of them shipped a build in which
|
|
385
|
-
every easing in the file was reversed — green, and sincerely reported as done.
|
|
386
|
-
|
|
387
|
-
`check` is the instrument for that. It renders the candidate with the same
|
|
388
|
-
rasteriser that drew the reference frames, onto the same pixel grid, and reports
|
|
389
|
-
per animation and per frame:
|
|
390
|
-
|
|
391
|
-
- **MAE over the union alpha** — the mean absolute RGB difference over the pixels
|
|
392
|
-
either side covers, 0..255. The whole-frame figure is printed beside it and never
|
|
393
|
-
instead of it: most of a frame is background on both sides, so that number is
|
|
394
|
-
small for every candidate and the gap between a good one and a bad one smaller
|
|
395
|
-
still.
|
|
396
|
-
- **The framing** — where the candidate's drawn pixels sit against the reference's,
|
|
397
|
-
as a scale, an offset and a residual. It is printed first because it is upstream
|
|
398
|
-
of everything else: get it wrong and the error arrives disguised as motion. On a
|
|
399
|
-
skeleton root it is decided **per animation directory**: a set whose own pixels
|
|
400
|
-
land in the box `frames.json` records is measured there, exactly, and the rest
|
|
401
|
-
share one fitted framing. `--framing shared` measures every set in the shared one
|
|
402
|
-
— the whole-root behaviour before issue #100, and worth 15–25 MAE on a character.
|
|
403
|
-
A **fitted** framing then gets one last pass that searches whole-pixel offsets
|
|
404
|
-
(±2 px) for the lowest MAE and takes the best one, because a fit registers extent
|
|
405
|
-
and the best fit of two extents is not the best alignment of two pictures — a
|
|
406
|
-
constant pixel is worth up to 30 % of a set's figure (issue #146). The line says
|
|
407
|
-
what it moved and what that was worth, and it says so when the identity won as
|
|
408
|
-
well. A box that is **not** an estimate — `frames.json`'s own, or one you pinned —
|
|
409
|
-
is never moved: there the same search is reported as a finding, because a constant
|
|
410
|
-
pixel inside the right box is the candidate's own figure sitting off, not framing.
|
|
411
|
-
- **The whole shot, against the contact sheet** — a set that ships a couple of
|
|
412
|
-
stills and folds every sampled frame into one `contact.png` (rung 2's do,
|
|
413
|
-
spineboy's `@30fps` sets do) used to be compared on the stills alone, honestly
|
|
414
|
-
reported and empty behind: nothing at all was measured about the frames in
|
|
415
|
-
between. `check` now samples the candidate at the set's own rate and compares it
|
|
416
|
-
against the sheet's own tiles, whose grid it measures off the sheet (issue #36).
|
|
417
|
-
MAE only, and a sheet that is not a grid of those frames is refused by name.
|
|
418
|
-
- **Per-frame change** — how many pixels each side moved since **its own** previous
|
|
419
|
-
frame, compared against each other. It is the only measure here that looks at the
|
|
420
|
-
relation between two frames rather than at one, and it is what catches a held pose
|
|
421
|
-
the candidate does not hold, or a one-frame event that never fired: both are cheap
|
|
422
|
-
in every individual frame and invisible to an aggregate.
|
|
423
|
-
- **Per-slot drift** — where each of the candidate's own slots landed against the
|
|
424
|
-
reference frame, in pixels. MAE says *how wrong*; a slot's drift says *which
|
|
425
|
-
part, which way, how far*. Where the reference merged two parts into one blob —
|
|
426
|
-
the trap [AUTHORING §8](docs/AUTHORING.md) opens with, and it counts as merged
|
|
427
|
-
even when one part is most of the blob (issue #37) — the slot is
|
|
428
|
-
template-matched against its own rendered pixels instead, with a confidence; and
|
|
429
|
-
where nothing inside the distance that slot could plausibly have moved matches
|
|
430
|
-
it, the answer is **no match** rather than a number about some other part.
|
|
431
|
-
- **Per-chain attribution** — the same two, rolled up onto the unit an author
|
|
432
|
-
repairs. `check` cuts the **candidate's own** bone tree into chains at its branch
|
|
433
|
-
points and prints, per chain per set, the worst slot drift with its slot and
|
|
434
|
-
frame, the mean, the error per pixel inside it, and its share of the set's error
|
|
435
|
-
over the reference's own drawn pixels — plus one rollup line per chain across
|
|
436
|
-
every set. A figure with a dozen joints otherwise collapses to one number a shot,
|
|
437
|
-
and *"motion ✗"* over sixteen shots does not say which limb to re-key.
|
|
438
|
-
|
|
439
|
-
🔒 **It never reads the reference skeleton.** It opens the candidate and PNG
|
|
440
|
-
frames, and nothing else: every reference-side read goes through one guard that
|
|
441
|
-
refuses a path which is not a `.png` or the frame set's `frames.json`, and the
|
|
442
|
-
selftest makes that guard fire. That is what lets `check` sit *inside* an
|
|
443
|
-
authoring loop where `bench` cannot — running it as often as you like does not
|
|
444
|
-
stop a run being an authoring run.
|
|
445
|
-
|
|
446
|
-
The candidate is framed **by its own drawn pixels**, not by the reference's world
|
|
447
|
-
box. A candidate is authored in its own coordinate system and under the ladder's
|
|
448
|
-
honesty rule could not be authored in any other, so both sides are measured the
|
|
449
|
-
same way — the content box of what each actually draws — and one similarity
|
|
450
|
-
transform, fitted by least squares over every edge of every frame, carries the
|
|
451
|
-
candidate's onto the reference's. Two skeletons depicting the same shot land on the
|
|
452
|
-
same pixels whatever coordinates they were authored in, an invisible transparent
|
|
453
|
-
margin cannot move the result, and no single quad corner in a single frame can set
|
|
454
|
-
the scale for a run.
|
|
455
|
-
|
|
456
|
-
There is no pass mark **in the tool**, for the same reason `diff` has none. The
|
|
457
|
-
ladder's pass definition and its thresholds are a document read by a person over
|
|
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
|
|
460
|
-
exit code either command could produce.
|
|
461
|
-
|
|
462
|
-
### Benchmark ladder — the rungs, and where they stand
|
|
463
|
-
|
|
464
|
-
🎓 **The ladder is complete, 2026-08-28.** All eight numbered rungs and the
|
|
465
|
-
spineboy graduation exam are cleared under gate v2.1 and hold under **v2.2**, every clause PASS or SKIP:
|
|
427
|
+
🎓 **The ladder was completed 2026-08-28.** All eight numbered rungs and the
|
|
428
|
+
spineboy graduation exam cleared under gate v2.1 and held under **v2.2**, every clause PASS or SKIP:
|
|
466
429
|
worst attributable slot drift **5.55 px** against a 6.0 px bar, and **0 of 124**
|
|
467
430
|
frame-change disagreements. Recompiling the same spec in a different session
|
|
468
431
|
reproduced every field of the measurement record **to the digit**. The rungs stay
|
|
469
432
|
in place as regression gates.
|
|
470
433
|
|
|
434
|
+
⚠️ **Current state, 2026-09-02: seven of the eight hold, and rung 7 is open.**
|
|
435
|
+
`check`'s extent tolerance ([PR #254](https://github.com/firejune/rigc/pull/254))
|
|
436
|
+
changed which box a set is measured in, and rung 7's stored candidate fails **G2**
|
|
437
|
+
under it — one of its three slots draws in every set and is attributable in none,
|
|
438
|
+
and no read-down kind survives the framing change. **Rungs 1–6 and 8 and the
|
|
439
|
+
graduation exam are unaffected**: each reproduces its gated figures to the digit,
|
|
440
|
+
and the 5.55 px and 0-of-124 figures above are among them. Verdict and reasoning
|
|
441
|
+
in [docs/LADDER.md](docs/LADDER.md)'s *PR #254 instrument re-inspection*.
|
|
442
|
+
|
|
471
443
|
⚠️ **What that certifies, stated exactly.** That **the tool, the guide and the
|
|
472
444
|
protocol reach the bar across a bounded series of honest attempts, each residual
|
|
473
445
|
diagnosed and fixed** — spineboy took five, and the last inherited its
|
|
@@ -475,574 +447,11 @@ predecessor's specs under the run protocol's inheritance clause. It is **not**
|
|
|
475
447
|
that an agent authors a spineboy-scale rig from the brief alone in one run: the
|
|
476
448
|
ladder has not demonstrated that, and each row records which of the two it is.
|
|
477
449
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
one with:
|
|
484
|
-
|
|
485
|
-
```bash
|
|
486
|
-
bun cli.ts bench 3 --candidate path/to/candidate/spine
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
`bench` validates the candidate under `--profile spine`, diffs it against that
|
|
490
|
-
rung's reference export, and prints both. It exits non-zero only when validation
|
|
491
|
-
fails: the diff has no threshold, because there is no rung score. Add
|
|
492
|
-
`--frames <dir>` and it folds in the `check` table below, so a ladder row carries
|
|
493
|
-
fidelity as well as structure.
|
|
494
|
-
|
|
495
|
-
#### What the rungs need
|
|
496
|
-
|
|
497
|
-
[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md) surveys the full Spine 4.3 export surface against what
|
|
498
|
-
rigc emits and against what the nine examples measurably use (`bun run bench:usage` regenerates the
|
|
499
|
-
counts). Three blockers sat *before* rung 1: **B1**, the bone tree was code in `archetype.ts` rather
|
|
500
|
-
than data, so no example could be expressed at all; **B2**, `A16`'s regex rejected the `"4.3.75-beta"`
|
|
501
|
-
that every example declares; and **B3**, every example ships a **packed** atlas (13–50 regions per
|
|
502
|
-
page) against rigc's one-part-per-page model, which `A06` enforced unconditionally. **B1 and B2 are
|
|
503
|
-
closed**; B3's validator half is (the packed-atlas clauses live behind `--profile`, above) and its
|
|
504
|
-
emitter half — no packer, no atlas importer — is not. Ordered gap list in Part 4 of that document;
|
|
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:
|
|
514
|
-
|
|
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
|
-
🎞️ **Authoring the movement that these two show you** — key poses, in-betweening,
|
|
535
|
-
and how to spread candidates so a ballot informs — is
|
|
536
|
-
[docs/MOTION.md](docs/MOTION.md).
|
|
537
|
-
|
|
538
|
-
### Letting someone choose — `rigc vote`
|
|
539
|
-
|
|
540
|
-
Sometimes looking is not enough on its own, because there is more than one
|
|
541
|
-
candidate and no instrument that can separate them: a pose fit with two local
|
|
542
|
-
optima that measure the same, a key density that is a matter of taste, a first
|
|
543
|
-
draft with no reference to compare against. `vote` is the deliberate human gate
|
|
544
|
-
for exactly that residue, and only for that residue.
|
|
545
|
-
|
|
546
|
-
```bash
|
|
547
|
-
rigc vote --candidate spine-a --candidate spine-b [--animation <name>] [--out ballot.html]
|
|
548
|
-
rigc vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl] [--again]
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
The first form writes one self-contained `ballot.html`: two to four compiled
|
|
552
|
-
candidates side by side, each in its own official player, looping, with one
|
|
553
|
-
button that restarts them together. The panes are labelled `A`, `B`, `C`, `D` and
|
|
554
|
-
show **no paths** — a voter who can see that `B` came out of `experiments/` is not
|
|
555
|
-
comparing pictures any more — so the path→label mapping lives in a manifest
|
|
556
|
-
embedded in the same file and is never rendered. A voter picks a winner or says
|
|
557
|
-
"tie / no preference", optionally writes a sentence, and copies or downloads a
|
|
558
|
-
small JSON result the page prints the filename for.
|
|
559
|
-
|
|
560
|
-
The second form checks that result against the ballot's own manifest and appends
|
|
561
|
-
it to an append-only JSONL ledger. Nothing is trusted: the result carries a
|
|
562
|
-
content **digest** per candidate, and a result whose digests are not this
|
|
563
|
-
ballot's, whose choice is not on it, or whose reason code contradicts its choice
|
|
564
|
-
is refused by a named rule (`V02_CANDIDATE_DIGESTS_ARE_THE_BALLOTS` and friends)
|
|
565
|
-
with nothing appended. A second vote on one ballot needs `--again`.
|
|
566
|
-
|
|
567
|
-
The loop it is built for, in one line: **the agent compiles N candidates that all
|
|
568
|
-
pass the gate → `rigc vote` writes the ballot → a human opens it, watches, and
|
|
569
|
-
votes → `rigc vote --record` checks the answer into `votes.jsonl` → the agent
|
|
570
|
-
reads the ledger and proceeds.** Compile first, vote last: a candidate reaches a
|
|
571
|
-
ballot only because it already validated green, so the human is never asked to
|
|
572
|
-
read JSON, a diff or a spec.
|
|
573
|
-
|
|
574
|
-
Three properties are worth stating because they are what make the ledger usable
|
|
575
|
-
by the next agent rather than by a reader:
|
|
576
|
-
|
|
577
|
-
- **A tie is a recorded outcome, not a missing one.** The ledger distinguishes a
|
|
578
|
-
ballot with a winner, a ballot the human called a tie, and a ballot nobody
|
|
579
|
-
opened. `both-unacceptable` is the tie that means *propose again*, and it is
|
|
580
|
-
unreachable if ties are not recordable.
|
|
581
|
-
- **The winner is a digest, not a label.** `B` means nothing outside one ballot;
|
|
582
|
-
the digest identifies the same pixels anywhere. Every line also carries its
|
|
583
|
-
`coverage` — which candidates the vote compared — so completeness is
|
|
584
|
-
computable rather than assumed.
|
|
585
|
-
- **Every line carries a reason code** from a closed enumeration, and the
|
|
586
|
-
enumeration is enforced: "tie, because this one is better" is refused.
|
|
587
|
-
|
|
588
|
-
Same player, same posture as `preview`: referenced from a CDN, never vendored,
|
|
589
|
-
and the file contains only your own art ([NOTICE.md](NOTICE.md)).
|
|
590
|
-
|
|
591
|
-
## Reading a pose you were given — `rigc pose`
|
|
592
|
-
|
|
593
|
-
```bash
|
|
594
|
-
rigc pose --images parts/ --frame poseA.png [--out pose.json]
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
Every command above takes something you authored and tells you about it. This one
|
|
598
|
-
runs the other way: it takes a **picture the user already has** — one key pose —
|
|
599
|
-
and reports where each loose part PNG sits in it, so an agent can write those
|
|
600
|
-
coordinates into a rig and a motion **by construction** and spend its effort on the
|
|
601
|
-
part no instrument can measure, the movement between two poses.
|
|
602
|
-
|
|
603
|
-
```
|
|
604
|
-
PLACE torso.png x= 44.4 y= 65.4 rot= 0.0° scale=1.118 residual=0.0770 unexplained= 19%
|
|
605
|
-
AMBIG arm.png x= 27.1 y= 56.3 rot= -35.2° scale=1.111 residual=0.0262 unexplained= 2%
|
|
606
|
-
alt 2: x= 61.5 y= 56.3 rot= 35.4° scale=1.116 residual=0.0279 unexplained= 2%
|
|
607
|
-
PLACE ball.png x= 44.3 y= 104.6 rot= 0.0° scale=1.144 residual=0.0203 unexplained= 3%
|
|
608
|
-
rotation is a FREE degree of freedom — the 0° above is a placeholder
|
|
609
|
-
REFUSE foreign.png no-match: the best placement found has residual 0.4245, above --max-residual 0.25
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
🚨 **Nothing here is a score, and no pass bar attaches to any of it.** `check` and
|
|
613
|
-
`bench` measure a build against a reference, so their numbers mean *how close*. A
|
|
614
|
-
pose frame is not a reference — it is a **given condition**, and once the spec
|
|
615
|
-
states those coordinates there is nothing left to be close to. The residual says
|
|
616
|
-
how far to trust a placement and where two answers are equally good, which is a
|
|
617
|
-
different job and needs the opposite defaults:
|
|
618
|
-
|
|
619
|
-
- a part that matches nowhere is **refused by name**, with its best guess still in
|
|
620
|
-
the JSON — a refusal tells you not to trust a number rather than hiding it;
|
|
621
|
-
- two near-equal optima are reported as **both**, flagged `ambiguous`, never
|
|
622
|
-
silently resolved. Two identical limbs look exactly like that;
|
|
623
|
-
- a part whose rotation genuinely does not matter — a ball — reports rotation as a
|
|
624
|
-
**free degree of freedom** rather than as a failure;
|
|
625
|
-
- a part the canvas cannot contain at any tested scale, and a part with no material
|
|
626
|
-
in it at all, each get their own named refusal.
|
|
627
|
-
|
|
628
|
-
⚠️ **Residuals degrade under occlusion and there is no depth solver here.** A part
|
|
629
|
-
drawn behind another has the occluder's pixels where its own should be, so its
|
|
630
|
-
residual rises at the *correct* placement; `unexplained` is the share of the part
|
|
631
|
-
that disagrees, and a middling residual beside a high `unexplained` usually means
|
|
632
|
-
*right place, seen through something else*. The output carries its own `caveats`
|
|
633
|
-
block saying so. Fields, coordinate contract and the rest of the limits:
|
|
634
|
-
**[AUTHORING.md §11](docs/AUTHORING.md)**.
|
|
635
|
-
|
|
636
|
-
## Run viewer — watching a *run* instead of reading it
|
|
637
|
-
|
|
638
|
-
🔎 **This is the ladder's instrument, not the way to look at your own rig** — that
|
|
639
|
-
is the section above. The viewer is reference-bound and repository-bound, and it
|
|
640
|
-
deliberately never ships.
|
|
641
|
-
|
|
642
|
-
`check.txt` says a candidate's worst frame is f0012 at 56 MAE. The viewer shows
|
|
643
|
-
you f0012.
|
|
644
|
-
|
|
645
|
-
```bash
|
|
646
|
-
bun run viewer # http://localhost:5173
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
Pick a run, a candidate and an animation. The left pane plays the candidate's
|
|
650
|
-
emitted `skeleton.json` — rendered by **[spine-html](https://github.com/firejune/spine-html)**,
|
|
651
|
-
plain DOM, one CSS matrix per slot — and the right pane shows the reference
|
|
652
|
-
frames for the same animation from `bench/reference/`, indexed by the scrubber's
|
|
653
|
-
time at the frame set's own fps. Both panes use the world box the run was
|
|
654
|
-
measured in (`bench.json`'s `check.viewport`, per frame set where the run framed
|
|
655
|
-
them separately), so the two pictures are comparable exactly as far as the
|
|
656
|
-
check's numbers say they are — and the pane label names which box that was.
|
|
657
|
-
Under them: `bench.json`'s section means and the framing plus per-animation
|
|
658
|
-
summary from `check.txt`.
|
|
659
|
-
|
|
660
|
-
It is also the smallest end-to-end proof the two modules have. rigc emits Spine
|
|
661
|
-
data; spine-html consumes Spine data; neither is checking its own work when the
|
|
662
|
-
skeleton one wrote comes up animating in the other.
|
|
663
|
-
|
|
664
|
-
Every run under `bench/runs/` is listed, including the ones that predate a
|
|
665
|
-
convention — those are greyed out with the reason (a missing atlas page usually
|
|
666
|
-
means `bun run fetch-examples` has not run) rather than dropped, because the
|
|
667
|
-
ladder's history is part of what the viewer is for.
|
|
668
|
-
|
|
669
|
-
🚫 **There is no build, and that is deliberate.** The viewer reads the working
|
|
670
|
-
tree: the runs, the reference frames, and `examples/` — which is Esoteric
|
|
671
|
-
Software's art, fetched rather than redistributed and non-commercial even then
|
|
672
|
-
(see [NOTICE.md](NOTICE.md)). A bundle would copy those pixels into a
|
|
673
|
-
distributable artifact. So there is one mode, `vite dev` on localhost, the dev
|
|
674
|
-
server serves nothing outside `bench/` and `examples/`, and `vite build` fails
|
|
675
|
-
on purpose. `viewer/` is not in `package.json`'s `files`, so it never ships
|
|
676
|
-
either; it is also outside the root `tsconfig.json` (it needs the DOM lib, which
|
|
677
|
-
the rest of the repository must not have) and is type-checked on its own with
|
|
678
|
-
`bunx tsc -p viewer --noEmit`. `bun run lint` covers it like everything else.
|
|
679
|
-
|
|
680
|
-
## What exists today
|
|
681
|
-
|
|
682
|
-
**Inputs — three files, one domain each.** Only the middle one is required.
|
|
683
|
-
|
|
684
|
-
- A **cut manifest** (`FaceManifest` in [`src/types.ts`](src/types.ts)) owns
|
|
685
|
-
**measured art**. Crop rectangle, the base plate, one entry per part with its
|
|
686
|
-
offset and size, mask polygons, the state machine, bone anchors, and — for a
|
|
687
|
-
joint cut — the entry point, the insertion axis (`deg` in screen degrees plus a
|
|
688
|
-
`unit` vector, cross-checked against each other), stroke amplitudes and any
|
|
689
|
-
measured ceilings. The compiler **never re-measures art**: every number here is
|
|
690
|
-
produced by a measuring tool or by the pipeline that cut the plates, and rigc
|
|
691
|
-
only reads it. **Optional** — a skeleton with no measured art behind it (any of
|
|
692
|
-
the benchmark examples) has none.
|
|
693
|
-
- A **rig spec** (`RigSpec` in [`src/rig.ts`](src/rig.ts), `spec: "rigc-rig/1"`)
|
|
694
|
-
owns **skeleton structure**: bones, slots, skins and their attachments, the 4.3
|
|
695
|
-
typed `constraints` array, and the invariants the emitted JSON cannot state
|
|
696
|
-
about itself. Its vocabulary is deliberately **Spine's own** — same concepts,
|
|
697
|
-
same field names, same defaults, cited to `SkeletonJson.ts` line numbers — so an
|
|
698
|
-
agent that has read Spine's documentation can author one without learning a
|
|
699
|
-
second vocabulary. rigc's additions sit on top and are namespaced: `from` on a
|
|
700
|
-
bone takes its position from the manifest instead of a literal that would drift
|
|
701
|
-
away from the art; `image` on an attachment names a PNG and rigc measures it;
|
|
702
|
-
`generator` on a mesh invokes a builder from `src/mesh.ts`; `invariants` carries
|
|
703
|
-
the axis bone, the forbidden parentage, the mesh budget.
|
|
704
|
-
- A **motion spec** (`MotionSpec`, `spec: "rigc-motion/1"`) owns **time**: the rig
|
|
705
|
-
it was authored against, named easing handles, setup overrides, a physics tuning
|
|
706
|
-
table, and the animations — each with a declared duration, a loop flag, its
|
|
707
|
-
tracks, and five timeline families that sit on the animation rather than in `tracks`:
|
|
708
|
-
`drawOrder` and `events`, which name no target at all, and `ik`, `transform`
|
|
709
|
-
and `deform`, whose keys carry named fields instead of one value (an IK mix and
|
|
710
|
-
softness, six transform mixes, a sparse run of vertex offsets) — which is also
|
|
711
|
-
where 4.3 writes each of them.
|
|
712
|
-
|
|
713
|
-
**Outputs — two files per cut**, written to the cut's `out` directory:
|
|
714
|
-
|
|
715
|
-
- `skeleton.json` — Spine **4.3** skeleton data. Bones, slots in draw order, one
|
|
716
|
-
skin, animations, and constraints in the 4.3 single `constraints` array.
|
|
717
|
-
- `skeleton.atlas` — a **one-part-per-page** atlas: every region covers its whole
|
|
718
|
-
page, `pma: false`. That convention is what makes the region/attachment/filename
|
|
719
|
-
join key checkable exactly rather than by convention.
|
|
720
|
-
|
|
721
|
-
**Where the three meet.** A manifest part joins a rig slot by its `rig_slot` field
|
|
722
|
-
(falling back to `slot`), and that slot's position in the rig's `slots` array **is**
|
|
723
|
-
the draw order — a manifest whose `draw_order` numbers disagree is a compile error
|
|
724
|
-
rather than a silent overrule. A slot filled by both a manifest part and a rig skin
|
|
725
|
-
is likewise refused, as is a setup pose declared in both the rig and the motion
|
|
726
|
-
spec: one fact, one author. A missing anchor is a compile error by design, so that
|
|
727
|
-
copying another cut's numbers is not the path of least resistance.
|
|
728
|
-
|
|
729
|
-
Two things are code and stay code, because neither is a table of numbers: the
|
|
730
|
-
**mesh generators** in [`src/mesh.ts`](src/mesh.ts), which encode a deformation
|
|
731
|
-
model (what is pinned, what may move, how authority falls off), and the
|
|
732
|
-
**coordinate contract** in [`src/transform.ts`](src/transform.ts).
|
|
733
|
-
|
|
734
|
-
### The validator
|
|
735
|
-
|
|
736
|
-
[`src/validate.ts`](src/validate.ts) parses the emitted artifacts with `spine-core`
|
|
737
|
-
and then runs 36 named assertions over the loaded skeleton. Each one exists because
|
|
738
|
-
the failure it catches is **silent**: the file loads, animates, and lies.
|
|
739
|
-
|
|
740
|
-
Assertions whose data is absent are reported as **SKIP**, never folded into the pass
|
|
741
|
-
count — an assertion with nothing to check has not checked anything.
|
|
742
|
-
|
|
743
|
-
#### Profiles — "wrong" versus "not how we do it here"
|
|
744
|
-
|
|
745
|
-
Not all 36 rules are about Spine. Some are about **spine-html**, the renderer this
|
|
746
|
-
compiler was built to feed, and about one project's frame budget; they fire on real,
|
|
747
|
-
correct, editor-produced Spine data, because the official example projects carry
|
|
748
|
-
clipping attachments, unweighted meshes, 116-triangle meshes and packed atlases —
|
|
749
|
-
all valid, none of them things spine-html will draw. Others are about **rigc's own
|
|
750
|
-
rigs** and mean nothing at all on a skeleton rigc did not compile — they read the
|
|
751
|
-
rig spec's `invariants` block, and they **SKIP** when it is absent rather than
|
|
752
|
-
counting as passes.
|
|
753
|
-
|
|
754
|
-
So `validate` and `build` take a `--profile`:
|
|
755
|
-
|
|
756
|
-
| Profile | Runs | For |
|
|
757
|
-
| --- | --- | --- |
|
|
758
|
-
| `spine` | the 22 validity rules | **the default.** Is this valid Spine 4.3 that any runtime plays correctly? |
|
|
759
|
-
| `spine-html` | all 36 | Opt-in. Is this a rig *this* project can ship? |
|
|
760
|
-
|
|
761
|
-
`spine` is the default because it is the question this package's output answers:
|
|
762
|
-
the artifact imports into the Spine editor and plays in any 4.3 runtime, and
|
|
763
|
-
that is what the 22 validity rules are about. The other 14 are somebody's policy
|
|
764
|
-
— one renderer's, one canvas budget's, one compiler's own formations' — and a
|
|
765
|
-
rig arriving from anywhere else has no stake in them. Ask for them with
|
|
766
|
-
`--profile spine-html` when you want them.
|
|
767
|
-
|
|
768
|
-
The **Profile** column below says which is which — `both` = validity, `renderer` and
|
|
769
|
-
`archetype` = `spine-html` only, and **`both ◑`** = a mixed assertion whose validity
|
|
770
|
-
half always runs while its policy clauses are gated (A06's `pma`/rotation/full-page
|
|
771
|
-
clauses, A08's "the two names must be identical", A20's "a mesh must be weighted at
|
|
772
|
-
all"). A report always names the profile it ran and lists what that profile left
|
|
773
|
-
out, on `PROF` lines: a `--profile spine` green means *valid Spine*, never *passes
|
|
774
|
-
the renderer policy*.
|
|
775
|
-
|
|
776
|
-
| Assertion | Profile | Holds that |
|
|
777
|
-
| --- | --- | --- |
|
|
778
|
-
| `A00_ROUNDTRIP_PARSE` | both | `spine-core` parses the skeleton and the atlas without throwing |
|
|
779
|
-
| `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS` | both | no 4.1/4.2-shaped `physics`/`ik`/… array — 4.3 folds them into one typed `constraints` array, and the old shape loads clean while the constraint vanishes |
|
|
780
|
-
| `A02_NO_BONE_TRANSFORM_KEY` | both | no bone uses 4.2's `transform`; 4.3 renamed it `inherit`, and the old key silently falls back to Normal inheritance |
|
|
781
|
-
| `A03_REGION_WIDTH_HEIGHT_FINITE` | both | every region attachment loaded a finite, positive width and height (a missing field loads as NaN, with no error) |
|
|
782
|
-
| `A04_MESH_TRIANGLES_AND_ENCODING` | both | triangles are a multiple of 3, indices are in range, and the vertex array's encoding agrees with the UV count |
|
|
783
|
-
| `A05_CURVE_ARRAY_LENGTH` | both | curve arrays carry 4 numbers per value channel and hold no non-finite value; timelines that cannot take a curve do not carry one. Covers all eleven 4.3 timeline groups — bone, slot, ik, transform, path, physics, slider, deform, drawOrder, drawOrderFolder, events |
|
|
784
|
-
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | each page's declared `size:` matches the PNG on disk, and its region covers the whole page |
|
|
785
|
-
| `A07_ATLAS_TEXT_SHAPE` | both | the atlas text obeys the parser's whitespace rules — no stray indentation on region names, no blank line splitting a page block |
|
|
786
|
-
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both ◑ | every attachment name resolves to a region of exactly that name |
|
|
787
|
-
| `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the compiled duration equals the duration the spec declared (skeleton JSON has no duration field — the last key *is* the duration). Two tolerances: a frame of slack for a duration declared long, but a key landing *past* the declared end is held to the grid the times are stored on, because nothing playing the animation ever reaches it. SKIPs without a motion spec |
|
|
788
|
-
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping every animation frame by frame produces no NaN anywhere in the pose |
|
|
789
|
-
| `A11_NO_CLIPPING_ATTACHMENTS` | renderer | no clipping attachments (the renderer skips them silently) |
|
|
790
|
-
| `A12_NO_DARK_COLOR` | renderer | no dark / two-colour tint on slots or timelines — parsed, then ignored |
|
|
791
|
-
| `A13_MESH_BUDGET` | renderer | no more mesh slots than the rig's `invariants.meshSlots`, and no mesh past its `invariants.meshTriangles`. SKIPs when the rig declares neither |
|
|
792
|
-
| `A14_NO_FULL_FRAME_MESH` | renderer | no mesh spans the whole stage (a full-frame mesh is a full-frame canvas that can never dirty-skip) |
|
|
793
|
-
| `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | `idle` keys no bone that drives a mesh, directly or as its control bone |
|
|
794
|
-
| `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` version label is on the 4.3 line (the parser never checks it) |
|
|
795
|
-
| `A17_ATLAS_PAGE_FILES_EXIST` | both | every page the atlas declares is a file on disk |
|
|
796
|
-
| `A18_DETERMINISTIC_EMIT` | both | a second, independent compile of the same inputs is byte-identical. SKIPs when re-gating artifacts already on disk |
|
|
797
|
-
| `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 |
|
|
798
|
-
| `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 |
|
|
799
|
-
| `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 |
|
|
800
|
-
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | every UV lies inside its region |
|
|
801
|
-
| `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | each physics constraint actually drives a component, is not muted by `mix: 0`, has non-zero mass, and has `damping < 1` so it settles |
|
|
802
|
-
| `A24_AXIS_SPACE_STROKE` | archetype | the stroke is authored in **axis space** — no screen-space Y component anywhere in the axis subtree, and no keys at all on the axis bone (its rotation is the one per-cut setup value) |
|
|
803
|
-
| `A25_DETACHED_BONE_PARENTAGE` | archetype | bones that must stay detached are not parented under a moving part |
|
|
804
|
-
| `A26_SLOT_DRAW_ORDER` | archetype | the slots array — which *is* the draw order — matches the rig spec's slot table |
|
|
805
|
-
| `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | each region's name equals its page's basename, closing the second link of the attachment → region → file chain |
|
|
806
|
-
| `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | both vertices of a ribbon row carry the same bones at the same weights, so the strip can lengthen and curve but never widen. **SKIPs on authored geometry** — rigc did not pair its rows |
|
|
807
|
-
| `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the stroke plus any inward keys stays within the cut's measured contact depth (skipped when the manifest declares none) |
|
|
808
|
-
| `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the stroke stays within the cut's measured containment ceiling, and nothing in the axis subtree scales — a scale key changes the contour the ceiling was measured on (skipped when the manifest declares none) |
|
|
809
|
-
| `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 |
|
|
810
|
-
| `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 |
|
|
811
|
-
| `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 |
|
|
812
|
-
| `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 |
|
|
813
|
-
| `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 |
|
|
814
|
-
|
|
815
|
-
## Usage
|
|
816
|
-
|
|
817
|
-
📘 **Writing a spec? Read [docs/AUTHORING.md](docs/AUTHORING.md) first.** It is the
|
|
818
|
-
guide an agent rigs from: both input files with a complete minimal example each,
|
|
819
|
-
every field with its Spine meaning, the rules that decide what is emitted, the
|
|
820
|
-
build → read the report → fix → repeat loop, the map from every named failure to
|
|
821
|
-
the file that has to change, and the list of format features rigc refuses by name
|
|
822
|
-
so you do not spend a loop discovering them. It travels **inside the npm package**
|
|
823
|
-
too, so an agent working from an install has it on disk at
|
|
824
|
-
`node_modules/spine-rigc/docs/AUTHORING.md`.
|
|
825
|
-
|
|
826
|
-
Compile by spelling out the paths. `--manifest` is optional; `--images <dir>` says
|
|
827
|
-
where a rig spec's `image` references live (it overrides the rig's own `images`
|
|
828
|
-
field):
|
|
829
|
-
|
|
830
|
-
```bash
|
|
831
|
-
bun cli.ts build \
|
|
832
|
-
--rig path/to/my_rig.rig.json \
|
|
833
|
-
--motion path/to/my.motion.json \
|
|
834
|
-
--out path/to/spine \
|
|
835
|
-
[--manifest path/to/manifest.json] [--images path/to/images]
|
|
836
|
-
```
|
|
837
|
-
|
|
838
|
-
By default, atlas page paths point back at the source art wherever it lives —
|
|
839
|
-
often outside `--out` — so add `--copy-images` when `spine/` itself needs to be
|
|
840
|
-
self-contained (zipped, committed, or handed off on its own): it copies every
|
|
841
|
-
referenced page PNG into `--out` and rewrites the atlas to match.
|
|
842
|
-
|
|
843
|
-
…or register cuts in a `cuts.json` and build them by name. Every path in the table
|
|
844
|
-
resolves **relative to the `cuts.json` file itself**, so the table lives with the
|
|
845
|
-
project that owns the art:
|
|
846
|
-
|
|
847
|
-
```json
|
|
848
|
-
{
|
|
849
|
-
"my_cut": {
|
|
850
|
-
"rig": "rigs/my_rig.rig.json",
|
|
851
|
-
"manifest": "output/my_cut/manifest.json",
|
|
852
|
-
"motion": "specs/my_cut.motion.json",
|
|
853
|
-
"out": "output/my_cut/spine"
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
```bash
|
|
859
|
-
bun cli.ts build --cut my_cut --cuts path/to/cuts.json
|
|
860
|
-
```
|
|
861
|
-
|
|
862
|
-
`build` compiles, then validates, and **writes only if the gate is green**. Other
|
|
863
|
-
commands:
|
|
864
|
-
|
|
865
|
-
```bash
|
|
866
|
-
bun cli.ts explain --cut my_cut --cuts path/to/cuts.json # the compiled rig as a table
|
|
867
|
-
bun cli.ts validate path/to/spine # re-gate artifacts already on disk
|
|
868
|
-
bun cli.ts validate --profile spine-html path/to/spine # …and this project's policy too (see Profiles)
|
|
869
|
-
bun cli.ts diff candidate.json reference.json # structural comparison
|
|
870
|
-
bun cli.ts check --candidate path/to/spine \
|
|
871
|
-
--frames bench/reference/3-timing-and-spacing # against pictures
|
|
872
|
-
bun cli.ts bench 3 --candidate path/to/spine # one rung of the ladder
|
|
873
|
-
bun cli.ts render --candidate path/to/spine # PNG frames + a contact sheet
|
|
874
|
-
bun cli.ts preview --candidate path/to/spine # one .html that plays it
|
|
875
|
-
bun cli.ts vote --candidate path/to/a --candidate path/to/b # one .html that asks which
|
|
876
|
-
bun cli.ts vote --record vote-<id>.json # check the answer into votes.jsonl
|
|
877
|
-
bun cli.ts pose --images path/to/parts --frame poseA.png # read a pose OUT of a picture
|
|
878
|
-
```
|
|
879
|
-
|
|
880
|
-
`validate` on a bare directory checks what it can see. Adding `--cut`/`--cuts` lets
|
|
881
|
-
it re-derive the declared durations and the structural expectations too, and the
|
|
882
|
-
report says which it had. `build` and `validate` both default to `--profile spine`,
|
|
883
|
-
the 22 validity rules; `--profile spine-html` adds this project's renderer and
|
|
884
|
-
archetype policy on top.
|
|
885
|
-
|
|
886
|
-
`render` and `preview` are the two that need no reference at all — see
|
|
887
|
-
[Looking at a rig](#looking-at-a-rig--rigc-render-and-rigc-preview). Run either
|
|
888
|
-
straight after a green `build`, on the same directory `--out` wrote. `vote` is the
|
|
889
|
-
same idea with more than one candidate in the page and an answer coming back —
|
|
890
|
-
see [Letting someone choose](#letting-someone-choose--rigc-vote). `pose` is the
|
|
891
|
-
one command that runs *before* a spec exists rather than after — see
|
|
892
|
-
[Reading a pose you were given](#reading-a-pose-you-were-given--rigc-pose).
|
|
893
|
-
|
|
894
|
-
## Checks
|
|
895
|
-
|
|
896
|
-
```bash
|
|
897
|
-
bun run typecheck # bunx tsc --noEmit over cli.ts, selftest.ts, src/, bench/, tools/, fixtures/
|
|
898
|
-
bun run lint # one rule: @typescript-eslint/no-explicit-any, as an error
|
|
899
|
-
bun run selftest # the validator's own negative controls (next section)
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
All three run on every push and pull request —
|
|
903
|
-
[`.github/workflows/ci.yml`](https://github.com/firejune/rigc/blob/main/.github/workflows/ci.yml). Bun runs the sources
|
|
904
|
-
directly, so the first two are not on the path of anything; they exist because a
|
|
905
|
-
convention nothing checks is a convention. `tsconfig.json` is
|
|
906
|
-
`strict: false` with `strictNullChecks: true` and says in place why the rest is
|
|
907
|
-
not on yet; `eslint.config.js` says why it carries exactly one rule.
|
|
908
|
-
|
|
909
|
-
## Selftest
|
|
910
|
-
|
|
911
|
-
```bash
|
|
912
|
-
bun run selftest # everything below; no arguments needed
|
|
913
|
-
bun run selftest --cuts path/to/cuts.json # …plus an extra suite over those cuts
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
A gate nobody has seen fail is not a gate. The selftest compiles a rig, breaks the
|
|
917
|
-
result one way at a time — 45 deliberate breaks, each modelled on a mistake that was
|
|
918
|
-
actually made or actually measured — and asserts that the **named** assertion fires
|
|
919
|
-
for each. Two further edits are *tolerance* controls the gate must let through,
|
|
920
|
-
because a widened assertion can fail by firing too often as easily as by firing too
|
|
921
|
-
rarely.
|
|
922
|
-
|
|
923
|
-
**The rigs it breaks are generated.** [`fixtures/public.ts`](https://github.com/firejune/rigc/blob/main/fixtures/public.ts)
|
|
924
|
-
writes three synthetic cuts into a temp directory on every run, and between them
|
|
925
|
-
they carry every structure the assertions have an opinion about — region
|
|
926
|
-
attachments, attachment swaps, rgba fades, a ring mesh on a control bone, a ribbon
|
|
927
|
-
on a bone chain, an axis bone whose subtree travels along it, a detached emitter,
|
|
928
|
-
physics constraints, and two measured ceilings. Every plate is a checkerboard with
|
|
929
|
-
`PLACEHOLDER` burned into it: they exist to be structurally real, and no claim
|
|
930
|
-
about appearance is made from any of them.
|
|
931
|
-
|
|
932
|
-
A fifth suite breaks an **input** instead of an artifact: nine malformed rig specs
|
|
933
|
-
that the compiler must refuse by name — a forward parent reference, a duplicate bone
|
|
934
|
-
name, a slot naming a missing bone, an ik target that does not exist, an attachment
|
|
935
|
-
image that is not on disk, an authored mesh binding a bone the rig does not have,
|
|
936
|
-
one that uses raw bone indices without asking for them, a wrong `spec` field, and a
|
|
937
|
-
constraint type the emitter cannot write. Each of those produces a file Spine's own
|
|
938
|
-
parser would accept while quietly meaning something else.
|
|
939
|
-
|
|
940
|
-
A **motion** spec can be wrong the same way, and the shape that costs the most is
|
|
941
|
-
the quietest: a key time that lands past the animation's declared duration is never
|
|
942
|
-
sampled, so the motion it was meant to carry simply does not happen. Five controls
|
|
943
|
-
hold that line — a key sitting exactly on a duration of 68/12 s is legal and must
|
|
944
|
-
compile, a key that 4 dp rounding pushed 0.000034 s past one is refused by name, the
|
|
945
|
-
same overshoot in an artifact the compiler never saw is caught by `A09`, an animation
|
|
946
|
-
whose last key is a frame short of its declared end is still accepted because that
|
|
947
|
-
direction is a different question, and a 32-second animation keyed exactly on its own
|
|
948
|
-
duration is *not* failed for the float32 grid its times come back on.
|
|
949
|
-
|
|
950
|
-
There is a positive control per suite as well: the pristine artifacts must come back
|
|
951
|
-
with zero failures, because a validator that failed everything would otherwise look
|
|
952
|
-
like a validator that worked.
|
|
953
|
-
|
|
954
|
-
`rigc check` gets the same treatment, and its pair is deliberately the same rig
|
|
955
|
-
twice: the rung 3 transcription against rung 3's frames, and then that transcription
|
|
956
|
-
with every key time reversed. Reversing leaves the structure untouched — same
|
|
957
|
-
timelines, same key count, same duration, and the gate stays green, which the
|
|
958
|
-
control asserts — and changes only what the shot looks like. Faithful reads 0.67 px
|
|
959
|
-
of slot drift; reversed reads 66.8 px. A third control makes the frames-only read
|
|
960
|
-
guard refuse a reference skeleton, because an honesty invariant nobody has seen
|
|
961
|
-
refuse anything is not an invariant.
|
|
962
|
-
|
|
963
|
-
The **mesh** path gets the same pair, on the rung 6 transcription against rung 6's
|
|
964
|
-
frames. Faithful reads a median 0.08 px of drift on the mesh-bearing slots; the
|
|
965
|
-
break is the one an authored mesh is actually exposed to — its weights bind bones
|
|
966
|
-
by *index*, so inserting a bone anywhere ahead of them rebinds every vertex in
|
|
967
|
-
silence — and it reads 31 px with the gate still green. Four further controls run
|
|
968
|
-
on a generated fixture and need no corpus: a ring mesh is posed at all, its pixels
|
|
969
|
-
reach the coverage mask `check` reads, an all-zero deform is the identity while a
|
|
970
|
-
real one moves the centroid, and two triangles sharing an edge draw it once.
|
|
971
|
-
|
|
972
|
-
Point the run at a `cuts.json` and an **extra suite** compiles every cut in it,
|
|
973
|
-
gates the result, and compiles it a second time for `A18`. That one is a positive
|
|
974
|
-
control on purpose: what real art adds is geometry a fixture cannot fake — measured
|
|
975
|
-
offsets, a measured axis, a measured ceiling, a mesh built over a contour nobody
|
|
976
|
-
drew by hand — so the question it asks is whether the whole gate still comes back
|
|
977
|
-
green on it. Without a cuts file it says it was skipped and the run passes on the
|
|
978
|
-
public suite alone; a cuts path that is *named and missing* exits 2.
|
|
979
|
-
|
|
980
|
-
Two suites measure against the Spine example corpus, which is downloaded rather
|
|
981
|
-
than redistributed. When `examples/` is absent they say so loudly and the summary
|
|
982
|
-
repeats it — an absent corpus is a hole in the run, not a pass — and a run in which
|
|
983
|
-
nothing substantive executed exits 2 rather than printing green.
|
|
984
|
-
|
|
985
|
-
## Layout
|
|
986
|
-
|
|
987
|
-
```
|
|
988
|
-
tsconfig.json type-check config (noEmit); eslint.config.js — the no-any gate
|
|
989
|
-
cli.ts build / validate / explain / diff / check / bench / render / preview / vote / pose
|
|
990
|
-
selftest.ts the validator's own negative controls, and diff's and check's
|
|
991
|
-
fixtures/ public.ts — the three synthetic cuts the selftest breaks
|
|
992
|
-
src/
|
|
993
|
-
compile.ts rig + motion spec (+ manifest) -> skeleton JSON + atlas text (pure data assembly)
|
|
994
|
-
rig.ts the rig spec — `spec: "rigc-rig/1"`, the skeleton as data
|
|
995
|
-
validate.ts spine-core round trip + the 36 assertions
|
|
996
|
-
diff.ts structural comparison of two skeletons, one ratio per measure
|
|
997
|
-
render.ts the rasteriser (regions + meshes), shared by the reference renderer,
|
|
998
|
-
`rigc render` and check
|
|
999
|
-
preview.ts the single-file HTML player page — the artifact embedded as data
|
|
1000
|
-
URIs, played by the official Spine Web Player (referenced, not vendored)
|
|
1001
|
-
ballot.ts the same page with 2–4 candidates in it and a vote coming back —
|
|
1002
|
-
candidate digests, the ballot manifest, and the refusals that
|
|
1003
|
-
stand between a saved vote and the ledger
|
|
1004
|
-
check.ts a candidate against rendered frames — pixels and per-slot drift,
|
|
1005
|
-
and it never opens the reference skeleton
|
|
1006
|
-
pose.ts the other direction: loose part PNGs against ONE pose frame, and
|
|
1007
|
-
where each part sits in it. An entry instrument — it reads a given
|
|
1008
|
-
condition into spec coordinates and grades nothing
|
|
1009
|
-
ladder.ts which example is which rung, and which file in it is the reference
|
|
1010
|
-
timelines.ts the 4.3 timeline catalogue and its walker (shared, pure JSON)
|
|
1011
|
-
mesh.ts ring and ribbon mesh builders, weighted-vertex encoding
|
|
1012
|
-
transform.ts crop pixels (y down) <-> Spine world (y up), world transforms
|
|
1013
|
-
png.ts PNG header reader (size, colour type, tRNS; no pixel decode)
|
|
1014
|
-
errors.ts CompileError, and NotImplementedError for what the format holds
|
|
1015
|
-
and the emitter does not write
|
|
1016
|
-
types.ts manifest, motion spec, and emitted-JSON shapes
|
|
1017
|
-
tools/ measurement and plate helpers (see below)
|
|
1018
|
-
scripts/ fetch-examples.sh
|
|
1019
|
-
bench/ count_features.ts — what the example corpus actually uses
|
|
1020
|
-
render_reference.ts — a rung's official export as PNG frames
|
|
1021
|
-
briefs/ — what an authoring agent is told about a rung
|
|
1022
|
-
reference/ — those frames, with the licence they travel under
|
|
1023
|
-
runs/ — one directory per attempt, and the run protocol
|
|
1024
|
-
transcriptions/ — rung specs transcribed from a reference export,
|
|
1025
|
-
which measure expressiveness and NOT authoring (see LADDER.md)
|
|
1026
|
-
viewer/ the run viewer — dev server only, no build (see above)
|
|
1027
|
-
vite.config.ts /api/inventory and /repo/<path>, and the build refusal
|
|
1028
|
-
inventory.ts what is under bench/runs, resolved to URLs
|
|
1029
|
-
main.ts the two panes, the transport, the report
|
|
1030
|
-
docs/ AUTHORING.md (how to author a rig), GATE.md (the clause statements
|
|
1031
|
-
a candidate is graded against), LADDER.md (live rung status),
|
|
1032
|
-
SPEC_COVERAGE.md (format survey),
|
|
1033
|
-
feature_matrix.{csv,json}
|
|
1034
|
-
.github/ workflows/ — ci.yml (the gates) and release.yml (release-please)
|
|
1035
|
-
CONTRIBUTING.md how to propose a change; RELEASING.md — how a version is cut
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
`tools/` are standalone utilities, each taking its paths as arguments:
|
|
1039
|
-
|
|
1040
|
-
| Tool | Does |
|
|
1041
|
-
| --- | --- |
|
|
1042
|
-
| `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 |
|
|
1043
|
-
| `contact.ts` | plate-vs-plate overlap measurement — the largest advance that keeps two footprints disjoint |
|
|
1044
|
-
| `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) |
|
|
1045
|
-
| `font5x7.ts` | bitmap labels for diagnostic images and generated plates |
|
|
450
|
+
The whole dossier — the yardstick, `diff` and `check` and what neither of them can
|
|
451
|
+
see, every rung, the run viewer, the 39 assertions and the selftest behind them — is
|
|
452
|
+
[docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
|
|
453
|
+
Live rung status is
|
|
454
|
+
[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
|
|
1046
455
|
|
|
1047
456
|
## Contributing
|
|
1048
457
|
|