spine-rigc 1.2.3 → 1.4.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 +2 -2
- package/cli.ts +124 -20
- package/docs/AUTHORING.md +471 -19
- package/docs/FACE.md +15 -0
- package/docs/INGEST.md +1 -1
- package/package.json +17 -1
- package/src/atlas.ts +151 -2
- package/src/compile.ts +335 -0
- package/src/mesh.ts +405 -1
- package/src/pose.ts +297 -10
- package/src/render.ts +376 -21
- package/src/rig.ts +140 -7
- package/src/types.ts +30 -0
- package/src/validate.ts +101 -24
package/docs/AUTHORING.md
CHANGED
|
@@ -200,6 +200,7 @@ What the flags mean:
|
|
|
200
200
|
| `--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. The default is one page per part — **§0.1** |
|
|
201
201
|
| `--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** |
|
|
202
202
|
| `--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** |
|
|
203
|
+
| `--page-edges` | `build --pack` only: `pot` (default) or `free`. `pot` keeps both page edges powers of two; `free` sizes the page to the parts — a smaller page, at the cost of region attachments sampling within one least significant bit of the loose build instead of exactly. Any other value is refused by name — **§0.1** |
|
|
203
204
|
| `--atlas-in` | `build` and `explain`: 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:`; `build` re-emits the atlas into `--out`, re-anchored, and `explain` writes nothing and poses through it — **§0.2**. On `explain` it is the flag that makes a **size-only** spec readable at all (`ingest --art none`), because posing resolves every attachment against an atlas; without it that pair is refused by name rather than thrown through (§5.1) |
|
|
204
205
|
| `--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. For `chainfit` it is only where each attachment's image name **resolves**: the candidate decides what the parts are, so extra PNGs are unused and a missing name is refused by name (§12.3) |
|
|
205
206
|
| `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
|
|
@@ -208,6 +209,7 @@ What the flags mean:
|
|
|
208
209
|
| `--profile` | `spine` = the 34 validity rules (**the default**) · `spine-html` = all 49, opt-in |
|
|
209
210
|
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
210
211
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
|
|
212
|
+
| `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
|
|
211
213
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
212
214
|
| `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
|
|
213
215
|
| `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
|
|
@@ -313,7 +315,35 @@ a gap:
|
|
|
313
315
|
`RegionAttachment.computeUVs` assigns a different corner order at 90, so a
|
|
314
316
|
rotated pack is one rigc's own `--atlas` substitution cannot read.
|
|
315
317
|
Rotation buys page area; a page that runs out of room spills to a second page
|
|
316
|
-
instead.
|
|
318
|
+
instead. Measured on two painting rigs (issue #860), a pack that turned 12
|
|
319
|
+
regions `rotate: 90` lifted every region back byte for byte and drew its
|
|
320
|
+
region attachments with 0 differing pixels — and made neither page smaller,
|
|
321
|
+
because both were already on the smallest power-of-two page their area allows.
|
|
322
|
+
Measured again on twelve region sets (issue #866: those two rigs and ten
|
|
323
|
+
production rigs of 20 to 22 parts), packing each with the same search twice —
|
|
324
|
+
as shipped, and with every part also tried turned under the same placement
|
|
325
|
+
score:
|
|
326
|
+
|
|
327
|
+
| Set | `pot` page | turned | `free` page | turned |
|
|
328
|
+
| --- | --- | --- | --- | --- |
|
|
329
|
+
| 22-part painting | 1024x2048 | 0.00 % | 1888x697 | −1.14 % |
|
|
330
|
+
| 20-part painting | 512x2048 | 0.00 % | 480x1166 | +2.31 % |
|
|
331
|
+
| P1 | 512x2048 | 0.00 % | 416x1633 | +1.43 % |
|
|
332
|
+
| P2 | 1024x1024 | 0.00 % | 1184x810 | +0.70 % |
|
|
333
|
+
| P3 | 512x1024 | 0.00 % | 480x1000 | +2.96 % |
|
|
334
|
+
| P4 | 1024x2048 | 0.00 % | 864x1346 | −6.16 % |
|
|
335
|
+
| P5 | 2048x2048 | 0.00 % | 1344x1768 | −13.37 % |
|
|
336
|
+
| P6 | 1024x2048 | 0.00 % | 800x1934 | −1.62 % |
|
|
337
|
+
| P7 | 512x2048 | 0.00 % | 1024x928 | −0.05 % |
|
|
338
|
+
| P8 | 512x2048 | 0.00 % | 608x1250 | −4.96 % |
|
|
339
|
+
| P9 | 1024x2048 | +100.00 % | 1024x2037 | +6.51 % |
|
|
340
|
+
| P10 | 512x2048 | 0.00 % | 352x1433 | +0.70 % |
|
|
341
|
+
|
|
342
|
+
Under `pot` turning shrank no page. Under `free` the largest gain, 13.37 %, is
|
|
343
|
+
inside the search's own noise: the neighbouring width on the 32-pixel grid
|
|
344
|
+
moves that set's unturned page by 18.13 %, and trying every width instead
|
|
345
|
+
finds an unturned page 10.22 % smaller. With every width tried, no set gains
|
|
346
|
+
more than 5.05 % from turning — so the packer still never turns a region.
|
|
317
347
|
- **no re-ordering of anything the skeleton says.** `skeleton.json` from a packed
|
|
318
348
|
build is **byte-identical** to the unpacked one, because sizes are still
|
|
319
349
|
measured from the loose PNGs and packing is an output arrangement.
|
|
@@ -348,6 +378,38 @@ it usually does not appear at all: eleven of the thirteen rigs in this repositor
|
|
|
348
378
|
render **byte-identically** packed and unpacked across 1,101 frames, and the two
|
|
349
379
|
that do not are the two with meshes, at 1 and 146 samples of 6 and 60 million.
|
|
350
380
|
|
|
381
|
+
**`--page-edges free`: a page sized to the parts.** The default, `pot`, tries
|
|
382
|
+
every power-of-two pair up to `--page-size` in order of area and writes the first
|
|
383
|
+
the pack fits — it never doubles past a smaller page that would do. So when the
|
|
384
|
+
parts' own area already exceeds the next power-of-two page down, no `pot` packer
|
|
385
|
+
can do better, and `free` is the lever left. Under `free` the candidate widths
|
|
386
|
+
are the multiples of 32 from the widest padded part up to `--page-size`; at each
|
|
387
|
+
width the parts are placed by the same MaxRects pass on a page `--page-size`
|
|
388
|
+
tall, and the height is the bottom edge of the lowest part. The page with the
|
|
389
|
+
least area wins, then the squarer one (smaller |width − height|), then the
|
|
390
|
+
narrower one — every candidate has a different width, so the order is total and
|
|
391
|
+
two builds of the same parts write the same bytes. Spilling, the packing order
|
|
392
|
+
and `--page-size` as the ceiling are the same as under `pot`.
|
|
393
|
+
|
|
394
|
+
The default stays `pot` because every runtime accepts a power-of-two page and
|
|
395
|
+
the editor's own packer writes one by default: 9 of the 10 atlases in the
|
|
396
|
+
fetched `examples/` corpus are power-of-two on both edges. What `free` costs is
|
|
397
|
+
the exactness above. `x / pageWidth` is no longer exact, so a **region**
|
|
398
|
+
attachment's samples join a mesh's at one least significant bit against the
|
|
399
|
+
loose build instead of 0. No region's bytes change: every one lifts back byte for
|
|
400
|
+
byte, as under `pot`. Observed on two painting rigs of 22 and 20 parts:
|
|
401
|
+
|
|
402
|
+
| Rig | `pot` page | `free` page | Area | Covered | Opaque (alpha > 0) | Render vs loose, region attachments only | Render vs loose, whole rig |
|
|
403
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
404
|
+
| 22 parts | 1024x2048 = 2,097,152 | 1888x697 = 1,315,936 | −37.3 % | 56.3 % → 89.7 % | 36.8 % → 58.7 % | 0 → 2,323 px, worst 1 | 3,854 → 5,216 px, worst 1 |
|
|
405
|
+
| 20 parts | 512x2048 = 1,048,576 | 480x1166 = 559,680 | −46.6 % | 49.7 % → 93.2 % | 28.7 % → 53.8 % | 0 → 527 px, worst 1 | 1,260 → 1,669 px, worst 1 |
|
|
406
|
+
|
|
407
|
+
The pixel counts are over 49 frames at `render --max 1024`. **Covered** is the
|
|
408
|
+
regions' own rectangles over the page, which the pack line prints; **opaque** is
|
|
409
|
+
the page's texels with any alpha. The gap between the two is transparency
|
|
410
|
+
inside the parts, which no packer that copies bytes can remove. The pack line
|
|
411
|
+
ends `, page edges free` when the flag is set.
|
|
412
|
+
|
|
351
413
|
### 0.2 Building against a pack somebody else made — `--atlas-in`
|
|
352
414
|
|
|
353
415
|
The other direction. `--atlas-in <file.atlas>` resolves every part against the
|
|
@@ -791,6 +853,7 @@ bun cli.ts check --candidate path/to/spine --frames path/to/frames [--skin
|
|
|
791
853
|
bun cli.ts bench 3 --candidate path/to/spine [--frames path/to/frames]
|
|
792
854
|
bun cli.ts render --candidate path/to/spine [--animation …] [--skin …] [--fps 12] [--max 256]
|
|
793
855
|
bun cli.ts render --candidate path/to/spine --slot <name,…> | --hide <name,…> # part of the rig, same grid
|
|
856
|
+
bun cli.ts render --candidate path/to/spine --geometry # + geometry.json per set: the numbers the frames were drawn from
|
|
794
857
|
bun cli.ts preview --candidate path/to/spine [--candidate path/to/another …] [--animation …] [--out preview.html]
|
|
795
858
|
bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
|
|
796
859
|
bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
|
|
@@ -814,8 +877,9 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
|
814
877
|
`build --atlas-in` gates green is readable here through the same flag, and
|
|
815
878
|
without it the pair is refused by name at exit 2 rather than posed
|
|
816
879
|
(§5.1). ⚠️ `--profile`,
|
|
817
|
-
`--pack`, `--page-size`, `--padding` and `--copy-images` are
|
|
818
|
-
not here:
|
|
880
|
+
`--pack`, `--page-size`, `--padding`, `--page-edges` and `--copy-images` are
|
|
881
|
+
`build`'s and are not here: five of them decide what is *written*, and this
|
|
882
|
+
command writes nothing.
|
|
819
883
|
|
|
820
884
|
📐 **What it will not measure: the texels of a page that is not its declared
|
|
821
885
|
size.** Under
|
|
@@ -884,7 +948,11 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
|
884
948
|
in draw order; a slot whose art lives only under another skin is refused naming
|
|
885
949
|
that skin (pass `--skin`); the two flags together are refused. `frames.json`
|
|
886
950
|
records the subset as `slots` or `hidden`, and **`check` refuses such a set as
|
|
887
|
-
a reference** (§9). `
|
|
951
|
+
a reference** (§9). `--geometry` writes the numbers each frame was drawn from
|
|
952
|
+
beside it — every bone's world transform and every attachment's world vertices
|
|
953
|
+
after skinning, with their rest geometry — for a judgement the pixels cannot
|
|
954
|
+
make: a mesh triangle's stretch, a region read in its own bone's frame (§8.2).
|
|
955
|
+
`preview`
|
|
888
956
|
writes one self-contained `.html`: your skeleton, atlas and page PNGs are
|
|
889
957
|
embedded in it as data URIs and played by the official **Spine Web Player**, so
|
|
890
958
|
double-clicking it is also the interop proof — what plays there was played by
|
|
@@ -1703,13 +1771,15 @@ is the wrap-around curve from `x = 180` back to `x = 0`, which nothing reads. Th
|
|
|
1703
1771
|
§3.5.1's constraint example rides: with `position: 0.25` its `cart` bone poses at
|
|
1704
1772
|
`worldX = 45.000000`.
|
|
1705
1773
|
|
|
1706
|
-
The generators are `ring`, `ribbon`, `contour` and `
|
|
1774
|
+
The generators are `ring`, `ribbon`, `contour`, `grid` and `segments` (see
|
|
1707
1775
|
[`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
|
|
1708
|
-
than a table of numbers, which is why they are code invoked by data.
|
|
1709
|
-
are geometry: they pin every vertex to the slot bone and exist to give
|
|
1710
|
-
`deform` timeline (§4.12) somewhere to push.
|
|
1711
|
-
|
|
1712
|
-
|
|
1776
|
+
than a table of numbers, which is why they are code invoked by data. `contour`
|
|
1777
|
+
and `grid` are geometry: they pin every vertex to the slot bone and exist to give
|
|
1778
|
+
a `deform` timeline (§4.12) somewhere to push. `segments` is both at once — a
|
|
1779
|
+
lattice traced off the art, weighted by distance to the bones you name. A
|
|
1780
|
+
generator is for a skeleton with **no** manifest; a cut that has one invokes the
|
|
1781
|
+
same builders through the manifest's `mesh` block — except `segments`, which has
|
|
1782
|
+
no manifest spelling and is reached from a rig spec only.
|
|
1713
1783
|
|
|
1714
1784
|
🚨 **A rig that invokes a generator must declare `invariants.meshSlots` (§3.7).**
|
|
1715
1785
|
Geometry rigc built is geometry rigc will not ship **unmeasured**: a generated
|
|
@@ -2256,6 +2326,130 @@ influence through that bone's own inverse, so the **angle** a raised surface tur
|
|
|
2256
2326
|
through and the **impact** a soft one answers sit on one slot. ⚠️ Read §4.11.1 before you add a mask to a part that already turns:
|
|
2257
2327
|
past one bone the model's own coordinates are **world** ones, so `radius` and
|
|
2258
2328
|
`about` change units.
|
|
2329
|
+
#### `segments` — a lattice over the art, weighted by distance to the bones you name
|
|
2330
|
+
|
|
2331
|
+
**When you need one:** an arbitrary layer that a chosen set of bones should
|
|
2332
|
+
pull — a robe over a chest, a hip and two legs; a head of hair over a skull and
|
|
2333
|
+
three locks; a sleeve under an arm chain. None of the other generators fits
|
|
2334
|
+
that: a `ribbon` needs a strip, a `ring` an aperture, and `contour`/`grid` pin
|
|
2335
|
+
everything to one bone. Without this, every pipeline weights such a layer
|
|
2336
|
+
itself and transcribes the result into authored `weights`.
|
|
2337
|
+
|
|
2338
|
+
⭐ **Its inputs are the one decision, and everything else is derived.** What
|
|
2339
|
+
you decide is **which segments may pull the part**. The lattice is read off
|
|
2340
|
+
the attachment's own `image`, each segment off the skeleton's setup pose, and
|
|
2341
|
+
every weight off the distance between a vertex and those segments — so the
|
|
2342
|
+
only thing in this block that is a judgement is `bones`, and the numbers
|
|
2343
|
+
beside it say how a distance becomes a share.
|
|
2344
|
+
|
|
2345
|
+
```json
|
|
2346
|
+
"invariants": { "meshSlots": 1, "meshTriangles": 200 },
|
|
2347
|
+
"skins": {
|
|
2348
|
+
"default": {
|
|
2349
|
+
"robe": {
|
|
2350
|
+
"robe": {
|
|
2351
|
+
"type": "mesh",
|
|
2352
|
+
"image": "robe.png",
|
|
2353
|
+
"generator": {
|
|
2354
|
+
"kind": "segments",
|
|
2355
|
+
"cell": 24,
|
|
2356
|
+
"bones": ["chest", ["skirt_0", "skirt_1", "skirt_2"],
|
|
2357
|
+
{ "bone": "hip", "from": [20, 180], "to": [220, 180] }],
|
|
2358
|
+
"falloff": { "power": 2, "radius": 10, "maxBones": 4, "minWeight": 0.03 },
|
|
2359
|
+
"anchor": [120, 96]
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
```
|
|
2366
|
+
|
|
2367
|
+
| Field | Meaning |
|
|
2368
|
+
| --- | --- |
|
|
2369
|
+
| `cell` | **required.** The lattice's square, in the part's pixels — a whole number of at least 1. No default: it is a length on this art, and how fine a mesh the part needs is not something rigc can read off it |
|
|
2370
|
+
| `bones` | **required, at least one entry.** Which segments may pull the part. An entry is one of three forms, below |
|
|
2371
|
+
| `falloff` | **required**, because `radius` is: `{ "power"?, "radius", "maxBones"?, "minWeight"? }` |
|
|
2372
|
+
| `radius` | **required.** Added to every distance before the power is taken, in the part's pixels, so a vertex ON a segment has a finite pull and the blend between two segments is about this wide. Positive |
|
|
2373
|
+
| `power` | the exponent a pull falls off by: `w = 1 / (d + radius)^power`. Positive. Default `2` |
|
|
2374
|
+
| `maxBones` | at most this many bones pull one vertex — the strongest ones. A whole number of at least 1. Default `4` |
|
|
2375
|
+
| `minWeight` | a share under this is dropped, and the kept shares normalised again. In `0.000001`..`1`, 1 excluded. Default `0.03` |
|
|
2376
|
+
| `alpha` | the alpha at or above which a pixel counts as art, `1`..`255`. Default `1` — `contour`'s own |
|
|
2377
|
+
| `anchor` | where the **slot bone** sits in the part's pixels, `[x, y]`, y down. Default the window's centre — the placement every generator on this route uses (the ⭐ above). State it when the art is not drawn centred on its bone: unlike a pinned generator, this one's weights depend on where the art is relative to the bones |
|
|
2378
|
+
|
|
2379
|
+
**The three forms of a `bones` entry**, each resolved by name against the rig:
|
|
2380
|
+
|
|
2381
|
+
| Entry | Its segment(s) |
|
|
2382
|
+
| --- | --- |
|
|
2383
|
+
| `"chest"` — a bone | from the bone's origin to its **`length` tip**, along its setup rotation. A bone with no `length` has an origin and no segment, and is refused |
|
|
2384
|
+
| `["skirt_0", "skirt_1", "skirt_2"]` — a chain, root first | one per link: each link from its origin to the **next link's** origin, the last from its origin to its `length` tip. Each link must be a descendant of the one before it — the list is the chain's order, which is `ribbon`'s `chain` convention |
|
|
2385
|
+
| `{ "bone": "hip", "from": [x, y], "to": [x, y] }` — a span | exactly that segment, in the part's pixels, y down, pulling for `bone`. For a line no bone's own span describes — a hip pulling across a waist, or two meshes over one bone that each want it along a different line. `from` equal to `to` is a point |
|
|
2386
|
+
|
|
2387
|
+
A bone named by two entries keeps the **stronger** of its pulls, and ties
|
|
2388
|
+
between bones keep the order they are first named in.
|
|
2389
|
+
|
|
2390
|
+
**What it does, in order:** a lattice of `cell`-pixel squares over the part,
|
|
2391
|
+
the last column and row clipped to the image; a cell is kept when any pixel in
|
|
2392
|
+
it reaches `alpha`, so the triangles cover **every** art pixel. Then the kept
|
|
2393
|
+
cells are made **one closed outline**: holes filled, every other island joined
|
|
2394
|
+
to the largest by a straight run of cells, every diagonal pinch filled,
|
|
2395
|
+
repeated until a pass changes nothing. That is the case the *"the triangles'
|
|
2396
|
+
outline is not one closed loop"* refusal (above) would otherwise fire on — a
|
|
2397
|
+
layer the alpha splits in two is still one attachment — and the cells the join
|
|
2398
|
+
adds hold no art and draw nothing. Two triangles per cell, the diagonal
|
|
2399
|
+
alternating so the lattice has no preferred shear, the outline walked first
|
|
2400
|
+
(`hull` is the outline's vertex count), and the uvs are the lattice points over
|
|
2401
|
+
the image size. Then, per vertex, `1 / (d + radius)^power` to the nearest point
|
|
2402
|
+
of each segment, the strongest `maxBones` kept and normalised, any share under
|
|
2403
|
+
`minWeight` dropped, the rest normalised again — so every vertex closes at 1 by
|
|
2404
|
+
construction. The emitted mesh is a weighted one, bound by name.
|
|
2405
|
+
|
|
2406
|
+
`build` and `explain` print what it measured, under the mesh's line. This is
|
|
2407
|
+
the selftest's own `segments_probe` (`fixtures/public.ts` in the repository) —
|
|
2408
|
+
one 96x64 plate of two islands under two two-link chains, `cell: 8`,
|
|
2409
|
+
`radius: 4`, `maxBones: 3` — as `build` prints it:
|
|
2410
|
+
|
|
2411
|
+
```
|
|
2412
|
+
MESH cloth segments 72 vertices / 100 triangles (budget 200) bones=[left_0, left_1, right_0, right_1] attachments=[cloth] covers 100.00% of the art, reaching 8.00px past it
|
|
2413
|
+
influence max 3 bone(s) per vertex, mean 2.61, 4 of 72 vertices (5.56%) on a single bone
|
|
2414
|
+
lattice cell 8px, 12x8 cells, 48 with art, 50 kept (2 island(s) joined into one outline, 2 cell(s) added that hold no art)
|
|
2415
|
+
```
|
|
2416
|
+
|
|
2417
|
+
The `influence` line is the falloff's report: the most bones any vertex binds
|
|
2418
|
+
(read it against `maxBones`), the mean, and how many vertices one bone owns
|
|
2419
|
+
outright. A named bone that **no** vertex binds is listed there by name — it is
|
|
2420
|
+
in no weight, so no other figure moves — and under `--profile spine-html`
|
|
2421
|
+
`A20_MESH_WEIGHTS_COHERENT` refuses it as a declared bone nothing binds. The
|
|
2422
|
+
`lattice` line says how many cells had art and how many were kept, and when
|
|
2423
|
+
islands were joined, how many and how many cells the join added. The coverage
|
|
2424
|
+
figure is §3.4's authored-mesh one: 100% by construction, and the reach past
|
|
2425
|
+
the art is the lattice's own — up to a cell past the silhouette, and a join run
|
|
2426
|
+
across a gap (the 8.00px above is the two join cells).
|
|
2427
|
+
|
|
2428
|
+
🔸 **What the gate does with it.** `A20`'s coherence rules and both of its
|
|
2429
|
+
generator-policy branches apply in full. `A21_MESH_RIM_PINNED` **SKIPs** on a
|
|
2430
|
+
segments mesh, by name: its outline is weighted by distance like every other
|
|
2431
|
+
vertex, because a layer pulled by named bones is *supposed* to move at its
|
|
2432
|
+
edge, so "the rim is pinned to the slot bone" is not a claim it makes.
|
|
2433
|
+
`A28_RIBBON_ROWS_SHARE_WEIGHTS` skips it too — a lattice has cells, not cross
|
|
2434
|
+
rows.
|
|
2435
|
+
|
|
2436
|
+
**Stated limits, each a named refusal rather than a mesh that loads wrong:**
|
|
2437
|
+
|
|
2438
|
+
| The input | What you get |
|
|
2439
|
+
| --- | --- |
|
|
2440
|
+
| a bone the rig does not declare | `"bones"[0][1] names mesh bone "nobody", which is not in the rig's bone list` |
|
|
2441
|
+
| an empty `bones` | `the "segments" generator names no bones. "bones" is the one decision this generator takes …` |
|
|
2442
|
+
| a part whose alpha keeps no cell | `no pixel of the 96x64 part reaches alpha 1, so a lattice at cell 8 keeps no cell — there is no art to cover` |
|
|
2443
|
+
| `minWeight` at or above 1, or under `0.000001` | `"falloff.minWeight" is 1; … at 1 or above every share is dropped, and under 0.000001, one step of the 6-decimal grid weights are written on, a kept binding could be written as 0` |
|
|
2444
|
+
| `maxBones` under 1 | `"falloff.maxBones" is 0; it is how many bones may pull one vertex, a whole number of at least 1` |
|
|
2445
|
+
| a vertex every one of whose kept shares is under `minWeight` | `vertex 0, at (0, 0) in the part's pixels, keeps no bone — the 4 strongest pull(s) share it as … and every one is under "minWeight" 0.3` — **refused, not handed the largest.** A bone the falloff dropped is a weight nobody decided; the message names the fix, a `minWeight` at or under `1 / maxBones`, which the strongest share can never fall below |
|
|
2446
|
+
| a bone with no `length` named alone, or as the last link of a chain | `names bone "body", which has no "length", so it has an origin and no segment` — give it a `length`, put it inside a chain, or state a span |
|
|
2447
|
+
| a chain whose next link is not beneath it | `chains "right_0" to "left_1", and "left_1" is not under "right_0" in the bone tree` |
|
|
2448
|
+
| a chain link at its next link's origin | `… the two bones share their setup origin (…), so the link has no length and no direction to weight along` |
|
|
2449
|
+
| an empty chain, a span with a malformed end, an entry of none of the three forms | `is an empty chain`; `both ends are [x, y] in the part's pixels`; `an entry is a bone name, a chain as a list of bone names, or { "bone", "from", "to" }` |
|
|
2450
|
+
| no `falloff`, a `radius` or `power` at or below 0, a malformed `anchor`, a `cell` under 1, no `image` | each named by its field — `radius` has no default because it is a length on this art |
|
|
2451
|
+
| a part on a packed page that declares a `scale:` | refused: `cell` and `radius` are the part's pixels and this generator does not convert them to a page's texels. Build from the loose part, or a pack at scale 1 |
|
|
2452
|
+
|
|
2259
2453
|
#### 3.4.1 A skin that switches bones and constraints on
|
|
2260
2454
|
|
|
2261
2455
|
**When you need one:** a skin that is more than a change of art — a variant with an
|
|
@@ -2591,7 +2785,7 @@ that lists it — see §3.4.1, and note that the flag alone does nothing.
|
|
|
2591
2785
|
that is deliberate. Every key on a constraint object is a Spine field the emitter
|
|
2592
2786
|
writes; a statement to the gate about who turns the dial is what the artifact cannot
|
|
2593
2787
|
say about itself, so it lives in `invariants.consumerDrivenMix` (§3.7), beside
|
|
2594
|
-
`deformMayFold` —
|
|
2788
|
+
`deformMayFold` — another exemption from a named rule, which names its subject
|
|
2595
2789
|
the same way.
|
|
2596
2790
|
|
|
2597
2791
|
#### 3.5.1 `path` — bones that travel along a curve
|
|
@@ -3105,7 +3299,7 @@ is not an array. Every field is optional and each is the payload a firing
|
|
|
3105
3299
|
Optional with one exception, and only meaningful for rigc's own formations:
|
|
3106
3300
|
`meshSlots` and `meshTriangles` (the two halves of the mesh budget `A13` measures
|
|
3107
3301
|
against), `axisBone`, `massBone`, `detached`, `deformMayFold`, `editorRoundTrip`,
|
|
3108
|
-
`consumerDrivenMix`.
|
|
3302
|
+
`consumerDrivenMix`, `idleDrivesMeshes`.
|
|
3109
3303
|
Nothing in skeleton
|
|
3110
3304
|
JSON records that a
|
|
3111
3305
|
bone carries a cut's axis or that a parentage is forbidden, so the rig spec says it
|
|
@@ -3141,8 +3335,8 @@ loads and still animates and merely lies — something released into the world t
|
|
|
3141
3335
|
must not ride the part that released it. [RIGGING.md](RIGGING.md) §10.3 has a
|
|
3142
3336
|
worked one.
|
|
3143
3337
|
|
|
3144
|
-
🚨 **`deformMayFold` is one of the
|
|
3145
|
-
(`consumerDrivenMix`, below,
|
|
3338
|
+
🚨 **`deformMayFold` is one of the three fields here that turn a check OFF**
|
|
3339
|
+
(`consumerDrivenMix` and `idleDrivesMeshes`, below, are the others), so it is held to a shape that is
|
|
3146
3340
|
refused rather than skipped. It is
|
|
3147
3341
|
`[{ "slot": …, "why": … }]`, it exempts that slot from
|
|
3148
3342
|
`A39_DEFORM_KEEPS_TRIANGLE_WINDING`, and three shapes are compile errors: a slot
|
|
@@ -3216,6 +3410,37 @@ spelling of `gallery/look`'s rule that a face angle is a value rather than a tim
|
|
|
3216
3410
|
- What it buys is a **SKIP by name, never a pass** (§4.12 has the line). The
|
|
3217
3411
|
declaration is a statement to the gate and changes no emitted byte.
|
|
3218
3412
|
|
|
3413
|
+
🎨 **`idleDrivesMeshes` says this rig's `idle` deforms meshes on purpose.** It is
|
|
3414
|
+
`{ "why": … }`, and it switches off `A15_IDLE_NO_MESH_BONE_KEYS` (§5.2). That rule
|
|
3415
|
+
assumes meshes are **mostly static**: the `spine-html` renderer skips redrawing a
|
|
3416
|
+
mesh nothing moved, so an `idle` keying a mesh's bone spends that skip on every
|
|
3417
|
+
frame. A **painting rig** is the genre where that is false by design — one
|
|
3418
|
+
illustration split into layers, most of them weighted meshes over bone chains, and
|
|
3419
|
+
an `idle` whose job is to move them (hair, sleeves, breathing). Without the field,
|
|
3420
|
+
the only way through is a parent bone above every keyed bone with the keys moved
|
|
3421
|
+
onto it: on the first such rig that was 42 extra bones for an identical pose.
|
|
3422
|
+
|
|
3423
|
+
- Four shapes are compile errors, each naming `invariants.idleDrivesMeshes`: a
|
|
3424
|
+
missing `why`, a blank one, one that is not a string, and anything that is not
|
|
3425
|
+
an object (a bare `true` included, since it is a switch with no reason). A key
|
|
3426
|
+
other than `why` is refused like any unknown key.
|
|
3427
|
+
- What it buys is a **SKIP, never a pass**, and the SKIP states the cost —
|
|
3428
|
+
the keyed bones (all of them up to eight, then the first eight and `+k`), the
|
|
3429
|
+
mesh attachments they drive and the vertices those hold, counted from each mesh's
|
|
3430
|
+
loaded `worldVerticesLength`: `declared by the rig ("<why>"): idle keys N bone(s)
|
|
3431
|
+
that drive M mesh attachment(s) totalling V vertices — "a", "b" — and each of
|
|
3432
|
+
those meshes is recomputed on every frame it is shown`. "Drive" is A15's own reading: the mesh's slot bone, or a bone its weights name.
|
|
3433
|
+
A mesh under a keyed bone's *descendant* also moves and is not counted, so the
|
|
3434
|
+
figures are a floor.
|
|
3435
|
+
- ⛔ **A declaration nothing exercises is refused at the gate**, not skipped: an
|
|
3436
|
+
`idle` that keys no mesh-driving bone, carries no bone timeline, or does not
|
|
3437
|
+
exist fails A15 with `the rig "X" declares invariants.idleDrivesMeshes ("…"),
|
|
3438
|
+
but "idle" keys no bone that drives a mesh, so the declaration switches off
|
|
3439
|
+
nothing — remove it`. That is the `consumerDrivenMix` standard: an opt-out that
|
|
3440
|
+
exempts nothing reads exactly like one that worked.
|
|
3441
|
+
- ⚠️ A15 is a renderer rule, so under `--profile spine` it reports `PROF` and the
|
|
3442
|
+
declaration is not read at all, stale or not.
|
|
3443
|
+
|
|
3219
3444
|
---
|
|
3220
3445
|
|
|
3221
3446
|
## 4. The motion spec, field by field
|
|
@@ -5487,20 +5712,20 @@ Fix A00 and run it again.
|
|
|
5487
5712
|
| `A12_NO_DARK_COLOR` | renderer | a slot `dark` colour or an `rgba2`/`rgb2` timeline; parsed, then ignored |
|
|
5488
5713
|
| `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) **SKIP** also when the rig budgets **only** triangles and the skeleton carries no mesh. A declared slot budget still PASSes there, because zero mesh slots is a count measured against a ceiling |
|
|
5489
5714
|
| `A14_NO_FULL_FRAME_MESH` | renderer | a mesh spans the whole stage — a full-frame canvas that can never dirty-skip. **SKIP** when the skeleton declares no stage (§3.1): there is no full frame to span, and *unmeasured* must not print the same green as *measured and clear* |
|
|
5490
|
-
| `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. **SKIP** when there is no `idle` animation, or when the one there is carries no bone timeline — a rule whose subject does not exist is unmeasured and not satisfied |
|
|
5715
|
+
| `A15_IDLE_NO_MESH_BONE_KEYS` | renderer | the `idle` animation keys a bone that drives a mesh, directly or as a control bone. The rule assumes meshes are mostly static — the renderer skips redrawing a mesh nothing moved — so it has three states. **FAIL per bone** when the rig declares nothing: `idle keys bone "X", which drives a mesh — meshes never idle-skip`, and the **first** of those lines, once per run, names the case: `K bone(s) keyed by idle drive meshes; if this idle is meant to deform them (a painting rig), declare invariants.idleDrivesMeshes: { "why": … } in the rig spec — or, where the motion belongs to a pivot above the mesh, key that pivot one link up (FACE.md §3)`. It rides the first finding, so the count of FAIL lines is still the count of bones. **SKIP with the cost** when the rig declares `invariants.idleDrivesMeshes` (§3.7): `declared by the rig ("<why>"): idle keys N bone(s) that drive M mesh attachment(s) totalling V vertices — "a", "b" — and each of those meshes is recomputed on every frame it is shown`. **FAIL on a stale declaration** — declared, and the `idle` keys no mesh-driving bone, carries no bone timeline or does not exist: `the rig "X" declares invariants.idleDrivesMeshes ("…"), but "idle" keys no bone that drives a mesh, so the declaration switches off nothing — remove it`. Otherwise **SKIP** when there is no `idle` animation, or when the one there is carries no bone timeline — a rule whose subject does not exist is unmeasured and not satisfied |
|
|
5491
5716
|
| `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`) |
|
|
5492
5717
|
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. **SKIP** when the atlas declares no page — as it is for `A06`, `A19` and `A27`; see `A07` |
|
|
5493
5718
|
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
|
|
5494
5719
|
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part cannot draw a transparent pixel, so it would paint a solid rectangle over what is behind it. **The texels decide, on both routes**; the file header is only the fast negative. A file with no alpha channel (colour type 4 or 6) and no `tRNS` chunk has nowhere to keep a clear texel and is refused without being opened, by a sentence that names its colour type — so re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. A file whose header says it **could** be transparent is opened and read until its first texel below full alpha; one clear texel passes, and none is refused in the words the packed route uses for a region, naming the file and what it can hold — `is opaque in every one of its 200x80 texels` … `its file can hold transparency — colour type 6 (truecolour + alpha) carries an alpha channel — and no texel uses it`. Saving as RGBA is not the repair: the runtime draws the texels, not the declaration. On a loose page the file is the part, so the whole decoded image is the rectangle and no atlas coordinate is read. Only the base plate may be opaque, and which image that is is decided once for both routes: the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art. ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page**: a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it**, and the clause above does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so a scan would come back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappears**, the scan finding a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. ⚠️ **A page file that cannot be read as PNG at all is the same non-measurement for every part on it**: one row per page naming its parts and pointing at `A06`, which names what the file is. **SKIP** when the atlas declares no page |
|
|
5495
|
-
| `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not. Fix the rig spec's `controls`/`chain`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
|
|
5496
|
-
| `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1 |
|
|
5720
|
+
| `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not. Fix the rig spec's `controls`/`chain`, a `segments` generator's `bones` (its `influence` line names a bone no vertex binds), or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment |
|
|
5721
|
+
| `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1. **SKIPs** by name on authored or linked geometry and on a `segments` mesh, whose outline is weighted by distance like the rest of it — none of them claims a pinned rim |
|
|
5497
5722
|
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment |
|
|
5498
5723
|
| `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, rests at `mix: 0` with **no timeline in any animation keying that `mix` above 0**, has `mass: 0`, has `strength` at or below 0 — `0` says `nothing pulls it back`, below 0 says the offset `is pushed away and grows with every step`, both read off the row's `outside` arms, which the key's refusal quotes too — or has `damping` outside `[0, 1]`, which says `` physics "C" has damping 1.5; must be inside [0, 1] — the per-step decay is `damping ** (60 * step)`, and the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: above 1 every velocity grows on every step and the offset diverges ``, the bound read off the row the key's refusal reads; `1` and `0` are inside, since both are finite at every rate — **at rest, and on every physics timeline key**. The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. Two differences between the two arms, and the runtime is the reason for both: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys — and a **key** of `strength: 0` is accepted, because it releases the constraint for the span with `damping` and `inertia` still applied and the next key pulls the offset back, measured through spine-core at no NaN, a coast to a limit and a return in 54 steps. As a **setup** value `strength: 0` is still refused by the arm above, and `mix: 0` is refused only when nothing keys it above 0. The `mix` branch above is why `mix` is the one setup value a key can answer for: at rest the constraint is **inert** rather than broken, so a rig that rests muted and is keyed above 0 is refused by nothing, while a rig resting at `mass: 0` is `massInverse` Infinity before anything plays and no key reaches back into that. The detail of the refusal says both halves and how many animations were searched: `` physics "C" has mix 0 and none of the 3 animations keys its mix above 0; the runtime runs this value finitely, so refusing it is rigc's call rather than the runtime's: at 0 `update` returns before it does anything (`PhysicsConstraint.js:109-111`), so the constraint is muted — rest it above 0, or key its mix above 0 in an animation ``. ⚠️ Every reason A23 prints for a setup value is that value's row `basis` arm (§4.4's table) — an arithmetic arm names its expression (`physics "C" has massInverse Infinity; mass must be > 0 — at 0 …`), a behavioural one says it is rigc's call and what the value does. A setup `mix` **below** 0 is told the jiggle is applied inverted, not that it is muted: such a constraint is measured moving its bone by exactly the opposite of a positive mix. The search counts the unnamed global timeline for every constraint whose own `mixGlobal` is set, reads each key through the runtime's accessor, counts every sample of a Bezier between two keys as a value the timeline poses — so two keys of 0 joined by a curve lifted above 0 are a rescue, measured to move the bone — and takes an animation a slider applies like any other. It is the one reading `A36` and `A37` use as well. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. `ingest` does not carry a constraint that drives no component into the spec it writes: it omits it with its timelines and reports `PHYSICS_DRIVES_NOTHING` ([INGEST §2.0](INGEST.md)), so this sentence is met on a file, never on a decompiled rebuild. **SKIP** when the skeleton declares no physics constraint — the same sentence `A36` and `A37` print for their own constraint types |
|
|
5499
5724
|
| `A24_AXIS_SPACE_STROKE` | archetype | a bone under the rig's `axisBone` was keyed with a screen-space Y component, or the axis bone itself was keyed. **SKIP** when the rig declares no axis bone, and also when no animation keys that bone or anything under it |
|
|
5500
5725
|
| `A25_DETACHED_BONE_PARENTAGE` | archetype | a bone the rig declares `detached` is a descendant of the bone it must never hang under |
|
|
5501
5726
|
| `A26_SLOT_DRAW_ORDER` | archetype | the emitted slots are not the rig's slot table — a slot is out of order, is not in the table at all, or is in the table and missing from the skeleton (§3.3). **SKIP** when the rig declares no canonical slot order. ⚠️ A skeleton with **no** slot beside a rig that declares some is **not** a skip, and it is the one rule in this family where an empty loop is not a vacuous pass: the completeness clause reads it as every declared slot lost and names them |
|
|
5502
5727
|
| `A27_REGION_NAME_MATCHES_PAGE_FILENAME` | renderer | a single-region page whose region name is not the PNG's basename. **SKIP** when the atlas declares no region |
|
|
5503
|
-
| `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width. **SKIPs** on authored geometry
|
|
5728
|
+
| `A28_RIBBON_ROWS_SHARE_WEIGHTS` | archetype | the two vertices of a ribbon row carry different weights, so the strip would change width. **SKIPs** on authored geometry, a contour mesh and a `segments` lattice — none has rows rigc paired |
|
|
5504
5729
|
| `A29_STROKE_WITHIN_CONTACT_DEPTH` | archetype | the animation drives deeper than the manifest's measured contact depth |
|
|
5505
5730
|
| `A30_STROKE_WITHIN_CAP_CONTAINMENT` | archetype | the animation drives past the measured containment ceiling, or scales a bone in the axis subtree |
|
|
5506
5731
|
| `A31_DRAW_ORDER_OFFSETS_RESOLVE` | both | a draw-order key names a slot the skeleton does not have, offsets one slot twice, puts a slot outside the slots array, or lists its offsets out of slot order (§4.7). The only assertion that runs **before** `A00` — the last of those shapes makes the loader spin rather than return, so the round trip is refused instead of attempted |
|
|
@@ -6068,6 +6293,119 @@ the loop with **§9**: the fit's own number says how near this pose is to this f
|
|
|
6068
6293
|
and only `check` says whether the shot is the shot. **§9.3** is the list of what even
|
|
6069
6294
|
that cannot see.
|
|
6070
6295
|
|
|
6296
|
+
### 8.2 The numbers the frames were drawn from — `render --geometry`
|
|
6297
|
+
|
|
6298
|
+
A frame set is pixels, and some judgements about a rig are not about pixels: how far
|
|
6299
|
+
a mesh triangle is stretched over its rest shape, or whether a region holds still in
|
|
6300
|
+
its own bone's frame. Both need the posed numbers, and both need them **on the frames'
|
|
6301
|
+
own grid** — a second tool re-sampling the animation would step the runtime its own
|
|
6302
|
+
way and drift from the pictures it is meant to explain. So `render --geometry` writes
|
|
6303
|
+
them beside the pictures, off the very frames it drew:
|
|
6304
|
+
|
|
6305
|
+
```bash
|
|
6306
|
+
bun cli.ts render --candidate path/to/spine --geometry [--animation …] [--skin …] [--fps 12]
|
|
6307
|
+
```
|
|
6308
|
+
|
|
6309
|
+
Each frame directory gains a `geometry.json` beside its `f####.png`. Nothing else
|
|
6310
|
+
changes: the PNGs, `contact.png` and `frames.json` are byte for byte what the same
|
|
6311
|
+
command writes without the flag. The export costs a small fraction of the render —
|
|
6312
|
+
measured on `spineboy-pro` at 12 fps, 190 frames: about 30 ms for the eleven files
|
|
6313
|
+
against about 610 ms for the rasterising alone — which is why there is no
|
|
6314
|
+
geometry-only mode: a consumer judging frames wants the frames, and skipping them
|
|
6315
|
+
would save little.
|
|
6316
|
+
|
|
6317
|
+
⛔ **Not with `--slot`/`--hide`.** The two are refused together by name, exit 2,
|
|
6318
|
+
nothing written. A subset is a statement about which pixels are *drawn*; the
|
|
6319
|
+
geometry is the whole pose whatever a picture leaves out, so beside a subset's frames
|
|
6320
|
+
it would describe slots those frames do not draw. The whole-rig run writes the same
|
|
6321
|
+
file either way.
|
|
6322
|
+
|
|
6323
|
+
**The file, field by field.** `spec` is `rigc-geometry/1`. The header fields sit one
|
|
6324
|
+
per line and each `rest` entry and each frame on a line of its own, so a diff of two
|
|
6325
|
+
exports names the frame that moved. Two exports of one skeleton are byte-identical.
|
|
6326
|
+
|
|
6327
|
+
| Field | What it holds |
|
|
6328
|
+
| --- | --- |
|
|
6329
|
+
| `coordinates` | the string `spine world, y up, world units` — every position below is in the skeleton's world, not frame pixels |
|
|
6330
|
+
| `animation` | the animation, or `null` for a skeleton with none (its one frame is the setup pose) |
|
|
6331
|
+
| `skin` | the skin these frames were posed under — absent when none was set, exactly as in `frames.json` |
|
|
6332
|
+
| `fps` | the rate the frames were sampled at |
|
|
6333
|
+
| `viewport` | `frames.json`'s own `viewport` object, the same numbers: world box `x`, `y`, `width`, `height`, `scale` (frame pixels per world unit), `pixelWidth`, `pixelHeight`. World to frame pixel is `px = (x − viewport.x) · scale`, `py = (viewport.y + viewport.height − y) · scale`, which is the projection the renderer used |
|
|
6334
|
+
| `bones` | every bone in declaration order: `{ name, parent }`, `parent` `null` for the root |
|
|
6335
|
+
| `slots` | every slot in declaration order: `{ name, bone }` — the bone the slot hangs from |
|
|
6336
|
+
| `rest` | one entry per `(slot, attachment)` any frame shows, in order of first appearance: `slot`, `attachment`, `kind` (`region` or `mesh`), `vertices` at rest, `triangles` (vertex index triplets — a region's are the runtime's `0 1 2 2 3 0`), and for a mesh `hull` (the number of hull vertices, which are the first ones) and `uvs` (the attachment's own, `u, v` per vertex over the untrimmed drawing, y down) |
|
|
6337
|
+
| `setup` | the setup pose, sampled the way a skeleton with no animation is rendered: `{ bones, attachments }` |
|
|
6338
|
+
| `frames` | one per PNG, in index order: `{ index, time, bones, attachments }`. `index` is the number in `f####.png`, `time` is `index / fps` in seconds |
|
|
6339
|
+
|
|
6340
|
+
Inside `setup` and each frame:
|
|
6341
|
+
|
|
6342
|
+
- `bones` — every bone in declaration order: `{ name, a, b, c, d, worldX, worldY }`,
|
|
6343
|
+
the world transform, so a point `(lx, ly)` in the bone's own frame lands at
|
|
6344
|
+
`(a·lx + b·ly + worldX, c·lx + d·ly + worldY)`. The matrix is complete — rotation,
|
|
6345
|
+
scale and shear all live in `a b c d`.
|
|
6346
|
+
- `attachments` — every slot showing a region or a mesh, in the frame's **draw
|
|
6347
|
+
order**: `{ slot, attachment, vertices, color }`. `vertices` are `x, y` per vertex
|
|
6348
|
+
in world units after skinning and deform — the runtime's own
|
|
6349
|
+
`computeWorldVertices` over the **whole** attachment. A region's four are its
|
|
6350
|
+
corners in the order bottom-left, top-left, top-right, bottom-right; a mesh's are
|
|
6351
|
+
in the attachment's own order, so vertex `i` is the same vertex in every frame and
|
|
6352
|
+
in `rest`. `color` is the slot colour times the attachment colour, straight RGBA
|
|
6353
|
+
`0..1` — the same tint the frame was drawn with.
|
|
6354
|
+
|
|
6355
|
+
What `rest` means, precisely: the **setup pose's bones with the slot's deform
|
|
6356
|
+
empty**, taken for every attachment the file shows — including one the setup pose
|
|
6357
|
+
does not show, which a slot only swaps to later. For an attachment the setup pose
|
|
6358
|
+
does show, its `rest` vertices and its `setup` vertices are the same numbers bit for
|
|
6359
|
+
bit, because they come off one posed skeleton.
|
|
6360
|
+
|
|
6361
|
+
⚠️ **What is deliberately not in it.** Slots `--slot`/`--hide` would leave out are in
|
|
6362
|
+
it (see above). A clip is not applied: a clipping attachment removes pixels, and the
|
|
6363
|
+
clipper's output is a new triangle list whose vertices are not the attachment's and
|
|
6364
|
+
are not numbered like them — so `vertices` are the attachment's own, and the frames
|
|
6365
|
+
show what the clip left of them. Bounding boxes, points, paths and clipping polygons
|
|
6366
|
+
draw nothing and are not listed. Numbers are printed the way every JSON file this
|
|
6367
|
+
tool writes prints them — the shortest decimal that reads back as the same double —
|
|
6368
|
+
so a vertex in the file *is* the runtime's vertex, not a rounding of it; a pose that
|
|
6369
|
+
is not finite is refused naming the frame, the slot, the attachment and the vertex,
|
|
6370
|
+
rather than written as `null`.
|
|
6371
|
+
|
|
6372
|
+
**The two judgements the file was designed for**, written once so a consumer
|
|
6373
|
+
implements what the file promises:
|
|
6374
|
+
|
|
6375
|
+
1. **Texture stretch of a mesh triangle.** For each `rest` entry of kind `mesh` and
|
|
6376
|
+
each triangle `(i, j, k)` in its `triangles`, over every frame whose `attachments`
|
|
6377
|
+
carry the same `slot` and `attachment`:
|
|
6378
|
+
|
|
6379
|
+
`stretch = max over frames, max over the edges (i,j) (j,k) (k,i) of |posed edge| / |rest edge|`
|
|
6380
|
+
|
|
6381
|
+
where an edge's length is `hypot(x_j − x_i, y_j − y_i)` over that frame's
|
|
6382
|
+
`vertices` and over `rest`'s. The worst triangle is named by its `slot`, its
|
|
6383
|
+
triangle index and its three vertex indices. A value of exactly `1` is a
|
|
6384
|
+
triangle no frame moved. It is measured against rest, so it counts what the
|
|
6385
|
+
bones do to a weighted mesh as well as what a deform key does — unlike the
|
|
6386
|
+
`DEFORM` block, which compares a key against the same bones with the deform
|
|
6387
|
+
cleared.
|
|
6388
|
+
2. **A region still in its bone's own frame.** Take the region's `slot`, look its
|
|
6389
|
+
`bone` up in `slots`, and for each frame carry its four corners into that bone's
|
|
6390
|
+
frame by the inverse of the bone's world transform:
|
|
6391
|
+
|
|
6392
|
+
`det = a·d − b·c`, `dx = x − worldX`, `dy = y − worldY`,
|
|
6393
|
+
`lx = (d·dx − b·dy) / det`, `ly = (−c·dx + a·dy) / det`
|
|
6394
|
+
|
|
6395
|
+
Against the same four carried from `setup`, the largest difference over frames is
|
|
6396
|
+
the region's motion *relative to its bone*: zero, to double rounding, for a region
|
|
6397
|
+
nothing but its bone moves. A face region measured against a head bone this way
|
|
6398
|
+
is the face with the head's own turn, translation and scale taken out.
|
|
6399
|
+
|
|
6400
|
+
**Measured on the example corpus** at 12 fps (these are one run's figures, not
|
|
6401
|
+
thresholds): `spineboy-pro`'s eleven files total 5.1 MB, from 72 kB (`aim`, one
|
|
6402
|
+
frame) to 1.46 MB (`death`, 60 frames), and `sack-pro`'s four 1.4 MB. The largest
|
|
6403
|
+
stretch is `sack-pro` `fall-in`, `cape-back` triangle 15, ×6.744; the largest
|
|
6404
|
+
still-in-frame residual over every region of `spineboy-pro` is 4.7e-12 world units
|
|
6405
|
+
(`portal-streaks2`, whose bone scales; `sack-pro` has no region).
|
|
6406
|
+
Topology and UVs sit in `rest` rather than on every frame because they do not
|
|
6407
|
+
change: repeating them per frame would add 35 % to `spineboy-pro`'s files.
|
|
6408
|
+
|
|
6071
6409
|
---
|
|
6072
6410
|
|
|
6073
6411
|
## 9. Checking against the frames — `rigc check`
|
|
@@ -8307,8 +8645,9 @@ Per part:
|
|
|
8307
8645
|
| `ambiguous` | at least one alternate is inside the ambiguity margin. **Choose with something this instrument cannot see** — anatomy, the other frame, or `rigc vote` |
|
|
8308
8646
|
| `rotationFree` | the part is self-similar under rotation, so `rotationDeg` is a placeholder and the value is yours |
|
|
8309
8647
|
| `rotationSelfSimilarity` | the number `rotationFree` is a threshold on. A part just over the line is worth a look |
|
|
8310
|
-
| `refusal` | `{ reason, detail }` or `null`. Reasons: `no-match`, `larger-than-canvas`, `empty-part`. A `no-match` whose best placement stopped **on a wall of the search window** names the wall in its detail — see §11.4 |
|
|
8648
|
+
| `refusal` | `{ reason, detail }` or `null`. Reasons: `no-match`, `larger-than-canvas`, `empty-part`. A `no-match` whose best placement stopped **on a wall of the search window** names the wall in its detail — see §11.4 — and every `no-match` with a placement ends with `legibility.reading` — see §11.5 |
|
|
8311
8649
|
| `coarse` | the grid this part was actually searched on. A handful of cells means the part is small relative to the frame and the first pass had little to go on |
|
|
8650
|
+
| `legibility` | what the part gave the search to work with, and how its answers stood against each other — `null` only when nothing was searched. `width`, `height`: the material box, part px · `span`: its longest side at the best placement's scale, **frame** px · `opaqueShare`: alpha weight over the box's area · `texture`: the part's **texture figure** (§11.5) · `candidates`: distinct placements measured at full resolution · `best`, `next`, `spread`: the best residual, the second distinct one, and `next − best` (`null` with no second) · `floor`: `below` or `above` the measured floor (§11.5), on every part · `reading`: on a refused or ambiguous part, the sentence saying which of the two readings those figures support; `null` on a placed one |
|
|
8312
8651
|
| `notes` | the same facts in prose, in the order they were found |
|
|
8313
8652
|
|
|
8314
8653
|
Per placement:
|
|
@@ -8383,6 +8722,119 @@ holds nothing because it already contains every angle.
|
|
|
8383
8722
|
with B — which placement of a repeated part belongs to which limb, across two
|
|
8384
8723
|
frames — is the authoring job, not this tool's.
|
|
8385
8724
|
|
|
8725
|
+
### 11.5 Too small or too plain — the measured floor
|
|
8726
|
+
|
|
8727
|
+
A refusal or an ambiguity has two readings the residual cannot separate: **the
|
|
8728
|
+
part is wrong**, or **the part is too small or too plain for `pose`** to pin
|
|
8729
|
+
down. A correct cut of a plain sleeve and a foreign part can end in the same
|
|
8730
|
+
verdict. So every searched part carries `legibility`, and a refused or
|
|
8731
|
+
ambiguous one ends with a sentence saying which reading its figures support:
|
|
8732
|
+
|
|
8733
|
+
- **below the floor** — a 40x28 part of one flat colour, cut from a plain block it fits anywhere inside:
|
|
8734
|
+
|
|
8735
|
+
> part is 40x28 px (span 40 frame px, opaque 1) with texture 0, detail 0; 14 candidate(s), best 0.0000, next 0.0000, spread 0.0000 — below the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up): pose placed 8% of the measured parts this plain, so this part is too plain for pose to tell its placements apart; that says nothing about whether the cut is right
|
|
8736
|
+
|
|
8737
|
+
- **above the floor** — a 200x150 black-and-white checker the frame does not hold:
|
|
8738
|
+
|
|
8739
|
+
> loud.png: the best placement found has residual 0.5000, above --max-residual 0.25; part is 200x150 px (span 200 frame px, opaque 1) with texture 0.2473, detail 49.455; 2 candidate(s), best 0.5000, next 0.5000, spread 0.0000 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; the cut (or the search window) is the suspect
|
|
8740
|
+
|
|
8741
|
+
Read the figures, not only the verdict. *Below* says plainness alone explains the
|
|
8742
|
+
failure: the cut may be right, and `pose` cannot tell. *Above* says it does not,
|
|
8743
|
+
so look at the cut, the `--scale`/`--rotation` window, or — for an ambiguity — at
|
|
8744
|
+
whether the frame really holds the part twice. ⚠️ Above the floor is **not** a
|
|
8745
|
+
promise that `pose` will place the part (see the rates below).
|
|
8746
|
+
|
|
8747
|
+
**The texture figure.** `legibility.texture` is the mean colour step between
|
|
8748
|
+
neighbouring material pixels, `0..1`, in the residual's own units: each pair of a
|
|
8749
|
+
pixel and its right or lower neighbour, both with material, contributes its mean
|
|
8750
|
+
absolute channel difference over 255, weighted by the smaller alpha. A part of
|
|
8751
|
+
one flat colour reads exactly `0`. It is chosen over a colour variance because it
|
|
8752
|
+
is, to first order, **what a one-pixel shift costs**: a part whose residual
|
|
8753
|
+
cannot rise when it moves cannot be told from itself moved. The silhouette is not
|
|
8754
|
+
counted — the material term already reads it, and only where the frame is ground.
|
|
8755
|
+
|
|
8756
|
+
**The floor**, measured with `bun tools/pose_floor.ts`:
|
|
8757
|
+
|
|
8758
|
+
Found within 2 px of the truth, out of 15 trials per cell (free search, default windows):
|
|
8759
|
+
|
|
8760
|
+
| texture level (mean) | 24 px | 32 px | 48 px | 64 px | 96 px | 128 px | 192 px |
|
|
8761
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
8762
|
+
| `native` (detail 3.49) | 12/15 (3 amb) | 14/15 (1 amb) | 13/15 (2 amb) | 15/15 | 12/15 (2 amb, 1 wrong) | 14/15 (1 wrong) | 14/15 (1 wrong) |
|
|
8763
|
+
| `blur1` (detail 1.58) | 10/15 (5 amb) | 12/15 (2 amb, 1 wrong) | 13/15 (2 amb) | 13/15 (2 amb) | 12/15 (1 amb, 2 wrong) | 11/15 (1 amb, 3 wrong) | 11/15 (2 amb, 2 wrong) |
|
|
8764
|
+
| `blur2` (detail 0.59) | 7/15 (7 amb, 1 wrong) | 8/15 (5 amb, 2 wrong) | 9/15 (4 amb, 2 wrong) | 8/15 (3 amb, 4 wrong) | 7/15 (3 amb, 5 wrong) | 6/15 (4 amb, 5 wrong) | 6/15 (2 amb, 7 wrong) |
|
|
8765
|
+
| `flat` (detail 0.00) | 0/15 (14 amb, 1 wrong) | 0/15 (14 amb, 1 wrong) | 0/15 (13 amb, 2 wrong) | 0/15 (11 amb, 4 wrong) | 0/15 (13 amb, 2 wrong) | 0/15 (12 amb, 3 wrong) | 0/15 (12 amb, 3 wrong) |
|
|
8766
|
+
|
|
8767
|
+
The same trials, grouped by `detail`:
|
|
8768
|
+
|
|
8769
|
+
| detail | found | ambiguous | wrong | refused |
|
|
8770
|
+
| --- | --- | --- | --- | --- |
|
|
8771
|
+
| under 0.25 | 8/119 | 89 | 22 | 0 |
|
|
8772
|
+
| 0.25–0.5 | 2/3 | 0 | 1 | 0 |
|
|
8773
|
+
| 0.5–0.75 | 44/81 | 16 | 21 | 0 |
|
|
8774
|
+
| 0.75–1 | 17/34 | 14 | 3 | 0 |
|
|
8775
|
+
| 1–1.5 | 24/25 | 1 | 0 | 0 |
|
|
8776
|
+
| 1.5–2 | 34/48 | 11 | 3 | 0 |
|
|
8777
|
+
| 2–3 | 36/44 | 5 | 3 | 0 |
|
|
8778
|
+
| 3–4 | 33/34 | 1 | 0 | 0 |
|
|
8779
|
+
| 4 and over | 29/32 | 3 | 0 | 0 |
|
|
8780
|
+
|
|
8781
|
+
**Method.** The truth set is `examples/spineboy`: its setup pose and one frame of
|
|
8782
|
+
`walk` (a third of the way in). For every trial the figure is rendered twice with one
|
|
8783
|
+
viewport — whole on the renderer's ground (the pose frame), and one slot alone on
|
|
8784
|
+
transparency (the part, cropped to its own alpha) — so the truth is the crop's own
|
|
8785
|
+
offset at rotation 0 and scale 1, fitted by nothing. Slots with at least 75% of their
|
|
8786
|
+
material visible take part: 8 in the setup pose and 7 in the walk frame, which makes 15
|
|
8787
|
+
trials per cell. The size is set by the viewport, so the part's longest side is the
|
|
8788
|
+
column's size in frame pixels. The texture is set on the atlas region the slot samples,
|
|
8789
|
+
so the part and the frame carry the same plainer art: `native`; `blur1`/`blur2`, a box
|
|
8790
|
+
blur of 3% and 10% of the region's longest side; `flat`, one alpha-weighted mean colour.
|
|
8791
|
+
A trial is *found* when it is neither refused nor ambiguous and its centre is within
|
|
8792
|
+
2 px of the truth.
|
|
8793
|
+
|
|
8794
|
+
**The figure the floor is stated in** is `legibility.detail` = `texture` × the material
|
|
8795
|
+
box's longest side in part pixels: the colour change a walk along the part
|
|
8796
|
+
accumulates. The per-pixel `texture` alone does not predict the outcome — an upscaled
|
|
8797
|
+
part has a low per-pixel gradient and still places — and size alone has no
|
|
8798
|
+
measurable effect across 24–192 px at any level.
|
|
8799
|
+
|
|
8800
|
+
**The edge**, derived by `deriveFloor` as the largest rung of a fixed ladder under
|
|
8801
|
+
which at most a tenth of the trials were found: **`detail` 0.5**. Under it, **8.2%**
|
|
8802
|
+
of the trials were found (10/122); at or over it, **72.8%** (217/298). A part is below
|
|
8803
|
+
the floor when its `detail` is under 0.5, or when its own longest side is under 24 px,
|
|
8804
|
+
the smallest size measured (there, nothing measured says `pose` can place it).
|
|
8805
|
+
|
|
8806
|
+
**What the search carries, and why.** The coarse pass scores the part at anchor
|
|
8807
|
+
cells an **eighth** of its own span apart, and every scale rung sends its **five**
|
|
8808
|
+
best distinct places down to the finer levels. Both were set on this grid (issue
|
|
8809
|
+
#865): of the 65 failures whose truth scored better than every answer reported, 38
|
|
8810
|
+
had a truth that beat its whole scale rung at the coarse level while the anchor cell
|
|
8811
|
+
holding it did not — at a quarter-span stride a thin gun or a gapped fist falls between
|
|
8812
|
+
two anchors — and only 3 had it ranked under the old cutoff of three. The finer grid
|
|
8813
|
+
took the grid from 185 to 227 trials found and those misses from 65 to 14, for about
|
|
8814
|
+
10–14% more CPU over the grid (615 s of user time before it, 678 s and 704 s on two runs after).
|
|
8815
|
+
|
|
8816
|
+
⚠️ **What this is and is not.** The grid is not a reliability line: four native cells
|
|
8817
|
+
place 14 or 15 of 15 and the trials at `detail` 3 and over place 62 of 66, but the
|
|
8818
|
+
goggles still place 10 of 42, and a threshold read off two dozen parts of one figure
|
|
8819
|
+
would be a claim about that figure. What the grid can separate is **whose** failure a
|
|
8820
|
+
failure is. `bun tools/pose_floor.ts` prints the residual at the truth for every
|
|
8821
|
+
failed trial and marks it **missed** when the truth scores better than everything
|
|
8822
|
+
reported and nothing reported is on it — a failure the search could fix. The other
|
|
8823
|
+
failures are the objective preferring another placement (a blurred part shrunk into
|
|
8824
|
+
its own region scores better than its truth) or the truth reported and tied, and no
|
|
8825
|
+
search can fix those. After the change, 14 of the 193 failed trials are misses, and 13
|
|
8826
|
+
of those sit within 7 px of the truth rather than on another hill. All this was
|
|
8827
|
+
measured on **ideal cuts**, the frame's own pixels, where a residual at the truth is
|
|
8828
|
+
near zero. A generated cut also disagrees with its picture, which can only make it
|
|
8829
|
+
harder to place. So the edge is where `pose` fails even on ideal cuts, and never a
|
|
8830
|
+
guarantee above it.
|
|
8831
|
+
|
|
8832
|
+
Re-run it with `bun tools/pose_floor.ts --textures <level> --json <out>` (one level per
|
|
8833
|
+
process parallelises it), then `bun tools/pose_floor.ts --from <a.json>,<b.json>,…` to
|
|
8834
|
+
print the table, derive the edge and list the misses per cell. `PO22` in
|
|
8835
|
+
`bun run selftest` re-measures three of its cells, `PO24` holds those three cells to
|
|
8836
|
+
no miss at all, and `PO23` places the 48 px fist this change was made for.
|
|
8837
|
+
|
|
8386
8838
|
## 12. Reading the half of that picture `pose` refuses — `rigc chainfit`
|
|
8387
8839
|
|
|
8388
8840
|
```bash
|