spine-rigc 0.2.1 → 0.3.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 +231 -6
- package/cli.ts +39 -5
- package/docs/AUTHORING.md +611 -24
- package/docs/SPEC_COVERAGE.md +16 -10
- package/package.json +5 -2
- package/src/chains.ts +170 -0
- package/src/check.ts +1013 -92
- package/src/compile.ts +325 -6
- package/src/ladder.ts +1 -1
- package/src/render.ts +22 -2
- package/src/rig.ts +169 -6
- 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,11 @@ 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.
|
|
137
141
|
- **Per-frame change** — how many pixels each side moved since **its own** previous
|
|
138
142
|
frame, compared against each other. It is the only measure here that looks at the
|
|
139
143
|
relation between two frames rather than at one, and it is what catches a held pose
|
|
@@ -146,6 +150,13 @@ per animation and per frame:
|
|
|
146
150
|
template-matched against its own rendered pixels instead, with a confidence; and
|
|
147
151
|
where nothing inside the distance that slot could plausibly have moved matches
|
|
148
152
|
it, the answer is **no match** rather than a number about some other part.
|
|
153
|
+
- **Per-chain attribution** — the same two, rolled up onto the unit an author
|
|
154
|
+
repairs. `check` cuts the **candidate's own** bone tree into chains at its branch
|
|
155
|
+
points and prints, per chain per set, the worst slot drift with its slot and
|
|
156
|
+
frame, the mean, the error per pixel inside it, and its share of the set's error
|
|
157
|
+
over the reference's own drawn pixels — plus one rollup line per chain across
|
|
158
|
+
every set. A figure with a dozen joints otherwise collapses to one number a shot,
|
|
159
|
+
and *"motion ✗"* over sixteen shots does not say which limb to re-key.
|
|
149
160
|
|
|
150
161
|
🔒 **It never reads the reference skeleton.** It opens the candidate and PNG
|
|
151
162
|
frames, and nothing else: every reference-side read goes through one guard that
|
|
@@ -195,6 +206,46 @@ closed**; B3's validator half is (the packed-atlas clauses live behind `--profil
|
|
|
195
206
|
emitter half — no packer, no atlas importer — is not. Ordered gap list in Part 4 of that document;
|
|
196
207
|
live status, and B1's proof, in [docs/LADDER.md](docs/LADDER.md).
|
|
197
208
|
|
|
209
|
+
## Run viewer — watching a run instead of reading it
|
|
210
|
+
|
|
211
|
+
`check.txt` says a candidate's worst frame is f0012 at 56 MAE. The viewer shows
|
|
212
|
+
you f0012.
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
bun run viewer # http://localhost:5173
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Pick a run, a candidate and an animation. The left pane plays the candidate's
|
|
219
|
+
emitted `skeleton.json` — rendered by **[spine-html](https://github.com/firejune/spine-html)**,
|
|
220
|
+
plain DOM, one CSS matrix per slot — and the right pane shows the reference
|
|
221
|
+
frames for the same animation from `bench/reference/`, indexed by the scrubber's
|
|
222
|
+
time at the frame set's own fps. Both panes use the world box the run was
|
|
223
|
+
measured in (`bench.json`'s `check.viewport`, per frame set where the run framed
|
|
224
|
+
them separately), so the two pictures are comparable exactly as far as the
|
|
225
|
+
check's numbers say they are — and the pane label names which box that was.
|
|
226
|
+
Under them: `bench.json`'s section means and the framing plus per-animation
|
|
227
|
+
summary from `check.txt`.
|
|
228
|
+
|
|
229
|
+
It is also the smallest end-to-end proof the two modules have. rigc emits Spine
|
|
230
|
+
data; spine-html consumes Spine data; neither is checking its own work when the
|
|
231
|
+
skeleton one wrote comes up animating in the other.
|
|
232
|
+
|
|
233
|
+
Every run under `bench/runs/` is listed, including the ones that predate a
|
|
234
|
+
convention — those are greyed out with the reason (a missing atlas page usually
|
|
235
|
+
means `bun run fetch-examples` has not run) rather than dropped, because the
|
|
236
|
+
ladder's history is part of what the viewer is for.
|
|
237
|
+
|
|
238
|
+
🚫 **There is no build, and that is deliberate.** The viewer reads the working
|
|
239
|
+
tree: the runs, the reference frames, and `examples/` — which is Esoteric
|
|
240
|
+
Software's art, fetched rather than redistributed and non-commercial even then
|
|
241
|
+
(see [NOTICE.md](NOTICE.md)). A bundle would copy those pixels into a
|
|
242
|
+
distributable artifact. So there is one mode, `vite dev` on localhost, the dev
|
|
243
|
+
server serves nothing outside `bench/` and `examples/`, and `vite build` fails
|
|
244
|
+
on purpose. `viewer/` is not in `package.json`'s `files`, so it never ships
|
|
245
|
+
either; it is also outside the root `tsconfig.json` (it needs the DOM lib, which
|
|
246
|
+
the rest of the repository must not have) and is type-checked on its own with
|
|
247
|
+
`bunx tsc -p viewer --noEmit`. `bun run lint` covers it like everything else.
|
|
248
|
+
|
|
198
249
|
## What exists today
|
|
199
250
|
|
|
200
251
|
**Inputs — three files, one domain each.** Only the middle one is required.
|
|
@@ -249,7 +300,7 @@ model (what is pinned, what may move, how authority falls off), and the
|
|
|
249
300
|
### The validator
|
|
250
301
|
|
|
251
302
|
[`src/validate.ts`](src/validate.ts) parses the emitted artifacts with `spine-core`
|
|
252
|
-
and then runs
|
|
303
|
+
and then runs 34 named assertions over the loaded skeleton. Each one exists because
|
|
253
304
|
the failure it catches is **silent**: the file loads, animates, and lies.
|
|
254
305
|
|
|
255
306
|
Assertions whose data is absent are reported as **SKIP**, never folded into the pass
|
|
@@ -257,7 +308,7 @@ count — an assertion with nothing to check has not checked anything.
|
|
|
257
308
|
|
|
258
309
|
#### Profiles — "wrong" versus "not how we do it here"
|
|
259
310
|
|
|
260
|
-
Not all
|
|
311
|
+
Not all 34 rules are about Spine. Some are about **spine-html**, the renderer this
|
|
261
312
|
compiler was built to feed, and about one project's frame budget; they fire on real,
|
|
262
313
|
correct, editor-produced Spine data, because the official example projects carry
|
|
263
314
|
clipping attachments, unweighted meshes, 116-triangle meshes and packed atlases —
|
|
@@ -270,8 +321,8 @@ So `validate` and `build` take a `--profile`:
|
|
|
270
321
|
|
|
271
322
|
| Profile | Runs | For |
|
|
272
323
|
| --- | --- | --- |
|
|
273
|
-
| `spine-html` | all
|
|
274
|
-
| `spine` | the
|
|
324
|
+
| `spine-html` | all 34 | **the default.** Is this a rig this project can ship? |
|
|
325
|
+
| `spine` | the 20 validity rules | Is this valid Spine 4.3 that any runtime plays correctly? |
|
|
275
326
|
|
|
276
327
|
The **Profile** column below says which is which — `both` = validity, `renderer` and
|
|
277
328
|
`archetype` = `spine-html` only, and **`both ◑`** = a mixed assertion whose validity
|
|
@@ -315,6 +366,8 @@ the renderer policy*.
|
|
|
315
366
|
| `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
367
|
| `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
368
|
| `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 |
|
|
369
|
+
| `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 |
|
|
370
|
+
| `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
371
|
|
|
319
372
|
## Install
|
|
320
373
|
|
|
@@ -347,6 +400,174 @@ frames rendered from them, which are fetched rather than redistributed (see
|
|
|
347
400
|
[NOTICE.md](NOTICE.md)). They need a clone and `bun run fetch-examples`, and say
|
|
348
401
|
so by name when the corpus is absent.
|
|
349
402
|
|
|
403
|
+
## First rig in ten minutes
|
|
404
|
+
|
|
405
|
+
A whole rig, end to end, in a scratch directory: three tiny plates, two JSON
|
|
406
|
+
files, one `build`, one `validate`. No clone, no art pipeline, nothing fetched.
|
|
407
|
+
|
|
408
|
+
🚫 **Every value below is invented for this section** — a doll that exists
|
|
409
|
+
nowhere else in this repository. That is [AUTHORING.md](docs/AUTHORING.md) §3's
|
|
410
|
+
rule applied here: no example value in these documents is copied out of a
|
|
411
|
+
reference export, so nothing you read in a quickstart is an answer to anything
|
|
412
|
+
[the ladder](docs/LADDER.md) measures.
|
|
413
|
+
|
|
414
|
+
**1. Install the command.**
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
bun add -g spine-rigc # installs `rigc`
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Or skip the install and prefix every command below with `bunx `, e.g.
|
|
421
|
+
`bunx spine-rigc build …`.
|
|
422
|
+
|
|
423
|
+
**2. Make a directory and three plates.** rigc measures PNGs rather than trusting
|
|
424
|
+
a number you typed (R5), so the art has to exist. These three are solid colours a
|
|
425
|
+
few dozen pixels across — a hull, a mast and a lamp:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
mkdir -p buoy/images && cd buoy
|
|
429
|
+
bun -e '
|
|
430
|
+
const parts = {
|
|
431
|
+
"images/hull.png": "iVBORw0KGgoAAAANSUhEUgAAADgAAAAMCAYAAAA3bX6lAAAAKElEQVR42mOI8bL6P5wxw6gHRz046sFRD456cNSDox4c9eCoBwcrBgDSZ+mdl2OiDgAAAABJRU5ErkJggg==",
|
|
432
|
+
"images/mast.png": "iVBORw0KGgoAAAANSUhEUgAAAAgAAAA0CAYAAAC3t3ldAAAAH0lEQVR42mO4dunIf3yYYVTBqIJRBaMKRhWMKhgcCgBGJo4s9YnopgAAAABJRU5ErkJggg==",
|
|
433
|
+
"images/lamp.png": "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAHElEQVR42mP4v8HhPzUww6hBowaNGjRq0HAzCADvdrVmFPbc+QAAAABJRU5ErkJggg=="
|
|
434
|
+
};
|
|
435
|
+
for (const [p, b] of Object.entries(parts)) await Bun.write(p, Buffer.from(b, "base64"));
|
|
436
|
+
'
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**3. The rig spec — `buoy.rig.json`.** Structure only: bones, the slots array in
|
|
440
|
+
draw order, and one skin mapping each slot to a plate.
|
|
441
|
+
|
|
442
|
+
```json
|
|
443
|
+
{
|
|
444
|
+
"spec": "rigc-rig/1",
|
|
445
|
+
"name": "buoy",
|
|
446
|
+
"images": "images",
|
|
447
|
+
"skeleton": { "width": 200, "height": 200 },
|
|
448
|
+
"bones": [
|
|
449
|
+
{ "name": "root" },
|
|
450
|
+
{ "name": "hull", "parent": "root", "x": 0, "y": 0 },
|
|
451
|
+
{ "name": "mast", "parent": "hull", "x": 0, "y": 4 },
|
|
452
|
+
{ "name": "lamp", "parent": "mast", "x": 0, "y": 52 }
|
|
453
|
+
],
|
|
454
|
+
"slots": [
|
|
455
|
+
{ "name": "mast", "bone": "mast", "attachment": "mast" },
|
|
456
|
+
{ "name": "hull", "bone": "hull", "attachment": "hull" },
|
|
457
|
+
{ "name": "lamp", "bone": "lamp", "attachment": "lamp" }
|
|
458
|
+
],
|
|
459
|
+
"skins": {
|
|
460
|
+
"default": {
|
|
461
|
+
"mast": { "mast": { "image": "mast.png", "y": 26 } },
|
|
462
|
+
"hull": { "hull": { "image": "hull.png" } },
|
|
463
|
+
"lamp": { "lamp": { "image": "lamp.png" } }
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Three things in there are worth naming, because each is a rule rather than a
|
|
470
|
+
style: the **slots array is the setup draw order** (R4) — index 0 is furthest
|
|
471
|
+
back, so the mast is behind the hull; the attachment carries an **`image`
|
|
472
|
+
instead of a `width`/`height`** (R5), which is what makes the size in the
|
|
473
|
+
skeleton and the size in the atlas incapable of drifting apart; and the mast's
|
|
474
|
+
`"y": 26` offsets the plate *within* its slot so the bone sits at the mast's foot
|
|
475
|
+
rather than its middle.
|
|
476
|
+
|
|
477
|
+
**4. The motion spec — `buoy.motion.json`.** Time only, aimed at the rig by name:
|
|
478
|
+
|
|
479
|
+
```json
|
|
480
|
+
{
|
|
481
|
+
"spec": "rigc-motion/1",
|
|
482
|
+
"archetype": "buoy",
|
|
483
|
+
"cut": "buoy",
|
|
484
|
+
"easings": { "swing": [0.42, 0, 0.58, 1] },
|
|
485
|
+
"animations": {
|
|
486
|
+
"bob": {
|
|
487
|
+
"duration": 2,
|
|
488
|
+
"loop": true,
|
|
489
|
+
"tracks": [
|
|
490
|
+
{
|
|
491
|
+
"bone": "hull",
|
|
492
|
+
"property": "translatey",
|
|
493
|
+
"keys": [
|
|
494
|
+
{ "t": 0, "v": [0], "ease": "swing" },
|
|
495
|
+
{ "t": 0.5, "v": [5], "ease": "swing" },
|
|
496
|
+
{ "t": 1.5, "v": [-5], "ease": "swing" },
|
|
497
|
+
{ "t": 2, "v": [0] }
|
|
498
|
+
]
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
"bone": "mast",
|
|
502
|
+
"property": "rotate",
|
|
503
|
+
"keys": [
|
|
504
|
+
{ "t": 0, "v": [-6], "ease": "swing" },
|
|
505
|
+
{ "t": 1, "v": [6], "ease": "swing" },
|
|
506
|
+
{ "t": 2, "v": [-6] }
|
|
507
|
+
]
|
|
508
|
+
}
|
|
509
|
+
]
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
`archetype` must equal the rig's `name`. `duration` is declared and then checked
|
|
516
|
+
against what actually compiled (R7). The **last key of each track carries no
|
|
517
|
+
easing** — there is nothing after it to ease towards, and saying otherwise is a
|
|
518
|
+
compile error.
|
|
519
|
+
|
|
520
|
+
**5. Build, then re-gate what it wrote.**
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
rigc build --rig buoy.rig.json --motion buoy.motion.json --images images --out spine
|
|
524
|
+
rigc validate spine
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
`build` prints every assertion by name, then the shape of what it emitted, then
|
|
528
|
+
the two files:
|
|
529
|
+
|
|
530
|
+
```
|
|
531
|
+
.. 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
|
|
532
|
+
rigc: wrote …/buoy/spine/skeleton.json
|
|
533
|
+
rigc: wrote …/buoy/spine/skeleton.atlas
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
and `validate` re-reads those artifacts from disk and ends `rigc: green`. That is
|
|
537
|
+
a rig. `spine/skeleton.json` is Spine 4.3 skeleton data — it loads in a Spine
|
|
538
|
+
runtime and it imports into the Spine editor.
|
|
539
|
+
|
|
540
|
+
**Try breaking it**, because the validator's messages are the interface here and
|
|
541
|
+
they are worth meeting once on purpose. Rename `images/hull.png` to
|
|
542
|
+
`images/raft.png`, point the spec's `image` at the new name, and build again:
|
|
543
|
+
|
|
544
|
+
```
|
|
545
|
+
FAIL A08_REGION_NAMES_MATCH_ATTACHMENTS: attachment "hull" resolves to region "raft"; v0 requires them identical
|
|
546
|
+
rigc: 1 assertion(s) failed — nothing written
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Nothing was written. A red run leaves no half-built artifact on disk to mistake
|
|
550
|
+
for a result, and there is no flag that changes that.
|
|
551
|
+
|
|
552
|
+
**Where to go next.**
|
|
553
|
+
|
|
554
|
+
- 📘 **[docs/AUTHORING.md](docs/AUTHORING.md)** is the real guide — both files
|
|
555
|
+
field by field, the emission rules, every named failure mapped to the file that
|
|
556
|
+
has to change, and §8–§9 for reproducing a shot you were given as pictures. It
|
|
557
|
+
ships inside the npm package too, at
|
|
558
|
+
`node_modules/spine-rigc/docs/AUTHORING.md`.
|
|
559
|
+
- `rigc explain --rig buoy.rig.json --motion buoy.motion.json --out spine` prints
|
|
560
|
+
the compiled rig as a table — every bone with its resolved parent, the slots in
|
|
561
|
+
draw order, every timeline key by key — and writes nothing. It is what to reach
|
|
562
|
+
for when a rig compiles and still looks wrong.
|
|
563
|
+
- 🚨 **A green gate does not mean the animation is right**, and no assertion
|
|
564
|
+
could. If you have reference pictures of the shot,
|
|
565
|
+
`rigc check --candidate spine --frames <dir>` is the half of the loop that can
|
|
566
|
+
see a wrong animation — AUTHORING.md §9.
|
|
567
|
+
- [docs/LADDER.md](docs/LADDER.md) is the benchmark: the same job, from a brief
|
|
568
|
+
and rendered frames, scored. [docs/PILOT.md](docs/PILOT.md) is how to run an
|
|
569
|
+
agent through it and score what comes back.
|
|
570
|
+
|
|
350
571
|
## Usage
|
|
351
572
|
|
|
352
573
|
📘 **Writing a spec? Read [docs/AUTHORING.md](docs/AUTHORING.md) first.** It is the
|
|
@@ -508,7 +729,7 @@ fixtures/ public.ts — the three synthetic cuts the selftest breaks
|
|
|
508
729
|
src/
|
|
509
730
|
compile.ts rig + motion spec (+ manifest) -> skeleton JSON + atlas text (pure data assembly)
|
|
510
731
|
rig.ts the rig spec — `spec: "rigc-rig/1"`, the skeleton as data
|
|
511
|
-
validate.ts spine-core round trip + the
|
|
732
|
+
validate.ts spine-core round trip + the 34 assertions
|
|
512
733
|
diff.ts structural comparison of two skeletons, one ratio per measure
|
|
513
734
|
render.ts the rasteriser (regions + meshes), shared by the reference renderer and check
|
|
514
735
|
check.ts a candidate against rendered frames — pixels and per-slot drift,
|
|
@@ -530,6 +751,10 @@ bench/ count_features.ts — what the example corpus actually uses
|
|
|
530
751
|
runs/ — one directory per attempt, and the run protocol
|
|
531
752
|
transcriptions/ — rung specs transcribed from a reference export,
|
|
532
753
|
which measure expressiveness and NOT authoring (see LADDER.md)
|
|
754
|
+
viewer/ the run viewer — dev server only, no build (see above)
|
|
755
|
+
vite.config.ts /api/inventory and /repo/<path>, and the build refusal
|
|
756
|
+
inventory.ts what is under bench/runs, resolved to URLs
|
|
757
|
+
main.ts the two panes, the transport, the report
|
|
533
758
|
docs/ AUTHORING.md (how to author a rig), LADDER.md (live rung status),
|
|
534
759
|
SPEC_COVERAGE.md (format survey),
|
|
535
760
|
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
|
|
@@ -528,9 +541,19 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
528
541
|
anim.changeDisagreements === 0
|
|
529
542
|
? ''
|
|
530
543
|
: `, ${anim.changeDisagreements}/${anim.changePairs} pair(s) change unlike the reference`;
|
|
544
|
+
// Which of the candidate's own bone chains the error is in — one name, so a
|
|
545
|
+
// loop between builds reads a unit to fix rather than a verdict on the shot.
|
|
546
|
+
// The full table is in `check`'s own report; this is its headline.
|
|
547
|
+
const worstChain = [...anim.chains].sort((a, b) => b.maeShare - a.maeShare)[0];
|
|
548
|
+
const chain =
|
|
549
|
+
worstChain === undefined ? '' : `, ${worstChain.chain} carries ${(worstChain.maeShare * 100).toFixed(0)}%`;
|
|
550
|
+
// `ref=` is the same difference over the reference's own drawn pixels. It is
|
|
551
|
+
// carried here and not only in `check`'s own table because this is the line a
|
|
552
|
+
// loop reads between builds, and `mean=` has a denominator the candidate can
|
|
553
|
+
// grow — see `FrameCheck.maeReference`.
|
|
531
554
|
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}`,
|
|
555
|
+
` ${anim.dir.padEnd(10)} MAE mean=${anim.meanMae.toFixed(2)} worst=${anim.worstMae.toFixed(2)} ` +
|
|
556
|
+
`ref=${anim.meanMaeReference.toFixed(2)} over ${anim.compared} frame(s) ${drift}${change}${chain}`,
|
|
534
557
|
);
|
|
535
558
|
}
|
|
536
559
|
} else {
|
|
@@ -541,10 +564,17 @@ function cmdBench(flags: Record<string, string>, positional: string[]): void {
|
|
|
541
564
|
console.log(' a rung is cleared by a person reading the measures, and docs/LADDER.md records it.');
|
|
542
565
|
|
|
543
566
|
if (flags.json !== undefined) {
|
|
567
|
+
// No `gates` field, deliberately. The rung's gate string names its features
|
|
568
|
+
// and its per-skeleton counts, which `bench/runs/README.md` forbids a run
|
|
569
|
+
// from reading — and this report is one of the six files the run protocol
|
|
570
|
+
// requires committing, so a copy of it here would sit inside every future
|
|
571
|
+
// run's directory, which is exactly where the next author looks for process
|
|
572
|
+
// notes. `rung` identifies the rung and carries nothing (issue #137). The
|
|
573
|
+
// console block above still prints the gate string: that is for the person
|
|
574
|
+
// reading the run, not a file the protocol commits.
|
|
544
575
|
writeJson(flags.json, {
|
|
545
576
|
rung: rung.id,
|
|
546
577
|
example: rung.example,
|
|
547
|
-
gates: rung.gates,
|
|
548
578
|
profile,
|
|
549
579
|
candidate: { skeleton: skeletonPath, atlas: atlasPath },
|
|
550
580
|
validate: report,
|
|
@@ -700,6 +730,10 @@ const USAGE = [
|
|
|
700
730
|
' --atlas <path> the candidate\'s atlas, when it is not beside the skeleton',
|
|
701
731
|
' --fps <n> only for a frame set with no frames.json sidecar',
|
|
702
732
|
' --viewport x,y,w,h pin the candidate\'s world box, y up, instead of fitting it',
|
|
733
|
+
' --framing per-shot|shared decide the framing per frame set (default), or once',
|
|
734
|
+
' across all of them. Per set, a set whose own pixels land in',
|
|
735
|
+
' frames.json\'s box is measured there; on a multi-shot root that',
|
|
736
|
+
' is worth 15-25 MAE against one shared fit for every set',
|
|
703
737
|
' --as <name> the candidate animation to play, when it is named differently',
|
|
704
738
|
' --all-frames print every frame, not just the worst by MAE',
|
|
705
739
|
' --json <out> the whole per-frame, per-slot report',
|