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/README.md +41 -13
- package/cli.ts +349 -14
- package/docs/AUTHORING.md +280 -24
- package/docs/INGEST.md +1117 -0
- package/docs/MOTION.md +16 -0
- package/docs/SPEC_COVERAGE.md +9 -2
- package/package.json +2 -1
- package/src/atlas.ts +871 -0
- package/src/bonedist.ts +632 -0
- package/src/check.ts +5 -2
- package/src/compile.ts +485 -59
- package/src/diff.ts +269 -1
- package/src/mesh.ts +183 -22
- package/src/render.ts +88 -3
- package/src/types.ts +56 -4
- package/src/validate.ts +45 -22
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
|
-
"
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
"
|
|
550
|
-
|
|
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
|
|
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`
|
|
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:
|
|
923
|
-
`meshTriangles` (the two halves of the mesh budget `A13` measures
|
|
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
|
|
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 —
|
|
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,
|
|
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
|
-
- **
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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.
|
|
2620
|
-
|
|
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
|