spine-rigc 2.55.0 → 2.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AUTHORING.md CHANGED
@@ -156,6 +156,9 @@ name the commands that need the runtime (its `--help` lists the ones it runs).
156
156
  gate before it wrote, and which rules that gate is on this entry, before the install
157
157
  route; on a Spine export it says what it always said (`RC36`).
158
158
  `rigc --version` names the entry that ran.
159
+ `rigc help <command>` prints what `rigc <command> --help` prints and exits the same
160
+ way, on either entry, and `rigc help` alone is `rigc --help`; a name that is no
161
+ command is refused naming the nearest one, exit 2, as an unknown command is.
159
162
 
160
163
  That entry's `build` takes the same flags and writes the same files, byte for byte:
161
164
  `skeleton.json`, `skeleton.atlas`, `skeleton.model.json`, and the pages `--pack` or
@@ -247,8 +250,11 @@ What the flags mean:
247
250
  | `--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** |
248
251
  | `--folds` | `render` only: also read which mesh triangles **fold** — turn over against the setup pose — and write a `folds.json` into each frame directory: per mesh, the frames with a folded triangle, the worst frame's count and which triangles, by index, with their world box and setup-pose area. One printed line per mesh that folds; a triangle degenerate or inverted at the setup pose is said once and never counted. A reading, not a gate. Every other file is byte for byte what the render writes without it, `geometry.json` included. Refused with `--slot`/`--hide`, for `--geometry`'s reason — **§8.3** |
249
252
  | `--pass-through` | `render` only: `<cloth>:<leg>[,<cloth>:<leg>…]` — also read where each cloth slot **passes through** its leg slot, and write a `pass-through.json` into each frame directory: the frames where the cloth draws over the leg and its drawn side lies inside the leg's drawn outline by 2 steps or more, each with the depth and the world box. One printed line per pair, and one per frame that passes through. The first name must draw over the second for a frame to be read — the declared order, never both. A reading, not a gate. Every other file is byte for byte what the render writes without it. An unknown slot, a slot that draws on no frame, a malformed list and a pair named twice are refused by name, and so is `--slot`/`--hide` beside it — **§8.5** |
253
+ | `--angle-range` | `render` only: `<bone>:<relative-to>[,<bone>:<relative-to>…]` — also read, for each pair, the range of the first bone's world direction against the second's over each set's frames, and print one line per pair per set: the lowest and the highest angle in degrees, the span, and the frame each was first reached on. A reading, not a gate: nothing is written, and every file is the bytes the render writes without it. An unknown bone (with the bones there are), a bone against itself, a malformed list and a pair named twice are refused by name, and so is a bone with no direction on a frame — **§8.7** |
254
+ | `--viewport`, `--viewport-from` | `render`: give the box every frame is drawn over instead of fitting one — `--viewport <x,y,w,h>`, a world box, y up, `x,y` its bottom-left corner, at the pixel size `--max` gives; or `--viewport-from <frames.json \| dir>`, an earlier render's grid exactly (its box, scale and pixel size), so two candidates land on one pixel grid. A slot that reaches past the box is named by set and frame, and `frames.json` records the box was given under `pinned`. The two together are refused, and so is `--max` beside `--viewport-from` — **§8.6**. On `check` and `inspect`, `--viewport` pins the box those commands draw over instead |
250
255
  | `--environment` | `render` only: the wind and gravity the rig's physics constraints answer, in time — a schedule file `{ "spec": "rigc-environment/1", "steps": [{ "t": <seconds>, "wind"?: [x, y], "gravity"?: [x, y] }, …] }`, x right and **y down**, each step holding from its `t` until the next. Both posers set each change before the step that poses the first frame at or after its `t`, and draw the same frames, `geometry.json` and `frames.json` (`RE01`). `frames.json` and `geometry.json` record it, and the framing reads it. A malformed file, a half no physics constraint answers, two steps on one frame and a step after every set's last frame are refused by name, exit 2, nothing written; `check` refuses a frame set drawn under one as a reference. Without it every file is the bytes it always was. An inspection of the response the rig states, not a scene — **§8.4** |
251
256
  | `--poser` | `render`, `inspect`, `check` and `explain`: `core` or `spine` — on `render`, `inspect` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`, `RC34`). On the entry that links none of spine-core — the installed `rigc` without the runtime beside it, `bun cli_core.ts` in a clone — that refusal ends on the route to the entry that does (``the entry that links it runs them — installed, the same `rigc` once @esotericsoftware/spine-core is installed beside the package; from a source checkout, `bun cli.ts` ``); a runtime that is installed and fails to load is refused without it, since installing it is not what that reader has to do. On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1), ending on the same route where the entry links none of it |
257
+ | `--lever-fold` | `explain` only: a fraction above 0 and at most 1. Also read, on the turn lines' walk, the steps where a physics constraint that drives `scaleX` has its inertia lever divide by less than this fraction of the divisor it takes at the setup pose, and the steps where the scale factor it applies moves by more than the bone's setup scale. **No default**: without it the reading is not taken and one line says so; a value that is not such a fraction is refused by name (exit 2). A reading, not a gate — **§4.6.2** |
252
258
  | `--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` |
253
259
  | `--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 |
254
260
  | `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
@@ -268,7 +274,8 @@ What the flags mean:
268
274
 
269
275
  `render` also takes `--fps <n>` (the rate it samples at, default 12 — the same
270
276
  protocol rate the reference frames use) and `--max <px>` (the long side of a
271
- frame, default 256). A clipping attachment is applied as the runtime applies it —
277
+ frame, default 256). A set drawn at any rate but the default is written to
278
+ `<anim>@<fps>fps/` rather than `<anim>/`, so two rates sit side by side: `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. A clipping attachment is applied as the runtime applies it —
272
279
  spine-core's own `SkeletonClipping`, in spine-webgl's call order, on a Spine export,
273
280
  and the core's clipper, drawing the same pixels, on a rigc build (the `--poser`
274
281
  row) — so a slot from
@@ -1117,6 +1124,8 @@ bun cli.ts render --candidate path/to/spine --slot <name,…> | --hide <name,
1117
1124
  bun cli.ts render --candidate path/to/spine --geometry # + geometry.json per set: the numbers the frames were drawn from
1118
1125
  bun cli.ts render --candidate path/to/spine --folds # + folds.json per set: which mesh triangles turn over, and when
1119
1126
  bun cli.ts render --candidate path/to/spine --pass-through skirt:leg # + pass-through.json: where a cloth goes through a leg
1127
+ bun cli.ts render --candidate path/to/spine --angle-range tail:hip # how far a bone turns against another, per animation
1128
+ bun cli.ts render --candidate path/to/spine --viewport-from render/ # an earlier render's grid: two builds, one pixel grid
1120
1129
  bun cli.ts preview --candidate path/to/spine [--candidate path/to/another …] [--animation …] [--out preview.html]
1121
1130
  bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
1122
1131
  bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
@@ -1129,8 +1138,13 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
1129
1138
  key with the curve kind. A rig with a `deform` timeline also gets the **`DEFORM`
1130
1139
  block** — per key, what that key did to the geometry: the area and stretch
1131
1140
  extremes, how far each vertex moved and whether the winding survived
1132
- (**§4.11.2**). It takes no `--profile`, it never gates, and it does not write
1133
- anything — so the figures are readable on a build the gate is refusing.
1141
+ (**§4.11.2**). A rig with a physics constraint also gets the **turn lines** —
1142
+ per constraint and animation, the largest turn its bone makes in one 1/60 s step,
1143
+ walked from the setup pose as a player starts it (**§4.6.1**) — and, given
1144
+ `--lever-fold <fraction>`, the **lever lines**: the steps a `scaleX` drive's
1145
+ inertia lever divides by a collapsed length (**§4.6.2**). It takes no
1146
+ `--profile`, it never gates, and it does not write anything — so the figures are
1147
+ readable on a build the gate is refusing.
1134
1148
 
1135
1149
  ⭐ **It reads what `ingest` writes, given the pack** — `--atlas-in <pack.atlas>`,
1136
1150
  with `build`'s meaning (§0.2). That matters more here than it looks: printing the
@@ -2293,7 +2307,7 @@ these messages prints the outline walk, because the walk is the fix:
2293
2307
  | a stated `hull` that disagrees | `hull 25 disagrees with the triangles, whose outline has 16 vertices (0 → 1 → 2 → …). Delete "hull" and rigc derives it, or state 16` |
2294
2308
  | outline vertices that are not the first `hull` of the list | `hull vertices must come first; vertex 19 is on the boundary and vertex 6 is not. The triangles' outline runs 0 → 1 → 2 → 3 → 4 → 9 → 14 → 19 → 24 → 23 → 22 → 21 → 20 → 15 → 10 → 5: list those 16 vertices first, in that order, then the 9 interior vertices` |
