spine-rigc 0.9.0 → 0.11.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
@@ -22,6 +22,12 @@ that can see that, and a run that skips it has verified nothing about the motion
22
22
  authoring input, and deliberately unlinked**: it inventories the benchmark corpus
23
23
  skeleton by skeleton, so it is on the ladder run's forbidden list. Named here for a
24
24
  maintainer, not offered to a run
25
+ - Working with a skeleton **somebody else authored** — what the CLI will and will not
26
+ do with a foreign `skeleton.json`, transcribing one into specs, re-pivoting and
27
+ renaming it: `docs/INGEST.md`. 🚫 **Not an authoring input, and deliberately
28
+ unlinked** for the same reason as the line above: it cites `SPEC_COVERAGE.md` and
29
+ the stored transcriptions, both of which are on the ladder run's forbidden list. Its
30
+ reader was handed a compiled skeleton rather than art and a brief
25
31
  - Reproducing a shot you were given as pictures: **§8**, and read it *before* you
26
32
  start measuring rather than after; **§8.1** if the figure has more joints than you
27
33
  can measure one at a time; then **§9** for the loop that closes it
@@ -133,6 +139,10 @@ What the flags mean:
133
139
  | `--motion` | the motion spec — time |
134
140
  | `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths are written relative to it |
135
141
  | `--copy-images` | `build` only: also copies every referenced page PNG into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own. Default is unchanged — page paths still point at the source art (issue #217) |
142
+ | `--pack` | `build` only: arrange every part onto **shared** atlas page(s), written into `--out` as real PNGs, instead of one page per part. Lossless — nothing is resampled, trimmed or rotated. Default is unchanged (issue #4) — **§0.1** |
143
+ | `--page-size` | `build --pack` only: the largest page edge (default `2048`). A ceiling, not the size: page edges are powers of two and the one written is the smallest that holds the pack — **§0.1** |
144
+ | `--padding` | `build --pack` only: the gutter each region reserves on every side (default `2`), filled by extending the region's own edge pixels outwards. `0` is not a legal-but-tight choice, it is bleed — **§0.1** |
145
+ | `--atlas-in` | `build` only: resolve every part against the **regions of a pre-packed `.atlas`** instead of against loose PNGs. Region geometry is read from the file and sizes are descaled by the page's `scale:`; the atlas is re-emitted into `--out`, re-anchored — **§0.2** |
136
146
  | `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** — every `.png` in it is a part, in name order |
