spine-rigc 2.50.0 → 2.51.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/NOTICE.md +2 -2
- package/README.md +6 -6
- package/docs/AUTHORING.md +13 -15
- package/docs/FACE.md +6 -6
- package/docs/INGEST.md +14 -12
- package/package.json +1 -1
- package/src/assertions/bodies/a19.ts +1 -1
- package/src/assertions/facts/atlas_regions.ts +2 -2
- package/src/assertions/facts/skin_entries.ts +1 -3
- package/src/assertions/kinds.ts +7 -2
- package/src/assertions/model/index.ts +0 -4
- package/src/assertions/model/skin_entries.ts +2 -4
- package/src/assertions/reasons.ts +1 -1
- package/src/validate.ts +9 -16
- package/src/assertions/bodies/a11.ts +0 -15
- package/src/assertions/bodies/a27.ts +0 -33
package/NOTICE.md
CHANGED
|
@@ -40,8 +40,8 @@ Software's terms, not a licence term of this project:
|
|
|
40
40
|
development dependency for its own gate, and so does an install with
|
|
41
41
|
`spine-core` added beside it. Validation is not optional on either: the gate
|
|
42
42
|
runs through the runtime where it is installed and through rigc's own validator
|
|
43
|
-
where it is not, and the two are held to the same verdicts in CI on
|
|
44
|
-
|
|
43
|
+
where it is not, and the two are held to the same verdicts in CI on 48 of the
|
|
44
|
+
49 assertions — the official parser's own parse runs only where the runtime is.
|
|
45
45
|
|
|
46
46
|
> **If a product integrates a Spine Runtime to play rigc's output, that integration
|
|
47
47
|
> is subject to the terms above** — rigc neither creates those terms nor removes
|
package/README.md
CHANGED
|
@@ -65,12 +65,12 @@ it cuts both ways:
|
|
|
65
65
|
is a development dependency: this repository uses it to hold rigc's own validator
|
|
66
66
|
to the round trip's verdicts — in CI, on the public example recipes and every
|
|
67
67
|
selftest call, and on a private corpus of production rigs measured before each
|
|
68
|
-
release. Of the
|
|
68
|
+
release. Of the 49 assertions, 48 run on both; `A00_ROUNDTRIP_PARSE`, the official
|
|
69
69
|
parser's own parse, runs only where the runtime is installed and is reported as a
|
|
70
70
|
SKIP elsewhere. There, in the parse's place, the report also carries the model
|
|
71
71
|
side's own two parse rules, `A00_MODEL_READ` and `A00_MODEL_REGIONS_ON_PAGES`,
|
|
72
72
|
which no round-trip rule matches and the registry does not hold — so its summary
|
|
73
|
-
totals
|
|
73
|
+
totals 51 on the core entry, and its last line counts 42 rules on the model side
|
|
74
74
|
([AUTHORING §5.2](docs/AUTHORING.md) has a row for each). Install it beside rigc
|
|
75
75
|
(`bun add -d @esotericsoftware/spine-core`) and the same `rigc` runs the round
|
|
76
76
|
trip on every build. The
|
|
@@ -620,10 +620,10 @@ and `bun run fetch-examples`. The reasoning behind them is in
|
|
|
620
620
|
|
|
621
621
|
`build` and `validate` both default to `--profile spine` — the 35 validity rules, which
|
|
622
622
|
ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile rig-play`
|
|
623
|
-
adds all
|
|
623
|
+
adds all 49: the other 14 are one renderer's policy and one canvas budget's, and they
|
|
624
624
|
fire on perfectly correct editor-produced Spine data, which is why they are opt-in.
|
|
625
625
|
⇒ **That reason is about foreign data and does not carry to a rig you are authoring
|
|
626
|
-
yourself: author under `--profile rig-play` and read the extra
|
|
626
|
+
yourself: author under `--profile rig-play` and read the extra 14 as findings, and
|
|
627
627
|
gate the release under `--profile spine`.** A `deform` key that folds a mesh inside
|
|
628
628
|
out is written out under the default and refused by name under `rig-play`, which
|
|
629
629
|
is the shape of what that split buys you. A report always names
|
|
@@ -782,7 +782,7 @@ letting `A17` blame the editor for the harness's own doing.
|
|
|
782
782
|
| 📥 **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
|
|
783
783
|
| 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
|
|
784
784
|
| 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | **the Spine 4.3 format, field by field.** Every key the 4.3 JSON parser and atlas reader take, what each defaults to, and the parser line it comes from — the rows rigc's refusals cite by part number. Ships in the package too |
|
|
785
|
-
| 🎓 **[the benchmark dossier](https://github.com/rig-c/core/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the
|
|
785
|
+
| 🎓 **[the benchmark dossier](https://github.com/rig-c/core/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 49 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
|
|
786
786
|
| 📋 [LADDER.md](https://github.com/rig-c/core/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/rig-c/core/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/rig-c/core/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
|
|
787
787
|
| 🔬 [SURVEY_2026-08-22.md](https://github.com/rig-c/core/blob/main/docs/SURVEY_2026-08-22.md) | the survey the ladder was climbed by: rigc and the official examples as measured on 2026-08-22, and the gap list ordered by rung. A dated record, not kept current. Repository material |
|
|
788
788
|
| 🧬 [GENERATIONS.md](https://github.com/rig-c/core/blob/main/docs/GENERATIONS.md) | **Spine data from another generation.** Why a 3.8–4.2 file read as 4.3 fails in silence, the policy that follows (detect from `skeleton.spine`, never guess, play on the matching runtime), what `A16` and `ingest` do with such a file, and the editor as the migration path. Repository material |
|
|
@@ -832,7 +832,7 @@ quality."* All six, with their verdicts, are in
|
|
|
832
832
|
[docs/LADDER.md](https://github.com/rig-c/core/blob/main/docs/LADDER.md).
|
|
833
833
|
|
|
834
834
|
The whole dossier — the yardstick, `diff` and `check` and what neither of them can
|
|
835
|
-
see, every rung, the run viewer, the
|
|
835
|
+
see, every rung, the run viewer, the 49 assertions and the selftest behind them — is
|
|
836
836
|
[docs/BENCHMARK.md](https://github.com/rig-c/core/blob/main/docs/BENCHMARK.md).
|
|
837
837
|
Live rung status is
|
|
838
838
|
[docs/LADDER.md](https://github.com/rig-c/core/blob/main/docs/LADDER.md).
|
package/docs/AUTHORING.md
CHANGED
|
@@ -239,7 +239,7 @@ What the flags mean:
|
|
|
239
239
|
| `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
|
|
240
240
|
| `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
|
|
241
241
|
| `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
|
|
242
|
-
| `--profile` | `spine` = the 35 validity rules (**the default**) · `rig-play` = all
|
|
242
|
+
| `--profile` | `spine` = the 35 validity rules (**the default**) · `rig-play` = all 49, opt-in. `spine-html`, the profile's former name, is read as `rig-play` until the next minor release and prints a deprecation line on stderr naming `rig-play` |
|
|
243
243
|
| `--report` | `build` and `repack`: also write the gate's report as a JSON document (`build-report/1`) to this file — every gate's `PASS`, `SKIP` and `FAIL` rows, its summary figures and stats line, which supplier judged it, and every `pack:` line's figures. Written when the command writes `--out` and when a gate is red, before the exit; a file already at the path is removed first, so a compile error or a refused repack leaves none. Never inside `--out`, refused by name. The printed lines are byte-identical with it and without it — **§5.3** |
|
|
244
244
|
| `--candidate` | `check`, `bench`, `render`, `inspect`, `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 |
|
|
245
245
|
| `--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 |
|
|
@@ -2120,7 +2120,7 @@ with **no pages**, and rigc writes `skeleton.atlas` as an **empty file** (zero
|
|
|
2120
2120
|
bytes). Four rules then report SKIP by name rather than a pass, because a page is
|
|
2121
2121
|
their whole subject: `A07_ATLAS_TEXT_SHAPE`, `A06_ATLAS_PAGE_SIZE_MATCHES_PNG`,
|
|
2122
2122
|
`A17_ATLAS_PAGE_FILES_EXIST` and — under `rig-play` —
|
|
2123
|
-
`A19_OVERLAY_PNGS_HAVE_ALPHA
|
|
2123
|
+
`A19_OVERLAY_PNGS_HAVE_ALPHA`.
|
|
2124
2124
|
|
|
2125
2125
|
⚠️ **This is not the case where an attachment WANTS a region.** A region or mesh
|
|
2126
2126
|
attachment that states `width`/`height` and names no `image` still resolves a
|
|
@@ -2457,10 +2457,10 @@ attachment rather than a second set of art.
|
|
|
2457
2457
|
| `convex` | default **false**. True tells the runtime the polygon is convex so it can clip without triangulating it, and a polygon that deforms concave is clipped by its convex hull instead (`ClippingAttachment.convex`). Nothing here checks that the polygon is in fact convex |
|
|
2458
2458
|
| `inverse` | default **false**. True makes everything **outside** the polygon visible instead of everything inside, and inverse clipping is always treated as convex (`ClippingAttachment.inverse`) |
|
|
2459
2459
|
|
|
2460
|
-
⚠️ **
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2460
|
+
⚠️ **No profile refuses a clipping attachment.** Until issue #1404 the rule A11
|
|
2461
|
+
refused one under `--profile rig-play`, because the renderer it was written for,
|
|
2462
|
+
spine-html 0.4.1, skipped clipping silently. The player that profile now names
|
|
2463
|
+
draws a clip in both of its modes, so A11 is retired and both profiles build one.
|
|
2464
2464
|
|
|
2465
2465
|
**Path** ([Spine: paths](http://esotericsoftware.com/spine-paths)) —
|
|
2466
2466
|
`"type": "path"`. The composite cubic Bezier a path **constraint** (§3.5.1) slides
|
|
@@ -6489,7 +6489,7 @@ The report prints one line per assertion:
|
|
|
6489
6489
|
```
|
|
6490
6490
|
PASS A08_REGION_NAMES_MATCH_ATTACHMENTS
|
|
6491
6491
|
SKIP A21_MESH_RIM_PINNED: the skeleton has no weighted mesh attachment, …
|
|
6492
|
-
PROF
|
|
6492
|
+
PROF A12_NO_DARK_COLOR: renderer rule, not in profile "spine"
|
|
6493
6493
|
FAIL A20_MESH_WEIGHTS_COHERENT: mesh "x" vertex 12 weights sum to 0.9000 (see rigc guide A20_MESH_WEIGHTS_COHERENT)
|
|
6494
6494
|
```
|
|
6495
6495
|
|
|
@@ -6502,8 +6502,8 @@ The report prints one line per assertion:
|
|
|
6502
6502
|
that subject and measures each member, so a skeleton with no region, no mesh or
|
|
6503
6503
|
no physics constraint gives it nothing and it **SKIPs**, naming the subject. A
|
|
6504
6504
|
rule named NO_⟨construct⟩ — `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS`,
|
|
6505
|
-
`A02_NO_BONE_TRANSFORM_KEY`, `
|
|
6506
|
-
`
|
|
6505
|
+
`A02_NO_BONE_TRANSFORM_KEY`, `A12_NO_DARK_COLOR`,
|
|
6506
|
+
`A14_NO_FULL_FRAME_MESH` — asks how many of a thing the
|
|
6507
6507
|
artifact carries, and **zero is the answer**, so it **PASSes**. ⇒ On a foreign
|
|
6508
6508
|
export the SKIP list is a free inventory of what the skeleton does not contain,
|
|
6509
6509
|
and the PASS list never contains a rule that looked at nothing.
|
|
@@ -6538,9 +6538,9 @@ prints six rows and is one failure here.
|
|
|
6538
6538
|
`A00_MODEL_READ` and `A00_MODEL_REGIONS_ON_PAGES` — the first two rows of the
|
|
6539
6539
|
table below — which stand where `A00_ROUNDTRIP_PARSE`'s parse stands and have no
|
|
6540
6540
|
round-trip counterpart, so they are not in the registry. That is the whole of the
|
|
6541
|
-
difference between the two totals: `<N>` is
|
|
6542
|
-
and the core report's last line states the split —
|
|
6543
|
-
which are the
|
|
6541
|
+
difference between the two totals: `<N>` is 51 on the core entry and 49 on `cli.ts build`,
|
|
6542
|
+
and the core report's last line states the split — 42 rules on the model side,
|
|
6543
|
+
which are the 40 registry assertions the model side runs and those two; the 8 restated over the emitted text;
|
|
6544
6544
|
and `A00_ROUNDTRIP_PARSE`, which it reports as a SKIP.
|
|
6545
6545
|
|
|
6546
6546
|
🚨 **When `A00_ROUNDTRIP_PARSE` fails, read the report as a report about A00 and
|
|
@@ -6566,13 +6566,12 @@ Fix A00 and run it again.
|
|
|
6566
6566
|
| <a id="A08_REGION_NAMES_MATCH_ATTACHMENTS"></a>`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 `rig-play` 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 |
|
|
6567
6567
|
| <a id="A09_ANIMATION_DURATION_MATCHES_SPEC"></a>`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 |
|
|
6568
6568
|
| <a id="A10_NO_NAN_AFTER_STEPPING"></a>`A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a pose that is not finite. It reads the **whole world transform** — all six terms `a`, `b`, `c`, `d`, `worldX`, `worldY` of every bone — then every vertex of every region and mesh the slots show, at the **setup pose** and at every stepped frame of every animation, through the same scan `render` refuses on, so the two cannot disagree. The detail names where (`the setup pose`, or `animation "x" frame i of 120 (t=…s)`), then the bone and the term (`bone "x" has a NaN`), or the slot, attachment and vertex (`vertex 3 has x Infinity`) when every bone is finite and a vertex is not — a `scaleX` chain whose product stays under the largest double does that. A bone at `rotation: 1e309` has a finite position and a `NaN` matrix, which is why the position alone is not read. Look for a number past the largest double, a degenerate curve or a zero scale. 🦴 **It also poses every bone `inherit` key at its own time**: the runtime resolves a mode by folding the case of its first letter and nothing else, and a spelling that misses is stored as **NaN** — the world position stays finite, `updateWorldTransform` matches no mode, and the bone keeps the rotation, scale and shear it had. The detail names the animation, the bone, the key's time and its spelling, beside the five. A bone whose *setup* spelling misses poses no mode at all, and is named the same way from the stepping loop. Neither is reachable from a rig spec — `build` refuses both spellings by name (§5.1) — so on a green build this clause is about files rigc did not write. Which mode a correct spelling poses is not judged here, because the lookup that resolved it is the one that would be checked. 🧍 **A skeleton with no animation is still read**: its setup pose is the whole of what it shows, so A10 reads it — the world transform and the vertices as above, and the bone's mode and the slot colours a stepped frame is read for, which on a static rig nothing else would reach — and names it `the setup pose` (`the setup pose: slot "x" colour is non-finite`). The stepping half had nothing, and a PASS row carries no detail, so the stats line says so: `nanStepping=skipped`, beside `animations=0`. `A09` does not follow — a declared duration is a fact about an animation and nothing else, so a static rig still gives it nothing. **SKIP** only when the skeleton carries **no bone and no animation**: there is then no pose to read and nothing to step |
|
|
6569
|
-
| <a id="A11_NO_CLIPPING_ATTACHMENTS"></a>`A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
|
|
6570
6569
|
| <a id="A12_NO_DARK_COLOR"></a>`A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored by rig-play's DOM mode. The rule is worded for that mode alone: rig-play's canvas mode carries `dark` on its frame, but no test of the player's holds that it draws it, so nothing here says it does |
|
|
6571
6570
|
| <a id="A13_MESH_BUDGET"></a>`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) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
|
|
6572
6571
|
| <a id="A14_NO_FULL_FRAME_MESH"></a>`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* |
|
|
6573
6572
|
| <a id="A15_IDLE_NO_MESH_BONE_KEYS"></a>`A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. The rule assumes meshes are mostly static — the renderer skips redrawing a mesh nothing moved — so it has three states. **FAIL per bone** when the rig declares nothing: `idle keys bone "X", which drives a mesh — meshes never idle-skip`, and the **first** of those lines, once per run, names the case: `K bone(s) keyed by idle drive meshes; if this idle is meant to deform them (a painting rig), declare invariants.idleDrivesMeshes: { "why": … } in the rig spec — or, where the motion belongs to a pivot above the mesh, key that pivot one link up (FACE.md §3)`. It rides the first finding, so the count of FAIL lines is still the count of bones. **SKIP with the cost** when the rig declares `invariants.idleDrivesMeshes` (§3.7): `declared by the rig ("<why>"): idle keys N bone(s) that drive M mesh attachment(s) totalling V vertices — "a", "b" — and each of those meshes is recomputed on every frame it is shown`. **FAIL on a stale declaration** — declared, and the `idle` keys no mesh-driving bone, carries no bone timeline or does not exist: `the rig "X" declares invariants.idleDrivesMeshes ("…"), but "idle" keys no bone that drives a mesh, so the declaration switches off nothing — remove it`. Otherwise **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 |
|
|
6574
6573
|
| <a id="A16_SKELETON_VERSION_4_3"></a>`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`). |
|
|
6575
|
-
| <a id="A17_ATLAS_PAGE_FILES_EXIST"></a>`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
|
|
6574
|
+
| <a id="A17_ATLAS_PAGE_FILES_EXIST"></a>`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` and `A19`; see `A07` |
|
|
6576
6575
|
| <a id="A18_DETERMINISTIC_EMIT"></a>`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`). |
|
|
6577
6576
|
| <a id="A19_OVERLAY_PNGS_HAVE_ALPHA"></a>`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`). On a rig-spec build the sentence goes on *"this skeleton declares no stage size to measure one against, and the build names no base plate"* and offers *"build from a cut manifest, whose base plate is the part whose window is the crop"*; a manifest build exempts the plate it names, with a stage or without one. 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. |
|
|
6578
6577
|
| <a id="A20_MESH_WEIGHTS_COHERENT"></a>`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 `rig-play` 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 |
|
|
@@ -6582,7 +6581,6 @@ Fix A00 and run it again.
|
|
|
6582
6581
|
| <a id="A24_AXIS_SPACE_STROKE"></a>`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. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it |
|
|
6583
6582
|
| <a id="A25_DETACHED_BONE_PARENTAGE"></a>`A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
|
|
6584
6583
|
| <a id="A26_SLOT_DRAW_ORDER"></a>`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). **SKIP** when the rig declares no canonical slot order. ⚠️ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass: the completeness clause reads it as every declared slot lost and names them |
|
|
6585
|
-
| <a id="A27_REGION_NAME_MATCHES_PAGE_FILENAME"></a>`A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename. **SKIP** when the atlas declares no region |
|
|
6586
6584
|
| <a id="A28_RIBBON_ROWS_SHARE_WEIGHTS"></a>`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, a contour mesh and a `segments` lattice — none has rows rigc paired |
|
|
6587
6585
|
| <a id="A29_STROKE_WITHIN_CONTACT_DEPTH"></a>`A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
|
|
6588
6586
|
| <a id="A30_STROKE_WITHIN_CAP_CONTAINMENT"></a>`A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
|
package/docs/FACE.md
CHANGED
|
@@ -850,13 +850,13 @@ counter-scale the turn stops reading at about **18°**; with it, about **26°**.
|
|
|
850
850
|
Eight degrees of usable range for two tracks, and the record reports nothing else
|
|
851
851
|
in the experiment came close to that ratio.
|
|
852
852
|
|
|
853
|
-
⚠️ **A `clipping` attachment would lift the iris's travel ceiling and
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
853
|
+
⚠️ **A `clipping` attachment would lift the iris's travel ceiling, and the worked
|
|
854
|
+
example has none.** Spine has them (AUTHORING §3.4) and both profiles build one
|
|
855
|
+
since issue #1404 retired A11, but the worked example was drawn while A11 refused
|
|
856
|
+
a clip under `--profile rig-play`. Its ceiling is therefore geometric: the
|
|
857
857
|
iris's ink ring has radius 28.5 against a socket opening 36 half-wide, so
|
|
858
858
|
`36 − 28.5 = 7.5` units is as far as it can travel before it crosses its own lash.
|
|
859
|
-
⇒ **
|
|
859
|
+
⇒ **Without a clip, compute that ceiling from the art before keying a gaze**; the first draft of
|
|
860
860
|
the worked example keyed 9 and had to come back to 7.
|
|
861
861
|
|
|
862
862
|
---
|
|
@@ -1835,7 +1835,7 @@ it come off:
|
|
|
1835
1835
|
MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
|
|
1836
1836
|
MESH hair_bang authored 15 vertices / 16 triangles (budget 32) bones=[bang] attachments=[hair_bang] covers 100.00% of the art, reaching 55.22px past it
|
|
1837
1837
|
.. validate (spine-core round trip + machine assertions, profile spine)
|
|
1838
|
-
.. profile spine —
|
|
1838
|
+
.. profile spine — 6 renderer-policy and 8 archetype assertion(s) do not apply
|
|
1839
1839
|
```
|
|
1840
1840
|
|
|
1841
1841
|
`spine` is the default, so that is the `build` above with no `--profile` on it —
|
package/docs/INGEST.md
CHANGED
|
@@ -137,7 +137,7 @@ rigc validate examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
|
137
137
|
```
|
|
138
138
|
rigc validate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.json
|
|
139
139
|
.. atlas …/examples/3-timing-and-spacing/export/3-timing-and-spacing.atlas
|
|
140
|
-
.. profile spine —
|
|
140
|
+
.. profile spine — 6 renderer-policy and 8 archetype assertion(s) do not apply
|
|
141
141
|
PASS A07_ATLAS_TEXT_SHAPE
|
|
142
142
|
PASS A00_ROUNDTRIP_PARSE
|
|
143
143
|
PASS A16_SKELETON_VERSION_4_3
|
|
@@ -168,7 +168,7 @@ Read it as three separate statements, because they answer three different questi
|
|
|
168
168
|
timeline, no event timeline, no constraint timeline, no deform timeline, no mesh
|
|
169
169
|
attachment, no physics constraint, and no bounding box, clipping attachment or
|
|
170
170
|
path. ⭐ What passes over an empty list is the other kind of rule — `A01`, `A02`,
|
|
171
|
-
`
|
|
171
|
+
`A12`, `A14` ask *how many of this does the file carry*, and **zero is the
|
|
172
172
|
answer**.
|
|
173
173
|
- **`PROF`** — the rule was excluded by the profile before its body ran. §3.3.
|
|
174
174
|
|
|
@@ -985,18 +985,18 @@ prints a `CONSUMER_DRIVEN_MIX` judgement (§2.0), and `build` then SKIPs it by n
|
|
|
985
985
|
runtime is content and the burden is on the rule. Reporting that is a better answer
|
|
986
986
|
than a quietly edited export.
|
|
987
987
|
|
|
988
|
-
### 3.3 Profile choice, and the
|
|
988
|
+
### 3.3 Profile choice, and the fourteen rules that will not fire
|
|
989
989
|
|
|
990
990
|
`--profile spine` is the default and answers *"is this valid Spine 4.3 that any
|
|
991
991
|
runtime plays correctly?"* — and it is the right profile for foreign data, because the
|
|
992
992
|
other one is this project's own renderer and archetype policy.
|
|
993
993
|
|
|
994
|
-
**
|
|
994
|
+
**Fourteen assertions do not run under `spine`, and they come back `PROF`, not
|
|
995
995
|
`SKIP`:**
|
|
996
996
|
|
|
997
997
|
| Excluded as | Rules |
|
|
998
998
|
| --- | --- |
|
|
999
|
-
| **renderer policy** (
|
|
999
|
+
| **renderer policy** (6) | `A12_NO_DARK_COLOR`, `A13_MESH_BUDGET`, `A14_NO_FULL_FRAME_MESH`, `A15_IDLE_NO_MESH_BONE_KEYS`, `A19_OVERLAY_PNGS_HAVE_ALPHA`, `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` |
|
|
1000
1000
|
| **archetype policy** (8) | `A21_MESH_RIM_PINNED`, `A24_AXIS_SPACE_STROKE`, `A25_DETACHED_BONE_PARENTAGE`, `A26_SLOT_DRAW_ORDER`, `A28_RIBBON_ROWS_SHARE_WEIGHTS`, `A29_STROKE_WITHIN_CONTACT_DEPTH`, `A30_STROKE_WITHIN_CAP_CONTAINMENT`, `A39_DEFORM_KEEPS_TRIANGLE_WINDING` |
|
|
1001
1001
|
|
|
1002
1002
|
Two further rules — **`A06`** and **`A20`** — are *mixed*: their validity clauses run
|
|
@@ -1007,7 +1007,7 @@ weighted at all is policy.
|
|
|
1007
1007
|
|
|
1008
1008
|
⚠️ **`--profile rig-play` on foreign data produces a wall of failures that mean
|
|
1009
1009
|
nothing about the file.** Same `spineboy-pro.json`, same atlas, one flag changed — the
|
|
1010
|
-
run ends `rigc:
|
|
1010
|
+
run ends `rigc: 12 assertion(s) failed`, and this is the tally with one real message
|
|
1011
1011
|
per rule — `A06` takes a page that is one part covering it exactly *or a tiling of
|
|
1012
1012
|
regions*, so a packed atlas passes both profiles and the wall is policy only:
|
|
1013
1013
|
|
|
@@ -1015,10 +1015,11 @@ regions*, so a packed atlas passes both profiles and the wall is policy only:
|
|
|
1015
1015
|
| --- | --- | --- |
|
|
1016
1016
|
| **10** | `A15_IDLE_NO_MESH_BONE_KEYS` | `idle keys bone "front-shoulder", which drives a mesh — meshes never idle-skip` |
|
|
1017
1017
|
| **2** | `A20_MESH_WEIGHTS_COHERENT` | `mesh "front-shin" is unweighted; the ring tier drives meshes by bones` |
|
|
1018
|
-
| **1** | `A11_NO_CLIPPING_ATTACHMENTS` | `1 clipping attachment(s); the renderer skips them silently` |
|
|
1019
1018
|
|
|
1020
1019
|
Every one of those is a correct statement about a correct file: a bone *does* key a
|
|
1021
|
-
mesh, a mesh *is* unweighted
|
|
1020
|
+
mesh, and a mesh *is* unweighted. (A third line named the export's clipping
|
|
1021
|
+
attachment until issue #1404 retired that rule: the player `rig-play` names draws
|
|
1022
|
+
clips.)
|
|
1022
1023
|
⇒ **Do not run `rig-play` against
|
|
1023
1024
|
somebody's export unless they asked whether it satisfies this project's renderer
|
|
1024
1025
|
policy**, which is a different question from whether their file is valid.
|
|
@@ -1263,10 +1264,11 @@ Three things to read out of that, in order:
|
|
|
1263
1264
|
something is invisible to every measure in that report. Pair it with a `check` against
|
|
1264
1265
|
frames rendered from the original.
|
|
1265
1266
|
|
|
1266
|
-
⚠️ **Do not rename toward what a rule seems to want.** `
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1267
|
+
⚠️ **Do not rename toward what a rule seems to want.** A `rig-play` policy rule
|
|
1268
|
+
(§3.3) does not fire under the default profile, and renaming somebody's attachments to
|
|
1269
|
+
satisfy a policy they never opted into is a change with no benefit to them. (This
|
|
1270
|
+
paragraph's example was A27, region name against page filename, until issue #1404
|
|
1271
|
+
retired it: no reader joins a region to its page's file name.)
|
|
1270
1272
|
|
|
1271
1273
|
### 4.3 Extending a foreign skeleton with a new animation
|
|
1272
1274
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.51.0",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
25
25
|
// A SKIP for the same reason A06's is (#568). This one is invisible under
|
|
26
26
|
// `spine`, where the profile excludes the rule before its body runs — and
|
|
27
27
|
// that is exactly why it was worth finding: `--profile spine-html` reported
|
|
28
|
-
// it, and A27
|
|
28
|
+
// it, and A27 (retired since, issue #1404), green on an atlas nothing had read.
|
|
29
29
|
if (!atlas) return skip('A19_OVERLAY_PNGS_HAVE_ALPHA', SKIP_NO_ATLAS);
|
|
30
30
|
if (atlas.pages.length === 0) return skip('A19_OVERLAY_PNGS_HAVE_ALPHA', SKIP_NO_ATLAS_PAGE);
|
|
31
31
|
// Every sentence this body prints goes through here, so its end can tell
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* The atlas as the page rules read it (issue #1025, step 4c of #380) — the
|
|
3
3
|
* census's F20 and F21 whole: every page's name, declared size and `pma`, every
|
|
4
4
|
* region's rectangle and the page it sits on, and the region a name resolves
|
|
5
|
-
* to. A06, A19 and
|
|
5
|
+
* to. A06, A19 and A49 read it; A17, which reads only the page names, keeps the
|
|
6
6
|
* narrower `./atlas_pages.ts`.
|
|
7
7
|
*
|
|
8
|
-
* ⭐ **The order is a fact, and it is the file's.** A06
|
|
8
|
+
* ⭐ **The order is a fact, and it is the file's.** A06 prints one line
|
|
9
9
|
* per offending page or region, and A06's overlap clause prints a pair in the
|
|
10
10
|
* order it meets them, so `pages` and `regions` hold the atlas's own order —
|
|
11
11
|
* pages as the file lists them, regions page by page in the file's order — and
|
|
@@ -28,10 +28,8 @@ export interface RegionEntry {
|
|
|
28
28
|
readonly path?: string;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
/** What the skins hold, for A03
|
|
31
|
+
/** What the skins hold, for A03 and A19. */
|
|
32
32
|
export interface SkinEntryFacts {
|
|
33
33
|
/** Every region attachment of every skin, in the file's walk order (the header's ⭐). */
|
|
34
34
|
readonly regionAttachments: readonly RegionEntry[];
|
|
35
|
-
/** How many clipping attachments the skins hold, every skin counted. */
|
|
36
|
-
readonly clippingCount: number;
|
|
37
35
|
}
|
package/src/assertions/kinds.ts
CHANGED
|
@@ -70,6 +70,13 @@ export type AssertionKind = 'validity' | 'renderer' | 'archetype';
|
|
|
70
70
|
* restating it as the renderer's own join made it a tautology over the validity
|
|
71
71
|
* half. The argument, with the renderer lines it is measured against, sits above
|
|
72
72
|
* `check('A08_…')`.
|
|
73
|
+
*
|
|
74
|
+
* The numbering has gaps, and they are retirements, not reservations: A41
|
|
75
|
+
* (issue #1202), and A11 and A27 (issue #1404) — two `renderer` rules stating
|
|
76
|
+
* limits the player `rig-play` names does not have, since it draws a clipping
|
|
77
|
+
* attachment and a region on a page named for something else in both of its
|
|
78
|
+
* modes. A retired code is not reused, so a line that names one in an old
|
|
79
|
+
* report still means what it meant.
|
|
73
80
|
*/
|
|
74
81
|
export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
|
|
75
82
|
A00_ROUNDTRIP_PARSE: 'validity',
|
|
@@ -83,7 +90,6 @@ export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
|
|
|
83
90
|
A08_REGION_NAMES_MATCH_ATTACHMENTS: 'validity', // no longer mixed — see above (#574)
|
|
84
91
|
A09_ANIMATION_DURATION_MATCHES_SPEC: 'validity',
|
|
85
92
|
A10_NO_NAN_AFTER_STEPPING: 'validity',
|
|
86
|
-
A11_NO_CLIPPING_ATTACHMENTS: 'renderer',
|
|
87
93
|
A12_NO_DARK_COLOR: 'renderer',
|
|
88
94
|
A13_MESH_BUDGET: 'renderer',
|
|
89
95
|
A14_NO_FULL_FRAME_MESH: 'renderer',
|
|
@@ -99,7 +105,6 @@ export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
|
|
|
99
105
|
A24_AXIS_SPACE_STROKE: 'archetype',
|
|
100
106
|
A25_DETACHED_BONE_PARENTAGE: 'archetype',
|
|
101
107
|
A26_SLOT_DRAW_ORDER: 'archetype',
|
|
102
|
-
A27_REGION_NAME_MATCHES_PAGE_FILENAME: 'renderer',
|
|
103
108
|
A28_RIBBON_ROWS_SHARE_WEIGHTS: 'archetype',
|
|
104
109
|
A29_STROKE_WITHIN_CONTACT_DEPTH: 'archetype',
|
|
105
110
|
A30_STROKE_WITHIN_CAP_CONTAINMENT: 'archetype',
|
|
@@ -41,7 +41,6 @@ import { ASSERTION_KIND, resolveProfile, type AssertionProfile } from '../kinds.
|
|
|
41
41
|
import { verdictHarness, type VerdictHarness, type Verdicts, type VerdictLists } from '../harness.ts';
|
|
42
42
|
import { SKIP_NO_ATLAS, SKIP_NO_MODEL } from '../reasons.ts';
|
|
43
43
|
import { a03RegionWidthHeightFinite } from '../bodies/a03.ts';
|
|
44
|
-
import { a11NoClippingAttachments } from '../bodies/a11.ts';
|
|
45
44
|
import { a17AtlasPageFilesExist } from '../bodies/a17.ts';
|
|
46
45
|
import { a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten } from '../bodies/a45.ts';
|
|
47
46
|
import { A00_MODEL_READ, A00_MODEL_REGIONS_ON_PAGES, MODEL_PARSE_KIND, modelRead, modelRegionsOnPages, SKIP_NO_MODEL_DOCUMENT, SKIP_NO_MODEL_PAGES, type ReadDocument } from './parse.ts';
|
|
@@ -69,7 +68,6 @@ import { modelSetupWorld } from './setup_world.ts';
|
|
|
69
68
|
import type { SetupWorldFacts } from '../facts/setup_world.ts';
|
|
70
69
|
import { modelStageBox } from './stage_box.ts';
|
|
71
70
|
import type { StageBoxFacts } from '../facts/stage_box.ts';
|
|
72
|
-
import { a27RegionNameMatchesPageFilename } from '../bodies/a27.ts';
|
|
73
71
|
import { modelAnimatedBones } from './animated_bones.ts';
|
|
74
72
|
import { modelSkinMembers } from './skin_members.ts';
|
|
75
73
|
import { modelRegionJoins } from './region_joins.ts';
|
|
@@ -290,7 +288,6 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
|
|
|
290
288
|
{ code: 'A03_REGION_WIDTH_HEIGHT_FINITE', run: (v, read, _input, supply) => a03RegionWidthHeightFinite(v, supply.skinEntries(read)), unread: SKIP_NO_MODEL },
|
|
291
289
|
{ code: 'A04_MESH_TRIANGLES_AND_ENCODING', run: (v, read, _input, supply) => a04MeshTrianglesAndEncoding(v, supply.meshes(read)), unread: SKIP_NO_MODEL },
|
|
292
290
|
{ code: 'A33_VERTEX_ATTACHMENT_GEOMETRY', run: (v, read, _input, supply) => a33VertexAttachmentGeometry(v, supply.polygons(read)), unread: SKIP_NO_MODEL },
|
|
293
|
-
{ code: 'A11_NO_CLIPPING_ATTACHMENTS', run: (v, read, _input, supply) => a11NoClippingAttachments(v, supply.skinEntries(read)), unread: SKIP_NO_MODEL },
|
|
294
291
|
{ code: 'A13_MESH_BUDGET', run: (v, read, input, supply) => a13MeshBudget(v, supply.meshes(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL },
|
|
295
292
|
{ code: 'A14_NO_FULL_FRAME_MESH', run: (v, read, input, supply) => a14NoFullFrameMesh(v, supply.meshes(read), supply.stage(read, input)), unread: SKIP_NO_MODEL },
|
|
296
293
|
{ code: 'A15_IDLE_NO_MESH_BONE_KEYS', run: (v, read, input, supply) => a15IdleNoMeshBoneKeys(v, supply.meshes(read), supply.animatedBones(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL },
|
|
@@ -320,7 +317,6 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
|
|
|
320
317
|
{ code: 'A24_AXIS_SPACE_STROKE', run: (v, read, input, supply) => a24AxisSpaceStroke(v, supply.boneTimelines(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
321
318
|
{ code: 'A25_DETACHED_BONE_PARENTAGE', run: (v, read, input, supply) => a25DetachedBoneParentage(v, supply.skeletonRoster(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
322
319
|
{ code: 'A26_SLOT_DRAW_ORDER', run: (v, read, input, supply) => a26SlotDrawOrder(v, supply.skeletonRoster(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
323
|
-
{ code: 'A27_REGION_NAME_MATCHES_PAGE_FILENAME', run: (v, read, input, supply) => a27RegionNameMatchesPageFilename(v, supply.atlasRegions(read, input)), unread: SKIP_NO_ATLAS },
|
|
324
320
|
{ code: 'A28_RIBBON_ROWS_SHARE_WEIGHTS', run: (v, read, input, supply) => a28RibbonRowsShareWeights(v, supply.meshes(read), supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
|
|
325
321
|
{ code: 'A29_STROKE_WITHIN_CONTACT_DEPTH', run: (v, read, input, supply) => a29StrokeWithinContactDepth(v, supply.boneTimelines(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
326
322
|
{ code: 'A30_STROKE_WITHIN_CAP_CONTAINMENT', run: (v, read, input, supply) => a30StrokeWithinCapContainment(v, supply.boneTimelines(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL, beforeTheParse: true },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The model side's supply of `SkinEntryFacts` (issue #1025, step 4c of #380):
|
|
3
|
-
* the skins' region attachments
|
|
3
|
+
* the skins' region attachments, read off the document.
|
|
4
4
|
*
|
|
5
5
|
* ⭐ **In the file's order, by the emitter's own rule.** The document holds the
|
|
6
6
|
* skins in the spec's order and the Spine file holds them in the editor's
|
|
@@ -55,9 +55,7 @@ export function fileOrderedEntries(read: ReadDocument, order?: SkinOrder): Array
|
|
|
55
55
|
|
|
56
56
|
export function modelSkinEntries(read: ReadDocument, order?: SkinOrder): SkinEntryFacts {
|
|
57
57
|
const regionAttachments: RegionEntry[] = [];
|
|
58
|
-
let clippingCount = 0;
|
|
59
58
|
for (const entry of fileOrderedEntries(read, order)) {
|
|
60
|
-
if (entry.record.kind === 'clipping') clippingCount++;
|
|
61
59
|
if (entry.record.kind !== 'region') continue;
|
|
62
60
|
const geometry = entry.record.geometry;
|
|
63
61
|
// `readModel` reads every region record's geometry or refuses the document, so a record without one is this module's defect.
|
|
@@ -65,5 +63,5 @@ export function modelSkinEntries(read: ReadDocument, order?: SkinOrder): SkinEnt
|
|
|
65
63
|
const shown = shownRow(entry);
|
|
66
64
|
regionAttachments.push({ name: shown.name, width: geometry.region.width, height: geometry.region.height, path: shown.path as string });
|
|
67
65
|
}
|
|
68
|
-
return { regionAttachments
|
|
66
|
+
return { regionAttachments };
|
|
69
67
|
}
|
|
@@ -28,7 +28,7 @@ export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure
|
|
|
28
28
|
/**
|
|
29
29
|
* The third of them, and it is NOT waiting on A00 (issue #608): the atlas parsed
|
|
30
30
|
* and it has no pages, which is what a compile that measured no art writes. The
|
|
31
|
-
*
|
|
31
|
+
* three rules whose only subject is a page — A06, A17, A19 — have nothing to
|
|
32
32
|
* look at, and an empty loop walking out of `check()` is reported as a PASS.
|
|
33
33
|
*/
|
|
34
34
|
/*
|
package/src/validate.ts
CHANGED
|
@@ -84,7 +84,6 @@ import type { SkinEntryFacts } from './assertions/facts/skin_entries.ts';
|
|
|
84
84
|
import type { AtlasPageFacts } from './assertions/facts/atlas_pages.ts';
|
|
85
85
|
import type { SlotColourFacts, SlotTimelines } from './assertions/facts/slot_colour.ts';
|
|
86
86
|
import { a03RegionWidthHeightFinite } from './assertions/bodies/a03.ts';
|
|
87
|
-
import { a11NoClippingAttachments } from './assertions/bodies/a11.ts';
|
|
88
87
|
import { a17AtlasPageFilesExist } from './assertions/bodies/a17.ts';
|
|
89
88
|
import { a18DeterministicEmit } from './assertions/bodies/a18.ts';
|
|
90
89
|
import { a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten } from './assertions/bodies/a45.ts';
|
|
@@ -108,7 +107,6 @@ import { a50StageBoxIsTheStage } from './assertions/bodies/a50.ts';
|
|
|
108
107
|
import { a51BonesLieAlongTheirDeclaredAxis } from './assertions/bodies/a51.ts';
|
|
109
108
|
import { inactiveAtSetup, type SetupWorldFacts } from './assertions/facts/setup_world.ts';
|
|
110
109
|
import { SKIP_NO_STAGE_BOX, type StageBoxFacts } from './assertions/facts/stage_box.ts';
|
|
111
|
-
import { a27RegionNameMatchesPageFilename } from './assertions/bodies/a27.ts';
|
|
112
110
|
import type { MeshEntry as MeshAttachmentEntry, MeshFacts } from './assertions/facts/mesh_attachments.ts';
|
|
113
111
|
import type { ClipEnd, PolygonEntry, PolygonFacts } from './assertions/facts/vertex_polygons.ts';
|
|
114
112
|
import type { LinkEntry, LinkFacts } from './assertions/facts/linked_meshes.ts';
|
|
@@ -846,14 +844,12 @@ export function timelineAddCells(data: ReturnType<SkeletonJson['readSkeletonData
|
|
|
846
844
|
*/
|
|
847
845
|
export function spineSkinEntries(data: ReturnType<SkeletonJson['readSkeletonData']>): SkinEntryFacts {
|
|
848
846
|
const regionAttachments: RegionAttachment[] = [];
|
|
849
|
-
let clippingCount = 0;
|
|
850
847
|
for (const skin of data.skins) {
|
|
851
848
|
for (const entry of skin.getAttachments()) {
|
|
852
849
|
if (entry.attachment instanceof RegionAttachment) regionAttachments.push(entry.attachment);
|
|
853
|
-
else if (entry.attachment instanceof ClippingAttachment) clippingCount++;
|
|
854
850
|
}
|
|
855
851
|
}
|
|
856
|
-
return { regionAttachments
|
|
852
|
+
return { regionAttachments };
|
|
857
853
|
}
|
|
858
854
|
|
|
859
855
|
/**
|
|
@@ -2668,8 +2664,9 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2668
2664
|
// `./assertions/bodies/a33.ts` (issue #1025, cut 4c-2).
|
|
2669
2665
|
check('A33_VERTEX_ATTACHMENT_GEOMETRY', () => a33VertexAttachmentGeometry(verdicts, polygonFacts()));
|
|
2670
2666
|
|
|
2671
|
-
// ---
|
|
2672
|
-
|
|
2667
|
+
// --- A13 / A14: canvas budgets ----
|
|
2668
|
+
// A11 stood here until issue #1404 retired it: the player `rig-play` names
|
|
2669
|
+
// draws a clipping attachment in both of its modes.
|
|
2673
2670
|
// 📐 The two numbers come from the rig spec's `invariants`, never from here.
|
|
2674
2671
|
// A mesh budget is one consumer's frame time written down — the editor's own
|
|
2675
2672
|
// example projects ship meshes many times denser and they are valid — so a
|
|
@@ -3386,15 +3383,11 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
3386
3383
|
// what a `drawOrder` key's offsets are counted against.
|
|
3387
3384
|
check('A26_SLOT_DRAW_ORDER', () => a26SlotDrawOrder(verdicts, rawSkeletonRoster(raw), input));
|
|
3388
3385
|
|
|
3389
|
-
//
|
|
3390
|
-
//
|
|
3391
|
-
//
|
|
3392
|
-
//
|
|
3393
|
-
//
|
|
3394
|
-
// called anything at all, every attachment could agree with it, and the rig
|
|
3395
|
-
// would load with the wrong pixels under the right name. One part per page (A06
|
|
3396
|
-
// forces it) makes the check exact.
|
|
3397
|
-
check('A27_REGION_NAME_MATCHES_PAGE_FILENAME', () => a27RegionNameMatchesPageFilename(verdicts, { atlas }));
|
|
3386
|
+
// A27 stood here until issue #1404 retired it. It held a page's only region
|
|
3387
|
+
// to the page's file basename, a join no reader makes: spine-core and the
|
|
3388
|
+
// player `rig-play` names both take a region off the page record the atlas
|
|
3389
|
+
// states it under, and the player draws a region on a page named for
|
|
3390
|
+
// something else in both of its modes.
|
|
3398
3391
|
|
|
3399
3392
|
// --- A28: a ribbon's rows share their weights ----------------------------
|
|
3400
3393
|
//
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A11, the body (issue #1025, step 4c of #380): no clipping attachment — the
|
|
3
|
-
* renderer skips them silently. A `renderer` rule, so `spine` never runs it.
|
|
4
|
-
*
|
|
5
|
-
* Moved out of `src/validate.ts` unchanged but for what it reads: the count is
|
|
6
|
-
* a fact (`../facts/skin_entries.ts`).
|
|
7
|
-
*/
|
|
8
|
-
import type { Verdicts } from '../harness.ts';
|
|
9
|
-
import type { SkinEntryFacts } from '../facts/skin_entries.ts';
|
|
10
|
-
|
|
11
|
-
export function a11NoClippingAttachments({ fail }: Verdicts, { clippingCount }: SkinEntryFacts): void {
|
|
12
|
-
if (clippingCount > 0) {
|
|
13
|
-
fail('A11_NO_CLIPPING_ATTACHMENTS', `${clippingCount} clipping attachment(s); the renderer skips them silently`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A27, the body (issue #1025, step 4c of #380): the only region on a page is
|
|
3
|
-
* named for the page's file.
|
|
4
|
-
*
|
|
5
|
-
* Moved out of `src/validate.ts` unchanged but for what it reads: the regions
|
|
6
|
-
* are a fact (`../facts/atlas_regions.ts`). Why the second link of the
|
|
7
|
-
* attachment -> region -> file chain needs holding stays above the `check`
|
|
8
|
-
* call in `validate()`.
|
|
9
|
-
*/
|
|
10
|
-
import { basename } from 'node:path';
|
|
11
|
-
import type { Verdicts } from '../harness.ts';
|
|
12
|
-
import type { AtlasRegionFacts } from '../facts/atlas_regions.ts';
|
|
13
|
-
import { SKIP_NO_ATLAS, SKIP_NO_ATLAS_REGION } from '../reasons.ts';
|
|
14
|
-
|
|
15
|
-
export function a27RegionNameMatchesPageFilename({ fail, skip }: Verdicts, { atlas }: AtlasRegionFacts): void {
|
|
16
|
-
if (!atlas) return skip('A27_REGION_NAME_MATCHES_PAGE_FILENAME', SKIP_NO_ATLAS);
|
|
17
|
-
if (atlas.regions.length === 0) return skip('A27_REGION_NAME_MATCHES_PAGE_FILENAME', SKIP_NO_ATLAS_REGION);
|
|
18
|
-
const perPage = new Map<string, number>();
|
|
19
|
-
for (const region of atlas.regions) perPage.set(region.page.name, (perPage.get(region.page.name) ?? 0) + 1);
|
|
20
|
-
for (const region of atlas.regions) {
|
|
21
|
-
// A real packer puts many regions on one page and the names stop matching
|
|
22
|
-
// filenames by design. Then this check has nothing to say, so it says
|
|
23
|
-
// nothing rather than something wrong.
|
|
24
|
-
if ((perPage.get(region.page.name) ?? 0) !== 1) continue;
|
|
25
|
-
const expected = basename(region.page.name).replace(/\.png$/i, '');
|
|
26
|
-
if (region.name !== expected) {
|
|
27
|
-
fail(
|
|
28
|
-
'A27_REGION_NAME_MATCHES_PAGE_FILENAME',
|
|
29
|
-
`region "${region.name}" is the only region on page "${region.page.name}", whose basename is "${expected}"`,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|