2295
2309
  | outline vertices that are first but zigzag | `hull vertices must trace the outline in order; the triangles' outline runs 0 → 1 → 3 → 5 → … → 4 → 2, so vertex 3 has to follow vertex 1 in the list, and vertex 2 does. Renumber the vertices along that walk` |
2296
- | triangles with no single outline | `the triangles do not tile the outline: 25 vertices with a 16-vertex outline tile as 32 triangles and there are 33` — an unused vertex or a doubled interior triangle; or `the triangles' outline is not one closed loop: vertex 4 has 4 boundary edges` — a pinch, a hole, or a doubled triangle on the perimeter |
2310
+ | triangles with no single outline | `the triangles do not tile the outline: 25 vertices with a 16-vertex outline tile as 32 triangles and there are 33` — an unused vertex or a doubled interior triangle; or `the triangles' outline is not one closed loop: vertex 4 has 4 boundary edges` — a pinch, a hole, or a doubled triangle on the perimeter. Triangles in pieces that share no vertex get the same sentence and then `— the triangles are 2 pieces that share no vertex …` naming `rigc split`, which writes one mesh per piece (§3.4.11) |
2297
2311
 
2298
2312
  🚨 **A row-major grid is the case this catches**: a 5×5 grid's perimeter is 16 of
2299
2313
  its 25 vertices, interleaved with the interior, so no `hull` can describe that
@@ -3541,6 +3555,104 @@ packed region is still refused.
3541
3555
  `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` (§5.2) holds the block and
3542
3556
  every frame the timelines show against the file.
3543
3557
 
