spine-rigc 0.2.1
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/LICENSE +21 -0
- package/NOTICE.md +76 -0
- package/README.md +558 -0
- package/cli.ts +739 -0
- package/docs/AUTHORING.md +1303 -0
- package/docs/SPEC_COVERAGE.md +1109 -0
- package/package.json +65 -0
- package/src/check.ts +1714 -0
- package/src/compile.ts +1861 -0
- package/src/diff.ts +847 -0
- package/src/errors.ts +22 -0
- package/src/framing.ts +539 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +433 -0
- package/src/png.ts +50 -0
- package/src/render.ts +974 -0
- package/src/rig.ts +731 -0
- package/src/slots.ts +603 -0
- package/src/timelines.ts +253 -0
- package/src/transform.ts +130 -0
- package/src/types.ts +586 -0
- package/src/validate.ts +1586 -0
- package/tools/font5x7.ts +101 -0
- package/tools/plate.ts +286 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 firejune (Joon Kyoung)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## Spine Runtimes
|
|
4
|
+
|
|
5
|
+
This project depends on `@esotericsoftware/spine-core`, part of the
|
|
6
|
+
[Spine Runtimes](https://github.com/EsotericSoftware/spine-runtimes),
|
|
7
|
+
Copyright (c) 2013-2025 Esoteric Software LLC, licensed under the
|
|
8
|
+
[Spine Runtimes License Agreement](https://esotericsoftware.com/spine-runtimes-license).
|
|
9
|
+
|
|
10
|
+
Key obligation that propagates to users of this project: integration of the Spine
|
|
11
|
+
Runtimes into software (including via this compiler, which links spine-core to
|
|
12
|
+
validate what it emits) is permitted **provided that each user of the resulting
|
|
13
|
+
product obtains their own Spine Editor license**, and any redistribution includes
|
|
14
|
+
the Spine Runtimes license and copyright notice.
|
|
15
|
+
|
|
16
|
+
### What that means for rigc, as a chain of facts
|
|
17
|
+
|
|
18
|
+
rigc's own code is MIT (see `LICENSE`). The following is a restatement of Esoteric
|
|
19
|
+
Software's terms, not a licence term of this project:
|
|
20
|
+
|
|
21
|
+
1. rigc's output **is Spine skeleton data**.
|
|
22
|
+
2. Playing Spine skeleton data in a product requires **a Spine Runtime**.
|
|
23
|
+
3. The Spine Runtimes License requires **each user of such a product to own a Spine
|
|
24
|
+
Editor license**.
|
|
25
|
+
4. rigc **links `spine-core`** itself — validation is not optional and cannot be
|
|
26
|
+
switched off — so the same obligation applies to running rigc at all.
|
|
27
|
+
|
|
28
|
+
> **Using rigc, or shipping rigc's output in a product, requires a Spine Editor
|
|
29
|
+
> license.** rigc does not change that requirement in either direction: it neither
|
|
30
|
+
> creates one where none existed nor removes one that did. It is not a route around
|
|
31
|
+
> the editor licence.
|
|
32
|
+
|
|
33
|
+
## Example assets
|
|
34
|
+
|
|
35
|
+
The official Spine example projects are the yardstick this compiler is measured
|
|
36
|
+
against. They are owned by Esoteric Software and are **not redistributed** in this
|
|
37
|
+
repository: `scripts/fetch-examples.sh` downloads them into a gitignored
|
|
38
|
+
`examples/` directory for local evaluation.
|
|
39
|
+
|
|
40
|
+
Each example directory upstream carries its own `license.txt`, so the terms are
|
|
41
|
+
per-directory rather than repository-wide. Verified on 2026-08-22 against
|
|
42
|
+
`spine-runtimes` branch `4.3`:
|
|
43
|
+
|
|
44
|
+
| Example | `license.txt` | Copyright line |
|
|
45
|
+
| --------------------- | ------------- | ----------------------------------------------- |
|
|
46
|
+
| `1-weight-and-mass` | present | (c) 2021-2025, Esoteric Software LLC |
|
|
47
|
+
| `2-the-12-principles` | present | (c) 2021-2025, Esoteric Software LLC |
|
|
48
|
+
| `3-timing-and-spacing`| present | (c) 2021-2025, Esoteric Software |
|
|
49
|
+
| `4-wave-principle` | present | (c) 2021-2025, Esoteric Software LLC |
|
|
50
|
+
| `5-squash-and-stretch`| present | (c) 2021-2025, Esoteric Software |
|
|
51
|
+
| `6-arcs` | present | (c) 2022-2025, Esoteric Software |
|
|
52
|
+
| `7-anticipation` | **absent** | — |
|
|
53
|
+
| `8-follow-through` | present | (c) 2024-2025, Esoteric Software |
|
|
54
|
+
| `spineboy` | present | (c) 2013, Esoteric Software LLC |
|
|
55
|
+
|
|
56
|
+
Every `license.txt` above states the same two terms verbatim, differing only in
|
|
57
|
+
the copyright line:
|
|
58
|
+
|
|
59
|
+
> The images in this project may be redistributed as long as they are accompanied
|
|
60
|
+
> by this license file. The images may not be used for commercial use of any
|
|
61
|
+
> kind.
|
|
62
|
+
>
|
|
63
|
+
> The project file is released into the public domain. It may be used as the basis
|
|
64
|
+
> for derivative work.
|
|
65
|
+
|
|
66
|
+
So, for this repository's purposes:
|
|
67
|
+
|
|
68
|
+
- **Images** — redistributable only with the accompanying `license.txt`, and
|
|
69
|
+
**non-commercial only**. That is why they are fetched rather than committed.
|
|
70
|
+
- **Project files** (`.spine`, and the exports derived from them) — **public
|
|
71
|
+
domain**, usable as the basis for derivative work. This is what makes the
|
|
72
|
+
examples usable as a structural yardstick.
|
|
73
|
+
- ⚠️ **`7-anticipation` has no `license.txt` upstream**, so the redistribution
|
|
74
|
+
grant its siblings carry does not exist for it — there is no licence file to
|
|
75
|
+
accompany its images with. `scripts/fetch-examples.sh` prints a warning naming
|
|
76
|
+
it. Treat its images as not redistributable.
|
package/README.md
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/banner.svg" alt="rigc - Rig compiler for Spine" width="100%" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/spine-rigc"><img src="https://img.shields.io/npm/v/spine-rigc.svg?style=flat-square&color=FF6B4A" alt="npm version" /></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/spine-rigc"><img src="https://img.shields.io/npm/dm/spine-rigc.svg?style=flat-square&color=A855F7" alt="npm downloads" /></a>
|
|
8
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-38BDF8.svg?style=flat-square" alt="license" /></a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
**Rig compiler for Spine.** Declarative rig specs in, Spine 4.3 skeleton data out,
|
|
12
|
+
verified by a `spine-core` round-trip. Built so AI agents can author rigs and check
|
|
13
|
+
their own work.
|
|
14
|
+
|
|
15
|
+
## What rigc is, and what it is not
|
|
16
|
+
|
|
17
|
+
rigc emits **Spine's own skeleton data format**. That is the whole positioning, and
|
|
18
|
+
it cuts both ways:
|
|
19
|
+
|
|
20
|
+
- The output loads in any Spine runtime, and it **imports into the Spine editor**.
|
|
21
|
+
A compiled rig is a starting point on a timeline, not a finished shot — **an AI
|
|
22
|
+
drafts, a human refines in the editor**. rigc is complementary to the editor. It
|
|
23
|
+
is not a replacement for it, and it is not a way around one.
|
|
24
|
+
- rigc **links `@esotericsoftware/spine-core`** to validate what it emits — the
|
|
25
|
+
round-trip through the official parser is the only reason its output can be
|
|
26
|
+
trusted at all. So the [Spine Runtimes License Agreement](https://esotericsoftware.com/spine-runtimes-license)
|
|
27
|
+
applies to rigc exactly as it applies to any other runtime integration.
|
|
28
|
+
|
|
29
|
+
### Licensing, stated plainly
|
|
30
|
+
|
|
31
|
+
rigc's own code is MIT (see [LICENSE](LICENSE)). That says nothing about Spine, and
|
|
32
|
+
the following is a restatement of Esoteric Software's terms, not a term of ours:
|
|
33
|
+
|
|
34
|
+
1. rigc's output **is Spine skeleton data**.
|
|
35
|
+
2. Playing Spine skeleton data in a product requires **a Spine Runtime**.
|
|
36
|
+
3. The Spine Runtimes License requires **each user of such a product to own a Spine
|
|
37
|
+
editor licence**.
|
|
38
|
+
4. rigc **links `spine-core`** itself, so the same obligation covers running rigc.
|
|
39
|
+
|
|
40
|
+
> **Using rigc, or shipping rigc's output in a product, requires a Spine editor
|
|
41
|
+
> licence.** rigc does not change that requirement in either direction — it neither
|
|
42
|
+
> adds one nor removes one. If you were going to need an editor licence to ship a
|
|
43
|
+
> Spine animation, you still do; rigc is not a route around it.
|
|
44
|
+
|
|
45
|
+
See [NOTICE.md](NOTICE.md) for the full notice.
|
|
46
|
+
|
|
47
|
+
The problem rigc is aimed at is narrow. An agent asked to author a rig has no way
|
|
48
|
+
to tell whether it succeeded: Spine's JSON parser accepts a great deal of nonsense
|
|
49
|
+
without a murmur — a constraint in the 4.2 shape simply vanishes, a `size:` that
|
|
50
|
+
disagrees with the PNG collapses every UV, a four-number curve array yields NaN,
|
|
51
|
+
a mesh whose vertex count happens to equal its UV count silently loses its bone
|
|
52
|
+
weights. Every one of those loads clean, plays, and is wrong. rigc's answer is to
|
|
53
|
+
make the failure legible: compile from a spec, round-trip through the real parser,
|
|
54
|
+
run a list of named assertions, and **write nothing unless all of them are green.**
|
|
55
|
+
|
|
56
|
+
## The yardstick
|
|
57
|
+
|
|
58
|
+
The measure of whether this works is **Spine's own official example projects** —
|
|
59
|
+
the `1-weight-and-mass` … `8-follow-through` series as a difficulty ladder (one
|
|
60
|
+
animation principle per rig, in roughly ascending order), and **spineboy as the
|
|
61
|
+
graduation exam**. The question is structural and per-frame: given the same source
|
|
62
|
+
art and a spec, does a compiled rig match the official export in bone hierarchy,
|
|
63
|
+
timeline shape, mesh topology and posed vertex positions?
|
|
64
|
+
|
|
65
|
+
`scripts/fetch-examples.sh` downloads those projects into a gitignored `examples/`
|
|
66
|
+
directory (they are not redistributed here — see [NOTICE.md](NOTICE.md) for the
|
|
67
|
+
per-example licence terms).
|
|
68
|
+
|
|
69
|
+
### Comparing a rig against a reference — `rigc diff`
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bun cli.ts diff candidate.json reference.json [--json report.json]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`diff` reads two skeletons and reports a ratio per **measure**, grouped into six
|
|
76
|
+
sections — bones, slots, attachments, constraints, animations, events — and it
|
|
77
|
+
does **not** combine them into a score. A single "87% match" cannot tell a rig
|
|
78
|
+
with the right skeleton and the wrong timing apart from a rig with the right
|
|
79
|
+
timing and the wrong skeleton, and those are opposite diagnoses.
|
|
80
|
+
|
|
81
|
+
Three properties the measures are built to have:
|
|
82
|
+
|
|
83
|
+
- **`diff X X` is 1.000 on every measure.** A comparison tool that cannot
|
|
84
|
+
recognise identity is reporting noise, and noise looks like a small honest gap.
|
|
85
|
+
The selftest asserts it.
|
|
86
|
+
- **A difference moves as few measures as possible.** Reordering two slots moves
|
|
87
|
+
`slots.order` and nothing else — not the slot-to-bone bindings, not the setup
|
|
88
|
+
attachments — so the report says *where* a rig is wrong, not just *how much*.
|
|
89
|
+
Each selftest case names the exact set of measures its edit may disturb.
|
|
90
|
+
- **Name-agnostic figures sit beside name-matched ones.** A candidate that builds
|
|
91
|
+
the right tree under its own bone names scores 0 on `bones.parent_by_name` and
|
|
92
|
+
1.000 on `bones.depth_histogram` and `bones.degree_sequence`. Reporting only the
|
|
93
|
+
first calls a correct rig a total failure; reporting only the second calls any
|
|
94
|
+
14-bone tree a match. That holds at the section level too: `bones` and `slots`
|
|
95
|
+
are the two sections whose measures are mostly name-keyed, so each reports **two
|
|
96
|
+
figures**, and the pair is the finding —
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
bones 0.567 (name-matched) · 1.000 (name-agnostic)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
reads *"the tree is right and the vocabulary is different"*, which the single
|
|
103
|
+
mean on its own could not say. They are two comparisons with their own measure
|
|
104
|
+
sets rather than two halves of one; the name-matched figure is unchanged, so
|
|
105
|
+
older reports stay comparable. `sections[].nameAgnostic` in the JSON lists them.
|
|
106
|
+
|
|
107
|
+
An assertion or measure with nothing to compare reports its `total` as 0 and says
|
|
108
|
+
so, exactly as the validator's SKIP does — a vacuous 1.000 that looks earned is
|
|
109
|
+
the same false green in a different costume.
|
|
110
|
+
|
|
111
|
+
### Checking a rig against the pictures — `rigc check`
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
bun cli.ts check --candidate path/to/spine --frames bench/reference/3-timing-and-spacing
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
⭐ **Neither the gate nor `diff` can see a wrong animation.** The gate checks
|
|
118
|
+
validity: it parses the skeleton, steps every animation and refuses anything
|
|
119
|
+
degenerate, and it has no opinion about whether the animation is the one that was
|
|
120
|
+
asked for. `diff` checks structure: a reversed easing is the same timeline, the
|
|
121
|
+
same key count and the same curve kind. Three honest ladder runs have now produced
|
|
122
|
+
**zero** validator FAILs between them, and one of them shipped a build in which
|
|
123
|
+
every easing in the file was reversed — green, and sincerely reported as done.
|
|
124
|
+
|
|
125
|
+
`check` is the instrument for that. It renders the candidate with the same
|
|
126
|
+
rasteriser that drew the reference frames, onto the same pixel grid, and reports
|
|
127
|
+
per animation and per frame:
|
|
128
|
+
|
|
129
|
+
- **MAE over the union alpha** — the mean absolute RGB difference over the pixels
|
|
130
|
+
either side covers, 0..255. The whole-frame figure is printed beside it and never
|
|
131
|
+
instead of it: most of a frame is background on both sides, so that number is
|
|
132
|
+
small for every candidate and the gap between a good one and a bad one smaller
|
|
133
|
+
still.
|
|
134
|
+
- **The framing** — where the candidate's drawn pixels sit against the reference's,
|
|
135
|
+
as a scale, an offset and a residual. It is printed first because it is upstream
|
|
136
|
+
of everything else: get it wrong and the error arrives disguised as motion.
|
|
137
|
+
- **Per-frame change** — how many pixels each side moved since **its own** previous
|
|
138
|
+
frame, compared against each other. It is the only measure here that looks at the
|
|
139
|
+
relation between two frames rather than at one, and it is what catches a held pose
|
|
140
|
+
the candidate does not hold, or a one-frame event that never fired: both are cheap
|
|
141
|
+
in every individual frame and invisible to an aggregate.
|
|
142
|
+
- **Per-slot drift** — where each of the candidate's own slots landed against the
|
|
143
|
+
reference frame, in pixels. MAE says *how wrong*; a slot's drift says *which
|
|
144
|
+
part, which way, how far*. Where the reference merged two parts into one blob —
|
|
145
|
+
the trap [AUTHORING §8](docs/AUTHORING.md) opens with — the slot is
|
|
146
|
+
template-matched against its own rendered pixels instead, with a confidence; and
|
|
147
|
+
where nothing inside the distance that slot could plausibly have moved matches
|
|
148
|
+
it, the answer is **no match** rather than a number about some other part.
|
|
149
|
+
|
|
150
|
+
🔒 **It never reads the reference skeleton.** It opens the candidate and PNG
|
|
151
|
+
frames, and nothing else: every reference-side read goes through one guard that
|
|
152
|
+
refuses a path which is not a `.png` or the frame set's `frames.json`, and the
|
|
153
|
+
selftest makes that guard fire. That is what lets `check` sit *inside* an
|
|
154
|
+
authoring loop where `bench` cannot — running it as often as you like does not
|
|
155
|
+
stop a run being an authoring run.
|
|
156
|
+
|
|
157
|
+
The candidate is framed **by its own drawn pixels**, not by the reference's world
|
|
158
|
+
box. A candidate is authored in its own coordinate system and under the ladder's
|
|
159
|
+
honesty rule could not be authored in any other, so both sides are measured the
|
|
160
|
+
same way — the content box of what each actually draws — and one similarity
|
|
161
|
+
transform, fitted by least squares over every edge of every frame, carries the
|
|
162
|
+
candidate's onto the reference's. Two skeletons depicting the same shot land on the
|
|
163
|
+
same pixels whatever coordinates they were authored in, an invisible transparent
|
|
164
|
+
margin cannot move the result, and no single quad corner in a single frame can set
|
|
165
|
+
the scale for a run.
|
|
166
|
+
|
|
167
|
+
There is no pass mark, for the same reason `diff` has none.
|
|
168
|
+
|
|
169
|
+
### Benchmark ladder — the rungs, and where they stand
|
|
170
|
+
|
|
171
|
+
**[docs/LADDER.md](docs/LADDER.md) is the live ledger**: the rung order
|
|
172
|
+
(blockers → rung 3 first → 1 · 2 · 4 · 5 → 6 → 8 → 7 → spineboy), what each
|
|
173
|
+
rung gates on, how a rung is scored, the honesty rule that keeps the reference
|
|
174
|
+
export away from the authoring agent, and a status table. Run one with:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
bun cli.ts bench 3 --candidate path/to/candidate/spine
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`bench` validates the candidate under `--profile spine`, diffs it against that
|
|
181
|
+
rung's reference export, and prints both. It exits non-zero only when validation
|
|
182
|
+
fails: the diff has no threshold, because there is no rung score. Add
|
|
183
|
+
`--frames <dir>` and it folds in the `check` table below, so a ladder row carries
|
|
184
|
+
fidelity as well as structure.
|
|
185
|
+
|
|
186
|
+
#### What the rungs need
|
|
187
|
+
|
|
188
|
+
[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md) surveys the full Spine 4.3 export surface against what
|
|
189
|
+
rigc emits and against what the nine examples measurably use (`bun run bench:usage` regenerates the
|
|
190
|
+
counts). Three blockers sat *before* rung 1: **B1**, the bone tree was code in `archetype.ts` rather
|
|
191
|
+
than data, so no example could be expressed at all; **B2**, `A16`'s regex rejected the `"4.3.75-beta"`
|
|
192
|
+
that every example declares; and **B3**, every example ships a **packed** atlas (13–50 regions per
|
|
193
|
+
page) against rigc's one-part-per-page model, which `A06` enforced unconditionally. **B1 and B2 are
|
|
194
|
+
closed**; B3's validator half is (the packed-atlas clauses live behind `--profile`, above) and its
|
|
195
|
+
emitter half — no packer, no atlas importer — is not. Ordered gap list in Part 4 of that document;
|
|
196
|
+
live status, and B1's proof, in [docs/LADDER.md](docs/LADDER.md).
|
|
197
|
+
|
|
198
|
+
## What exists today
|
|
199
|
+
|
|
200
|
+
**Inputs — three files, one domain each.** Only the middle one is required.
|
|
201
|
+
|
|
202
|
+
- A **cut manifest** (`FaceManifest` in [`src/types.ts`](src/types.ts)) owns
|
|
203
|
+
**measured art**. Crop rectangle, the base plate, one entry per part with its
|
|
204
|
+
offset and size, mask polygons, the state machine, bone anchors, and — for a
|
|
205
|
+
joint cut — the entry point, the insertion axis (`deg` in screen degrees plus a
|
|
206
|
+
`unit` vector, cross-checked against each other), stroke amplitudes and any
|
|
207
|
+
measured ceilings. The compiler **never re-measures art**: every number here is
|
|
208
|
+
produced by a measuring tool or by the pipeline that cut the plates, and rigc
|
|
209
|
+
only reads it. **Optional** — a skeleton with no measured art behind it (any of
|
|
210
|
+
the benchmark examples) has none.
|
|
211
|
+
- A **rig spec** (`RigSpec` in [`src/rig.ts`](src/rig.ts), `spec: "rigc-rig/1"`)
|
|
212
|
+
owns **skeleton structure**: bones, slots, skins and their attachments, the 4.3
|
|
213
|
+
typed `constraints` array, and the invariants the emitted JSON cannot state
|
|
214
|
+
about itself. Its vocabulary is deliberately **Spine's own** — same concepts,
|
|
215
|
+
same field names, same defaults, cited to `SkeletonJson.ts` line numbers — so an
|
|
216
|
+
agent that has read Spine's documentation can author one without learning a
|
|
217
|
+
second vocabulary. rigc's additions sit on top and are namespaced: `from` on a
|
|
218
|
+
bone takes its position from the manifest instead of a literal that would drift
|
|
219
|
+
away from the art; `image` on an attachment names a PNG and rigc measures it;
|
|
220
|
+
`generator` on a mesh invokes a builder from `src/mesh.ts`; `invariants` carries
|
|
221
|
+
the axis bone, the forbidden parentage, the mesh budget.
|
|
222
|
+
- A **motion spec** (`MotionSpec`, `spec: "rigc-motion/1"`) owns **time**: the rig
|
|
223
|
+
it was authored against, named easing handles, setup overrides, a physics tuning
|
|
224
|
+
table, and the animations — each with a declared duration, a loop flag, its
|
|
225
|
+
tracks, and optionally a `drawOrder` timeline (the one timeline that names no
|
|
226
|
+
target, so it sits on the animation rather than in `tracks`).
|
|
227
|
+
|
|
228
|
+
**Outputs — two files per cut**, written to the cut's `out` directory:
|
|
229
|
+
|
|
230
|
+
- `skeleton.json` — Spine **4.3** skeleton data. Bones, slots in draw order, one
|
|
231
|
+
skin, animations, and constraints in the 4.3 single `constraints` array.
|
|
232
|
+
- `skeleton.atlas` — a **one-part-per-page** atlas: every region covers its whole
|
|
233
|
+
page, `pma: false`. That convention is what makes the region/attachment/filename
|
|
234
|
+
join key checkable exactly rather than by convention.
|
|
235
|
+
|
|
236
|
+
**Where the three meet.** A manifest part joins a rig slot by its `rig_slot` field
|
|
237
|
+
(falling back to `slot`), and that slot's position in the rig's `slots` array **is**
|
|
238
|
+
the draw order — a manifest whose `draw_order` numbers disagree is a compile error
|
|
239
|
+
rather than a silent overrule. A slot filled by both a manifest part and a rig skin
|
|
240
|
+
is likewise refused, as is a setup pose declared in both the rig and the motion
|
|
241
|
+
spec: one fact, one author. A missing anchor is a compile error by design, so that
|
|
242
|
+
copying another cut's numbers is not the path of least resistance.
|
|
243
|
+
|
|
244
|
+
Two things are code and stay code, because neither is a table of numbers: the
|
|
245
|
+
**mesh generators** in [`src/mesh.ts`](src/mesh.ts), which encode a deformation
|
|
246
|
+
model (what is pinned, what may move, how authority falls off), and the
|
|
247
|
+
**coordinate contract** in [`src/transform.ts`](src/transform.ts).
|
|
248
|
+
|
|
249
|
+
### The validator
|
|
250
|
+
|
|
251
|
+
[`src/validate.ts`](src/validate.ts) parses the emitted artifacts with `spine-core`
|
|
252
|
+
and then runs 32 named assertions over the loaded skeleton. Each one exists because
|
|
253
|
+
the failure it catches is **silent**: the file loads, animates, and lies.
|
|
254
|
+
|
|
255
|
+
Assertions whose data is absent are reported as **SKIP**, never folded into the pass
|
|
256
|
+
count — an assertion with nothing to check has not checked anything.
|
|
257
|
+
|
|
258
|
+
#### Profiles — "wrong" versus "not how we do it here"
|
|
259
|
+
|
|
260
|
+
Not all 32 rules are about Spine. Some are about **spine-html**, the renderer this
|
|
261
|
+
compiler was built to feed, and about one project's frame budget; they fire on real,
|
|
262
|
+
correct, editor-produced Spine data, because the official example projects carry
|
|
263
|
+
clipping attachments, unweighted meshes, 116-triangle meshes and packed atlases —
|
|
264
|
+
all valid, none of them things spine-html will draw. Others are about **rigc's own
|
|
265
|
+
rigs** and mean nothing at all on a skeleton rigc did not compile — they read the
|
|
266
|
+
rig spec's `invariants` block, and they **SKIP** when it is absent rather than
|
|
267
|
+
counting as passes.
|
|
268
|
+
|
|
269
|
+
So `validate` and `build` take a `--profile`:
|
|
270
|
+
|
|
271
|
+
| Profile | Runs | For |
|
|
272
|
+
| --- | --- | --- |
|
|
273
|
+
| `spine-html` | all 32 | **the default.** Is this a rig this project can ship? |
|
|
274
|
+
| `spine` | the 18 validity rules | Is this valid Spine 4.3 that any runtime plays correctly? |
|
|
275
|
+
|
|
276
|
+
The **Profile** column below says which is which — `both` = validity, `renderer` and
|
|
277
|
+
`archetype` = `spine-html` only, and **`both ◑`** = a mixed assertion whose validity
|
|
278
|
+
half always runs while its policy clauses are gated (A06's `pma`/rotation/full-page
|
|
279
|
+
clauses, A08's "the two names must be identical", A20's "a mesh must be weighted at
|
|
280
|
+
all"). A report always names the profile it ran and lists what that profile left
|
|
281
|
+
out, on `PROF` lines: a `--profile spine` green means *valid Spine*, never *passes
|
|
282
|
+
the renderer policy*.
|
|
283
|
+
|
|
284
|
+
| Assertion | Profile | Holds that |
|
|
285
|
+
| --- | --- | --- |
|
|
286
|
+
| `A00_ROUNDTRIP_PARSE` | both | `spine-core` parses the skeleton and the atlas without throwing |
|
|
287
|
+
| `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 |
|
|
288
|
+
| `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 |
|
|
289
|
+
| `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) |
|
|
290
|
+
| `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 |
|
|
291
|
+
| `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 |
|
|
292
|
+
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | each page's declared `size:` matches the PNG on disk, and its region covers the whole page |
|
|
293
|
+
| `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 |
|
|
294
|
+
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both ◑ | every attachment name resolves to a region of exactly that name |
|
|
295
|
+
| `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 |
|
|
296
|
+
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping every animation frame by frame produces no NaN anywhere in the pose |
|
|
297
|
+
| `A11_NO_CLIPPING_ATTACHMENTS` | renderer | no clipping attachments (the renderer skips them silently) |
|
|
298
|
+
| `A12_NO_DARK_COLOR` | renderer | no dark / two-colour tint on slots or timelines — parsed, then ignored |
|
|
299
|
+
| `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 |
|
|
300
|
+
| `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) |
|
|
301
|
+
| `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | `idle` keys no bone that drives a mesh, directly or as its control bone |
|
|
302
|
+
| `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` version label is on the 4.3 line (the parser never checks it) |
|
|
303
|
+
| `A17_ATLAS_PAGE_FILES_EXIST` | both | every page the atlas declares is a file on disk |
|
|
304
|
+
| `A18_DETERMINISTIC_EMIT` | both | a second, independent compile of the same inputs is byte-identical. SKIPs when re-gating artifacts already on disk |
|
|
305
|
+
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | every overlay page carries an alpha channel; only the base plate — identified structurally as the region covering the stage — may be opaque |
|
|
306
|
+
| `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 |
|
|
307
|
+
| `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 |
|
|
308
|
+
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | every UV lies inside its region |
|
|
309
|
+
| `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 |
|
|
310
|
+
| `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) |
|
|
311
|
+
| `A25_DETACHED_BONE_PARENTAGE` | archetype | bones that must stay detached are not parented under a moving part |
|
|
312
|
+
| `A26_SLOT_DRAW_ORDER` | archetype | the slots array — which *is* the draw order — matches the rig spec's slot table |
|
|
313
|
+
| `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 |
|
|
314
|
+
| `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 |
|
|
315
|
+
| `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) |
|
|
316
|
+
| `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) |
|
|
317
|
+
| `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 |
|
|
318
|
+
|
|
319
|
+
## Install
|
|
320
|
+
|
|
321
|
+
rigc runs on [Bun](https://bun.sh). The package ships its TypeScript sources and
|
|
322
|
+
Bun runs them, so there is no build step and no `dist/` that can drift from the
|
|
323
|
+
repository it was cut from.
|
|
324
|
+
|
|
325
|
+
**The npm package is `spine-rigc`; the command it installs is `rigc`.** npm
|
|
326
|
+
refuses the name `rigc` as too similar to packages that already exist, so the
|
|
327
|
+
project, this repository and the executable keep their name and only the
|
|
328
|
+
registry entry is spelled out.
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
bunx spine-rigc --help # run it without installing
|
|
332
|
+
bun add -g spine-rigc # or install the command
|
|
333
|
+
bun add -d spine-rigc # or pin it in a project
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`npx spine-rigc` works too, as long as Bun is on `PATH` — the executable is a
|
|
337
|
+
Bun script, and npm only writes the shim that calls it.
|
|
338
|
+
|
|
339
|
+
Installed, the command is `rigc`. The examples below spell it `bun cli.ts`
|
|
340
|
+
because they are written from a clone of this repository (`bun install`, then run
|
|
341
|
+
the CLI in place); the two are interchangeable — `rigc build …` is
|
|
342
|
+
`bun cli.ts build …`.
|
|
343
|
+
|
|
344
|
+
Two commands are repository workflows rather than package ones: `bench` and
|
|
345
|
+
`check` measure against Spine's official example projects and the reference
|
|
346
|
+
frames rendered from them, which are fetched rather than redistributed (see
|
|
347
|
+
[NOTICE.md](NOTICE.md)). They need a clone and `bun run fetch-examples`, and say
|
|
348
|
+
so by name when the corpus is absent.
|
|
349
|
+
|
|
350
|
+
## Usage
|
|
351
|
+
|
|
352
|
+
📘 **Writing a spec? Read [docs/AUTHORING.md](docs/AUTHORING.md) first.** It is the
|
|
353
|
+
guide an agent rigs from: both input files with a complete minimal example each,
|
|
354
|
+
every field with its Spine meaning, the rules that decide what is emitted, the
|
|
355
|
+
build → read the report → fix → repeat loop, the map from every named failure to
|
|
356
|
+
the file that has to change, and the list of format features rigc refuses by name
|
|
357
|
+
so you do not spend a loop discovering them. It travels **inside the npm package**
|
|
358
|
+
too, so an agent working from an install has it on disk at
|
|
359
|
+
`node_modules/spine-rigc/docs/AUTHORING.md`.
|
|
360
|
+
|
|
361
|
+
Compile by spelling out the paths. `--manifest` is optional; `--images <dir>` says
|
|
362
|
+
where a rig spec's `image` references live (it overrides the rig's own `images`
|
|
363
|
+
field):
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
bun cli.ts build \
|
|
367
|
+
--rig path/to/my_rig.rig.json \
|
|
368
|
+
--motion path/to/my.motion.json \
|
|
369
|
+
--out path/to/spine \
|
|
370
|
+
[--manifest path/to/manifest.json] [--images path/to/images]
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
…or register cuts in a `cuts.json` and build them by name. Every path in the table
|
|
374
|
+
resolves **relative to the `cuts.json` file itself**, so the table lives with the
|
|
375
|
+
project that owns the art:
|
|
376
|
+
|
|
377
|
+
```json
|
|
378
|
+
{
|
|
379
|
+
"my_cut": {
|
|
380
|
+
"rig": "rigs/my_rig.rig.json",
|
|
381
|
+
"manifest": "output/my_cut/manifest.json",
|
|
382
|
+
"motion": "specs/my_cut.motion.json",
|
|
383
|
+
"out": "output/my_cut/spine"
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
bun cli.ts build --cut my_cut --cuts path/to/cuts.json
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
`build` compiles, then validates, and **writes only if the gate is green**. Other
|
|
393
|
+
commands:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
bun cli.ts explain --cut my_cut --cuts path/to/cuts.json # the compiled rig as a table
|
|
397
|
+
bun cli.ts validate path/to/spine # re-gate artifacts already on disk
|
|
398
|
+
bun cli.ts validate --profile spine path/to/any/skeleton # spec rules only (see Profiles)
|
|
399
|
+
bun cli.ts diff candidate.json reference.json # structural comparison
|
|
400
|
+
bun cli.ts check --candidate path/to/spine \
|
|
401
|
+
--frames bench/reference/3-timing-and-spacing # against pictures
|
|
402
|
+
bun cli.ts bench 3 --candidate path/to/spine # one rung of the ladder
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`validate` on a bare directory checks what it can see. Adding `--cut`/`--cuts` lets
|
|
406
|
+
it re-derive the declared durations and the structural expectations too, and the
|
|
407
|
+
report says which it had. `build` and `validate` both take `--profile spine` to drop
|
|
408
|
+
the renderer and archetype policy; the default stays `spine-html`.
|
|
409
|
+
|
|
410
|
+
## Checks
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
bun run typecheck # bunx tsc --noEmit over cli.ts, selftest.ts, src/, bench/, tools/, fixtures/
|
|
414
|
+
bun run lint # one rule: @typescript-eslint/no-explicit-any, as an error
|
|
415
|
+
bun run selftest # the validator's own negative controls (next section)
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
All three run on every push and pull request —
|
|
419
|
+
[`.github/workflows/ci.yml`](.github/workflows/ci.yml). Bun runs the sources
|
|
420
|
+
directly, so the first two are not on the path of anything; they exist because a
|
|
421
|
+
convention nothing checks is a convention. `tsconfig.json` is
|
|
422
|
+
`strict: false` with `strictNullChecks: true` and says in place why the rest is
|
|
423
|
+
not on yet; `eslint.config.js` says why it carries exactly one rule.
|
|
424
|
+
|
|
425
|
+
## Selftest
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
bun run selftest # everything below; no arguments needed
|
|
429
|
+
bun run selftest --cuts path/to/cuts.json # …plus an extra suite over those cuts
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
A gate nobody has seen fail is not a gate. The selftest compiles a rig, breaks the
|
|
433
|
+
result one way at a time — 45 deliberate breaks, each modelled on a mistake that was
|
|
434
|
+
actually made or actually measured — and asserts that the **named** assertion fires
|
|
435
|
+
for each. Two further edits are *tolerance* controls the gate must let through,
|
|
436
|
+
because a widened assertion can fail by firing too often as easily as by firing too
|
|
437
|
+
rarely.
|
|
438
|
+
|
|
439
|
+
**The rigs it breaks are generated.** [`fixtures/public.ts`](fixtures/public.ts)
|
|
440
|
+
writes three synthetic cuts into a temp directory on every run, and between them
|
|
441
|
+
they carry every structure the assertions have an opinion about — region
|
|
442
|
+
attachments, attachment swaps, rgba fades, a ring mesh on a control bone, a ribbon
|
|
443
|
+
on a bone chain, an axis bone whose subtree travels along it, a detached emitter,
|
|
444
|
+
physics constraints, and two measured ceilings. Every plate is a checkerboard with
|
|
445
|
+
`PLACEHOLDER` burned into it: they exist to be structurally real, and no claim
|
|
446
|
+
about appearance is made from any of them.
|
|
447
|
+
|
|
448
|
+
A fifth suite breaks an **input** instead of an artifact: nine malformed rig specs
|
|
449
|
+
that the compiler must refuse by name — a forward parent reference, a duplicate bone
|
|
450
|
+
name, a slot naming a missing bone, an ik target that does not exist, an attachment
|
|
451
|
+
image that is not on disk, an authored mesh binding a bone the rig does not have,
|
|
452
|
+
one that uses raw bone indices without asking for them, a wrong `spec` field, and a
|
|
453
|
+
constraint type the emitter cannot write. Each of those produces a file Spine's own
|
|
454
|
+
parser would accept while quietly meaning something else.
|
|
455
|
+
|
|
456
|
+
A **motion** spec can be wrong the same way, and the shape that costs the most is
|
|
457
|
+
the quietest: a key time that lands past the animation's declared duration is never
|
|
458
|
+
sampled, so the motion it was meant to carry simply does not happen. Five controls
|
|
459
|
+
hold that line — a key sitting exactly on a duration of 68/12 s is legal and must
|
|
460
|
+
compile, a key that 4 dp rounding pushed 0.000034 s past one is refused by name, the
|
|
461
|
+
same overshoot in an artifact the compiler never saw is caught by `A09`, an animation
|
|
462
|
+
whose last key is a frame short of its declared end is still accepted because that
|
|
463
|
+
direction is a different question, and a 32-second animation keyed exactly on its own
|
|
464
|
+
duration is *not* failed for the float32 grid its times come back on.
|
|
465
|
+
|
|
466
|
+
There is a positive control per suite as well: the pristine artifacts must come back
|
|
467
|
+
with zero failures, because a validator that failed everything would otherwise look
|
|
468
|
+
like a validator that worked.
|
|
469
|
+
|
|
470
|
+
`rigc check` gets the same treatment, and its pair is deliberately the same rig
|
|
471
|
+
twice: the rung 3 transcription against rung 3's frames, and then that transcription
|
|
472
|
+
with every key time reversed. Reversing leaves the structure untouched — same
|
|
473
|
+
timelines, same key count, same duration, and the gate stays green, which the
|
|
474
|
+
control asserts — and changes only what the shot looks like. Faithful reads 0.67 px
|
|
475
|
+
of slot drift; reversed reads 66.8 px. A third control makes the frames-only read
|
|
476
|
+
guard refuse a reference skeleton, because an honesty invariant nobody has seen
|
|
477
|
+
refuse anything is not an invariant.
|
|
478
|
+
|
|
479
|
+
The **mesh** path gets the same pair, on the rung 6 transcription against rung 6's
|
|
480
|
+
frames. Faithful reads a median 0.08 px of drift on the mesh-bearing slots; the
|
|
481
|
+
break is the one an authored mesh is actually exposed to — its weights bind bones
|
|
482
|
+
by *index*, so inserting a bone anywhere ahead of them rebinds every vertex in
|
|
483
|
+
silence — and it reads 31 px with the gate still green. Four further controls run
|
|
484
|
+
on a generated fixture and need no corpus: a ring mesh is posed at all, its pixels
|
|
485
|
+
reach the coverage mask `check` reads, an all-zero deform is the identity while a
|
|
486
|
+
real one moves the centroid, and two triangles sharing an edge draw it once.
|
|
487
|
+
|
|
488
|
+
Point the run at a `cuts.json` and an **extra suite** compiles every cut in it,
|
|
489
|
+
gates the result, and compiles it a second time for `A18`. That one is a positive
|
|
490
|
+
control on purpose: what real art adds is geometry a fixture cannot fake — measured
|
|
491
|
+
offsets, a measured axis, a measured ceiling, a mesh built over a contour nobody
|
|
492
|
+
drew by hand — so the question it asks is whether the whole gate still comes back
|
|
493
|
+
green on it. Without a cuts file it says it was skipped and the run passes on the
|
|
494
|
+
public suite alone; a cuts path that is *named and missing* exits 2.
|
|
495
|
+
|
|
496
|
+
Two suites measure against the Spine example corpus, which is downloaded rather
|
|
497
|
+
than redistributed. When `examples/` is absent they say so loudly and the summary
|
|
498
|
+
repeats it — an absent corpus is a hole in the run, not a pass — and a run in which
|
|
499
|
+
nothing substantive executed exits 2 rather than printing green.
|
|
500
|
+
|
|
501
|
+
## Layout
|
|
502
|
+
|
|
503
|
+
```
|
|
504
|
+
tsconfig.json type-check config (noEmit); eslint.config.js — the no-any gate
|
|
505
|
+
cli.ts build / validate / explain / diff / check / bench
|
|
506
|
+
selftest.ts the validator's own negative controls, and diff's and check's
|
|
507
|
+
fixtures/ public.ts — the three synthetic cuts the selftest breaks
|
|
508
|
+
src/
|
|
509
|
+
compile.ts rig + motion spec (+ manifest) -> skeleton JSON + atlas text (pure data assembly)
|
|
510
|
+
rig.ts the rig spec — `spec: "rigc-rig/1"`, the skeleton as data
|
|
511
|
+
validate.ts spine-core round trip + the 32 assertions
|
|
512
|
+
diff.ts structural comparison of two skeletons, one ratio per measure
|
|
513
|
+
render.ts the rasteriser (regions + meshes), shared by the reference renderer and check
|
|
514
|
+
check.ts a candidate against rendered frames — pixels and per-slot drift,
|
|
515
|
+
and it never opens the reference skeleton
|
|
516
|
+
ladder.ts which example is which rung, and which file in it is the reference
|
|
517
|
+
timelines.ts the 4.3 timeline catalogue and its walker (shared, pure JSON)
|
|
518
|
+
mesh.ts ring and ribbon mesh builders, weighted-vertex encoding
|
|
519
|
+
transform.ts crop pixels (y down) <-> Spine world (y up), world transforms
|
|
520
|
+
png.ts PNG header reader (size and colour type, no decode)
|
|
521
|
+
errors.ts CompileError, and NotImplementedError for what the format holds
|
|
522
|
+
and the emitter does not write
|
|
523
|
+
types.ts manifest, motion spec, and emitted-JSON shapes
|
|
524
|
+
tools/ measurement and plate helpers (see below)
|
|
525
|
+
scripts/ fetch-examples.sh
|
|
526
|
+
bench/ count_features.ts — what the example corpus actually uses
|
|
527
|
+
render_reference.ts — a rung's official export as PNG frames
|
|
528
|
+
briefs/ — what an authoring agent is told about a rung
|
|
529
|
+
reference/ — those frames, with the licence they travel under
|
|
530
|
+
runs/ — one directory per attempt, and the run protocol
|
|
531
|
+
transcriptions/ — rung specs transcribed from a reference export,
|
|
532
|
+
which measure expressiveness and NOT authoring (see LADDER.md)
|
|
533
|
+
docs/ AUTHORING.md (how to author a rig), LADDER.md (live rung status),
|
|
534
|
+
SPEC_COVERAGE.md (format survey),
|
|
535
|
+
feature_matrix.{csv,json}
|
|
536
|
+
.github/ workflows/ — ci.yml (the gates) and release.yml (release-please)
|
|
537
|
+
CONTRIBUTING.md how to propose a change; RELEASING.md — how a version is cut
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
`tools/` are standalone utilities, each taking its paths as arguments:
|
|
541
|
+
|
|
542
|
+
| Tool | Does |
|
|
543
|
+
| --- | --- |
|
|
544
|
+
| `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 |
|
|
545
|
+
| `contact.ts` | plate-vs-plate overlap measurement — the largest advance that keeps two footprints disjoint |
|
|
546
|
+
| `plate.ts` / `png_probe.mjs` | minimal PNG read/write and decode |
|
|
547
|
+
| `font5x7.ts` | bitmap labels for diagnostic images and generated plates |
|
|
548
|
+
|
|
549
|
+
## Contributing
|
|
550
|
+
|
|
551
|
+
Issues are the ledger; see [CONTRIBUTING.md](CONTRIBUTING.md) for what a change
|
|
552
|
+
has to clear before it lands. Releases are cut by release-please —
|
|
553
|
+
[RELEASING.md](RELEASING.md).
|
|
554
|
+
|
|
555
|
+
## Licence
|
|
556
|
+
|
|
557
|
+
MIT — see [LICENSE](LICENSE). Third-party terms, including the Spine editor licence
|
|
558
|
+
requirement that this project inherits, are in [NOTICE.md](NOTICE.md).
|