spine-rigc 0.2.1 → 0.4.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 +258 -9
- package/cli.ts +57 -6
- package/docs/AUTHORING.md +1006 -50
- package/docs/SPEC_COVERAGE.md +21 -14
- package/package.json +5 -2
- package/src/chains.ts +170 -0
- package/src/check.ts +1555 -98
- package/src/compile.ts +325 -6
- package/src/framing.ts +280 -0
- package/src/ladder.ts +1 -1
- package/src/render.ts +43 -2
- package/src/rig.ts +169 -6
- package/src/slots.ts +102 -4
- package/src/timelines.ts +9 -5
- package/src/types.ts +80 -1
- package/src/validate.ts +192 -2
package/README.md
CHANGED
|
@@ -133,7 +133,26 @@ per animation and per frame:
|
|
|
133
133
|
still.
|
|
134
134
|
- **The framing** — where the candidate's drawn pixels sit against the reference's,
|
|
135
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.
|
|
136
|
+
of everything else: get it wrong and the error arrives disguised as motion. On a
|
|
137
|
+
skeleton root it is decided **per animation directory**: a set whose own pixels
|
|
138
|
+
land in the box `frames.json` records is measured there, exactly, and the rest
|
|
139
|
+
share one fitted framing. `--framing shared` measures every set in the shared one
|
|
140
|
+
— the whole-root behaviour before issue #100, and worth 15–25 MAE on a character.
|
|
141
|
+
A **fitted** framing then gets one last pass that searches whole-pixel offsets
|
|
142
|
+
(±2 px) for the lowest MAE and takes the best one, because a fit registers extent
|
|
143
|
+
and the best fit of two extents is not the best alignment of two pictures — a
|
|
144
|
+
constant pixel is worth up to 30 % of a set's figure (issue #146). The line says
|
|
145
|
+
what it moved and what that was worth, and it says so when the identity won as
|
|
146
|
+
well. A box that is **not** an estimate — `frames.json`'s own, or one you pinned —
|
|
147
|
+
is never moved: there the same search is reported as a finding, because a constant
|
|
148
|
+
pixel inside the right box is the candidate's own figure sitting off, not framing.
|
|
149
|
+
- **The whole shot, against the contact sheet** — a set that ships a couple of
|
|
150
|
+
stills and folds every sampled frame into one `contact.png` (rung 2's do,
|
|
151
|
+
spineboy's `@30fps` sets do) used to be compared on the stills alone, honestly
|
|
152
|
+
reported and empty behind: nothing at all was measured about the frames in
|
|
153
|
+
between. `check` now samples the candidate at the set's own rate and compares it
|
|
154
|
+
against the sheet's own tiles, whose grid it measures off the sheet (issue #36).
|
|
155
|
+
MAE only, and a sheet that is not a grid of those frames is refused by name.
|
|
137
156
|
- **Per-frame change** — how many pixels each side moved since **its own** previous
|
|
138
157
|
frame, compared against each other. It is the only measure here that looks at the
|
|
139
158
|
relation between two frames rather than at one, and it is what catches a held pose
|
|
@@ -142,10 +161,18 @@ per animation and per frame:
|
|
|
142
161
|
- **Per-slot drift** — where each of the candidate's own slots landed against the
|
|
143
162
|
reference frame, in pixels. MAE says *how wrong*; a slot's drift says *which
|
|
144
163
|
part, which way, how far*. Where the reference merged two parts into one blob —
|
|
145
|
-
the trap [AUTHORING §8](docs/AUTHORING.md) opens with
|
|
164
|
+
the trap [AUTHORING §8](docs/AUTHORING.md) opens with, and it counts as merged
|
|
165
|
+
even when one part is most of the blob (issue #37) — the slot is
|
|
146
166
|
template-matched against its own rendered pixels instead, with a confidence; and
|
|
147
167
|
where nothing inside the distance that slot could plausibly have moved matches
|
|
148
168
|
it, the answer is **no match** rather than a number about some other part.
|
|
169
|
+
- **Per-chain attribution** — the same two, rolled up onto the unit an author
|
|
170
|
+
repairs. `check` cuts the **candidate's own** bone tree into chains at its branch
|
|
171
|
+
points and prints, per chain per set, the worst slot drift with its slot and
|
|
172
|
+
frame, the mean, the error per pixel inside it, and its share of the set's error
|
|
173
|
+
over the reference's own drawn pixels — plus one rollup line per chain across
|
|
174
|
+
every set. A figure with a dozen joints otherwise collapses to one number a shot,
|
|
175
|
+
and *"motion ✗"* over sixteen shots does not say which limb to re-key.
|
|
149
176
|
|
|
150
177
|
🔒 **It never reads the reference skeleton.** It opens the candidate and PNG
|
|
151
178
|
frames, and nothing else: every reference-side read goes through one guard that
|
|
@@ -164,14 +191,19 @@ same pixels whatever coordinates they were authored in, an invisible transparent
|
|
|
164
191
|
margin cannot move the result, and no single quad corner in a single frame can set
|
|
165
192
|
the scale for a run.
|
|
166
193
|
|
|
167
|
-
There is no pass mark
|
|
194
|
+
There is no pass mark **in the tool**, for the same reason `diff` has none. The
|
|
195
|
+
ladder's pass definition and its thresholds are a document read by a person over
|
|
196
|
+
the whole table — [docs/LADDER.md](docs/LADDER.md)'s *Operating rules* — and not an
|
|
197
|
+
exit code either command could produce.
|
|
168
198
|
|
|
169
199
|
### Benchmark ladder — the rungs, and where they stand
|
|
170
200
|
|
|
171
201
|
**[docs/LADDER.md](docs/LADDER.md) is the live ledger**: the rung order
|
|
172
202
|
(blockers → rung 3 first → 1 · 2 · 4 · 5 → 6 → 8 → 7 → spineboy), what each
|
|
173
203
|
rung gates on, how a rung is scored, the honesty rule that keeps the reference
|
|
174
|
-
export away from the authoring agent,
|
|
204
|
+
export away from the authoring agent, the operating rules — what a pass is, and
|
|
205
|
+
the numbered thresholds of the current gate (**gate v2**) that decide one — and a status table. Run
|
|
206
|
+
one with:
|
|
175
207
|
|
|
176
208
|
```bash
|
|
177
209
|
bun cli.ts bench 3 --candidate path/to/candidate/spine
|
|
@@ -195,6 +227,46 @@ closed**; B3's validator half is (the packed-atlas clauses live behind `--profil
|
|
|
195
227
|
emitter half — no packer, no atlas importer — is not. Ordered gap list in Part 4 of that document;
|
|
196
228
|
live status, and B1's proof, in [docs/LADDER.md](docs/LADDER.md).
|
|
197
229
|
|
|
230
|
+
## Run viewer — watching a run instead of reading it
|
|
231
|
+
|
|
232
|
+
`check.txt` says a candidate's worst frame is f0012 at 56 MAE. The viewer shows
|
|
233
|
+
you f0012.
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
bun run viewer # http://localhost:5173
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Pick a run, a candidate and an animation. The left pane plays the candidate's
|
|
240
|
+
emitted `skeleton.json` — rendered by **[spine-html](https://github.com/firejune/spine-html)**,
|
|
241
|
+
plain DOM, one CSS matrix per slot — and the right pane shows the reference
|
|
242
|
+
frames for the same animation from `bench/reference/`, indexed by the scrubber's
|
|
243
|
+
time at the frame set's own fps. Both panes use the world box the run was
|
|
244
|
+
measured in (`bench.json`'s `check.viewport`, per frame set where the run framed
|
|
245
|
+
them separately), so the two pictures are comparable exactly as far as the
|
|
246
|
+
check's numbers say they are — and the pane label names which box that was.
|
|
247
|
+
Under them: `bench.json`'s section means and the framing plus per-animation
|
|
248
|
+
summary from `check.txt`.
|
|
249
|
+
|
|
250
|
+
It is also the smallest end-to-end proof the two modules have. rigc emits Spine
|
|
251
|
+
data; spine-html consumes Spine data; neither is checking its own work when the
|
|
252
|
+
skeleton one wrote comes up animating in the other.
|
|
253
|
+
|
|
254
|
+
Every run under `bench/runs/` is listed, including the ones that predate a
|
|
255
|
+
convention — those are greyed out with the reason (a missing atlas page usually
|
|
256
|
+
means `bun run fetch-examples` has not run) rather than dropped, because the
|
|
257
|
+
ladder's history is part of what the viewer is for.
|
|
258
|
+
|
|
259
|
+
🚫 **There is no build, and that is deliberate.** The viewer reads the working
|
|
260
|
+
tree: the runs, the reference frames, and `examples/` — which is Esoteric
|
|
261
|
+
Software's art, fetched rather than redistributed and non-commercial even then
|
|
262
|
+
(see [NOTICE.md](NOTICE.md)). A bundle would copy those pixels into a
|
|
263
|
+
distributable artifact. So there is one mode, `vite dev` on localhost, the dev
|
|
264
|
+
server serves nothing outside `bench/` and `examples/`, and `vite build` fails
|
|
265
|
+
on purpose. `viewer/` is not in `package.json`'s `files`, so it never ships
|
|
266
|
+
either; it is also outside the root `tsconfig.json` (it needs the DOM lib, which
|
|
267
|
+
the rest of the repository must not have) and is type-checked on its own with
|
|
268
|
+
`bunx tsc -p viewer --noEmit`. `bun run lint` covers it like everything else.
|
|
269
|
+
|
|
198
270
|
## What exists today
|
|
199
271
|
|
|
200
272
|
**Inputs — three files, one domain each.** Only the middle one is required.
|
|
@@ -249,7 +321,7 @@ model (what is pinned, what may move, how authority falls off), and the
|
|
|
249
321
|
### The validator
|
|
250
322
|
|
|
251
323
|
[`src/validate.ts`](src/validate.ts) parses the emitted artifacts with `spine-core`
|
|
252
|
-
and then runs
|
|
324
|
+
and then runs 34 named assertions over the loaded skeleton. Each one exists because
|
|
253
325
|
the failure it catches is **silent**: the file loads, animates, and lies.
|
|
254
326
|
|
|
255
327
|
Assertions whose data is absent are reported as **SKIP**, never folded into the pass
|
|
@@ -257,7 +329,7 @@ count — an assertion with nothing to check has not checked anything.
|
|
|
257
329
|
|
|
258
330
|
#### Profiles — "wrong" versus "not how we do it here"
|
|
259
331
|
|
|
260
|
-
Not all
|
|
332
|
+
Not all 34 rules are about Spine. Some are about **spine-html**, the renderer this
|
|
261
333
|
compiler was built to feed, and about one project's frame budget; they fire on real,
|
|
262
334
|
correct, editor-produced Spine data, because the official example projects carry
|
|
263
335
|
clipping attachments, unweighted meshes, 116-triangle meshes and packed atlases —
|
|
@@ -270,8 +342,8 @@ So `validate` and `build` take a `--profile`:
|
|
|
270
342
|
|
|
271
343
|
| Profile | Runs | For |
|
|
272
344
|
| --- | --- | --- |
|
|
273
|
-
| `spine-html` | all
|
|
274
|
-
| `spine` | the
|
|
345
|
+
| `spine-html` | all 34 | **the default.** Is this a rig this project can ship? |
|
|
346
|
+
| `spine` | the 20 validity rules | Is this valid Spine 4.3 that any runtime plays correctly? |
|
|
275
347
|
|
|
276
348
|
The **Profile** column below says which is which — `both` = validity, `renderer` and
|
|
277
349
|
`archetype` = `spine-html` only, and **`both ◑`** = a mixed assertion whose validity
|
|
@@ -315,6 +387,8 @@ the renderer policy*.
|
|
|
315
387
|
| `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
388
|
| `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
389
|
| `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 |
|
|
390
|
+
| `A32_EVENT_KEYS_RESOLVE` | both | every event key fires an event the skeleton declares, no key sits earlier in time than the one before it, and `volume`/`balance` appear only on an event with an `audio` path. Only the first of those is loud in the parser; the other two load clean and drop the firing or the value in silence. SKIPs when no animation carries an event timeline |
|
|
391
|
+
| `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 |
|
|
318
392
|
|
|
319
393
|
## Install
|
|
320
394
|
|
|
@@ -347,6 +421,177 @@ frames rendered from them, which are fetched rather than redistributed (see
|
|
|
347
421
|
[NOTICE.md](NOTICE.md)). They need a clone and `bun run fetch-examples`, and say
|
|
348
422
|
so by name when the corpus is absent.
|
|
349
423
|
|
|
424
|
+
## First rig in ten minutes
|
|
425
|
+
|
|
426
|
+
A whole rig, end to end, in a scratch directory: three tiny plates, two JSON
|
|
427
|
+
files, one `build`, one `validate`. No clone, no art pipeline, nothing fetched.
|
|
428
|
+
|
|
429
|
+
🚫 **Every value below is invented for this section** — a doll that exists
|
|
430
|
+
nowhere else in this repository. That is [AUTHORING.md](docs/AUTHORING.md) §3's
|
|
431
|
+
rule applied here: no example value in these documents is copied out of a
|
|
432
|
+
reference export, so nothing you read in a quickstart is an answer to anything
|
|
433
|
+
[the ladder](docs/LADDER.md) measures.
|
|
434
|
+
|
|
435
|
+
**1. Install the command.**
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
bun add -g spine-rigc # installs `rigc`
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Or skip the install and prefix every command below with `bunx `, e.g.
|
|
442
|
+
`bunx spine-rigc build …`.
|
|
443
|
+
|
|
444
|
+
**2. Make a directory and three plates.** rigc measures PNGs rather than trusting
|
|
445
|
+
a number you typed (R5), so the art has to exist. These three are solid colours a
|
|
446
|
+
few dozen pixels across — a hull, a mast and a lamp:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
mkdir -p buoy/images && cd buoy
|
|
450
|
+
bun -e '
|
|
451
|
+
const parts = {
|
|
452
|
+
"images/hull.png": "iVBORw0KGgoAAAANSUhEUgAAADgAAAAMCAYAAAA3bX6lAAAAKElEQVR42mOI8bL6P5wxw6gHRz046sFRD456cNSDox4c9eCoBwcrBgDSZ+mdl2OiDgAAAABJRU5ErkJggg==",
|
|
453
|
+
"images/mast.png": "iVBORw0KGgoAAAANSUhEUgAAAAgAAAA0CAYAAAC3t3ldAAAAH0lEQVR42mO4dunIf3yYYVTBqIJRBaMKRhWMKhgcCgBGJo4s9YnopgAAAABJRU5ErkJggg==",
|
|
454
|
+
"images/lamp.png": "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAHElEQVR42mP4v8HhPzUww6hBowaNGjRq0HAzCADvdrVmFPbc+QAAAABJRU5ErkJggg=="
|
|
455
|
+
};
|
|
456
|
+
for (const [p, b] of Object.entries(parts)) await Bun.write(p, Buffer.from(b, "base64"));
|
|
457
|
+
'
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**3. The rig spec — `buoy.rig.json`.** Structure only: bones, the slots array in
|
|
461
|
+
draw order, and one skin mapping each slot to a plate.
|
|
462
|
+
|
|
463
|
+
```json
|
|
464
|
+
{
|
|
465
|
+
"spec": "rigc-rig/1",
|
|
466
|
+
"name": "buoy",
|
|
467
|
+
"images": "images",
|
|
468
|
+
"skeleton": { "width": 200, "height": 200 },
|
|
469
|
+
"bones": [
|
|
470
|
+
{ "name": "root" },
|
|
471
|
+
{ "name": "hull", "parent": "root", "x": 0, "y": 0 },
|
|
472
|
+
{ "name": "mast", "parent": "hull", "x": 0, "y": 4 },
|
|
473
|
+
{ "name": "lamp", "parent": "mast", "x": 0, "y": 52 }
|
|
474
|
+
],
|
|
475
|
+
"slots": [
|
|
476
|
+
{ "name": "mast", "bone": "mast", "attachment": "mast" },
|
|
477
|
+
{ "name": "hull", "bone": "hull", "attachment": "hull" },
|
|
478
|
+
{ "name": "lamp", "bone": "lamp", "attachment": "lamp" }
|
|
479
|
+
],
|
|
480
|
+
"skins": {
|
|
481
|
+
"default": {
|
|
482
|
+
"mast": { "mast": { "image": "mast.png", "y": 26 } },
|
|
483
|
+
"hull": { "hull": { "image": "hull.png" } },
|
|
484
|
+
"lamp": { "lamp": { "image": "lamp.png" } }
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Three things in there are worth naming, because each is a rule rather than a
|
|
491
|
+
style: the **slots array is the setup draw order** (R4) — index 0 is furthest
|
|
492
|
+
back, so the mast is behind the hull; the attachment carries an **`image`
|
|
493
|
+
instead of a `width`/`height`** (R5), which is what makes the size in the
|
|
494
|
+
skeleton and the size in the atlas incapable of drifting apart; and the mast's
|
|
495
|
+
`"y": 26` offsets the plate *within* its slot so the bone sits at the mast's foot
|
|
496
|
+
rather than its middle.
|
|
497
|
+
|
|
498
|
+
**4. The motion spec — `buoy.motion.json`.** Time only, aimed at the rig by name:
|
|
499
|
+
|
|
500
|
+
```json
|
|
501
|
+
{
|
|
502
|
+
"spec": "rigc-motion/1",
|
|
503
|
+
"archetype": "buoy",
|
|
504
|
+
"cut": "buoy",
|
|
505
|
+
"easings": { "swing": [0.42, 0, 0.58, 1] },
|
|
506
|
+
"animations": {
|
|
507
|
+
"bob": {
|
|
508
|
+
"duration": 2,
|
|
509
|
+
"loop": true,
|
|
510
|
+
"tracks": [
|
|
511
|
+
{
|
|
512
|
+
"bone": "hull",
|
|
513
|
+
"property": "translatey",
|
|
514
|
+
"keys": [
|
|
515
|
+
{ "t": 0, "v": [0], "ease": "swing" },
|
|
516
|
+
{ "t": 0.5, "v": [5], "ease": "swing" },
|
|
517
|
+
{ "t": 1.5, "v": [-5], "ease": "swing" },
|
|
518
|
+
{ "t": 2, "v": [0] }
|
|
519
|
+
]
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
"bone": "mast",
|
|
523
|
+
"property": "rotate",
|
|
524
|
+
"keys": [
|
|
525
|
+
{ "t": 0, "v": [-6], "ease": "swing" },
|
|
526
|
+
{ "t": 1, "v": [6], "ease": "swing" },
|
|
527
|
+
{ "t": 2, "v": [-6] }
|
|
528
|
+
]
|
|
529
|
+
}
|
|
530
|
+
]
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`archetype` must equal the rig's `name`. `duration` is declared and then checked
|
|
537
|
+
against what actually compiled (R7). The **last key of each track carries no
|
|
538
|
+
easing** — there is nothing after it to ease towards, and saying otherwise is a
|
|
539
|
+
compile error.
|
|
540
|
+
|
|
541
|
+
**5. Build, then re-gate what it wrote.**
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
rigc build --rig buoy.rig.json --motion buoy.motion.json --images images --out spine
|
|
545
|
+
rigc validate spine
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
`build` prints every assertion by name, then the shape of what it emitted, then
|
|
549
|
+
the two files:
|
|
550
|
+
|
|
551
|
+
```
|
|
552
|
+
.. pages=3 regions=3 bones=4 slots=3 animations=1 version=4.3.13 regionAttachments=3 meshAttachments=0 physicsConstraints=0 rig=buoy profile=spine-html
|
|
553
|
+
rigc: wrote …/buoy/spine/skeleton.json
|
|
554
|
+
rigc: wrote …/buoy/spine/skeleton.atlas
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
and `validate` re-reads those artifacts from disk and ends `rigc: green`. That is
|
|
558
|
+
a rig. `spine/skeleton.json` is Spine 4.3 skeleton data — it loads in a Spine
|
|
559
|
+
runtime and it imports into the Spine editor.
|
|
560
|
+
|
|
561
|
+
**Try breaking it**, because the validator's messages are the interface here and
|
|
562
|
+
they are worth meeting once on purpose. Rename `images/hull.png` to
|
|
563
|
+
`images/raft.png`, point the spec's `image` at the new name, and build again:
|
|
564
|
+
|
|
565
|
+
```
|
|
566
|
+
FAIL A08_REGION_NAMES_MATCH_ATTACHMENTS: attachment "hull" resolves to region "raft"; v0 requires them identical
|
|
567
|
+
rigc: 1 assertion(s) failed — nothing written
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Nothing was written. A red run leaves no half-built artifact on disk to mistake
|
|
571
|
+
for a result, and there is no flag that changes that.
|
|
572
|
+
|
|
573
|
+
**Where to go next.**
|
|
574
|
+
|
|
575
|
+
- 📘 **[docs/AUTHORING.md](docs/AUTHORING.md)** is the real guide — both files
|
|
576
|
+
field by field, the emission rules, every named failure mapped to the file that
|
|
577
|
+
has to change, and §8–§9 for reproducing a shot you were given as pictures. It
|
|
578
|
+
ships inside the npm package too, at
|
|
579
|
+
`node_modules/spine-rigc/docs/AUTHORING.md`.
|
|
580
|
+
- `rigc explain --rig buoy.rig.json --motion buoy.motion.json --out spine` prints
|
|
581
|
+
the compiled rig as a table — every bone with its resolved parent, the slots in
|
|
582
|
+
draw order, every timeline key by key — and writes nothing. It is what to reach
|
|
583
|
+
for when a rig compiles and still looks wrong.
|
|
584
|
+
- 🚨 **A green gate does not mean the animation is right**, and no assertion
|
|
585
|
+
could. If you have reference pictures of the shot,
|
|
586
|
+
`rigc check --candidate spine --frames <dir>` is the half of the loop that can
|
|
587
|
+
see a wrong animation — AUTHORING.md §9.
|
|
588
|
+
- [docs/LADDER.md](docs/LADDER.md) is the benchmark: the same job, from a brief
|
|
589
|
+
and rendered frames, scored. [docs/PILOT.md](docs/PILOT.md) is how to run an
|
|
590
|
+
agent through it and score what comes back.
|
|
591
|
+
- 🤖 **Handing the authoring to an AI agent?**
|
|
592
|
+
[docs/PROMPTING.md](docs/PROMPTING.md) is the operator's page — the six prompt
|
|
593
|
+
clauses a measured pilot run paid for, and what you can leave unsaid.
|
|
594
|
+
|
|
350
595
|
## Usage
|
|
351
596
|
|
|
352
597
|
📘 **Writing a spec? Read [docs/AUTHORING.md](docs/AUTHORING.md) first.** It is the
|
|
@@ -508,7 +753,7 @@ fixtures/ public.ts — the three synthetic cuts the selftest breaks
|
|
|
508
753
|
src/
|
|
509
754
|
compile.ts rig + motion spec (+ manifest) -> skeleton JSON + atlas text (pure data assembly)
|
|
510
755
|
rig.ts the rig spec — `spec: "rigc-rig/1"`, the skeleton as data
|
|
511
|
-
validate.ts spine-core round trip + the
|
|
756
|
+
validate.ts spine-core round trip + the 34 assertions
|
|
512
757
|
diff.ts structural comparison of two skeletons, one ratio per measure
|
|
513
758
|
render.ts the rasteriser (regions + meshes), shared by the reference renderer and check
|
|
514
759
|
check.ts a candidate against rendered frames — pixels and per-slot drift,
|
|
@@ -530,6 +775,10 @@ bench/ count_features.ts — what the example corpus actually uses
|
|
|
530
775
|
runs/ — one directory per attempt, and the run protocol
|
|
531
776
|
transcriptions/ — rung specs transcribed from a reference export,
|
|
532
777
|
which measure expressiveness and NOT authoring (see LADDER.md)
|
|
778
|
+
viewer/ the run viewer — dev server only, no build (see above)
|
|
779
|
+
vite.config.ts /api/inventory and /repo/<path>, and the build refusal
|
|
780
|
+
inventory.ts what is under bench/runs, resolved to URLs
|
|
781
|
+
main.ts the two panes, the transport, the report
|
|
533
782
|
docs/ AUTHORING.md (how to author a rig), LADDER.md (live rung status),
|
|
534
783
|
SPEC_COVERAGE.md (format survey),
|
|
535
784
|
feature_matrix.{csv,json}
|
package/cli.ts
CHANGED
|
@@ -358,8 +358,14 @@ function cmdDiff(flags: Record<string, string>, positional: string[]): void {
|
|
|
358
358
|
*
|
|
359
359
|
* There is no pass mark, for the same reason `diff` has none.
|
|
360
360
|
*/
|
|
361
|
-
function readCheckFlags(flags: Record<string, string>): Pick<CheckOptions, 'fps' | 'viewport' | 'as'> {
|
|
362
|
-
const out: Pick<CheckOptions, 'fps' | 'viewport' | 'as'> = {};
|
|
361
|
+
function readCheckFlags(flags: Record<string, string>): Pick<CheckOptions, 'fps' | 'viewport' | 'as' | 'framing'> {
|
|
362
|
+
const out: Pick<CheckOptions, 'fps' | 'viewport' | 'as' | 'framing'> = {};
|
|
363
|
+
if (flags.framing !== undefined) {
|
|
364
|
+
if (flags.framing !== 'per-shot' && flags.framing !== 'shared') {
|
|
365
|
+
throw new UsageError('--framing takes per-shot (the default) or shared');
|
|
366
|
+
}
|
|
367
|
+
out.framing = flags.framing;
|
|
368
|
+
}
|
|
363
369
|
if (flags.fps !== undefined) {
|
|
364
370
|
const fps = Number(flags.fps);
|
|
365
371
|
if (!Number.isFinite(fps) || fps <= 0) throw new UsageError('--fps must be a positive number');
|
|
@@ -503,6 +509,13 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
503
509
|
// summary that reported those numbers without saying how the two shots were
|
|
504
510
|
// put on each other is how issue #34 stayed invisible for two ladder runs.
|
|
505
511
|
const framing = check.framingFit;
|
|
512
|
+
if (!framing && check.sharedFraming) {
|
|
513
|
+
const f = check.sharedFraming.fit;
|
|
514
|
+
console.log(
|
|
515
|
+
` framing one per set (${check.animations.length}); one shared box leaves ` +
|
|
516
|
+
`x${f.scale.toFixed(6)}, rms ${f.rms.toFixed(2)}px — see the check table above for each set's own`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
506
519
|
if (framing) {
|
|
507
520
|
const signed = (n: number): string => `${n >= 0 ? '+' : ''}${n.toFixed(2)}`;
|
|
508
521
|
const how = !framing.applied
|
|
@@ -510,9 +523,18 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
510
523
|
: framing.source === 'declared'
|
|
511
524
|
? `frames.json's own box, the candidate measured into it`
|
|
512
525
|
: `fitted to the candidate's pixels, ${framing.passes} pass(es)${framing.settled ? '' : framing.cycled ? ', cycling' : ', unsettled'}`;
|
|
526
|
+
// The MAE-refined offset belongs on this line rather than only in `check`'s
|
|
527
|
+
// own table: it moved the box every figure below was measured in, so a row
|
|
528
|
+
// that quoted the figures without it would not say what they were measured
|
|
529
|
+
// against — issue #146's own version of the #34 lesson above.
|
|
530
|
+
const r = framing.refinement;
|
|
531
|
+
const refined =
|
|
532
|
+
r === null || !r.applied
|
|
533
|
+
? ''
|
|
534
|
+
: ` MAE-refined ${signed(r.dx)}, ${signed(r.dy)}px (${r.before.toFixed(2)} → ${r.after.toFixed(2)} ref)`;
|
|
513
535
|
console.log(
|
|
514
536
|
` framing fit x${framing.fit.scale.toFixed(6)} rms ${framing.fit.rms.toFixed(2)}px union residual ` +
|
|
515
|
-
`${signed(framing.fit.residualWidth)} x ${signed(framing.fit.residualHeight)}px (${how})`,
|
|
537
|
+
`${signed(framing.fit.residualWidth)} x ${signed(framing.fit.residualHeight)}px (${how})${refined}`,
|
|
516
538
|
);
|
|
517
539
|
}
|
|
518
540
|
for (const anim of check.animations) {
|
|
@@ -528,9 +550,27 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
528
550
|
anim.changeDisagreements === 0
|
|
529
551
|
? ''
|
|
530
552
|
: `, ${anim.changeDisagreements}/${anim.changePairs} pair(s) change unlike the reference`;
|
|
553
|
+
// Which of the candidate's own bone chains the error is in — one name, so a
|
|
554
|
+
// loop between builds reads a unit to fix rather than a verdict on the shot.
|
|
555
|
+
// The full table is in `check`'s own report; this is its headline.
|
|
556
|
+
const worstChain = [...anim.chains].sort((a, b) => b.maeShare - a.maeShare)[0];
|
|
557
|
+
const chain =
|
|
558
|
+
worstChain === undefined ? '' : `, ${worstChain.chain} carries ${(worstChain.maeShare * 100).toFixed(0)}%`;
|
|
559
|
+
// `ref=` is the same difference over the reference's own drawn pixels. It is
|
|
560
|
+
// carried here and not only in `check`'s own table because this is the line a
|
|
561
|
+
// loop reads between builds, and `mean=` has a denominator the candidate can
|
|
562
|
+
// grow — see `FrameCheck.maeReference`.
|
|
563
|
+
// ...and the contact sheet, when the set ships one: a row reading "over 2
|
|
564
|
+
// frame(s)" for a 311-frame shot is the hole issue #36 closed, and the whole
|
|
565
|
+
// -shot figure is the one that says the frames between the stills were seen.
|
|
566
|
+
const sheet =
|
|
567
|
+
anim.sheet === null
|
|
568
|
+
? ''
|
|
569
|
+
: `, sheet ${anim.sheet.compared} tile(s) mean=${anim.sheet.meanMae.toFixed(2)} ` +
|
|
570
|
+
`worst=${anim.sheet.worstMae.toFixed(2)}`;
|
|
531
571
|
console.log(
|
|
532
|
-
` ${anim.dir.padEnd(10)} MAE mean=${anim.meanMae.toFixed(2)} worst=${anim.worstMae.toFixed(2)}
|
|
533
|
-
`over ${anim.compared} frame(s) ${drift}${change}`,
|
|
572
|
+
` ${anim.dir.padEnd(10)} MAE mean=${anim.meanMae.toFixed(2)} worst=${anim.worstMae.toFixed(2)} ` +
|
|
573
|
+
`ref=${anim.meanMaeReference.toFixed(2)} over ${anim.compared} frame(s) ${drift}${change}${chain}${sheet}`,
|
|
534
574
|
);
|
|
535
575
|
}
|
|
536
576
|
} else {
|
|
@@ -541,10 +581,17 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
541
581
|
console.log(' a rung is cleared by a person reading the measures, and docs/LADDER.md records it.');
|
|
542
582
|
|
|
543
583
|
if (flags.json !== undefined) {
|
|
584
|
+
// No `gates` field, deliberately. The rung's gate string names its features
|
|
585
|
+
// and its per-skeleton counts, which `bench/runs/README.md` forbids a run
|
|
586
|
+
// from reading — and this report is one of the six files the run protocol
|
|
587
|
+
// requires committing, so a copy of it here would sit inside every future
|
|
588
|
+
// run's directory, which is exactly where the next author looks for process
|
|
589
|
+
// notes. `rung` identifies the rung and carries nothing (issue #137). The
|
|
590
|
+
// console block above still prints the gate string: that is for the person
|
|
591
|
+
// reading the run, not a file the protocol commits.
|
|
544
592
|
writeJson(flags.json, {
|
|
545
593
|
rung: rung.id,
|
|
546
594
|
example: rung.example,
|
|
547
|
-
gates: rung.gates,
|
|
548
595
|
profile,
|
|
549
596
|
candidate: { skeleton: skeletonPath, atlas: atlasPath },
|
|
550
597
|
validate: report,
|
|
@@ -700,6 +747,10 @@ const USAGE = [
|
|
|
700
747
|
' --atlas <path> the candidate\'s atlas, when it is not beside the skeleton',
|
|
701
748
|
' --fps <n> only for a frame set with no frames.json sidecar',
|
|
702
749
|
' --viewport x,y,w,h pin the candidate\'s world box, y up, instead of fitting it',
|
|
750
|
+
' --framing per-shot|shared decide the framing per frame set (default), or once',
|
|
751
|
+
' across all of them. Per set, a set whose own pixels land in',
|
|
752
|
+
' frames.json\'s box is measured there; on a multi-shot root that',
|
|
753
|
+
' is worth 15-25 MAE against one shared fit for every set',
|
|
703
754
|
' --as <name> the candidate animation to play, when it is named differently',
|
|
704
755
|
' --all-frames print every frame, not just the worst by MAE',
|
|
705
756
|
' --json <out> the whole per-frame, per-slot report',
|