spine-rigc 0.34.0 → 0.35.1

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
@@ -328,9 +328,12 @@ when drawing through the pack's own texels is the point.
328
328
  ([#762](https://github.com/firejune/rigc/issues/762)). A depth sheet or a soft
329
329
  mask is read in the drawing's pixels on a `scale:` page as on loose parts — each
330
330
  vertex's texel position over the stated scale — and a mesh fit's overshoot is
331
- printed in them, with the texel it was measured on named beside it. What stays
332
- in texels is what is taken off them: a `contour`'s trace, whose `margin` and
333
- `tolerance` are applied on the texels there are (below).
331
+ printed in them, with the texel it was measured on named beside it. So are a
332
+ `contour`'s `margin` and `tolerance`
333
+ ([#779](https://github.com/firejune/rigc/issues/779)): the trace runs on the
334
+ texels there are, and the two distances are applied on them as `value × scale`
335
+ texels — the scale the header states, never a measured one (below). What stays
336
+ in texels is only what is counted off them: a contour's hole count.
334
337
 
335
338
  🚨 **A page that declares a size it does not have is a different thing, and it is
336
339
  refused** ([#715](https://github.com/firejune/rigc/issues/715)). The common shape
@@ -396,10 +399,8 @@ compile error carrying `A06`'s whole sentence — ratio and repair — on `expla
396
399
  and on `build` alike, where on `build` it arrives before the gate would have said
397
400
  it. Carry out the repair and every figure comes back. ⚠️ They come back measured
398
401
  on the **coarser** texels the page really has, so a figure that depends on the
399
- grid need not equal the one the pack the page was halved from reads: on the same
400
- fixture the `scale: 0.5` restatement traces the contour as 11 vertices where the
401
- full-resolution page traced 15, because a trace runs on the texels there are and
402
- its `margin` and `tolerance` are applied on them.
402
+ grid need not equal the one the pack the page was halved from reads — and a
403
+ contour's outline is one of them, below.
403
404
 
404
405
  📐 **A fit's overshoot is stated in the drawing's pixels on every page**
405
406
  ([#762](https://github.com/firejune/rigc/issues/762)). It is a distance, and on a
@@ -417,12 +418,44 @@ the drawing, so that is the step the figure moves in:
417
418
  ```
418
419
 
419
420
  All three pages read the fan at 16.00px, because its rim lands on whole texels of
420
- each. A figure that does not is exact only to that step — the contour above reads
421
- 6.00px on the `scale: 0.5` page (3.00 texels) against the full page's 3.16px, a
422
- different outline measured on a coarser grid, and the line's clause is what says
423
- so. On a loose part and a page at scale 1 the texels are the drawing, and the line
424
- is the one it always was. A `contour`'s hole count is a count of those cells and
425
- says `texel(s)` on such a page.
421
+ each. A figure that does not is exact only to that step — a contour at
422
+ `tolerance: 3`, `margin: 4` reads 6.00px on the `scale: 0.5` page (3.00 texels)
423
+ against the full page's 5.39px, a different outline measured on a coarser grid,
424
+ and the line's clause is what says so. On a loose part and a page at scale 1 the
425
+ texels are the drawing, and the line is the one it always was. A `contour`'s hole
426
+ count is a count of those cells and says `texel(s)` on such a page.
427
+
428
+ 📐 **A `contour`'s `margin` and `tolerance` are the drawing's pixels on every
429
+ page** ([#779](https://github.com/firejune/rigc/issues/779)). They are your
430
+ statement, in the unit every other size in the spec is in, so on a `scale:` page
431
+ the trace applies them as `margin × scale` and `tolerance × scale` texels, and
432
+ `maxVertices` is judged on the outline that asks for. They used to be applied in
433
+ texels unconverted, which asked each page a different question. Measured on
434
+ `halvedMeshPacks`, one spec — `tolerance: 1.5`, `margin: 2`, `maxVertices: 48`:
435
+
436
+ | page | applied in texels (before) | applied as the drawing's pixels (now) |
437
+ | --- | --- | --- |
438
+ | declared size | 15 vertices | **15** vertices, the same mesh |
439
+ | `scale: 2` | refused: `simplified to 66 vertices at tolerance 1.5, past the 48` | **15** vertices, the declared outline to 0.00px |
440
+ | `scale: 0.5` | 11 vertices, 4.20px from the declared outline | refused: the outline crosses itself after a margin of 2px, applied as 1 texel |
441
+
442
+ A **finer** page resolves everything the declared one does, so it traces the
443
+ declared outline, vertex for vertex. A **coarser** one holds less: its silhouette
444
+ is a staircase of texels 2px of the drawing apart, and a tolerance under one texel
445
+ keeps the steps — which is what the declared page does too at a tolerance under
446
+ one of *its* pixels (at `tolerance: 0.75` it keeps 66 vertices, and given the
447
+ budget for them it is refused by the same crossing). So
448
+ on a coarser page, ask for a tolerance of at least a texel, or supply the loose
449
+ art; the spec above at twice its figures traces 11 vertices there against the
450
+ declared page's 10, 3.69px apart. Every refusal a trace raises on a `scale:` page
451
+ names the figure the spec states, the texels it was applied as and the scale:
452
+
453
+ ```bash
454
+ # rigc compile error: skin "default" slot "blob" attachment "blob": the outline crosses itself: edge 8 meets edge 10
455
+ # after a margin of 2px (the drawing's pixels — applied as 1 texel(s) of this page, whose scale: 0.5 makes a texel
456
+ # 2.00px of the drawing) was pushed out of a silhouette narrower than that — lower the margin, or the art has a
457
+ # neck too thin to mesh
458
+ ```
426
459
 
427
460
  🚨 **A page that is not a PNG is refused by name, before anything is compiled
428
461
  against it** ([#732](https://github.com/firejune/rigc/issues/732)). rigc reads PNG
@@ -904,11 +937,27 @@ one A09 does compare.
904
937
 
905
938
  ## 2. The rules that decide what lands in the file
906
939
 
907
- **R1 — A field is emitted exactly when you declare it.** Not "when it differs from
908
- the default". Spine's own exporter omits anything equal to a default; rigc cannot,
909
- because a rig may need to say `x: 0` out loud and because deciding emission from
910
- the *value* would make the file depend on arithmetic rather than on what you wrote.
911
- Omit a field and Spine's default stands; write it and it is in the file.
940
+ **R1 — A field is emitted when you declare it, and left out where the parser would
941
+ read the same value without it.** Omit a field and Spine's default stands; write it
942
+ and it reaches the file — unless what you wrote **is** that default, in which case
943
+ the emitter leaves it out, the way the editor's own exporter does (issue
944
+ [#716](https://github.com/firejune/rigc/issues/716)). Writing `x: 0` is still
945
+ legitimate, and your spec still says it: the spec is the record of what you wrote,
946
+ and the file is what the runtime reads. The two cannot disagree about a value,
947
+ because a key is left out only where the 4.3 parser loads the same `SkeletonData`
948
+ without it — exact equality with the float the file would hold, so `x: 1e-45` is
949
+ written — and the selftest loads every build both ways to hold that (§10.6c).
950
+
951
+ ⚠️ This rule said *"not 'when it differs from the default'"* until #716, on two
952
+ grounds: that a rig may need to say `x: 0` out loud, and that deciding emission from
953
+ the value would make the file depend on arithmetic. Neither survived being measured.
954
+ What reads the file is the 4.3 parser, which reads the absent key as the same number,
955
+ and the editor, whose own export leaves the same key out — so there is nobody to say
956
+ it out loud *to*; and the decision is an exact comparison with the parser's fallback,
957
+ which is not arithmetic in any sense a reader has to redo. What the
958
+ old rule cost was concrete: a rebuild of an editor export restated **2,338** keys the
959
+ export leaves to the parser, so `ingest → build` could never give an editor's file
960
+ back.
912
961
 
913
962
  **R2 — The compiler never invents a value.** No defaults guessed from the art, no
914
963
  re-measured plates, no reasonable fallbacks. A missing number is a `CompileError`
@@ -996,6 +1045,15 @@ page:
996
1045
  | 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 |
997
1046
  | 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` |
998
1047
 
1048
+ 🔸 **One clause of the rule is not from those files.** Before comparing, the
1049
+ comparator reads U+3000 IDEOGRAPHIC SPACE as a space and the full-width digits
1050
+ U+FF10–U+FF19 as `0`–`9`. No probe carried either; the fold was measured on a
1051
+ production skin's slot keys ([#791](https://github.com/firejune/rigc/issues/791),
1052
+ §10.6b says what was measured and why it is those two classes and not NFKC), and
1053
+ it applies here because R10, R11 and a skin's slot keys are one comparator. So
1054
+ `shot2` sorts before `shot10`, and a pair an ideographic space decides is
1055
+ ordered rather than refused as `separator`.
1056
+
999
1057
  ⭐ **A tie is not an ambiguity, and that is what retired most of this rule's
1000
1058
  refusals.** Two names the comparator cannot separate come back in the order the
1001
1059
  file gave them, so the order rigc emits for such a pair is **your own declaration
@@ -1082,6 +1140,7 @@ is recorded in `bench/runs/README.md`, *What a run may read*.)
1082
1140
  | `fps` | nonessential editor hint | `SkeletonData.fps` stays 30 |
1083
1141
  | `referenceScale` | 4.2+ physics/scale reference | parser default 100 |
1084
1142
  | `images` | where the editor's import looks for the part PNGs, as a path from the skeleton file | **written for you**: under `--copy-images` the `--out` directory itself, spelled `../<its basename>/` (a literal `./` is dropped by the editor on import; a named directory is kept and every part is found — measured on 4.3.23); otherwise the relative path from `--out` to the one directory the spec names every part PNG in (the rig's images directory, or the manifest's plates). A declared value is carried through verbatim — and overridden by `--copy-images`, which moved the parts. Parts spread over several directories have no single true path, so nothing is written (issue #370) |
1143
+ | `audio` | nonessential: where the editor looks for the skeleton's audio files, as a path from the skeleton file — a string, or `null` for none | **not written unless you state it.** rigc has no audio to point at, so this is a value a spec states or does not; stated, it is carried verbatim, `null` included, because `null` is what an editor export writes when no audio folder is set (all twelve under `examples/` do) and `ingest` carries it from there ([#716](https://github.com/firejune/rigc/issues/716)). Anything but a string or `null` is a compile error naming the value |
1085
1144
 
1086
1145
  `spine` and `hash` are not yours to write: rigc emits its own version label
1087
1146
  (`A16` re-checks it is on the 4.3 line) and inventing a hash would claim an export
@@ -1769,9 +1828,9 @@ included, because without it the generator is refused (issue #274). The
1769
1828
 
1770
1829
  | Field | Meaning |
1771
1830
  | --- | --- |
1772
- | `tolerance` | **required.** Douglas-Peucker tolerance, in part pixels. Bigger spends fewer vertices and cuts more corners |
1773
- | `margin` | how far the outline is pushed out past the traced silhouette, in pixels. Default `1` |
1774
- | `maxVertices` | refuse rather than emit more outline vertices than this. Default `64` |
1831
+ | `tolerance` | **required.** Douglas-Peucker tolerance, in the drawing's pixels — on a packed page that declares a `scale:`, applied as `tolerance × scale` of its texels (§0.2, [#779](https://github.com/firejune/rigc/issues/779)). Bigger spends fewer vertices and cuts more corners |
1832
+ | `margin` | how far the outline is pushed out past the traced silhouette, in the drawing's pixels (`margin × scale` texels on a `scale:` page). Default `1` |
1833
+ | `maxVertices` | refuse rather than emit more outline vertices than this — judged on the outline the two distances above ask for, so a finer `scale:` page of the same art needs no bigger budget. Default `64` |
1775
1834
  | `alpha` | the alpha at or above which a pixel counts as art, `1`..`255`. Default `1` — any pixel that is not fully transparent |
1776
1835
 
1777
1836
  The numbers above are invented, and the pair that matters is `tolerance` and
@@ -1821,6 +1880,7 @@ mesh rather than a rim prefix, and `A28_RIBBON_ROWS_SHARE_WEIGHTS` **SKIPs** wit
1821
1880
  | a neck narrower than `margin` | `the outline crosses itself: edge 0 meets edge 3 after a margin of 3px was pushed out of a silhouette narrower than that` |
1822
1881
  | more outline than `maxVertices` | `the silhouette simplified to 15 vertices at tolerance 1.5, past the 4 this mesh allows` — refused, never silently decimated |
1823
1882
  | a `margin` too small for the `tolerance` | `the mesh covers 91.81% of the art (2936 of 3198 px), under the 99.5% a contour mesh guarantees` |
1883
+ | any of the three above, on a packed page that declares a `scale:` | `…after a margin of 2px (the drawing's pixels — applied as 1 texel(s) of this page, whose scale: 0.5 makes a texel 2.00px of the drawing) was pushed out…` — the spec's figure, the texels the trace ran it at, and the scale that converted it; a count of the part's cells says `texels` there. A tolerance under one texel of a coarser page keeps its stair steps, as one under a pixel does on the declared page (§0.2) |
1824
1884
 
1825
1885
  That last one is the guarantee: **the emitted triangles cover at least 99.5% of
1826
1886
  the art**, measured by rasterising them back over the mask, and a build that would
@@ -2526,6 +2586,13 @@ carrying here:
2526
2586
  Every constraint may also carry `skin: true`, which makes it run only under the skin
2527
2587
  that lists it — see §3.4.1, and note that the flag alone does nothing.
2528
2588
 
2589
+ 🎛️ **An ik or transform whose mix a game sets from code has no field here**, and
2590
+ that is deliberate. Every key on a constraint object is a Spine field the emitter
2591
+ writes; a statement to the gate about who turns the dial is what the artifact cannot
2592
+ say about itself, so it lives in `invariants.consumerDrivenMix` (§3.7), beside
2593
+ `deformMayFold` — the one other exemption from a named rule, which names its subject
2594
+ the same way ([#784](https://github.com/firejune/rigc/issues/784)).
2595
+
2529
2596
  #### 3.5.1 `path` — bones that travel along a curve
2530
2597
 
2531
2598
  **When you need one:** anything that moves *along* something rather than around a
@@ -3075,7 +3142,8 @@ is not an array. Every field is optional and each is the payload a firing
3075
3142
 
3076
3143
  Optional with one exception, and only meaningful for rigc's own formations:
3077
3144
  `meshSlots` and `meshTriangles` (the two halves of the mesh budget `A13` measures
3078
- against), `axisBone`, `massBone`, `detached`, `deformMayFold`, `editorRoundTrip`.
3145
+ against), `axisBone`, `massBone`, `detached`, `deformMayFold`, `editorRoundTrip`,
3146
+ `consumerDrivenMix`.
3079
3147
  Nothing in skeleton
3080
3148
  JSON records that a
3081
3149
  bone carries a cut's axis or that a parentage is forbidden, so the rig spec says it
@@ -3083,7 +3151,10 @@ and the validator's archetype assertions read it. **An assertion whose field is
3083
3151
  absent reports SKIP, never a pass.** If you are reproducing a foreign skeleton,
3084
3152
  leave this out entirely and run `--profile spine` — and expect `PROF` rather than
3085
3153
  that SKIP, because the profile excludes an archetype assertion before its body
3086
- could notice the missing field (§5.2).
3154
+ could notice the missing field (§5.2). The one field `ingest` writes is
3155
+ `consumerDrivenMix`, and only for a constraint the file rests muted and never keys
3156
+ up, each with a `CONSUMER_DRIVEN_MIX` finding ([INGEST.md](INGEST.md) §2.0) — see
3157
+ the last block of this section.
3087
3158
 
3088
3159
  🚨 **The exception: `meshSlots` is required by a rig that invokes a mesh
3089
3160
  generator** (`ring`, `ribbon`, `contour`, `grid` — §3.4), and it is a **compile-time**
@@ -3108,8 +3179,9 @@ loads and still animates and merely lies — something released into the world t
3108
3179
  must not ride the part that released it. [RIGGING.md](RIGGING.md) §10.3 has a
3109
3180
  worked one.
3110
3181
 
3111
- 🚨 **`deformMayFold` is the one field here that turns a check OFF**, so it is the
3112
- one field whose own shape is refused rather than skipped. It is
3182
+ 🚨 **`deformMayFold` is one of the two fields here that turn a check OFF**
3183
+ (`consumerDrivenMix`, below, is the other), so it is held to a shape that is
3184
+ refused rather than skipped. It is
3113
3185
  `[{ "slot": …, "why": … }]`, it exempts that slot from
3114
3186
  `A39_DEFORM_KEEPS_TRIANGLE_WINDING`, and three shapes are compile errors: a slot
3115
3187
  the rig does not declare, a slot that carries no mesh, and a missing or blank
@@ -3166,6 +3238,29 @@ drawn and are gated (§4.11.3); this used to be a rule you had to follow and is
3166
3238
  now one the gate keeps
3167
3239
  ([#403](https://github.com/firejune/rigc/issues/403)).
3168
3240
 
3241
+ 🎛️ **`consumerDrivenMix` names the ik and transform constraints whose mix the
3242
+ CONSUMER sets** — from code, at runtime — rather than any animation in this file
3243
+ ([#784](https://github.com/firejune/rigc/issues/784)). It is
3244
+ `[{ "constraint": …, "type": "ik" | "transform", "why": … }]`, and it exempts that
3245
+ constraint from `A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT` or
3246
+ `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` (§4.12). A constraint resting muted
3247
+ that nothing keys up is either a leftover or a dial a game turns, the two export as
3248
+ the same bytes, and the gate refuses the shape because nothing *in the file* ever
3249
+ moves it — this is the statement that something outside it does, the rig-side
3250
+ spelling of `gallery/look`'s rule that a face angle is a value rather than a time.
3251
+
3252
+ - `type` is required because a constraint's name is unique **per kind** — an ik and
3253
+ a transform may share one — so a name alone could exempt two constraints.
3254
+ - Five shapes are compile errors, each by name: a constraint the rig does not
3255
+ declare under that `type`, a `type` other than `ik` or `transform` (a physics,
3256
+ path or slider constraint has its own muted-at-rest rule — `A23`, `A36`, `A37` —
3257
+ and none of them reads this declaration, so the entry would exempt nothing), the
3258
+ same constraint twice, a missing or blank `why`, and a key the entry does not have.
3259
+ - ⛔ A declared constraint the file **also** switches on — resting live, or keyed
3260
+ above 0 — is refused at the gate: the declaration exempts nothing there.
3261
+ - What it buys is a **SKIP by name, never a pass** (§4.12 has the line). The
3262
+ declaration is a statement to the gate and changes no emitted byte.
3263
+
3169
3264
  ---
3170
3265
 
3171
3266
  ## 4. The motion spec, field by field
@@ -4059,8 +4154,11 @@ here was measured off a real rig. Copy the shape, not the values.
4059
4154
  rebuilds with `bendPositive: false` on every key if the flag is left silent,
4060
4155
  and with `true` — what the source plays — when `ingest` writes it out. So
4061
4156
  `ingest` restates every field any key of a track names, at the parser's default
4062
- where the source omits one, and records a `CONSTRAINT_KEY_RESTATED` finding. On
4063
- rigc's own output the two agree and the round trip is byte-identical.
4157
+ where the source omits one — in the **spec**. The emitter then leaves each such
4158
+ value out of the file again wherever it is the one the parser reads without it
4159
+ (§10.6c), so the rebuild is the source's own text, and `CONSTRAINT_KEY_RESTATED`
4160
+ is printed only for a value the file would still carry. On rigc's own output the
4161
+ two agree and the round trip is byte-identical.
4064
4162
  - `mix` outside `0..1` is a compile error: `IkConstraintPose.mix` is documented as
4065
4163
  a percentage. A **transform** mix is documented *unbounded*, which is why §4.10
4066
4164
  has no such rule — the asymmetry is the runtime's, not ours.
@@ -4082,6 +4180,13 @@ rigc refuses four things here:
4082
4180
  | `softness` on the first key only | `key 0 names "softness" and key 1 (t=…) does not … it would snap to 0` |
4083
4181
  | `mix: 1.5` | `mix is 1.5, outside 0..1 — the runtime documents it as a percentage 0-1` |
4084
4182
 
4183
+ 🎛️ **A constraint this section never keys** — resting at `mix` 0 with no `ik` track
4184
+ in any animation lifting it — is refused by `A47` with three doors: rest it above 0,
4185
+ key it here, or declare that the consumer drives its mix in the rig spec's
4186
+ `invariants.consumerDrivenMix` (§3.7), for an ik a game turns on from code. The
4187
+ third is a statement about the object, not a way past the gate: what it buys is a
4188
+ SKIP by name (§4.12).
4189
+
4085
4190
  The type check matters because the parser resolves a timeline's target by name
4086
4191
  **and** type: `findConstraint(name, IkConstraintData)` misses a transform
4087
4192
  constraint of the same name, returns null, and `readAnimation` throws — in the
@@ -4141,7 +4246,10 @@ exactly what a mix that was 0 at setup needs said.
4141
4246
  omitted mix as 1, so a key of `mixRotate: 0` alone on a rotate-only constraint
4142
4247
  carries five mixes of 1 that nothing reads. `A48` judges only the mixes of the
4143
4248
  properties the constraint drives (§4.12).
4144
- - The refusals are §4.9's, with `transform` in place of `ik`.
4249
+ - The refusals are §4.9's, with `transform` in place of `ik` — and so is the third
4250
+ door: a transform resting muted on every mix it reads that no key here lifts is
4251
+ refused by `A48` with *rest it above 0, key it, or declare that the consumer drives
4252
+ its mix* in `invariants.consumerDrivenMix` (§3.7, §4.12).
4145
4253
 
4146
4254
  ### 4.11 `deform` — moving an attachment's vertices
4147
4255
 
@@ -5049,8 +5157,36 @@ rescue and a 0-only timeline is not, with two differences that are the runtime's
5049
5157
 
5050
5158
  `ik constraint "reach" has mix 0 at setup and none of the 1 animation keys its mix
5051
5159
  above 0; update() returns on mix 0, so "upper" never reaches for "goal" — rest it
5052
- above 0, or key its mix above 0 in an animation`. A rig resting at 0 and keyed up by
5053
- the animation that needs it — spineboy's aim — is refused by neither.
5160
+ above 0, or key its mix above 0 in an animation, or declare that the consumer drives
5161
+ its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "reach",
5162
+ "type": "ik", "why": … }]`. A rig resting at 0 and keyed up by the animation that
5163
+ needs it — spineboy's aim — is refused by neither.
5164
+
5165
+ 🎛️ **The third door is for a dial the file cannot show turning**
5166
+ ([#784](https://github.com/firejune/rigc/issues/784)). A constraint resting muted
5167
+ that nothing keys up is either a leftover or a mix a game sets from code, and the two
5168
+ export as the same bytes — a production skeleton's rebuild was refused for exactly
5169
+ that, over an ik its game switches on at runtime. `invariants.consumerDrivenMix`
5170
+ (§3.7) is the statement the file cannot make, and what it buys is a **SKIP by name,
5171
+ never a pass**: the file still shows nothing moving the constraint.
5172
+
5173
+ ```
5174
+ SKIP A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT: every ik constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "reach" (why: a game turns it on from code): it rests muted and none of the 1 animation keys its mix above 0
5175
+ ```
5176
+
5177
+ - **Beside a live constraint of the same kind** the live one is still measured, so
5178
+ the rule PASSes and the build's stats line names the declared one
5179
+ (`ikConsumerDriven=reach`, `transformConsumerDriven=…`) — `A39`'s shape for
5180
+ `deformMayFold`. A SKIP there would print "nothing measured" over a rule that
5181
+ measured one.
5182
+ - ⛔ **A declared constraint the file also switches on is refused** — resting live,
5183
+ or keyed above 0 by any animation — because the declaration exempts nothing
5184
+ there: the gate passes it without one. [measured] an ik keyed at mix 0.5 with code
5185
+ writing 1: written before `state.apply` it is overwritten (applied 0.5), written
5186
+ after it wins (1.0). Which of the two authors holds a frame is the order of the
5187
+ consumer's own loop, which is the scene's, not the object's.
5188
+ - `validate <dir>` has no rig spec and so no declaration: a muted constraint there
5189
+ is refused with all three doors.
5054
5190
 
5055
5191
  ### 4.13 `sequence` — which frame of a numbered series shows
5056
5192
 
@@ -5499,7 +5635,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5499
5635
  | `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`) |
5500
5636
  | `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)) |
5501
5637
  | `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
5502
- | `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 base plate may be opaque, and which image that is is decided once for both routes ([#770](https://github.com/firejune/rigc/issues/770)): the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art ([#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. ⚠️ **A page file that cannot be read as PNG at all is the same non-measurement for every part on it** ([#732](https://github.com/firejune/rigc/issues/732)): one row per page naming its parts and pointing at `A06`, which names what the file is — where it used to print `threw: cannot decode PNG …: unexpected end of file`, an inflate error about a file that was never a PNG. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
5638
+ | `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes** ([#777](https://github.com/firejune/rigc/issues/777)); the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. Until #777 the loose route stopped at the header, so the same fully opaque RGBA part passed a loose build and was refused by `--pack` of the same rig. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes ([#770](https://github.com/firejune/rigc/issues/770)): the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art ([#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. ⚠️ **A page file that cannot be read as PNG at all is the same non-measurement for every part on it** ([#732](https://github.com/firejune/rigc/issues/732)): one row per page naming its parts and pointing at `A06`, which names what the file is — where it used to print `threw: cannot decode PNG …: unexpected end of file`, an inflate error about a file that was never a PNG. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
5503
5639
  | `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)) |
5504
5640
  | `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 |
5505
5641
  | `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)) |
@@ -5527,8 +5663,8 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
5527
5663
  | `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | both | a **linked mesh** (§3.4) — `type: "linkedmesh"`, or a `type: "mesh"` carrying `source` — 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 `setSourceMesh` fills the attachment with the source's arrays instead: the file says one mesh and every runtime draws another, in silence. The detail names the attachment by skin, slot and placeholder, every key it states, the `source` and where the parser looks for it — the two defaults spelled out, because an omitted `skin` is the **default** skin rather than the one the link is written in — and the shape the keys describe beside the shape the attachment loaded. ⚠️ **`width`/`height` are not part of this.** `setSourceMesh` overwrites both with the source's, so they are as dead at runtime — but the parser reads them (`:569-570`), the format carries them on a link and rigc emits them, so refusing them would refuse every link rigc writes (§3.4). `compile.ts` refuses the same shape outright in a rig rigc builds (§5.1); this is that fact held against a skeleton it did not write, and `ingest` reports it as `ATTACHMENT_LINK_GEOMETRY` ([INGEST §2.0](INGEST.md)). **SKIP** when no attachment in the skeleton takes its geometry from another — which is almost every skeleton, so a pass here means a link was read ([#710](https://github.com/firejune/rigc/issues/710)) |
5528
5664
  | `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | both | an `rgb` or `alpha` timeline (§4.4) the runtime does not hold as the file states it, in one of two shapes that both parse in silence. **A channel keyed twice**: another colour timeline of the same slot in the same animation poses a channel this one poses — `rgba` beside `alpha` is the shape a converter leaves when it writes a separable `rgb` back as `rgba` next to the `alpha` it kept. Every colour timeline poses its channels at every time, the setup value before its first key included, so the one the file states later overwrites the other everywhere; the detail names both timelines, the channel, and which one survives. **A key not posed as written**: the animation is stepped to each key's own time — at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298…`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before (a correct file was refused that way until [#771](https://github.com/firejune/rigc/issues/771)), and the posed r g b (for `rgb`, against the hex, to half a quantisation step) or alpha (for `alpha`, against `value`, whose absence the parser reads as 0) is compared — a colour that is not six hex digits loads as NaN, and a key whose time another key repeats is read by nothing. ⚠️ An `rgb` alone written as an `rgba` holding the setup alpha is **not** caught and cannot be from the file: it is a correct `rgba`, and the difference shows only under another track that moves the alpha. The loaded timeline class and the channels a separable timeline leaves alone are measured in the selftest (`S83`–`S85`) rather than here, because against the linked parser neither can come out wrong. The channel table is `SLOT_COLOR_CHANNELS` in `src/timelines.ts`, shared with the compiler's refusal and held to the runtime's own property ids (`S89`). **SKIP** when no animation keys an `rgb` or `alpha` — there is then no separable slot colour to read back |
5529
5665
  | `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | both | a **numbered series** (§3.4.3, §4.13) that the runtime does not show as the file states it. Every shape below loads without a word, measured on spine-core 4.3.13 ([#729](https://github.com/firejune/rigc/issues/729)). **The block**: a `sequence` with no `count` (`readSequence` reads 0, and the attachment holds no region) or a `setup` at or past `count` (`Sequence.resolveIndex` clamps it to the last frame). **The keys**: a `mode` outside the seven — `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` — loads as `hold`; an `index` that is fractional (`index << 4` truncates it) or past the end (clamped); an advancing mode at an effective delay of 0 (the parser carries a key's `delay` from the key before; `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so it never advances); a timeline on an attachment that carries no block (the parser gives every region a one-region series, so every mode shows it). **The pose**: every key is stepped to mid-frame sample times — enough to wrap every mode, and a `hold` key to its own time as the runtime stores it, a 32-bit float ([#771](https://github.com/firejune/rigc/issues/771)) — and the region the slot shows is held to the frame the file's own statement gives, the arithmetic of `SequenceTimeline.applyToSlot` and the names of `Sequence.getPath` transcribed rather than read off the loaded timeline, so the check is not the runtime agreeing with itself. Before the first key the frame is `setup`. ⚠️ A sample where the slot shows another attachment is not compared, because the runtime writes nothing there; a timeline with no comparable sample is counted in `stats.sequenceSamplesUnshown`. `compile.ts` refuses every one of these shapes in a spec (§5.1); this is them held against a skeleton it did not write. **SKIP** when no attachment carries a `sequence` block and no animation keys a `sequence` timeline |
5530
- | `A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | an ik constraint resting at `mix` 0 that no animation keys **away from 0** (§4.9, §4.12). `IkConstraint.update` returns on `mix === 0`, so it sits in the update cache and moves nothing. The keys are read the way `A23`/`A36`/`A37` read theirs — every value the loaded timeline poses on its `mix` channel, Bezier samples included — so a timeline keying 0 only is no rescue: [measured] it poses every bone exactly where the same rig with no constraint does, and before [#765](https://github.com/firejune/rigc/issues/765) it passed. Live is the runtime's `!== 0`, so a negative mix is not refused. `ik constraint "C" has mix 0 at setup and none of the 1 animation keys its mix above 0; update() returns on mix 0, so "upper" never reaches for "goal" — rest it above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no ik constraint |
5531
- | `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | a transform constraint none of whose mixes **for a property it drives** is away from 0 at setup or on any value an animation poses (§4.10, §4.12), or one whose `properties` name no `to` at all. A property is applied only when its own mix `!== 0`, and a key that omits a mix reads it as 1, so the six-mix early return of `TransformConstraint.update` would take a key of `mixRotate: 0` alone as a rescue — [measured] that key, and one keying `mixX` 1 on a rotate-only constraint, pose every bone exactly where no constraint does ([#765](https://github.com/firejune/rigc/issues/765)). A negative mix runs, and five transforms in the editor's example exports rest at −1. `transform constraint "C" drives rotate and has mixRotate 0 at setup, and none of the 1 animation keys its mix above 0; a mix is read only for a property the constraint drives, and update() skips each one at 0, so nothing ever moves "follower" — rest mixRotate above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no transform constraint |
5666
+ | `A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | an ik constraint resting at `mix` 0 that no animation keys **away from 0** (§4.9, §4.12). `IkConstraint.update` returns on `mix === 0`, so it sits in the update cache and moves nothing. The keys are read the way `A23`/`A36`/`A37` read theirs — every value the loaded timeline poses on its `mix` channel, Bezier samples included — so a timeline keying 0 only is no rescue: [measured] it poses every bone exactly where the same rig with no constraint does, and before [#765](https://github.com/firejune/rigc/issues/765) it passed. Live is the runtime's `!== 0`, so a negative mix is not refused. `ik constraint "C" has mix 0 at setup and none of the 1 animation keys its mix above 0; update() returns on mix 0, so "upper" never reaches for "goal" — rest it above 0, or key its mix above 0 in an animation, or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "C", "type": "ik", "why": … }]`. A constraint the rig spec declares in `invariants.consumerDrivenMix` (§3.7) is not measured, and one the file also switches on is refused: `ik constraint "C" is declared in the rig spec as invariants.consumerDrivenMix, and the file already switches it on — … — so the declaration exempts nothing; drop the entry` ([#784](https://github.com/firejune/rigc/issues/784)). **SKIP** when the skeleton declares no ik constraint, and **SKIP by name** when every ik constraint it declares is declared consumer-driven: `every ik constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "C" (why: …): it rests muted and none of the 1 animation keys its mix above 0`. With a live one beside it the rule measures that one and the stats line names the declared (`ikConsumerDriven`) |
5667
+ | `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | a transform constraint none of whose mixes **for a property it drives** is away from 0 at setup or on any value an animation poses (§4.10, §4.12), or one whose `properties` name no `to` at all. A property is applied only when its own mix `!== 0`, and a key that omits a mix reads it as 1, so the six-mix early return of `TransformConstraint.update` would take a key of `mixRotate: 0` alone as a rescue — [measured] that key, and one keying `mixX` 1 on a rotate-only constraint, pose every bone exactly where no constraint does ([#765](https://github.com/firejune/rigc/issues/765)). A negative mix runs, and five transforms in the editor's example exports rest at −1. `transform constraint "C" drives rotate and has mixRotate 0 at setup, and none of the 1 animation keys its mix above 0; a mix is read only for a property the constraint drives, and update() skips each one at 0, so nothing ever moves "follower" — rest mixRotate above 0, or key its mix above 0 in an animation, or declare that the consumer drives its mix, in the rig spec as invariants.consumerDrivenMix: [{ "constraint": "C", "type": "transform", "why": … }]`. A constraint the rig spec declares in `invariants.consumerDrivenMix` (§3.7) is not measured, and one the file also switches on is refused as `A47`'s is; one that drives no property is refused with its own sentence declared or not, since no mix it carries is read by anybody ([#784](https://github.com/firejune/rigc/issues/784)). **SKIP** when the skeleton declares no transform constraint, and **SKIP by name** when every transform constraint it declares is declared consumer-driven: `every transform constraint here is declared in the rig spec as invariants.consumerDrivenMix, so its mix is the consumer's to set and nothing in this file shows it moving — "C" (why: …): it rests muted and none of the 1 animation keys its mix above 0`. With a live one beside it the stats line names the declared (`transformConsumerDriven`) |
5532
5668
 
5533
5669
  `both ◑` marks a mixed assertion: its validity half always runs and its policy
5534
5670
  clauses are gated by profile.
@@ -7167,8 +7303,9 @@ about your bone tree rather than about a hole in your figure.
7167
7303
 
7168
7304
  Every reference in this repository was made in the Spine editor by a person, and a
7169
7305
  rig authored here is measured against one. rigc's own defaults are deliberately
7170
- *absent* rather than opinionated (R1: a field is emitted exactly when you declare
7171
- it), so nothing in the compiler will push you toward the shape an editor rig has.
7306
+ *absent* rather than opinionated (R1: a field reaches the file when you declare it,
7307
+ and nothing you did not declare is supplied), so nothing in the compiler will push you
7308
+ toward the shape an editor rig has.
7172
7309
  This section is that push, and it comes from **Spine's public documentation only** —
7173
7310
  what the editor does when nobody tells it otherwise, and what its user guide
7174
7311
  recommends.
@@ -7274,10 +7411,16 @@ population is every object the format keys by a name and that carries more than
7274
7411
  one key, deform blocks counted at each of their three levels;
7275
7412
  [`src/compile.ts`](../src/compile.ts) states it in full beside
7276
7413
  `editorAnimationOrder`, so the count can be re-taken rather than trusted.)
7277
- ⇒ in rigc: only `animations` is emitted sorted (R10), because it is the one
7414
+ ⇒ in rigc: `animations` is emitted sorted (R10), because it is the one
7278
7415
  object measured here whose ORDER is also an index space — every reference into
7279
7416
  the re-sorted *other* objects is by name on both sides, so nothing moves when
7280
- they are re-keyed. rigc emits **that comparator's own order**, which
7417
+ they are re-keyed. ⚠️ Since [#716](https://github.com/firejune/rigc/issues/716)
7418
+ a skin's `attachments` slot keys are emitted sorted too, for the **text** and
7419
+ for no reference: a rebuild of an editor export is the export only if it is the
7420
+ same text (§10.6b), and 11 of the twelve under `examples/` keyed a skin in draw
7421
+ order where the export sorts it. Nothing is refused on that map — a pair the
7422
+ comparator leaves open, or a slot name with a `/` in it, keeps the whole map in
7423
+ rigc's order instead. rigc emits **that comparator's own order**, which
7281
7424
  [#728](https://github.com/firejune/rigc/issues/728) then measured in full off five
7282
7425
  stored round trips (R10) rather than quantifying over a family. Sorting the
7283
7426
  105 collections that way reproduces **105 of 105**, the three codepoint cannot
@@ -7808,11 +7951,20 @@ either.
7808
7951
  *"assume … if omitted"* default — the same fact R5 states from the parser's side:
7809
7952
  omit them in raw JSON and every UV collapses, in silence. Name an `image`.
7810
7953
 
7811
- ⚠️ **Do not imitate the exporter's omissions.** Spine's exporter drops fields equal
7812
- to their default, which is why the format page is a long list of *"assume 0 if
7813
- omitted"* — and **rigc deliberately does the opposite** (R1, §2). Writing `x: 0` is
7814
- legitimate here. The habit worth carrying over is not *omit defaults*, it is
7815
- *declare only what the shot needs*.
7954
+ ✅ **You need not imitate the exporter's omissions — the emitter does it for you.**
7955
+ Spine's exporter drops fields equal to their default, which is why the format page is
7956
+ a long list of *"assume 0 if omitted"*, and since
7957
+ [#716](https://github.com/firejune/rigc/issues/716) rigc's emitter drops the same
7958
+ ones (R1, §2; the table is §10.6c). So an author may still state a default — `x: 0`
7959
+ in a rig spec is legitimate and stays in the spec — and the file leaves it out.
7960
+ ⚠️ This paragraph said *"do not imitate the exporter's omissions … rigc deliberately
7961
+ does the opposite"* until then. The advice was re-derived rather than kept: **the
7962
+ round trip is the reason.** A rebuild of an editor export that writes back every
7963
+ default the export left out is a different file from the export in 2,338 places over
7964
+ the twelve under `examples/`, and one that leaves them out is the export's own text
7965
+ apart from the header's `hash` and `spine` (`IG83`, and [INGEST §2.3](INGEST.md)).
7966
+ The habit worth carrying over is still not *omit defaults* — it is *declare only what
7967
+ the shot needs*; what changed is that stating a default costs nothing in the file.
7816
7968
 
7817
7969
  ### 10.6 What a round trip gives back
7818
7970
 
@@ -7834,7 +7986,12 @@ comes back `48.0`) and **omitted defaults** — the export drops any field equal
7834
7986
  its parser default, so the header loses `x: 0` and `y: 0`, a bone loses `x: 0`, and
7835
7987
  a key at t=0 loses its `"time": 0`. Every name-keyed object is also re-sorted, per
7836
7988
  §10.1. None of those is a loss of information, and each is worth knowing before
7837
- you read a `diff`.
7989
+ you read a `diff`. 🔁 Since [#716](https://github.com/firejune/rigc/issues/716) the
7990
+ bone's `x: 0` and the key's `"time": 0` are left out by rigc as well (§10.6c); the
7991
+ header's origin is not, because the 4.3 JSON reader has no default for it
7992
+ (`skeletonData.x = skeletonMap.x`, `SkeletonJson.js:70`), so an absent origin loads
7993
+ as `undefined` where a written one loads as `0` — writing it is exact for both
7994
+ readers, and leaving it out would move a loaded value.
7838
7995
 
7839
7996
  ⚠️ Read every line below as *this construct survived*, never as *this construct is
7840
7997
  recommended*. §10.1–§10.5 are the recommendations; this subsection is only the
@@ -7946,6 +8103,223 @@ identical** on an emitted array all three of whose numbers moved. ⇒ Read a
7946
8103
  `lengths` disagreement as *certainly wrong data, and visible only under
7947
8104
  `positionMode: fixed`*.
7948
8105
 
8106
+ ### 10.6b The order it writes keys in
8107
+
8108
+ 🔬 **The editor writes every object's keys in one fixed order per kind of
8109
+ object**, and since [#716](https://github.com/firejune/rigc/issues/716) rigc
8110
+ writes the same. Read off the twelve exports under `examples/`: for each kind,
8111
+ every object's key sequence is a subsequence of one order, no two exports
8112
+ contradict each other on any kind, and the table below is that order. It lives
8113
+ in one place, [`src/keyorder.ts`](../src/keyorder.ts)'s `EDITOR_KEY_ORDER`, and
8114
+ the compiler applies it once to the finished skeleton — `CUR83` holds this table
8115
+ to that one, and `IG77` holds that one to the exports on every run with a
8116
+ corpus.
8117
+
8118
+ | Kind | Keys, in the order the editor writes them |
8119
+ | --- | --- |
8120
+ | `top level` | `skeleton`, `bones`, `slots`, `constraints`, `skins`, `events`, `animations` |
8121
+ | `header` | `hash`, `spine`, `x`, `y`, `width`, `height`, `images`, `audio` |
8122
+ | `bone` | `name`, `parent`, `length`, `rotation`, `x`, `y`, `scaleX`, `scaleY`, `inherit`, `color`, `icon` |
8123
+ | `slot` | `name`, `bone`, `color`, `attachment`, `blend` |
8124
+ | `ik constraint` | `type`, `name`, `target`, `bones`, `mix`, `bendPositive` |
8125
+ | `transform constraint` | `type`, `name`, `source`, `bones`, `rotation`, `x`, `y`, `properties`, `localSource`, `localTarget`, `mixRotate`, `mixX`, `mixY`, `mixScaleX`, `mixShearY` |
8126
+ | `physics constraint` | `type`, `name`, `bone`, `x`, `y`, `rotate`, `damping` |
8127
+ | `skin` | `name`, `attachments` |
8128
+ | `region attachment` | `x`, `y`, `scaleX`, `scaleY`, `rotation`, `width`, `height` |
8129
+ | `mesh attachment` | `type`, `uvs`, `triangles`, `vertices`, `hull`, `edges`, `width`, `height` |
8130
+ | `boundingbox attachment` | `type`, `vertexCount`, `vertices` |
8131
+ | `clipping attachment` | `type`, `end`, `vertexCount`, `vertices`, `color` |
8132
+ | `animation` | `slots`, `bones`, `ik`, `transform`, `physics`, `attachments`, `drawOrder`, `events` |
8133
+ | `bone rotate key` | `time`, `value`, `curve` |
8134
+ | `bone translate key` | `time`, `x`, `y`, `curve` |
8135
+ | `bone translatex key` | `time`, `value`, `curve` |
8136
+ | `bone translatey key` | `time`, `value`, `curve` |
8137
+ | `bone scale key` | `time`, `x`, `y`, `curve` |
8138
+ | `bone shear key` | `time`, `x`, `y`, `curve` |
8139
+ | `slot attachment key` | `time`, `name` |
8140
+ | `slot rgba key` | `time`, `color`, `curve` |
8141
+ | `ik key` | `time`, `mix`, `softness`, `bendPositive`, `curve` |
8142
+ | `transform key` | `time`, `mixRotate`, `mixX`, `mixY`, `curve` |
8143
+ | `physics damping key` | `time`, `value` |
8144
+ | `physics inertia key` | `time`, `value` |
8145
+ | `physics mass key` | `time`, `value` |
8146
+ | `physics mix key` | `time`, `value`, `curve` |
8147
+ | `physics wind key` | `time`, `value` |
8148
+ | `attachment deform key` | `time`, `offset`, `vertices`, `curve` |
8149
+ | `drawOrder key` | `time`, `offsets` |
8150
+ | `drawOrder offset` | `slot`, `offset` |
8151
+ | `event key` | `time`, `name` |
8152
+
8153
+ ⚠️ **Three things about that table are rigc's rather than the editor's, and it
8154
+ says which.**
8155
+
8156
+ - **A pair no export carries together has no measured order**, and the row
8157
+ places it the way rigc already emitted it — a transform constraint's
8158
+ `rotation` before `x`, an ik key's `time` before `mix`, a physics
8159
+ constraint's `x`/`y` before `rotate`. `src/keyorder.ts` lists every such pair.
8160
+ Such a pair did not move, and nothing here says the editor agrees.
8161
+ - **A kind with no row keeps rigc's order whole**: a linked mesh, a path or
8162
+ point attachment, a sequence, the path and slider constraints, an event
8163
+ definition, and the keys of every timeline the twelve do not key with two
8164
+ fields (`scalex`, `rgb`, `alpha`, `rgba2`, `sequence`, the path and slider
8165
+ timelines, …). Unmeasured is not certified.
8166
+ - **A key a row does not list stays where its constructor put it** — a bone's
8167
+ `shearX`, a region's `name` and `path`, a header's `fps`. The row's keys are
8168
+ permuted among the places they hold. Sending such a key to the end instead was
8169
+ rejected on a measured case: the corpus's physics `strength` keys are all at
8170
+ t=0, so the editor wrote them `value` alone, and that rule would have moved
8171
+ `time` — first in every other key kind the editor writes — behind `value`.
8172
+
8173
+ 🔬 **Two positions and one fold are measured on a production set, not on
8174
+ `examples/`** ([#791](https://github.com/firejune/rigc/issues/791)). The twelve
8175
+ exports carry no mesh `path` or `color` and no slot name outside ASCII — `IG84`
8176
+ and `IG85` count both on every run with a corpus — so the table cannot learn
8177
+ either, and what rigc writes for them was read off 42 production exports
8178
+ instead:
8179
+
8180
+ - **A mesh attachment writes `path` and `color` right after `type`, `path` before `color` — measured on a production set (#791), not on `examples/`.**
8181
+ 18 meshes whose `path` differs from their name wrote it second, and 1 with a
8182
+ `color` wrote that second; no mesh carried both. The row does not list them,
8183
+ so the position is the constructor's — `meshTextureKeys` in
8184
+ [`src/compile.ts`](../src/compile.ts), which every mesh route spreads right
8185
+ after `type` (`S104`). A region's `path` and a linked mesh's keys stay where
8186
+ they were: neither was in the set, and an analogy is not a measurement.
8187
+ - **A skin's slot keys are compared after `foldBeforeComparing`, which reads U+3000 as U+0020 and U+FF10–U+FF19 as U+0030–U+0039** —
8188
+ the ideographic space as a space, the full-width digits as `0`–`9`
8189
+ (`EDITOR_NAME_FOLD`). One map of 95 slot keys, 7 of them carrying those
8190
+ characters, came back in an order this reproduces 95 of 95 and neither a
8191
+ codepoint sort nor the comparator without the fold does (`S105`). It is **not**
8192
+ `normalize('NFKC')`, though NFKC folds both the same way: NFKC, a classifier
8193
+ that reads every Unicode digit as a digit and every space separator as a space,
8194
+ and a width fold alone all reproduce that map and disagree past it (on `fi`,
8195
+ `²`, `٣`, `A`, a no-break space), and what all three agree on is exactly
8196
+ these two classes. The comparator is R10's, so animation and skin names are
8197
+ compared after the fold too. A pair the fold does not close — a tab, a
8198
+ no-break space — still keeps the map in rigc's order, whole (`S106`).
8199
+
8200
+ 🔸 **An object keyed by NAMES is not a field order**, and the table does not
8201
+ touch one. A bone's or a slot's timelines, and a transform constraint's
8202
+ `properties` and each `to` inside them, are read into the runtime's arrays in the
8203
+ order they are keyed — for timelines, the order they are applied in — so their
8204
+ order is more than a key's position, and rigc already emits the export's own on
8205
+ all twelve. A skin's `attachments` slot keys are the one name-keyed map this
8206
+ changed: sorted by the editor's comparator (§10.1). The per-slot maps inside are
8207
+ the rig's own order.
8208
+
8209
+ ⇒ **What this means for you: nothing to write.** No input format changed and no
8210
+ key order is yours to state — a rig spec is read by name, and its field order
8211
+ still means nothing. What changed is that a canonical-form comparison of an
8212
+ export's rebuild against the export no longer finds an object whose keys sit
8213
+ elsewhere (`IG76`). What it found next was the defaults the export leaves out
8214
+ and rigc wrote, `"name": null`, and the three header keys (`IG78`) — and since
8215
+ §10.6c it finds the header's `hash` and `spine` and nothing else (`IG83`).
8216
+
8217
+ ### 10.6c The keys it leaves out
8218
+
8219
+ The editor writes a key only where its value is not the one the parser reads in its
8220
+ absence, and since [#716](https://github.com/firejune/rigc/issues/716) rigc's emitter
8221
+ does the same, in one pass over the finished skeleton (`withoutParserDefaults` in
8222
+ `src/keyorder.ts`, run just before §10.6b's). A key is left out when its value is
8223
+ **exactly** the one below — the float the file would hold, not a value near it — so a
8224
+ rotation of `1e-45` is written and a rotation of `0` is not. The table is
8225
+ `PARSER_DEFAULTS`, and every row is the linked `spine-core` parser's own fallback
8226
+ (`getValue(map, key, default)` in `SkeletonJson.js`); the selftest loads an object of
8227
+ each kind with the key at that value and without it, and requires the two to be the
8228
+ same `SkeletonData` (`S103` on the in-tree builds, `IG82` on the corpus), and deletes
8229
+ every key of every in-tree build in turn to find one the table is missing (`S101`):
8230
+
8231
+ | Kind | Left out when it is |
8232
+ | --- | --- |
8233
+ | `header` | `referenceScale` 100 |
8234
+ | `bone` | `length` 0 · `rotation` 0 · `x` 0 · `y` 0 · `scaleX` 1 · `scaleY` 1 · `shearX` 0 · `shearY` 0 · `inherit` "normal" · `skin` false · `iconSize` 1 · `iconRotation` 0 |
8235
+ | `slot` | `color` "ffffffff" · `attachment` null · `blend` "normal" · `visible` true |
8236
+ | `ik constraint` | `skin` false · `mix` 1 · `softness` 0 · `bendPositive` true · `compress` false · `stretch` false |
8237
+ | `transform constraint` | `skin` false · `localSource` false · `localTarget` false · `additive` false · `clamp` false · `rotation` 0 · `x` 0 · `y` 0 · `scaleX` 0 · `scaleY` 0 · `shearY` 0 · `mixRotate` 1 · `mixX` 1 · `mixScaleX` 1 · `mixShearY` 1 |
8238
+ | `path constraint` | `skin` false · `positionMode` "percent" · `spacingMode` "length" · `rotateMode` "tangent" · `rotation` 0 · `position` 0 · `spacing` 0 · `mixRotate` 1 · `mixX` 1 · `mixY` `mixX`'s, only at 1 |
8239
+ | `physics constraint` | `skin` false · `x` 0 · `y` 0 · `rotate` 0 · `scaleX` 0 · `shearX` 0 · `limit` 5000 · `fps` 60 · `inertia` 0.5 · `strength` 100 · `damping` 0.85 · `mass` 1 · `wind` 0 · `gravity` 0 · `mix` 1 · `inertiaGlobal` false · `strengthGlobal` false · `dampingGlobal` false · `massGlobal` false · `windGlobal` false · `gravityGlobal` false · `mixGlobal` false |
8240
+ | `slider constraint` | `skin` false · `additive` false · `loop` false · `mix` 1 · `from` 0 · `to` 0 · `scale` 1 · `max` 0 · `local` false |
8241
+ | `region attachment` | `x` 0 · `y` 0 · `scaleX` 1 · `scaleY` 1 · `rotation` 0 · `color` "ffffffff" |
8242
+ | `mesh attachment` | `color` "ffffffff" · `hull` 0 · `width` 0 · `height` 0 |
8243
+ | `path attachment` | `closed` false · `constantSpeed` true |
8244
+ | `clipping attachment` | `convex` false · `inverse` false |
8245
+ | `sequence` | `start` 1 · `setup` 0 |
8246
+ | `event` | `int` 0 · `float` 0 · `string` "" · `audio` null |
8247
+ | `bone rotate key` | `time` 0 · `value` 0 |
8248
+ | `bone translate key` | `time` 0 · `x` 0 · `y` 0 |
8249
+ | `bone translatex key` | `time` 0 · `value` 0 |
8250
+ | `bone translatey key` | `time` 0 · `value` 0 |
8251
+ | `bone scale key` | `time` 0 · `x` 1 · `y` 1 |
8252
+ | `bone scalex key` | `time` 0 · `value` 1 |
8253
+ | `bone scaley key` | `time` 0 · `value` 1 |
8254
+ | `bone shear key` | `time` 0 · `x` 0 · `y` 0 |
8255
+ | `slot attachment key` | `time` 0 · `name` null |
8256
+ | `slot rgba key` | `time` 0 |
8257
+ | `ik key` | `time` 0 · `mix` 1 · `softness` 0 · `bendPositive` true · `compress` false · `stretch` false |
8258
+ | `transform key` | `time` 0 · `mixRotate` 1 · `mixX` 1 · `mixY` `mixX`'s, only at 1 · `mixScaleX` 1 · `mixScaleY` 1 · `mixShearY` 1 |
8259
+ | `path position key` | `time` 0 · `value` 0 |
8260
+ | `physics damping key` | `time` 0 · `value` 0 |
8261
+ | `physics inertia key` | `time` 0 · `value` 0 |
8262
+ | `physics mass key` | `time` 0 · `value` 0 |
8263
+ | `physics strength key` | `time` 0 · `value` 0 |
8264
+ | `physics wind key` | `time` 0 · `value` 0 |
8265
+ | `physics mix key` | `time` 0 · `value` 1 |
8266
+ | `attachment deform key` | `time` 0 · `offset` 0 |
8267
+ | `attachment sequence key` | `time` 0 · `index` 0 · `mode` "hold" · `delay` the key before's, 0 on key 0 |
8268
+ | `drawOrder key` | `time` 0 |
8269
+ | `event key` | `time` 0 |
8270
+
8271
+ Read a row as *"left out when it is"*: `x` 0 is a key left out at `0`; `mixY` `mixX`'s
8272
+ is a key left out where it equals the same object's `mixX` as the parser reads it,
8273
+ and *only at 1* narrows that to the one value the editor leaves it out at — measured:
8274
+ `sack-pro` writes `"mixX": 0, "mixY": 0` on six transform keys, where the parser would
8275
+ read an absent `mixY` as that same `0`. *The key before's* is a sequence key's
8276
+ `delay`, which the parser carries from the previous key (`lastDelay`).
8277
+
8278
+ ⚠️ **What is not a row, and why.**
8279
+
8280
+ - **The header's `x`, `y` and `fps`.** The 4.3 JSON reader assigns them raw
8281
+ (`skeletonData.x = skeletonMap.x`, `SkeletonJson.js:70`), so an absent origin loads
8282
+ as `undefined` and a written `0` as `0`: there is no fallback for them to equal, and
8283
+ rigc keeps writing the origin of a declared stage. The editor leaves it out
8284
+ (§10.6's round trip 6), so a rebuild of an export whose stage sits at `0,0` spells
8285
+ two fields the export does not — `LOSS HEADER_ORIGIN` says so on `ingest`.
8286
+ - **A transform constraint's `mixY` and `mixScaleY`.** Their fallback is the
8287
+ constraint's `mixX` / `mixScaleX` — but the parser reads those only for a property
8288
+ the constraint drives, so on one that drives `y` alone the fallback is the pose's
8289
+ initial `0`, not `1`. A row there was tried and measured wrong: it left out
8290
+ `8-follow-through-pro-ball`'s `"mixY": 1` on two such constraints, they loaded at
8291
+ `0`, and `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` refused the rebuild.
8292
+ - **A region's `path`, an attachment's `name`, an event key's payload.** Their
8293
+ fallbacks are another value — the attachment's own name, its placeholder, the event
8294
+ definition's — which rigc writes only where it differs.
8295
+ - **A slider's `time` and a sequence's `digits`.** The parser has a fallback for
8296
+ both, and nothing here can load it: `time` is read only on a slider with no `bone`,
8297
+ which no build or export carries, and `digits` renames every frame of the series,
8298
+ so no atlas a build resolves against loads it either way. Both are written as
8299
+ stated.
8300
+ - **Every kind with no row** — the keys of a `shearx`, `sheary`, `inherit`, `alpha`,
8301
+ `rgb`, `rgb2`, `rgba2`, path `spacing`/`mix`, physics `gravity`/`reset` or slider
8302
+ timeline, a linked mesh, a point attachment. No build the selftest loads and no
8303
+ export carries an object of those kinds, and a row nothing loads is a claim about
8304
+ the parser nobody has checked — where a missing row only costs a key the parser
8305
+ reads the same either way. `S101` names such a key the day a build emits one.
8306
+
8307
+ ⚠️ **Why 4.3's defaults, when #706 row 4 says they are not every generation's.**
8308
+ An omitted physics `inertia` is 0.5 to the 4.3 parser and 1 to the 4.2 one, and that
8309
+ is exactly why a key is left out only against the parser rigc links: the file states
8310
+ `"spine": "4.3.13"`, and the parser it is gated against is that one. Writing the
8311
+ default out would not make the file safe for a 4.2 reader: that reader applies its
8312
+ own defaults to every *other* key the file leaves out as well — every one the editor's
8313
+ own 4.3 export leaves out, too — and #706 rows 1, 2 and 6 measure what reading one
8314
+ generation's data on another generation's runtime does, none of it repaired by one
8315
+ key written out. That misread is refused where it can be seen, on the reading side
8316
+ (`GENERATION_UNSUPPORTED` in `ingest`), rather than written for on the emitting one.
8317
+
8318
+ ⇒ **What this means for you: nothing to write, and nothing to stop writing.** A
8319
+ default you state stays in your spec and leaves the file; `explain` prints a bone
8320
+ key's channels as the parser reads them, so a key that left the file still shows its
8321
+ value there.
8322
+
7949
8323
  ### 10.7 What this section does not claim
7950
8324
 
7951
8325
  Conventions that are visible in reference exports but that **no public Spine page