spine-rigc 2.48.0 → 2.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/AUTHORING.md +30 -23
- package/docs/FACE.md +13 -13
- package/docs/INGEST.md +4 -4
- package/docs/RIGGING.md +3 -3
- package/package.json +1 -1
- package/skills/rigc-face/SKILL.md +1 -1
- package/src/assertions/bodies/a06.ts +1 -1
- package/src/assertions/bodies/a12.ts +6 -3
- package/src/assertions/bodies/a19.ts +6 -6
- package/src/assertions/bodies/a20.ts +2 -2
- package/src/assertions/emitted/index.ts +4 -2
- package/src/assertions/harness.ts +5 -1
- package/src/assertions/kinds.ts +28 -3
- package/src/assertions/model/index.ts +6 -4
- package/src/assertions/report.ts +13 -2
- package/src/atlas.ts +1 -1
- package/src/cli/shared.ts +25 -17
- package/src/cli/spine_commands.ts +1 -1
- package/src/compile.ts +1 -1
- package/src/core/entry.ts +3 -2
- package/src/core/raw.ts +15 -1
- package/src/core/track.ts +48 -4
- package/src/core/tracks.ts +15 -5
- package/src/rig.ts +1 -1
- package/src/validate.ts +9 -8
package/README.md
CHANGED
|
@@ -619,13 +619,13 @@ and `bun run fetch-examples`. The reasoning behind them is in
|
|
|
619
619
|
[the benchmark dossier](https://github.com/rig-c/core/blob/main/docs/BENCHMARK.md).
|
|
620
620
|
|
|
621
621
|
`build` and `validate` both default to `--profile spine` — the 35 validity rules, which
|
|
622
|
-
ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile
|
|
622
|
+
ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile rig-play`
|
|
623
623
|
adds all 51: the other 16 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
|
|
626
|
+
yourself: author under `--profile rig-play` and read the extra 16 as findings, and
|
|
627
627
|
gate the release under `--profile spine`.** A `deform` key that folds a mesh inside
|
|
628
|
-
out is written out under the default and refused by name under `
|
|
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
|
|
630
630
|
the profile it ran and lists what that profile left out.
|
|
631
631
|
|
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**) · `
|
|
242
|
+
| `--profile` | `spine` = the 35 validity rules (**the default**) · `rig-play` = all 51, 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 |
|
|
@@ -306,7 +306,7 @@ reported `already linked`.
|
|
|
306
306
|
|
|
307
307
|
Pick the profile deliberately, and know which one you got by saying nothing. The
|
|
308
308
|
default is `spine`: "is this valid Spine 4.3 that any runtime plays correctly",
|
|
309
|
-
which is the question a rig authored anywhere is asking. `--profile
|
|
309
|
+
which is the question a rig authored anywhere is asking. `--profile rig-play`
|
|
310
310
|
adds one renderer's policy and one project's canvas budget on top, and those
|
|
311
311
|
extra rules fire on perfectly correct Spine data (clipping attachments,
|
|
312
312
|
unweighted meshes, packed atlases) — reach for it when you are shipping into
|
|
@@ -314,8 +314,8 @@ unweighted meshes, packed atlases) — reach for it when you are shipping into
|
|
|
314
314
|
lists what that profile left out, on `PROF` lines.
|
|
315
315
|
|
|
316
316
|
*Packed atlases* are in that list for what a page may hold, not for being packed:
|
|
317
|
-
under `
|
|
318
|
-
--profile
|
|
317
|
+
under `rig-play` a page is **one part per page OR a tiling page**, so `--pack
|
|
318
|
+
--profile rig-play` is an ordinary build — and it is the only one that puts the
|
|
319
319
|
renderer's own rulebook over shared-page sampling. What a *tiling* page has to
|
|
320
320
|
satisfy under that profile is §5.2's `A49` row: no two regions on one page
|
|
321
321
|
drawing over each other — refused where their rectangles overlap **and** their
|
|
@@ -600,7 +600,7 @@ what changes is that two rectangles may overlap where neither footprint is.
|
|
|
600
600
|
against 155 and 44, all by 1, between their own `rect` packs under `pot` and
|
|
601
601
|
`free`. No texel is read from a neighbour (that is three orders of magnitude
|
|
602
602
|
louder, `PK06`).
|
|
603
|
-
- **The gate reads footprints, not rectangles.** Under `--profile
|
|
603
|
+
- **The gate reads footprints, not rectangles.** Under `--profile rig-play`
|
|
604
604
|
`A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` refuses two regions on one page only
|
|
605
605
|
where their rectangles overlap **and** what they draw overlaps — a mesh's
|
|
606
606
|
hull (its UVs over the region's bounds; a linked mesh its source's), a region
|
|
@@ -928,7 +928,7 @@ report carries the rectangle to cut instead of leaving it to be derived:
|
|
|
928
928
|
|
|
929
929
|
- `--atlas-in` cannot recover what a `scale:` quantised away (above), turned or not;
|
|
930
930
|
- the **renderer** profile refuses a turned region outright
|
|
931
|
-
(`A06`, `--profile
|
|
931
|
+
(`A06`, `--profile rig-play`), because that profile is about artifacts rigc
|
|
932
932
|
itself emits and it never packs one turned. Reading a foreign pack and gating
|
|
933
933
|
one under somebody else's renderer policy are different questions.
|
|
934
934
|
|
|
@@ -2057,7 +2057,7 @@ and the inheritance silently falls back to Normal — assertion `A02` refuses it
|
|
|
2057
2057
|
| `bone` | required; must be a bone this rig declares | — |
|
|
2058
2058
|
| `attachment` | the **setup pose** attachment name, or `null` for "show nothing" | must come from here or from `motion.setup` (R3) — **except** on a slot nothing fills, where it can only be `null` and may be left out |
|
|
2059
2059
|
| `color` | `rrggbbaa` tint | opaque white |
|
|
2060
|
-
| `dark` | two-colour tint, `rrggbb`. The **setup** half; §4.4's `rgba2` and `rgb2` tracks key it over time and require it | — (🚫 `A12` under `
|
|
2060
|
+
| `dark` | two-colour tint, `rrggbb`. The **setup** half; §4.4's `rgba2` and `rgb2` tracks key it over time and require it | — (🚫 `A12` under `rig-play`) |
|
|
2061
2061
|
| `blend` | `normal` · `additive` · `multiply` · `screen`. Only the first letter's case is free: `Additive` reads as `additive`, while `ADDITIVE` or `mUlTiPlY` reads as **no mode** in the runtime with no error, so `build` refuses it by name (§5.1). Emitted as written | `normal` |
|
|
2062
2062
|
|
|
2063
2063
|
✅ **Every slot you declare is emitted, in this order.** A slot nothing fills — no
|
|
@@ -2119,7 +2119,7 @@ name no `image` — an empty `default`, or one carrying only a `boundingbox`, a
|
|
|
2119
2119
|
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
|
-
`A17_ATLAS_PAGE_FILES_EXIST` and — under `
|
|
2122
|
+
`A17_ATLAS_PAGE_FILES_EXIST` and — under `rig-play` —
|
|
2123
2123
|
`A19_OVERLAY_PNGS_HAVE_ALPHA` and `A27_REGION_NAME_MATCHES_PAGE_FILENAME`.
|
|
2124
2124
|
|
|
2125
2125
|
⚠️ **This is not the case where an attachment WANTS a region.** A region or mesh
|
|
@@ -2458,8 +2458,8 @@ attachment rather than a second set of art.
|
|
|
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
2460
|
⚠️ **A clipping attachment is refused by the renderer profile and by nothing
|
|
2461
|
-
else.** `A11_NO_CLIPPING_ATTACHMENTS` (§5.2) fires under `--profile
|
|
2462
|
-
because
|
|
2461
|
+
else.** `A11_NO_CLIPPING_ATTACHMENTS` (§5.2) fires under `--profile rig-play`
|
|
2462
|
+
because the renderer it was written for, spine-html 0.4.1, skipped clipping silently; it is one renderer's policy rather
|
|
2463
2463
|
than anything about the data, and the default `spine` profile builds one.
|
|
2464
2464
|
|
|
2465
2465
|
**Path** ([Spine: paths](http://esotericsoftware.com/spine-paths)) —
|
|
@@ -2562,7 +2562,7 @@ window off the attachment's own `image`; a `ring` and a `ribbon` are built from
|
|
|
2562
2562
|
the numbers in this block and rigc compares neither to the PNG. The emitted
|
|
2563
2563
|
`width`/`height` are this `size`, so a `size` that is not the art's own scales the
|
|
2564
2564
|
drawing — measured: a 240x240 part declared `"size": [64, 64]` builds green under
|
|
2565
|
-
`--profile
|
|
2565
|
+
`--profile rig-play`. That is R1 rather than a gap: the compiler emits the
|
|
2566
2566
|
number the spec states and does not re-measure a plate to overrule it.
|
|
2567
2567
|
|
|
2568
2568
|
⭐ **Several grips split the ring by where the rig put them, on this route as on
|
|
@@ -3152,7 +3152,7 @@ one 96x64 plate of two islands under two two-link chains, `cell: 8`,
|
|
|
3152
3152
|
The `influence` line is the falloff's report: the most bones any vertex binds
|
|
3153
3153
|
(read it against `maxBones`), the mean, and how many vertices one bone owns
|
|
3154
3154
|
outright. A named bone that **no** vertex binds is listed there by name — it is
|
|
3155
|
-
in no weight, so no other figure moves — and under `--profile
|
|
3155
|
+
in no weight, so no other figure moves — and under `--profile rig-play`
|
|
3156
3156
|
`A20_MESH_WEIGHTS_COHERENT` refuses it as a declared bone nothing binds. The
|
|
3157
3157
|
`lattice` line says how many cells had art and how many were kept, and when
|
|
3158
3158
|
islands were joined, how many and how many cells the join added. The coverage
|
|
@@ -4413,13 +4413,13 @@ attachment, rgba, rgb, alpha, rgba2, rgb2)`, §5.1's row.
|
|
|
4413
4413
|
has one, so a file keying it without one parses cleanly and then throws in the
|
|
4414
4414
|
player the first time the animation is applied. `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN`
|
|
4415
4415
|
(§5.2) holds the same pairing on a skeleton rigc did not write. 🚫 Both halves
|
|
4416
|
-
of the two-colour tint are refused under `--profile
|
|
4416
|
+
of the two-colour tint are refused under `--profile rig-play`, whose DOM mode
|
|
4417
4417
|
ignores them; the default `spine` profile that `build` runs reports `A12` as
|
|
4418
4418
|
`PROF` and never applies it.
|
|
4419
4419
|
- The six are every slot timeline the format has; anything else is refused
|
|
4420
4420
|
here, and would be refused by the runtime's own reader too (`Invalid timeline
|
|
4421
4421
|
type for a slot`). `A12_NO_DARK_COLOR` refuses `rgb2` — and `rgba2`, and the
|
|
4422
|
-
slot field — in a file under the `
|
|
4422
|
+
slot field — in a file under the `rig-play` profile.
|
|
4423
4423
|
`sequence` is a timeline on an **attachment**, not on a slot — it is the
|
|
4424
4424
|
family beside `deform`, §4.13.
|
|
4425
4425
|
|
|
@@ -4927,8 +4927,15 @@ that poses through its posers (`check`, `bonedist`, `inspect`, `--geometry`), ru
|
|
|
4927
4927
|
the physics clock `2⁻³⁰` faster than the frames, so every such frame takes the steps
|
|
4928
4928
|
exact arithmetic takes; the animation time is unchanged. A spring that has converged
|
|
4929
4929
|
by the loop point therefore closes, which the loop-seam suite's `LS06` holds at all
|
|
4930
|
-
five rates. A player
|
|
4931
|
-
|
|
4930
|
+
five rates. A player that steps the core entry's live tracks at a fixed rate — a
|
|
4931
|
+
capture, an export, a test harness — is the render's case and takes the render's
|
|
4932
|
+
clock: `openTrack` and `openTracks` accept `clock`, what each step moves the physics
|
|
4933
|
+
clock by, and `physicsClockStep` (with `PHYSICS_CLOCK_LEAD`) is exported from
|
|
4934
|
+
`rig-c/core` to pass to it (issue #1396). It moves the physics alone — the track
|
|
4935
|
+
time, the animation time and the events stay on the steps, so the player keeps
|
|
4936
|
+
agreeing with `render` — and left out, a track steps the physics by the step itself,
|
|
4937
|
+
exactly as before. Which clock a player with a real clock uses is the player's
|
|
4938
|
+
question, and rigc does not answer it.
|
|
4932
4939
|
|
|
4933
4940
|
`mix` is a player-side `AnimationStateData` config and is **not** emitted into
|
|
4934
4941
|
skeleton JSON, so the parser is the only check it gets: it is
|
|
@@ -6478,7 +6485,7 @@ name or point the flag at the pack that has it.
|
|
|
6478
6485
|
|
|
6479
6486
|
The report prints one line per assertion:
|
|
6480
6487
|
|
|
6481
|
-
**No run reproduces this:** assembled — one line of each verdict kind; the `PASS` and the `PROF` are verbatim from a `build` this page states, the `SKIP` is cut at the ellipsis and only `--profile
|
|
6488
|
+
**No run reproduces this:** assembled — one line of each verdict kind; the `PASS` and the `PROF` are verbatim from a `build` this page states, the `SKIP` is cut at the ellipsis and only `--profile rig-play` prints it, and the `FAIL` is invented, because a green build prints none
|
|
6482
6489
|
```
|
|
6483
6490
|
PASS A08_REGION_NAMES_MATCH_ATTACHMENTS
|
|
6484
6491
|
SKIP A21_MESH_RIM_PINNED: the skeleton has no weighted mesh attachment, …
|
|
@@ -6554,13 +6561,13 @@ Fix A00 and run it again.
|
|
|
6554
6561
|
| <a id="A03_REGION_WIDTH_HEIGHT_FINITE"></a>`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 |
|
|
6555
6562
|
| <a id="A04_MESH_TRIANGLES_AND_ENCODING"></a>`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 |
|
|
6556
6563
|
| <a id="A05_CURVE_ARRAY_LENGTH"></a>`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`). |
|
|
6557
|
-
| <a id="A06_ATLAS_PAGE_SIZE_MATCHES_PNG"></a>`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 `
|
|
6564
|
+
| <a id="A06_ATLAS_PAGE_SIZE_MATCHES_PNG"></a>`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 `rig-play` also: `pma` and rotation. Two regions on one page over the same texels was a clause here until issue #1099 and is `A49`'s now, read over what the two regions draw rather than over their rectangles. **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 |
|
|
6558
6565
|
| <a id="A07_ATLAS_TEXT_SHAPE"></a>`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/rig-c/core/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`). |
|
|
6559
|
-
| <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 `
|
|
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 |
|
|
6560
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 |
|
|
6561
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 |
|
|
6562
6569
|
| <a id="A11_NO_CLIPPING_ATTACHMENTS"></a>`A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
|
|
6563
|
-
| <a id="A12_NO_DARK_COLOR"></a>`A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
|
|
6570
|
+
| <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 |
|
|
6564
6571
|
| <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 |
|
|
6565
6572
|
| <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* |
|
|
6566
6573
|
| <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 |
|
|
@@ -6568,7 +6575,7 @@ Fix A00 and run it again.
|
|
|
6568
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`, `A19` and `A27`; see `A07` |
|
|
6569
6576
|
| <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`). |
|
|
6570
6577
|
| <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. |
|
|
6571
|
-
| <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 `
|
|
6578
|
+
| <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 |
|
|
6572
6579
|
| <a id="A21_MESH_RIM_PINNED"></a>`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 |
|
|
6573
6580
|
| <a id="A22_MESH_UVS_IN_UNIT_RANGE"></a>`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 |
|
|
6574
6581
|
| <a id="A23_PHYSICS_CONSTRAINT_EFFECTIVE"></a>`A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too — or has `damping` outside `[0, 1]`, which says `` physics "C" has damping 1.5; must be inside [0, 1] — the per-step decay is `damping ** (60 * step)`, and the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: above 1 every velocity grows on every step and the offset diverges ``, the bound read off the row the key's refusal reads; `1` and `0` are inside, since both are finite at every rate — **at rest, and on every physics timeline key**. The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps. As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `` physics "C" has mix 0 and none of the 3 animations keys its mix above 0; the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted — rest it above 0, or key its mix above 0 in an animation ``. ⚠️ Every reason A23 prints for a setup value is that value's row `basis` arm (§4.4's table) — an arithmetic arm names its expression (`physics "C" has massInverse Infinity; mass must be > 0 — at 0 …`), a behavioural one says it is rigc's call and what the value does. A setup `mix` **below** 0 is told the jiggle is applied inverted, not that it is muted: such a constraint is measured moving its bone by exactly the opposite of a positive mix. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md)), so this sentence is met on a file, never on a decompiled rebuild. 🦴 **A lever drive on a bone with no lever** (issue #1195): a constraint driving `rotate`, `shearX` or `scaleX` on a bone whose `length` is 0 (or states none) is refused, at rest, with one sentence per reading. `rotate` and `shearX` chase the bone's tip, so with no tip the solver aims the bone at a **world** direction — measured through rigc's poser on a chain held still for 3 s, a length-0 bone turns 53.204° from a 90° rest and holds there, where length 0.01 moves 0.0000: `` physics "C" drives rotate on bone "B" whose length is 0 — with no tip the solver aims the bone at world angle 0 and holds it there; give "B" a length or drive x/y instead ``. The rest angle is not read: a bone resting at world 0° does not move, but only until a key or a parent turns it. `scaleX` divides the motion along the bone by its length and adds nothing at 0, so the drive does nothing: `` physics "C" drives scaleX on bone "B" whose length is 0, which the solver divides the motion by, so it drives nothing; give "B" a length ``. Fix in the **rig spec**: give the bone its `length` (§3.2), measured off the art it carries, or, for the first sentence, drive the constraint in `x`/`y` in the **motion spec** instead. `x` and `y` read the bone's origin and never its tip, so a constraint driving only those on a length-0 bone is not refused; the editor's own example exports carry them there. The Spine editor's export drops `rotate`, `scaleX` and `shearX` from a physics constraint on a zero-length bone and keeps them on any bone with a length — measured on 4.3.23 and 4.3.26, issue #1196 — so what this clause refuses is also what the editor would silently lose. **SKIP** when the skeleton declares no physics constraint — the same sentence `A36` and `A37` print for their own constraint types |
|
|
@@ -6731,7 +6738,7 @@ Two more limits that are not errors but will shape what you can attempt:
|
|
|
6731
6738
|
1. `build --profile <the one you meant>` exits 0 and the report has **no FAIL**.
|
|
6732
6739
|
Saying nothing means `spine`, so "the one you meant" is a decision either way —
|
|
6733
6740
|
the report's first line names the profile that judged it.
|
|
6734
|
-
📎 The two profiles judge attachment **naming** identically: `
|
|
6741
|
+
📎 The two profiles judge attachment **naming** identically: `rig-play` adds
|
|
6735
6742
|
the renderer and archetype rules and has no opinion about how an attachment is
|
|
6736
6743
|
spelled, so a rig whose named skins share a placeholder (§3.4.9) is green
|
|
6737
6744
|
under either.
|
|
@@ -6752,7 +6759,7 @@ Two more limits that are not errors but will shape what you can attempt:
|
|
|
6752
6759
|
3. Read the `PROF` lines. They are where "was this rig held to that rule at all"
|
|
6753
6760
|
gets answered for everything the profile left out — the renderer policy *and*
|
|
6754
6761
|
the archetype rules. A green under `spine` has been held to neither; a green
|
|
6755
|
-
under `
|
|
6762
|
+
under `rig-play` has been held to both.
|
|
6756
6763
|
4. Run `explain` and read the slots table: every slot you declared **is** there
|
|
6757
6764
|
(§3.3 — `A26_SLOT_DRAW_ORDER` holds that, and a slot nothing fills reads
|
|
6758
6765
|
`setup=null attachments=[]`), so what this step is for is the two things the
|
package/docs/FACE.md
CHANGED
|
@@ -539,7 +539,7 @@ which `rigc explain` prints as a parent column (§9.2).
|
|
|
539
539
|
⭐ **The same reasoning puts the head's pivot one link above the mesh.** A head
|
|
540
540
|
rotates about the top of the neck, not about the middle of its own face, so the
|
|
541
541
|
roll belongs on a `headroll` bone above `head`. There is a rule that enforces it
|
|
542
|
-
under `--profile
|
|
542
|
+
under `--profile rig-play`: `A15_IDLE_NO_MESH_BONE_KEYS` refuses an `idle` that
|
|
543
543
|
keys a bone driving a mesh — its own slot bone or its control bone — because that
|
|
544
544
|
renderer must never idle-skip a mesh. Keying the pivot one link up satisfies it,
|
|
545
545
|
and **the rig that satisfies the assertion is the better rig anyway.**
|
|
@@ -852,7 +852,7 @@ in the experiment came close to that ratio.
|
|
|
852
852
|
|
|
853
853
|
⚠️ **A `clipping` attachment would lift the iris's travel ceiling and you cannot
|
|
854
854
|
have one.** Spine has them (AUTHORING §3.4) but `A11_NO_CLIPPING_ATTACHMENTS`
|
|
855
|
-
refuses one under `--profile
|
|
855
|
+
refuses one under `--profile rig-play`, so a rig carrying one builds on only one
|
|
856
856
|
of the two profiles. The ceiling is then geometric: in the worked example 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.
|
|
@@ -1473,7 +1473,7 @@ bun install # once
|
|
|
1473
1473
|
|
|
1474
1474
|
bun cli.ts build --rig gallery/portrait/rig.json \
|
|
1475
1475
|
--motion gallery/portrait/motion.json \
|
|
1476
|
-
--out gallery/portrait/build --profile
|
|
1476
|
+
--out gallery/portrait/build --profile rig-play
|
|
1477
1477
|
bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
|
|
1478
1478
|
--out gallery/portrait/render
|
|
1479
1479
|
```
|
|
@@ -1481,8 +1481,8 @@ bun cli.ts render --candidate gallery/portrait/build --fps 25 --max 640 \
|
|
|
1481
1481
|
```
|
|
1482
1482
|
MESH head authored 25 vertices / 32 triangles (budget 32) bones=[head] attachments=[head] covers 100.00% of the art, reaching 95.90px past it
|
|
1483
1483
|
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
|
|
1484
|
-
.. validate (spine-core round trip + machine assertions, profile
|
|
1485
|
-
.. profile
|
|
1484
|
+
.. validate (spine-core round trip + machine assertions, profile rig-play)
|
|
1485
|
+
.. profile rig-play — every assertion applies
|
|
1486
1486
|
```
|
|
1487
1487
|
|
|
1488
1488
|
Green — the profile line above says so in the tool's own words.
|
|
@@ -1515,7 +1515,7 @@ for (const k of d.keys) if (k.transform) {
|
|
|
1515
1515
|
await Bun.write("/tmp/swapped.motion.json", JSON.stringify(m, null, 2));
|
|
1516
1516
|
'
|
|
1517
1517
|
bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/swapped.motion.json \
|
|
1518
|
-
--out /tmp/swapped --profile
|
|
1518
|
+
--out /tmp/swapped --profile rig-play
|
|
1519
1519
|
|
|
1520
1520
|
# (b) FOLD IT: evaluate the same closed form at 40°, past the 31.37° of §4.2,
|
|
1521
1521
|
# so the two far columns swap order and the mesh turns inside out. Since
|
|
@@ -1527,23 +1527,23 @@ for (const k of d.keys) if (k.transform) k.transform.degrees = 40;
|
|
|
1527
1527
|
await Bun.write("/tmp/folded.motion.json", JSON.stringify(m, null, 2));
|
|
1528
1528
|
'
|
|
1529
1529
|
bun cli.ts build --rig gallery/portrait/rig.json --motion /tmp/folded.motion.json \
|
|
1530
|
-
--out /tmp/folded --profile
|
|
1530
|
+
--out /tmp/folded --profile rig-play
|
|
1531
1531
|
```
|
|
1532
1532
|
|
|
1533
1533
|
**What comes back from both:**
|
|
1534
1534
|
|
|
1535
1535
|
| | good | (a) one band inverted | (b) mesh folded |
|
|
1536
1536
|
| --- | --- | --- | --- |
|
|
1537
|
-
| `--profile
|
|
1537
|
+
| `--profile rig-play`, **without `A39`** | green | **green, and the same counts** | **green, and the same counts** |
|
|
1538
1538
|
| `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | PASS | **PASS** | **PASS** |
|
|
1539
1539
|
| the `MESH` coverage line | 100.00%, 95.90px past | **byte-identical** | **byte-identical** |
|
|
1540
1540
|
| `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | PASS | PASS | **FAIL, both keys, 8 of 32 triangles** |
|
|
1541
|
-
| `--profile
|
|
1541
|
+
| `--profile rig-play`, **with `A39`** | green | green | **refused, and nothing written** |
|
|
1542
1542
|
| the `DEFORM` block, `head` key 1 `area` | x0.637174 … x1.319122 | **x0.765250 … x1.362834** | **x−0.288121 … x1.820211** |
|
|
1543
1543
|
| … and its `winding` | 32 of 32 kept | 32 of 32 kept | **24 of 32 kept** |
|
|
1544
1544
|
|
|
1545
1545
|
🚨 **All three were green, and the coverage line is the same string in all three,
|
|
1546
|
-
because it reports the SETUP pose.** The `--profile
|
|
1546
|
+
because it reports the SETUP pose.** The `--profile rig-play` row with `A39` is
|
|
1547
1547
|
the only verdict that separates them, and the two rows above it measure only the
|
|
1548
1548
|
setup geometry: `A35` is silent about build (b). `A35` checks that a deform run *fits* its
|
|
1549
1549
|
attachment — an honest and useful check, and orthogonal to whether the numbers in
|
|
@@ -1609,7 +1609,7 @@ bun cli.ts explain --rig gallery/portrait/rig.json \
|
|
|
1609
1609
|
legitimate counter-example is the fold, which is `A39`'s.
|
|
1610
1610
|
|
|
1611
1611
|
**`A39_DEFORM_KEEPS_TRIANGLE_WINDING` closes the half of that gap the fold
|
|
1612
|
-
lives in** — `--profile
|
|
1612
|
+
lives in** — `--profile rig-play`. Build (b) above is refused
|
|
1613
1613
|
by name, on both its keys, with the triangles listed:
|
|
1614
1614
|
|
|
1615
1615
|
```
|
|
@@ -1711,7 +1711,7 @@ bun cli.ts check --candidate /tmp/folded --frames gallery/portrait/re
|
|
|
1711
1711
|
```
|
|
1712
1712
|
|
|
1713
1713
|
⚠️ **The third of those needs a build §9.2's own command will not write.** Build (b)
|
|
1714
|
-
under `--profile
|
|
1714
|
+
under `--profile rig-play` is refused and nothing
|
|
1715
1715
|
lands in `/tmp/folded` — which is the row above it in §9.2's table, working. Take
|
|
1716
1716
|
that candidate from the **default** profile, where `A39` reads `PROF` and the
|
|
1717
1717
|
artifact is written — a profile selects which assertions apply and not what is
|
|
@@ -1840,7 +1840,7 @@ it come off:
|
|
|
1840
1840
|
|
|
1841
1841
|
`spine` is the default, so that is the `build` above with no `--profile` on it —
|
|
1842
1842
|
the report names what the profile leaves out rather than this page counting it.
|
|
1843
|
-
§9.2 runs the same rig under `--profile
|
|
1843
|
+
§9.2 runs the same rig under `--profile rig-play` and quotes what comes back
|
|
1844
1844
|
there; `A13_MESH_BUDGET` and `A15_IDLE_NO_MESH_BONE_KEYS` are among the rules
|
|
1845
1845
|
the default profile excludes.
|
|
1846
1846
|
|
package/docs/INGEST.md
CHANGED
|
@@ -1000,12 +1000,12 @@ other one is this project's own renderer and archetype policy.
|
|
|
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
|
|
1003
|
-
in both profiles and their policy clauses only under `
|
|
1003
|
+
in both profiles and their policy clauses only under `rig-play`. `A06`'s
|
|
1004
1004
|
size-vs-PNG check is validity; rotation and premultiplied alpha are policy (two
|
|
1005
1005
|
regions over the same texels was a policy clause of `A06` until it became `A49`). `A20`'s weight coherence is validity; requiring a mesh to be
|
|
1006
1006
|
weighted at all is policy.
|
|
1007
1007
|
|
|
1008
|
-
⚠️ **`--profile
|
|
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
1010
|
run ends `rigc: 13 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
|
|
@@ -1019,7 +1019,7 @@ regions*, so a packed atlas passes both profiles and the wall is policy only:
|
|
|
1019
1019
|
|
|
1020
1020
|
Every one of those is a correct statement about a correct file: a bone *does* key a
|
|
1021
1021
|
mesh, a mesh *is* unweighted, a clipping attachment *is* present.
|
|
1022
|
-
⇒ **Do not run `
|
|
1022
|
+
⇒ **Do not run `rig-play` against
|
|
1023
1023
|
somebody's export unless they asked whether it satisfies this project's renderer
|
|
1024
1024
|
policy**, which is a different question from whether their file is valid.
|
|
1025
1025
|
|
|
@@ -1264,7 +1264,7 @@ something is invisible to every measure in that report. Pair it with a `check` a
|
|
|
1264
1264
|
frames rendered from the original.
|
|
1265
1265
|
|
|
1266
1266
|
⚠️ **Do not rename toward what a rule seems to want.** `A27`'s
|
|
1267
|
-
region-name-matches-page-filename is `
|
|
1267
|
+
region-name-matches-page-filename is `rig-play` policy (§3.3): under the default
|
|
1268
1268
|
profile it does not fire, and renaming somebody's attachments to satisfy a policy they
|
|
1269
1269
|
never opted into is a change with no benefit to them.
|
|
1270
1270
|
|
package/docs/RIGGING.md
CHANGED
|
@@ -572,7 +572,7 @@ The same rig keys `headroll` and never `head`: *"a head rotates about the top of
|
|
|
572
572
|
neck, not about the middle of its own face."* A renderer policy assertion
|
|
573
573
|
(`A15_IDLE_NO_MESH_BONE_KEYS`, AUTHORING §5) forced that answer independently — two
|
|
574
574
|
arguments, one bone. ⚠️ **It is a renderer rule, so like §10.3's `A25` it only fires
|
|
575
|
-
under `--profile
|
|
575
|
+
under `--profile rig-play`**; under `--profile spine` it reports `PROF` and the
|
|
576
576
|
structural argument is the only one you get. [FACE.md](FACE.md) §3 is the same bone
|
|
577
577
|
from the mesh's side.
|
|
578
578
|
|
|
@@ -1201,7 +1201,7 @@ Declare the pair in `invariants.detached` (AUTHORING §3.7) and it fires:
|
|
|
1201
1201
|
|
|
1202
1202
|
```bash
|
|
1203
1203
|
rigc build --rig detached.rig.json --motion stack.motion.json --images parts \
|
|
1204
|
-
--out det --profile
|
|
1204
|
+
--out det --profile rig-play
|
|
1205
1205
|
```
|
|
1206
1206
|
|
|
1207
1207
|
```
|
|
@@ -1414,7 +1414,7 @@ rigc build --rig stack.rig.json --motion stack.motion.json --images parts --out
|
|
|
1414
1414
|
# .. pages=4 regions=4 bones=6 slots=5 animations=4 version=4.3.13 … profile=spine
|
|
1415
1415
|
```
|
|
1416
1416
|
|
|
1417
|
-
✅ It is green under **both** profiles, so `--profile
|
|
1417
|
+
✅ It is green under **both** profiles, so `--profile rig-play` is available for
|
|
1418
1418
|
§10.3 without any unrelated failure in the report.
|
|
1419
1419
|
|
|
1420
1420
|
### The three variants the sections build
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.50.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": {
|
|
@@ -37,7 +37,7 @@ names the reversed triangles with their signed areas. `invariants.deformMayFold`
|
|
|
37
37
|
in the rig spec is the one declared exemption, so reach for it when the slot
|
|
38
38
|
folds on purpose and never to quiet a refusal you have not read. It is an
|
|
39
39
|
**archetype** rule: a `--profile spine` build prints `PROF` for it and says
|
|
40
|
-
nothing about the fold, so author under `--profile
|
|
40
|
+
nothing about the fold, so author under `--profile rig-play`.
|
|
41
41
|
|
|
42
42
|
What `A39` still cannot say is whether the projection was the right one —
|
|
43
43
|
nothing measures whether 12° was the angle the shot wanted. FACE §9.2 is that
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A06, the body (issue #1025, step 4c of #380): every page the atlas names is
|
|
3
3
|
* the size its PNG is, every region lies inside its page, and — under
|
|
4
|
-
* `
|
|
4
|
+
* `rig-play` — no page claims premultiplied alpha and no region is turned.
|
|
5
5
|
* The clause that refused two regions on one page overlapping is
|
|
6
6
|
* `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` since issue #1099 (`./a49.ts`).
|
|
7
7
|
*
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A12, the body (issue #1025, cut 4c-4 of #380): no dark colour and no
|
|
3
|
-
* two-colour timeline — both parsed, then
|
|
3
|
+
* two-colour timeline — both parsed, then ignored by rig-play's DOM mode.
|
|
4
|
+
* Only the DOM mode is named (issue #1277): rig-play's canvas mode carries
|
|
5
|
+
* `dark` on the frame, but no test of the player's draws it, so nothing here
|
|
6
|
+
* says it is drawn there.
|
|
4
7
|
*
|
|
5
8
|
* Moved out of `src/validate.ts` unchanged but for what it reads: the slots
|
|
6
9
|
* are a fact (`../facts/skeleton_roster.ts`) and the slot timelines are A45's
|
|
@@ -16,14 +19,14 @@ import type { SlotTimelines } from '../facts/slot_colour.ts';
|
|
|
16
19
|
export function a12NoDarkColor({ fail }: Verdicts, { slots }: SkeletonRosterFacts, slotTimelines: readonly SlotTimelines[]): void {
|
|
17
20
|
for (const slot of slots) {
|
|
18
21
|
if (slot.dark) {
|
|
19
|
-
fail('A12_NO_DARK_COLOR', `slot "${slot.name}" declares a dark colour;
|
|
22
|
+
fail('A12_NO_DARK_COLOR', `slot "${slot.name}" declares a dark colour; rig-play's DOM mode ignores it`);
|
|
20
23
|
}
|
|
21
24
|
}
|
|
22
25
|
for (const { animation, slot, timelines } of slotTimelines) {
|
|
23
26
|
for (const [name, keys] of Object.entries(timelines)) {
|
|
24
27
|
if (!Array.isArray(keys)) continue;
|
|
25
28
|
if (name === 'rgba2' || name === 'rgb2') {
|
|
26
|
-
fail('A12_NO_DARK_COLOR', `${animation}.slots.${slot}.${name}: two-colour timeline "${name}" is
|
|
29
|
+
fail('A12_NO_DARK_COLOR', `${animation}.slots.${slot}.${name}: two-colour timeline "${name}" is ignored by rig-play's DOM mode`);
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -145,7 +145,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
145
145
|
// ⭐ Who names it is read off the two bodies, not chosen. Both ask
|
|
146
146
|
// `readPngHeader` (`pngProblem`, the one reader of a page's identity) the
|
|
147
147
|
// same question about the same file, A06 runs under both profiles and this
|
|
148
|
-
// rule under `
|
|
148
|
+
// rule under `rig-play` alone, and A06 FAILs on every page for which it
|
|
149
149
|
// answers with a problem — so on every input that reaches this branch, A06
|
|
150
150
|
// has already named the file and the report is red. A FAIL here would be
|
|
151
151
|
// the second naming and a PASS a certificate over alpha nobody read, so
|
|
@@ -246,7 +246,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
246
246
|
`${region.y} is not where "${region.name}"'s texels are on this file and this rule states ` +
|
|
247
247
|
'nothing about whether it can draw a transparent pixel. The page grid is ' +
|
|
248
248
|
"A06_ATLAS_PAGE_SIZE_MATCHES_PNG's to judge, and it names the ratio and how to declare the page " +
|
|
249
|
-
'honestly. This is renderer policy, and it belongs to --profile
|
|
249
|
+
'honestly. This is renderer policy, and it belongs to --profile rig-play: the default ' +
|
|
250
250
|
'--profile spine does not run this check.',
|
|
251
251
|
);
|
|
252
252
|
}
|
|
@@ -280,7 +280,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
280
280
|
`${plate.width}x${plate.height}, so it states nothing about whether "${region.name}" can draw a ` +
|
|
281
281
|
"transparent pixel. A region's rectangle is A06_ATLAS_PAGE_SIZE_MATCHES_PNG's to judge, and one " +
|
|
282
282
|
'that runs off its page is refused there by name. This is renderer policy, and it belongs to ' +
|
|
283
|
-
'--profile
|
|
283
|
+
'--profile rig-play: the default --profile spine does not run this check.',
|
|
284
284
|
);
|
|
285
285
|
continue;
|
|
286
286
|
}
|
|
@@ -298,7 +298,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
298
298
|
'A19_OVERLAY_PNGS_HAVE_ALPHA',
|
|
299
299
|
`part "${region.name}" is opaque in ${over}, so it would paint a solid rectangle over whatever is ` +
|
|
300
300
|
`drawn behind it. Re-export the part with transparency and pack again. ${exemption} This is ` +
|
|
301
|
-
'renderer policy, and it belongs to --profile
|
|
301
|
+
'renderer policy, and it belongs to --profile rig-play: the default --profile spine does not run ' +
|
|
302
302
|
'this check.',
|
|
303
303
|
);
|
|
304
304
|
}
|
|
@@ -316,7 +316,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
316
316
|
`(${colourTypeName(info.colourType)}) with no tRNS chunk, so it would paint a solid rectangle over ` +
|
|
317
317
|
'whatever is drawn behind it. Re-export it with transparency — as RGBA, or as an indexed or greyscale ' +
|
|
318
318
|
`PNG that keeps its tRNS chunk. ${exemption} This is renderer policy, and it belongs to --profile ` +
|
|
319
|
-
'
|
|
319
|
+
'rig-play: the default --profile spine does not run this check.',
|
|
320
320
|
);
|
|
321
321
|
continue;
|
|
322
322
|
}
|
|
@@ -352,7 +352,7 @@ export function a19OverlayPngsHaveAlpha({ fail: failed, skip }: Verdicts, { atla
|
|
|
352
352
|
`part image "${page.name}" is opaque in every one of its ${plate.width}x${plate.height} texels, so it ` +
|
|
353
353
|
`would paint a solid rectangle over whatever is drawn behind it: its file can hold transparency — ${holds} ` +
|
|
354
354
|
'— and no texel uses it. Re-export the part with the transparency it is meant to have. ' +
|
|
355
|
-
`${exemption} This is renderer policy, and it belongs to --profile
|
|
355
|
+
`${exemption} This is renderer policy, and it belongs to --profile rig-play: the default --profile ` +
|
|
356
356
|
'spine does not run this check.',
|
|
357
357
|
);
|
|
358
358
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A20, the body (issue #1025, step 4c of #380): a weighted mesh's weights
|
|
3
3
|
* cohere — every vertex bound, every weight finite and non-negative, every
|
|
4
|
-
* vertex's weights summing to 1 — and, under `
|
|
4
|
+
* vertex's weights summing to 1 — and, under `rig-play`, a generated mesh is
|
|
5
5
|
* weighted, binds only bones that move it and binds every bone it declares.
|
|
6
6
|
*
|
|
7
7
|
* Moved out of `src/validate.ts` clause by clause, every clause about the
|
|
@@ -72,7 +72,7 @@ export function a20MeshWeightsCoherent({ fail, skip }: Verdicts, { meshes, bones
|
|
|
72
72
|
// In a rigc-GENERATED ring or ribbon it is still a defect: the
|
|
73
73
|
// generator bound a bone that does nothing, which is a bug in the
|
|
74
74
|
// generator and dead work in the runtime's inner loop. So it stays a
|
|
75
|
-
// failure under
|
|
75
|
+
// failure under rig-play and is not one under spine.
|
|
76
76
|
else if (policy && generated && weight === 0) {
|
|
77
77
|
fail(
|
|
78
78
|
'A20_MESH_WEIGHTS_COHERENT',
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*
|
|
23
23
|
* Links nothing from the runtime.
|
|
24
24
|
*/
|
|
25
|
-
import { ASSERTION_KIND, type AssertionProfile } from '../kinds.ts';
|
|
25
|
+
import { ASSERTION_KIND, resolveProfile, type AssertionProfile } from '../kinds.ts';
|
|
26
26
|
import { verdictHarness, type VerdictLists } from '../harness.ts';
|
|
27
27
|
import { MOVED_ASSERTIONS } from '../model/index.ts';
|
|
28
28
|
import type { Verdicts } from '../harness.ts';
|
|
@@ -125,7 +125,9 @@ export function roundTripOnlyCodes(): string[] {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/** The restated rules over one build's emitted texts, in `validate()`'s report shape. */
|
|
128
|
-
export function validateEmittedText(
|
|
128
|
+
export function validateEmittedText(given: EmittedTextInput): VerdictLists & { profile: AssertionProfile } {
|
|
129
|
+
// A former profile name reads as its profile (issue #1277), for the rules and the report alike.
|
|
130
|
+
const input: EmittedTextInput = { ...given, profile: resolveProfile(given.profile, 'the emitted text') };
|
|
129
131
|
const known = new Set(EMITTED_TEXT_RULES.map((r) => r.code));
|
|
130
132
|
const unrun = roundTripOnlyCodes().filter((code) => !known.has(code));
|
|
131
133
|
if (unrun.length > 0) {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* This module links nothing from the runtime, directly or through what it
|
|
19
19
|
* imports — the condition for the model side to use it, held by the selftest.
|
|
20
20
|
*/
|
|
21
|
-
import { kindRunsUnder, type AssertionKind, type AssertionProfile } from './kinds.ts';
|
|
21
|
+
import { kindRunsUnder, resolveProfile, type AssertionKind, type AssertionProfile } from './kinds.ts';
|
|
22
22
|
|
|
23
23
|
/** One failure: the assertion's code and the sentence naming the object, the value found and the value required. */
|
|
24
24
|
export interface Failure {
|
|
@@ -75,6 +75,10 @@ export interface VerdictHarness extends VerdictLists {
|
|
|
75
75
|
* the table lacks — `validate` for the gate, so its message is the one it was.
|
|
76
76
|
*/
|
|
77
77
|
export function verdictHarness(profile: AssertionProfile, kinds: Readonly<Record<string, AssertionKind>>, who: string): VerdictHarness {
|
|
78
|
+
// A former name resolves to its profile and anything outside the pair is
|
|
79
|
+
// refused by name — `resolveProfile` says why. The entries resolve before
|
|
80
|
+
// they get here, so this is the floor for a caller of the harness itself.
|
|
81
|
+
profile = resolveProfile(profile, who);
|
|
78
82
|
const failures: Failure[] = [];
|
|
79
83
|
const passed: string[] = [];
|
|
80
84
|
const skipped: VerdictLists['skipped'] = [];
|
package/src/assertions/kinds.ts
CHANGED
|
@@ -12,7 +12,32 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** The two rulebooks — `ValidateProfile` in `src/validate.ts`, where the choice between them is argued. */
|
|
15
|
-
export type AssertionProfile = 'spine' | '
|
|
15
|
+
export type AssertionProfile = 'spine' | 'rig-play';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A profile's former name, which the CLI's `--profile` still reads for one
|
|
19
|
+
* minor and resolves at parse time, printing the deprecation line that names
|
|
20
|
+
* the profile it reads as (issue #1277). Nothing past the flag parser sees an
|
|
21
|
+
* alias: the rule set is keyed on the profile it resolves to, so the alias and
|
|
22
|
+
* its target cannot come to run different rules.
|
|
23
|
+
*/
|
|
24
|
+
export const PROFILE_ALIASES: Readonly<Record<string, AssertionProfile>> = { 'spine-html': 'rig-play' };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The profile a gate entry runs under: a profile as given, a former name as the
|
|
28
|
+
* profile it became — silently, because the CLI prints the deprecation line and
|
|
29
|
+
* a library caller of `validate()` through a courtesy key passed the former
|
|
30
|
+
* name on the release before this one and must get the same rules — and
|
|
31
|
+
* anything else refused by name. `kindRunsUnder` reads every name but
|
|
32
|
+
* `rig-play` as validity-only, so an unresolved name would run a weaker gate in
|
|
33
|
+
* silence, under a name nobody chose (issue #1277).
|
|
34
|
+
*/
|
|
35
|
+
export function resolveProfile(profile: string, who: string): AssertionProfile {
|
|
36
|
+
if (profile === 'spine' || profile === 'rig-play') return profile;
|
|
37
|
+
const target = Object.hasOwn(PROFILE_ALIASES, profile) ? PROFILE_ALIASES[profile] : undefined;
|
|
38
|
+
if (target !== undefined) return target;
|
|
39
|
+
throw new Error(`${who}: profile ${JSON.stringify(profile)} is not a profile; the profiles are "spine" and "rig-play"`);
|
|
40
|
+
}
|
|
16
41
|
|
|
17
42
|
/** The three kinds a rule can be; the table's own comment says what each means. */
|
|
18
43
|
export type AssertionKind = 'validity' | 'renderer' | 'archetype';
|
|
@@ -100,7 +125,7 @@ export const ASSERTION_KIND: Readonly<Record<string, AssertionKind>> = {
|
|
|
100
125
|
A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS: 'validity',
|
|
101
126
|
};
|
|
102
127
|
|
|
103
|
-
/** Whether `profile` runs a rule of `kind`: `spine` runs validity alone, `
|
|
128
|
+
/** Whether `profile` runs a rule of `kind`: `spine` runs validity alone, `rig-play` every kind. */
|
|
104
129
|
export function kindRunsUnder(kind: AssertionKind, profile: AssertionProfile): boolean {
|
|
105
|
-
return kind === 'validity' || profile === '
|
|
130
|
+
return kind === 'validity' || profile === 'rig-play';
|
|
106
131
|
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* Links nothing from the runtime, directly or through what it imports — held
|
|
38
38
|
* by the selftest's `VF` controls.
|
|
39
39
|
*/
|
|
40
|
-
import { ASSERTION_KIND, type AssertionProfile } from '../kinds.ts';
|
|
40
|
+
import { ASSERTION_KIND, resolveProfile, type AssertionProfile } from '../kinds.ts';
|
|
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';
|
|
@@ -294,7 +294,7 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
|
|
|
294
294
|
{ code: 'A13_MESH_BUDGET', run: (v, read, input, supply) => a13MeshBudget(v, supply.meshes(read), { rig: supply.rigInfo(read, input) }), unread: SKIP_NO_MODEL },
|
|
295
295
|
{ 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
296
|
{ 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 },
|
|
297
|
-
{ code: 'A20_MESH_WEIGHTS_COHERENT', run: (v, read, input, supply) => a20MeshWeightsCoherent(v, supply.meshes(read), input.profile === '
|
|
297
|
+
{ code: 'A20_MESH_WEIGHTS_COHERENT', run: (v, read, input, supply) => a20MeshWeightsCoherent(v, supply.meshes(read), input.profile === 'rig-play', supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
|
|
298
298
|
{ code: 'A21_MESH_RIM_PINNED', run: (v, read, input, supply) => a21MeshRimPinned(v, supply.meshes(read), supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
|
|
299
299
|
{ code: 'A22_MESH_UVS_IN_UNIT_RANGE', run: (v, read, _input, supply) => a22MeshUvsInUnitRange(v, supply.meshes(read)), unread: SKIP_NO_MODEL },
|
|
300
300
|
{ code: 'A39_DEFORM_KEEPS_TRIANGLE_WINDING', run: (v, read, input, supply) => a39DeformKeepsTriangleWinding(v, supply.deformSurvey(read), supply.rigInfo(read, input)), unread: SKIP_NO_MODEL },
|
|
@@ -313,7 +313,7 @@ export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
|
|
|
313
313
|
{ code: 'A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN', run: (v, read, _input, supply) => a44LinkedMeshStatesNoGeometryOfItsOwn(v, supply.links(read)), unread: SKIP_NO_MODEL },
|
|
314
314
|
{ code: 'A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES', run: (v, read, _input, supply) => a46SequenceAttachmentsShowTheFrameTheFileStates(v, supply.sequences(read)), unread: SKIP_NO_MODEL },
|
|
315
315
|
{ code: 'A17_ATLAS_PAGE_FILES_EXIST', run: (v, read, input, supply) => a17AtlasPageFilesExist(v, supply.atlasPages(read), input), unread: SKIP_NO_ATLAS },
|
|
316
|
-
{ code: 'A06_ATLAS_PAGE_SIZE_MATCHES_PNG', run: (v, read, input, supply) => a06AtlasPageSizeMatchesPng(v, supply.atlasRegions(read, input), input, input.profile === '
|
|
316
|
+
{ code: 'A06_ATLAS_PAGE_SIZE_MATCHES_PNG', run: (v, read, input, supply) => a06AtlasPageSizeMatchesPng(v, supply.atlasRegions(read, input), input, input.profile === 'rig-play'), unread: SKIP_NO_ATLAS },
|
|
317
317
|
{ code: 'A19_OVERLAY_PNGS_HAVE_ALPHA', run: (v, read, input, supply) => a19OverlayPngsHaveAlpha(v, supply.atlasRegions(read, input), supply.stage(read, input), supply.skinEntries(read), { atlasDir: input.atlasDir, rig: supply.rigInfo(read, input) }), unread: SKIP_NO_ATLAS },
|
|
318
318
|
{ code: 'A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP', run: (v, read, input, supply) => a49PackedFootprintsDoNotOverlap(v, supply.atlasRegions(read, input), supply.regionJoins(read), supply.meshes(read)), unread: SKIP_NO_ATLAS },
|
|
319
319
|
{ code: 'A50_STAGE_BOX_IS_THE_STAGE', run: (v, read, _input, supply) => a50StageBoxIsTheStage(v, supply.stageBox(read)), unread: SKIP_NO_MODEL },
|
|
@@ -389,7 +389,9 @@ function rigFigures(stats: Record<string, number | string>, read: ReadDocument |
|
|
|
389
389
|
* prints. `plant` replaces a family's supplier; only the selftest's plants
|
|
390
390
|
* pass one.
|
|
391
391
|
*/
|
|
392
|
-
export function validateModel(
|
|
392
|
+
export function validateModel(given: ModelValidateInput, plant: Partial<ModelSupply> = {}): ModelReport {
|
|
393
|
+
// A former profile name reads as its profile (issue #1277), for the policy switches and the report alike.
|
|
394
|
+
const input: ModelValidateInput = { ...given, profile: resolveProfile(given.profile, 'the model side') };
|
|
393
395
|
const supply: ModelSupply = { ...MODEL_SUPPLY, ...plant };
|
|
394
396
|
const h = verdictHarness(input.profile, { ...ASSERTION_KIND, ...MODEL_PARSE_KIND }, 'the model side');
|
|
395
397
|
const { read, held } = parse(h, input.modelText);
|
package/src/assertions/report.ts
CHANGED
|
@@ -10,7 +10,18 @@ import type { AssertionProfile } from './kinds.ts';
|
|
|
10
10
|
import type { VerdictLists } from './harness.ts';
|
|
11
11
|
import { failLine } from '../guide.ts';
|
|
12
12
|
|
|
13
|
-
export const VALIDATE_PROFILES: readonly AssertionProfile[] = ['spine', '
|
|
13
|
+
export const VALIDATE_PROFILES: readonly AssertionProfile[] = ['spine', 'rig-play'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The line `--profile <alias>` prints on stderr before the command runs: the
|
|
17
|
+
* old name, the profile it reads as, and the flag to pass instead (issue
|
|
18
|
+
* #1277). Stderr so a report on stdout reads the same whichever name the
|
|
19
|
+
* caller typed; the report itself names the profile that judged it, which is
|
|
20
|
+
* the target and never the alias.
|
|
21
|
+
*/
|
|
22
|
+
export function profileAliasLine(alias: string, target: AssertionProfile): string {
|
|
23
|
+
return `rigc: --profile ${alias} is deprecated and reads as --profile ${target}, the same rules; it is accepted until the next minor release — pass --profile ${target}`;
|
|
24
|
+
}
|
|
14
25
|
|
|
15
26
|
/**
|
|
16
27
|
* What the CLI uses when `--profile` is absent — and ONLY the CLI. This is not
|
|
@@ -21,7 +32,7 @@ export const VALIDATE_PROFILES: readonly AssertionProfile[] = ['spine', 'spine-h
|
|
|
21
32
|
* into the Spine editor", which is exactly the question `spine` asks, and a
|
|
22
33
|
* stranger's first build was being judged instead against one renderer's policy
|
|
23
34
|
* and one project's canvas budget — 14 rules they have no stake in, with the
|
|
24
|
-
* escape hatch documented only in prose. Defaults beat prose. `
|
|
35
|
+
* escape hatch documented only in prose. Defaults beat prose. `rig-play` is
|
|
25
36
|
* still one flag away, and every report names the profile that judged it.
|
|
26
37
|
*/
|
|
27
38
|
export const CLI_DEFAULT_PROFILE: AssertionProfile = 'spine';
|
package/src/atlas.ts
CHANGED
|
@@ -553,7 +553,7 @@ export interface EmitPage {
|
|
|
553
553
|
* against the unturned pack. But on neither rig did a turn shrink the page: both
|
|
554
554
|
* were already on the smallest power-of-two page their area allows, and a turn
|
|
555
555
|
* that buys no area only moves bytes. Shipping it would also need two clauses
|
|
556
|
-
* changed that say rigc never turns a region — `A06`'s under `
|
|
556
|
+
* changed that say rigc never turns a region — `A06`'s under `rig-play`
|
|
557
557
|
* ([`src/validate.ts`](validate.ts)) and `artUvsOf`
|
|
558
558
|
* ([`src/render.ts`](render.ts)), which returns no art UVs for a turned region
|
|
559
559
|
* attachment, so `check`'s texture substitution would report it unmatched.
|
package/src/cli/shared.ts
CHANGED
|
@@ -26,10 +26,11 @@ import {
|
|
|
26
26
|
CLI_DEFAULT_PROFILE,
|
|
27
27
|
type GateHere,
|
|
28
28
|
type PackPageFigures,
|
|
29
|
+
profileAliasLine,
|
|
29
30
|
VALIDATE_PROFILES,
|
|
30
31
|
} from '../assertions/report.ts';
|
|
31
32
|
import type { VerdictLists } from '../assertions/harness.ts';
|
|
32
|
-
import type
|
|
33
|
+
import { PROFILE_ALIASES, type AssertionProfile } from '../assertions/kinds.ts';
|
|
33
34
|
import { MAX_CANDIDATES, MIN_CANDIDATES } from '../ballot.ts';
|
|
34
35
|
import { BONEDIST_SPEC, IDENTITY_CORRESPONDENCE } from '../correspondence.ts';
|
|
35
36
|
import { ANCHOR_MAX_RESIDUAL, ANCHOR_MAX_UNEXPLAINED, DEFAULT_HINGE_MAX, DEFAULT_HINGE_MIN, DEFAULT_MIN_LEVER_PX, DEFAULT_MIN_VISIBLE, DEFAULT_PASSES } from '../chainfit.ts';
|
|
@@ -988,7 +989,7 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
988
989
|
cuts: 'the cuts.json --cut names',
|
|
989
990
|
profile:
|
|
990
991
|
'which rulebook to check against (default: spine) — spine = valid Spine 4.3 that any runtime plays ' +
|
|
991
|
-
"correctly;
|
|
992
|
+
"correctly; rig-play = also this project's renderer/archetype policy",
|
|
992
993
|
atlas: "the candidate's atlas, when it is not beside the skeleton",
|
|
993
994
|
reference: 'the reference skeleton to pose beside the candidate — a directory or a skeleton.json path',
|
|
994
995
|
'reference-atlas': "the reference's atlas, when it is not beside the reference skeleton",
|
|
@@ -1179,7 +1180,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1179
1180
|
manifest: '<path>',
|
|
1180
1181
|
cut: '<name>',
|
|
1181
1182
|
cuts: '<path>',
|
|
1182
|
-
profile: 'spine|
|
|
1183
|
+
profile: 'spine|rig-play',
|
|
1183
1184
|
atlas: '<path>',
|
|
1184
1185
|
'atlas-in': '<file.atlas>',
|
|
1185
1186
|
'page-size': '<px>',
|
|
@@ -1324,7 +1325,7 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1324
1325
|
for: 'the gate round-trips every build through it before anything is written',
|
|
1325
1326
|
core: {
|
|
1326
1327
|
usage: [
|
|
1327
|
-
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|
|
|
1328
|
+
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|rig-play] [--copy-images] [--report <file>] (the same files the round-tripped build writes; gated without spine-core — see build --help)',
|
|
1328
1329
|
`rigc build … --pack [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] (parts onto shared pages, written into --out)`,
|
|
1329
1330
|
'rigc build … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made)',
|
|
1330
1331
|
'rigc build --cut <name> --cuts <cuts.json>',
|
|
@@ -1348,7 +1349,7 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1348
1349
|
},
|
|
1349
1350
|
spineFormat: true,
|
|
1350
1351
|
usage: [
|
|
1351
|
-
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|
|
|
1352
|
+
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|rig-play] [--copy-images] [--report <file>]',
|
|
1352
1353
|
`rigc build … --pack [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] (parts onto shared pages, written into --out)`,
|
|
1353
1354
|
'rigc build … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made)',
|
|
1354
1355
|
'rigc build --cut <name> --cuts <cuts.json>',
|
|
@@ -1378,8 +1379,8 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1378
1379
|
for: 'the rebuild it writes is gated by build\'s gate, which round-trips it through spine-core',
|
|
1379
1380
|
core: {
|
|
1380
1381
|
usage: [
|
|
1381
|
-
`rigc repack <dir | skeleton.json> --out <dir> [--atlas <path>] [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] [--profile spine|
|
|
1382
|
-
`rigc repack <dir | skeleton.json> --out <dir> --keep-pages [--atlas <path>] [--profile spine|
|
|
1382
|
+
`rigc repack <dir | skeleton.json> --out <dir> [--atlas <path>] [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] [--profile spine|rig-play] [--stage x,y,w,h] [--stage-box <slot>] [--accept-skeleton-differences] [--report <file>] (gated without spine-core — see repack --help)`,
|
|
1383
|
+
`rigc repack <dir | skeleton.json> --out <dir> --keep-pages [--atlas <path>] [--profile spine|rig-play] [--stage x,y,w,h] [--stage-box <slot>] [--report <file>] (skeleton.model.json alone, pages kept)`,
|
|
1383
1384
|
],
|
|
1384
1385
|
notes: [
|
|
1385
1386
|
'a packed build repacked from its own output — skeleton.json, skeleton.atlas and the',
|
|
@@ -1401,8 +1402,8 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1401
1402
|
},
|
|
1402
1403
|
spineFormat: true,
|
|
1403
1404
|
usage: [
|
|
1404
|
-
`rigc repack <dir | skeleton.json> --out <dir> [--atlas <path>] [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] [--profile spine|
|
|
1405
|
-
`rigc repack <dir | skeleton.json> --out <dir> --keep-pages [--atlas <path>] [--profile spine|
|
|
1405
|
+
`rigc repack <dir | skeleton.json> --out <dir> [--atlas <path>] [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] [--pack-shape rect|polygon] [--profile spine|rig-play] [--stage x,y,w,h] [--stage-box <slot>] [--accept-skeleton-differences] [--report <file>]`,
|
|
1406
|
+
`rigc repack <dir | skeleton.json> --out <dir> --keep-pages [--atlas <path>] [--profile spine|rig-play] [--stage x,y,w,h] [--stage-box <slot>] [--report <file>] (skeleton.model.json alone, pages kept)`,
|
|
1406
1407
|
],
|
|
1407
1408
|
flags: ['out', 'atlas', 'page-size', 'padding', 'page-edges', 'pack-shape', 'profile', 'stage', 'stage-box', 'accept-skeleton-differences', 'keep-pages', 'report'],
|
|
1408
1409
|
overrides: {
|
|
@@ -1469,7 +1470,7 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1469
1470
|
runtime: { for: 'the gate it re-runs is the round trip through it', rerunsTheGate: true },
|
|
1470
1471
|
spineFormat: true,
|
|
1471
1472
|
usage: [
|
|
1472
|
-
'rigc validate <dir | skeleton.json> [--atlas <path>] [--profile spine|
|
|
1473
|
+
'rigc validate <dir | skeleton.json> [--atlas <path>] [--profile spine|rig-play]',
|
|
1473
1474
|
'rigc validate --cut <name> --cuts <cuts.json> (also re-derives declared durations)',
|
|
1474
1475
|
],
|
|
1475
1476
|
flags: ['atlas', 'profile', 'cut', 'cuts', 'rig', 'motion', 'out', 'manifest', 'images'],
|
|
@@ -1815,17 +1816,17 @@ export function commandHelp(name: string, docs: readonly CommandDoc[] = COMMANDS
|
|
|
1815
1816
|
* `--profile` paragraph states each profile's rule count, which only the
|
|
1816
1817
|
* validator knows (`CliEntry.profileRules`).
|
|
1817
1818
|
*/
|
|
1818
|
-
const USAGE_PARAGRAPHS: ReadonlyArray<{ about: readonly string[] | { flag: string }; lines: (rules: (profile: 'spine' | '
|
|
1819
|
+
const USAGE_PARAGRAPHS: ReadonlyArray<{ about: readonly string[] | { flag: string }; lines: (rules: (profile: 'spine' | 'rig-play') => number) => string[] }> = [
|
|
1819
1820
|
{
|
|
1820
1821
|
about: ['build', 'validate', 'bench'],
|
|
1821
1822
|
lines: (rules) => [
|
|
1822
|
-
'build, validate and bench take --profile spine|
|
|
1823
|
+
'build, validate and bench take --profile spine|rig-play:',
|
|
1823
1824
|
' spine is this valid Spine 4.3 that any runtime plays correctly?',
|
|
1824
1825
|
` THE DEFAULT — ${rules('spine')} rules, and the question the output answers when`,
|
|
1825
1826
|
' you import it into the Spine editor.',
|
|
1826
|
-
'
|
|
1827
|
-
` all ${rules('
|
|
1828
|
-
`${rules('
|
|
1827
|
+
' rig-play the above, plus this project\'s renderer and archetype policy:',
|
|
1828
|
+
` all ${rules('rig-play')} rules, opt-in. Those extra ` +
|
|
1829
|
+
`${rules('rig-play') - rules('spine')} fire on real, correct,`,
|
|
1829
1830
|
' editor-produced Spine data, so they are somebody\'s policy rather',
|
|
1830
1831
|
' than anybody\'s validity.',
|
|
1831
1832
|
'',
|
|
@@ -1955,7 +1956,7 @@ const USAGE_PARAGRAPHS: ReadonlyArray<{ about: readonly string[] | { flag: strin
|
|
|
1955
1956
|
* lines, then every paragraph about only those commands (`USAGE_PARAGRAPHS`).
|
|
1956
1957
|
* `checkout` is the file a source checkout runs this entry as.
|
|
1957
1958
|
*/
|
|
1958
|
-
export function usageText(docs: readonly CommandDoc[], checkout: string, rules: ((profile: 'spine' | '
|
|
1959
|
+
export function usageText(docs: readonly CommandDoc[], checkout: string, rules: ((profile: 'spine' | 'rig-play') => number) | undefined): string {
|
|
1959
1960
|
const names = new Set(docs.map((doc) => doc.name));
|
|
1960
1961
|
const takes = (flag: string): boolean => docs.some((doc) => doc.flags.includes(flag));
|
|
1961
1962
|
const lines = [
|
|
@@ -2002,6 +2003,13 @@ export function usageText(docs: readonly CommandDoc[], checkout: string, rules:
|
|
|
2002
2003
|
export function readProfile(flags: Record<string, string>): AssertionProfile {
|
|
2003
2004
|
const raw = flags.profile;
|
|
2004
2005
|
if (raw === undefined) return CLI_DEFAULT_PROFILE;
|
|
2006
|
+
// A former name resolves here and nowhere else (issue #1277): past this line
|
|
2007
|
+
// the alias does not exist, so it cannot select a rule set of its own.
|
|
2008
|
+
const aliased = Object.hasOwn(PROFILE_ALIASES, raw) ? PROFILE_ALIASES[raw] : undefined;
|
|
2009
|
+
if (aliased !== undefined) {
|
|
2010
|
+
console.error(profileAliasLine(raw, aliased));
|
|
2011
|
+
return aliased;
|
|
2012
|
+
}
|
|
2005
2013
|
const found = VALIDATE_PROFILES.find((p) => p === raw);
|
|
2006
2014
|
if (!found) throw new UsageError(`--profile ${JSON.stringify(raw)}; known profiles: ${VALIDATE_PROFILES.join(', ')}`);
|
|
2007
2015
|
return found;
|
|
@@ -2633,7 +2641,7 @@ export interface CliEntry {
|
|
|
2633
2641
|
/** Every command this entry runs, by name — exactly its set, or the run refuses to start. */
|
|
2634
2642
|
runs: Readonly<Record<string, CommandRun>>;
|
|
2635
2643
|
/** Each profile's rule count, for the `--profile` paragraph; the validator's, so only an entry that links it states it. */
|
|
2636
|
-
profileRules?: (profile: 'spine' | '
|
|
2644
|
+
profileRules?: (profile: 'spine' | 'rig-play') => number;
|
|
2637
2645
|
/** The refusals of bodies only this entry registers (`CliRefusal`). */
|
|
2638
2646
|
refusals?: readonly CliRefusal[];
|
|
2639
2647
|
}
|
|
@@ -818,4 +818,4 @@ export const SPINE_COMMAND_REFUSALS: readonly CliRefusal[] = [
|
|
|
818
818
|
];
|
|
819
819
|
|
|
820
820
|
/** Each profile's rule count — the validator's, which the usage's `--profile` paragraph states. */
|
|
821
|
-
export const PROFILE_RULES = (profile: 'spine' | '
|
|
821
|
+
export const PROFILE_RULES = (profile: 'spine' | 'rig-play'): number => assertionCountForProfile(profile);
|
package/src/compile.ts
CHANGED
|
@@ -1420,7 +1420,7 @@ const SLIDER_TRACKS: Record<string, ValueTrackShape> = {
|
|
|
1420
1420
|
* refused by `A12_NO_DARK_COLOR`, and that is not a contradiction — A12 is filed
|
|
1421
1421
|
* `renderer` in `ASSERTION_KIND`, so it states what ONE renderer ignores rather
|
|
1422
1422
|
* than what is wrong. The `spine` profile `build` runs reports it `PROF` and
|
|
1423
|
-
* never applies it; `--profile
|
|
1423
|
+
* never applies it; `--profile rig-play` is where it fires. Emitting a
|
|
1424
1424
|
* construct one consumer drops is exactly what a profile is for.
|
|
1425
1425
|
*/
|
|
1426
1426
|
export const SLOT_TRACKS: Record<string, 'attachment' | SlotColourShape> = {
|
package/src/core/entry.ts
CHANGED
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ⚠️ A symbol is listed here only because a dependant was seen using it,
|
|
19
19
|
* or asked for it in writing before it could be (`openTrack`, issue #1276;
|
|
20
|
-
* `openTracks`, issue #1336
|
|
20
|
+
* `openTracks`, issue #1336; `physicsClockStep` and `PHYSICS_CLOCK_LEAD`,
|
|
21
|
+
* the clock a live track's `clock` option takes, issue #1396).
|
|
21
22
|
* The list is `scripts/install_smoke.ts`'s `OBSERVED_SYMBOLS` rows for
|
|
22
23
|
* `./core`, and a symbol added here and not there is not promised.
|
|
23
24
|
*/
|
|
24
25
|
export { readModel, underSkin, underNoSkin, CoreInputError, CORE_DOCUMENT_SPEC, CORE_DOCUMENT_SPECS, CORE_BLEND_MODES, activeBones } from './index.ts';
|
|
25
26
|
export type { CompiledDocument, CoreSlotRow, CoreBlendMode } from './index.ts';
|
|
26
|
-
export { poseRawSetup, poseRawAnimation, poseRawAnimationEach, loopedTime, setupBounds } from './raw.ts';
|
|
27
|
+
export { poseRawSetup, poseRawAnimation, poseRawAnimationEach, loopedTime, setupBounds, physicsClockStep, PHYSICS_CLOCK_LEAD } from './raw.ts';
|
|
27
28
|
export type { RawPose, RawBone, RawDrawn, RawClip, RawClipped, RawEvent, RawReset } from './raw.ts';
|
|
28
29
|
export { poseWalkSetup, poseLoopingWalk } from './walk.ts';
|
|
29
30
|
export { openTrack } from './track.ts';
|
package/src/core/raw.ts
CHANGED
|
@@ -257,7 +257,7 @@ export interface WalkMode {
|
|
|
257
257
|
keep: boolean;
|
|
258
258
|
/** A plant only (the core suite's `CO26`): the animation time a looping step applies at, from its track time, the duration and the track time before it — `loopedTime` unless given. */
|
|
259
259
|
time?: (trackTime: number, duration: number, before: number) => number;
|
|
260
|
-
/** What the physics clock moves by, from the step and how far the animation time moved — the step itself unless given: a plant (`CO26`),
|
|
260
|
+
/** What the physics clock moves by, from the step and how far the animation time moved — the step itself unless given: a plant (`CO26`), the render's offline clock passed to `poseRawAnimationEach` (issue #1380), or a live track's `clock` option (`./track.ts`, issue #1396). */
|
|
261
261
|
clock?: (dt: number, moved: number) => number;
|
|
262
262
|
/** A plant only (`CO26`): `false` fires no `reset` key across the wrap — the reading the walk was measured against and rejected. */
|
|
263
263
|
wrapResets?: boolean;
|
|
@@ -561,6 +561,20 @@ export function physicsClockStep(step: number): number {
|
|
|
561
561
|
return step + step * PHYSICS_CLOCK_LEAD;
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
+
/**
|
|
565
|
+
* A live track's `clock` option (issue #1396, `./track.ts` and `./tracks.ts`):
|
|
566
|
+
* the caller's function, called with each step, its answer checked before the
|
|
567
|
+
* skeleton's clock moves by it — a finite time at or above 0, or the step
|
|
568
|
+
* refused by name, as a step itself is. `who` names the track in the refusal.
|
|
569
|
+
*/
|
|
570
|
+
export function checkedClock(clock: (dt: number) => number, who: string): (dt: number) => number {
|
|
571
|
+
return (dt) => {
|
|
572
|
+
const moved = clock(dt);
|
|
573
|
+
if (typeof moved !== 'number' || !Number.isFinite(moved) || moved < 0) throw new CoreInputError(`${who} clock moved the physics by ${String(moved)} for a step of ${dt}, not a finite time at or above 0`);
|
|
574
|
+
return moved;
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
564
578
|
/**
|
|
565
579
|
* The animation time a looping track applies its animation at (`./walk.ts`,
|
|
566
580
|
* *The time*): the track time wrapped by the duration, and 0 over a duration
|
package/src/core/track.ts
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
*
|
|
27
27
|
* - **Held** (`loop: false`): the raw entry's walk — the track time the
|
|
28
28
|
* running sum of the steps, the animation time held at the duration, the
|
|
29
|
-
* physics clock moved by the step
|
|
29
|
+
* physics clock moved by the step (or by `clock`, *The physics clock*
|
|
30
|
+
* below), events over `(previous animation time,
|
|
30
31
|
* animation time]` (`./raw.ts`, *One step*). `CO46` holds it to
|
|
31
32
|
* `poseRawAnimation` pose for pose, every field, bit for bit.
|
|
32
33
|
* - **Looped** (`loop: true`): A10's looping walk's time, clock and `reset`
|
|
@@ -135,6 +136,44 @@
|
|
|
135
136
|
* children, the meshes they carry and the physics do not.
|
|
136
137
|
* - **Pose 0 not adjusted** under `reset: 'animation'`.
|
|
137
138
|
*
|
|
139
|
+
* ## The physics clock (issue #1396)
|
|
140
|
+
*
|
|
141
|
+
* `clock` is what each step moves the skeleton's physics clock by — the time
|
|
142
|
+
* a physics constraint integrates — with `poseRawAnimationEach`'s meaning.
|
|
143
|
+
* Absent, it is the step itself, and every pose is the one the track posed
|
|
144
|
+
* before the option existed (`CO46`–`CO50`, unchanged). `physicsClockStep` is
|
|
145
|
+
* the render's clock (issue #1380), for a player stepping at a fixed rate.
|
|
146
|
+
* The track time, the animation time, the wrap, the events and `trackTime`
|
|
147
|
+
* stay on the running sum of the steps: the clock moves the physics and
|
|
148
|
+
* nothing else.
|
|
149
|
+
*
|
|
150
|
+
* - **Why the track takes it.** Stepped at a fixed rate whose frame lasts a
|
|
151
|
+
* whole number of physics steps, a spring shows the render's defect. On the
|
|
152
|
+
* public trailer's `show`, read with play#103's detector, the frame clock
|
|
153
|
+
* adds outlier poses 5, 11–13, 44–45 and 187–189 at a fixed 1/20 and 8,
|
|
154
|
+
* 15–17, 31–33 and 505–507 at 1/60, and `clock: physicsClockStep` removes
|
|
155
|
+
* every one (the PR of issue #1396 carries the table). A player scaling its
|
|
156
|
+
* own `dt` instead moves the animation time too: a key at 0.5 s fires on
|
|
157
|
+
* the tenth step of 1/20 where the render and the runtime fire it on the
|
|
158
|
+
* eleventh, and the player stops agreeing with `rigc render`.
|
|
159
|
+
* - **The reset.** Pose 0 under `reset: 'animation'` is a step of 0 and moves
|
|
160
|
+
* the clock by `clock(0)`, as the render's walk does — 0 for
|
|
161
|
+
* `physicsClockStep`; under `setup` nothing is stepped. A physics `reset`
|
|
162
|
+
* key restarts a constraint's time from the clock as it stands, so it is
|
|
163
|
+
* read on the same clock as every other step.
|
|
164
|
+
* - **The seek** stays the caller's — open, then step to the time — and its
|
|
165
|
+
* steps go through the same clock, so a seek's physics is the physics of
|
|
166
|
+
* the steps it took.
|
|
167
|
+
* - **A clock that returns no time** — not finite, or below 0 — refuses the
|
|
168
|
+
* step by name after the track time has moved, so the track stays refused.
|
|
169
|
+
*
|
|
170
|
+
* `CO53` holds the mechanism on LS05's spring probe (every constraint update
|
|
171
|
+
* takes the steps exact arithmetic takes; the frame clock is red), `CO54` the
|
|
172
|
+
* poses (equal to the render's walk, to `poseRawAnimationEach` on the same
|
|
173
|
+
* clock, between the two live entries, and to spine-core with its
|
|
174
|
+
* `AnimationState` updated by the step and its `Skeleton` by the clock), and
|
|
175
|
+
* `CO55` that the times and the events are those of the walk without it.
|
|
176
|
+
*
|
|
138
177
|
* ## The oracle chain
|
|
139
178
|
*
|
|
140
179
|
* Nothing here is held to spine-core directly but the events: the live track
|
|
@@ -153,15 +192,17 @@
|
|
|
153
192
|
* history, so a seek is "open, then step to the time".
|
|
154
193
|
*/
|
|
155
194
|
import { CoreInputError, type CompiledDocument } from './index.ts';
|
|
156
|
-
import { openRawTrack, stepRawTrack, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
|
|
195
|
+
import { checkedClock, openRawTrack, stepRawTrack, type RawPose, type RawReset, type TrackState, type WalkMode } from './raw.ts';
|
|
157
196
|
import type { LocalAdjust, TimelinePlant } from './animation.ts';
|
|
158
197
|
export type { AdjustableLocals, BoneLocals, LocalAdjust } from './animation.ts';
|
|
159
198
|
|
|
160
|
-
/** How a live track is opened: whether it loops, and where its physics is reset (`RawReset`) — both the caller's, neither with a default —
|
|
199
|
+
/** How a live track is opened: whether it loops, and where its physics is reset (`RawReset`) — both the caller's, neither with a default — the caller's adjustment of every pose's bone locals (the header's *The adjustment*), and what each step moves the physics clock by (the header's *The physics clock*). */
|
|
161
200
|
export interface TrackOptions {
|
|
162
201
|
loop: boolean;
|
|
163
202
|
reset: RawReset;
|
|
164
203
|
adjust?: LocalAdjust;
|
|
204
|
+
/** What each step of `dt` moves the skeleton's physics clock by — `physicsClockStep` is the render's; absent, the step itself. The track time, the animation time and the events stay on the running sum of the steps. */
|
|
205
|
+
clock?: (dt: number) => number;
|
|
165
206
|
}
|
|
166
207
|
|
|
167
208
|
/** A live track (`openTrack`): pose 0, then one pose per `step`. */
|
|
@@ -191,7 +232,10 @@ export function openTrack(doc: CompiledDocument, animation: string, options: Tra
|
|
|
191
232
|
if (typeof options !== 'object' || options === null || typeof options.loop !== 'boolean') throw new CoreInputError(`the live track's options.loop is ${JSON.stringify((options as Partial<TrackOptions> | null)?.loop)}, not true or false`);
|
|
192
233
|
if (options.reset !== 'animation' && options.reset !== 'setup') throw new CoreInputError(`the live track's options.reset is ${JSON.stringify(options.reset)}, not "animation" or "setup"`);
|
|
193
234
|
if (options.adjust !== undefined && typeof options.adjust !== 'function') throw new CoreInputError(`the live track's options.adjust is ${typeof options.adjust}, not a function`);
|
|
194
|
-
|
|
235
|
+
if (options.clock !== undefined && typeof options.clock !== 'function') throw new CoreInputError(`the live track's options.clock is ${typeof options.clock}, not a function`);
|
|
236
|
+
const mode = options.loop ? LOOPED : HELD;
|
|
237
|
+
const clock = options.clock === undefined ? undefined : checkedClock(options.clock, 'the live track\'s');
|
|
238
|
+
const { track, first } = openRawTrack(doc, animation, plant, options.reset, clock === undefined ? mode : { ...mode, clock: (dt) => clock(dt) }, options.adjust);
|
|
195
239
|
return new Track(track, first);
|
|
196
240
|
}
|
|
197
241
|
|
package/src/core/tracks.ts
CHANGED
|
@@ -73,7 +73,12 @@
|
|
|
73
73
|
* out fires none** — not "until the mix passes half way": `eventThreshold`
|
|
74
74
|
* defaults to 0 and an entry's events are applied only while `mix <
|
|
75
75
|
* eventThreshold`, which no mix is. `complete` is not carried (issue #1276).
|
|
76
|
-
* - **Physics** steps on top of the mixed locals, as on one track
|
|
76
|
+
* - **Physics** steps on top of the mixed locals, as on one track — and on
|
|
77
|
+
* the same clock: `TracksOptions.clock`, what each step moves the physics
|
|
78
|
+
* clock by, has the single track's meaning (`./track.ts`, *The physics
|
|
79
|
+
* clock*, issue #1396). The entries' track times, the mixes, the queue and
|
|
80
|
+
* the events stay on the steps; `start()` under `reset: 'animation'` moves
|
|
81
|
+
* the clock by `clock(0)`.
|
|
77
82
|
*
|
|
78
83
|
* `CO51` holds all of it to spine-core 4.3.13 at tolerance 0 — every bone's
|
|
79
84
|
* world transform, every drawn attachment's world vertices, every slot's
|
|
@@ -159,13 +164,15 @@ import { activeBones, CoreInputError, rawNumber, readColour, shownAttachment, ty
|
|
|
159
164
|
import { channelAt, keyIndexAt, SLOT_COLOUR_TIMELINES, slotRowsFrom, steppedLocalsWorld, type BoneTimelineKind, type CoreKey, type LocalAdjust, type SlotBase, type SlotColourKind, type TimelinePlant } from './animation.ts';
|
|
160
165
|
import { deformAt, setupArray, type CoreAttachmentTimeline } from './deform.ts';
|
|
161
166
|
import { eventsFired, type CoreEventKey, type CoreEventRow } from './events.ts';
|
|
162
|
-
import { openRawTracksPath, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
|
|
167
|
+
import { checkedClock, openRawTracksPath, type RawPose, type RawReset, type RawTracksPath } from './raw.ts';
|
|
163
168
|
import { slotTimelinesApply } from './skins.ts';
|
|
164
169
|
|
|
165
|
-
/** How the live tracks are opened: where the physics is reset (`RawReset`, the caller's, no default)
|
|
170
|
+
/** How the live tracks are opened: where the physics is reset (`RawReset`, the caller's, no default), the caller's adjustment of every pose's bone locals, and what each step moves the physics clock by (the header's *The physics clock*). */
|
|
166
171
|
export interface TracksOptions {
|
|
167
172
|
reset: RawReset;
|
|
168
173
|
adjust?: LocalAdjust;
|
|
174
|
+
/** What each step of `dt` moves the skeleton's physics clock by — `physicsClockStep` is the render's; absent, the step itself. Every entry's track time, the mixes, the queue and the events stay on the steps. */
|
|
175
|
+
clock?: (dt: number) => number;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
/**
|
|
@@ -1299,7 +1306,9 @@ function delayOf(v: number, call: string): number {
|
|
|
1299
1306
|
export function openTracks(doc: CompiledDocument, options: TracksOptions, plant: TracksPlant = {}): LiveTracks {
|
|
1300
1307
|
if (typeof options !== 'object' || options === null || (options.reset !== 'animation' && options.reset !== 'setup')) throw new CoreInputError(`the live tracks' options.reset is ${JSON.stringify((options as Partial<TracksOptions> | null)?.reset)}, not "animation" or "setup"`);
|
|
1301
1308
|
if (options.adjust !== undefined && typeof options.adjust !== 'function') throw new CoreInputError(`the live tracks' options.adjust is ${typeof options.adjust}, not a function`);
|
|
1302
|
-
|
|
1309
|
+
if (options.clock !== undefined && typeof options.clock !== 'function') throw new CoreInputError(`the live tracks' options.clock is ${typeof options.clock}, not a function`);
|
|
1310
|
+
const clock = options.clock === undefined ? undefined : checkedClock(options.clock, 'the live tracks\'');
|
|
1311
|
+
return new Tracks(doc, options.reset, options.adjust, clock, plant, openRawTracksPath(doc, plant));
|
|
1303
1312
|
}
|
|
1304
1313
|
|
|
1305
1314
|
class Tracks implements LiveTracks {
|
|
@@ -1313,6 +1322,7 @@ class Tracks implements LiveTracks {
|
|
|
1313
1322
|
private readonly doc: CompiledDocument,
|
|
1314
1323
|
private readonly reset: RawReset,
|
|
1315
1324
|
private readonly adjust: LocalAdjust | undefined,
|
|
1325
|
+
private readonly clock: ((dt: number) => number) | undefined,
|
|
1316
1326
|
private readonly plant: TracksPlant,
|
|
1317
1327
|
private readonly path: RawTracksPath,
|
|
1318
1328
|
) {
|
|
@@ -1426,7 +1436,7 @@ class Tracks implements LiveTracks {
|
|
|
1426
1436
|
const { doc, path } = this;
|
|
1427
1437
|
const ctx = path.ctx;
|
|
1428
1438
|
const before = ctx.time;
|
|
1429
|
-
ctx.time += dt;
|
|
1439
|
+
ctx.time += this.clock === undefined ? dt : this.clock(dt);
|
|
1430
1440
|
const pose = new Pose(doc, this.plant);
|
|
1431
1441
|
const animationTime = this.mixer.apply(pose);
|
|
1432
1442
|
const mixed = new Map(doc.slots.map((s, k) => [s.name, { deform: pose.slots[k].deform, seen: pose.slots[k].seen }]));
|
package/src/rig.ts
CHANGED
|
@@ -426,7 +426,7 @@ export interface RigSlot {
|
|
|
426
426
|
attachment?: string | null;
|
|
427
427
|
/** `rrggbbaa`. Default opaque white. */
|
|
428
428
|
color?: string;
|
|
429
|
-
/** Two-colour tint, `rrggbb`. 🚫 `A12_NO_DARK_COLOR` under `
|
|
429
|
+
/** Two-colour tint, `rrggbb`. 🚫 `A12_NO_DARK_COLOR` under `rig-play`. */
|
|
430
430
|
dark?: string;
|
|
431
431
|
/** Default `normal`. Only the first letter's case is free (`RigSlotBlend`). */
|
|
432
432
|
blend?: RigSlotBlend;
|
package/src/validate.ts
CHANGED
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
walkTimelines,
|
|
78
78
|
} from './timelines.ts';
|
|
79
79
|
import type { RigInfo } from './types.ts';
|
|
80
|
-
import { ASSERTION_KIND, kindRunsUnder, type AssertionProfile } from './assertions/kinds.ts';
|
|
80
|
+
import { ASSERTION_KIND, kindRunsUnder, resolveProfile, type AssertionProfile } from './assertions/kinds.ts';
|
|
81
81
|
import { verdictHarness, type Failure } from './assertions/harness.ts';
|
|
82
82
|
import { isObj, type Json } from './assertions/values.ts';
|
|
83
83
|
import type { SkinEntryFacts } from './assertions/facts/skin_entries.ts';
|
|
@@ -168,14 +168,14 @@ export type { Failure } from './assertions/harness.ts';
|
|
|
168
168
|
* ⭐ The distinction this draws is the difference between "wrong" and "not how we
|
|
169
169
|
* do it here", and conflating the two is how a validator stops being usable on
|
|
170
170
|
* anybody else's data. Fifteen of the 40 assertions are policy — seven for one
|
|
171
|
-
* renderer (`
|
|
171
|
+
* renderer (`rig-play`) and one project's canvas budget, eight for rigc's own
|
|
172
172
|
* formations — and every one of them fires
|
|
173
173
|
* on real, correct, editor-produced Spine data — the official example projects
|
|
174
174
|
* carry clipping attachments, unweighted meshes, 116-triangle meshes and packed
|
|
175
175
|
* atlases, all of which are perfectly valid and none of which spine-html likes.
|
|
176
176
|
*
|
|
177
177
|
* - `spine` — is this valid Spine 4.3 that any runtime will play correctly?
|
|
178
|
-
* - `
|
|
178
|
+
* - `rig-play` — the above, plus this project's renderer and archetype policy.
|
|
179
179
|
*
|
|
180
180
|
* ⚠️ `validate()` has NO default profile — `ValidateInput.profile` is required,
|
|
181
181
|
* and that is deliberate. A silent default here can only be wrong in one of two
|
|
@@ -207,7 +207,7 @@ export const ASSERTION_NAMES: readonly string[] = Object.keys(ASSERTION_KIND);
|
|
|
207
207
|
|
|
208
208
|
/** How many assertions this profile applies, by the kinds it carries. */
|
|
209
209
|
export function assertionCountForProfile(profile: ValidateProfile): number {
|
|
210
|
-
if (profile === '
|
|
210
|
+
if (profile === 'rig-play') return ASSERTION_NAMES.length;
|
|
211
211
|
return ASSERTION_NAMES.filter((name) => ASSERTION_KIND[name] === 'validity').length;
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -2012,9 +2012,10 @@ function once<T>(supply: () => T): () => T {
|
|
|
2012
2012
|
}
|
|
2013
2013
|
|
|
2014
2014
|
export function validate(input: ValidateInput): ValidateReport {
|
|
2015
|
-
|
|
2015
|
+
// A former name reads as its profile (issue #1277): the policy switch below and the report both use the resolved one.
|
|
2016
|
+
const profile = resolveProfile(input.profile, 'validate');
|
|
2016
2017
|
/** True when this profile's rulebook includes the policy layer. */
|
|
2017
|
-
const policy = profile === '
|
|
2018
|
+
const policy = profile === 'rig-play';
|
|
2018
2019
|
// The harness — `check`, `fail`, `skip` and the lists they fill — is
|
|
2019
2020
|
// `./assertions/harness.ts`'s since issue #1025, and the rule is the one it
|
|
2020
2021
|
// always was: the model side runs the bodies that moved there in the same
|
|
@@ -2566,7 +2567,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
2566
2567
|
});
|
|
2567
2568
|
|
|
2568
2569
|
// --- A12: no dark / two-colour tint --------------------------------------
|
|
2569
|
-
// Parsed, then
|
|
2570
|
+
// Parsed, then ignored by rig-play's DOM mode.
|
|
2570
2571
|
check('A12_NO_DARK_COLOR', () => a12NoDarkColor(verdicts, rawSkeletonRoster(raw), rawSlotTimelines(raw)));
|
|
2571
2572
|
|
|
2572
2573
|
// --- A05: curve arrays are 4 numbers per value channel --------------------
|
|
@@ -3309,7 +3310,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
3309
3310
|
// first build — and told them, untruthfully, that the file held no transparency
|
|
3310
3311
|
// at all. That audit is also why the CLI's default is now `spine`, which does
|
|
3311
3312
|
// not run this rule at all (#221): a stranger reaches this refusal only by
|
|
3312
|
-
// asking for `
|
|
3313
|
+
// asking for `rig-play`. It still has to be both TRUE and actionable when it
|
|
3313
3314
|
// fires, and it now has to name the profile it belongs to as well as the one
|
|
3314
3315
|
// that does not ask — the reader opted in, and the message is where they find
|
|
3315
3316
|
// out what they opted into.
|