spine-rigc 0.9.0 → 0.10.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 +25 -13
- package/cli.ts +313 -10
- package/docs/AUTHORING.md +174 -9
- package/docs/INGEST.md +1117 -0
- package/docs/MOTION.md +9 -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 +359 -52
- package/src/diff.ts +269 -1
- package/src/render.ts +87 -2
- package/src/types.ts +42 -1
- 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
|
|
@@ -1405,10 +1555,20 @@ The overrun is the one to fear. `readAnimation` copies with
|
|
|
1405
1555
|
JavaScript** — no throw, no warning, no `NaN`. A run one pair too long loses its
|
|
1406
1556
|
tail and deforms the rest of the mesh correctly, which looks nearly right, and
|
|
1407
1557
|
"nearly right" is the hardest kind of wrong to find.
|
|
1408
|
-
`A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` checks
|
|
1558
|
+
`A35_DEFORM_KEYS_FIT_THE_ATTACHMENT` checks the overrun from the other side, on the
|
|
1409
1559
|
emitted file, measuring the array's length from the attachment rather than assuming
|
|
1410
1560
|
an encoding.
|
|
1411
1561
|
|
|
1562
|
+
📌 **The two parity rows above are an AUTHORING rule and not a validity one, and
|
|
1563
|
+
the distinction is deliberate** (issue #262). Nothing in the runtime aligns a run
|
|
1564
|
+
to a pair — `arrayCopy` copies at the raw index and the `deform[i] += vertices[i]`
|
|
1565
|
+
after it walks the whole array — so a run may legitimately begin and end mid-pair,
|
|
1566
|
+
which is what an editor's trimmed delta run looks like. In *this* spec an odd
|
|
1567
|
+
`offset` is a typo with a better spelling (`fromVertex`), so it is refused here,
|
|
1568
|
+
where the remedy is a line you own. `A35` does **not** refuse it: it is pointed at
|
|
1569
|
+
other people's files, and a rule stricter than the runtime tells its reader to go
|
|
1570
|
+
and break correct data.
|
|
1571
|
+
|
|
1412
1572
|
---
|
|
1413
1573
|
|
|
1414
1574
|
### 4.12 `path` and `slider` timelines — tracks, not their own groups
|
|
@@ -1496,7 +1656,7 @@ the frequent ones, verbatim:
|
|
|
1496
1656
|
| `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
1657
|
| `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
1658
|
| `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 —
|
|
1659
|
+
| `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
1660
|
| `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
1661
|
| `deform …: slot "X" in skin "default" has no attachment "Y" (it has: …)` | §4.11 — fix the placeholder name |
|
|
1502
1662
|
| `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 |
|
|
@@ -1573,7 +1733,7 @@ The report prints one line per assertion:
|
|
|
1573
1733
|
| `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
1734
|
| `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
1735
|
| `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,
|
|
1736
|
+
| `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
1737
|
| `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
1738
|
| `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
1739
|
| `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 +1764,13 @@ says so, because a deferral without its reason is a wall rather than a work item
|
|
|
1604
1764
|
|
|
1605
1765
|
Two more limits that are not errors but will shape what you can attempt:
|
|
1606
1766
|
|
|
1607
|
-
- **
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1767
|
+
- **The atlas packer and importer exist and are narrow** (issue #4). `--pack`
|
|
1768
|
+
arranges the parts onto shared pages and `--atlas-in` resolves them against a
|
|
1769
|
+
pack somebody else made — **§0.1** and **§0.2** state what each does and, more
|
|
1770
|
+
usefully, what neither does: no trimming, no rotation, no scaling, and no
|
|
1771
|
+
`--profile spine-html` over a packed atlas. The default is still **one part per
|
|
1772
|
+
page** with `pma: false` and every region covering its whole page, and nothing
|
|
1773
|
+
about a build changes unless one of those flags is given.
|
|
1611
1774
|
- **`sequence` timelines and `drawOrderFolder`** are walked by the validator (so
|
|
1612
1775
|
`A05` checks their curves and `diff` counts their keys) and cannot be *written*:
|
|
1613
1776
|
there is no motion-spec property for either. Everything a motion spec **can** key
|
|
@@ -2616,8 +2779,10 @@ that has nothing to do with your keys.
|
|
|
2616
2779
|
🚨 **Part of your MAE is the texture, not the animation.** The reference frames are
|
|
2617
2780
|
rendered through the example's **own packed atlas**, and a packed atlas may carry a
|
|
2618
2781
|
`scale:` line — the ladder has one at `scale: 0.5`, whose 745x212 part is packed at
|
|
2619
|
-
373x106.
|
|
2620
|
-
|
|
2782
|
+
373x106. A candidate samples the art at its own resolution and resamples every edge
|
|
2783
|
+
differently. That is equally true of a `--pack`ed candidate (**§0.1**): rigc's packer
|
|
2784
|
+
is lossless and writes no `scale:` line, so packing rearranges texels without
|
|
2785
|
+
resampling one and does not move this constant either way. The pixels are
|
|
2621
2786
|
the same shape in the same place; they are filtered from a different source, and the
|
|
2622
2787
|
difference lands on the outline of every part in every frame. It is a constant, it is
|
|
2623
2788
|
invisible to `content`, `rms` and the `±2 px` refinement — a resampling difference is
|