spine-rigc 0.22.1 → 0.23.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 +64 -1
- package/cli.ts +205 -11
- package/docs/AUTHORING.md +421 -64
- package/docs/INGEST.md +185 -41
- package/docs/SPEC_COVERAGE.md +26 -8
- package/package.json +1 -1
- package/skills/ingest/SKILL.md +33 -11
- package/src/atlas.ts +57 -13
- package/src/check.ts +83 -1
- package/src/compile.ts +400 -63
- package/src/diff.ts +125 -2
- package/src/ingest.ts +1078 -0
- package/src/render.ts +104 -10
- package/src/rig.ts +92 -8
- package/src/types.ts +53 -7
- package/src/validate.ts +205 -27
- package/tools/editor_roundtrip.ts +172 -21
package/docs/AUTHORING.md
CHANGED
|
@@ -329,28 +329,119 @@ Four things are refused rather than warned about, because each of them otherwise
|
|
|
329
329
|
| a page the atlas names and the disk lacks | nothing to sample; caught on the way in, so the message names the atlas rather than the artifact rigc wrote from it |
|
|
330
330
|
| a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does |
|
|
331
331
|
|
|
332
|
-
|
|
332
|
+
One limit, stated rather than discovered:
|
|
333
333
|
|
|
334
334
|
- an **optional state** (a manifest `states:` entry) whose region is not in the
|
|
335
335
|
pack is a `DROP`, not a refusal — the same documented absence a missing PNG has
|
|
336
|
-
always been, and the line names the atlas rather than a file nobody opened
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
336
|
+
always been, and the line names the atlas rather than a file nobody opened.
|
|
337
|
+
|
|
338
|
+
A **turned** region is not one of them, and used to be (issue #570). A pack made
|
|
339
|
+
by somebody else routinely rotates a region to save space — `rotate: 90`,
|
|
340
|
+
`rotate: 180`, `rotate: 270`, or the format's older `rotate: true` — and rigc
|
|
341
|
+
reads all of them: anything that measures a part's pixels sees the same grid it
|
|
342
|
+
would have seen from the loose PNG, so a `contour` mesh traces the same
|
|
343
|
+
silhouette and an authored mesh's fit figure is the same number. The one thing
|
|
344
|
+
that does not change is what rigc **writes**: its own packer never turns a
|
|
345
|
+
region.
|
|
346
|
+
|
|
347
|
+
⚠️ Two limits here are real and neither is about rotation:
|
|
348
|
+
|
|
349
|
+
- `--atlas-in` cannot recover what a `scale:` quantised away (above), turned or not;
|
|
350
|
+
- the **renderer** profile still refuses a turned region outright
|
|
351
|
+
(`A06`, `--profile spine-html`), because that profile is about artifacts rigc
|
|
352
|
+
itself emits and it never packs one turned. Reading a foreign pack and gating
|
|
353
|
+
one under somebody else's renderer policy are different questions.
|
|
341
354
|
|
|
342
355
|
`--pack` and `--atlas-in` are opposite directions through the same door and are
|
|
343
356
|
refused together.
|
|
344
357
|
|
|
358
|
+
### 0.3 Starting from a skeleton somebody else made — `ingest`
|
|
359
|
+
|
|
360
|
+
Everything above starts from two spec files you wrote. `rigc ingest` starts from a
|
|
361
|
+
**Spine 4.3 `skeleton.json` you were handed** and writes those two files for you, so
|
|
362
|
+
an existing rig is a starting point instead of 250 KB of arrays to retype.
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
bun cli.ts ingest hero.json --out specs/ --stage 0,0,1024,768
|
|
366
|
+
# .. out /abs/path/specs
|
|
367
|
+
# .. art loose
|
|
368
|
+
# JUDGE DURATION: animation "idle" — skeleton JSON carries no duration; the largest key time (2.667) is used …
|
|
369
|
+
# LOSS PATH_LENGTHS: skin "default" slot "track" attachment "track" — the source states `lengths`; rigc RE-MEASURES it …
|
|
370
|
+
# rigc: wrote /abs/path/specs/rig.json
|
|
371
|
+
# rigc: wrote /abs/path/specs/motion.json
|
|
372
|
+
# rigc: wrote /abs/path/specs/findings.json
|
|
373
|
+
|
|
374
|
+
bun cli.ts build --rig specs/rig.json --motion specs/motion.json --images parts/ --out spine
|
|
375
|
+
bun cli.ts diff spine/skeleton.json hero.json # 1.000 on every measure
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**The loop it belongs in is the one above, with a different first step.** `ingest`
|
|
379
|
+
writes the specs; you *edit* them the way you would edit specs you wrote; `build`
|
|
380
|
+
gates; `explain`, `render`, `preview` and `check` read the result. Nothing
|
|
381
|
+
downstream knows or cares that the file started somewhere else — which is the point,
|
|
382
|
+
and also the hazard the `note` below exists for.
|
|
383
|
+
|
|
384
|
+
**The contract is an equality, not a rulebook.** `build(ingest(x))` is `x`: the
|
|
385
|
+
rebuilt `skeleton.json` is byte for byte the file `ingest` read, and the rebuilt
|
|
386
|
+
atlas holds the same **region blocks** — as a multiset, because the order the pages
|
|
387
|
+
come out in is in no field of the skeleton and a decompiled spec cannot know it.
|
|
388
|
+
That is a gate rather than a claim: `bun run selftest` round-trips every rig this
|
|
389
|
+
repository builds on every run.
|
|
390
|
+
|
|
391
|
+
**Two flags, for the two things a skeleton does not encode.**
|
|
392
|
+
|
|
393
|
+
| flag | what it decides |
|
|
394
|
+
| --- | --- |
|
|
395
|
+
| `--art loose` (default) | name an `image` per attachment — `<path or placeholder>.png` — so the rebuild is `build --images <dir>` and rigc measures the PNGs |
|
|
396
|
+
| `--art none` | state `width`/`height` only, so the rebuild is `build --atlas-in <pack.atlas>` and every part resolves out of the pack |
|
|
397
|
+
| `--stage x,y,w,h` | the setup bounding box. **Required for an editor export**, which carries none |
|
|
398
|
+
| `--name <n>` | the rig spec's `name`, which the motion spec's `archetype` must equal (default: the file's basename) |
|
|
399
|
+
|
|
400
|
+
🚨 **The stage is the one value `ingest` will not guess.** rigc always emits
|
|
401
|
+
`skeleton.width`/`height` and an editor export never does, so a foreign file needs
|
|
402
|
+
`--stage`; without it the missing box is a **blocker**, named. It is not derivable —
|
|
403
|
+
posing the rig gives the *animated* extent, which is a different number from the
|
|
404
|
+
editor's setup box — and it is the value that costs least to get wrong, because no
|
|
405
|
+
measure `diff` reports reads the skeleton header at all. Supply it from the project
|
|
406
|
+
the file came from, or from the editor's own canvas.
|
|
407
|
+
|
|
408
|
+
⚠️ **The duration is a convention, and it is recorded as one.** Skeleton JSON has no
|
|
409
|
+
duration field. The largest key time is the only derivable answer and it is what a
|
|
410
|
+
runtime plays to — but it is wrong for an animation that holds its last pose past its
|
|
411
|
+
last key, and nothing in the file can tell the two apart. `ingest` writes the largest
|
|
412
|
+
key time, states the convention in the motion spec's `note`, and records a finding per
|
|
413
|
+
animation. If you know the real number, edit it: the declared duration is checked
|
|
414
|
+
against the compiled keys, so an honest one costs you nothing.
|
|
415
|
+
|
|
416
|
+
**Read the findings; they are the product.** Three kinds, and the exit code turns on
|
|
417
|
+
the first:
|
|
418
|
+
|
|
419
|
+
| gutter | meaning |
|
|
420
|
+
| --- | --- |
|
|
421
|
+
| `BLOCK` | the spec format cannot say it, so the rebuild will **not** be the file that was read — `linkedmesh`, `point`, an attachment `sequence`, an unknown field on a bone, slot or constraint, a timeline family the motion spec has no track for. The command exits non-zero **and still writes both specs**, because a spec plus a list of what is missing from it beats no spec |
|
|
422
|
+
| `JUDGE` | the skeleton cannot answer and somebody has to: the stage, and each animation's duration |
|
|
423
|
+
| `LOSS` | the skeleton says it and rigc re-derives it, on purpose. A path attachment's `lengths` is the one that matters — it is `PathConstraint`'s own four-sample measurement rather than an arc length (#560), so a transcribed one would freeze whatever produced the source |
|
|
424
|
+
|
|
425
|
+
📝 **Do not delete the `note`.** Both written specs carry one saying the file is
|
|
426
|
+
decompiled and naming the skeleton it came from. A decompiled spec is
|
|
427
|
+
indistinguishable from an authored one by inspection, every gate here calls it green —
|
|
428
|
+
because it *is* green — and no gate can catch the note's absence. It carries no
|
|
429
|
+
timestamp, deliberately: `A18_DETERMINISTIC_EMIT` compares two independent compiles
|
|
430
|
+
byte for byte, and a dated note would break the first rebuild from the spec.
|
|
431
|
+
|
|
432
|
+
⛔ **It reads skeleton JSON and nothing else** — not a `.spine` project, not a binary
|
|
433
|
+
`.skel`, not the atlas, not the art. A path that is not a `.json` is refused by name.
|
|
434
|
+
[INGEST.md](INGEST.md) is the whole page on working from a file you were handed.
|
|
435
|
+
|
|
345
436
|
The other commands:
|
|
346
437
|
|
|
347
438
|
```bash
|
|
348
439
|
bun cli.ts explain --rig … --motion … --out … # the compiled rig as a table
|
|
349
440
|
bun cli.ts validate path/to/spine # re-gate artifacts already on disk
|
|
350
441
|
bun cli.ts diff candidate.json reference.json
|
|
351
|
-
bun cli.ts check --candidate path/to/spine --frames path/to/frames
|
|
442
|
+
bun cli.ts check --candidate path/to/spine --frames path/to/frames [--skin …]
|
|
352
443
|
bun cli.ts bench 3 --candidate path/to/spine [--frames path/to/frames]
|
|
353
|
-
bun cli.ts render --candidate path/to/spine [--animation …] [--fps 12] [--max 256]
|
|
444
|
+
bun cli.ts render --candidate path/to/spine [--animation …] [--skin …] [--fps 12] [--max 256]
|
|
354
445
|
bun cli.ts preview --candidate path/to/spine [--animation …] [--out preview.html]
|
|
355
446
|
bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
|
|
356
447
|
bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
|
|
@@ -372,6 +463,9 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
|
372
463
|
for opposite fixes. A measure with nothing to compare says `0/0` and says so.
|
|
373
464
|
- **`check`** renders your candidate into the reference frames' own pixel grid and
|
|
374
465
|
compares pixels — the only thing here that can see a wrong animation. **§9.**
|
|
466
|
+
🚨 What it certifies is the **default skin** unless you pass `--skin <name>`:
|
|
467
|
+
on a rig with named skins, a run with no skin draws none of their art on either
|
|
468
|
+
side and reports a perfect `0.0000` about it (**§9**).
|
|
375
469
|
- **`bench <rung>`** runs one rung of [the benchmark ladder](https://github.com/firejune/rigc/blob/main/docs/LADDER.md): validate
|
|
376
470
|
under `--profile spine`, then diff against that rung's reference export, and with
|
|
377
471
|
`--frames` the `check` table as well. Unlike the three above it is a **finish
|
|
@@ -642,11 +736,17 @@ against `mike2`). Both of those *build* as animation names and are refused as sk
|
|
|
642
736
|
names, and the two refusals say which is which.
|
|
643
737
|
|
|
644
738
|
**R12 — A placeholder that more than one skin fills gets a per-skin attachment
|
|
645
|
-
`name
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
is
|
|
739
|
+
`name`, and the `default` skin may not be one of those skins.** rigc writes
|
|
740
|
+
`"name": "<skin>/<placeholder>"` on each of those entries and restates `path`
|
|
741
|
+
beside it so the texture still resolves where it did; you do not author that and
|
|
742
|
+
there is nothing to do about it, but it is visible in the emitted file, so §3.4.2
|
|
743
|
+
says what it is and why. What you *do* author is where the shared art lives: a
|
|
744
|
+
placeholder the `default` skin shares with a named skin is a **compile error**,
|
|
745
|
+
because the Spine editor has no representation for it in either spelling (#567,
|
|
746
|
+
measured on 4.3.26 — named, the export re-keys it and the default skin draws
|
|
747
|
+
nothing; unnamed, the import is refused). Put the shared entry in a named skin —
|
|
748
|
+
`base` — and every filler is a named skin. A placeholder only one skin fills is
|
|
749
|
+
emitted exactly as before, in the default skin or anywhere else.
|
|
650
750
|
|
|
651
751
|
---
|
|
652
752
|
|
|
@@ -667,8 +767,8 @@ is recorded in `bench/runs/README.md`, *What a run may read*.)
|
|
|
667
767
|
|
|
668
768
|
| Field | Spine meaning | Default |
|
|
669
769
|
| --- | --- | --- |
|
|
670
|
-
| `x`, `y` | setup-pose bounding box origin | `0` |
|
|
671
|
-
| `width`, `height` | setup-pose bounding box size | falls back to the manifest's crop; **with neither
|
|
770
|
+
| `x`, `y` | setup-pose bounding box origin | `0` — and refused outright beside a stated absence, below |
|
|
771
|
+
| `width`, `height` | setup-pose bounding box size, **or both `null` for "this skeleton declares no stage"** | falls back to the manifest's crop; **with neither the number nor the `null`, the compile fails** |
|
|
672
772
|
| `fps` | nonessential editor hint | `SkeletonData.fps` stays 30 |
|
|
673
773
|
| `referenceScale` | 4.2+ physics/scale reference | parser default 100 |
|
|
674
774
|
| `images` | where the editor's import looks for the part PNGs, as a path from the skeleton file | **written for you**: under `--copy-images` the `--out` directory itself, spelled `../<its basename>/` (a literal `./` is dropped by the editor on import; a named directory is kept and every part is found — measured on 4.3.23); otherwise the relative path from `--out` to the one directory the spec names every part PNG in (the rig's images directory, or the manifest's plates). A declared value is carried through verbatim — and overridden by `--copy-images`, which moved the parts. Parts spread over several directories have no single true path, so nothing is written (issue #370) |
|
|
@@ -684,6 +784,40 @@ links, not the editor that will open the file, and the warning is harmless
|
|
|
684
784
|
`width`/`height` are what `A14` and `A19` measure against, so a guessed stage is a
|
|
685
785
|
gate measuring against a number nobody wrote down.
|
|
686
786
|
|
|
787
|
+
⭐ **A skeleton may declare no stage, and saying so is not the same as saying
|
|
788
|
+
nothing** (issue #578). Write the pair as `null`:
|
|
789
|
+
|
|
790
|
+
```json
|
|
791
|
+
"skeleton": { "width": null, "height": null }
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
and the emitted header carries **none** of `x`/`y`/`width`/`height` — which is
|
|
795
|
+
what an export of a skeleton whose stage was never set looks like, and the shape a
|
|
796
|
+
transcriber of one now has something to write. `null` is this spec's spelling for
|
|
797
|
+
a stated absence wherever it has one (`slots[].attachment` is `null` for "show
|
|
798
|
+
nothing"), so nothing new is introduced here but a third value of a field that
|
|
799
|
+
already existed.
|
|
800
|
+
|
|
801
|
+
Three readings stay apart, and the middle one is the point of the other two:
|
|
802
|
+
|
|
803
|
+
| What the spec says | What happens |
|
|
804
|
+
| --- | --- |
|
|
805
|
+
| a number for each | the stage, as before; a manifest `crop` is the fallback |
|
|
806
|
+
| `"width": null, "height": null` | builds, and emits a header with no stage at all — and this **beats** a manifest's `crop`, because a rig spec is where a claim about the skeleton is made |
|
|
807
|
+
| neither | **refused**, exactly as before: `no stage size: …` |
|
|
808
|
+
| one `null`, one number | refused — a stage has both extents or neither, and which half was meant is not derivable |
|
|
809
|
+
| the pair `null` **and** an `x` or `y` | refused — an origin for a box that is not there |
|
|
810
|
+
|
|
811
|
+
⚠️ A stage-less skeleton is **unmeasured, not certified**: `A14_NO_FULL_FRAME_MESH`
|
|
812
|
+
reports **SKIP** on one, because there is no full frame for a mesh to span. And
|
|
813
|
+
`rigc diff` reports it — `skeleton.stage_present` and `skeleton.stage_box`, in the
|
|
814
|
+
header block at the top of the report — so a stage somebody invented now reads
|
|
815
|
+
below 1.000 against a source that has none. Both are reported and gate nothing, for
|
|
816
|
+
the reason every reported measure is: no reading of the rendered frames could have
|
|
817
|
+
decided a setup-pose bounding box. The measure inventory that says so lives in
|
|
818
|
+
[BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md), which
|
|
819
|
+
is repository material and not in the published package.
|
|
820
|
+
|
|
687
821
|
### 3.2 `bones` — Spine's bone list
|
|
688
822
|
|
|
689
823
|
`parent` is resolved **by name against bones already declared**, exactly as the
|
|
@@ -714,17 +848,38 @@ and the inheritance silently falls back to Normal — assertion `A02` refuses it
|
|
|
714
848
|
| --- | --- | --- |
|
|
715
849
|
| `name` | required, unique | — |
|
|
716
850
|
| `bone` | required; must be a bone this rig declares | — |
|
|
717
|
-
| `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) |
|
|
851
|
+
| `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) — **except** on a slot nothing fills, where it can only be `null` and may be left out |
|
|
718
852
|
| `color` | `rrggbbaa` tint | opaque white |
|
|
719
853
|
| `dark` | two-colour tint, `rrggbb` | — (🚫 `A12` under `spine-html`) |
|
|
720
854
|
| `blend` | `normal` · `additive` · `multiply` · `screen` | `normal` |
|
|
721
855
|
|
|
722
|
-
|
|
723
|
-
entry, no manifest part —
|
|
724
|
-
|
|
725
|
-
slot
|
|
726
|
-
|
|
727
|
-
|
|
856
|
+
✅ **Every slot you declare is emitted, in this order.** A slot nothing fills — no
|
|
857
|
+
skin entry, no manifest part — is emitted **empty**: `name` and `bone`, and no
|
|
858
|
+
`attachment` key at all, which is how the format spells "shows nothing"
|
|
859
|
+
(`SkeletonJson`'s slot reader takes `attachment` with a `null` default; 34 of the 52 slots of the
|
|
860
|
+
official `spineboy-pro` export omit the key, though those are slots a skin fills
|
|
861
|
+
whose setup pose shows nothing). So the emitted slots array
|
|
862
|
+
**is** the rig's slot table, and `A26_SLOT_DRAW_ORDER` checks it in both directions:
|
|
863
|
+
nothing out of order, and nothing missing.
|
|
864
|
+
|
|
865
|
+
Declaring a slot no cut fills is therefore normal — it fixes where that slot sits
|
|
866
|
+
whether or not this cut has art for it — and transcribing a foreign skeleton that
|
|
867
|
+
carries an empty slot reproduces it exactly.
|
|
868
|
+
|
|
869
|
+
⚠️ **This changed with issue #575.** Such a slot used to be *dropped*, with no message, and
|
|
870
|
+
the gate allowed the emitted array to be any subsequence of the rig's. What it cost
|
|
871
|
+
is the index: every slot below the dropped one moved up one place, which is what a
|
|
872
|
+
`drawOrder` key's offsets are counted against and what an index-keyed consumer
|
|
873
|
+
splits on. Two production exports declaring 53 and 61 slots built green at 51 and 57
|
|
874
|
+
and read 0.962 and 0.934 under `diff` against the file they were transcribed from.
|
|
875
|
+
If you have a rig that leaned on the drop, the emitted array simply grows; nothing
|
|
876
|
+
else about it moves.
|
|
877
|
+
|
|
878
|
+
🚫 **Naming an attachment on a slot nothing fills is refused by name**: `the setup
|
|
879
|
+
pose shows attachment "x" on slot "y", which no skin and no manifest part fills`.
|
|
880
|
+
That is the half-finished wiring-up the old silence hid — the slot is emitted empty
|
|
881
|
+
and the name resolves to nothing, so either give the slot an attachment or state the
|
|
882
|
+
setup pose as `null`.
|
|
728
883
|
|
|
729
884
|
### 3.4 `skins` — placeholder → attachment maps
|
|
730
885
|
|
|
@@ -745,7 +900,7 @@ the default `type`:
|
|
|
745
900
|
| `type` | `"region"`, or omit |
|
|
746
901
|
| `image` | **rigc extension.** A PNG relative to the rig's `images` directory; rigc measures it (R5) |
|
|
747
902
|
| `width`, `height` | required by the format — give them, or give an `image` |
|
|
748
|
-
| `path` | the atlas region to resolve; defaults to the attachment's own name. rigc sets it for you when the PNG basename differs from the placeholder, and whenever it composes a `name` because more than one skin fills this placeholder (§3.4.2) |
|
|
903
|
+
| `path` | the atlas region to resolve; defaults to the attachment's own name. rigc sets it for you when the PNG basename differs from the placeholder, and whenever it composes a `name` because more than one skin fills this placeholder (§3.4.2). **One rule, both kinds** — see the note under *Mesh attachment* |
|
|
749
904
|
| `x`, `y` | offset from the bone, in the bone's local space |
|
|
750
905
|
| `rotation` | degrees; cancels a rotated bone for a plate authored screen-upright |
|
|
751
906
|
| `scaleX`, `scaleY`, `color` | as Spine |
|
|
@@ -754,7 +909,19 @@ the default `type`:
|
|
|
754
909
|
either authored geometry (`uvs` + `triangles` + geometry) **or** a `generator`,
|
|
755
910
|
never both. `hull`, `edges`, `width` and `height` may be stated; whichever is
|
|
756
911
|
omitted, rigc derives — `hull` and `edges` from the triangles, the size from the
|
|
757
|
-
PNG — and the rules are a few paragraphs down.
|
|
912
|
+
PNG — and the rules are a few paragraphs down. `type`, `image`, `path` and `color`
|
|
913
|
+
mean exactly what they mean on a region.
|
|
914
|
+
|
|
915
|
+
🔑 **`path` is one rule for both kinds.** A mesh derives it from `image` the way a
|
|
916
|
+
region does: stated wins, otherwise the PNG's basename when that differs from the
|
|
917
|
+
placeholder, otherwise nothing. The parser reads `path` off both with the same line
|
|
918
|
+
(`getValue(map, "path", name)`, `SkeletonJson.ts:541` and `:570`), and `path`
|
|
919
|
+
defaults to the attachment's **name** rather than to the placeholder — so a mesh
|
|
920
|
+
with `image: hair_short.png` under a placeholder called `hair` resolves the region
|
|
921
|
+
`hair`, which no atlas has. Until
|
|
922
|
+
[#577](https://github.com/firejune/rigc/issues/577) a region derived it and an
|
|
923
|
+
authored mesh did not, so that rig **built** and then failed
|
|
924
|
+
`A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: hair`.
|
|
758
925
|
|
|
759
926
|
Geometry comes in one of two fields:
|
|
760
927
|
|
|
@@ -1430,20 +1597,80 @@ names you already gave it:
|
|
|
1430
1597
|
|
|
1431
1598
|
```json
|
|
1432
1599
|
"skins": {
|
|
1433
|
-
"default": { "
|
|
1600
|
+
"default": { "block": { "block": { "image": "block.png" } } },
|
|
1601
|
+
"base": { "patch": { "patch": { "image": "patch_a.png" } } },
|
|
1434
1602
|
"zulu": { "patch": { "patch": { "image": "patch_a.png", "x": 4 } } }
|
|
1435
1603
|
}
|
|
1436
1604
|
```
|
|
1437
1605
|
|
|
1438
|
-
emits
|
|
1606
|
+
emits, for slot `patch`
|
|
1439
1607
|
|
|
1440
1608
|
```json
|
|
1441
|
-
{ "name": "
|
|
1442
|
-
{ "name": "zulu/patch",
|
|
1609
|
+
{ "name": "base/patch", "path": "patch", "width": 64, "height": 64 }
|
|
1610
|
+
{ "name": "zulu/patch", "path": "patch", "width": 64, "height": 64, "x": 4 }
|
|
1443
1611
|
```
|
|
1444
1612
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1613
|
+
🚨 **Every skin that shares a placeholder has to be a named one — the default
|
|
1614
|
+
skin may not be among them, and rigc refuses the rig if it is.** That is not a
|
|
1615
|
+
style rule; it is the editor's model, and two round trips through Spine
|
|
1616
|
+
**4.3.26** established it by ruling out both of the only two spellings there are
|
|
1617
|
+
([#567](https://github.com/firejune/rigc/issues/567)):
|
|
1618
|
+
|
|
1619
|
+
- **Give the default skin's entry a name of its own** (`"name": "default/patch"`)
|
|
1620
|
+
and the import succeeds — then the export comes back with that name as the
|
|
1621
|
+
JSON **key** (`"default/patch": { … }`, the `name` field gone), because the
|
|
1622
|
+
editor's default skin holds no skin placeholders: an attachment there hangs on
|
|
1623
|
+
the slot and is known by its name alone. The slot's setup `attachment: "patch"`
|
|
1624
|
+
now names a key the default skin does not have, so the default skin **draws
|
|
1625
|
+
nothing** — `diff` read `attachments.names 3/5`, `check` read 98.52 mean MAE
|
|
1626
|
+
with `drewSlots: 0` on that chain, and `validate --profile spine` stayed green
|
|
1627
|
+
throughout.
|
|
1628
|
+
- **Leave it as its placeholder** (no `name`, which is the obvious repair) and
|
|
1629
|
+
the editor **refuses the import**:
|
|
1630
|
+
|
|
1631
|
+
```
|
|
1632
|
+
ERROR: Unable to import skeleton.
|
|
1633
|
+
Cause: [error] Error reading attachment: mike/patch (nSX)
|
|
1634
|
+
Cause: [error] Multiple attachments have the same name:
|
|
1635
|
+
patch
|
|
1636
|
+
patch
|
|
1637
|
+
```
|
|
1638
|
+
|
|
1639
|
+
In one slot, a default-skin attachment name and a named skin's placeholder name
|
|
1640
|
+
are the same namespace, and both are `patch`.
|
|
1641
|
+
|
|
1642
|
+
⇒ **The rule: move the shared art into a named skin.** Call it `base`. Every
|
|
1643
|
+
filler of that placeholder is then a named skin, rigc composes all of them, and
|
|
1644
|
+
the names are unique within the slot — which is all
|
|
1645
|
+
[#541](https://github.com/firejune/rigc/issues/541) needed: `base/patch`,
|
|
1646
|
+
`zulu/patch` and `mike/patch` are three names. That shape is the one the editor
|
|
1647
|
+
does hold: the same three fillers in named skins imported, exported and measured
|
|
1648
|
+
**0.00 mean MAE** with names and paths intact.
|
|
1649
|
+
|
|
1650
|
+
📎 **The earlier reading, kept because it was reasonable and wrong.** Between the
|
|
1651
|
+
two trips this guide said *compose off the default skin only* — keep the default
|
|
1652
|
+
skin's entry as its placeholder and name the others. Trip 7 supported it and trip
|
|
1653
|
+
8 refuted it: that is the spelling the editor refuses at the door. There is no
|
|
1654
|
+
third spelling, which is why this is a refusal rather than a naming scheme.
|
|
1655
|
+
|
|
1656
|
+
What to know about it, and nothing to author:
|
|
1657
|
+
|
|
1658
|
+
- **The renderer accepts this shape too, and that is measured rather than
|
|
1659
|
+
assumed.** `spine-html@0.4.1` resolves a part in two steps and neither one
|
|
1660
|
+
reads a placeholder or an attachment name: `DomTexture.js:78,102` builds its
|
|
1661
|
+
image map with `put(atlasRegion.name, …)` over every region of the atlas, and
|
|
1662
|
+
`SpineHtmlRenderer.js:172` reads it back as
|
|
1663
|
+
`const regionImage = region && this.regionImages.get(region.name)`, where
|
|
1664
|
+
`region` came off the attachment — which `AtlasAttachmentLoader` resolved
|
|
1665
|
+
through `path`. Every published version of that renderer keys the same way.
|
|
1666
|
+
⚠️ `A08` used to carry a `--profile spine-html` clause requiring a
|
|
1667
|
+
placeholder to be spelled exactly like the region it resolves to, which made
|
|
1668
|
+
this shape and a green `spine-html` **mutually exclusive** from
|
|
1669
|
+
[#567](https://github.com/firejune/rigc/issues/567) onwards; the first
|
|
1670
|
+
production rig with named skins hit it three times. That clause is retired —
|
|
1671
|
+
restated as the join the renderer actually performs it was a tautology over
|
|
1672
|
+
the resolve check beside it
|
|
1673
|
+
([#574](https://github.com/firejune/rigc/issues/574)).
|
|
1447
1674
|
- **`path` is restated, and it has to be.** `path` defaults to the attachment's
|
|
1448
1675
|
**name**, not to its placeholder, so an entry given a name and no path would
|
|
1449
1676
|
resolve its texture at `zulu/patch` and find no such region. `A00_ROUNDTRIP_PARSE`
|
|
@@ -1454,6 +1681,9 @@ Three things to know about it and nothing to author:
|
|
|
1454
1681
|
- **A composed name that collides is a compile error, not a surprise.** If some
|
|
1455
1682
|
other placeholder in the same slot is literally called `zulu/patch`, rigc refuses
|
|
1456
1683
|
and names both sites rather than emitting two attachments with one name again.
|
|
1684
|
+
The walk covers every *uncontested* entry's plain name too, the default skin's
|
|
1685
|
+
included — a name that composed nothing can still be the one another skin
|
|
1686
|
+
composes.
|
|
1457
1687
|
(`/` is the separator because it appears in **0** of the 160 placeholder names and
|
|
1458
1688
|
159 atlas region names in `examples/` and `gallery/`, where `-` appears in 85 and
|
|
1459
1689
|
`_` in 37.)
|
|
@@ -1472,6 +1702,15 @@ bounding box in slot `head-bb`, and reuses `hoverglow-small` across eight slots.
|
|
|
1472
1702
|
a name shared between slots is normal and rigc leaves it alone; what #541 refused
|
|
1473
1703
|
was one slot holding two.
|
|
1474
1704
|
|
|
1705
|
+
🚨 **Once a rig has named skins, no instrument here can see them until you say
|
|
1706
|
+
which one** ([#571](https://github.com/firejune/rigc/issues/571)). `render` and
|
|
1707
|
+
`check` set no skin unless told to, so every slot resolves through the *default*
|
|
1708
|
+
skin alone and the art you just moved into `base`, `zulu` and `mike` draws
|
|
1709
|
+
nothing at all. `check` then compares blank against blank and reports a perfect
|
|
1710
|
+
`0.0000` — about the very placeholder this subsection is about. Pass
|
|
1711
|
+
`--skin <name>` to both, once per skin (**§9**); `tools/editor_roundtrip.ts`
|
|
1712
|
+
loops over every skin the build declares for the same reason.
|
|
1713
|
+
|
|
1475
1714
|
### 3.5 `constraints` — 4.3's single typed array
|
|
1476
1715
|
|
|
1477
1716
|
Spine 4.3 folds every constraint into one `constraints` array with a `type`
|
|
@@ -2533,6 +2772,18 @@ here was measured off a real rig. Copy the shape, not the values.
|
|
|
2533
2772
|
runtime. **Stating a flag on every key still overrides the rig** — the format
|
|
2534
2773
|
keys them per key on purpose, a bend that flips partway through is a real thing
|
|
2535
2774
|
to write, and it is what the editor's own export does.
|
|
2775
|
+
- 🔁 **A spec that came out of `ingest` (§0.3) states all three on every key, and
|
|
2776
|
+
that is not noise.** The stamping above reads a silent key as *"the rig's
|
|
2777
|
+
value"*, which is right for a spec somebody wrote; in a **decompiled** spec a
|
|
2778
|
+
silent key means *"the parser's default"*, because that is what the export the
|
|
2779
|
+
spec came from actually plays. The two readings differ exactly when the rig
|
|
2780
|
+
declares a non-default flag and the export's keys omit it — measured: an ik
|
|
2781
|
+
timeline keying only `mix` under a constraint declaring `bendPositive: false`
|
|
2782
|
+
rebuilds with `bendPositive: false` on every key if the flag is left silent,
|
|
2783
|
+
and with `true` — what the source plays — when `ingest` writes it out. So
|
|
2784
|
+
`ingest` restates every field any key of a track names, at the parser's default
|
|
2785
|
+
where the source omits one, and records a `CONSTRAINT_KEY_RESTATED` finding. On
|
|
2786
|
+
rigc's own output the two agree and the round trip is byte-identical.
|
|
2536
2787
|
- `mix` outside `0..1` is a compile error: `IkConstraintPose.mix` is documented as
|
|
2537
2788
|
a percentage. A **transform** mix is documented *unbounded*, which is why §4.10
|
|
2538
2789
|
has no such rule — the asymmetry is the runtime's, not ours.
|
|
@@ -2653,7 +2904,7 @@ Per key:
|
|
|
2653
2904
|
|
|
2654
2905
|
| Field | Meaning |
|
|
2655
2906
|
| --- | --- |
|
|
2656
|
-
| `vertices` | the run: `x, y`
|
|
2907
|
+
| `vertices` | the run: consecutive numbers copied into the deform array from the start index on, `x, y` per vertex or influence — an **odd** count is legal and ends on a lone x (§ the 📌 below). **Absent** = back to the setup pose |
|
|
2657
2908
|
| `transform` | the run stated as a **model** instead, evaluated over the attachment's own geometry — §4.11.1. Never with `vertices`, `fromVertex` or `offset` |
|
|
2658
2909
|
| `fromVertex` | which VERTEX the run starts at — rigc translates it |
|
|
2659
2910
|
| `offset` | the same start as a raw index into the deform array. Never with `fromVertex` |
|
|
@@ -2703,14 +2954,12 @@ geometry to the next one's. So a named `ease` behaves as it does anywhere, and a
|
|
|
2703
2954
|
raw `curve` is 4 numbers whose value axis runs 0..1, not the range of your vertex
|
|
2704
2955
|
offsets.
|
|
2705
2956
|
|
|
2706
|
-
rigc refuses
|
|
2707
|
-
|
|
2957
|
+
rigc refuses these, and the first is the quietest defect in the whole animation
|
|
2958
|
+
half of the format:
|
|
2708
2959
|
|
|
2709
2960
|
| You wrote | You get |
|
|
2710
2961
|
| --- | --- |
|
|
2711
2962
|
| a run that ends past the array | `the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long (4 vertices)` |
|
|
2712
|
-
| an odd `offset` | `offset 3 is odd. The deform array is x, y pairs, so an odd start puts every x of this run on a y` |
|
|
2713
|
-
| an odd number of `vertices` | `"vertices" holds 3 numbers; the deform array is x, y PAIRS` |
|
|
2714
2963
|
| `fromVertex` on a multi-bone vertex | `"fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` |
|
|
2715
2964
|
| an attachment that is not there | `slot "flat" in skin "default" has no attachment "flatt" (it has: flat)` |
|
|
2716
2965
|
| a deform on a region attachment | `a deform timeline keys the vertices of an attachment, and this one is a "region"` |
|
|
@@ -2728,15 +2977,30 @@ tail and deforms the rest of the mesh correctly, which looks nearly right, and
|
|
|
2728
2977
|
emitted file, measuring the array's length from the attachment rather than assuming
|
|
2729
2978
|
an encoding.
|
|
2730
2979
|
|
|
2731
|
-
📌 **
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
`
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2980
|
+
📌 **A run is not required to be an even number of numbers, and it is not required
|
|
2981
|
+
to start on an even index.** Two rows stood here saying otherwise until issue
|
|
2982
|
+
#576. Nothing in either reader aligns a run to a pair: `SkeletonJson` does
|
|
2983
|
+
`Utils.arrayCopy(vertices, 0, deform, offset, vertices.length)` — a raw copy, at
|
|
2984
|
+
the raw index the key gives — and `SkeletonBinary` reads a count and a start and
|
|
2985
|
+
fills `for (let v = start; v < end; v++)`. So a run may begin and end mid-pair,
|
|
2986
|
+
which is exactly what an editor's trimmed delta run looks like: `spineboy-pro`'s
|
|
2987
|
+
`hoverboard-board` key starts at 1 and carries 147 numbers of a 148-long array,
|
|
2988
|
+
the whole delta minus one leading zero.
|
|
2989
|
+
|
|
2990
|
+
⭐ **An odd run's last number is an x with no y beside it, and that is a
|
|
2991
|
+
statement, not an accident.** It moves that vertex in x and leaves its y at the
|
|
2992
|
+
setup value, because the parser copies your numbers and touches nothing else. It
|
|
2993
|
+
is also the reason the rule could not stay as an authoring convenience: padding a
|
|
2994
|
+
`0` to make the run even *changes what plays* wherever that setup y is non-zero,
|
|
2995
|
+
so there is no second spelling of such a key — refusing it would mean no
|
|
2996
|
+
transcription of that file can be written in this spec at all. `A35` reached the
|
|
2997
|
+
same conclusion from the other side in issue #262, and between that fix and this
|
|
2998
|
+
one the two halves of rigc disagreed about what the format holds.
|
|
2999
|
+
|
|
3000
|
+
⚠️ What you lose with those rows is a typo filter: `offset: 3` written where
|
|
3001
|
+
`fromVertex: 3` was meant now compiles. It always half-did — `offset: 4` meant as
|
|
3002
|
+
vertex 4 lands on vertex 2 and was never refused — so read the field name twice.
|
|
3003
|
+
`offset` is an index into the deform array; `fromVertex` is a vertex.
|
|
2740
3004
|
|
|
2741
3005
|
🖼️ **Worked examples, and they use a deform for four different things** — all
|
|
2742
3006
|
four are repository material rather than part of the published package, so the
|
|
@@ -3456,9 +3720,13 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
3456
3720
|
| `bone "X" names parent "Y", which is not declared before it` | move `Y` earlier in `bones` |
|
|
3457
3721
|
| `two bones are called "X"` | bone names are the join key; rename one |
|
|
3458
3722
|
| `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
|
|
3459
|
-
| `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 — pick one file and declare it there |
|
|
3723
|
+
| `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 — pick one file and declare it there. A slot **nothing** fills is exempt: its setup pose can only be "show nothing" and is not asked for |
|
|
3724
|
+
| `the setup pose shows attachment "A" on slot "X", which no skin and no manifest part fills` | §3.3 — the slot is emitted empty, so `A` resolves to nothing. Give the slot an attachment (a skin entry or a manifest part), or state the setup pose as `null` |
|
|
3460
3725
|
| `a region needs width and height — give them, or give an "image" and rigc will measure the PNG` | add `image`, or both sizes |
|
|
3461
3726
|
| `a mesh needs width and height — give them, or give an "image" and rigc will measure the PNG` | §3.4 — the same rule for a mesh |
|
|
3727
|
+
| `"type" is null, which is not a name. An attachment's type is one of region, mesh, linkedmesh, … or the key is absent and reads as "region"` | §6 — **remove the key**. Absent is the format's own default; present-and-null matches no parser case and the attachment is dropped in silence |
|
|
3728
|
+
| `attachment type "X" is not one of the 7 the Spine 4.3 format defines (…)` | §6 — a name the format does not have. Not a deferral, and not something rigc will grow: fix the spelling (`sequence` is a key on a region or a mesh, not a type) |
|
|
3729
|
+
| `this attachment is a "linkedmesh"` / `"point"` … `rigc does not emit it yet` | §6 — a construct the format has and rigc does not write. The message says what it would carry; SPEC_COVERAGE part 1-6 is the row it reads from |
|
|
3462
3730
|
| `hull N disagrees with the triangles, whose outline has K vertices (0 → …)` | §3.4 — delete `hull`, or state K |
|
|
3463
3731
|
| `hull vertices must come first; vertex i is on the boundary and vertex j is not. The triangles' outline runs …: list those K vertices first, in that order, then the M interior vertices` | §3.4 — renumber the vertices: the printed walk first, then the interior |
|
|
3464
3732
|
| `hull vertices must trace the outline in order; the triangles' outline runs …, so vertex a has to follow vertex b in the list, and vertex c does` | §3.4 — renumber along the printed walk |
|
|
@@ -3488,7 +3756,9 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
3488
3756
|
| `animation "A" group "G" P (t=…): derive <kind> projects onto "scaleX" and member "M" states depth −z` | §4.5.1 — a foreshortening needs the part in front of the axis; a part behind it takes the displacement projection |
|
|
3489
3757
|
| `animation "A" group "G" P (t=…): derive <kind> states carried=… on a "scalex" track` | §4.5.1 — the foreshortening reads no depth difference, so `carried` belongs on the displacement track |
|
|
3490
3758
|
| `animation "A" group "G" P (t=…): derive <kind> turns member "M" … past its own edge` | §4.5.1 — `cos(α − t) ≤ 0` would mirror the drawing; the turn is past what this construction carries (FACE §8) |
|
|
3491
|
-
| `no stage size: give the rig spec a \`skeleton.width\`/\`skeleton.height\`` | §3.1 |
|
|
3759
|
+
| `no stage size: give the rig spec a \`skeleton.width\`/\`skeleton.height\`` | §3.1 — or state `"width": null, "height": null` if the skeleton you are transcribing declares no stage |
|
|
3760
|
+
| `"skeleton" states width: null and a height of N` / `states height: null and a width of N` | §3.1 — a stage has both extents or neither |
|
|
3761
|
+
| `"skeleton" declares no stage (width: null, height: null) and still states x` | §3.1 — an origin for a box that is not there |
|
|
3492
3762
|
| `N mesh slot(s) emitted but the rig "X" allows 0 — a mesh rigc GENERATED counts against \`invariants.meshSlots\`…` | §3.4 / §3.7 — a rig that invokes a mesh generator declares the budget; undeclared is zero. Add `"invariants": { "meshSlots": N, "meshTriangles": M }` |
|
|
3493
3763
|
| `drawOrder at t=…: slot "X" is not one this rig emits` / `is offset twice in one key` / `puts it at N, outside the … emitted slots` | §4.7 |
|
|
3494
3764
|
| `events at t=…: event "X" is not declared in the rig spec's "events" block` | declare it in the rig spec (§3.6), or fix the name |
|
|
@@ -3503,7 +3773,6 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
3503
3773
|
| `ik constraint "X": key 0 names "softness" and key 1 (t=…) does not` | §4.9 — every key is read with its own default, so state the field on every key or on none |
|
|
3504
3774
|
| `ik constraint "X" (t=…): mix is 1.5, outside 0..1` | §4.9 — an IK mix is a percentage; a transform mix is unbounded |
|
|
3505
3775
|
| `deform …: the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long` | §4.11 — shorten the run or move its start; the parser would drop the tail in silence |
|
|
3506
|
-
| `deform … (t=…): offset 3 is odd` | §4.11 — in **this spec** a run starts on an even index, or names its vertex with `fromVertex`. Not a validity rule; the runtime allows either (issue #262) |
|
|
3507
3776
|
| `deform …: "fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` | §4.11 — key the control bone, or write bind-space pairs and start with `offset` |
|
|
3508
3777
|
| `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
|
|
3509
3778
|
| `deform … (t=…): the key carries both a "transform" and a "vertices" run` | §4.11.1 — a model and a table are two answers to one question; drop one |
|
|
@@ -3529,7 +3798,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
3529
3798
|
| `animation "A": "position" is a path constraint timeline, and this track names no constraint` | §4.12 — put the name in `"path"` |
|
|
3530
3799
|
| `N pair(s) of animation names have no one order: … "turn" / "Turn" (case) — they are one name in two cases, and which of them the editor puts first is not measured; rename one of them so they differ by more than letter case` | **R10** — rename until no pair is left. The kind in brackets says which of the editor comparator's four UNMEASURED choices decides the pair: `case` (a pure case tie), `number` (one number written two ways, or a run of digits against a word) or `separator` (make the first character that differs a letter or a digit). rigc keys `animations` in the editor's own comparator — natural and case-insensitive ([#539](https://github.com/firejune/rigc/issues/539), [#543](https://github.com/firejune/rigc/issues/543)) — so a pair that comparator settles is emitted rather than refused, and only the four choices nobody has measured are a compile error; on those, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
|
|
3531
3800
|
| `N pair(s) of skin names have no one order: … "Zulu" / "mike" (case) — folded to one case "Zulu" and "mike" order the other way round, so whether the editor folds SKIN names decides this pair` | **R11** — rename until no pair is left. The same shape as the row above with a **wider** family: #539 measured the editor's comparator for animation names and thereby ruled codepoint out, and nothing has ruled anything out for skin names, so a pair the candidates could disagree about is refused even where the animation rule would emit it. `Zulu`/`mike` and `mike10`/`mike2` build as animation names and are refused as skin names ([#541](https://github.com/firejune/rigc/issues/541)) |
|
|
3532
|
-
| `
|
|
3801
|
+
| `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that … Move the default skin's entry for this slot into a named skin — call it "base"` | **R12** — do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and §3.4.2 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
|
|
3802
|
+
| `N attachment name collision(s): a placeholder that more than one skin fills is emitted with the name "<skin>/<placeholder>" … slot "patch": skin "base" placeholder "zulu/patch" and skin "zulu" placeholder "patch" would both be named "zulu/patch"` | **R12** — rename the placeholder or the skin. rigc composes an attachment name for every placeholder more than one skin fills (§3.4.2), and this fires when a composed name is one another entry in the same slot already answers to — including a plain name in the default skin, which composed nothing. Both sites are named; either rename ends it |
|
|
3533
3803
|
|
|
3534
3804
|
### 5.2 Assertions — the gate
|
|
3535
3805
|
|
|
@@ -3555,9 +3825,31 @@ The report prints one line per assertion:
|
|
|
3555
3825
|
- **FAIL** — the detail names the object, the value found and the value required.
|
|
3556
3826
|
That detail is the instruction; the table below says which file to change.
|
|
3557
3827
|
|
|
3828
|
+
**Every assertion leaves exactly one row, on every run.** The four kinds partition
|
|
3829
|
+
the registry, so the rows you can see are the whole of what was asked — there is
|
|
3830
|
+
no fifth state in which a rule quietly did not come up. The last line of the
|
|
3831
|
+
report states that partition, and every figure in it is a count of *assertions*
|
|
3832
|
+
you can reproduce by counting rows:
|
|
3833
|
+
|
|
3834
|
+
```
|
|
3835
|
+
.. <N> assertions: <M> measured (<P> passed, <F> failed), <S> skipped, <X> not in profile "<profile>"
|
|
3836
|
+
```
|
|
3837
|
+
|
|
3838
|
+
`<M>` is `<P> + <F>`, and `<N>` is all four added together. ⚠️ `<F>` counts
|
|
3839
|
+
assertions and not `FAIL` lines: one assertion that finds six wrong vertices
|
|
3840
|
+
prints six rows and is one failure here.
|
|
3841
|
+
|
|
3842
|
+
🚨 **When `A00_ROUNDTRIP_PARSE` fails, read the report as a report about A00 and
|
|
3843
|
+
nothing else.** Most of the rules read the skeleton or the atlas that A00 loads,
|
|
3844
|
+
and with no parse there is nothing for them to look at — so they report `SKIP`
|
|
3845
|
+
naming that, *the round trip did not produce a skeleton to measure* or *…an atlas
|
|
3846
|
+
to measure*, and the summary's `<S>` goes up while `<M>` collapses. A run in that
|
|
3847
|
+
state is not a rig that nearly passed; it is a rig that was measured on one rule.
|
|
3848
|
+
Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
|
|
3849
|
+
|
|
3558
3850
|
| Assertion | Profile | What tripped it, and where to fix it |
|
|
3559
3851
|
| --- | --- | --- |
|
|
3560
|
-
| `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Everything else in the report is downstream of this one — fix it first |
|
|
3852
|
+
| `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Everything else in the report is downstream of this one — fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked ([#568](https://github.com/firejune/rigc/issues/568)) |
|
|
3561
3853
|
| `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS` | both | a 4.1/4.2-shaped `ik`/`transform`/`path`/`physics`/`slider` array. rigc emits the 4.3 `constraints` array, so this normally means hand-edited JSON |
|
|
3562
3854
|
| `A02_NO_BONE_TRANSFORM_KEY` | both | a bone uses 4.2's `transform`; rename it `inherit` in the rig spec |
|
|
3563
3855
|
| `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height` |
|
|
@@ -3565,14 +3857,14 @@ The report prints one line per assertion:
|
|
|
3565
3857
|
| `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel** |
|
|
3566
3858
|
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk. Under `spine-html` also: `pma`, rotation, and a page that is neither **one part covering it exactly** (the unpacked convention) nor a **tiling** — a page whose regions all sit inside it and none of which overlap ([#266](https://github.com/firejune/rigc/issues/266)). A packed atlas therefore gates under this profile; what the message names is the region that runs off its page, or the pair that shares texels |
|
|
3567
3859
|
| `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file |
|
|
3568
|
-
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both
|
|
3860
|
+
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | an atlas region name carrying stray whitespace. rigc writes the atlas, so this means a hand-edited file, and `A07` names the same line with its line number. ⚠️ A `path` that resolves to **no** region never reaches here: `AtlasAttachmentLoader` throws `Region not found in atlas: <path> (attachment: <name>)` while the skeleton is still loading, so it arrives as `A00_ROUNDTRIP_PARSE`. There is no `spine-html` clause here any more — a placeholder is free to differ from the region its `path` names ([#574](https://github.com/firejune/rigc/issues/574)) |
|
|
3569
3861
|
| `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
|
|
3570
3862
|
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale |
|
|
3571
3863
|
| `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
|
|
3572
3864
|
| `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
|
|
3573
3865
|
| `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7, issue #274) |
|
|
3574
|
-
| `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip |
|
|
3575
|
-
| `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone |
|
|
3866
|
+
| `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip. **SKIP** when the skeleton declares no stage (§3.1): there is no full frame to span, and *unmeasured* must not print the same green as *measured and clear* |
|
|
3867
|
+
| `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. **SKIP** when there is no `idle` animation, or when the one there is carries no bone timeline — a rule whose subject does not exist is unmeasured and not satisfied ([#568](https://github.com/firejune/rigc/issues/568)) |
|
|
3576
3868
|
| `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
|
|
3577
3869
|
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out` |
|
|
3578
3870
|
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
|
|
@@ -3583,7 +3875,7 @@ The report prints one line per assertion:
|
|
|
3583
3875
|
| `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, is muted by `mix: 0`, has `mass: 0`, has `strength: 0`, or has `damping` outside `(0, 1)` so it never settles |
|
|
3584
3876
|
| `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed |
|
|
3585
3877
|
| `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
|
|
3586
|
-
| `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not
|
|
3878
|
+
| `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3) |
|
|
3587
3879
|
| `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename |
|
|
3588
3880
|
| `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width. **SKIPs** on authored geometry and on a contour mesh — neither has rows rigc paired |
|
|
3589
3881
|
| `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
|
|
@@ -3607,19 +3899,30 @@ clauses are gated by profile.
|
|
|
3607
3899
|
|
|
3608
3900
|
## 6. What rigc will refuse — do not spend a loop on these
|
|
3609
3901
|
|
|
3610
|
-
|
|
3611
|
-
a **`NotImplementedError` naming the
|
|
3612
|
-
worse: an unknown attachment `type` returns `null` and the
|
|
3613
|
-
and a constraint entry with an unrecognised `type` matches no
|
|
3902
|
+
Most of these are in the Spine 4.3 format and the emitter does not write them. Each
|
|
3903
|
+
of *those* is a **`NotImplementedError` naming the construct**, because the parser's
|
|
3904
|
+
own behaviour is worse: an unknown attachment `type` returns `null` and the
|
|
3905
|
+
attachment disappears, and a constraint entry with an unrecognised `type` matches no
|
|
3906
|
+
case and vanishes.
|
|
3614
3907
|
|
|
3615
|
-
|
|
3616
|
-
**neither of
|
|
3908
|
+
A deferral carries its reason, and the reason is the same one in every deferred row:
|
|
3909
|
+
**neither of those types appears anywhere in the benchmark corpus** (SPEC_COVERAGE
|
|
3617
3910
|
parts 3-1 and 4-2), so neither is on the ladder's critical path. The message
|
|
3618
3911
|
says so, because a deferral without its reason is a wall rather than a work item.
|
|
3619
3912
|
|
|
3913
|
+
⚠️ **A spelling the format does not have is a different refusal and says so.**
|
|
3914
|
+
`sequence` is not an attachment type, and a `"type"` that is `null` is not an absent
|
|
3915
|
+
one — telling either author that "rigc does not emit it yet" promises work that will
|
|
3916
|
+
never be done, on a map the parser would have dropped in silence. Those rows below
|
|
3917
|
+
are `CompileError`s, and they name what the format actually defines
|
|
3918
|
+
([#577](https://github.com/firejune/rigc/issues/577)).
|
|
3919
|
+
|
|
3620
3920
|
| You wrote | You get |
|
|
3621
3921
|
| --- | --- |
|
|
3622
|
-
| attachment `type` of `point` or `linkedmesh` | `attachment
|
|
3922
|
+
| attachment `type` of `point` or `linkedmesh` | `this attachment is a "linkedmesh" — a mesh that takes its geometry from another mesh instead of stating any — a region/mesh head, then "source" …. rigc does not emit it yet, deliberately: it emits region, mesh, boundingbox, clipping, path, and neither a point nor a linked mesh appears anywhere in the benchmark corpus …` — the message names the **construct**, not just its type string, and part 1-6 is where the sentence comes from |
|
|
3923
|
+
| a mesh carrying `source` (`type: "mesh"` **or** `type: "linkedmesh"`) | the same refusal, prefixed `(a mesh carrying "source" is one)`. The two spellings share one parser branch and `source` is what decides between them (SPEC_COVERAGE part 1-6), so `source` on a mesh is a linked mesh whatever `type` says. It used to be refused as *2 keys this compiler does not read: "source", "skin" … fix the spelling or remove it*, whose remedy destroys the construct ([#577](https://github.com/firejune/rigc/issues/577)) |
|
|
3924
|
+
| attachment `type` of anything else — `sequence`, a typo | `attachment type "X" is not one of the 7 the Spine 4.3 format defines (region, mesh, linkedmesh, boundingbox, path, point, clipping). … the attachment is dropped from the skeleton without a word` — a **`CompileError`**, not a deferral: rigc is not going to implement a name the format does not have. (`sequence` is a key on a region or a mesh, not a type of its own.) |
|
|
3925
|
+
| `"type": null` | `"type" is null, which is not a name. … PRESENT-and-null is not absent: getValue(map, "type", "region") takes the default only when the key is missing, so this map matches no case, readAttachment returns null, and the attachment is dropped from the skeleton without a word. Remove the key, or name a type.` Leaving the key **out** is legal and reads as `region`; writing it as `null` is not the same thing ([#577](https://github.com/firejune/rigc/issues/577)) |
|
|
3623
3926
|
| constraint `type` of anything else | `constraint type "X" is not one Spine 4.3 knows. The five are: ik, transform, path, physics, slider.` — all five are emitted, so this is a typo, and a typo is what the parser drops in silence |
|
|
3624
3927
|
| a path attachment's `lengths` | `"lengths" is not authored — rigc measures the setup arc length of each curve off the geometry` (§3.4). Not a deferral: a second copy of a number the vertices already fix |
|
|
3625
3928
|
| a `deform` timeline on a path attachment | `a path attachment does have a vertex array, and rigc does not key it yet` — the format allows it and an animated track is a real idiom, but a deformed path invalidates the `lengths` a `constantSpeed: false` traversal reads. Move the curve by posing the bones its vertices are bound to |
|
|
@@ -3648,19 +3951,35 @@ Two more limits that are not errors but will shape what you can attempt:
|
|
|
3648
3951
|
1. `build --profile <the one you meant>` exits 0 and the report has **no FAIL**.
|
|
3649
3952
|
Saying nothing means `spine`, so "the one you meant" is a decision either way —
|
|
3650
3953
|
the report's first line names the profile that judged it.
|
|
3954
|
+
📎 The two profiles judge attachment **naming** identically since
|
|
3955
|
+
[#574](https://github.com/firejune/rigc/issues/574): `spine-html` adds the
|
|
3956
|
+
renderer and archetype rules and has no opinion about how an attachment is
|
|
3957
|
+
spelled, so a rig whose named skins share a placeholder (§3.4.2) is green
|
|
3958
|
+
under either. Before that it was green under exactly one of them, and which
|
|
3959
|
+
one was not a property of the rig.
|
|
3651
3960
|
2. Read the `SKIP` lines. Each one is a check that did *not* run — make sure none of
|
|
3652
|
-
them is a check you were relying on.
|
|
3961
|
+
them is a check you were relying on. The summary's *measured* figure is the
|
|
3962
|
+
shortest version of this step: it is how many of the rules actually looked at
|
|
3963
|
+
anything, and it is the number to quote when you say a rig gated green.
|
|
3653
3964
|
⚠️ Under `--profile spine` a foreign skeleton usually produces **no SKIP lines
|
|
3654
3965
|
at all**, and that is not a clean bill of health. The archetype assertions are
|
|
3655
3966
|
excluded by the profile before the missing `invariants` block could make them
|
|
3656
3967
|
skip, so they come back `PROF` instead. Do not go looking for a SKIP that the
|
|
3657
3968
|
profile already accounted for; read step 3 instead.
|
|
3969
|
+
⚠️ And a **red** report is where this step matters most, which is the opposite
|
|
3970
|
+
of how it reads: if `A00` failed, most of the SKIP lines say the round trip
|
|
3971
|
+
denied them a result, and the handful of `PASS` rows beside them were measured
|
|
3972
|
+
on the raw text alone. Step 1 already sent you back; do not take anything from
|
|
3973
|
+
the rest of that report on the way ([#568](https://github.com/firejune/rigc/issues/568)).
|
|
3658
3974
|
3. Read the `PROF` lines. They are where "was this rig held to that rule at all"
|
|
3659
3975
|
gets answered for everything the profile left out — the renderer policy *and*
|
|
3660
3976
|
the archetype rules. A green under `spine` has been held to neither; a green
|
|
3661
3977
|
under `spine-html` has been held to both.
|
|
3662
|
-
4. Run `explain` and read the slots table: every slot you declared
|
|
3663
|
-
(§3.3
|
|
3978
|
+
4. Run `explain` and read the slots table: every slot you declared **is** there
|
|
3979
|
+
(§3.3 — `A26_SLOT_DRAW_ORDER` holds that, and a slot nothing fills reads
|
|
3980
|
+
`setup=null attachments=[]`), so what this step is for is the two things the
|
|
3981
|
+
gate cannot check — that the order is the one you meant, and that each slot
|
|
3982
|
+
shows the setup attachment you meant.
|
|
3664
3983
|
5. If you were given **frames**, run `check` and read the table (§9). Steps 1–4 are
|
|
3665
3984
|
all about validity and structure; none of them can tell you the animation is
|
|
3666
3985
|
wrong, and this is the step that can. Do it before step 6, not after — `bench`
|
|
@@ -4142,10 +4461,48 @@ your animation is called something the frame directory is not, `--framing shared
|
|
|
4142
4461
|
to fit one framing across every set instead of one each,
|
|
4143
4462
|
`--texture-from <atlas>` to attribute how much of the MAE is texture resampling
|
|
4144
4463
|
rather than the rig (**§9.2**'s atlas floor — and note that it is *not* `--atlas`,
|
|
4145
|
-
which re-seats your geometry on that atlas's packing), `--
|
|
4464
|
+
which re-seats your geometry on that atlas's packing), `--skin <name>` to pose
|
|
4465
|
+
your candidate under one of its skins (below), `--all-frames` to list
|
|
4146
4466
|
every frame instead of the worst by MAE, `--json <out>` for the whole per-frame,
|
|
4147
4467
|
per-slot report.
|
|
4148
4468
|
|
|
4469
|
+
🚨 **What `check` certifies is the DEFAULT skin, unless you pass `--skin`**
|
|
4470
|
+
(issue #571). With no `--skin` no skin is set at all, which is `spine-core`'s own
|
|
4471
|
+
initial state: every slot resolves through `SkeletonData.defaultSkin` alone, and a
|
|
4472
|
+
slot whose art lives only in a named skin draws **nothing** — on both sides, since
|
|
4473
|
+
the reference frames came out of the same renderer. A multi-skin rig checked that
|
|
4474
|
+
way compares blank against blank and reports `MAE mean 0.00`, which reads like the
|
|
4475
|
+
best possible answer and is a measurement of no art at all. On a single-skin rig
|
|
4476
|
+
this is the whole rig and there is nothing to pass.
|
|
4477
|
+
|
|
4478
|
+
So a multi-skin rig is checked **once per skin**, and both sides are rendered
|
|
4479
|
+
under the same one:
|
|
4480
|
+
|
|
4481
|
+
```bash
|
|
4482
|
+
bun cli.ts render --candidate path/to/spine --skin patch --out frames/patch
|
|
4483
|
+
bun cli.ts check --candidate path/to/spine --frames frames/patch --skin patch
|
|
4484
|
+
```
|
|
4485
|
+
|
|
4486
|
+
Three things keep that honest, and none of them is a convention you have to
|
|
4487
|
+
remember:
|
|
4488
|
+
|
|
4489
|
+
- `render --skin` writes the name into `frames.json`, so a frame set says which
|
|
4490
|
+
picture of the rig it is. A set rendered with **no** `--skin` records nothing,
|
|
4491
|
+
because "no skin was set" and "this file predates the field" are the same bytes
|
|
4492
|
+
on disk and neither is a claim.
|
|
4493
|
+
- `check` reads that back. A candidate posed under a **different** skin from the
|
|
4494
|
+
one the frames record — or under none, where the frames name one — is
|
|
4495
|
+
**refused by name** rather than scored, because the number would be about the
|
|
4496
|
+
difference between two skins. Frames that record nothing cannot be checked, and
|
|
4497
|
+
the report says so in a `⚠️` note instead of implying agreement.
|
|
4498
|
+
- The report header names the skin on both sides on every run, `--skin` or not:
|
|
4499
|
+
`skin candidate patch frames patch`, or `candidate no skin set (the default
|
|
4500
|
+
skin alone)`. `check.json` carries the same two under `skin` and
|
|
4501
|
+
`referenceSkin`.
|
|
4502
|
+
|
|
4503
|
+
A skin name the candidate does not declare is refused with the ones it does —
|
|
4504
|
+
`the candidate declares no skin "path"; it declares [default, patch, torn]`.
|
|
4505
|
+
|
|
4149
4506
|
⭐ **A frame set may ship a contact sheet instead of every frame, and the sheet is
|
|
4150
4507
|
compared too.** A long shot does not commit 311 near-duplicate PNGs: rung 2's sets
|
|
4151
4508
|
ship `f0000.png` and `f0310.png` plus a `contact.png` holding all 311 sampled
|