spine-rigc 2.0.1 → 2.0.3
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 +8 -3
- package/docs/AUTHORING.md +41 -5
- package/docs/INGEST.md +29 -20
- package/package.json +1 -1
- package/src/assertions/bodies/a06.ts +28 -2
- package/src/assertions/bodies/a19.ts +28 -10
- package/src/compile.ts +4 -1
- package/src/diff.ts +433 -6
- package/src/png.ts +34 -7
- package/src/spine_side.ts +17 -2
- package/src/validate.ts +90 -7
- package/tools/editor_roundtrip.ts +112 -2
- package/tools/plate.ts +100 -27
package/README.md
CHANGED
|
@@ -668,8 +668,11 @@ bun cli.ts build --rig … --motion … --out build/ --copy-images
|
|
|
668
668
|
bun tools/editor_roundtrip.ts --build build/ --editor /Applications/Spine.app/Contents/MacOS/Spine
|
|
669
669
|
```
|
|
670
670
|
|
|
671
|
-
|
|
672
|
-
|
|
671
|
+
Of the editor's `--version` it records one line, the `Spine <x.y.z> …` line that
|
|
672
|
+
carries the version, and nothing else that command prints — the rest of it
|
|
673
|
+
names the licence holder — or `editor version: not found in --version output`
|
|
674
|
+
where no such line is there. It prints the import and export exit codes, the
|
|
675
|
+
validator's verdict on the export, every `diff` measure that moved, `check`'s mean MAE and worst drift per
|
|
673
676
|
animation **for each skin the build and the export both declare** — one
|
|
674
677
|
render-and-check block per skin, with a per-skin roll-up under them, because a
|
|
675
678
|
rig's contested art lives in its named skins and a single un-skinned check draws
|
|
@@ -677,7 +680,9 @@ none of it; a skin only one side declares is a FAIL naming it as **lost** (or
|
|
|
677
680
|
**added**) **by the export**, with `diff`'s `attachments.skins` beside it, and is
|
|
678
681
|
rendered on neither side — and a
|
|
679
682
|
field-by-field list of what the editor rewrote. Every step quotes what its child
|
|
680
|
-
said when that child did not do what it was for, the renderers included
|
|
683
|
+
said when that child did not do what it was for, the renderers included — the
|
|
684
|
+
editor's words in full and in order, except its `Licensed to:` line, which names
|
|
685
|
+
the licence holder and is replaced by a line saying it was withheld; a skin
|
|
681
686
|
**neither** side can draw — a hit-box rig, say — is a **SKIP** naming that, not a
|
|
682
687
|
red, because `check` had nothing to compare and `diff` and `validate` have
|
|
683
688
|
already measured the rig. One side drawing where the other does not is the
|
package/docs/AUTHORING.md
CHANGED
|
@@ -230,7 +230,7 @@ What the flags mean:
|
|
|
230
230
|
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. A directory holding no `skeleton.json`, or a path with nothing at it, is refused `nothing at <path>`, exit 2, nothing written, by every command that takes a build — `validate <dir>` and a directory given to `diff` included (`RC27`). `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
231
231
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
|
|
232
232
|
| `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
|
|
233
|
-
| `--poser` | `render`, `check` and `explain`: `core` or `spine` — on `render` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`). On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1) |
|
|
233
|
+
| `--poser` | `render`, `check` and `explain`: `core` or `spine` — on `render` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`, `RC34`). On the entry that links none of spine-core — the installed `rigc` without the runtime beside it, `bun cli_core.ts` in a clone — that refusal ends on the route to the entry that does (``the entry that links it runs them — installed, the same `rigc` once @esotericsoftware/spine-core is installed beside the package; from a source checkout, `bun cli.ts` ``); a runtime that is installed and fails to load is refused without it, since installing it is not what that reader has to do. On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1), ending on the same route where the entry links none of it |
|
|
234
234
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
235
235
|
| `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
|
|
236
236
|
| `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
|
|
@@ -666,7 +666,9 @@ and a file's name is not evidence: a production pack shipped WebP pages called
|
|
|
666
666
|
there — a WebP, JPEG, GIF, KTX or KTX2 file by its signature, or no format rigc
|
|
667
667
|
recognises, with the first bytes in hex either way — and a file that begins as a
|
|
668
668
|
PNG and runs out before its `IEND` chunk is called **truncated**, wherever it was
|
|
669
|
-
cut.
|
|
669
|
+
cut. A PNG whose `IHDR` states a width or a height of **0** is refused the same way
|
|
670
|
+
— the format requires each to be at least 1, and such a file describes no texel —
|
|
671
|
+
naming the size it states and which dimension is 0. `build --atlas-in` and `explain --atlas-in` say so where they open the pack,
|
|
670
672
|
every such page in one sentence and before the compile; a loose part that is not a
|
|
671
673
|
PNG is refused as the image it is; and `rigc validate` on a directory holding such
|
|
672
674
|
a page fails `A06` with the same sentence:
|
|
@@ -679,6 +681,15 @@ a page fails `A06` with the same sentence:
|
|
|
679
681
|
# measured. Re-export it as PNG
|
|
680
682
|
```
|
|
681
683
|
|
|
684
|
+
A page whose header and chunks read and whose **image data cannot be decoded** — a
|
|
685
|
+
compressed stream broken behind an intact chunk walk, or one that inflates short of
|
|
686
|
+
the rows its header states — fails `A06` too, under both profiles, with the
|
|
687
|
+
decoder's own words: `… is a PNG whose header and chunks read and whose image data
|
|
688
|
+
cannot be decoded: the decoder stops with "incorrect data check", so no texel of it
|
|
689
|
+
can be read`. The same sentence is what `render` and a mesh part's trace refuse such
|
|
690
|
+
a file with. Without it the default profile built such a page green — no rule it
|
|
691
|
+
runs decoded a texel — and wrote a directory `render` then could not draw.
|
|
692
|
+
|
|
682
693
|
⇒ **Re-export the page as PNG under the name the atlas gives it**, and the next run
|
|
683
694
|
measures it like any other. ⚠️ `A06` deliberately does not read the width and
|
|
684
695
|
height a WebP header carries in a fixed field. Doing so would print a size that no
|
|
@@ -997,6 +1008,24 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
|
997
1008
|
[INGEST.md](INGEST.md) §1.3.1, and *The measure inventory* in
|
|
998
1009
|
[BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md),
|
|
999
1010
|
which is repository material rather than part of this package.
|
|
1011
|
+
|
|
1012
|
+
🔍 **The wiring is compared by name too, not only the rosters** (issue #1085).
|
|
1013
|
+
`constraints.order` reads the order of the one constraint array, which is the
|
|
1014
|
+
order the runtime applies it in; `animations.targets` reads what each timeline
|
|
1015
|
+
keys and `animations.keyed_names` what each key names — an attachment key's
|
|
1016
|
+
attachment, a draw-order offset's slot, an event key's event; `attachments.refs`
|
|
1017
|
+
reads the region an attachment draws, a clipping polygon's `end` and a linked
|
|
1018
|
+
mesh's `source`; `attachments.skin_members` reads what each skin activates.
|
|
1019
|
+
Every one of those, changed alone, poses or draws differently, and every one read
|
|
1020
|
+
1.000 before. Each note names what differs, candidate first —
|
|
1021
|
+
`candidate only: "heavy|bones|root"; reference only: "heavy|bones|bone"` is a
|
|
1022
|
+
rotate timeline moved from `bone` to `root`, and `attachment
|
|
1023
|
+
"default/clipping/clipping": end "portal-flare6" vs "head-bb"` is a clipping that
|
|
1024
|
+
stops at another slot. A region's `path` is read as the runtime resolves it —
|
|
1025
|
+
`path`, else `name`, else the key — so stating the path an attachment already
|
|
1026
|
+
resolves is no difference. These are name-matched: your own names read low here
|
|
1027
|
+
exactly as they do on `names`, and a low figure with `names` at 1.000 is the
|
|
1028
|
+
wiring that moved.
|
|
1000
1029
|
- **`check`** renders your candidate into the reference frames' own pixel grid and
|
|
1001
1030
|
compares pixels — the only thing here that can see a wrong animation. **§9.**
|
|
1002
1031
|
🚨 What it certifies is the **default skin** unless you pass `--skin <name>`:
|
|
@@ -1330,6 +1359,13 @@ physics constraint ([FACE.md](FACE.md)), and `diff` moved only
|
|
|
1330
1359
|
repointed sliders**, and the re-rendered mean absolute error of 8.43–10.41 is the
|
|
1331
1360
|
size #535 measured out of order. Hence a refusal rather than a note.
|
|
1332
1361
|
|
|
1362
|
+
⇒ **`diff` names it now** (issue #1078). `constraints.refs` compares every name a
|
|
1363
|
+
constraint resolves — its bones and slots, a slider's `animation` and `property`,
|
|
1364
|
+
and the `from`/`to` pairs of a transform's `properties` — and its note names each
|
|
1365
|
+
constraint wired differently with both sides' names. On the probe above, planted
|
|
1366
|
+
without the editor: `0.333 refs 1/3 … — 2 wired differently: slider constraint
|
|
1367
|
+
"yaw": animation "-a" vs "5"; slider constraint "tilt": animation "2b" vs "-a"`.
|
|
1368
|
+
|
|
1333
1369
|
**R11 — The `skins` array is written with `default` first and the rest in exactly
|
|
1334
1370
|
the order R10 describes.** A skin is a **name** in the JSON half of the format and
|
|
1335
1371
|
an **ordinal** in the binary half — `skins[readInt()]` for an attachment timeline,
|
|
@@ -5721,7 +5757,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
5721
5757
|
| `vertex N binds bone index I, which is not in the bone list` | §3.4 — a `"boneIndexing": "raw"` run on a mesh, bounding box, clipping polygon or path names a bone by its place in the bone array, and I is no place in it: past the end, negative or not a whole number. Count the rig's `bones` from 0, or give the bindings by name as `weights`, which cannot miss this way. ⚠️ **Refused earlier, not newly** (issue #917): the run is decoded to bone names once, when the attachment is built, and this is where an index that names no bone stops. Before that a mesh compiled and `A20` named the index at the gate, a bounding box, clipping polygon or path with its `lengths` stated compiled and `A33` named it, and only a path whose `lengths` rigc measures refused here, in this sentence. `MV04` plants it on each of the four kinds |
|
|
5722
5758
|
| `the raw weighted run has a bone count of C at index I` / `the raw weighted run is truncated — vertex N claims C bone(s) and the array ends first` / `the raw weighted run decodes to N vertices but there are M uv pairs` | §3.4 — a mesh's `"boneIndexing": "raw"` run is not `boneCount, (index, x, y, weight) × boneCount` per vertex, one vertex per uv pair. These are the polygon's three sentences (whose last clause there reads `but vertexCount is M`), now asked of a mesh as well. Fix the run, or give the bindings by name as `weights`. ⚠️ **Refused earlier, not newly** (issue #917): the mesh run is decoded to bone names when the mesh is built, and a run that does not decode cannot be. Before that it compiled and `A20` named it at the gate (`has weights for 5 vertices but 4 uv pairs`, `vertex 3 has weight NaN`). `MV04` plants each |
|
|
5723
5759
|
| `image "X.png" is not on disk at …` | fix the name, or point `--images` at the right directory |
|
|
5724
|
-
| `image "X.png": /…/X.png is a WebP image (…), not a PNG: its first 12 byte(s) are …` | the file is there and is not a PNG — the name ends in `.png` and the bytes decide. Re-export it as PNG; the same sentence says **truncated** for a PNG that runs out before its `IEND`, and then the repair is a whole copy (§0.2) |
|
|
5760
|
+
| `image "X.png": /…/X.png is a WebP image (…), not a PNG: its first 12 byte(s) are …` | the file is there and is not a PNG — the name ends in `.png` and the bytes decide. Re-export it as PNG; the same sentence says **truncated** for a PNG that runs out before its `IEND`, and then the repair is a whole copy, and names the dimension for a PNG whose `IHDR` states a width or height of 0 (§0.2) |
|
|
5725
5761
|
| `a "contour" generator traces the part's own alpha, and "X.png" is lifted off a packed page at the coordinates the atlas states, which on this file are not where its texels are — so there is no silhouette here to trace, only another part of the page. page "p.png" declares …` | the page's PNG is not the size its atlas declares, and the rest of the message is `A06`'s sentence for it: re-declare the page with the `scale:` header it names, or re-export the page at its declared size (§0.2) |
|
|
5726
5762
|
| `--atlas-in <pack>.atlas: N of its M page(s) cannot be read as PNG, and nothing was compiled against the pack — page "p.png": …` | the same, for every page of the pack at once, before the compile: re-export each named page as PNG under the name the atlas gives it (§0.2) |
|
|
5727
5763
|
| `parts/iris_open.png is 96x64 but slot "iris" declares 96x60` | R5 — a manifest `states:` entry whose art is not the window the part declares. Re-export the PNG, or fix the part's `size`; a quad sized against art of another size is the silence `A06` exists for, and the window is what the quad is built from |
|
|
@@ -5911,7 +5947,7 @@ Fix A00 and run it again.
|
|
|
5911
5947
|
| `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height`. **SKIP** when the skeleton carries no region attachment |
|
|
5912
5948
|
| `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) **SKIP** when the skeleton carries no mesh attachment |
|
|
5913
5949
|
| `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**. **SKIP** when no animation carries a timeline at all. Timelines with no `curve` on any key still PASS: every timeline name is checked against the channel table whether or not a curve sits on one Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5914
|
-
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk — the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and §0.2 has the whole of it: a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles: a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels — a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling**, and what that message names is the pair that shares texels. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes — one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way — or calls it **truncated** when it begins as a PNG and runs out before its `IEND` (§0.2). That clause is validity too: nothing in rigc can read such a page back — and its failure is the **one** line that names such a page: `A19` SKIPs over it and points here rather than naming it again, the way `A17` alone names a missing file. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page's file is **not on disk** and nothing else here failed: that page's size was not measured, so the reason names the pages not read (or says none was, when every file is missing) and points at `A17`, which fails naming each missing file — one missing file is named once, by the rule whose subject it is. With another `A06` failure standing, the missing page is `A17`'s alone |
|
|
5950
|
+
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk — the message names the ratio it measured on **each** axis and the header that states the same art truthfully, and §0.2 has the whole of it: a runtime maps a region as a fraction of the DECLARED size and never reads the texture's own, so such a page draws, and what it breaks is every reader that addresses the page in texels. A uniform ratio is a `size:`/`scale:` pair away from honest and the message prints it; two ratios are not, because `scale:` carries one number, and the message says so rather than offering a header that would not work. **Or** a region's rectangle is not inside the page it names — rotation honoured, so a region at `rotate: 90` or `270` occupies `height x width` of the page and a region that fits only because it is turned is inside it. The message names the region, the rectangle it occupies, the page and the page's size. That clause is **validity** and runs under both profiles: a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels — a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling**, and what that message names is the pair that shares texels. **Or** the page file is not a PNG at all, and then nothing about its size is measured: the message carries the page's path and the size the atlas declares, and names what the file is by its first bytes — one of WebP, JPEG, GIF, KTX, KTX2 by its signature, or no image format rigc recognises, the bytes in hex either way — or calls it **truncated** when it begins as a PNG and runs out before its `IEND`, or names the dimension when its `IHDR` states a width or height of **0**, which the format forbids (§0.2). **Or** the file reads as a PNG and its **image data cannot be decoded** — its compressed stream is inflated (not unfiltered) and the decoder refuses it, or it inflates short of the rows the header states — and the message names the file and carries the decoder's own words (`the decoder stops with "incorrect data check"`). That clause is validity too: nothing in rigc can read such a page back — and its failure is the **one** line that names such a page: `A19` SKIPs over it and points here rather than naming it again, the way `A17` alone names a missing file. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page's file is **not on disk** and nothing else here failed: that page's size was not measured, so the reason names the pages not read (or says none was, when every file is missing) and points at `A17`, which fails naming each missing file — one missing file is named once, by the rule whose subject it is. With another `A06` failure standing, the missing page is `A17`'s alone |
|
|
5915
5951
|
| `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. Three sentences are about blank lines and name three different repairs: `line 1: the file begins with a blank line` (or `with N blank lines` for a run, one finding) is a blank before the first page name — delete it; `line N: the file ends with a blank line` (or `with N blank lines`, one finding) is a blank after the last page block's final line — delete it, since rigc ends the file on one newline; `line N: consecutive blank lines` is two blank lines side by side between the first page and the last — delete one. None of the three is `the last page block declares no region`, which means a page name and its fields with no region after them. rigc writes none of the three: under `--atlas-in` the pack's blank lines are put in rigc's shape on re-emission (§0.2), so any of them means a file rigc did not write, handed to `validate`. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5916
5952
|
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | three things, and the message says which: an attachment whose `path` names **no region** of this atlas; a `path` carrying **stray whitespace**, printed quoted so you can see it; an **atlas region name** carrying stray whitespace (`A07` names that same line with its line number). The first two are read off the raw file **before** the loader is asked, so the miss is named here with the skin, the slot, the placeholder and the attachment's own name — the four things `AtlasAttachmentLoader`'s own `Region not found in atlas: <path> (attachment: <name>)` does not carry. There is no `spine-html` clause here — a placeholder is free to differ from the region its `path` names **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once |
|
|
5917
5953
|
| `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 |
|
|
@@ -5924,7 +5960,7 @@ Fix A00 and run it again.
|
|
|
5924
5960
|
| `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`) Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5925
5961
|
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. This is the one rule that names a missing page file; `A06` and `A19` SKIP over the page they could not read and point here rather than naming it again. **SKIP** when the atlas declares no page — as it is for `A06`, `A19` and `A27`; see `A07` |
|
|
5926
5962
|
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed — in `skeleton.json`, `skeleton.atlas` or the model document `skeleton.model.json`, the one named (the document's first differing line with it). That is a compiler bug, not a spec bug — report it Under `cli_core.ts build` the same clause runs over the text the emitter wrote and prints this line in the same words (`RC28`). |
|
|
5927
|
-
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes**; the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes: the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art. ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page**: a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it**, and the clause above does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so a scan would come back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappears**, the scan finding a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page carrying a part was not read and nothing else here failed: the reason names the pages not read and how many of the pages carrying a part were (or says none was), and for each page points at the one rule that names its file — `A17` when the file is **not on disk**, `A06` when it is on disk and **cannot be read as PNG** (not a PNG, or
|
|
5963
|
+
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes**; the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes: the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art. ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page**: a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it**, and the clause above does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so a scan would come back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappears**, the scan finding a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. **SKIP** when the atlas declares no page — and, rather than a PASS, when a page carrying a part was not read and nothing else here failed: the reason names the pages not read and how many of the pages carrying a part were (or says none was), and for each page points at the one rule that names its file — `A17` when the file is **not on disk**, `A06` when it is on disk and **cannot be read as PNG** (not a PNG, truncated, an `IHDR` dimension of 0, or image data that does not decode; `A06` fails naming what the file is on every such page, under both profiles). Undecodable data is found where this rule decodes, so the header's fast negative above still refuses a part with no alpha channel and no `tRNS` without opening it — a verdict about the header, not about the pixels. So one unreadable file is one red line, never two: with another `A19` failure standing, the unread page is that rule's alone and this one says nothing about it. A page holding only the base plate moves nothing here, missing or unreadable: this rule had no part on it to read. |
|
|
5928
5964
|
| `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. On a rig spec's `"boneIndexing": "raw"` mesh an index out of range or a run that does not decode is refused at compile first (§5.1, issue #917), so here it names a skeleton rigc did not compile. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not. Fix the rig spec's `controls`/`chain`, a `segments` generator's `bones` (its `influence` line names a bone no vertex binds), or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
|
|
5929
5965
|
| `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1. **SKIPs** by name on authored or linked geometry and on a `segments` mesh, whose outline is weighted by distance like the rest of it — none of them claims a pinned rim |
|
|
5930
5966
|
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment |
|
package/docs/INGEST.md
CHANGED
|
@@ -64,7 +64,7 @@ an upstream `license.txt` (Appendix, and [NOTICE.md](../NOTICE.md)).
|
|
|
64
64
|
| **`preview --candidate <skeleton.json>`** | ✅ **yes** | one self-contained `.html` that plays it in the official Spine Web Player. Needs a network the first time it is opened ([NOTICE.md](../NOTICE.md)) |
|
|
65
65
|
| **`vote --candidate <a> --candidate <b>`** | ✅ **yes, on either side** | a ballot page. Pairing a foreign export against your own transcription is a legitimate ballot, and the panes carry no paths |
|
|
66
66
|
| **`check --candidate <skeleton.json> --frames <dir>`** | ✅ **yes** | ⭐ it reads **frames and never a reference skeleton**, so a foreign export enters this one *twice over*: as the candidate, or — via `render` — as the source of the frames. §1.4 |
|
|
67
|
-
| **`diff <candidate.json> <reference.json>`** | ✅ **yes, both sides** |
|
|
67
|
+
| **`diff <candidate.json> <reference.json>`** | ✅ **yes, both sides** | 54 structural measures over bones, slots, attachments, constraints, animations and events. ⛔ **Blind to every coordinate** — §1.3 |
|
|
68
68
|
| **`ingest <skeleton.json> --out <dir>`** | ✅ **yes — and it is the only reader that WRITES specs** | the `.json` alone; no atlas, no art, no project file. Out come `rig.json`, `motion.json` and a findings report, such that `build`ing them reproduces the skeleton it read **byte for byte — for a skeleton rigc emitted**. ⚠️ For an editor export the claim is identity in canonical form apart from `hash` and `spine`, which §2.3 states in full. The seventh reader, and the one that ends §2's hand work — §2.0 and §5 |
|
|
69
69
|
| **`pose --images <dir> --frame <png>`** | ⛔ **not the skeleton** | loose part PNGs and one picture. A packed atlas page is not loose parts, and pointing it at one produces a confident answer about nothing — §5 |
|
|
70
70
|
| **`explain --rig … --motion … --out …`** | ⛔ **no** | rig spec + motion spec. It explains **what you wrote**, which makes it a transcription instrument rather than a reading one — §1.5 |
|
|
@@ -215,7 +215,7 @@ Spine runtime plays it, whatever rigc's own rasteriser or validator thinks.
|
|
|
215
215
|
|
|
216
216
|
### 1.3 `diff` — and the two things it cannot see
|
|
217
217
|
|
|
218
|
-
`diff` takes two compiled skeletons and reports
|
|
218
|
+
`diff` takes two compiled skeletons and reports 54 measures in eight groups, plus two
|
|
219
219
|
blocks that report and gate nothing: the `(reported)` measures beside `attachments`
|
|
220
220
|
and `animations`, and the `skeleton` header block at the top, which measures the stage.
|
|
221
221
|
A ninth group of six joins them when something has paired the two sides'
|
|
@@ -250,7 +250,7 @@ rigc diff
|
|
|
250
250
|
|
|
251
251
|
bones (name-agnostic) mean 1.000 over 5 measures — the same two skeletons compared with names thrown away
|
|
252
252
|
…
|
|
253
|
-
animations mean 1.000 over
|
|
253
|
+
animations mean 1.000 over 11 measures
|
|
254
254
|
1.000 count 2/2 how many animations
|
|
255
255
|
1.000 names 2/2 the animation names
|
|
256
256
|
1.000 duration 2/2 each animation runs as long (last key time, within one frame)
|
|
@@ -267,7 +267,7 @@ it does not say *which* side has three. The `..` line above is where you read th
|
|
|
267
267
|
⛔ **`diff` is blind to every coordinate a bone, an attachment or a key carries.** No
|
|
268
268
|
measure reads a bone's `x`/`y`/`rotation`, an attachment's offset, or a key's value —
|
|
269
269
|
only *presence*, *names*, *counts*, *order* and *kinds*. §4.1 moves a pivot 236.5
|
|
270
|
-
units and every one of the
|
|
270
|
+
units and every one of the 54 measures still reads **1.000**. ⇒ Never take a green
|
|
271
271
|
`diff` as evidence that a geometric edit did not land, and never take it as evidence
|
|
272
272
|
that one did.
|
|
273
273
|
|
|
@@ -325,7 +325,7 @@ two sections, the second over the paired shots — `duration`, `timeline_kinds`,
|
|
|
325
325
|
it used:
|
|
326
326
|
|
|
327
327
|
```
|
|
328
|
-
animations mean 0.
|
|
328
|
+
animations mean 0.182 over 11 measures
|
|
329
329
|
1.000 count 1/1 how many animations
|
|
330
330
|
0.000 names 0/2 the animation names
|
|
331
331
|
…
|
|
@@ -475,8 +475,8 @@ no field of the file).
|
|
|
475
475
|
📊 **For an editor export the claim is weaker, and measured.** Every
|
|
476
476
|
`examples/*/export/*.json` ingested with `--art none`, rebuilt through the pack
|
|
477
477
|
beside it, and `diff`ed against the file it was read from: **12 of 12 come back with 0
|
|
478
|
-
blockers and 1.000 on all
|
|
479
|
-
all **nine value measures** too, over **
|
|
478
|
+
blockers and 1.000 on all 54 ratio-bearing measures and all 6 reported ones** — and on
|
|
479
|
+
all **nine value measures** too, over **195,363** compared values. Byte
|
|
480
480
|
identity is not the claim there and the reason is the input, not the round trip — §2.3
|
|
481
481
|
has the pass line, and what the value measures do and do not reach. ⚠️ Which pack is "the one beside it" is
|
|
482
482
|
resolved rather than guessed, for §0.2's reason: `spineboy/export` holds two, and
|
|
@@ -702,7 +702,7 @@ State the ambition in the right units, because three different things get called
|
|
|
702
702
|
|
|
703
703
|
| Ambition | Reachable? | What it costs, and what it proves |
|
|
704
704
|
| --- | --- | --- |
|
|
705
|
-
| **Structural agreement** — same bones, slots, attachments, timelines, key counts, curve kinds | ✅ yes, and `diff` measures it | the 3-timing transcription reads **1.000 on all
|
|
705
|
+
| **Structural agreement** — same bones, slots, attachments, timelines, key counts, curve kinds | ✅ yes, and `diff` measures it | the 3-timing transcription reads **1.000 on all 54 measures**. Aim here first |
|
|
706
706
|
| **Geometric agreement** — the same drawn pixels, allowing for the atlas | ✅ yes, and `check` measures it | see below |
|
|
707
707
|
| **Byte-identical JSON** | ✅ **for a rebuild, in canonical form, apart from `hash` and `spine`** — the pass line below | a **rebuild** of an editor export (`ingest`, then `build`) is the export. A **transcription** by hand is not held to it: what differs there is what a person chose to write, not the emitter |
|
|
708
708
|
|
|
@@ -745,7 +745,7 @@ keys**, which is why a moved value is invisible to it.
|
|
|
745
745
|
⭐ **The values are measured by a second comparison rather than by `diff`.**
|
|
746
746
|
Structure at 1.000 is silent about the numbers inside it: a decompiler that halved
|
|
747
747
|
every rotation, dropped every bone's `length` or mirrored every vertex would read
|
|
748
|
-
1.000 on all
|
|
748
|
+
1.000 on all 54 measures and on every `(reported)` one. So the example round trip is
|
|
749
749
|
also compared **value by value**,
|
|
750
750
|
with the format's defaults taken from the parser rather than from a table — both files
|
|
751
751
|
are read through `spine-core` and the parsed forms are compared path by path, under a
|
|
@@ -755,12 +755,12 @@ storage. Nine measures — here is the `6-arcs` export's, wrapped to fit this
|
|
|
755
755
|
page:
|
|
756
756
|
|
|
757
757
|
```
|
|
758
|
-
values: 9/9 measure(s) at 1.000 over
|
|
758
|
+
values: 9/9 measure(s) at 1.000 over 13977 compared value(s); skeleton 1.000 ·
|
|
759
759
|
bones 1.000 · slots 1.000 · attachments 1.000 · constraints 1.000 · events 1.000 ·
|
|
760
760
|
key_times 1.000 · key_values 1.000 · curves 1.000
|
|
761
761
|
```
|
|
762
762
|
|
|
763
|
-
Over the twelve exports that is **
|
|
763
|
+
Over the twelve exports that is **195,363 values** compared, and all twelve read
|
|
764
764
|
1.000 on all nine.
|
|
765
765
|
|
|
766
766
|
`docs/BENCHMARK.md`'s *The nine value measures* is the full statement. What it still
|
|
@@ -1082,7 +1082,7 @@ same displacement evaluated across the bone's own rotation:
|
|
|
1082
1082
|
**What the instruments say about it** — and this pair is why §1.3 carries its warning:
|
|
1083
1083
|
|
|
1084
1084
|
- **`diff` sees nothing.** `rigc diff work/t3b/skeleton.json <the export>` reads
|
|
1085
|
-
**1.000 on all
|
|
1085
|
+
**1.000 on all 54 measures**: same bones, names, parents, order, slots, draw order,
|
|
1086
1086
|
attachments, animations, timelines, key counts, curve kinds. All true, and all silent
|
|
1087
1087
|
about a 236.5-unit move.
|
|
1088
1088
|
- **`check` sees it loudly**, with the framing pinned so the comparison is of pictures
|
|
@@ -1192,28 +1192,35 @@ slots to `arm-art`/`block-art` and their attachments to match:
|
|
|
1192
1192
|
1.000 bone_binding_shape 2/2 as many slots hang off a bone of each shape (`?` = no such bone is declared)
|
|
1193
1193
|
1.000 order_shape 2/2 the draw order is the same order of `<attachment type>@<bone shape>`
|
|
1194
1194
|
|
|
1195
|
-
attachments mean 0.
|
|
1195
|
+
attachments mean 0.818 over 11 measures
|
|
1196
1196
|
1.000 skins 1/1 the skin names
|
|
1197
1197
|
1.000 count 2/2 how many attachments
|
|
1198
1198
|
0.000 names 0/4 skin/slot/attachment keys
|
|
1199
1199
|
1.000 type_counts 2/2 as many of each attachment type
|
|
1200
1200
|
…
|
|
1201
|
-
|
|
1201
|
+
0.000 refs 0/2 each attachment resolves the same region, clipping end and linked-mesh source — 2 on one side only, which `attachments.names` names
|
|
1202
|
+
1.000 skin_members 1/1 each skin activates the same skin-required bones and constraints
|
|
1203
|
+
…
|
|
1204
|
+
animations mean 0.909 over 11 measures
|
|
1205
|
+
…
|
|
1206
|
+
0.000 targets 0/8 each timeline keys the same bone, slot, constraint or attachment — …
|
|
1202
1207
|
```
|
|
1203
1208
|
|
|
1204
1209
|
Three things to read out of that, in order:
|
|
1205
1210
|
|
|
1206
1211
|
- **The name-keyed collapse is the task, not a defect.** `bones` 0.567, `slots` 0.143,
|
|
1207
|
-
`attachments` 0.
|
|
1212
|
+
`attachments` 0.818, `animations` 0.909. Note that several *non*-name measures fall with them —
|
|
1208
1213
|
`slots.bone`, `slots.blend`, `bones.parent_by_name` — because they are keyed **by**
|
|
1209
1214
|
the name that changed. They are not saying the binding changed.
|
|
1210
1215
|
- 🚨 **`bones (name-agnostic)` and `slots (name-agnostic)` must stay 1.000.** They
|
|
1211
1216
|
measure the rig with the vocabulary thrown away, so a rename that changed only names
|
|
1212
1217
|
leaves them untouched. **A drop there is a structural mistake wearing a rename's
|
|
1213
1218
|
clothes**, and it is the only assertion this recipe really has.
|
|
1214
|
-
- **`animations`
|
|
1215
|
-
|
|
1216
|
-
|
|
1219
|
+
- **`animations` moves on one measure only, `targets`**, because animation *names*
|
|
1220
|
+
were not part of the ask but every timeline keys a renamed bone — `targets` is keyed
|
|
1221
|
+
by the name that changed, as `slots.bone` is. Every other `animations` measure stays
|
|
1222
|
+
1.000. Under the same edit `bones.names` reads `1/5` — `root` survived, and the total
|
|
1223
|
+
is the union of both vocabularies.
|
|
1217
1224
|
|
|
1218
1225
|
⛔ And remember §1.3: `diff` reads no coordinates either way. A rename that also moved
|
|
1219
1226
|
something is invisible to every measure in that report. Pair it with a `check` against
|
|
@@ -1255,7 +1262,7 @@ that already round-trips.
|
|
|
1255
1262
|
4. **Read `diff` knowing what it is about to say.**
|
|
1256
1263
|
|
|
1257
1264
|
```
|
|
1258
|
-
animations mean 0.
|
|
1265
|
+
animations mean 0.821 over 11 measures
|
|
1259
1266
|
0.667 count 2/3 how many animations
|
|
1260
1267
|
0.667 names 2/3 the animation names
|
|
1261
1268
|
0.667 duration 2/3 each animation runs as long (last key time, within one frame)
|
|
@@ -1265,10 +1272,12 @@ that already round-trips.
|
|
|
1265
1272
|
1.000 event_keys 0/0 as many event firings — neither side has any
|
|
1266
1273
|
0.667 draw_order 2/3 a draw-order timeline is present or absent alike
|
|
1267
1274
|
0.667 deform 2/3 a deform timeline is present or absent alike
|
|
1275
|
+
0.889 targets 8/9 each timeline keys the same bone, slot, constraint or attachment — …
|
|
1276
|
+
1.000 keyed_names 0/0 each key names the same attachment, draw-order slot or event — neither side has any
|
|
1268
1277
|
```
|
|
1269
1278
|
|
|
1270
1279
|
🚨 **Every one of those got worse, and that is the correct result.** The
|
|
1271
|
-
`animations` section went 1.000 → 0.
|
|
1280
|
+
`animations` section went 1.000 → 0.821 because the candidate now has something the
|
|
1272
1281
|
reference does not. `diff` measures agreement with a reference; you were asked to
|
|
1273
1282
|
*disagree* with it, in one specific way. ⇒ **Check that the drop is confined to the
|
|
1274
1283
|
`animations` section and is the size the addition explains** — one animation of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
* and the argument for the SKIP guards it shares with A17 and A19 (#568, #608)
|
|
11
11
|
* — stays in `validate()`.
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { resolve } from 'node:path';
|
|
15
15
|
import type { Verdicts } from '../harness.ts';
|
|
16
16
|
import type { AtlasRegionEntry, AtlasRegionFacts } from '../facts/atlas_regions.ts';
|
|
17
17
|
import { SKIP_NO_ATLAS, SKIP_NO_ATLAS_PAGE } from '../reasons.ts';
|
|
18
18
|
import { pageFootprint, pageGridSentence } from '../../atlas.ts';
|
|
19
19
|
import { readPngHeader } from '../../png.ts';
|
|
20
|
+
import { imageDataProblem } from '../../../tools/plate.ts';
|
|
20
21
|
|
|
21
22
|
export function a06AtlasPageSizeMatchesPng({ fail: failed, skip }: Verdicts, { atlas }: AtlasRegionFacts, input: { atlasDir: string }, policy: boolean): void {
|
|
22
23
|
if (!atlas) return skip('A06_ATLAS_PAGE_SIZE_MATCHES_PNG', SKIP_NO_ATLAS);
|
|
@@ -104,7 +105,32 @@ export function a06AtlasPageSizeMatchesPng({ fail: failed, skip }: Verdicts, { a
|
|
|
104
105
|
`size was not measured: ${header.problem}`,
|
|
105
106
|
);
|
|
106
107
|
}
|
|
107
|
-
|
|
108
|
+
// 🔒 **And a page whose header reads and whose image data does not decode
|
|
109
|
+
// is this rule's too** (issue #1074), for the reason #1064 made the
|
|
110
|
+
// unreadable header its: this is the one reader of every page that runs
|
|
111
|
+
// under both profiles. Measured before this, on the tree's 19 recipes with
|
|
112
|
+
// the first page's compressed stream overwritten past its first half,
|
|
113
|
+
// this rule PASSED on 19 of 19 (it read the header only) and `A19` alone
|
|
114
|
+
// named the file, as `threw: cannot decode PNG …`, on the 12 whose first
|
|
115
|
+
// page carries a part. On the 7 gallery rigs, whose first page is the base
|
|
116
|
+
// plate `A19` does not scan, nothing did: both rules PASSED under both
|
|
117
|
+
// profiles, and the default build wrote a directory `render` then died on.
|
|
118
|
+
//
|
|
119
|
+
// ⚠️ The stream is inflated, not decoded: `imageDataProblem` is the
|
|
120
|
+
// decoder's own refusing half and nothing after it — 68.9 ms over the 19
|
|
121
|
+
// builds' pages against `validate()`'s 286.2 ms under `spine`, where a
|
|
122
|
+
// whole decode would be 459.6 ms. A page whose data does not decode has no
|
|
123
|
+
// size worth comparing, so the grid clause is not asked of it: one file,
|
|
124
|
+
// one line.
|
|
125
|
+
const undecodable = header.info === null ? null : imageDataProblem(readFileSync(abs), abs);
|
|
126
|
+
if (undecodable !== null) {
|
|
127
|
+
fail(
|
|
128
|
+
'A06_ATLAS_PAGE_SIZE_MATCHES_PNG',
|
|
129
|
+
`page "${page.name}" declares ${page.width}x${page.height} and its file cannot be read as PNG, so ` +
|
|
130
|
+
`nothing on it was measured: ${undecodable}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const info = undecodable === null ? header.info : null;
|
|
108
134
|
const onPage = atlas.regions.filter((region) => region.page.name === page.name);
|
|
109
135
|
const gridSaid = info === null ? null : pageGridSentence(page, info, onPage);
|
|
110
136
|
if (gridSaid !== null) {
|
|
@@ -17,8 +17,8 @@ import type { SkinEntryFacts } from '../facts/skin_entries.ts';
|
|
|
17
17
|
import type { StageFacts } from '../facts/stage.ts';
|
|
18
18
|
import { SKIP_NO_ATLAS, SKIP_NO_ATLAS_PAGE } from '../reasons.ts';
|
|
19
19
|
import { pageFootprint } from '../../atlas.ts';
|
|
20
|
-
import { colourTypeName, readPngHeader } from '../../png.ts';
|
|
21
|
-
import { readPlate } from '../../../tools/plate.ts';
|
|
20
|
+
import { colourTypeName, NotAPngError, readPngHeader } from '../../png.ts';
|
|
21
|
+
import { type Plate, readPlate } from '../../../tools/plate.ts';
|
|
22
22
|
import type { RigInfo } from '../../types.ts';
|
|
23
23
|
|
|
24
24
|
export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atlas }: AtlasRegionFacts, stage: StageFacts, { regionAttachments }: Pick<SkinEntryFacts, 'regionAttachments'>, input: { atlasDir: string; rig?: RigInfo }): void {
|
|
@@ -160,16 +160,32 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
160
160
|
// holding nothing but the base plate had nothing for this rule to read
|
|
161
161
|
// either way and moves nothing, exactly as a missing one does.
|
|
162
162
|
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
163
|
+
// 🔒 **So does a page whose header reads and whose image data does not
|
|
164
|
+
// decode** (issue #1074). It used to reach `readPlate` below and come out
|
|
165
|
+
// as this rule's `threw: cannot decode PNG …` — a stack message as the
|
|
166
|
+
// detail, on 12 of the 19 recipes with a stream broken — because `A06`
|
|
167
|
+
// read the header only. `A06` now asks the decoder's refusing half too
|
|
168
|
+
// (`imageDataProblem`) and fails naming the file under both profiles, so
|
|
169
|
+
// the page is its, by #1064's rule, and joins the same list — at the two
|
|
170
|
+
// places below where this rule decodes (`decoded`), and not before them:
|
|
171
|
+
// the header's fast negative (#215) still refuses a file that has nowhere
|
|
172
|
+
// to keep a clear texel without opening it, which is a verdict about the
|
|
173
|
+
// header and not about the pixels A06 found unreadable (PT13).
|
|
168
174
|
const header = readPngHeader(abs);
|
|
169
|
-
if (header.
|
|
175
|
+
if (header.info === null) {
|
|
170
176
|
if (carriesPart) unreadable.push(page.name); // A06 names the file; this body says only that it did not read it
|
|
171
177
|
continue;
|
|
172
178
|
}
|
|
179
|
+
const decoded = (): Plate | null => {
|
|
180
|
+
try {
|
|
181
|
+
return readPlate(abs);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
// `pngProblem` has accepted the file, so this is the decoder's refusal (`readPlate`'s, #1074): A06 names it.
|
|
184
|
+
if (!(err instanceof NotAPngError)) throw err;
|
|
185
|
+
if (carriesPart) unreadable.push(page.name);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
173
189
|
if (on.length > 1) {
|
|
174
190
|
// 🚨 A rotated region is refused by A06 under this profile, so this
|
|
175
191
|
// reading was assumed to be cosmetic — a rectangle printed beside a
|
|
@@ -203,7 +219,8 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
203
219
|
// what was not measured, which is also what "green means measured"
|
|
204
220
|
// requires: a part this rule could not read must not be certified by
|
|
205
221
|
// it.
|
|
206
|
-
const plate =
|
|
222
|
+
const plate = decoded();
|
|
223
|
+
if (plate === null) continue;
|
|
207
224
|
// 🚨 **The scan's coordinates are the atlas's, so a file that is not the
|
|
208
225
|
// declared grid is a non-measurement for every region on it** (issue
|
|
209
226
|
// #715), and #705's clause above does not cover it. That one fires when
|
|
@@ -315,7 +332,8 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
315
332
|
// loose page the file IS the part, so no atlas coordinate is read and
|
|
316
333
|
// #705's and #715's non-measurements cannot arise here — and the scan
|
|
317
334
|
// stops at the first clear texel, as the shared page's does.
|
|
318
|
-
const plate =
|
|
335
|
+
const plate = decoded();
|
|
336
|
+
if (plate === null) continue;
|
|
319
337
|
let transparent = false;
|
|
320
338
|
for (let y = 0; y < plate.height && !transparent; y++) {
|
|
321
339
|
for (let x = 0; x < plate.width; x++) {
|
package/src/compile.ts
CHANGED
|
@@ -7482,7 +7482,10 @@ function buildMesh(
|
|
|
7482
7482
|
);
|
|
7483
7483
|
|
|
7484
7484
|
return {
|
|
7485
|
-
kind
|
|
7485
|
+
// `kind` is what chose the builder above, so it is the claim; `geometry.kind`
|
|
7486
|
+
// is the wider `MeshKind`, which a strict tsc refuses here (and a dependant
|
|
7487
|
+
// with `strict: true` reads this file through `src/render.ts`).
|
|
7488
|
+
kind,
|
|
7486
7489
|
attachment: {
|
|
7487
7490
|
kind: 'mesh',
|
|
7488
7491
|
uvs: geometry.uvs.map(f32),
|