3558
+ #### 3.4.11 `split` — one mesh per piece, when the triangles are several
3559
+
3560
+ One mesh is one outline (the `hull` rule above), so a mesh whose triangles fall into
3561
+ pieces that share no vertex is refused at compile — typically after the transparent
3562
+ triangles that bridged two legs were deleted:
3563
+
3564
+ ```
3565
+ rigc compile error: skin "default" slot "legs" attachment "legs": the triangles' outline is not one closed
3566
+ loop: the walk from vertex 0 closes after 8 of 16 boundary vertices — the triangles are 2 pieces that share no
3567
+ vertex, and one mesh is one outline: `rigc split --rig <rig> --motion <motion> --slot legs --attachment legs
3568
+ --out <dir>` writes one mesh per piece, its deform keys renumbered (AUTHORING §3.4.11) (see rigc guide §5.1)
3569
+ ```
3570
+
3571
+ Each name in that command is one shell word: a slot, placeholder or skin whose name
3572
+ holds a space or a character a shell reads (`'`, `$`, `;`, …) is printed
3573
+ single-quoted, a `'` inside it as `'\''` — `--slot 'left legs'` — and a plain name
3574
+ is printed as it is. So the command copies into a shell as printed, with only
3575
+ `<rig>`, `<motion>` and `<dir>` to fill in.
3576
+
3577
+ The fix is one mesh per piece, and every number in it is a renumbering: each piece's
3578
+ `uvs`, its `weights` (or `vertices`), its `triangles`, and every `deform` key that
3579
+ moved it (§4.11). `rigc split` writes that proposal from the two spec files — not from
3580
+ a build, because a mesh of several pieces has none:
3581
+
3582
+ ```bash
3583
+ rigc split --rig rig.json --motion motion.json --slot legs --out split/
3584
+ # rigc split /abs/rig.json
3585
+ # .. motion /abs/motion.json
3586
+ # .. mesh skin "default" slot "legs" attachment "legs": 18 vertices, 16 triangles, 2 pieces (triangles that share a vertex are one piece)
3587
+ # .. piece 1 slot "legs" (the original slot): 9 vertices, 8 triangles, hull 8 — original order kept: [3, 4, 5, 6, 7, 8, 9, 16, 17]
3588
+ # .. piece 2 slot "legs_2" (new, drawn after "legs"): 9 vertices, 8 triangles, hull 8 — renumbered outline first, as the hull rule needs: piece vertex 0.. is original 0 → 1 → 2 → 15 → 10 → 11 → 12 → 13, then the interior [14]
3589
+ # .. deform animation "walk": 5 key(s) re-sliced into 2 timelines — "legs" 3 with a run, "legs_2" 2 with a run
3590
+ # .. track animation "walk": slot "legs" "rgba" copied to "legs_2"
3591
+ # .. images ../ (the rig spec's own, restated from /abs/split — it resolves to /abs)
3592
+ # rigc: wrote /abs/split/rig.json
3593
+ # rigc: wrote /abs/split/motion.json
3594
+ # rigc: wrote /abs/split/split.json
3595
+ # rigc: build it with rigc build --rig /abs/split/rig.json --motion /abs/split/motion.json --out <dir>
3596
+ ```
3597
+
3598
+ What it decides, and the one reason for each:
3599
+
3600
+ | | |
3601
+ | --- | --- |
3602
+ | a piece | triangles that share a vertex — nothing else: no distance, no art, no threshold. Two triangles touching at one vertex are one piece, and that piece is refused for its pinch as the compiler would refuse it |
3603
+ | which piece is 1 | the one whose first triangle is listed first. Spine draws a mesh's triangles in list order, so that is the piece the original drew first; when the pieces' triangles interleave in the list, an `order` line says the split draws each piece whole and where they overlap the stacking can differ |
3604
+ | where the pieces go | piece 1 stays in the slot under its placeholder, so everything naming the slot keeps its meaning; piece k gets a new slot **`<slot>_<k>`** on the same bone with the same colour, dark colour and blend, directly after the original in `slots` — the draw order — filled under the **same placeholder**, so `name`, `path` and `image` resolve as before. A derived name the rig already declares is refused by name |
3605
+ | a piece's vertex order | the original's relative order when it already lists the piece's outline first and in order; otherwise the outline walk, then the interior in original order — the order the `hull` refusals ask for, so the piece builds without a second edit |
3606
+ | `hull`, `edges` | `hull` is left out (rigc derives it); stated `edges` are split with their vertices, and an edge joining two pieces is refused — so is one from a piece to a vertex no triangle names, the refusal saying `(in no piece: no triangle names it)` for that end. An edge whose **two** vertices no triangle names has no piece to go in: it is dropped, and named on a `dropped` line — `dropped 1 stated edge(s) between vertices no triangle names, edge 2 (vertex 18 to vertex 19): …` — so the pieces' `edges` are the original's minus exactly the ones that line names |
3607
+ | a deform key | its run re-sliced: each number goes to its vertex's piece at the vertex's new index, written as `offset` + `vertices`, and an index inside a piece's new run that the original run did not cover is written **`0`** — what the parser held there, because a key is a sparse edit and both encodings are offsets from setup. A piece the run did not reach gets the key with no `vertices`, the setup pose, which is what the original did to it. Every piece keeps the key's time and curve |
3608
+ | colour and attachment tracks on the slot | copied to every new slot; an `attachment` key showing the split placeholder shows each piece, any other shows nothing in the new slots. `setup` (or the slot's `attachment`) the same way |
3609
+ | `invariants.deformMayFold` naming the slot | its entry copied to the new slots |
3610
+ | `invariants.meshSlots` | left as stated, and the line does the arithmetic: the mesh slots the split adds, the mesh attachments the proposal's skins then hold (every `type: "mesh"` without a `source`, as the compile counts them), and — when that is over the budget — the figure to raise it by: `invariant invariants.meshSlots states 1, and the split adds 1 mesh slot(s): the proposal's skins hold 2 mesh attachment(s), 1 over the budget, so its build is refused at compile ("2 mesh slot(s) emitted") unless the rig spec raises invariants.meshSlots by 1, to 2; …`. Within the budget it says so. The refusal it predicts is the compile's (§3.7), which stops before `A13_MESH_BUDGET` runs |
3611
+ | a vertex no triangle names | no piece can hold it and still build, and it draws nothing in either rig: dropped, and named on a `dropped` line and in `split.json` |
3612
+ | `images` | restated from `--out`, so every `image` resolves to the file it resolved to before |
3613
+
3614
+ It writes three files into an `--out` that is **absent or empty** — `rig.json` and
3615
+ `motion.json`, your two specs with the mesh split and nothing else changed but
3616
+ `images`, and `split.json` (`rigc-split/1`): for each piece its slot, placeholder,
3617
+ `vertices` (piece vertex `j` is original vertex `vertices[j]`), `triangles` (original
3618
+ triangle indices) and `hull`, and the `dropped` vertices. There is no in-place flag:
3619
+ your specs are the only statement of what the proposal must reproduce, so it is
3620
+ written beside them, never over them. Build it and look before adopting it.
3621
+
3622
+ A mesh of one piece is said so on one line — `rigc split: skin "default" slot "legs"
3623
+ attachment "legs" is one piece — …; nothing to split, and nothing written` — exit 0.
3624
+ A slot, attachment or skin the specs do not have is refused with the names they do,
3625
+ exit 2. What the split cannot carry is refused by name, exit 1, because each needs a
3626
+ decision the specs do not state:
3627
+
3628
+ | Refused | Why |
3629
+ | --- | --- |
3630
+ | a `generator` mesh | a generator builds one outline by construction; there are no authored triangles |
3631
+ | a `sequence` on the mesh, or a sequence timeline on it | which frames each piece shows is not stated |
3632
+ | `"boneIndexing": "raw"` | write the weights by name (`weights`) and split that |
3633
+ | a deform key stated as a `transform` (§4.11.1) | the model is evaluated over the whole attachment's geometry, and splitting changes the geometry it reads; state it as a `vertices` run |
3634
+ | a `fromVertex` key over a vertex with two or more bones, a run past the array | the compiler refuses the key; so does the split |
3635
+ | the slot filled in another skin | the new slots would show this skin's pieces under that skin too |
3636
+ | a linked mesh borrowing the mesh | its geometry would have to be one of the pieces |
3637
+ | a clip whose range ends at the slot | the new slots would fall outside it |
3638
+ | a draw-order key, or a slot-property track on a `group`, naming the slot | where the new slots go then is not stated |
3639
+ | a piece that is not one mesh either (a pinch, a hole) | splitting does not fix it; the message is the outline refusal for that piece |
3640
+
3641
+ ⭐ **What makes the proposal exact rather than plausible**: a piece's vertices carry
3642
+ the original's entries unchanged, so posing the split rig puts every piece vertex where
3643
+ the original put its vertex. The selftest holds that at tolerance 0 — the setup pose and
3644
+ every frame of every animation, deform keys, colour and attachment keys included, on
3645
+ the core and on spine-core — on a public probe of two legs, against the same mesh with
3646
+ its bridge still in (deleting triangles moves no vertex, so that mesh is the original
3647
+ every frame is held to), and plants a renumbering off by one and a deform key left
3648
+ un-renumbered to see each go red (`SPL01`–`SPL11`). The three lines above are held
3649
+ the same way: the printed command on a slot, placeholder and skin named with spaces
3650
+ and shell characters runs in `/bin/sh` and splits that mesh, and printed bare it does
3651
+ not (`SPL12`); the proposal's edges map back to the original's minus the named one
3652
+ (`SPL13`); and at an exact `meshSlots` budget the raise the line states is the build
3653
+ refusal's own count minus its budget, raised by it the proposal builds green, and one
3654
+ more slot of budget prints no raise (`SPL14`).
3655
+
3544
3656
  ### 3.5 `constraints` — 4.3's single typed array
3545
3657
 
3546
3658
  Spine 4.3 folds every constraint into one `constraints` array with a `type`
@@ -4313,6 +4425,97 @@ physics, no skin set. `A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` holds it (§5.2)
4313
4425
  declaration the profile did not read would be one nothing reads. The
4314
4426
  declaration is a statement to the gate and changes no emitted Spine byte.
4315
4427
 
4428
+ ### 3.8 `stops` — a swinging chain held off a leg
4429
+
4430
+ Spine has no collider and no angle limit — a physics constraint's `limit` is a
4431
+ velocity cap — so a skirt chain that physics swings carries its hem straight
4432
+ through the thigh it hangs in front of, on a build that passes every assertion.
4433
+ `render --pass-through` (§8.5) names those frames. A **stop** is the stand-in:
4434
+ a point on the leg, a frame that aims at it from the chain's root, the swinging
4435
+ link's rotation copied onto that frame, and the **drawn** link — the bone the
4436
+ mesh is weighted to — held by a transform clamp, so it may swing away from the
4437
+ leg and not past its rest angle toward it. One entry here is that network,
4438
+ written once:
4439
+
4440
+ ```json
4441
+ "stops": [
4442
+ {
4443
+ "chain": "hem",
4444
+ "links": ["hem0", "hem1"],
4445
+ "against": "thigh",
4446
+ "at": [-10, -84],
4447
+ "inward": 0,
4448
+ "outward": 60,
4449
+ "lower": { "inward": 0, "outward": 30 }
4450
+ }
4451
+ ]
4452
+ ```
4453
+
4454
+ | Field | What it is |
4455
+ | --- | --- |
4456
+ | `chain` | the name the point, the frame and the aim are called after |
4457
+ | `links` | the chain's swinging links, root first — each written by a constraint in the rig spec's `constraints` (its physics, or whatever swings it), each one below the first hanging from the drawn link of the one above |
4458
+ | `against` | the bone the point rides on — the leg |
4459
+ | `at` | the point, `[x, y]` in `against`'s local space, as a bone's `x`/`y` are in its parent's. Put it on the leg's outer edge at the hem's height |
4460
+ | `inward`, `outward` | the first drawn link's range, as its two ends, in degrees from its rest angle, counted **positive away from the point**. `inward: 0, outward: 60` is "never past rest toward the leg, up to 60° away"; `inward: -5` lets it turn 5° past rest toward the leg |
4461
+ | `lower` | `{ "inward", "outward" }`: each lower drawn link's range off its own rest bend, the same way round. Required when `links` has more than one, refused when it has one |
4462
+
4463
+ Every field is required and none has a default. **Inward** is the turn from the
4464
+ first link toward the aim line — toward the point, which is on the leg — read off
4465
+ the setup pose, so a stop on the left side of a skirt and one on the right need no
4466
+ sign of their own.
4467
+
4468
+ **What it expands to.** Before anything else reads the rig, `parseRigSpec`
4469
+ replaces the entry with bones and constraints — nothing downstream knows a stop
4470
+ was there, and the build is byte for byte the build of the same network written
4471
+ by hand (`LG01`):
4472
+
4473
+ | Generated | Kind | What it is |
4474
+ | --- | --- | --- |
4475
+ | `<chain>_touch` | bone | the point: parent `against`, `x`/`y` from `at`. Listed after every other bone |
4476
+ | `<chain>_stop` | bone | the frame: the first link's parent, `x`, `y` and `length`, rotated at the point. Listed just before the first link |
4477
+ | `<link>_rel` | bone | a child of the frame, `length` the link's, rotated by the link's rest angle off the aim line. Just after the first link |
4478
+ | `<link>_d` | bone | the first link's **drawn** link — the same as `_rel`, just after it. For each lower link, its drawn link: a sibling with the link's own `length`, `x`, `y`, `rotation`, scale, shear and `inherit`, just after that link |
4479
+ | `<chain>_stop_aim` | ik | aims the frame at the point (`mix` 1). Just before the last constraint that writes the first link |
4480
+ | `<link>_rel_copy` | transform | gives `_rel` the first link's world rotation (`mixRotate` 1). Just after that constraint |
4481
+ | `<link>_clamp` | transform | local to local, `clamp: true`: gives the drawn link its source's turn, held to the range — the first link's from `_rel`, each lower link's from the link. Just after `_rel_copy` for the first, just after the last constraint that writes the link for a lower one |
4482
+
4483
+ So a chain with a stop is authored with each link's parent below the drawn link
4484
+ above it — `hem1`'s control bone under `hem0_d` — and its mesh weighted to the
4485
+ `_d` bones; those names are the expansion's, and the build refuses a name it
4486
+ generates that the rig already declares. The order is the point of the
4487
+ placement: the frame is aimed before the link swings, and each swing is copied
4488
+ and clamped before anything below reads the drawn link.
4489
+
4490
+ The frame's angles are read off the setup pose the rig spec states — its bones as
4491
+ written, no constraint applied, in the exact frame `src/transform.ts` measures in — so at rest the drawn
4492
+ link lies on the link and the clamp's bounds are its rest angle moved by
4493
+ `inward` and `outward`. A bone the angles are read through that takes its place
4494
+ `from` a manifest is refused: its numbers are not known until the compile.
4495
+
4496
+ ⚠️ **The link's swing has to be declared in the rig spec.** The motion spec's
4497
+ `physics` table (§4.6) is emitted after every rig constraint, past the copy and
4498
+ the clamp, which would then read the link before it swings; a link only that
4499
+ table drives is refused as one nothing swings, and a table entry on a stopped link
4500
+ is refused by name.
4501
+
4502
+ Refused, each naming the stop, the field and the value (§5.1): a missing
4503
+ `chain`, `links`, `against`, `at`, `inward`, `outward` or (on a chain of two or
4504
+ more) `lower`; a `lower` on one link; a link or an `against` the rig does not
4505
+ declare; a link stopped twice; the root as a link; `at` that is not two numbers;
4506
+ `inward` above `outward`, at either range; a bound past ±180° once the rest angle
4507
+ is added (the clamp reads a local rotation in (−180, 180]); a generated bone or
4508
+ constraint name already declared, or generated by two stops; a link no rig
4509
+ constraint writes; a lower link not below the drawn link above it; an `against`
4510
+ that hangs below the chain; a point on the link's root or on its own line at
4511
+ rest; a frame parent scaled unevenly, sheared or reflected, or such a bone
4512
+ between two links; a link below a bone a later stop generates; a `from` bone in
4513
+ the angles' path.
4514
+
4515
+ The stop decides nothing about *when* — it is what the object can do. Read it
4516
+ with `render --pass-through <cloth>:<leg>` (§8.5) on the motion that swung
4517
+ through before.
4518
+
4316
4519
  ---
4317
4520
 
4318
4521
  ## 4. The motion spec, field by field
@@ -4613,7 +4816,8 @@ a delta from the constraint's own setting.
4613
4816
  of `mix`.** The runtime documents no range for the first three, and
4614
4817
  `PhysicsConstraintPose` documents `mix` as "a percentage (0+)" — so a negative
4615
4818
  wind is the other direction and a `mix` of `1.5` is an over-mix. Both are real
4616
- and both compile.
4819
+ and both compile. An `inertia` above 1 is kept and posed by the editor too, and
4820
+ what it does to a bone is read rather than refused — §4.6.1.
4617
4821
  - ⚠️ **A `mix` or `strength` key of exactly `0` is legal where a setup value of
4618
4822
  `0` is not**, and the difference is not an inconsistency: a setup pose says what
4619
4823
  the constraint IS and a key says what it is doing for a stretch. For `mix`,
@@ -5023,6 +5227,21 @@ file can be read; it is a **key** outside its bound that `build` refuses (§4.4)
5023
5227
  number: a non-number would reach the file as `null`, which the runtime reads as
5024
5228
  **zero**, so `"mass": "heavy"` would ship a constraint that never settles.
5025
5229
 
5230
+ 🌀 **`inertia` has no upper bound, and above 1 a link can whirl** (issue #1423).
5231
+ Values above 1 are kept by the editor and posed: the Spine 4.3.26 editor imports
5232
+ 2, 3 and 10 and exports them unchanged, the export posing as the build does, and
5233
+ the user guide describes the field as 0–100% with no cap stated. So `build`
5234
+ refuses none of them. What one does is a turn: a bone whose parent moves is kicked
5235
+ by `inertia` times that motion, and a 1-link rotate drive under a parent keyed
5236
+ ±15° turns its bone at most 2.29–4.10° (by its `fps`), 18.78°, 35.61° and 153.38°
5237
+ in a single 1/60 s step at `inertia` 1, 2, 3 and 10 — the last half a turn between
5238
+ two frames. **`explain`
5239
+ prints that turn for every physics constraint and every animation** (§4.6.1), so a
5240
+ whirl is a figure you read rather than a frame you happen to watch. On a `scaleX`
5241
+ drive, a high `inertia` down a chain can also make a link's lever divide by a
5242
+ length near zero and throw its scale offset by hundreds in one step — `explain
5243
+ --lever-fold <fraction>` names those steps (§4.6.2).
5244
+
5026
5245
  🧮 **Each value inside its bound is not enough: the step has to settle**
5027
5246
  (issue #1416). With `step = 1 / fps`, a constraint that drives anything must
5028
5247
  satisfy
@@ -5059,6 +5278,16 @@ keeps more velocity per step, yet it is what widens the bound: from 2 at `0`
5059
5278
  toward 4 as it nears `1`, which is itself out); the refusal prints both sides so
5060
5279
  you can see how far each lever has to go.
5061
5280
 
5281
+ 📏 **What a value does to a link is read off a render, not estimated.** The bound
5282
+ above says whether a spring settles; it does not say how far it swings, which
5283
+ depends on the link's length, its parent's motion and every value together. Render
5284
+ the animation with `--angle-range <link>:<parent>` (§8.7) and the line prints how
5285
+ far the link turns against the bone it hangs from — the lowest and the highest
5286
+ angle and the frames they fall on — so a change of `inertia` or `strength` is read
5287
+ as the change of that range, before and after. On the selftest's angle probe a
5288
+ link's span widens when its `inertia` is raised from 0.2 to 0.8, and `AR02` prints
5289
+ both spans.
5290
+
5062
5291
  ⚠️ **What `rigc render` draws a spring with is not a player's clock to the bit.** A
5063
5292
  constraint integrates whole steps of `1 / fps` and shows the rest as a lag weight, so
5064
5293
  a frame lasting a whole number of steps (12, 20, 30 or 60 fps against the default
@@ -5126,6 +5355,134 @@ skeleton JSON, so the parser is the only check it gets: it is
5126
5355
  `default` that is not a number would pass the gate and the round trip before
5127
5356
  becoming a `NaN` mix duration in the player.
5128
5357
 
5358
+ #### 4.6.1 How far a physics bone turns in one step — `explain`'s turn lines
5359
+
5360
+ Under its `physics constraints` block, `explain` prints one line per physics
5361
+ constraint per animation: the largest turn the constraint's bone makes in one
5362
+ step of the animation's walk, the step it happens at and its time.
5363
+
5364
+ ```
5365
+ turn per step (a reading, never a gate: each animation walked by rigc's core as a player starts it — …)
5366
+ .. turn "swing": physics "p_i10_f60" (bone "l_i10_f60") turns at most 153.38° in one step — step 1 of 240 (t=0.0167s)
5367
+ .. turn "sweep": physics "whip" (bone "ahoge_whip") turns at most 0.29° in one step — step 90 of 193 (t=1.5000s)
5368
+ ```
5369
+
5370
+ **The walk.** Each animation is walked as a player that starts it plays it: a
5371
+ skeleton with no skin set, at the setup pose with every physics state reset there,
5372
+ then the animation in steps of 1/60 s until its duration (one step at least) — the
5373
+ physics stepped by the step itself, as spine-core's `update(1/60)` steps it. It is
5374
+ posed by rigc's core; the repository's selftest walks the same probe through
5375
+ spine-core and holds the two to the same figure and step, to the bit (`WH01`).
5376
+
5377
+ ⚠️ **Not `render`'s walk, on purpose.** `render` resets the physics at the
5378
+ animation's first pose, so a bone the first key moves away from the setup pose is
5379
+ already there when the physics starts. A player starts from the setup pose, and the
5380
+ first step moves every keyed bone at once — which is where a high `inertia` answers
5381
+ hardest: on the probe above, inertia 10 turns 153.38° in step 1 from the setup pose,
5382
+ and walked from the animation's first pose at most 4.10°, 3.82° and 13.68° at `fps`
5383
+ 15, 30 and 60. The whirl is a property of
5384
+ starting the animation, so it is read where a player starts it.
5385
+
5386
+ **The turn** is the signed angle between the bone's world x axis at two consecutive
5387
+ steps, in (−180°, 180°], printed as its magnitude to two decimals; the largest over
5388
+ the walk, the earliest step on a tie. It is the change of the direction the bone is
5389
+ drawn along, whatever moved it — its physics, its parent, a key, a mirrored scale —
5390
+ so every physics constraint has a line, whichever components it drives. A turn near
5391
+ 180° in one step is a direction a frame cannot tell from its opposite. A bone that
5392
+ never turns reads `does not turn in any of N step(s)`.
5393
+
5394
+ **Not read, and said so:** a skin-required constraint (`not read — skin-required,
5395
+ and a skeleton with no skin set does not step it`); an animation a slider applies —
5396
+ a table posed at a value the consumer holds, not a span of time (`A39`'s reading) —
5397
+ whose line names the slider, while the animation that moves the slider's value is
5398
+ walked; a walk the core refuses to pose (`not read — the core refused to pose the
5399
+ walk — …`); a bone whose world direction stops being a number (`not read — its
5400
+ bone's world direction is not a number from step k (t=…s)`), rather than the finite
5401
+ figure it reached before (`WH04`); and a skeleton with no animation.
5402
+
5403
+ **A reading, not a gate.** Nothing in it is refused, and `explain`'s exit is the
5404
+ same whatever it prints — `src/whirl.ts`, which computes it, is imported by
5405
+ `explain` alone (`WH02`). Whether half a turn per frame is the motion you meant is
5406
+ yours: a link that should trail its parent wants an `inertia` at or under 1, and
5407
+ the figure is how you see the difference without watching the right frame. A walk
5408
+ that ignores the parent's motion — the turn read in the parent's frame, or
5409
+ `render`'s walk — misses the figures above, and the selftest plants both to show it
5410
+ (`WH03`).
5411
+
5412
+ #### 4.6.2 Where a `scaleX` lever divides by a collapsed length — `explain --lever-fold`
5413
+
5414
+ A physics constraint that drives `scaleX` kicks its scale offset by its bone's
5415
+ motion **divided by a length**: `scaleOffset += (dx·cos + dy·sin) · inertia / rr`.
5416
+ `limit` caps `dx` and `dy`; nothing caps the division, and the kick is skipped
5417
+ only when `rr` is not above 0. `rr` is the bone's `length` times its world x-axis
5418
+ length (`√(a² + c²)` of the world matrix the step is handed) — and, on a drive
5419
+ that does not also rotate (`rotate` and `shearX` both 0), **less `scaleLag ·
5420
+ shown`**, the last integration's move of the offset weighted by how much of it was
5421
+ shown. When that divisor collapses, the next kick is enormous, children inherit
5422
+ the scale, and the lengths multiply down a chain: [measured] on a 5-link chain
5423
+ driven on `scaleX` alone at `inertia` 10, `fps` 15 and 0.95 of the step bound,
5424
+ link 3's offset jumps 24.3 → 349 in one step and the tip passes 10⁶ units with
5425
+ every assertion green (issue #1422). Whether a chain gets there depends on how far
5426
+ an offset swings, which no number in the spec states — so `build` refuses nothing
5427
+ for it, and `explain` reads it when you ask:
5428
+
5429
+ ```
5430
+ lever under 0.5× setup (a reading, never a gate: the turn lines' walk; …)
5431
+ .. lever "quiet": physics "p_v_1" (bone "v_1") reads nothing in 240 step(s): no lever divides by less than 0.5× its setup divisor 50.00, and no step moves its scale factor by its setup scale
5432
+ .. lever "quiet": physics "p_v_3" (bone "v_3") lever divides by less than 0.5× its setup divisor 50.00 on 8 of 240 step(s) [8, 17, 28, 56, 83-84, 92, 99] — least 2.940 (0.05880× setup; world x axis 0.5636× setup) at step 17 (t=0.2833s)
5433
+ .. lever "quiet": physics "p_v_3" (bone "v_3") scale factor moves by more than its setup scale in one step on 73 of 240 step(s) [5-13, …] — largest 1268, scale offset -616.8 → 651.4 at step 25 (t=0.4167s)
5434
+ ```
5435
+
5436
+ **The walk** is the turn lines' (§4.6.1): from the setup pose with every physics
5437
+ state reset, in steps of 1/60 s to the duration, posed by rigc's core. The
5438
+ repository's selftest walks the same probe through spine-core and holds every
5439
+ step and figure to the core's, to the bit (`LV01`).
5440
+
5441
+ **What it names**, per animation, for every constraint that drives `scaleX`:
5442
+
5443
+ - **a collapsed division** — a step where the lever divides (`rr` above 0) by less
5444
+ than `--lever-fold` × the divisor it takes at the setup pose (`length` × the
5445
+ world x-axis length there). A lever divides on a step where the constraint is
5446
+ not pending (the first update after a reset integrates nothing), its `mix` is
5447
+ not 0 and its `inertia` is not 0. The line gives the steps as runs and the
5448
+ least divisor, as that fraction, with the world x-axis length the lever read on
5449
+ that step beside it.
5450
+ - **a jump** — a step where the scale factor the constraint applies, `1 +
5451
+ scaleOffset · mix · scaleX`, moves by more than 1: by more than the bone's own
5452
+ setup scale, since the factor is 1 at the setup pose and multiplies whatever
5453
+ scale the bone already has. The line gives the steps and the largest move,
5454
+ with the offset on either side of it.
5455
+
5456
+ ⚠️ **The divisor, not the world length alone.** On the step above, link 3's lever
5457
+ reads its world x axis at 0.5636 of its setup length — not near zero — and divides
5458
+ by 0.0588 of its setup divisor, because `scaleLag · shown` (25.24) takes almost all
5459
+ of `length · |world x axis|` (28.18) away. A reading of the world length alone misses
5460
+ that step at every fraction below 0.5636, and names steps where the kick is skipped
5461
+ altogether: the same link reads 0.481 of its setup length three steps earlier while
5462
+ its divisor is −8.47, so nothing is divided (`LV04`). On a drive that rotates too
5463
+ the two are one number. A reading of the parent's **local** scale sees none of it —
5464
+ physics moves the world matrix, never a local transform (`LV03`).
5465
+
5466
+ **The fraction is yours, stated.** `--lever-fold` has no default: what counts as
5467
+ collapsed is not a number the spec carries. Without the flag `explain` prints one
5468
+ line saying the reading was not taken (`` not read — `--lever-fold <fraction>` … ``);
5469
+ a value that is not a number above 0 and at most 1 is refused by name, exit 2
5470
+ (`LV05`).
5471
+
5472
+ **Not read, and said so:** constraints that drive no `scaleX`, on one line —
5473
+ no lever of theirs divides by a length, which is what the gallery's and the
5474
+ public fixtures' physics read (`LV02`); and, per constraint, the cases §4.6.1
5475
+ lists — skin-required, an animation a slider applies, a walk the core refuses — plus
5476
+ a divisor, world length or offset that stops being a number (`not read — … is not
5477
+ a number from step k`), rather than the steps it named before.
5478
+
5479
+ **A reading, not a gate.** Nothing in it is refused, `explain`'s exit is the same
5480
+ whatever it prints, and the module that computes it is the turn lines' own
5481
+ (`src/whirl.ts`, imported by `explain` alone — `WH02`). A chain that collapses is
5482
+ yours to judge: an `inertia` at or under 1, a stiffness further from the bound, or
5483
+ `rotate` driven alongside (which drops the lag term from the divisor) are the
5484
+ levers you hold.
5485
+
5129
5486
  ### 4.7 `drawOrder` — reordering the slots over time
5130
5487
 
5131
5488
  The one timeline that names no target, so it sits on the animation rather than in
@@ -5627,7 +5984,9 @@ convention:
5627
5984
  an edit of part of it, so it starts at deform index 0 and runs to the end — which
5628
5985
  is why `fromVertex` and `offset` are refused beside it. A model applied to part
5629
5986
  of a run leaves a **step at the run's edge**. If you want a
5630
- partial run, write it.
5987
+ partial run, write it — or, to move part of the attachment by the model, give the
5988
+ key an `envelope` (below), which weighs every vertex rather than cutting the run,
5989
+ so there is no edge to step at.
5631
5990
 
5632
5991
  **There is no `parallax` kind, and the reason is worth stating.** A pure depth
5633
5992
  slide — `d = z · offset`, no angle — is this form with a term dropped: subtract
@@ -5691,7 +6050,10 @@ about where they came from. A `yaw` past its fold angle folds the mesh and A39
5691
6050
  says so — the construct removes the transcription, not the arithmetic's
5692
6051
  consequences. One kind is the exception and it is an exception with a proof:
5693
6052
  `affine` refuses a determinant at or below zero, and above zero a positive
5694
- determinant means no triangle **can** reverse.
6053
+ determinant means no triangle **can** reverse. ⚠️ The proof is about the
6054
+ un-enveloped key: under an `envelope` each vertex takes its own share of the map,
6055
+ so the keyed map is no longer affine, the `derived` line says so, and A39
6056
+ measures it like any other.
5695
6057
 
5696
6058
  **A model that evaluates to nothing is refused too.** Every parameter can be
5697
6059
  individually legal and the model still come out as a **run of zeros** — a `wave`
@@ -5758,6 +6120,83 @@ the same turn, where the missing number is a depth rather than a vertex, take a
5758
6120
  per-key `DEFORM` report block quotes the model above rather than re-evaluating it
5759
6121
  — **§4.11.2**.
5760
6122
 
6123
+ ##### `envelope` — which vertices the model moves, and how much
6124
+
6125
+ A transform moves **every** vertex of the attachment, so a `wave` on a skirt
6126
+ ripples the waistband too and "only the hem ripples" cannot be written as the
6127
+ model alone. An `envelope` on the transform gives each vertex a **weight in
6128
+ [0, 1]**, and each vertex's offset is multiplied by its weight before it is
6129
+ written. Every kind takes one:
6130
+
6131
+ ```json
6132
+ { "t": 1, "transform": {
6133
+ "kind": "wave", "amplitude": 6, "wavelength": 64, "phase": 90, "along": "x", "axis": "y",
6134
+ "envelope": { "axis": [0, 1], "from": 224, "to": 120, "shape": "smooth" } } }
6135
+ ```
6136
+
6137
+ ⚠️ **These numbers are the public skirt probe's** (`fixtures/public.ts`,
6138
+ `skirtFixture`): its waist row sits at world `y` 224 and its hem rows at 120
6139
+ and below. On your part, `from` and `to` are positions you read off the part —
6140
+ `explain` prints where its vertices sit — and nothing here is a default.
6141
+
6142
+ It comes in two forms, and a key states exactly one:
6143
+
6144
+ | Field | Meaning |
6145
+ | --- | --- |
6146
+ | `axis` | **ramp, required.** The unit direction a vertex's position is read along, `[x, y]` — `[0, 1]` reads `y`, `[1, 0]` reads `x`, a diagonal reads across both. Its length is 1 on the model's 1e-6 grid |
6147
+ | `from` | **ramp, required.** The position along `axis` where the weight is **0** — and before which it stays 0 |
6148
+ | `to` | **ramp, required.** The position along `axis` where the weight is **1** — and past which it stays 1. Differs from `from`; either may be the larger |
6149
+ | `shape` | **ramp, required.** How the weight rises between them: `"linear"` is `u`, `"smooth"` is `u²(3 − 2u)`, flat at both ends — the two curves `influence.shape` names (§3.4.7), read rising rather than falling |
6150
+ | `mask` | **mask, required, and alone.** The file name of this attachment's own `soft` mask (§3.4.6). The weight is the level that mask was sampled to at the vertex when the mesh was built, black 0 and white 1 — the same number the soft region's binding carries |
6151
+
6152
+ For a ramp, `u = clamp((axis · position − from) / (to − from), 0, 1)` per
6153
+ vertex, and the weight is the shape at `u`. Weights are quantised onto the
6154
+ model's 1e-6 grid and each emitted offset is `round(w × the un-enveloped
6155
+ offset)` on the same grid — so a weight of 0 writes exactly 0, a weight of 1
6156
+ writes exactly the offset the key writes with no envelope, and a reader holding
6157
+ the weights `explain` prints reproduces every number.
6158
+
6159
+ ⭐ **The ramp is read in the space the model is.** The weight multiplies the
6160
+ offset the model produced at a vertex, so the position that picks it is the
6161
+ position the model read: the attachment's own space while every vertex is one
6162
+ pair of one bone, and setup **world** past that (above). `from` and `to`
6163
+ therefore change units exactly when `radius`, `about` and a bend's `from` do,
6164
+ and the `derived` line that says so applies to them too. A skirt weighted to
6165
+ its chains is on the world path, which is why the example's positions are world
6166
+ `y`.
6167
+
6168
+ 🔒 **It is baked, not played.** The weighted offsets are the deform table the
6169
+ file carries, so the runtime is asked to do nothing it did not already do:
6170
+ rigc's core and spine-core pose an enveloped key identically, and the gate
6171
+ judges it like any table — A35 the run, A39 the winding, `render --folds` the
6172
+ posed triangles. An envelope that rises over too short a distance can fold a
6173
+ triangle the model alone could not (a wave that reads `x` and displaces `y` is
6174
+ a shear and folds nothing), and A39 names it.
6175
+
6176
+ What is refused, each by the field and the value:
6177
+
6178
+ | You wrote | You get |
6179
+ | --- | --- |
6180
+ | `from` equal to `to` | `transform fields "envelope.from" and "envelope.to" are both 224; they are where the weight is 0 and where it is 1, so they differ` |
6181
+ | an `axis` that is not a unit direction | `transform field "envelope.axis" is [0, 2], whose length is 2; it is a unit direction, length 1 on the model's 1e-6 grid` |
6182
+ | a `shape` outside the two | `transform field "envelope.shape" is "cubic"; it is one of "linear", "smooth"` |
6183
+ | a ramp field missing | `transform field "envelope.from" is undefined; it is a finite number the spec has to state` |
6184
+ | a `mask` on an attachment with no `soft` block | `transform field "envelope.mask" is "skirt_soft.png", and this attachment has no "soft" block, so the mask names nothing` |
6185
+ | a `mask` naming another file than the attachment's soft mask | `… and this attachment's soft mask is "blob_soft.png", so the mask names nothing it was built with` |
6186
+ | a `mask` beside a ramp field | `transform field "envelope" states "mask" … beside the ramp's "axis", "from", "to", "shape". They are two answers to one question` |
6187
+ | an empty `envelope` | `transform field "envelope" is {}, which states no weight at all` |
6188
+ | a weight outside [0, 1] | `transform field "envelope.shape" weighs vertex 2 at 1.2; an envelope's weight is in [0, 1]` — unreachable from a spec (the ramp clamps, both shapes map [0, 1] onto itself, a mask is a sampled level), and kept because a weight past 1 would push a vertex past the model in silence |
6189
+ | a ramp that weighs every moving vertex 0 | `… and envelope axis=[0, 1] from=-16 to=-152 shape=smooth weighs every one of them 0 — so the key states a deformation and emits the identity`, with where the vertices sit along the axis |
6190
+ | a ramp the part never enters, so nothing changes | `… it changes no emitted number: every vertex the model moves takes weight 1` — a field a reader would credit for an output it did not shape |
6191
+
6192
+ The last two are the "evaluates to nothing" rule above, one step on, and a key
6193
+ whose model states the identity (`amplitude: 0`, …) is exempt from both — the
6194
+ envelope multiplies the zeros it already said.
6195
+
6196
+ `explain` prints the envelope under the model — its fields, its formula, how
6197
+ many vertices took 0 and 1, and every vertex's weight — and the `DEFORM` block's
6198
+ model line carries it as `envelope(…)` after the model's parameters.
6199
+
5761
6200
  ---
5762
6201
 
5763
6202
  #### 4.11.2 The `DEFORM` block — what a key does to the geometry
@@ -6509,6 +6948,22 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6509
6948
  | `ik constraint "X" names 3 bones ("a", "b", "c"); the solver applies one or two, so a constraint over 3 moves nothing — name one bone, or two where the second is the first's child` | §3.5 — the constraint's `bones` is what changes: one bone, or a parent and its child. The runtime's ik update has a case for one bone and one for two, so a longer list loads, sits in the update cache and applies nothing — [measured] every bone posed exactly where the same rig with no constraint has it. A longer chain is several constraints, each over one bone or a pair |
6510
6949
  | `ik constraint "X": "B" is not a child of "A" ("C" stands between), so the two-bone solve is not of the chain drawn — name "B"'s own parent as the first bone, or make "B" a child of "A"` (also `("A" is not above it; its parent is "P")`, `("B" has no parent)`) | §3.5 — the constraint's `bones` is what changes, or `B`'s `parent`. The two-bone solve places `B` through `A`'s matrix from `B`'s own local offset, so what stands between them is left out of the triangle it solves — [measured] a bone between them at `B`'s origin left the tip up to 2.60 off the target, and one at `A`'s origin landed only while it held its identity |
6511
6950
  | `physics constraint "X" is declared in both the rig spec and the motion spec's physics table` | §4.6 — the rig spec declares a physics constraint's structure and the motion spec's `physics` table declares one outright; pick the file it belongs in. Per kind, like every other constraint name: an `ik` "X" in the rig spec beside a `physics` "X" here is two constraints and is not this error |
6951
+ | `stops[i] needs a "chain" …` / `stop "C" states "links" …; it is the chain's swinging links, root first …` / `stop "C" needs "against" …` / `stop "C" states no "at" …` / `stop "C" states no "inward"` (or `"outward"`, `"lower.inward"`, `"lower.outward"`) / `stop "C" states no "lower" …` | §3.8 — state the field; a stop has no defaults. `at` is `[x, y]` in `against`'s local space, and `lower` is required on a chain of two or more links |
6952
+ | `stop "C" states "at" [V]; it is [x, y], two numbers in "A"'s local space` | §3.8 — two numbers |
6953
+ | `stop "C" states "lower" and its chain has one link …` | §3.8 — remove `lower`, or list the links below the first |
6954
+ | `stop "C": links[k] names bone "X", which this rig does not declare` / `stop "C": "against" names bone "X", which this rig does not declare` | §3.8 — fix the name; both resolve against the rig spec's own `bones` |
6955
+ | `stop "C": links[k] "X" is already stopped by stop "D" …` | §3.8 — a link has one drawn link and one clamp; stop it once |
6956
+ | `stop "C": links[k] "X" is the skeleton's root …` / `… takes its place from the manifest ("from") …` | §3.8 — a link hangs from a parent, and its drawn link copies its stated `x`, `y` and `rotation`; state them. The same `from` refusal names a bone the stop's angles are read through |
6957
+ | `stop "C": "inward" N is greater than "outward" M …` (also `"lower.inward"`) | §3.8 — the range is `[inward, outward]`, counted positive away from the point; `inward` is the lower end |
6958
+ | `stop "C": "outward" N puts "L_d"'s bound at B°, past ±180° …` (also `"inward"`, `"lower.…"`) | §3.8 — the clamp reads a local rotation in (−180, 180]; the bound is the drawn link's rest angle moved by the range, so narrow the range |
6959
+ | `stop "C" generates bone "X", which this rig already declares …` / `… generates ik constraint "X" …` / `… generates transform constraint "X" …` / `… which stop "D" generates too …` | §3.8 — the names are the expansion's (`<chain>_touch`, `<chain>_stop`, `<chain>_stop_aim`, `<link>_rel`, `<link>_rel_copy`, `<link>_d`, `<link>_clamp`); rename the bone or constraint, or the `chain` |
6960
+ | `stop "C": links[k] "X" is written by no constraint of this rig spec …` | §3.8 — declare what swings the link (a physics constraint, usually) in the rig spec's `constraints`; the motion spec's `physics` table is emitted after the clamp |
6961
+ | `stop "C": links[k] "X" does not hang below "P_d", the drawn link of links[k-1] …` | §3.8 — parent the link (or its control bone) below the drawn link above it, so the clamp that holds that one carries it |
6962
+ | `stop "C": "against" "A" hangs below "B", which the stop moves …` | §3.8 — name the leg; a point on the chain would swing with it |
6963
+ | `stop "C": "at" [x, y] on "A" sits on the root of "L" …` / `… lies on "L"'s own line at the setup pose …` | §3.8 — move the point onto the leg's edge beside the hem; on the link's line it has no side to call inward |
6964
+ | `stop "C": "P", where the frame hangs, is scaled unevenly, sheared or reflected …` / `stop "C": "B", between links[k] "L" and the drawn link above it, is scaled unevenly, sheared, reflected or inherits other than normally …` | §3.8 — the stop's angles add only through a rotation and an even scale; take the scale off that bone |
6965
+ | `stop "C": "L" hangs below "B", which a later stop generates …` | §3.8 — list that stop first |
6966
+ | `physics constraint "X" drives "L", a link stop "C" holds, from the physics table …` | §3.8, §4.6 — declare the constraint in the rig spec's `constraints`, where the stop's copy and clamp are placed after it |
6512
6967
  | `skin "S" lists "X" under "transform", but the rig declares it as a "ik" constraint — a skin looks its constraints up by name AND type, so this one is a miss and the loader throws` | §3.4.8 — move the name to the list named after the constraint's own kind. A name the rig declares under **no** kind is the other miss and says so (`skin "S" activates ik constraint "X", which this rig does not declare`). ⚠️ A name listed by **several skins** is not an error: the lists are per-skin sets and a bone or constraint in two of them is active under either |
6513
6968
  | `slot "X" names bone "Y", which this rig does not declare` | add the bone, or fix the slot's `bone` |
6514
6969
  | `no setup pose for slot "X": give the motion spec a \`setup\` entry or the rig slot an \`attachment\`` | R3 — pick one file and declare it there. A slot **nothing** fills is exempt: its setup pose can only be "show nothing" and is not asked for |
@@ -6538,7 +6993,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6538
6993
  | `hull N disagrees with the triangles, whose outline has K vertices (0 → …)` | §3.4 — delete `hull`, or state K |
6539
6994
  | `hull vertices must come first; vertex i is on the boundary and vertex j is not. The triangles' outline runs …: list those K vertices first, in that order, then the M interior vertices` | §3.4 — renumber the vertices: the printed walk first, then the interior |
6540
6995
  | `hull vertices must trace the outline in order; the triangles' outline runs …, so vertex a has to follow vertex b in the list, and vertex c does` | §3.4 — renumber along the printed walk |
6541
- | `the triangles do not tile the outline: …` / `the triangles' outline is not one closed loop: …` | §3.4 — a doubled triangle, an unused vertex, a pinch or a hole in `triangles` |
6996
+ | `the triangles do not tile the outline: …` / `the triangles' outline is not one closed loop: …` | §3.4 — a doubled triangle, an unused vertex, a pinch or a hole in `triangles`. When the message goes on `— the triangles are N pieces that share no vertex …`, the mesh is several meshes: `rigc split` writes one per piece (§3.4.11) |
6542
6997
  | `vertex N binds bone "X", which the rig does not declare as a bone` | §3.4 — an authored mesh's `weights` bind by NAME, like everything else in a rig spec. Fix the spelling, or declare the bone. The message names the skin, the slot, the placeholder and the vertex, because an index would name none of them |
6543
6998
  | `vertex N binds bone index I, which is not in the bone list` | §3.4 — a `"boneIndexing": "raw"` run on a mesh, bounding box, clipping polygon or path names a bone by its place in the bone array, and I is no place in it: past the end, negative or not a whole number. Count the rig's `bones` from 0, or give the bindings by name as `weights`, which cannot miss this way. ⚠️ **Refused earlier, not newly** (issue #917): the run is decoded to bone names once, when the attachment is built, and this is where an index that names no bone stops. Before that a mesh compiled and `A20` named the index at the gate, a bounding box, clipping polygon or path with its `lengths` stated compiled and `A33` named it, and only a path whose `lengths` rigc measures refused here, in this sentence. `MV04` plants it on each of the four kinds |
6544
6999
  | `the raw weighted run has a bone count of C at index I` / `the raw weighted run is truncated — vertex N claims C bone(s) and the array ends first` / `the raw weighted run decodes to N vertices but there are M uv pairs` | §3.4 — a mesh's `"boneIndexing": "raw"` run is not `boneCount, (index, x, y, weight) × boneCount` per vertex, one vertex per uv pair. These are the polygon's three sentences (whose last clause there reads `but vertexCount is M`), now asked of a mesh as well. Fix the run, or give the bindings by name as `weights`. ⚠️ **Refused earlier, not newly** (issue #917): the mesh run is decoded to bone names when the mesh is built, and a run that does not decode cannot be. Before that it compiled and `A20` named it at the gate (`has weights for 5 vertices but 4 uv pairs`, `vertex 3 has weight NaN`). `MV04` plants each |
@@ -6604,6 +7059,8 @@ or the key's position in its own track. These are the frequent ones, verbatim:
6604
7059
  | `deform … (t=…): transform yaw has radius R, and vertex v sits at x=… past it` | §4.11.1 — the cylinder has no surface there; raise the radius to where the part sits |
6605
7060
  | `deform … (t=…): transform affine has scale […], whose determinant is …` | §4.11.1 — at or below zero the map reverses every triangle |
6606
7061
  | `deform … (t=…): transform <kind> states …, and every one of this attachment's N vertices evaluates to an offset of 0` | §4.11.1 — the parameters state a deformation and the geometry sampled it to nothing; the message names the measured cause. A key that means the setup pose states the identity in its parameters, or carries no run |
7062
+ | `deform … (t=…): transform field "envelope.<field>" is …` | §4.11.1 `envelope` — the field the message names: `from` and `to` differ, `axis` is a unit direction, `shape` is `"linear"` or `"smooth"`, a `mask` is this attachment's own soft mask, and a weight is in [0, 1] |
7063
+ | `deform … (t=…): transform <kind> states … and envelope … weighs every one of them 0` / `… it changes no emitted number` | §4.11.1 `envelope` — the ramp does not cross the part; the message says where its vertices sit along the axis |
6607
7064
  | `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 |
6608
7065
  | `vertexCount is N and an open path needs at least 6` | §3.4 — an open path drops its first and last point, so it needs six for one curve |
6609
7066
  | `"lengths" has N entry(ies) where the parser sizes M — vertexCount / 3 on …` | §3.4 — `vertexCount / 3` entries on either shape; give the array the source states, or leave the field out and rigc measures it |
@@ -7459,6 +7916,8 @@ exports names the frame that moved. Two exports of one skeleton are byte-identic
7459
7916
  | `setup` | the setup pose, sampled the way a skeleton with no animation is rendered: `{ bones, attachments }` |
7460
7917
  | `frames` | one per PNG, in index order: `{ index, time, bones, attachments }`. `index` is the number in `f####.png`, `time` is `index / fps` in seconds |
7461
7918
 
7919
+ **The keys, as the file writes them.** Top level: `spec`, `coordinates`, `animation`, `skin`?, `environment`?, `fps`, `viewport`, `bones`, `slots`, `rest`, `setup`, `frames` — a `?` marks a key written only when it is set. Each frame: `index`, `time`, `bones`, `attachments`; and `setup`: `bones`, `attachments`. What a frame draws is under `attachments`: the top-level `slots` is the table of the bone each slot hangs from, not a frame's pieces.
7920
+
7462
7921
  Inside `setup` and each frame:
7463
7922
 
7464
7923
  - `bones` — every bone in declaration order: `{ name, a, b, c, d, worldX, worldY }`,
@@ -7804,6 +8263,88 @@ passes through is a defect, and what to do about it — a leg stop, a wider hem,
7804
8263
  different swing — is yours. To look at one, draw it:
7805
8264
  `inspect --show art --animation <name> --at <time> --zoom slot:<cloth>`.
7806
8265
 
8266
+ ### 8.6 One box for two renders — `render --viewport`, `render --viewport-from`
8267
+
8268
+ `render` fits its box to the rig it draws, so two builds of one rig — before and
8269
+ after a change — usually land on two grids of different sizes, and their pixels
8270
+ cannot be compared without a transform written by hand. Give the box instead:
8271
+
8272
+ ```bash
8273
+ bun cli.ts render --candidate before/ --out render-before
8274
+ bun cli.ts render --candidate after/ --viewport-from render-before --out render-after
8275
+ bun cli.ts render --candidate after/ --viewport -40,70,90,140 --max 256 --out render-box
8276
+ ```
8277
+
8278
+ - `--viewport-from <frames.json | dir>` reads an earlier render's `frames.json` (the
8279
+ file, or the directory `render --out` wrote it into) and draws on **its grid
8280
+ exactly**: the same world box, the same scale and the same pixel size, so a world
8281
+ point lands on the same pixel in both renders and the two frame sets overlay.
8282
+ `--max` is refused beside it, because the file already fixes the pixel size.
8283
+ - `--viewport <x,y,w,h>` draws over a world box you state, y up, `x,y` its
8284
+ bottom-left corner — the numbers `frames.json`'s `viewport` records, and the
8285
+ spelling `check --viewport` and `inspect --viewport` take. Its pixel size follows
8286
+ from `--max` as a fitted box's does.
8287
+
8288
+ The render prints a `viewport given by …` line with the box and the box it would
8289
+ have fitted, so you can see what the given one leaves out.
8290
+
8291
+ ⚠️ **Nothing is cropped in silence.** Each frame is the box: what lies past it is
8292
+ not drawn. A fitted box is measured over every pose the rig strikes at 60 fps and padded; a given one is not,
8293
+ so every slot whose drawn geometry reaches past it — a piece's posed vertex outside
8294
+ the box — is named, once per set: the frames, the farthest it reaches in world
8295
+ units, and the sides (`left`, `right`, `bottom`, `top`, y up). `frames.json` records
8296
+ the box was given and those slots under `pinned`: `{ "by": "--viewport" |
8297
+ "--viewport-from", "outside": [{ "set", "slot", "frames" }] }` — written only when a
8298
+ box was given, so a fitted render is the bytes it always was.
8299
+
8300
+ **Refused, nothing written:** the two flags together; `--max` beside
8301
+ `--viewport-from`; a `--viewport` that is not four numbers or whose width or height
8302
+ is not above 0; a `--viewport-from` path with no `frames.json`, a file whose `spec`
8303
+ is not `rigc-frames/1`, and a `viewport` field that is not a finite number (`x`,
8304
+ `y`), a number above 0 (`width`, `height`, `scale`) or a whole number of pixels above
8305
+ 0 (`pixelWidth`, `pixelHeight`) — each named with the value found. The framing still
8306
+ runs under a given box, so a pose with nothing to draw, one that is not finite and
8307
+ one with no extent are refused as they are without one.
8308
+
8309
+ ### 8.7 How far a bone turns against another — `render --angle-range`
8310
+
8311
+ A physics value has no scale you can see from the spec: what `inertia` 0.5 does to
8312
+ a link depends on its length, on how its parent moves and on every other value
8313
+ together. The answer is a number the pose already holds — how far the link turned
8314
+ against the bone it hangs from — and `--angle-range` reads it off the frames
8315
+ `render` draws:
8316
+
8317
+ ```bash
8318
+ bun cli.ts render --candidate path/to/spine --angle-range tail:hip,sleeve:arm [--animation …] [--fps 12]
8319
+ ```
8320
+
8321
+ One line per pair per set, beside the set's line: the lowest and the highest angle
8322
+ in degrees, the span, the frames read and the rate, and the frame each extreme was
8323
+ first reached on. Nothing is written, and every file is the bytes the render writes
8324
+ without the flag.
8325
+
8326
+ **What the angle is.** At each frame, the first bone's world direction less the
8327
+ second's — each the direction its x axis points, degrees counter-clockwise, y up,
8328
+ as the poser that drew the frame poses it (`rotationX`). The first frame's
8329
+ difference is taken into (−180°, 180°], and each later frame's is moved by whole
8330
+ turns to within half a turn of the frame before, so a link that sweeps 200° reads
8331
+ 200°, not −160°. ⚠️ It is the angle **drawn**, not the bone's local `rotation`: the
8332
+ two agree for a child of the other bone when the parent carries no non-uniform scale
8333
+ or shear.
8334
+
8335
+ **Where it is read.** On the frames' own grid — `--fps`, from t = 0 to each set's
8336
+ last frame — so a peak between two frames is not read; raise `--fps` to read finer.
8337
+ It reads bones, which `--slot`/`--hide` do not change, so it takes them.
8338
+
8339
+ **Refused, nothing written:** a bone the skeleton does not have (with the bones it
8340
+ does), an entry without exactly one `:`, an empty entry or name, a bone against
8341
+ itself (0° on every frame), a pair named twice, and a bone with no direction on a
8342
+ frame — a world matrix of zero, which a bone the posed skin leaves unposed has, or
8343
+ one scaled to nothing — named with the frame.
8344
+
8345
+ **A reading, not a gate.** Whether a range is too wide is yours; §4.6 says which
8346
+ values move it.
8347
+
7807
8348
  ---
7808
8349
 
7809
8350
  ## 9. Checking against the frames — `rigc check`