137
147
  | `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
138
148
  | `--profile` | `spine` = the 22 validity rules (**the default**) · `spine-html` = all 36, opt-in |
@@ -162,6 +172,146 @@ unweighted meshes, packed atlases) — reach for it when you are shipping into
162
172
  *that* project, not to be thorough. A report always prints which profile ran and
163
173
  lists what that profile left out, on `PROF` lines.
164
174
 
175
+ ⚠️ *Packed atlases* in that list includes the ones rigc now writes itself, so
176
+ `--pack --profile spine-html` is refused by name rather than compiled and then
177
+ failed: `A06`'s full-page-coverage clause **is** the unpacked convention, and a
178
+ legitimate pack cannot satisfy it. Build a pack under the default `spine`.
179
+
180
+ ### 0.1 Packing the parts onto shared pages — `--pack`
181
+
182
+ By default rigc emits **one part, one page**: nine loose PNGs compile to
183
+ `pages=9 regions=9`, which is correct, valid, and not what anybody means by an
184
+ atlas. `--pack` is the other arrangement.
185
+
186
+ ```bash
187
+ bun cli.ts build --rig … --motion … --out spine --pack
188
+ # .. pack: skeleton.png 512x1024, 26 region(s), 67.9% covered, padding 2
189
+ # .. validate (packed atlas, pages on disk)
190
+ ```
191
+
192
+ **What it does.** Every part goes onto a shared page, written into `--out` as a
193
+ real PNG (`skeleton.png`, then `skeleton2.png`, … — libgdx's own numbering, which
194
+ is what the Spine packer uses). The atlas carries `bounds:`/`offsets:` per region
195
+ instead of a page each. `--out` is therefore **self-contained**: that is what
196
+ packing means here, so `--pack` and `--copy-images` are refused together — the
197
+ copy flag copies the loose PNGs, which a packed atlas does not reference.
198
+
199
+ **What it deliberately does NOT do**, and each of these is a promise rather than
200
+ a gap:
201
+
202
+ - **no resampling, no scaling.** A region's pixels are the loose PNG's pixels.
203
+ rigc writes no `scale:` line, so a pack cannot be coarser than its drawings the
204
+ way the shipped examples' packs are (§9.4).
205
+ - **no trimming.** `offsets:` always states a zero inset and the drawing's full
206
+ size. Stripping transparent border would shrink pages, and it would also make
207
+ every region attachment's quad depend on the packer — measure it yourself if you
208
+ want it, do not assume rigc did.
209
+ - **no rotation.** `rotate: 0` on every region, and it is a fact rather than a
210
+ field: the runtime transposes `u2/v2` at 90 and not at 270, and
211
+ `RegionAttachment.computeUVs` assigns a different corner order at 90, so a
212
+ rotated pack is one rigc's own `--atlas` substitution cannot read (issue #199).
213
+ Rotation buys page area; a page that runs out of room spills to a second page
214
+ instead.
215
+ - **no re-ordering of anything the skeleton says.** `skeleton.json` from a packed
216
+ build is **byte-identical** to the unpacked one, because sizes are still
217
+ measured from the loose PNGs and packing is an output arrangement.
218
+
219
+ **Sizes and gutters.** `--page-size` is a ceiling (default `2048`). Page edges are
220
+ powers of two — `region.x / page.width` is the coordinate every texel is sampled
221
+ through, and a power-of-two denominator makes that division exact in binary
222
+ floating point — and the page written is the **smallest** power-of-two pair that
223
+ holds the pack, so a two-part rig gets `1024x256` rather than a megabyte of
224
+ transparency. A part whose padded cell will not fit the biggest allowed page is
225
+ **refused by name**: the format cannot split one drawing across two pages, so the
226
+ alternatives are a bigger page or no pack, and the message says both.
227
+
228
+ `--padding` (default `2`) is the gutter each region reserves on **every** side, so
229
+ two neighbours end up at least twice that apart. The gutter is not empty: it is
230
+ filled by extending the region's own edge pixels outwards, and that is what makes
231
+ the packed render match the unpacked one. The rasteriser samples bilinearly and
232
+ clamps its taps at the page's edge, so on an unpacked page a sample at the outer
233
+ edge reads that edge twice; in the middle of a shared page the clamp stops
234
+ happening and the second tap is whatever is next door. Measured on this
235
+ repository's fixtures: with the gutter, 0 to 480 channel samples of 7 to 21
236
+ million differ and the worst difference is **1**; with `--padding 0` it is 22,000
237
+ to 60,000 samples and a worst difference of **77**.
238
+
239
+ ⚠️ **That residual `1` is arithmetic, not resampling, and it is worth knowing
240
+ where it comes from.** A packed region's UVs are `x / pageWidth` rather than
241
+ `0..1`, which is one more rounding step in the sampling coordinate, so the
242
+ interpolation weight can differ in its last bit and a `Math.round` sitting exactly
243
+ on a `.5` boundary can then land the other way. It is bounded at one least
244
+ significant bit of one channel and it never reads a different texel. On real art
245
+ it usually does not appear at all: eleven of the thirteen rigs in this repository
246
+ render **byte-identically** packed and unpacked across 1,101 frames, and the two
247
+ that do not are the two with meshes, at 1 and 146 samples of 6 and 60 million.
248
+
249
+ ### 0.2 Building against a pack somebody else made — `--atlas-in`
250
+
251
+ The other direction. `--atlas-in <file.atlas>` resolves every part against the
252
+ **regions** of an existing pack instead of against loose PNGs.
253
+
254
+ ```bash
255
+ bun cli.ts build --rig … --motion … --out spine --atlas-in art/hero.atlas
256
+ # .. atlas-in /abs/path/art/hero.atlas
257
+ # .. torso 98x180 <- ../art/hero.png @ 363,209
258
+ ```
259
+
260
+ The join key is the region **name**, which rigc already equates with the PNG
261
+ basename everywhere else — so a rig written against loose parts resolves against
262
+ a pack of the same parts with no edit. Geometry (`bounds`/`offsets`/`rotate`)
263
+ comes off the file, and a part's width and height are the region's
264
+ `originalWidth`/`originalHeight` **divided by the page's `scale:`**: the untrimmed
265
+ drawing at its own size, which is what an attachment's size means.
266
+
267
+ ⚠️ **That division is load-bearing, and it is not exact.** A page declaring
268
+ `scale: 0.5` holds texels half the size of the drawings it was packed from — the
269
+ line says so, and an attachment's `width` is in world units, which the runtime
270
+ reads straight out of the skeleton JSON with the atlas nowhere in the expression.
271
+ So the scale has to be undone here or never (issue #267). But the packer wrote
272
+ `round(drawing × scale)`, so a 373-texel region at `scale: 0.5` is consistent with
273
+ a 745- and a 746-pixel drawing and the file does not say which: an imported size
274
+ is right to within `0.5 / scale` source pixels, and the build report prints the
275
+ texel count beside it so both numbers are visible:
276
+
277
+ ```bash
278
+ # .. pendulum 746x212 <- ../export/atlas.png @ 2,2 scale 0.5 (373x106 texels)
279
+ ```
280
+
281
+ ⇒ **If the size has to be exact, supply the loose art** — the default route
282
+ measures the PNG. Reach for `--atlas-in` when the pack is what you were handed, or
283
+ when drawing through the pack's own texels is the point.
284
+
285
+ The emitted `skeleton.atlas` **is** the imported one, verbatim except for its page
286
+ name lines, which are paths and have to be re-anchored to `--out`. Fields rigc
287
+ does not re-serialise (`format:`, `repeat:`, and `scale:` itself) survive the trip
288
+ because the text passes through by line; regions the rig does not use stay in the
289
+ file, because a real pack is shared between cuts and an importer that quietly
290
+ dropped half of one would make `--out` disagree with the pack it was built from.
291
+
292
+ Four things are refused rather than warned about, because each of them otherwise
293
+ **loads clean and draws wrong**:
294
+
295
+ | What | Why it cannot be a warning |
296
+ | --- | --- |
297
+ | a region name the atlas does not have | `AtlasAttachmentLoader` returns null and the part silently does not draw. The refusal lists the near misses — the usual cause is one character |
298
+ | a size the spec disagrees with | the same silence `A06` exists for, one link earlier: a quad sized against a region of another size collapses |
299
+ | a page the atlas names and the disk lacks | nothing to sample; caught on the way in, so the message names the atlas rather than the artifact rigc wrote from it |
300
+ | a rectangle that runs off its page | `x + width` past the page width makes `u2 > 1`, which samples whatever the wrap mode does |
301
+
302
+ Two limits, stated rather than discovered:
303
+
304
+ - an **optional state** (a manifest `states:` entry) whose region is not in the
305
+ pack is a `DROP`, not a refusal — the same documented absence a missing PNG has
306
+ always been, and the line names the atlas rather than a file nobody opened;
307
+ - a generator that **measures a part's pixels** — the `contour` mesh — lifts the
308
+ drawing back off the page, and refuses by name on a region packed `rotate: 90`.
309
+ Supply the loose PNG for that part instead. rigc's own packs never rotate, so
310
+ only a foreign pack reaches it.
311
+
312
+ `--pack` and `--atlas-in` are opposite directions through the same door and are
313
+ refused together.
314
+
165
315
  The other commands:
166
316
 
167
317
  ```bash
