spine-rigc 0.23.0 → 0.24.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 +24 -8
- package/cli.ts +67 -11
- package/docs/AUTHORING.md +205 -29
- package/docs/INGEST.md +62 -12
- package/docs/SPEC_COVERAGE.md +1 -2
- package/package.json +1 -1
- package/src/atlas.ts +78 -3
- package/src/compile.ts +118 -16
- package/src/deformmeasure.ts +322 -151
- package/src/ingest.ts +72 -13
- package/src/render.ts +9 -7
- package/src/timelines.ts +163 -0
- package/src/types.ts +35 -2
- package/src/validate.ts +600 -105
- package/tools/editor_roundtrip.ts +74 -14
package/docs/AUTHORING.md
CHANGED
|
@@ -362,16 +362,18 @@ Everything above starts from two spec files you wrote. `rigc ingest` starts from
|
|
|
362
362
|
an existing rig is a starting point instead of 250 KB of arrays to retype.
|
|
363
363
|
|
|
364
364
|
```bash
|
|
365
|
-
bun cli.ts ingest hero.json --out specs/ --stage 0,0,1024,768
|
|
365
|
+
bun cli.ts ingest hero.json --out specs/ --stage 0,0,1024,768 --images parts/
|
|
366
366
|
# .. out /abs/path/specs
|
|
367
367
|
# .. art loose
|
|
368
|
+
# .. images ../parts/ (the rig spec's own, from /abs/path/specs)
|
|
368
369
|
# JUDGE DURATION: animation "idle" — skeleton JSON carries no duration; the largest key time (2.667) is used …
|
|
369
370
|
# LOSS PATH_LENGTHS: skin "default" slot "track" attachment "track" — the source states `lengths`; rigc RE-MEASURES it …
|
|
370
371
|
# rigc: wrote /abs/path/specs/rig.json
|
|
371
372
|
# rigc: wrote /abs/path/specs/motion.json
|
|
372
373
|
# rigc: wrote /abs/path/specs/findings.json
|
|
374
|
+
# rigc: build it with rigc build --rig /abs/path/specs/rig.json --motion /abs/path/specs/motion.json --out <dir>
|
|
373
375
|
|
|
374
|
-
bun cli.ts build --rig specs/rig.json --motion specs/motion.json --
|
|
376
|
+
bun cli.ts build --rig specs/rig.json --motion specs/motion.json --out spine
|
|
375
377
|
bun cli.ts diff spine/skeleton.json hero.json # 1.000 on every measure
|
|
376
378
|
```
|
|
377
379
|
|
|
@@ -388,15 +390,23 @@ come out in is in no field of the skeleton and a decompiled spec cannot know it.
|
|
|
388
390
|
That is a gate rather than a claim: `bun run selftest` round-trips every rig this
|
|
389
391
|
repository builds on every run.
|
|
390
392
|
|
|
391
|
-
**
|
|
393
|
+
**The flags are for what a skeleton does not encode**, and nothing else is a flag.
|
|
392
394
|
|
|
393
395
|
| flag | what it decides |
|
|
394
396
|
| --- | --- |
|
|
395
|
-
| `--art loose` (default) | name an `image` per attachment — `<path or placeholder>.png` — so the rebuild
|
|
397
|
+
| `--art loose` (default) | name an `image` per attachment — `<path or placeholder>.png` — so the rebuild resolves loose PNGs and rigc measures them |
|
|
396
398
|
| `--art none` | state `width`/`height` only, so the rebuild is `build --atlas-in <pack.atlas>` and every part resolves out of the pack |
|
|
399
|
+
| `--images <dir>` | **write** the rig spec's own `images` directory, spelled relative to `--out`, so the rebuild is a plain `build --rig … --motion … --out …`. Without it the field is left out and every `image` resolves against `--out` itself, which holds the specs and no art — so every rebuild has to repeat `build --images <dir>`. Refused together with `--art none`, which writes no `image` for it to be the base of |
|
|
397
400
|
| `--stage x,y,w,h` | the setup bounding box. **Required for an editor export**, which carries none |
|
|
398
401
|
| `--name <n>` | the rig spec's `name`, which the motion spec's `archetype` must equal (default: the file's basename) |
|
|
399
402
|
|
|
403
|
+
⚠️ **`ingest --images` and `build --images` point opposite ways.** `build --images`
|
|
404
|
+
*overrides* the rig spec's own directory for one invocation; `ingest --images`
|
|
405
|
+
*writes* it, once, so no invocation needs the override. They share a name because
|
|
406
|
+
they name the same field — and `ingest` spells the value with the same function
|
|
407
|
+
`build` spells `skeleton.images` with, so a spec and the skeleton it came from say
|
|
408
|
+
where the parts are in one convention.
|
|
409
|
+
|
|
400
410
|
🚨 **The stage is the one value `ingest` will not guess.** rigc always emits
|
|
401
411
|
`skeleton.width`/`height` and an editor export never does, so a foreign file needs
|
|
402
412
|
`--stage`; without it the missing box is a **blocker**, named. It is not derivable —
|
|
@@ -892,6 +902,33 @@ A skin can also say which bones and constraints it **switches on**, and that nee
|
|
|
892
902
|
one more level, so a skin entry has a second spelling — see §3.4.1. The short one
|
|
893
903
|
above is unchanged and is what almost every rig wants.
|
|
894
904
|
|
|
905
|
+
🔸 **A skin may fill no slot with anything that needs art, and that build is
|
|
906
|
+
green.** The atlas is built out of what the skins reference, so a rig whose skins
|
|
907
|
+
name no `image` — an empty `default`, or one carrying only a `boundingbox`, a
|
|
908
|
+
`clipping` polygon or a `path`, none of which has a page — compiles to an atlas
|
|
909
|
+
with **no pages**, and rigc writes `skeleton.atlas` as an **empty file** (zero
|
|
910
|
+
bytes). Four rules then report SKIP by name rather than a pass, because a page is
|
|
911
|
+
their whole subject: `A07_ATLAS_TEXT_SHAPE`, `A06_ATLAS_PAGE_SIZE_MATCHES_PNG`,
|
|
912
|
+
`A17_ATLAS_PAGE_FILES_EXIST` and — under `spine-html` —
|
|
913
|
+
`A19_OVERLAY_PNGS_HAVE_ALPHA` and `A27_REGION_NAME_MATCHES_PAGE_FILENAME`. Until
|
|
914
|
+
[#608](https://github.com/firejune/rigc/issues/608) the same compile wrote one
|
|
915
|
+
newline instead and `A07` refused it with two findings, so a hit-box skeleton — a
|
|
916
|
+
correct rig, whose geometry `A33_VERTEX_ATTACHMENT_GEOMETRY` passes — could not be
|
|
917
|
+
built at all.
|
|
918
|
+
|
|
919
|
+
⚠️ **This is not the case where an attachment WANTS a region.** A region or mesh
|
|
920
|
+
attachment that states `width`/`height` and names no `image` still resolves a
|
|
921
|
+
region by `path`, and with no atlas to supply it that is
|
|
922
|
+
`A08_REGION_NAMES_MATCH_ATTACHMENTS` naming the skin, the slot, the placeholder
|
|
923
|
+
and the path — which is what a spec written by `ingest --art none` does when it is
|
|
924
|
+
built without `--atlas-in` (§0.2, §0.3). The empty atlas is legal; an attachment
|
|
925
|
+
pointing into it is not.
|
|
926
|
+
|
|
927
|
+
⚠️ **`rigc render` still refuses such a build**, by name and before it draws
|
|
928
|
+
anything: `… posed no drawable attachment in any animation or in its setup pose —
|
|
929
|
+
there is nothing to draw`. That is the honest division — the rig is valid Spine
|
|
930
|
+
data, and there is no picture of it.
|
|
931
|
+
|
|
895
932
|
**Region attachment** ([Spine: region attachments](http://esotericsoftware.com/spine-regions)),
|
|
896
933
|
the default `type`:
|
|
897
934
|
|
|
@@ -921,7 +958,11 @@ with `image: hair_short.png` under a placeholder called `hair` resolves the regi
|
|
|
921
958
|
`hair`, which no atlas has. Until
|
|
922
959
|
[#577](https://github.com/firejune/rigc/issues/577) a region derived it and an
|
|
923
960
|
authored mesh did not, so that rig **built** and then failed
|
|
924
|
-
`A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: hair
|
|
961
|
+
`A00_ROUNDTRIP_PARSE: threw: Region not found in atlas: hair` — which is the
|
|
962
|
+
loader's sentence and all the report had. Since
|
|
963
|
+
[#589](https://github.com/firejune/rigc/issues/589) the same miss is named by
|
|
964
|
+
`A08_REGION_NAMES_MATCH_ATTACHMENTS`, with the skin, the slot, the placeholder
|
|
965
|
+
and the attachment's own name beside the path.
|
|
925
966
|
|
|
926
967
|
Geometry comes in one of two fields:
|
|
927
968
|
|
|
@@ -1673,8 +1714,11 @@ What to know about it, and nothing to author:
|
|
|
1673
1714
|
([#574](https://github.com/firejune/rigc/issues/574)).
|
|
1674
1715
|
- **`path` is restated, and it has to be.** `path` defaults to the attachment's
|
|
1675
1716
|
**name**, not to its placeholder, so an entry given a name and no path would
|
|
1676
|
-
resolve its texture at `zulu/patch` and find no such region.
|
|
1677
|
-
says so
|
|
1717
|
+
resolve its texture at `zulu/patch` and find no such region.
|
|
1718
|
+
`A08_REGION_NAMES_MATCH_ATTACHMENTS` says so if it is ever dropped, naming the
|
|
1719
|
+
skin, the slot, the placeholder and the path
|
|
1720
|
+
([#589](https://github.com/firejune/rigc/issues/589)); `A00_ROUNDTRIP_PARSE`
|
|
1721
|
+
reported it in the parser's own words until then, and now defers to A08.
|
|
1678
1722
|
- **Only contested placeholders are touched.** One skin filling a placeholder, or
|
|
1679
1723
|
two skins filling a slot under *different* placeholders, emit exactly what they
|
|
1680
1724
|
always did — every rig in this repository is byte-identical across the change.
|
|
@@ -2317,7 +2361,8 @@ a deform). Folding them in would make `v` mean four different things depending o
|
|
|
2317
2361
|
| `bone` | `translatex`, `translatey`, `scalex`, `scaley`, `shearx`, `sheary`, `rotate` | `[value]` |
|
|
2318
2362
|
| `slot` | `rgba` | `[r, g, b, a]` in 0..1 |
|
|
2319
2363
|
| `slot` | `attachment` | the attachment name, or `null` for "show nothing" |
|
|
2320
|
-
| `physics` | `
|
|
2364
|
+
| `physics` | `inertia`, `strength`, `damping`, `mass`, `wind`, `gravity` | `[value]` — the constraint's own tuning, keyed over time |
|
|
2365
|
+
| `physics` | `mix` | `[mix]`, **0 or more** — the constraint's authority |
|
|
2321
2366
|
| `physics` | `reset` | `null` — the key *is* the event |
|
|
2322
2367
|
| `path` | `position`, `spacing` | `[value]` — see §4.12 |
|
|
2323
2368
|
| `path` | `mix` | `[mixRotate, mixX, mixY]` — one timeline, three channels |
|
|
@@ -2327,6 +2372,42 @@ a deform). Folding them in would make `v` mean four different things depending o
|
|
|
2327
2372
|
Translate values are **relative to the bone's setup position**; scale values are
|
|
2328
2373
|
multipliers where `1` is setup; rotation is in degrees.
|
|
2329
2374
|
|
|
2375
|
+
**A physics constraint's six tuning timelines override §4.6's table for the
|
|
2376
|
+
length of an animation.** `{ "physics": "hair", "property": "wind", "keys": […] }`
|
|
2377
|
+
is a wind that rises and falls; `damping` is how fast the jiggle settles,
|
|
2378
|
+
`strength` how hard it is pulled back, `inertia` how much of the bone's motion it
|
|
2379
|
+
carries, `mass` the weight it swings with, `gravity` the constant pull. They are
|
|
2380
|
+
the same field names §4.6 uses at rest, and one key states the whole value — not
|
|
2381
|
+
a delta from the constraint's own setting.
|
|
2382
|
+
|
|
2383
|
+
- ⚠️ **The per-key default is 0 on all six, and 1 on `mix` — not the
|
|
2384
|
+
constraint's default.** The parser opens every physics key at 0 and only
|
|
2385
|
+
`mix` reassigns it, so a key that omits its number reads 0 for `damping`, not
|
|
2386
|
+
the 0.85 §4.6 gives a constraint that states none. rigc never omits a channel,
|
|
2387
|
+
so this bites only when you compare an emitted file against an editor export,
|
|
2388
|
+
or when you read an export by hand: a `damping` key with no `value` in
|
|
2389
|
+
somebody else's file means **0**.
|
|
2390
|
+
- ⚠️ `mass` is the one whose keyed number is not what the runtime stores. The
|
|
2391
|
+
key states a mass and the pose holds `1 / mass`, so a `mass` key of `0` is an
|
|
2392
|
+
infinite inverse mass — the constraint stops moving.
|
|
2393
|
+
- **Four of the seven are bounded, and a key outside its bound is a compile
|
|
2394
|
+
error** ([#610](https://github.com/firejune/rigc/issues/610)). `mass` and
|
|
2395
|
+
`strength` must be `> 0`, `damping` must be strictly inside `(0, 1)`, and `mix`
|
|
2396
|
+
must be `0` or more. `A23_PHYSICS_CONSTRAINT_EFFECTIVE` applies the same four to
|
|
2397
|
+
a file rigc did not write, naming the animation, the constraint, the key time
|
|
2398
|
+
and the value — so the compiler is where a spec you wrote is refused, and the
|
|
2399
|
+
assertion is where an import is.
|
|
2400
|
+
- 🚫 **`inertia`, `wind` and `gravity` are bounded nowhere, and neither is the top
|
|
2401
|
+
of `mix`.** The runtime documents no range for the first three, and
|
|
2402
|
+
`PhysicsConstraintPose` documents `mix` as "a percentage (0+)" — so a negative
|
|
2403
|
+
wind is the other direction and a `mix` of `1.5` is an over-mix. Both are real
|
|
2404
|
+
and both compile.
|
|
2405
|
+
- ⚠️ **A `mix` key of exactly `0` is legal where a setup `mix` of `0` is not**, and
|
|
2406
|
+
the difference is not an inconsistency. `PhysicsConstraint.update` opens with
|
|
2407
|
+
`if (mix === 0) return;`, so muting a constraint for a stretch of an animation is
|
|
2408
|
+
what a mix timeline is for; a constraint muted *at rest* does nothing at all
|
|
2409
|
+
unless some animation rescues it, which is the silence `A23` was built for.
|
|
2410
|
+
|
|
2330
2411
|
On a track that names a `group`, one key's `v` may instead be a **map keyed by
|
|
2331
2412
|
member name**, whose entries are each exactly the `v` above — or a `derive`
|
|
2332
2413
|
model the compiler evaluates per member. §4.5.1.
|
|
@@ -2612,8 +2693,11 @@ key times. One lag, one place.
|
|
|
2612
2693
|
|
|
2613
2694
|
`name → { bone, x?, y?, rotate?, scaleX?, shearX?, inertia?, strength?, damping?,
|
|
2614
2695
|
mass?, wind?, gravity?, mix?, fps?, limit? }`. These are emitted into the 4.3
|
|
2615
|
-
`constraints` array.
|
|
2616
|
-
|
|
2696
|
+
`constraints` array. Seven of them — the six tuning numbers and `mix` — can also
|
|
2697
|
+
be **keyed over time** as `tracks` entries naming this constraint (§4.4); this
|
|
2698
|
+
table is the value at rest, and a timeline overrides it while it plays. `mass: 0` becomes an infinite inverse mass and `damping ≥ 1`
|
|
2699
|
+
never settles — both are `A23`, here and on every timeline key that states them
|
|
2700
|
+
([#610](https://github.com/firejune/rigc/issues/610)). Every field but `bone` and `note` must be a finite
|
|
2617
2701
|
number: a non-number is rounded to `NaN` and emitted as `null`, which the runtime
|
|
2618
2702
|
reads as **zero**, so `"mass": "heavy"` used to ship a constraint that never
|
|
2619
2703
|
settles with no word from anybody (#307).
|
|
@@ -3307,6 +3391,13 @@ keys were passed over and how many reversed triangles nothing gated:
|
|
|
3307
3391
|
8 reversed triangle(s) nothing gates <- A39 counts them as deformKeysNotDrawn
|
|
3308
3392
|
```
|
|
3309
3393
|
|
|
3394
|
+
⭐ **On the "shows another attachment" half, that sentence also names the skin the
|
|
3395
|
+
pose was taken in** — `the slot shows attachment "away" at this time, with skin
|
|
3396
|
+
"suit" worn, not this mesh …` — because what a slot shows is resolved through the
|
|
3397
|
+
worn skin first and `defaultSkin` second, so the reading is only meaningful beside
|
|
3398
|
+
the dress it was taken in (§4.11.5). The alpha half carries no such clause: an
|
|
3399
|
+
alpha is read off the pose and no skin is in it.
|
|
3400
|
+
|
|
3310
3401
|
🚫 **What it does not print, and why — `coverage`.** A deform **cannot move
|
|
3311
3402
|
coverage.** That figure is rasterised from the attachment's **uvs** against the
|
|
3312
3403
|
part's alpha, and a deform moves positions and never uvs, so it is identical at
|
|
@@ -3582,6 +3673,70 @@ records that, so a slider-applied animation is measured in its slider frames onl
|
|
|
3582
3673
|
|
|
3583
3674
|
---
|
|
3584
3675
|
|
|
3676
|
+
### 4.11.5 Which skin a deform key is measured in — the one it is keyed on
|
|
3677
|
+
|
|
3678
|
+
§4.11 opens on the triple: a deform timeline is the only one keyed on
|
|
3679
|
+
**skin / slot / attachment**. So the skin is not context around the key, it is a
|
|
3680
|
+
third of the key's own address — and every pose `A39` and the `DEFORM` block take
|
|
3681
|
+
is now taken with **that skin worn**
|
|
3682
|
+
([#583](https://github.com/firejune/rigc/issues/583)). You do not ask for it and
|
|
3683
|
+
there is no flag: the skin comes out of the timeline.
|
|
3684
|
+
|
|
3685
|
+
🚨 **It used to wear nothing at all**, which is `spine-core`'s own initial state
|
|
3686
|
+
and the same one `check` reports as `no skin set (the default skin alone)`
|
|
3687
|
+
(§9) — every slot resolved through `SkeletonData.defaultSkin` and nothing else.
|
|
3688
|
+
Move a deformed mesh into a named skin, which the format not only allows but keys
|
|
3689
|
+
the timeline on, and the slot showed **no attachment**. This is what
|
|
3690
|
+
`gallery/squash`'s ball printed with its one mesh moved into a skin `suit`, on
|
|
3691
|
+
every one of its five keys, before #583:
|
|
3692
|
+
|
|
3693
|
+
```
|
|
3694
|
+
DEFORM bounce suit/ball/ball key 1 t=0.340000 transform affine scale=[0.88, 1.16]
|
|
3695
|
+
skipped A39 reads no winding off this key: the slot shows no attachment at all at this
|
|
3696
|
+
time, not this mesh, so the runtime applies no deform to it here and draws
|
|
3697
|
+
none of it — a triangle that draws no pixels cannot draw them backwards
|
|
3698
|
+
```
|
|
3699
|
+
|
|
3700
|
+
— `A39` went **PASS → SKIP** on a rig whose only edit was which skin one mesh sat
|
|
3701
|
+
in, in a sentence that reads as a verdict on that rig. Worn, the same build
|
|
3702
|
+
reports every figure the default-skin one does, to the last digit, and `A39` gates
|
|
3703
|
+
it: the two `DEFORM` blocks differ in the skin of the triple and in nothing else.
|
|
3704
|
+
|
|
3705
|
+
**Two things follow, and one of them is not about art:**
|
|
3706
|
+
|
|
3707
|
+
- ⚠️ **A "nothing is drawn" sentence now names the skin it was measured in** —
|
|
3708
|
+
`the slot shows attachment "away" at this time, with skin "suit" worn, not this
|
|
3709
|
+
mesh …`. It is on the *shows-something-else* branch only, because that is the
|
|
3710
|
+
branch a skin decides; an alpha is read off the pose and has no skin in it. The
|
|
3711
|
+
clause is what separates "this slot is empty" from "this slot is empty in the
|
|
3712
|
+
dress this mesh lives in", and only the second is a measurement.
|
|
3713
|
+
- ⭐ **A `skin: true` bone or constraint (§3.4.1) is switched on too.**
|
|
3714
|
+
`Skeleton.setSkin` calls `updateCache`, which leaves a `skinRequired` bone
|
|
3715
|
+
inactive and a `skinRequired` constraint out of the update cache under any skin
|
|
3716
|
+
that does not list it. So a **slider** dressed into the same skin as the mesh
|
|
3717
|
+
it drives used to be measured with itself switched off, and failed in two ways
|
|
3718
|
+
depending on its `local` flag: a world-read property never moved, so the frame
|
|
3719
|
+
was reported as *"played on a track"* — an animation a slider is the only way
|
|
3720
|
+
into (§4.11.4) — while a local-read one kept its mapping but never left
|
|
3721
|
+
`SliderPose.time` 0, so every key came back as *"at a time no dial selects"*.
|
|
3722
|
+
Both now reach their own key times.
|
|
3723
|
+
|
|
3724
|
+
⛔ **What it does not do is try every skin.** `Attachment.timelineSlots` lets one
|
|
3725
|
+
deform reach a second slot — a linked mesh with `inheritTimelines` — and that copy
|
|
3726
|
+
may live in a skin of its own; `setSkin` dresses the whole skeleton at once, so
|
|
3727
|
+
under the timeline's own skin that copy resolves to nothing and is reported as
|
|
3728
|
+
drawing nothing, which is exactly what the runtime does with that same skin on.
|
|
3729
|
+
Posing the key again under some other skin would be rigc choosing which dress the
|
|
3730
|
+
character is wearing, and which skin is worn is yours. What it owes you instead is
|
|
3731
|
+
the skin in the sentence, so the reading is never mistaken for a verdict.
|
|
3732
|
+
|
|
3733
|
+
📌 **`check` is the other half of the same question and it is not automatic**:
|
|
3734
|
+
a rig whose art lives in named skins is rendered and checked **once per skin**,
|
|
3735
|
+
with `--skin` on both sides (§9). The difference is where the name comes from —
|
|
3736
|
+
a deform timeline carries its own skin and a frame set does not.
|
|
3737
|
+
|
|
3738
|
+
---
|
|
3739
|
+
|
|
3585
3740
|
### 4.12 `path` and `slider` timelines — tracks, not their own groups
|
|
3586
3741
|
|
|
3587
3742
|
Unlike `ik` and `transform`, these two are ordinary `tracks` entries: the format
|
|
@@ -3740,6 +3895,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
3740
3895
|
| `animation "A" slot "X" attachment: key at Ns is Ms past the declared duration Ds` | §4.5 — the key is past the end of the animation and nothing will sample it. Move the key onto `duration`, or raise `duration` |
|
|
3741
3896
|
| `animation "A" keys unknown bone "X"` | the track's `bone` is not in the rig |
|
|
3742
3897
|
| `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (§4.4) |
|
|
3898
|
+
| `animation "A" physics constraint "C" mass key at t=… is 0 (massInverse Infinity); must be > 0 — …` | §4.4 — a keyed physics value the runtime cannot use. The message names the bound and the `PhysicsConstraint.js` lines that make it one: `mass` and `strength` are `> 0`, `damping` is inside `(0, 1)`, `mix` is `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)) |
|
|
3743
3899
|
| `a key carries both a named easing and a raw curve; pick one` | R6 |
|
|
3744
3900
|
| `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
|
|
3745
3901
|
| `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
|
|
@@ -3816,6 +3972,18 @@ The report prints one line per assertion:
|
|
|
3816
3972
|
- **PASS** — it ran and held.
|
|
3817
3973
|
- **SKIP** — it had *nothing to look at*, and the reason says what was missing. A
|
|
3818
3974
|
skip is never folded into the pass count.
|
|
3975
|
+
🚨 **"Nothing to look at" includes a subject the skeleton does not carry**
|
|
3976
|
+
([#580](https://github.com/firejune/rigc/issues/580)). A rule named
|
|
3977
|
+
⟨subject⟩_⟨property⟩ — `A03_REGION_WIDTH_HEIGHT_FINITE`,
|
|
3978
|
+
`A04_MESH_TRIANGLES_AND_ENCODING`, `A23_PHYSICS_CONSTRAINT_EFFECTIVE` — walks
|
|
3979
|
+
that subject and measures each member, so a skeleton with no region, no mesh or
|
|
3980
|
+
no physics constraint gives it nothing and it **SKIPs**, naming the subject. A
|
|
3981
|
+
rule named NO_⟨construct⟩ — `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS`,
|
|
3982
|
+
`A02_NO_BONE_TRANSFORM_KEY`, `A11_NO_CLIPPING_ATTACHMENTS`,
|
|
3983
|
+
`A12_NO_DARK_COLOR`, `A14_NO_FULL_FRAME_MESH` — asks how many of a thing the
|
|
3984
|
+
artifact carries, and **zero is the answer**, so it **PASSes**. ⇒ On a foreign
|
|
3985
|
+
export the SKIP list is a free inventory of what the skeleton does not contain,
|
|
3986
|
+
and the PASS list never contains a rule that looked at nothing.
|
|
3819
3987
|
- **PROF** — the profile you chose does not carry that kind of rule. Two kinds sit
|
|
3820
3988
|
outside `spine`, not one: the renderer rules **and** the archetype rules. The
|
|
3821
3989
|
exclusion is checked before the assertion's body runs, so an archetype rule with
|
|
@@ -3849,46 +4017,46 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
|
|
|
3849
4017
|
|
|
3850
4018
|
| Assertion | Profile | What tripped it, and where to fix it |
|
|
3851
4019
|
| --- | --- | --- |
|
|
3852
|
-
| `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas.
|
|
4020
|
+
| `A00_ROUNDTRIP_PARSE` | both | `spine-core` could not parse the skeleton or the atlas. Almost everything else in the report is downstream of this one — fix it first. When it fails, every rule that reads the loaded skeleton or the loaded atlas reports **SKIP** saying so by name, so the row count stays at the full registry and the summary's *measured* figure tells you how little was actually asked ([#568](https://github.com/firejune/rigc/issues/568)). ⚠️ **Two rules run before it and can be upstream of it**: `A31_DRAW_ORDER_OFFSETS_RESOLVE`, because a bad draw-order key makes the loader spin rather than return, and `A08_REGION_NAMES_MATCH_ATTACHMENTS`, because a `path` naming no region makes it throw. The round trip is still attempted either way; when the loader refuses a path A08 has already refused, this row **defers** to A08 by name instead of restating the miss in the parser's poorer words ([#589](https://github.com/firejune/rigc/issues/589)) |
|
|
3853
4021
|
| `A01_NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS` | both | a 4.1/4.2-shaped `ik`/`transform`/`path`/`physics`/`slider` array. rigc emits the 4.3 `constraints` array, so this normally means hand-edited JSON |
|
|
3854
4022
|
| `A02_NO_BONE_TRANSFORM_KEY` | both | a bone uses 4.2's `transform`; rename it `inherit` in the rig spec |
|
|
3855
|
-
| `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height
|
|
3856
|
-
| `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) |
|
|
3857
|
-
| `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel** |
|
|
3858
|
-
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk. Under `spine-html` also: `pma`, rotation, and a page that is neither **one part covering it exactly** (the unpacked convention) nor a **tiling** — a page whose regions all sit inside it and none of which overlap ([#266](https://github.com/firejune/rigc/issues/266)). A packed atlas therefore gates under this profile; what the message names is the region that runs off its page, or the pair that shares texels |
|
|
3859
|
-
| `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file |
|
|
3860
|
-
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both | an
|
|
4023
|
+
| `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 ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
4024
|
+
| `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 ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
4025
|
+
| `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 ([#580](https://github.com/firejune/rigc/issues/580)). 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 |
|
|
4026
|
+
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk. Under `spine-html` also: `pma`, rotation, and a page that is neither **one part covering it exactly** (the unpacked convention) nor a **tiling** — a page whose regions all sit inside it and none of which overlap ([#266](https://github.com/firejune/rigc/issues/266)). A packed atlas therefore gates under this profile; what the message names is the region that runs off its page, or the pair that shares texels. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
4027
|
+
| `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. ⚠️ An atlas with **no page block at all** — no non-blank line — is not one of those: its subject is absent, so this reports **SKIP** naming the byte count it read, and so do the four rules below whose subject is a page ([#608](https://github.com/firejune/rigc/issues/608)). A rig whose skins need no art writes exactly that file (§3.4), and before #608 this row refused it with two findings naming a page block that was not there. What an empty atlas does **not** excuse is an attachment that wants a region out of it — that is `A08` |
|
|
4028
|
+
| `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. Until [#589](https://github.com/firejune/rigc/issues/589) they were unreachable: the loader threw first and the miss arrived as `A00_ROUNDTRIP_PARSE`. There is no `spine-html` clause here any more — a placeholder is free to differ from the region its `path` names ([#574](https://github.com/firejune/rigc/issues/574)) **SKIP** when no attachment names a region *and* the atlas declares none — both of its subjects at once ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
3861
4029
|
| `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 |
|
|
3862
|
-
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale |
|
|
4030
|
+
| `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale. **SKIP** when the skeleton carries no animation ([#580](https://github.com/firejune/rigc/issues/580)): the NaN is produced by stepping, and a static rig is never stepped — the same subject `A09` skips on |
|
|
3863
4031
|
| `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
|
|
3864
4032
|
| `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
|
|
3865
|
-
| `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7, issue #274) |
|
|
4033
|
+
| `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7, issue #274) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh ([#580](https://github.com/firejune/rigc/issues/580)). A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
|
|
3866
4034
|
| `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip. **SKIP** when the skeleton declares no stage (§3.1): there is no full frame to span, and *unmeasured* must not print the same green as *measured and clear* |
|
|
3867
4035
|
| `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. **SKIP** when there is no `idle` animation, or when the one there is carries no bone timeline — a rule whose subject does not exist is unmeasured and not satisfied ([#568](https://github.com/firejune/rigc/issues/568)) |
|
|
3868
4036
|
| `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
|
|
3869
|
-
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out` |
|
|
4037
|
+
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) — as it is for `A06`, `A19` and `A27`; see `A07` ([#608](https://github.com/firejune/rigc/issues/608)) |
|
|
3870
4038
|
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
|
|
3871
|
-
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the full-stage base plate may be opaque. 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 ([#266](https://github.com/firejune/rigc/issues/266)) |
|
|
3872
|
-
| `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. Under `spine-html` also: an unweighted mesh, or a binding at weight 0 |
|
|
4039
|
+
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the full-stage base plate may be opaque. 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 ([#266](https://github.com/firejune/rigc/issues/266)) **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
4040
|
+
| `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. Under `spine-html` also: an unweighted mesh, or a binding at weight 0. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
3873
4041
|
| `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 |
|
|
3874
|
-
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count |
|
|
3875
|
-
| `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, is muted by `mix: 0`, has `mass: 0`, has `strength: 0`, or has `damping` outside `(0, 1)` so it never settles |
|
|
3876
|
-
| `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed |
|
|
4042
|
+
| `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 ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
4043
|
+
| `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, is muted by `mix: 0`, has `mass: 0`, has `strength: 0`, or has `damping` outside `(0, 1)` so it never settles — **at rest, and on every physics timeline key** ([#610](https://github.com/firejune/rigc/issues/610)). 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. One difference between the two arms, and the runtime is the reason for it: 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. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. **SKIP** when the skeleton declares no physics constraint ([#580](https://github.com/firejune/rigc/issues/580)) — the same sentence `A36` and `A37` have always printed for their own constraint types |
|
|
4044
|
+
| `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
3877
4045
|
| `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
|
|
3878
|
-
| `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3) |
|
|
3879
|
-
| `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename |
|
|
4046
|
+
| `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3). **SKIP** when the rig declares no canonical slot order. ⚠️ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass ([#580](https://github.com/firejune/rigc/issues/580)): the completeness clause reads it as every declared slot lost and names them, which is the maximal case of what [#575](https://github.com/firejune/rigc/issues/575) filed |
|
|
4047
|
+
| `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename. **SKIP** when the atlas declares no region ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
3880
4048
|
| `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width. **SKIPs** on authored geometry and on a contour mesh — neither has rows rigc paired |
|
|
3881
4049
|
| `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
|
|
3882
4050
|
| `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
|
|
3883
4051
|
| `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted |
|
|
3884
4052
|
| `A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
|
|
3885
4053
|
| `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index, a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
|
|
3886
|
-
| `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. **SKIP** when no animation carries one |
|
|
4054
|
+
| `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path`, `physics` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.4, §4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. **SKIP** when no animation carries one |
|
|
3887
4055
|
| `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11, issue #262). **SKIP** when no animation carries a deform timeline |
|
|
3888
4056
|
| `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying its `mix` (§3.5.1). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. **SKIP** when the skeleton declares no path constraint |
|
|
3889
4057
|
| `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` (§3.5.2). **SKIP** when the skeleton declares no slider |
|
|
3890
4058
|
| `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
|
|
3891
|
-
| `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
|
|
4059
|
+
| `A39_DEFORM_KEEPS_TRIANGLE_WINDING` | archetype | a `deform` key reverses a triangle's winding, so the mesh has locally turned inside out and draws its texture backwards there (§4.11). The detail names the animation, the slot, the attachment, the key index and time, and each reversed triangle with its vertex triple and its signed area before and after. Measured at the key's **own** time, deformed against the same posed bones undeformed, so a mirrored slot bone cancels and a wrong *projection* with intact winding is correctly silent. A projection past its fold angle is the usual cause — [FACE.md §4.2](FACE.md) has the closed form. Legitimate art does fold, so declare `invariants.deformMayFold` (§3.7) for a slot that folds on purpose. ⚠️ A key whose slot **draws no pixels at that key's own time** — faded to alpha exactly 0, or showing another attachment — is measured and then passed over, because "draws its texture backwards" is false when nothing of it is drawn; the key is named on the stats line (`deformKeysNotDrawn`) and in the `DEFORM` block, never silently. The bar is **exactly 0**: at alpha 0.5 the fold is still refused and the alpha is in the message. It is per key and per time, so the same slot folding at full alpha in another animation is refused as before. ⚠️ And the **spans between** consecutive keys are scanned too (§4.11.3, issue #403): the runtime interpolates, so a deform inside its fold angle at every key can be past it in between. That refusal is its own sentence — `BETWEEN key 0 (t=0s) and key 1 (t=0.5s), at t=…` — with the time solved for in closed form and then posed and measured like any key, alpha read at that same moment. `deformSpansScanned` says on every green build that the scan ran. ⚠️ And the **frame** it poses in is the one the animation is reached in (§4.11.4, issue #407): on a track when nothing applies it, and otherwise once per **slider**, with that slider's mapping inverted and its bone driven until the runtime selects the key's own time — because a slider picks the time, so the two are one number and posing them independently is a frame that never occurs. The frame is on every `DEFORM` line, on the stats line as `deformFrames`, and in the refusal itself when it is not the track. A key at a time **no dial value selects** is measured in the frame the runtime does land on, left out of `deformKeysMeasured` and named as `deformKeysUnreachable`/`deformUnreachable` — never refused and never silent. ⚠️ And the **skin** it poses in is the one the timeline is keyed on (§4.11.5, issue #583), since a deform's address is a `skin / slot / attachment` triple: the pose wears that skin, which also switches on any `skin: true` bone or constraint it activates, and the "nothing is drawn" sentence names the skin it was read under. **SKIP** when no animation carries a deform timeline, when nothing keyed has triangles, when every mesh keyed is exempt, when every key measured draws no pixels or is unreachable *and no span between them folds where anything is drawn*, or when there is no rig info at all |
|
|
3892
4060
|
| `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be additive (a slot colour, an attachment swap, a draw order, a sequence), where `"additive": true` is not the fix and one of the two has to go. The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, and which one wins today. Three shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, and two sliders on different properties. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
|
|
3893
4061
|
| `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` | both | a physics constraint driving a component the **Spine editor** cannot hold, on a rig that declared `invariants.editorRoundTrip` (§3.7). The editor's physics model holds `x` and `y` only, with no cap on how many at once, so a constraint driving `rotate`, `scaleX` or `shearX` is imported, exported and handed back driving **nothing** — measured over three rigs and twelve constraints with the predictions written first ([#540](https://github.com/firejune/rigc/issues/540)). The detail names the constraint and each component. ⚠️ rigc's own output is correct — every runtime plays a rotation jiggle — so this is opt-in and the default is *not* silence: on a rig that declares nothing it **SKIPs**, and the SKIP names the constraint and the component anyway, so an author learns without having asked. Fix by driving the constraint in `x`/`y`, or by dropping the declaration if the rig never goes near the editor. Disjoint from `A23_PHYSICS_CONSTRAINT_EFFECTIVE` by construction: A23 refuses an **empty** driven set, which is what comes back from the editor, and this refuses a non-empty one that will not survive going in. **SKIP** also when the rig declares the editor and carries no physics constraint at all |
|
|
3894
4062
|
|
|
@@ -4503,6 +4671,14 @@ remember:
|
|
|
4503
4671
|
A skin name the candidate does not declare is refused with the ones it does —
|
|
4504
4672
|
`the candidate declares no skin "path"; it declares [default, patch, torn]`.
|
|
4505
4673
|
|
|
4674
|
+
📌 **Deform measurement needs no such flag, because the timeline carries the
|
|
4675
|
+
name.** `A39` and the `DEFORM` block pose each key with the skin that key is keyed
|
|
4676
|
+
on — a deform timeline's address is a `skin / slot / attachment` triple — so a
|
|
4677
|
+
mesh in a named skin is measured in that skin without anything being passed on the
|
|
4678
|
+
command line (§4.11.5, issue #583). The difference is where the name comes from,
|
|
4679
|
+
not which command cares: `check` compares two picture sets and neither of them
|
|
4680
|
+
records a skin unless you say so, while a deform key knows its own.
|
|
4681
|
+
|
|
4506
4682
|
⭐ **A frame set may ship a contact sheet instead of every frame, and the sheet is
|
|
4507
4683
|
compared too.** A long shot does not commit 311 near-duplicate PNGs: rung 2's sets
|
|
4508
4684
|
ship `f0000.png` and `f0310.png` plus a `contact.png` holding all 311 sampled
|
package/docs/INGEST.md
CHANGED
|
@@ -162,7 +162,11 @@ rigc validate …/examples/3-timing-and-spacing/export/3-timing-and-spacing-ess.
|
|
|
162
162
|
SKIP A32_EVENT_KEYS_RESOLVE: no animation carries an event timeline
|
|
163
163
|
SKIP A34_CONSTRAINT_TIMELINE_TARGETS: no animation carries a constraint timeline
|
|
164
164
|
SKIP A35_DEFORM_KEYS_FIT_THE_ATTACHMENT: no animation carries a deform timeline
|
|
165
|
+
SKIP A04_MESH_TRIANGLES_AND_ENCODING: the skeleton carries no mesh attachment
|
|
165
166
|
SKIP A33_VERTEX_ATTACHMENT_GEOMETRY: the skeleton carries no bounding box, clipping attachment or path
|
|
167
|
+
SKIP A20_MESH_WEIGHTS_COHERENT: the skeleton carries no mesh attachment
|
|
168
|
+
SKIP A22_MESH_UVS_IN_UNIT_RANGE: the skeleton carries no mesh attachment
|
|
169
|
+
SKIP A23_PHYSICS_CONSTRAINT_EFFECTIVE: the skeleton declares no physics constraint
|
|
166
170
|
…
|
|
167
171
|
PROF A12_NO_DARK_COLOR: renderer rule, not in profile "spine"
|
|
168
172
|
PROF A21_MESH_RIM_PINNED: archetype rule, not in profile "spine"
|
|
@@ -175,10 +179,17 @@ Read it as three separate statements, because they answer three different questi
|
|
|
175
179
|
- **`PASS` / `FAIL`** — the rule ran, and this is its verdict.
|
|
176
180
|
- **`SKIP`** — the rule ran and had nothing to measure, and the line says what was
|
|
177
181
|
absent. A `SKIP` is never a pass, and on foreign data the `SKIP` list is also a
|
|
178
|
-
**free inventory of what the skeleton does not contain**. The
|
|
182
|
+
**free inventory of what the skeleton does not contain**. The nine lines above tell
|
|
179
183
|
you, without your having opened the JSON, that this export has no draw-order
|
|
180
|
-
timeline, no event timeline, no constraint timeline, no deform timeline,
|
|
181
|
-
bounding box, clipping attachment or
|
|
184
|
+
timeline, no event timeline, no constraint timeline, no deform timeline, no mesh
|
|
185
|
+
attachment, no physics constraint, and no bounding box, clipping attachment or
|
|
186
|
+
path. ⭐ Four of those lines used to read `PASS`
|
|
187
|
+
([#580](https://github.com/firejune/rigc/issues/580)): a rule that walks the
|
|
188
|
+
meshes, or the physics constraints, and finds none has measured nothing, and
|
|
189
|
+
reporting that as held both overstated the gate and cost you the inventory line.
|
|
190
|
+
What still passes over an empty list is the other kind of rule — `A01`, `A02`,
|
|
191
|
+
`A11`, `A12`, `A14` ask *how many of this does the file carry*, and **zero is the
|
|
192
|
+
answer**.
|
|
182
193
|
- **`PROF`** — the rule was excluded by the profile before its body ran. §3.3.
|
|
183
194
|
|
|
184
195
|
⚠️ **A green here is a statement about validity and nothing else.** It does not say
|
|
@@ -422,6 +433,16 @@ no field of the file). `bun run selftest` holds every rig this repository builds
|
|
|
422
433
|
that on every run, which is the one gate here that compares an emitted file against
|
|
423
434
|
a file rigc did not write.
|
|
424
435
|
|
|
436
|
+
📊 **And it holds the twelve editor exports to the weaker claim that is available for
|
|
437
|
+
them** ([#594](https://github.com/firejune/rigc/issues/594)). Every
|
|
438
|
+
`examples/*/export/*.json` is ingested with `--art none`, rebuilt through the pack
|
|
439
|
+
beside it, and `diff`ed against the file it was read from: **12 of 12 come back with 0
|
|
440
|
+
blockers and 1.000 on all 49 ratio-bearing measures and all 5 reported ones.** Byte
|
|
441
|
+
identity is not the claim there and the reason is the input, not the round trip — §2.3
|
|
442
|
+
has the three kinds of difference, measured. ⚠️ Which pack is "the one beside it" is
|
|
443
|
+
resolved rather than guessed, for §0.2's reason: `spineboy/export` holds two, and
|
|
444
|
+
`spineboy-run.atlas` covers neither skeleton in it.
|
|
445
|
+
|
|
425
446
|
**What it will not do is invent.** Everything the spec format cannot hold is a
|
|
426
447
|
finding with a code — `BLOCK` for a construct the rebuild will be missing, `JUDGE`
|
|
427
448
|
for the two values a skeleton does not carry, `LOSS` for the one number rigc
|
|
@@ -429,19 +450,29 @@ re-derives on purpose. A blocker exits non-zero and still writes both files.
|
|
|
429
450
|
|
|
430
451
|
**Two values are not in a skeleton**, so `ingest` asks rather than guesses:
|
|
431
452
|
|
|
432
|
-
- **the stage** (`skeleton.width`/`height`) —
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
the
|
|
453
|
+
- **the stage** (`skeleton.width`/`height`) — `--stage x,y,w,h` is how you supply one
|
|
454
|
+
when the file has none. ⚠️ **This page said an editor export carries none until
|
|
455
|
+
[#594](https://github.com/firejune/rigc/issues/594) measured it: all twelve exports in
|
|
456
|
+
the fetched corpus carry a stage**, `ingest` reads it straight through, and not one of
|
|
457
|
+
them needed the flag. What holds without qualification is that the box cannot be
|
|
458
|
+
*derived* — posing the rig gives the *animated* extent, which is a different number
|
|
459
|
+
from the setup box — so a skeleton that really declares none is a `NO_STAGE` blocker
|
|
460
|
+
rather than a guess. It is also the value that costs least to get wrong: `diff`
|
|
461
|
+
reports it as two measures of its own (`stage_present`, `stage_box`, since
|
|
462
|
+
[#578](https://github.com/firejune/rigc/issues/578)) and they are `(reported)`, so
|
|
463
|
+
nothing on the ladder reads them and an absurd box is green nearly everywhere. The
|
|
464
|
+
corpus half of the selftest's `IG` suite is the one gate that does read them;
|
|
437
465
|
- **each animation's duration** — the format has no such field. The largest key time
|
|
438
466
|
is used, stated in the motion spec's `note`, and recorded as a finding per
|
|
439
467
|
animation. Edit it if you know the real number.
|
|
440
468
|
|
|
441
|
-
And
|
|
442
|
-
names an `image` per attachment
|
|
443
|
-
`width`/`height` for `build --atlas-in
|
|
444
|
-
|
|
469
|
+
And two flags for what the skeleton also does not encode: `--art loose` (the default)
|
|
470
|
+
names an `image` per attachment resolved against loose PNGs, `--art none` states
|
|
471
|
+
`width`/`height` for `build --atlas-in`; and under `loose`, `--images <dir>` writes
|
|
472
|
+
the rig spec's own images directory relative to `--out`, so the rebuild is a plain
|
|
473
|
+
`build --rig … --motion … --out …` rather than one carrying `--images` forever. It is
|
|
474
|
+
refused together with `--art none`, which writes no `image` for a directory to be the
|
|
475
|
+
base of. [AUTHORING §0.3](AUTHORING.md) is the loop in full.
|
|
445
476
|
|
|
446
477
|
📝 Both written specs carry a `note` saying they are decompiled and naming the file
|
|
447
478
|
they came from. Leave it there — §2.4 is why.
|
|
@@ -558,6 +589,25 @@ State the ambition in the right units, because three different things get called
|
|
|
558
589
|
| **Geometric agreement** — the same drawn pixels, allowing for the atlas | ✅ yes, and `check` measures it | see below |
|
|
559
590
|
| **Byte-identical JSON** | ⛔ **no, and not because of the geometry** | rigc writes defaults explicitly where the editor omits them, and the editor writes bookkeeping rigc has no field for. SPEC_COVERAGE records the count on rung 6: a field-by-field comparison against the reference export leaves **49 differences, every one benign** — 39 explicit defaults, 3 editor bookkeeping keys, 1 runtime version string, and 6 bone `icon` values, which was the only thing the rig spec could not say at all |
|
|
560
591
|
|
|
592
|
+
⚠️ **The third row holds for `ingest` too, and it is worth knowing in which direction.**
|
|
593
|
+
`build(ingest(x))` is byte-identical for a skeleton **rigc** emitted — that is the
|
|
594
|
+
contract `bun run selftest` gates on every run — and it is not, for a skeleton the
|
|
595
|
+
editor emitted. Measured over all twelve corpus exports, a field-by-field walk of the
|
|
596
|
+
rebuild against its source produces differences of exactly three kinds, in every file:
|
|
597
|
+
|
|
598
|
+
| Kind | Example, candidate vs reference | Why |
|
|
599
|
+
| --- | --- | --- |
|
|
600
|
+
| **header bookkeeping**, 3 per file | `skeleton.hash: undefined vs "VFWbaK2UoCM"`, `skeleton.audio: undefined vs null`, `skeleton.spine: "4.3.13" vs "4.3.75-beta"` | the rig spec has no field for `hash` or `audio`, and the version is the runtime rigc links. `ingest` reports all three as findings — `HEADER_BOOKKEEPING` and `HEADER_REDERIVED` |
|
|
601
|
+
| **an omitted default written out** | `…rotate[0].time: 0 vs undefined` | the editor omits a zero `time`; rigc writes it. AUTHORING §10.5's *do not imitate the exporter's omissions*, from the other side |
|
|
602
|
+
| **the emitted precision** | `…curve[0]: 0.066667 vs 0.06666667`, `uvs[0]: 0 vs 2.554152e-7` | rigc emits six decimals |
|
|
603
|
+
|
|
604
|
+
⇒ **So the corpus gate is `diff` at 1.000 rather than a byte comparison**, and it is
|
|
605
|
+
worth being exact about what that does and does not cover. `diff` compares structure —
|
|
606
|
+
counts, names, parentage, order, timeline kinds, key counts, curve kinds — and **not
|
|
607
|
+
the values inside the keys**, which is why the precision row above is invisible to it.
|
|
608
|
+
On rigc's own rigs byte identity covers both; on a foreign export the values are held
|
|
609
|
+
by `check` (pixels) or by nothing, depending on what you render.
|
|
610
|
+
|
|
561
611
|
The geometric row needs a real number, because a naive reading of `check` makes an
|
|
562
612
|
exact transcription look wrong. Here is the 3-timing transcription against frames
|
|
563
613
|
rendered from the export it was transcribed from:
|
package/docs/SPEC_COVERAGE.md
CHANGED
|
@@ -658,8 +658,7 @@ editor-made meshes — those arrive as authored `uvs`/`triangles`/`weights`.
|
|
|
658
658
|
| `slots.rgba` | ✅ |
|
|
659
659
|
| `slots.rgb`, `alpha` | ❌ emitted (validator knows the channel counts) |
|
|
660
660
|
| `slots.rgba2`, `rgb2` | 🚫 **A12_NO_DARK_COLOR** (`validate.ts:257-261`) |
|
|
661
|
-
| `physics.mix`, `physics.reset` | ✅ (`PHYSICS_TRACKS
|
|
662
|
-
| `physics.inertia/strength/damping/mass/wind/gravity` | ❌ |
|
|
661
|
+
| `physics.inertia/strength/damping/mass/wind/gravity`, `physics.mix`, `physics.reset` | ✅ — all eight (`PHYSICS_TRACKS` in `compile.ts`), authored as `tracks` entries naming `physics`. ⚠️ Their per-key defaults are the parser's, and part 1-8 above is the source: 0 on the six, 1 on `mix`. Not the constraint defaults at `:306-312` |
|
|
663
662
|
| `ik`, `transform` | ✅ — one unnamed timeline per constraint, keyed by the motion spec's `ik` / `transform` arrays |
|
|
664
663
|
| `path.position/spacing/mix`, `slider.time/mix` | ✅ (`PATH_TRACKS` / `SLIDER_TRACKS`) — authored as `tracks` entries naming `path` or `slider`, the same shape as `physics`, since both groups put named timelines under a constraint name. `path.mix` is one timeline of three channels |
|
|
665
664
|
| `attachments.<skin>.…deform` | ❌ (validator knows it: 1 channel, `validate.ts:104-107`) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|