spine-rigc 0.28.0 β†’ 0.29.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/docs/AUTHORING.md CHANGED
@@ -324,6 +324,42 @@ texel count beside it so both numbers are visible:
324
324
  measures the PNG. Reach for `--atlas-in` when the pack is what you were handed, or
325
325
  when drawing through the pack's own texels is the point.
326
326
 
327
+ 🚨 **A page that declares a size it does not have is a different thing, and it is
328
+ refused** ([#715](https://github.com/firejune/rigc/issues/715)). The common shape
329
+ is a pack whose `4096x4096` pages ship as `2048x2048` PNGs with the atlas
330
+ untouched β€” every region still stated in 4096-space β€” and that is **not** what a
331
+ `scale:` line says. A runtime draws it anyway: `TextureAtlas` computes every
332
+ region's UVs as a fraction of the **declared** size (`region.u = region.x /
333
+ page.width`) and nothing in the region mapping opens the file, so the same
334
+ fraction of the picture is addressed whatever size the PNG is. What a page grid
335
+ that disagrees with its file breaks is every reader that addresses the page in
336
+ **texels** β€” `A19`'s alpha scan, the region lift a mesh generator traces, and a
337
+ canvas renderer that cuts each part out by source rectangle. So `A06` refuses it
338
+ under **both** profiles, names the ratio it measured on each axis, and prints the
339
+ header that states the same art truthfully:
340
+
341
+ ```bash
342
+ # FAIL A06_ATLAS_PAGE_SIZE_MATCHES_PNG: page "hero.png" declares 4096x4096 and its PNG is 2048x2048 β€”
343
+ # 0.5000 of the declared width and 0.5000 of the declared height. … The format states coarser texels
344
+ # with the `scale:` header and rigc builds that: declare `size: 2048, 2048` with `scale: 0.5000` and
345
+ # multiply every `bounds`/`offsets` on this page by 0.5000, and every part keeps the size it has now.
346
+ ```
347
+
348
+ β‡’ **Carry that out and the build is green with the attachment sizes it already
349
+ had**, because the division cancels (`originalWidth / scale`, above) β€” which is
350
+ the measurement that settles where this clause belongs: the format already has a
351
+ line for coarser texels and rigc already reads it, so the refusal hands over a
352
+ repair rather than a preference. Two limits, and the message names whichever one
353
+ it hits instead of leaving it to be discovered:
354
+
355
+ - a region whose `bounds`/`offsets` do not land on a whole texel of the file
356
+ cannot be re-declared at all, and the refusal names the first number that does
357
+ not divide;
358
+ - a page whose two axes differ β€” `4096x4096` declared over a `2048x3072` PNG β€”
359
+ has no honest header either, because `scale:` carries **one** ratio. The message
360
+ says so rather than offering one, and the repair is to re-export the page at the
361
+ size the atlas declares, or repack.
362
+
327
363
  **What `--out` holds afterwards:** `skeleton.json` and a `skeleton.atlas` that is
328
364
  the pack, page paths pointing back at the pack's own PNGs β€” so `rigc validate
329
365
  <that directory>` reads it green with no flags, exactly as it reads a loose
@@ -819,8 +855,7 @@ behind it writes literal `x`/`y` instead.
819
855
 
820
856
  **R10 β€” The `animations` object is keyed in the editor's order, not in yours, and
821
857
  names that have no one order are refused.** Declare animations in whatever order
822
- reads best; the emit keys them the way the Spine editor does β€” **natural and
823
- case-insensitive**. This is the one place rigc
858
+ reads best; the emit keys them the way the Spine editor does. This is the one place rigc
824
859
  reorders anything you wrote, and it is not cosmetic: a `slider`'s animation is a
825
860
  **name** in JSON and an **ordinal** in the format's binary half, so an editor that
826
861
  re-sorts the object repoints every slider whose animation moved index β€” silently,
@@ -829,61 +864,73 @@ in a file that still parses and still gates green (Β§3.5.2,
829
864
  animation's own body is byte-identical either way, and every other collection is
830
865
  emitted in the order you gave it.
831
866
 
832
- ⚠️ **What is measured about that comparator, and what is not.** The editor sorts
833
- natural and case-insensitive β€” measured, two rigs, one axis each:
834
- `Turn, sweep, wave` came back `sweep, Turn, wave`, and `turn10, turn2, zoom` came
835
- back `turn2, turn10, zoom` ([#539](https://github.com/firejune/rigc/issues/539)).
836
- But "natural and case-insensitive" is a **family** of comparators, not one, and
837
- **four** of its choices have never been measured. rigc emits the order every
838
- member of that family agrees on, and **refuses the sets where one of the four
839
- would decide**, naming the pair. So the rule you have to hold is about *names*,
840
- and it is four things:
841
-
842
- | Do not let two animation names differ | Because | Instead |
867
+ ⚠️ **What that comparator is, measured rather than inferred.** Five name lists
868
+ went through a licensed 4.3.26 editor as JSON and came back as JSON, and the ten
869
+ answers are in the repository as
870
+ `fixtures/editor-order/probe{1..5}.{in,out}.json`
871
+ ([#728](https://github.com/firejune/rigc/issues/728)). Every row below is a clause
872
+ of the rule with the pair from those files that shows it β€” nothing here is a
873
+ guess, and the selftest re-derives each row from the files rather than from this
874
+ page:
875
+
876
+ | The editor | Shown by |
877
+ | --- | --- |
878
+ | folds case **upward**, per character, and leaves a character with no single upper case where it is | `z` before `_x` (`_` is 0x5F, above `Z` and below `a`); `z` before `ß`, which a whole-string upper case would write `SS` and sort among the `S`s |
879
+ | reads a **run of digits as a number** β€” at any position, after any script | `x2` before `x10`; `δΈ­2` before `δΈ­10` |
880
+ | **defers** two runs that are one number written twice, and settles them at the end with the leading zeros later | `a01` before `a1b`, so the walk did not stop at the equal runs; `a1` before `a01`, which is how it ends |
881
+ | **skips a space**, and gives that tie to the name with fewer of them β€” after the leading-zero tie-break, not before it | `bc` before `b c`; `a 1` before `a01` |
882
+ | otherwise orders by **code point after folding**, and does not treat punctuation as ignorable | `a-1` before `a1`; `a1b` before `a_1` |
883
+ | leaves a remaining **tie in the order your file declares it** | `turn` before `Turn`; `Mango` before `mango` β€” each given in that order and returned in it |
884
+ | writes **leaves then folders at the root**, and **sub-folders then leaves inside a folder** | `h` before `f/sub1/deep/q`; `f/sub1/x` before `f/leafA` |
885
+
886
+ ⭐ **A tie is not an ambiguity, and that is what retired most of this rule's
887
+ refusals.** Two names the comparator cannot separate come back in the order the
888
+ file gave them, so the order rigc emits for such a pair is **your own declaration
889
+ order** and the editor keeps it. Nothing moves index, so there is nothing to
890
+ refuse: `Turn` beside `turn`, `turn01` beside `turn1`, `1turn` beside `turn`,
891
+ `wave_x` beside `wavea` all build, and each is keyed the way the round trips say.
892
+
893
+ So what is left refused is short, and each row is a pair two readings of the
894
+ **same** measurement order differently:
895
+
896
+ | Refused | Because | Instead |
843
897
  | --- | --- | --- |
844
- | by **case alone** (`Turn` against `turn`) | they fold together, so only a tie-break separates them, and nobody has measured which way it breaks | pick one case for all of them, or change a letter |
845
- | by a **number written two ways** (`turn01` against `turn1`) | `01` and `1` are one number twice; shorter-first, longer-first and lexicographic are all real tie-breaks | write the number one way β€” with leading zeros or without, but not both |
846
- | by a **digit run against a word** (`1turn` against `turn`) | comparators differ on whether a number sorts before a word | rename so a run of digits is never compared against a word |
847
- | by a **separator** β€” anything that is neither a letter nor a digit (`wave_x` against `wavea`, `wave` against `wave-`) | a collator may treat `-` or a space as ignorable, and `_` sits *between* `Z` and `a`, so folding up and folding down order it oppositely | rename so the first character that differs is a letter or a digit |
848
-
849
- ⭐ **Capitals and numbered series are not what is refused** β€” only pairs one of
850
- those four decides. `Sweep, Turn, Wave, Zoom02, Zoom10` builds, and so does
851
- `shot1 … shot12`: every member of the family puts each of those sets in one
852
- order, and that order is what rigc emits. A numbered series that crosses 9 β†’ 10 is
853
- keyed **1, 2, … 9, 10, 11, 12**, which is what the editor does with it β€” and is
854
- not what a codepoint sort does.
855
-
856
- βœ… **This list had two more rows before
857
- [#543](https://github.com/firejune/rigc/issues/543), and both were artefacts of
858
- the emit rather than facts about the editor.** rigc used to key `animations`
859
- **codepoint-ascending** and refuse every pair codepoint and the editor could order
860
- differently β€” which refused a pair that folds the other way (`Turn` against
861
- `sweep`) and a pair of digit runs of unequal width (`turn10` against `turn2`).
862
- Those are the only two name sets anybody has ever put through the editor and read
863
- back, so the tool was refusing precisely the pairs it knew the most about, and its
864
- only repair was *rename* β€” the one repair a transcription cannot take. Emitting a
865
- member of the family instead moves no byte on any set the old rule accepted; it
866
- just stops refusing the ones it did.
867
-
868
- **R11 β€” The `skins` array is written with `default` first and the rest in the
869
- editor's order, and skin names that have no one order are refused.** The same rule
870
- as R10, in the collection that was believed exempt from it. A skin is a **name** in
871
- the JSON half of the format and an **ordinal** in the binary half β€”
872
- `skins[readInt()]` for an attachment timeline, `skins[skinIndex]` for a linked mesh
873
- β€” so an editor that writes the array in another order repoints every such
874
- reference, silently, in a file that still parses. Measured: a rig built
875
- `default, zulu, mike, alpha` exported `default, alpha, mike, zulu`
898
+ | `number` β€” two or more digit runs that are each one number written twice, pointing opposite ways (`x01y1` against `x1y01`) | the round trips measured one deferred run; keeping the first such difference and keeping the last are both consistent with that | write each number one way, with leading zeros or without |
899
+ | `separator` β€” a whitespace character that is not a space (a tab, a no-break space) | the space is measured skipped and nothing else is, so a rule that skips all whitespace and one that skips only the space disagree here | rename so the only whitespace in either name is a space |
900
+ | `folder` β€” two sibling **folders** the comparator cannot separate (`Fx/a` against `fx/b`) | a tie between two leaves is file order, but the editor holds a folder as an object and keeps its entries together, and no round trip carried two folders one fold apart | rename one of the folders so they differ by more than letter case, spacing or a leading zero |
901
+
902
+ ⭐ **Capitals and numbered series are not what is refused.**
903
+ `Sweep, Turn, Wave, Zoom02, Zoom10` builds, and so does `shot1 … shot12`: a
904
+ numbered series that crosses 9 β†’ 10 is keyed **1, 2, … 9, 10, 11, 12**, which is
905
+ what the editor does with it β€” and is not what a codepoint sort does.
906
+
907
+ βœ… **This rule was a quantifier over comparators until #728, and that is what
908
+ changed.** rigc keyed `animations` codepoint-ascending until
909
+ [#543](https://github.com/firejune/rigc/issues/543) and then emitted a member of
910
+ the "natural, case-insensitive" family, refusing every pair the family could
911
+ disagree about. Both refusals were sound and both over-refused by construction,
912
+ because a quantifier stands in for a measurement: a name with a capital, an
913
+ accent, a space or a folder in it was refused rather than emitted in the order the
914
+ editor returns. Measuring the comparator moves no byte on any set the old rule
915
+ accepted; it stops refusing the ones it did.
916
+
917
+ **R11 β€” The `skins` array is written with `default` first and the rest in exactly
918
+ the order R10 describes.** A skin is a **name** in the JSON half of the format and
919
+ an **ordinal** in the binary half β€” `skins[readInt()]` for an attachment timeline,
920
+ `skins[skinIndex]` for a linked mesh β€” so an editor that writes the array in
921
+ another order repoints every such reference, silently, in a file that still
922
+ parses. Measured: a rig built `default, zulu, mike, alpha` exported
923
+ `default, alpha, mike, zulu`
876
924
  ([#541](https://github.com/firejune/rigc/issues/541)).
877
925
 
878
- ⚠️ **The refusal here is wider than R10's, and deliberately.** R10 can be narrow
879
- because #539 measured two animation-name pairs and thereby *refuted* a codepoint
880
- sort. The skins measurement refutes nothing β€” `alpha, mike, zulu` is the answer
881
- codepoint, folding and natural order all give β€” so the editor's skin comparator is
882
- **not established**, and rigc refuses any pair those candidates could disagree
883
- about. In practice that is R10's four rows plus two more: a pair a case fold
884
- reverses (`Zulu` against `mike`) and two digit runs of unequal width (`mike10`
885
- against `mike2`). Both of those *build* as animation names and are refused as skin
886
- names, and the two refusals say which is which.
926
+ ⚠️ **It is one comparator, and that is measured too.** Two of the five round
927
+ trips carried one name list as **both** collections and both came back in one
928
+ order, which is what retired the wider skin refusal #541 shipped: until #728,
929
+ `Zulu` beside `mike` and `mike10` beside `mike2` built as animation names and were
930
+ refused as skin names. Both build now. `default` is pinned rather than sorted, on
931
+ names the same comparator puts ahead of it β€” `default` before `2`, `default`
932
+ before `A` β€” so renaming a skin away from `default` makes it an ordinary name that
933
+ sorts like one.
887
934
 
888
935
  **R12 β€” A placeholder that more than one skin fills gets a per-skin attachment
889
936
  `name`, and the `default` skin may not be one of those skins.** rigc writes
@@ -2045,8 +2092,24 @@ is dead data in silence:
2045
2092
  runs, under any skin there is.
2046
2093
 
2047
2094
  rigc refuses both halves by name, and `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` checks
2048
- the artifact for them. A bone or constraint belongs to **one** skin; two skins
2049
- naming the same one is also refused.
2095
+ the artifact for them.
2096
+
2097
+ ⭐ **A bone or a constraint may be listed by more than one skin**, and each list
2098
+ is emitted as written: two mutually exclusive variants of one body region both
2099
+ activating the bone they switch on is the ordinary case. At runtime it is active
2100
+ while **the skin being worn** lists it β€” `Skeleton.updateCache` walks that skin's
2101
+ `bones` and turns each one on, along with its whole ancestor chain β€” so a bone two
2102
+ skins name poses under either of them and under neither of the rest. A consumer
2103
+ that combines the two with `Skin.addSkin` gets it once; the runtime deduplicates
2104
+ by object identity.
2105
+
2106
+ ⚠️ **The worn skin, and only the worn skin β€” including `default`.** The
2107
+ default-skin fallback is about *art*: `getAttachment` looks in the current skin
2108
+ and then in `SkeletonData.defaultSkin`, and `updateCache` does no such thing. So a
2109
+ `skin: true` bone that only the `default` skin lists is **inactive under every
2110
+ other skin** and inactive with no skin set. Listing it there to mean "always on"
2111
+ gets the opposite; leave the flag off instead, which is what "active under every
2112
+ skin" is spelled as.
2050
2113
 
2051
2114
  ⚠️ **The two spellings are told apart by these seven keys** β€” `attachments`,
2052
2115
  `bones`, `ik`, `transform`, `path`, `physics`, `slider` β€” so a skin that uses any of
@@ -2377,9 +2440,11 @@ re-rendered mean absolute error from 10.4655 / 8.4961 / 8.7140 down to
2377
2440
  identically, which is what rigc emitted when that trip was measured.)
2378
2441
 
2379
2442
  ⚠️ The editor's comparator is natural and case-insensitive
2380
- ([#539](https://github.com/firejune/rigc/issues/539)), and four of its choices are
2381
- unmeasured β€” so the emit is that family's order for names none of the four
2382
- decides, and the rest are a compile error. **R10** has the four shapes to avoid.
2443
+ ([#539](https://github.com/firejune/rigc/issues/539)) and is now measured in full
2444
+ off five stored round trips
2445
+ ([#728](https://github.com/firejune/rigc/issues/728)) β€” so the emit is the
2446
+ editor's own order, and only what those files leave open is a compile error.
2447
+ **R10** has the rule and the three shapes to avoid.
2383
2448
 
2384
2449
  βœ… **What that repair does not reach is a compile error now, not a hazard.** This
2385
2450
  paragraph used to say that names a codepoint sort and a friendlier one disagree
@@ -2391,7 +2456,8 @@ that to naming discipline β€” and since
2391
2456
  [#543](https://github.com/firejune/rigc/issues/543) it does better than refusing
2392
2457
  those two, because they are the two sets the editor's answer is **known** for:
2393
2458
  both are emitted in the order it returned. What is still a compile error is the
2394
- set whose order turns on one of the four unmeasured choices, printed with both
2459
+ set whose order turns on one of the three things the round trips of
2460
+ [#728](https://github.com/firejune/rigc/issues/728) leave open, printed with both
2395
2461
  names, which of them decides it, and the rename that settles it. What changed is
2396
2462
  the price of forgetting: a build that stops, rather than a slider that silently
2397
2463
  applies the wrong animation.
@@ -3142,8 +3208,8 @@ a delta from the constraint's own setting.
3142
3208
  key states a mass and the pose holds `1 / mass`, so a `mass` key of `0` is an
3143
3209
  infinite inverse mass β€” the constraint stops moving.
3144
3210
  - **Four of the seven are bounded, and a key outside its bound is a compile
3145
- error** ([#610](https://github.com/firejune/rigc/issues/610)). `mass` and
3146
- `strength` must be `> 0`, `damping` must be strictly inside `(0, 1)`, and `mix`
3211
+ error** ([#610](https://github.com/firejune/rigc/issues/610)). `mass` must be
3212
+ `> 0`, `damping` must be strictly inside `(0, 1)`, and `mix` and `strength`
3147
3213
  must be `0` or more. `A23_PHYSICS_CONSTRAINT_EFFECTIVE` applies the same four to
3148
3214
  a file rigc did not write, naming the animation, the constraint, the key time
3149
3215
  and the value β€” so the compiler is where a spec you wrote is refused, and the
@@ -3153,11 +3219,39 @@ a delta from the constraint's own setting.
3153
3219
  `PhysicsConstraintPose` documents `mix` as "a percentage (0+)" β€” so a negative
3154
3220
  wind is the other direction and a `mix` of `1.5` is an over-mix. Both are real
3155
3221
  and both compile.
3156
- - ⚠️ **A `mix` key of exactly `0` is legal where a setup `mix` of `0` is not**, and
3157
- the difference is not an inconsistency. `PhysicsConstraint.update` opens with
3158
- `if (mix === 0) return;`, so muting a constraint for a stretch of an animation is
3159
- what a mix timeline is for; a constraint muted *at rest* does nothing at all
3160
- unless some animation rescues it, which is the silence `A23` was built for.
3222
+ - ⚠️ **A `mix` or `strength` key of exactly `0` is legal where a setup value of
3223
+ `0` is not**, and the difference is not an inconsistency: a setup pose says what
3224
+ the constraint IS and a key says what it is doing for a stretch. For `mix`,
3225
+ `PhysicsConstraint.update` opens with `if (mix === 0) return;`, so muting a
3226
+ constraint for part of an animation is what a mix timeline is for. For
3227
+ `strength`, 0 takes the restoring term out of the velocity update and leaves
3228
+ `damping` and `inertia` applied β€” the offset is not pulled back *while the key
3229
+ holds*, which is "physics released" for that span, and the next key pulls it
3230
+ back ([#727](https://github.com/firejune/rigc/issues/727)). At rest the two
3231
+ part ways: a `strength` of 0 is a constraint nothing pulls back, which `A23`
3232
+ refuses, and a `mix` of 0 is a constraint muted until an animation keys it
3233
+ above 0 β€” the next bullet.
3234
+ - ⚠️ **A setup `mix` of `0` is legal when some animation keys it above 0** ([#743](https://github.com/firejune/rigc/issues/743)).
3235
+ A constraint muted *at rest* is a rig whose physics is off until an animation
3236
+ switches it on, which is a design rather than the silence `A23` was built for.
3237
+ What `A23` refuses is the constraint nothing rescues: muted at setup and keyed
3238
+ **nowhere**, or keyed **to 0 only**. [measured] the three are not a matter of
3239
+ taste β€” muted-and-keyed-to-1 poses its bone exactly where the same rig resting
3240
+ at 1 does, and keyed-to-0 poses it exactly where one with no timeline at all
3241
+ does. The unnamed global timeline counts as a key for every constraint whose
3242
+ own `mixGlobal` is set (Β§3.5), and so does an animation only a slider applies.
3243
+ - πŸ“ **What a `strength` key of `0` costs, measured through spine-core** on the
3244
+ generated overlay fixture, stepping at 60 fps from `Physics.reset` (#727). With
3245
+ no wind or gravity the offset coasts to a limit rather than running away β€” a
3246
+ 0.5 s release and a 2.0 s release end **0.95 %** apart β€” and the restoring key
3247
+ takes it from 5.5063 back under 0.01 in **54 steps**. With `gravity -40` pulling,
3248
+ the offset travels at terminal velocity for as long as the key holds (178 units
3249
+ over 0.5 s, 843 over 2.0 s) and the restoring key still returns it to the
3250
+ never-released run's own equilibrium, **39.999969 against 40.000000**, in 13
3251
+ steps. No NaN in any of it. ⚠️ The neighbour that does NOT come back is the
3252
+ reason `mass` keeps the narrow bound on a key: a keyed `mass` of `0` is NaN from
3253
+ the first sub-step and **still NaN after the restoring key**, and a keyed
3254
+ `damping` of `2` was still 6.9e4 two seconds later.
3161
3255
 
3162
3256
  On a track that names a `group`, one key's `v` may instead be a **map keyed by
3163
3257
  member name**, whose entries are each exactly the `v` above β€” or a `derive`
@@ -3478,7 +3572,15 @@ mass?, wind?, gravity?, mix?, fps?, limit? }`. These are emitted into the 4.3
3478
3572
  be **keyed over time** as `tracks` entries naming this constraint (Β§4.4); this
3479
3573
  table is the value at rest, and a timeline overrides it while it plays. `mass: 0` becomes an infinite inverse mass and `damping β‰₯ 1`
3480
3574
  never settles β€” both are `A23`, here and on every timeline key that states them
3481
- ([#610](https://github.com/firejune/rigc/issues/610)). Every field but `bone` and `note` must be a finite
3575
+ ([#610](https://github.com/firejune/rigc/issues/610)). ⚠️ `strength: 0` is `A23` **here and not on a key**:
3576
+ at rest it is a constraint nothing pulls back, and on a key it is a release somebody
3577
+ asked for, which Β§4.4 states with the measurement behind it
3578
+ ([#727](https://github.com/firejune/rigc/issues/727)). `mix: 0` is the one value
3579
+ here an animation can answer for: it rests the constraint **muted**, which is
3580
+ legal, and `A23` names it only when no timeline in any animation keys that `mix`
3581
+ above 0 β€” on a key it is the mute for a span (Β§4.4, [#743](https://github.com/firejune/rigc/issues/743)). None of the
3582
+ three is a compile error β€” a setup value this table states reaches the gate, where the whole
3583
+ file can be read; it is a **key** outside its bound that `build` refuses (Β§4.4). Every field but `bone` and `note` must be a finite
3482
3584
  number: a non-number is rounded to `NaN` and emitted as `null`, which the runtime
3483
3585
  reads as **zero**, so `"mass": "heavy"` used to ship a constraint that never
3484
3586
  settles with no word from anybody (#307).
@@ -4588,7 +4690,11 @@ so `v` is three numbers and not one.
4588
4690
  ⚠️ **A muted constraint with no timeline is a finding, not an idiom.** Turning a
4589
4691
  constraint on from an animation is the idiom (Β§4.10), so `A36`/`A37` only object to
4590
4692
  all-zero mixes when **no** animation keys that constraint's `mix`. If you mute one
4591
- at setup, key it somewhere.
4693
+ at setup, key it somewhere. `A23` asks the same question of a physics constraint
4694
+ ([#743](https://github.com/firejune/rigc/issues/743)) and asks it more sharply:
4695
+ it reads the key **values**, so a `mix` timeline keying 0 only is not a rescue,
4696
+ and it counts the unnamed global timeline for every constraint declaring
4697
+ `mixGlobal`. `A36`/`A37` take any non-empty key array.
4592
4698
 
4593
4699
  ---
4594
4700
 
@@ -4737,7 +4843,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4737
4843
  | `two bones are called "X"` | bone names are the join key; rename one |
4738
4844
  | `two ik constraints are called "X" β€” a constraint resolves by name AND type (\`SkeletonData.findConstraint\`), so names are unique PER KIND: an ik and a transform constraint may share one, two of a kind may not` | Β§3.5 β€” rename one of the two. The kind in the sentence is the pair's own, so `two transform constraints are called "X"` is the same refusal on another kind; a name shared **across** kinds is not this error and never was one to fix |
4739
4845
  | `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | Β§4.6 β€” the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
4740
- | `skin "S" activates ik constraint "X", which skin "T" already activates; a constraint belongs to one skin` | Β§3.4.1 β€” a constraint runs under one skin or under all of them. The kind is in the sentence because `ik` "X" and `transform` "X" are two constraints, and each may belong to a different skin |
4846
+ | `skin "S" lists "X" under "transform", but the rig declares it as a "ik" constraint β€” a skin looks its constraints up by name AND type, so this one is a miss and the loader throws` | Β§3.4.1 β€” move the name to the list named after the constraint's own kind. A name the rig declares under **no** kind is the other miss and says so (`skin "S" activates ik constraint "X", which this rig does not declare`). ⚠️ A name listed by **several skins** is not an error and no longer was one as of [#725](https://github.com/firejune/rigc/issues/725): the lists are per-skin sets and a bone or constraint in two of them is active under either |
4741
4847
  | `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
4742
4848
  | `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 β€” pick one file and declare it there. A slot **nothing** fills is exempt: its setup pose can only be "show nothing" and is not asked for |
4743
4849
  | `the setup pose shows attachment "A" on slot "X", which no skin and no manifest part fills` | Β§3.3 β€” the slot is emitted empty and nothing was ever going to fill it, so `A` resolves to nothing. Give the slot an attachment (a skin entry or a manifest part), or state the setup pose as `null` |
@@ -4772,7 +4878,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4772
4878
  | `animation "A" slot "X" attachment: attachment "N" is not in slot "X" under any skin (searched: default, alt) β€” the slot has: plain, trim` | Β§4.4 β€” the keyed name is in **no** skin, and the two clauses say where the compiler looked and what it would have taken. Fix the spelling, or give some skin a placeholder called `N`. A name only a NAMED skin fills is not this error and never was one to fix β€” it compiles, and the slot shows nothing under the skins that lack it. Before [#695](https://github.com/firejune/rigc/issues/695) the message read `attachment "N" is not in slot "X"` and was raised against the **default skin alone**, so it fired on correct rigs: any key into named-skin art, and every key in a rig with no default skin. `the slot has no attachments at all` is the same message where nothing fills the slot |
4773
4879
  | `animation "A" keys unknown bone "X"` | the track's `bone` is not in the rig |
4774
4880
  | `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (Β§4.4) |
4775
- | `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)) |
4881
+ | `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` is `> 0`, `damping` is inside `(0, 1)`, `mix` and `strength` are `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)). ⚠️ Those are the bounds a **key** is held to. A setup `strength` of `0` is refused too, but by `A23` rather than here, and with its own sentence β€” `physics "C" has strength 0; nothing pulls it back` ([#727](https://github.com/firejune/rigc/issues/727)) |
4776
4882
  | `a key carries both a named easing and a raw curve; pick one` | R6 |
4777
4883
  | `last key carries an easing but has nothing to ease to` | drop `ease`/`curve` from the final key |
4778
4884
  | `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
@@ -4837,8 +4943,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
4837
4943
  | `animation "A" group "G" has no timeline "P" (a bone group has: translate, translatex, translatey, scale, scalex, scaley, shear, shearx, sheary, rotate; a slot group has: attachment, rgba, rgba2; a physics constraint group has: inertia, strength, damping, mass, wind, gravity, mix, reset)` | Β§4.3, Β§4.4 β€” a group's family is decided by the property, and `P` is in none of the three tables, so there is no family to resolve the members as. Fix the spelling and the group becomes whichever family the property names. The group is refused before its members are looked up, so a member the rig does not declare is a **later** message; an unknown group NAME is an earlier one. Before [#661](https://github.com/firejune/rigc/issues/661) a group of bones read `animation "A" targets unknown slot "M"` and a group of slots got the slot row below, naming one family out of three |
4838
4944
  | `animation "A" slot "X" has no timeline "P" (it has: attachment, rgba, rgba2)` | Β§4.4 β€” a slot has exactly three timelines and `P` is none of them. Fix the spelling; a bone or constraint property written on a slot track is refused by its own row instead. Before [#650](https://github.com/firejune/rigc/issues/650) every other name compiled as an **rgba** timeline called `P`, and what you saw was `A00_ROUNDTRIP_PARSE` on the emitted file β€” or, for the one-channel spelling, `rgba value needs 4 channels, got 1` |
4839
4945
  | `animation "A" slot "X" rgba2: slot "X" declares no setup "dark", and an "rgba2" timeline poses a slot's dark colour …` | Β§3.3, Β§4.4 β€” the two-colour tint has a setup half and a keyed half, and the keyed half cannot exist without the other. `Slot`'s constructor allocates a dark colour only for a slot whose setup pose declares one, and `RGBA2Timeline` writes it unconditionally β€” so without the `dark` the file loads, and the first `state.apply` throws `TypeError: null is not an object` in the consumer's process. Give the slot the `dark` it holds at rest, or key `rgba` if only the light colour moves. Raised before the keys are read, with the slot named, for the same reason the row above is |
4840
- | `N pair(s) of animation names have no one order: … "turn" / "Turn" (case) β€” they are one name in two cases, and which of them the editor puts first is not measured; rename one of them so they differ by more than letter case` | **R10** β€” rename until no pair is left. The kind in brackets says which of the editor comparator's four UNMEASURED choices decides the pair: `case` (a pure case tie), `number` (one number written two ways, or a run of digits against a word) or `separator` (make the first character that differs a letter or a digit). rigc keys `animations` in the editor's own comparator β€” natural and case-insensitive ([#539](https://github.com/firejune/rigc/issues/539), [#543](https://github.com/firejune/rigc/issues/543)) β€” so a pair that comparator settles is emitted rather than refused, and only the four choices nobody has measured are a compile error; on those, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
4841
- | `N pair(s) of skin names have no one order: … "Zulu" / "mike" (case) β€” folded to one case "Zulu" and "mike" order the other way round, so whether the editor folds SKIN names decides this pair` | **R11** β€” rename until no pair is left. The same shape as the row above with a **wider** family: #539 measured the editor's comparator for animation names and thereby ruled codepoint out, and nothing has ruled anything out for skin names, so a pair the candidates could disagree about is refused even where the animation rule would emit it. `Zulu`/`mike` and `mike10`/`mike2` build as animation names and are refused as skin names ([#541](https://github.com/firejune/rigc/issues/541)) |
4946
+ | `N pair(s) of animation names have no one order: … "Fx/a" / "fx/b" (folder) β€” "Fx/a" and "fx/b" sit in the sibling folders "Fx" and "fx", which the comparator leaves in one place …; rename one of the two folders so they differ by more than letter case, spacing or a leading zero` | **R10** β€” rename until no pair is left. The kind in brackets says which of the three things the five stored round trips leave open decides the pair: `number` (two digit runs that are each one number written twice, pointing opposite ways), `separator` (a whitespace character that is not a space) or `folder` (two sibling folders the comparator cannot separate). rigc keys `animations` in the editor's own comparator, read off `fixtures/editor-order/probe{1..5}.{in,out}.json` ([#728](https://github.com/firejune/rigc/issues/728)) β€” so a pair those files settle is emitted rather than refused, **including a pair that differs only in case**, whose order is then the one your spec declared. On the three that are left, the editor's re-key repoints every slider whose animation moves index ([#535](https://github.com/firejune/rigc/issues/535)) |
4947
+ | `N pair(s) of skin names have no one order: … "Fx/a" / "fx/b" (folder) β€” …` | **R11** β€” rename until no pair is left. The same shape and the same three kinds as the row above, because it is the same comparator: two of the five round trips carried one name list as both collections and both came back in one order ([#728](https://github.com/firejune/rigc/issues/728)). ⚠️ This row was **wider** than R10's until then β€” `Zulu`/`mike` and `mike10`/`mike2` built as animation names and were refused as skin names ([#541](https://github.com/firejune/rigc/issues/541)) β€” and both build now |
4842
4948
  | `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that … Move the default skin's entry for this slot into a named skin β€” call it "base"` | **R12** β€” do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and Β§3.4.2 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
4843
4949
  | `N attachment name collision(s): a placeholder that more than one skin fills is emitted with the name "<skin>/<placeholder>" … slot "patch": skin "base" placeholder "zulu/patch" and skin "zulu" placeholder "patch" would both be named "zulu/patch"` | **R12** β€” rename the placeholder or the skin. rigc composes an attachment name for every placeholder more than one skin fills (Β§3.4.2), and this fires when a composed name is one another entry in the same slot already answers to β€” including a plain name in the default skin, which composed nothing. Both sites are named; either rename ends it |
4844
4950
 
@@ -4937,7 +5043,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
4937
5043
  | `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)) |
4938
5044
  | `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)) |
4939
5045
  | `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 |
4940
- | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both β—‘ | the atlas `size:` disagrees with the PNG on disk, **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 ([#694](https://github.com/firejune/rigc/issues/694)): a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (Β§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels β€” a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling** ([#266](https://github.com/firejune/rigc/issues/266)), and what that message names is the pair that shares texels. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
5046
+ | `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 ([#715](https://github.com/firejune/rigc/issues/715)): 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 ([#694](https://github.com/firejune/rigc/issues/694)): a rectangle outside its page makes `u2 > 1` and samples whatever the wrap mode returns, and `--atlas-in` already refuses the same rectangle at compile time (Β§0.2). Under `spine-html` also: `pma`, rotation, and two regions on one page over the same texels β€” a packed page must be **one part covering it exactly** (the unpacked convention) or a **tiling** ([#266](https://github.com/firejune/rigc/issues/266)), and what that message names is the pair that shares texels. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
4941
5047
  | `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` |
4942
5048
  | `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)) |
4943
5049
  | `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 |
@@ -4950,11 +5056,11 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
4950
5056
  | `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`) |
4951
5057
  | `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)) |
4952
5058
  | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug β€” report it |
4953
- | `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)). ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page** ([#705](https://github.com/firejune/rigc/issues/705)): 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. 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. Before #705 the walk was silent about its own reach, so a part nobody could read printed *opaque in every one of its 12x8 texels* over zero of them, which is a refusal pointing at the wrong file: the art it names may be transparent, and the repair is the rectangle in `A06`'s row above **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
5059
+ | `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)). ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page** ([#705](https://github.com/firejune/rigc/issues/705)): 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** ([#715](https://github.com/firejune/rigc/issues/715)), and #705's clause 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 the scan came 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 **disappeared**, the scan having found 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. Before #705 the walk was silent about its own reach, so a part nobody could read printed *opaque in every one of its 12x8 texels* over zero of them, which is a refusal pointing at the wrong file: the art it names may be transparent, and the repair is the rectangle in `A06`'s row above **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
4954
5060
  | `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, 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 ([#684](https://github.com/firejune/rigc/issues/684)). Fix the rig spec's `controls`/`chain`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
4955
5061
  | `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 |
4956
5062
  | `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)) |
4957
- | `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 |
5063
+ | `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: 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), [#743](https://github.com/firejune/rigc/issues/743)). 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 ([#727](https://github.com/firejune/rigc/issues/727)). 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; it is muted β€” rest it above 0, or key its mix above 0 in an animation`. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, and takes an animation a slider applies like any other. `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 |
4958
5064
  | `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)) |
4959
5065
  | `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
4960
5066
  | `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 |
@@ -6722,9 +6828,9 @@ one key, deform blocks counted at each of their three levels;
6722
6828
  β‡’ in rigc: only `animations` is emitted sorted (R10), because it is the one
6723
6829
  object measured here whose ORDER is also an index space β€” every reference into
6724
6830
  the re-sorted *other* objects is by name on both sides, so nothing moves when
6725
- they are re-keyed. rigc emits **that comparator's own order** and refuses the name
6726
- sets on which its leading-zero, case-tie, digit-against-word or separator
6727
- behaviour β€” the four choices still unmeasured β€” would decide a pair. Sorting the
6831
+ they are re-keyed. rigc emits **that comparator's own order**, which
6832
+ [#728](https://github.com/firejune/rigc/issues/728) then measured in full off five
6833
+ stored round trips (R10) rather than quantifying over a family. Sorting the
6728
6834
  105 collections that way reproduces **105 of 105**, the three codepoint cannot
6729
6835
  included, and refuses none of them; the codepoint rule that stood until
6730
6836
  [#543](https://github.com/firejune/rigc/issues/543) reproduced 102 and refused
@@ -6747,10 +6853,13 @@ does β€” `skins` carries ordinals in the binary half, `skins[readInt()]` for an
6747
6853
  attachment timeline and `skins[skinIndex]` for a linked mesh β€” so this is the
6748
6854
  `animations` defect (#535) in the collection nobody had checked. β‡’ in rigc: R11.
6749
6855
 
6750
- ⚠️ **What that measurement does *not* settle is which comparator.** `alpha, mike,
6751
- zulu` is the order codepoint, case-folding and natural order all produce, so unlike
6752
- the animations case nothing here refutes anything, and rigc refuses any skin-name
6753
- pair the candidates could disagree about (R11).
6856
+ βœ… **Which comparator it is was the open half of that, and #728 closed it.**
6857
+ `alpha, mike, zulu` is the order codepoint, case-folding and natural order all
6858
+ produce, so #541's rig refuted nothing and rigc refused any skin-name pair the
6859
+ candidates could disagree about. Two of the five round trips carried one name list
6860
+ as **both** collections and both came back in one order, so `skins` and
6861
+ `animations` share one comparator β€” measured, not inferred β€” and the wider skin
6862
+ refusal is gone (R11).
6754
6863
 
6755
6864
  βœ… **The two readings this replaces.** A pull request once called `skins` *measured
6756
6865
  preserved*, on the strength of a one-skin rig where a one-element array comes back
package/docs/INGEST.md CHANGED
@@ -594,7 +594,7 @@ is the one failure a comparison of two sets cannot show you.
594
594
  | `ANIMATION_GROUP` | `BLOCK` | 1 | the animation carries a group the motion spec has no home for. The detail names the ten it does carry. `drawOrderFolder` is the group to know about: the runtime reads it and builds a timeline from it, and no export in this corpus carries one | transcribe that group by hand (Β§2), or accept that the rebuild does not carry it |
595
595
  | `ATTACHMENT_<TYPE>` | `BLOCK` | 1 | an attachment of a type rigc does not emit; the code is composed from the type, so on the one type left it reads `ATTACHMENT_POINT`. rigc emits region, mesh, linkedmesh, boundingbox, clipping and path β€” `linkedmesh` since [#691](https://github.com/firejune/rigc/issues/691), and `point` is the remaining deferred type | the rebuild will not have that attachment at all. `docs/SPEC_COVERAGE.md` part 1-6 says what a deferred type would carry |
596
596
  | `ATTACHMENT_LINK_GEOMETRY` | `LOSS` | 0 | a **linked mesh** that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by nothing at all and the attachment draws the geometry its `source` names; the rig spec has no home for them either, because `build` refuses geometry on a link by name. The detail lists the keys and the source. Until [#710](https://github.com/firejune/rigc/issues/710) the rebuild dropped them with no line at all, so an `ingest` that normalised somebody's file said nothing about it | nothing. The rebuild is the mesh the runtime was already drawing β€” and if those keys were the geometry you meant, take `source` off and author it as a mesh of its own. `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` is the same fact at the gate |
597
- | `ATTACHMENT_NAME` | `LOSS` | 0 | the attachment states a `name` and only **one** skin fills the placeholder, so rigc writes none β€” it composes `<skin>/<placeholder>` exactly where a placeholder is contested | nothing, unless something downstream looks that attachment up by the name the source gave it |
597
+ | `ATTACHMENT_NAME` | `LOSS` | 0 | the attachment states a `name` and only **one** skin fills the placeholder, so rigc writes none β€” it composes `<skin>/<placeholder>` exactly where a placeholder is contested. 🚨 **The name is also the ATLAS REGION KEY**, and the detail says what became of it: `readAttachment` reads `name = getValue(map, "name", placeholder)` and then `path = getValue(map, "path", name)`, so `path` defaults to the **name** and not to the placeholder. A name that differs from the placeholder is therefore **kept as `path`** on an attachment that resolves a region β€” region, mesh, linked mesh β€” and the detail names the region. The three shapes that keep nothing say which they are: the source stated its own `path` (carried unchanged), the name **is** the placeholder (same region either way), or the type resolves no region at all (`boundingbox`, `clipping`, `path`). Until [#742](https://github.com/firejune/rigc/issues/742) nothing was written, so the rebuild asked the atlas for the placeholder and `A08_REGION_NAMES_MATCH_ATTACHMENTS` refused it | nothing about the art, which is carried. The **name** is what is gone, so this matters where something downstream looks that attachment up by the name the source gave it |
598
598
  | `ATTACHMENT_SEQUENCE` | `BLOCK` | 1 | the attachment carries a `sequence` block β€” a numbered image series β€” which the rig spec cannot say | the rebuild draws the single region the attachment names; the frames have to be driven some other way |
599
599
  | `ATTACHMENT_TIMELINE` | `BLOCK` | 1 | an attachment timeline other than `deform`, which is the only one the motion spec carries | transcribe it, or accept that the rebuild does not play it |
600
600
  | `BONE_FIELD` | `BLOCK` | 1 | a bone field with no rig-spec field, so it is dropped. A 4.0/4.1 export spelling `transform` where 4.3 spells `inherit` lands here; so does a misspelling | check the name against AUTHORING Β§3 first β€” a typo and an unsupported field read exactly the same |
@@ -621,6 +621,18 @@ is the one failure a comparison of two sets cannot show you.
621
621
  | `TIMELINE_KEY_RESTATED` | `LOSS` | 0 | **the commonest line in a real run.** An editor omits a channel that equals the parser's default; the motion spec's `v` is positional, so the omission is written out at that default | nothing. The same values the runtime reads, spelled out β€” a larger file and the same animation |
622
622
  | `TRANSFORM_KEY_FIELD` | `BLOCK` | 1 | as `IK_KEY_FIELD`, on a `transform` timeline | as `IK_KEY_FIELD` |
623
623
 
624
+ ⚠️ **One thing the table cannot carry: the region key is kept where a placeholder is
625
+ CONTESTED too, and there is no finding there at all.** `ATTACHMENT_NAME` is silent
626
+ exactly where the name is re-derived β€” rigc composes `<skin>/<placeholder>` for a
627
+ contested placeholder, so nothing is lost about the name β€” and until
628
+ [#742](https://github.com/firejune/rigc/issues/742) the region went with it anyway:
629
+ `compile` pins a composed name's `path` at the **placeholder**, so two skins naming two
630
+ regions rebuilt onto **one**, neither of them the art the source drew, with no line
631
+ printed. `ingest` now writes the source's region as `path` on both, and `IG53` in
632
+ `bun run selftest` is what holds it β€” the clause it measures is the count of *distinct*
633
+ regions, because every row can name a region the pack has and still be one region doing
634
+ the work of two.
635
+
624
636
  ### Transcription β€” the route that made a foreign skeleton yours
625
637
 
626
638
  ⚠️ **The rest of Β§2 is the route that existed before #569, and it is kept because
@@ -732,7 +732,7 @@ This is the split Part 4(c) needs. **Spine-validity** = the file is wrong for an
732
732
  | `A16_SKELETON_VERSION_4_3` | validity (portability) | a `spine` string outside `4.3.x` |
733
733
  | `A17_ATLAS_PAGE_FILES_EXIST` | validity | a page PNG not on disk |
734
734
  | `A18_DETERMINISTIC_EMIT` | tool contract | recompiling differs byte-for-byte |
735
- | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | **mixed** | size≠PNG is **validity** (case 6h), and so is a region whose rectangle is outside the page it names, rotation honoured ([#694](https://github.com/firejune/rigc/issues/694)); `pma:true`, region rotation, and two regions on one page over the same texels are **renderer-profile** |
735
+ | `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | **mixed** | size≠PNG is **validity** (case 6h) — measured rather than inherited ([#715](https://github.com/firejune/rigc/issues/715)): the runtime's UVs are a fraction of the DECLARED size, so a rescaled page draws, and the clause stays validity because every texel reader breaks on it (two of them rigc's own, under both profiles) and the format can state the same art truthfully with `scale:`, which the message prints. And so is a region whose rectangle is outside the page it names, rotation honoured ([#694](https://github.com/firejune/rigc/issues/694)); `pma:true`, region rotation, and two regions on one page over the same texels are **renderer-profile** |
736
736
  | `A11_NO_CLIPPING_ATTACHMENTS` | **renderer-profile** | clipping attachments β€” "the renderer skips them silently" |
737
737
  | `A12_NO_DARK_COLOR` | **renderer-profile** | slot `dark`, `rgba2`/`rgb2` timelines β€” "parsed, then ignored". ⚠️ rigc **emits** the first two; a renderer that drops a construct is what a profile is for, not a reason not to emit it |
738
738
  | `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | validity | a slot `dark` the parser drops or reads as NaN, an `rgba2` timeline on a slot with no dark colour to pose, or a key whose posed light/dark is not what it states |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-rigc",
3
- "version": "0.28.0",
3
+ "version": "0.29.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": {