@@ -519,12 +669,44 @@ is weighted; a generated mesh binds only bones that move it) do not apply to one
519
669
  #44; before it was fixed, `A21` reported 40 failures on a correct 40-vertex editor
520
670
  mesh because an absent `meshKinds` entry read as `ring`.
521
671
 
672
+ ⭐ **Coverage is the exception, and it is reported for an authored mesh too.** Those
673
+ rules are about a mesh's **structure** — where its rim is, how its rows pair —
674
+ which rigc cannot know about geometry it did not build. Coverage is a measurement
675
+ between two things it has in front of it: the emitted triangles, and the PNG the
676
+ attachment names with `image`. So any mesh that names one gets the figure on its
677
+ `MESH` line, authored or generated:
678
+
679
+ ```
680
+ MESH ball authored 9 vertices / 8 triangles (budget 8) bones=[ball] attachments=[ball] covers 94.31% of the art, reaching 2.50px past it
681
+ ```
682
+
683
+ **A number, not a bar.** A `contour` under 99.5% is *refused* because rigc
684
+ generated that geometry as a claim about the art; an authored mesh that sits inside
685
+ its art is a legitimate thing to draw — a soft feather, a trimmed hull, a mesh
686
+ meant to bend a core while its edges stretch — so the figure informs and the
687
+ decision stays with the author. A mesh with no `image` reports nothing, because
688
+ there is nothing to measure it against. The silence was worth closing: the line
689
+ above is a round part meshed as a centre vertex plus 8 rim vertices placed on the
690
+ silhouette, and an octagon's sides pass `R · cos(π/8)` from its centre, so 5.7% of
691
+ the drawing — its whole ink outline, between the spokes — was not going to be
692
+ drawn, and every assertion passed (issue #277).
693
+
522
694
  The generators are `ring`, `ribbon` and `contour` (see
523
695
  [`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
524
696
  than a table of numbers, which is why they are code invoked by data. A generator
525
697
  is for a skeleton with **no** manifest; a cut that has one invokes the same
526
698
  builders through the manifest's `mesh` block.
527
699
 
700
+ 🚨 **A rig that invokes a generator must declare `invariants.meshSlots` (§3.7).**
701
+ Geometry rigc built is geometry rigc will not ship **unmeasured**: a generated
702
+ mesh counts against that budget, a rig that declares none has a budget of
703
+ **zero**, and the build is refused before the gate —
704
+ `1 mesh slot(s) emitted but the rig "hello" allows 0`. Declare `meshTriangles`
705
+ beside it: without it `A13_MESH_BUDGET` has nothing to measure against and SKIPs,
706
+ and the `MESH` report line has no budget to print. Authored geometry is exempt in
707
+ the other direction and for the same reason — rigc did not draw it, so leaving it
708
+ unmeasured is the author's call (issue #274).
709
+
528
710
  ⭐ **The no-manifest path centres the part window on its own slot bone.** There is
529
711
  no crop to flip against, so `size` (or, for a contour, the PNG's own size) is
530
712
  placed with its centre on the bone the slot names — which is also exactly where a
@@ -540,18 +722,31 @@ vertices to push with a `deform` timeline (§4.11) at the places the silhouette
540
722
  actually is, instead of at four corners.
541
723
 
542
724
  It takes **no geometry and no size**: the shape is traced off the attachment's own
543
- `image`, so there is no number here that can disagree with the pixels.
725
+ `image`, so there is no number here that can disagree with the pixels. The rig
726
+ header still budgets for it, which is the `invariants` block below and not a
727
+ detail of the attachment.
544
728
 
545
729
  ```json
546
- "cape": {
547
- "cape": {
548
- "type": "mesh",
549
- "image": "cape.png",
550
- "generator": { "kind": "contour", "tolerance": 1.5, "margin": 2, "maxVertices": 48 }
730
+ "invariants": { "meshSlots": 1, "meshTriangles": 64 },
731
+ "skins": {
732
+ "default": {
733
+ "cape": {
734
+ "cape": {
735
+ "type": "mesh",
736
+ "image": "cape.png",
737
+ "generator": { "kind": "contour", "tolerance": 1.5, "margin": 2, "maxVertices": 48 }
738
+ }
739
+ }
551
740
  }
552
741
  }
553
742
  ```
554
743
 
744
+ ⭐ **Both keys are the fragment.** Drop the two of them into §1.1's minimal rig in
745
+ place of its `skins`, rename its `box` bone and slot to `cape`, put a
746
+ transparent-margined `cape.png` in `images/`, and it compiles — `invariants`
747
+ included, because without it the generator is refused (issue #274). The
748
+ `meshTriangles` figure is invented; pick the one your renderer can afford.
749
+
555
750
  | Field | Meaning |
556
751
  | --- | --- |
557
752
  | `tolerance` | **required.** Douglas-Peucker tolerance, in part pixels. Bigger spends fewer vertices and cuts more corners |
@@ -572,7 +767,18 @@ the raw outline encloses every art pixel *whole*), simplify it, push it out by
572
767
  against the mask it came from**. `build` and `explain` print what it measured:
573
768
 
574
769
  ```
575
- MESH cape contour 15 vertices / 13 triangles (budget 80) bones=[cape] attachments=[cape] covers 100.00% of the art, reaching 3.16px past it
770
+ MESH cape contour 15 vertices / 13 triangles (budget 64) bones=[cape] attachments=[cape] covers 100.00% of the art, reaching 3.16px past it
771
+ ```
772
+
773
+ The budget in that line is the rig's `invariants.meshTriangles` — `(no budget
774
+ declared)` when it declares none, which is the same distinction `A13` SKIPs on. It
775
+ used to be the literal `80` whatever the rig said, so the line an author reads and
776
+ the assertion that measures could print two different numbers (issue #275). A part
777
+ whose outline encloses a hole says so too, because nothing else in the output
778
+ moves when one appears:
779
+
780
+ ```
781
+ MESH flag_b contour 48 vertices / 46 triangles (budget 96) bones=[flag_b] attachments=[flag_b] covers 100.00% of the art, reaching 2.00px past it, enclosing 848px of hole
576
782
  ```
577
783
 
578
784
  🚨 **It is geometry, not a deformation model.** Every vertex is pinned to the slot
@@ -590,7 +796,7 @@ mesh rather than a rim prefix, and `A28_RIBBON_ROWS_SHARE_WEIGHTS` **SKIPs** wit
590
796
  | --- | --- |
591
797
  | every pixel opaque | `every pixel of the 96x64 part reaches alpha 1, so its silhouette IS the part window and a contour mesh of it is a region attachment with extra vertices` |
592
798
  | two or more islands | `the art is 2 separate islands and one outline can only enclose the largest (529 of 989 px, 53.49%)` — one outline encloses one region; give each island its own slot |
593
- | a **hole** (a donut) | **accepted, and the hole is inside the mesh.** Ear clipping has no bridging step, so the outline is the art's *outer* boundary; those pixels draw nothing (their alpha is still 0) and the extra triangles are the whole cost. `explain` reports the coverage either way |
799
+ | a **hole** (a donut) | **accepted, and the hole is inside the mesh.** Ear clipping has no bridging step, so the outline is the art's *outer* boundary; those pixels draw nothing (their alpha is still 0) and the extra triangles are the whole cost. `build` and `explain` print the hole in pixels, because `coverage` and `overshoot` are both measured against the FILLED silhouette and neither of them moves when a part gains one |
594
800
  | a **diagonal pinch** — two parts of the art meeting at one pixel corner | `the alpha silhouette pinches to a single point at pixel corner (2,2) … one outline cannot pass through one point twice` |
595
801
  | 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` |
596
802
  | more outline than `maxVertices` | `the silhouette simplified to 15 vertices at tolerance 1.5, past the 4 this mesh allows` — refused, never silently decimated |
@@ -607,6 +813,9 @@ Self-intersection is refused; **holes are not cut out**; and nothing here does
607
813
  interior/Steiner points, so a contour mesh bends only where its outline has
608
814
  vertices.
609
815
 
816
+ 🖼️ **Worked example: [`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex)** — four contours over real art, with
817
+ the `tolerance`/`margin` sweep that picked their settings and what each one measured.
818
+
610
819
  **Bounding box** ([Spine: bounding boxes](http://esotericsoftware.com/spine-bounding-boxes))
611
820
  and **clipping** ([Spine: clipping](http://esotericsoftware.com/spine-clipping))
612
821
  attachments — a polygon, and nothing else.
@@ -832,6 +1041,9 @@ unresolved `spacingMode` fails the `=== Length` test and spaces bones as though
832
1041
  `Fixed` had been asked for; an unresolved `rotateMode` is neither `Tangent` nor
833
1042
  `ChainScale`, so bones follow the curve and never turn along it.
834
1043
 
1044
+ 🖼️ **Worked example: [`gallery/ride`](https://github.com/firejune/rigc/tree/main/gallery/ride)** — a trolley on a drawn rail, moved
1045
+ by a `position` timeline, with `groups` + `stagger` keying the wheels.
1046
+
835
1047
  #### 3.5.2 `slider` — a value that drives an animation
836
1048
 
837
1049
  **When you need one:** a pose that has to be driven by a value instead of by time —
@@ -919,9 +1131,9 @@ is not an array. Every field is optional and each is the payload a firing
919
1131
 
920
1132
  ### 3.7 `invariants` — what the artifact cannot say about itself
921
1133
 
922
- Optional, and only meaningful for rigc's own formations: `meshSlots` and
923
- `meshTriangles` (the two halves of the mesh budget `A13` measures against),
924
- `axisBone`, `massBone`, `detached`. Nothing in skeleton JSON records that a
1134
+ Optional with one exception, and only meaningful for rigc's own formations:
1135
+ `meshSlots` and `meshTriangles` (the two halves of the mesh budget `A13` measures
1136
+ against), `axisBone`, `massBone`, `detached`. Nothing in skeleton JSON records that a
925
1137
  bone carries a cut's axis or that a parentage is forbidden, so the rig spec says it
926
1138
  and the validator's archetype assertions read it. **An assertion whose field is
927
1139
  absent reports SKIP, never a pass.** If you are reproducing a foreign skeleton,
@@ -929,6 +1141,15 @@ leave this out entirely and run `--profile spine` — and expect `PROF` rather t
929
1141
  that SKIP, because the profile excludes an archetype assertion before its body
930
1142
  could notice the missing field (§5.2).
931
1143
 
1144
+ 🚨 **The exception: `meshSlots` is required by a rig that invokes a mesh
1145
+ generator** (`ring`, `ribbon`, `contour` — §3.4), and it is a **compile-time**
1146
+ refusal rather than an assertion. Undeclared means a budget of zero, so the build
1147
+ stops before the gate with `N mesh slot(s) emitted but the rig "X" allows 0`.
1148
+ Geometry rigc built is geometry rigc will not ship unmeasured; geometry the author
1149
+ drew is exempt, because rigc did not draw it. So `A13`'s **SKIP** means *this rig
1150
+ is unmeasured*, not *this budget is inert* — those are two code paths with one
1151
+ name, and reading the SKIP as the whole story is what issue #274 was.
1152
+
932
1153
  ---
933
1154
 
934
1155
  ## 4. The motion spec, field by field
@@ -1219,13 +1440,25 @@ here was measured off a real rig. Copy the shape, not the values.
1219
1440
  | --- | --- | --- |
1220
1441
  | `mix` | 0..1, how much of the solved rotation is applied | `1` |
1221
1442
  | `softness` | distance from full reach at which the bones stop straightening | `0` |
1222
- | `bendPositive` | two-bone bend direction | `true` |
1223
- | `compress` | one-bone IK scales the bone down to reach a close target | `false` |
1224
- | `stretch` | scales the bone up to reach a far target | `false` |
1443
+ | `bendPositive` | two-bone bend direction | **the rig's** (`true` if the rig says nothing) |
1444
+ | `compress` | one-bone IK scales the bone down to reach a close target | **the rig's** (`false` if the rig says nothing) |
1445
+ | `stretch` | scales the bone up to reach a far target | **the rig's** (`false` if the rig says nothing) |
1225
1446
 
1226
1447
  - **`mix` and `softness` are the two curve channels, in that order.** A raw
1227
1448
  `curve` is therefore 8 numbers, and the three booleans are stepped by nature —
1228
1449
  nothing interpolates a bend direction.
1450
+ - 🚨 **The three booleans are the one place a key's absent value is the RIG's and
1451
+ not the format's.** The parser reads them twice with the same defaults — once on
1452
+ the constraint (`SkeletonJson:155`) and once on **every timeline key**
1453
+ (`:912`) — so a key that omits `bendPositive` does not inherit the constraint's
1454
+ value, it asserts `true`. A rig declaring `bendPositive: false` under a timeline
1455
+ that keys only `mix` therefore bent the *other* way for the whole animation,
1456
+ with the field still in the file and inert: four builds differing only in those
1457
+ values posed one pose, and the gate was green throughout (issue #273). rigc now
1458
+ stamps the rig's value onto every emitted key, so the declaration reaches the
1459
+ runtime. **Stating a flag on every key still overrides the rig** — the format
1460
+ keys them per key on purpose, a bend that flips partway through is a real thing
1461
+ to write, and it is what the editor's own export does.
1229
1462
  - `mix` outside `0..1` is a compile error: `IkConstraintPose.mix` is documented as
1230
1463
  a percentage. A **transform** mix is documented *unbounded*, which is why §4.10
1231
1464
  has no such rule — the asymmetry is the runtime's, not ours.
@@ -1254,6 +1487,10 @@ consumer's process, which is late. `A34_CONSTRAINT_TIMELINE_TARGETS` checks the
1254
1487
  same two from the other side, plus one thing the compiler cannot produce and a
1255
1488
  hand-edited file can: an **empty key array**, which the parser skips in silence.
1256
1489
 
1490
+ 🖼️ **Worked example: [`gallery/walk`](https://github.com/firejune/rigc/tree/main/gallery/walk)** — two mirrored two-bone leg chains
1491
+ whose `mix`, `softness` and `bendPositive` are keyed through a stance and a swing,
1492
+ with the README's table of what each key is for.
1493
+
1257
1494
  ### 4.10 `transform` — turning a muted transform constraint on
1258
1495
 
1259
1496
  Same shape as §4.9 and the same absent-means-default rule; six mixes instead of
@@ -1405,10 +1642,23 @@ The overrun is the one to fear. `readAnimation` copies with
1405
1642
  JavaScript** — no throw, no warning, no `NaN`. A run one pair too long loses its
1406
1643
  tail and deforms the rest of the mesh correctly, which looks nearly right, and
1407
1644
  "nearly right" is the hardest kind of wrong to find.
1408
- `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` checks all of it from the other side, on the
1645
+ `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` checks the overrun from the other side, on the
1409
1646
  emitted file, measuring the array's length from the attachment rather than assuming
1410
1647
  an encoding.
1411
1648
 
1649
+ 📌 **The two parity rows above are an AUTHORING rule and not a validity one, and
1650
+ the distinction is deliberate** (issue #262). Nothing in the runtime aligns a run
1651
+ to a pair — `arrayCopy` copies at the raw index and the `deform[i] += vertices[i]`
1652
+ after it walks the whole array — so a run may legitimately begin and end mid-pair,
1653
+ which is what an editor's trimmed delta run looks like. In *this* spec an odd
1654
+ `offset` is a typo with a better spelling (`fromVertex`), so it is refused here,
1655
+ where the remedy is a line you own. `A35` does **not** refuse it: it is pointed at
1656
+ other people's files, and a rule stricter than the runtime tells its reader to go
1657
+ and break correct data.
1658
+
1659
+ 🖼️ **Worked example: [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash)** — a 9-vertex ball squashed about
1660
+ its contact point, with the two affine transforms its keys were derived from.
1661
+
1412
1662
  ---
1413
1663
 
1414
1664
  ### 4.12 `path` and `slider` timelines — tracks, not their own groups
@@ -1483,6 +1733,7 @@ the frequent ones, verbatim:
1483
1733
  | `key times must strictly increase (at t=…)` | including after `lag` and `stagger` |
1484
1734
  | `animation "A" has two tracks on X.property; merge them into one track` | one timeline per target property |
1485
1735
  | `no stage size: give the rig spec a \`skeleton.width\`/\`skeleton.height\`` | §3.1 |
1736
+ | `N mesh slot(s) emitted but the rig "X" allows 0 — a mesh rigc GENERATED counts against \`invariants.meshSlots\`…` | §3.4 / §3.7 — a rig that invokes a mesh generator declares the budget; undeclared is zero. Add `"invariants": { "meshSlots": N, "meshTriangles": M }` |
1486
1737
  | `drawOrder at t=…: slot "X" is not one this rig emits` / `is offset twice in one key` / `puts it at N, outside the … emitted slots` | §4.7 |
1487
1738
  | `events at t=…: event "X" is not declared in the rig spec's "events" block` | declare it in the rig spec (§3.6), or fix the name |
1488
1739
  | `events: key times must not go backwards` | put the firings in time order (§4.8) |
@@ -1496,7 +1747,7 @@ the frequent ones, verbatim:
1496
1747
  | `ik constraint "X": key 0 names "softness" and key 1 (t=…) does not` | §4.9 — every key is read with its own default, so state the field on every key or on none |
1497
1748
  | `ik constraint "X" (t=…): mix is 1.5, outside 0..1` | §4.9 — an IK mix is a percentage; a transform mix is unbounded |
1498
1749
  | `deform …: the run starts at deform index 4 and is 6 long, which ends at 10; this attachment's deform array is 8 long` | §4.11 — shorten the run or move its start; the parser would drop the tail in silence |
1499
- | `deform … (t=…): offset 3 is odd` | §4.11 — the deform array is `x, y` pairs, so a run starts on an even index |
1750
+ | `deform … (t=…): offset 3 is odd` | §4.11 — in **this spec** a run starts on an even index, or names its vertex with `fromVertex`. Not a validity rule; the runtime allows either (issue #262) |
1500
1751
  | `deform …: "fromVertex" counts VERTICES, and this attachment is weighted … vertex 2 has 2 of them` | §4.11 — key the control bone, or write bind-space pairs and start with `offset` |
1501
1752
  | `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
1502
1753
  | `vertexCount is N, which is not a multiple of 3` | §3.4 — a path's vertices are knots and handles read in groups of three: `3(K + 1)` open, `3K` closed |
@@ -1551,7 +1802,7 @@ The report prints one line per assertion:
1551
1802
  | `A10_NO_NAN_AFTER_STEPPING` | both | stepping the animation produced a `NaN` pose. Look for a degenerate curve or a zero scale |
1552
1803
  | `A11_NO_CLIPPING_ATTACHMENTS` | renderer | a clipping attachment; the target renderer skips them silently |
1553
1804
  | `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
1554
- | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither |
1805
+ | `A13_MESH_BUDGET` | renderer | more mesh slots than the rig's `invariants.meshSlots`, or a mesh over its `invariants.meshTriangles`. Thin the mesh, or raise the budget in the rig spec. **SKIP** when the rig declares neither — which means *unmeasured*, not that the budget is inert: the same `meshSlots` is a **compile-time** refusal for rigc's own generators, before the gate (§3.7, issue #274) |
1555
1806
  | `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip |
1556
1807
  | `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone |
1557
1808
  | `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`) |
@@ -1573,7 +1824,7 @@ The report prints one line per assertion:
1573
1824
  | `A32_EVENT_KEYS_RESOLVE` | both | an event key fires a name the skeleton's `events` block does not declare, sits earlier in time than the key before it, or sets `volume`/`balance` on an event with no `audio` (§4.8). **SKIP** when no animation carries an event timeline |
1574
1825
  | `A33_VERTEX_ATTACHMENT_GEOMETRY` | both | a bounding box, clipping polygon or path whose `vertexCount` is missing or disagrees with its vertex array, a weighted run that decodes to the wrong number of vertices or an out-of-range bone index, a clipping `end` naming a slot the skeleton does not have, a path whose vertex count is not a multiple of 3, or a path `lengths` array that does not strictly increase (§3.4). **SKIP** when the skeleton carries none of the three |
1575
1826
  | `A34_CONSTRAINT_TIMELINE_TARGETS` | both | an `ik`, `transform`, `path` or `slider` timeline names a constraint the skeleton does not declare, names one of another type, or carries no keys at all (§4.9, §4.10, §4.12). The last is silent: the parser reads key 0, finds nothing, and skips the timeline. **SKIP** when no animation carries one |
1576
- | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, starts on an odd index, holds an odd count of numbers or a non-finite one, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. **SKIP** when no animation carries a deform timeline |
1827
+ | `A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` | both | a deform key's run runs past the end of the attachment's deform array, holds a non-finite number, has an empty key array, or names a skin/slot/attachment triple that does not resolve (§4.11). The overrun is the quiet one — the parser copies into a `Float32Array` and drops the tail. ⛔ It does **not** require pair alignment: the runtime has no such rule and a trimmed editor run legitimately starts and ends mid-pair (§4.11, issue #262). **SKIP** when no animation carries a deform timeline |
1577
1828
  | `A36_PATH_CONSTRAINT_EFFECTIVE` | both | a path constraint whose slot has no path attachment in any skin, one that constrains no bone, or one whose three mixes are all 0 at setup with no animation keying its `mix` (§3.5.1). The first is the quiet one: `update()` returns on its first line and the constraint reports mixes it never applies. **SKIP** when the skeleton declares no path constraint |
1578
1829
  | `A37_SLIDER_CONSTRAINT_EFFECTIVE` | both | a slider whose animation carries no timeline, one that loops a zero-length animation (the applied time is NaN), one driving off a bone at `scale: 0`, or one muted at setup with no animation keying its `mix` (§3.5.2). **SKIP** when the skeleton declares no slider |
1579
1830
  | `A38_SKIN_MEMBERS_ARE_SKIN_REQUIRED` | both | a bone or constraint a skin activates that is not `skinRequired` (the list changes nothing), or one that is `skinRequired` and no skin activates (it is never active). Two keys in two places, and only together do they mean "this belongs to that skin" (§3.4.1). **SKIP** when no skin activates anything and nothing is `skinRequired` |
@@ -1604,10 +1855,13 @@ says so, because a deferral without its reason is a wall rather than a work item
1604
1855
 
1605
1856
  Two more limits that are not errors but will shape what you can attempt:
1606
1857
 
1607
- - **No atlas packer and no atlas importer.** rigc emits **one part per page**: every
1608
- region covers its whole page, `pma: false`. To reproduce a skeleton whose art
1609
- ships as a packed atlas you either supply loose PNGs and let rigc build its own
1610
- atlas, or hand the packed atlas to `validate`/`bench` alongside the candidate.
1858
+ - **The atlas packer and importer exist and are narrow** (issue #4). `--pack`
1859
+ arranges the parts onto shared pages and `--atlas-in` resolves them against a
1860
+ pack somebody else made — **§0.1** and **§0.2** state what each does and, more
1861
+ usefully, what neither does: no trimming, no rotation, no scaling, and no
1862
+ `--profile spine-html` over a packed atlas. The default is still **one part per
1863
+ page** with `pma: false` and every region covering its whole page, and nothing
1864
+ about a build changes unless one of those flags is given.
1611
1865
  - **`sequence` timelines and `drawOrderFolder`** are walked by the validator (so
1612
1866
  `A05` checks their curves and `diff` counts their keys) and cannot be *written*:
1613
1867
  there is no motion-spec property for either. Everything a motion spec **can** key
@@ -2616,8 +2870,10 @@ that has nothing to do with your keys.
2616
2870
  🚨 **Part of your MAE is the texture, not the animation.** The reference frames are
2617
2871
  rendered through the example's **own packed atlas**, and a packed atlas may carry a
2618
2872
  `scale:` line — the ladder has one at `scale: 0.5`, whose 745x212 part is packed at
2619
- 373x106. rigc has no packer (**§6**), so a candidate built from the loose PNGs samples
2620
- a texture at twice that resolution and resamples every edge differently. The pixels are
2873
+ 373x106. A candidate samples the art at its own resolution and resamples every edge
2874
+ differently. That is equally true of a `--pack`ed candidate (**§0.1**): rigc's packer
2875
+ is lossless and writes no `scale:` line, so packing rearranges texels without
2876
+ resampling one and does not move this constant either way. The pixels are
2621
2877
  the same shape in the same place; they are filtered from a different source, and the
2622
2878
  difference lands on the outline of every part in every frame. It is a constant, it is
2623
2879
  invisible to `content`, `rms` and the `±2 px` refinement — a resampling difference is