@rig-c/core 2.59.0 β†’ 2.61.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 CHANGED
@@ -433,7 +433,10 @@ frames. It draws with rigc's own rasteriser (the one `check` measures with), so
433
433
  it needs no browser and no network, and the `frames.json` it leaves beside the
434
434
  directories makes the result a frame set like any other β€” the world box every
435
435
  frame is a picture of. `--animation <name>` narrows it to one, `--fps` and
436
- `--max` change the rate and the frame size.
436
+ `--max` change the rate and the frame size. A set drawn at any rate but the
437
+ default is written to `<anim>@<fps>fps/` rather than `<anim>/`, so two rates sit
438
+ side by side: `--fps 24` writes `<anim>@24fps/`, and the default 12 writes
439
+ `<anim>/`. Each set's summary line ends with the directory it wrote.
437
440
 
438
441
  > 🚨 **`--skin <name>` if your rig has more than one.** With no `--skin` no skin
439
442
  > is set at all, so every slot resolves through the **default** skin alone β€” and
@@ -603,7 +606,7 @@ commands take it and what its default is.
603
606
  | `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one β€” and is refused, rather than ignored, beside one that declares a box β€” and `--images <dir>` writes the spec's own images directory β€” the opposite direction from `build --images`, which overrides it β€” so the rebuild carries no flag at all |
604
607
  | `split --rig … --motion … --slot <name> --out <dir>` | the answer to a mesh refused because its triangles are **several pieces that share no vertex** β€” two legs after the triangles bridging them were deleted. It reads the two specs and writes them back into an absent or empty `--out` with one mesh per piece: piece 1 stays in the slot, piece k gets a new slot `<slot>_<k>` after it, each with the original's uvs and weights unchanged, its triangles renumbered outline first where the hull rule needs it, and every deform key re-sliced to the new indices β€” plus `split.json`, where every original vertex and triangle went. One piece is said so and nothing is written; what it cannot carry is refused by name ([AUTHORING Β§3.4.11](docs/AUTHORING.md)) |
605
608
  | `explain --rig … --motion …` | the compiled rig as a table β€” every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
606
- | `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box β€” a reading, not a gate. `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on β€” a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time β€” the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene |
609
+ | `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default β€” `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box β€” a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box β€” a reading, not a gate; the exit status never moves. `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on β€” a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time β€” the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene |
607
610
  | `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version β€” a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` |
608
611
  | `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
609
612
  | `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
@@ -614,6 +617,7 @@ commands take it and what its default is.
614
617
  | `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
615
618
  | `guide <RULE>` / `guide Β§<n>[.<m>[.<k>]] [--all]` | the guide's row for the rule a failure line names β€” every `FAIL` line, `vote --record` refusal, `ingest` finding and coded compile error ends with ` (see rigc guide <RULE>)`; only legacy uncoded library errors point at `Β§5.1` β€” or a section's heading, own prose and subsection index, read from the installed `docs/AUTHORING.md` when it runs. `--all` includes the subsection bodies; a leaf prints its full text either way. Sizes describe full ledger spans. `--all` with a rule is refused, exit 2; an unknown name is refused with the nearest real ones, exit 1 |
616
619
  | `skills install [--dir …] [--copy]` | links every agent skill the package ships into `.agents/skills` (or `--dir`) as relative symlinks, or copies them with `--copy` β€” where Codex, Gemini CLI and Antigravity look. A second run has nothing to do; an entry already there that is not this command's is refused by name and nothing is written |
620
+ | `review <dir> […]` | the human review of a build, handed to the `rig-review` bin of `@rig-c/review` installed beside the package, with every argument untouched and its exit code returned: `rigc review open <dir>` prints a loopback URL to give a person, `check <dir>` validates the folder it would serve, `feedback <dir>` prints what they left, and `rigc review --help` is review's own usage. rigc does not depend on it: without it the command prints `rigc review: @rig-c/review is not installed β€” npm i -D @rig-c/review` and exits 2 |
617
621
 
618
622
  `diff`, `bonedist`, `check` and `bench` measure against something you were given; the
619
623
  first three work on any reference you have, and `bench` is a repository workflow that needs a clone
package/docs/AUTHORING.md CHANGED
@@ -141,6 +141,11 @@ bun cli.ts inspect --candidate path/to/spine --show art,bones,names --dim --out
141
141
  # them, ask a human β€” the one loop step this toolchain cannot run for you:
142
142
  bun cli.ts vote --candidate path/to/spine-a --candidate path/to/spine-b # -> ballot.html
143
143
  bun cli.ts vote --record vote-<id>.json # -> votes.jsonl
144
+
145
+ # …and when the build is for a person to look at rather than for you to choose
146
+ # between, hand it over: with @rig-c/review installed beside rigc, give the person
147
+ # the loopback URL this prints (rigc review --help is review's own usage):
148
+ bun cli.ts review open path/to/spine
144
149
  ```
145
150
 
146
151
  `build` compiles, runs the named assertions β€” through a `@esotericsoftware/spine-core`
@@ -255,6 +260,7 @@ What the flags mean:
255
260
  | `--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** |
256
261
  | `--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** |
257
262
  | `--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** |
263
+ | `--seam` | `render` only: `<slot>:<slot>[,<slot>:<slot>…]` β€” also read, for each pair, where the two drawings **stop meeting**, and write a `seam.json` into each frame directory: per pair, the frames with a **gap** (background inside the joint, where the two covered together at the setup pose) and the frames with a **ghost** (the overlap band of one showing where, from its partner's point of view, nothing was drawn), each with its cell count, area and world box, and as runs of frames with their times. One printed line per pair; a pair whose drawings share no cell at the setup pose is a **SKIP**. A reading, not a gate: the exit status never moves. Every other file is byte for byte what the render writes without it. An unknown slot, a slot with itself, a slot that draws on no frame, a malformed list and a pair named twice in either order are refused by name, and so is `--slot`/`--hide` beside it β€” **Β§8.8** |
258
264
  | `--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** |
259
265
  | `--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 |
260
266
  | `--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** |
@@ -1130,6 +1136,7 @@ bun cli.ts render --candidate path/to/spine --slot <name,…> | --hide <name,
1130
1136
  bun cli.ts render --candidate path/to/spine --geometry # + geometry.json per set: the numbers the frames were drawn from
1131
1137
  bun cli.ts render --candidate path/to/spine --folds # + folds.json per set: which mesh triangles turn over, and when
1132
1138
  bun cli.ts render --candidate path/to/spine --pass-through skirt:leg # + pass-through.json: where a cloth goes through a leg
1139
+ bun cli.ts render --candidate path/to/spine --seam thigh:shin # + seam.json: where two slots that meet open a gap or show a ghost
1133
1140
  bun cli.ts render --candidate path/to/spine --angle-range tail:hip # how far a bone turns against another, per animation
1134
1141
  bun cli.ts render --candidate path/to/spine --viewport-from render/ # an earlier render's grid: two builds, one pixel grid
1135
1142
  bun cli.ts preview --candidate path/to/spine [--candidate path/to/another …] [--animation …] [--out preview.html]
@@ -1235,7 +1242,11 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
1235
1242
  gate can know you did not mean them. `render` writes
1236
1243
  `render/<animation>/f0000.png…` with a `contact.png` grid of **every** frame
1237
1244
  beside them (open that one first β€” spacing is a comparison across frames) and a
1238
- `frames.json` sidecar naming the world box they are pictures of. `contact.png`
1245
+ `frames.json` sidecar naming the world box they are pictures of. It also says
1246
+ whether the physics ran: under each set's line, `physics: N constraint(s)
1247
+ stepped at <fps> fps`, and the same two figures on that set in `frames.json` β€”
1248
+ or, once, `physics: none in this rig` for a rig that declares no physics
1249
+ constraint (Β§8.4). `contact.png`
1239
1250
  is for spacing across frames; a defect is read on **one frame at full size**,
1240
1251
  because a tile is too small to say which part a pixel belongs to. When a frame
1241
1252
  looks wrong, render it again with `--hide <slot>` β€” the **largest attachment**
@@ -4518,6 +4529,17 @@ rest; a frame parent scaled unevenly, sheared or reflected, or such a bone
4518
4529
  between two links; a link below a bone a later stop generates; a `from` bone in
4519
4530
  the angles' path.
4520
4531
 
4532
+ On the first link that bound is |rest| + `outward`, where rest is the angle
4533
+ between the direction the link hangs in and the aim from its root at
4534
+ `<chain>_touch`, so where the point sits decides it as much as `outward` does.
4535
+ The refusal names the link, its root's world position and hang, the point's world
4536
+ position, rest, the end and the sum, and says what to move: `<chain>_touch` to
4537
+ within 180Β° βˆ’ `outward` of the link's hang direction β€” below the root, for a link
4538
+ that hangs straight down and `outward` up to 90Β° β€” or `outward` down to
4539
+ 180Β° βˆ’ |rest|. A leg that rises from behind the cloth, as on a prone figure,
4540
+ runs its local +x up the screen, so `at` at its far end lands above the chain's
4541
+ root; put it at the knee, below the root (`LG34`).
4542
+
4521
4543
  The stop decides nothing about *when* β€” it is what the object can do. Read it
4522
4544
  with `render --pass-through <cloth>:<leg>` (Β§8.5) on the motion that swung
4523
4545
  through before.
@@ -6825,6 +6847,14 @@ it had nothing to step (`nanStepping=skipped`). What it does not see is a skin o
6825
6847
  than the default, which A10 does not pose. The sentence, and where to look for the
6826
6848
  number, is under *nothing to draw* in Β§3.4.
6827
6849
 
6850
+ ⌨️ **An invocation missing a flag is one line, before any of these.** `build`,
6851
+ `explain` and `validate` given a cut spelled out with `--rig`, `--motion` or
6852
+ `--out` absent print `rigc: <command> needs --<flag> <value> (or --cut <name>
6853
+ --cuts <cuts.json>)` on stderr and exit 2 β€” the line alone, with no command
6854
+ catalogue under it. An input flag that names a **directory** is a compile error
6855
+ with a remedy in it: `CE_COMPILE_JSON_READ_FAILED` says what the flag wants and
6856
+ names the one file there that reads as it (Β§5.1.2). It is never read as that file.
6857
+
6828
6858
  ### 5.1 Compile errors β€” before the gate
6829
6859
 
6830
6860
  A `CompileError` names the object and the field, and nothing is written. Its line
@@ -6901,7 +6931,7 @@ retain the minimal inputs and report that code rather than inventing author data
6901
6931
  | <a id="CE_COMPILE_RGBA_CHANNEL_COUNT"></a>`CE_COMPILE_RGBA_CHANNEL_COUNT` | β€œrgba value needs 4 channels, got &lt;value&gt;” | Supply four rgba channels. |
6902
6932
  | <a id="CE_COMPILE_RGBA2_CHANNEL_COUNT"></a>`CE_COMPILE_RGBA2_CHANNEL_COUNT` | β€œrgba2 value needs 7 channels, got &lt;value&gt;” | Supply seven rgba2 channels. |
6903
6933
  | <a id="CE_COMPILE_COLOUR_CHANNEL_COUNT"></a>`CE_COMPILE_COLOUR_CHANNEL_COUNT` | β€œ&lt;value&gt; value needs &lt;value&gt; channel[ / s], got &lt;value&gt;” | Supply the channel count named for this colour shape. |
6904
- | <a id="CE_COMPILE_JSON_READ_FAILED"></a>`CE_COMPILE_JSON_READ_FAILED` | β€œcannot read &lt;value&gt;: &lt;value&gt;”<br>Also: β€œ&lt;value&gt; (line &lt;value&gt;, column &lt;value&gt;)” | Fix the named JSON file's path, permissions or JSON syntax. |
6934
+ | <a id="CE_COMPILE_JSON_READ_FAILED"></a>`CE_COMPILE_JSON_READ_FAILED` | β€œcannot read &lt;value&gt;: &lt;value&gt;”<br>Also: β€œ&lt;value&gt; (line &lt;value&gt;, column &lt;value&gt;)”<br>Also, when `--rig`, `--motion` or `--manifest` names a directory: β€œ&lt;value&gt; names a directory; it wants &lt;value&gt; β€” &lt;value&gt; exists there” Β· β€œβ€¦ β€” &lt;value&gt; holds no .json file” Β· β€œβ€¦ β€” &lt;value&gt; holds &lt;value&gt; .json file(s) and none reads as one: &lt;value&gt;” Β· β€œβ€¦ β€” &lt;value&gt; .json files in &lt;value&gt; read as one, so name it: &lt;value&gt;”, a list cut at eight names with β€œ, … &lt;value&gt; in all” | Fix the named JSON file's path, permissions or JSON syntax. For a directory, name the file the line names β€” the one `.json` there that reads as that input (a rig spec through the rig spec's parser, a motion spec through the motion spec's; for `--manifest`, any `.json`) β€” or pick one of the files it lists. A directory is never read as its file: rigc compiles what it was given. |
6905
6935
  | <a id="CE_COMPILE_IMAGE_MISSING"></a>`CE_COMPILE_IMAGE_MISSING` | β€œimage "&lt;value&gt;" is not on disk at &lt;value&gt;”<br>Also: β€œ&lt;value&gt;.png” | Supply the PNG at the named images path or correct the image name. |
6906
6936
  | <a id="CE_COMPILE_IMAGE_PNG_INVALID"></a>`CE_COMPILE_IMAGE_PNG_INVALID` | β€œ&lt;value&gt; is &lt;value&gt;, not a PNG: its first &lt;value&gt; byte(s) are &lt;value&gt;, where a PNG's first 8 are &lt;value&gt;&lt;value&gt;. rigc reads PNG and nothing else β€” its page-size and alpha readers, its renderer and its region lift all decode PNG, and it links no decoder for any other format β€” so nothing in this file was measured. Re-export it as PNG”<br>β€œ&lt;value&gt; is a PNG whose IHDR chunk declares &lt;value&gt; byte(s) of data, where the format's is 13”<br>β€œ&lt;value&gt; is a PNG whose IHDR chunk states its size as &lt;value&gt;x&lt;value&gt;: &lt;value&gt; 0, where the format requires each dimension to be at least 1, so the file describes no texel and nothing in it can be measured or drawn. Re-export it”<br>β€œ&lt;value&gt; is a PNG whose first chunk is &lt;value&gt; rather than the IHDR the format requires first”<br>β€œ&lt;value&gt; is a truncated PNG: it is &lt;value&gt; byte(s) long and &lt;value&gt;, so no reader can decode it. Re-export or re-copy the file whole”<br>β€œ&lt;value&gt; is a truncated PNG: it is &lt;value&gt; byte(s) long and ends &lt;value&gt; byte(s) into the 8-byte PNG signature, so no reader can decode it. Re-export or re-copy the file whole”<br>β€œ&lt;value&gt; is a truncated PNG: it is &lt;value&gt; byte(s) long and ends after the signature with no IEND chunk, so no reader can decode it. Re-export or re-copy the file whole”<br>β€œ&lt;value&gt; is a truncated PNG: it is &lt;value&gt; byte(s) long and its &lt;value&gt; chunk at byte &lt;value&gt; declares &lt;value&gt; byte(s) of data, which with its CRC runs to byte &lt;value&gt;, so no reader can decode it. Re-export or re-copy the file whole”<br>β€œ&lt;value&gt; is a truncated PNG: it is &lt;value&gt; byte(s) long and {ends after &lt;value&gt; with no IEND chunk / ends &lt;value&gt; byte(s) into the header of the chunk after &lt;value&gt;}, so no reader can decode it. Re-export or re-copy the file whole”<br>β€œ&lt;value&gt; is empty (0 bytes), and a PNG's first 8 bytes are &lt;value&gt;”<br>β€œ[; its name ends in .png, and it is the bytes that decide what a file is / ]”<br>β€œ[its width and its height are / [its width is / its height is]]”<br>β€œa &lt;value&gt; image”<br>β€œa WebP image (a RIFF/WEBP container whose first chunk is &lt;value&gt;)”<br>β€œimage "&lt;value&gt;": &lt;value&gt;”<br>β€œin no image format rigc recognises by its signature (it names &lt;value&gt; when it meets them)”<br>Here the values named in the message include: β€œKTX2”, β€œGIF”, β€œJPEG”. | Supply a readable PNG with the required header and dimensions at the named path. |
6907
6937
  | <a id="CE_COMPILE_ATLAS_INPUT_MISSING"></a>`CE_COMPILE_ATLAS_INPUT_MISSING` | β€œ--atlas-in names &lt;value&gt;, which is not on disk” | Correct --atlas-in or supply the atlas file at that path. |
@@ -7352,7 +7382,7 @@ retain the minimal inputs and report that code rather than inventing author data
7352
7382
  | <a id="CE_STOP_LINK_UNDRIVEN"></a>`CE_STOP_LINK_UNDRIVEN` | β€œ&lt;stop.who&gt;: links[&lt;k&gt;] "&lt;link&gt;" is written by no constraint of this rig spec β€” a stop holds a link something swings (a physics, ik, transform or path constraint in "constraints"), and its copy and clamp run right after that. A physics constraint in the motion spec's table is emitted after every rig constraint, past the clamp; declare it here” | Declare the swinging physics, ik, transform or path constraint in the rig spec's constraints before its stop clamp. |
7353
7383
  | <a id="CE_STOP_LINK_PARENT_CHAIN"></a>`CE_STOP_LINK_PARENT_CHAIN` | β€œ&lt;stop.who&gt;: links[&lt;k&gt;] "&lt;links[k]&gt;" does not hang below "&lt;names.drawn[k - 1]&gt;", the drawn link of links[&lt;k - 1&gt;] β€” a chain's next link hangs from the drawn link above it, so the clamp that holds that one carries this one; parent it there” | Parent each lower link under the preceding generated drawn link. |
7354
7384
  | <a id="CE_STOP_AGAINST_MOVES_WITH_STOP"></a>`CE_STOP_AGAINST_MOVES_WITH_STOP` | β€œ&lt;stop.who&gt;: "against" "&lt;stop.against&gt;" hangs below "&lt;under&gt;", which the stop moves β€” the point would swing with the chain it holds off; name the leg” | Name an against bone outside the subtree moved by the stop. |
7355
- | <a id="CE_STOP_LOCAL_BOUND_RANGE"></a>`CE_STOP_LOCAL_BOUND_RANGE` | β€œ&lt;stop.who&gt;: "&lt;field&gt;&lt;end&gt;" &lt;range[end]&gt; puts &lt;what&gt;'s bound at &lt;value&gt;Β°, past Β±&lt;BOUND&gt;Β° β€” the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds”<br>Here the values named in the message include: β€œinward”, β€œoutward”. | Adjust the printed limits so the local clamp bounds stay within its supported angular range. |
7385
+ | <a id="CE_STOP_LOCAL_BOUND_RANGE"></a>`CE_STOP_LOCAL_BOUND_RANGE` | β€œ&lt;stop.who&gt;: "&lt;field&gt;&lt;end&gt;" &lt;range[end]&gt; puts &lt;what&gt;'s bound at &lt;value&gt;Β°, past Β±&lt;BOUND&gt;Β° β€” the clamp reads a local rotation in (-180, 180], so a bound past it is never reached and the range it states is not the one that holds”<br>On the first link it goes on: β€œ. "&lt;stop.links[0]&gt;" hangs at &lt;at.hang&gt;Β° from its root at [&lt;at.root&gt;], and "&lt;stop.names.point&gt;" ("at" [&lt;stop.at&gt;] on "&lt;stop.against&gt;") sits at [&lt;at.point&gt;] β€” Spine world, y up, at the setup pose β€” so the aim from that root to the point lies &lt;off&gt;Β° off the hang (rest &lt;rest&gt;Β°): \|rest\| &lt;off&gt;Β° + "&lt;end&gt;" &lt;e&gt;Β° is &lt;sum&gt;°”, then one of β€œ, above &lt;BOUND&gt;Β°. Move "&lt;stop.names.point&gt;" to within &lt;BOUND βˆ’ e&gt;Β° (&lt;BOUND&gt;Β° βˆ’ "&lt;end&gt;") of the direction "&lt;stop.links[0]&gt;" hangs in from its root, &lt;at.hang&gt;Β°, or state "&lt;end&gt;" at most &lt;BOUND βˆ’ off&gt;°”, β€œ, above &lt;BOUND&gt;Β°, and no point lowers it: \|rest\| is above 0Β° for every point a stop accepts. State "&lt;end&gt;" below &lt;BOUND&gt;°”, β€œ, below -&lt;BOUND&gt;Β°. State "&lt;end&gt;" at least &lt;βˆ’BOUND βˆ’ off&gt;°”<br>Here the values named in the message include: β€œinward”, β€œoutward”. | On the first link the bound is \|rest\| + the end, where rest is the angle from the link's hang to the aim at `<chain>_touch`, both read from the link's root: move `<chain>_touch` (its `at` on `against`) to within 180Β° βˆ’ `outward` of the direction the link hangs in from its root β€” on a link hanging straight down with `outward` at most 90Β°, any point below the root β€” or lower `outward` to at most 180Β° βˆ’ \|rest\|. A leg that rises from behind the cloth, as on a prone figure, puts a point at its far end above the root; put it at the knee. On a lower link, bring `lower`'s ends within Β±180Β° of the link's own rest bend. |
7356
7386
  | <a id="CE_STOP_DEPENDENCY_ORDER"></a>`CE_STOP_DEPENDENCY_ORDER` | β€œ&lt;who&gt;: "&lt;tip&gt;" hangs below "&lt;name&gt;", which a later stop generates β€” declare that stop first, so its angles are known when this one is read”<br>Also: β€œ&lt;value&gt;: stop "&lt;value&gt;"” | Declare the stop generating the parent before the stop depending on it. |
7357
7387
  | <a id="CE_STOP_ANCESTOR_MANIFEST_POSITION"></a>`CE_STOP_ANCESTOR_MANIFEST_POSITION` | β€œ&lt;who&gt;: "&lt;name&gt;" takes its place from the manifest ("from"), and a stop's angles are read off the setup pose this rig spec states β€” state its x, y and rotation”<br>Also: β€œ&lt;value&gt;: stop "&lt;value&gt;"” | State x, y and rotation for the named setup-pose ancestor instead of from. |
7358
7388
  | <a id="CE_STOP_FRAME_PARENT_TRANSFORM"></a>`CE_STOP_FRAME_PARENT_TRANSFORM` | β€œ&lt;who&gt;: "&lt;parent&gt;", where the frame hangs, is scaled unevenly, sheared or reflected at the setup pose β€” the frame's angles add to its parent's only through a rotation and an even scale, and inward is a turn's sense”<br>Also: β€œ&lt;value&gt;: stop "&lt;value&gt;"” | Use a frame parent with rotation and even positive scale, without shear or reflection. |
@@ -8478,6 +8508,34 @@ from its own frame and no earlier (`RE02`); and plants on the core the readings
8478
8508
  mapping rejects β€” the schedule ignored, its y read up, each change a frame late or
8479
8509
  early β€” each red against spine-core (`RE03`).
8480
8510
 
8511
+ **Whether the physics ran.** Every render β€” with this flag or without β€” says, under
8512
+ each set's line, how many physics constraints the poser stepped on at least one of
8513
+ its frames, and at what rate:
8514
+
8515
+ ```text
8516
+ .. idle 25 frame(s), 2.000s + contact.png -> render/idle
8517
+ .. physics: 3 constraint(s) stepped at 12 fps
8518
+ ```
8519
+
8520
+ and `frames.json` carries the same two figures on that set, `"physics": { "stepped":
8521
+ 3, "fps": 12 }`. The rate is the set's own: one physics update a frame. The count is
8522
+ read off the posed state, not off the declaration: a constraint counts when, after
8523
+ a step that moved the skeleton's clock, its own clock reads the skeleton's β€” which
8524
+ the physics update sets and every way of not running leaves behind. So a constraint
8525
+ that is inactive on every frame, or at a mix of 0, is not counted, and the line names
8526
+ it: `physics: 0 constraint(s) stepped at 12 fps β€” 1 of the 1 declared took none:
8527
+ "iris_settle" (inactive, or a mix of 0, on every frame)`; a set of one pose β€” an
8528
+ animation of no length, or the setup pose of a skeleton with none β€” takes no step
8529
+ and says so. A rig that declares no physics constraint prints `physics: none in this
8530
+ rig β€” it declares no physics constraint, so no frame steps one` once, before the
8531
+ sets, and its `frames.json` carries no `physics` field, as every frame set written
8532
+ before the field existed carries none. This is what tells a reading β€” a fold count
8533
+ from `--folds`, say (Β§8.3) β€” taken with the physics from one taken without. Both
8534
+ posers read the same constraints on every frame, and `cli.ts`, `cli.ts --poser
8535
+ spine` and `cli_core.ts` write the same files (`PH01`, `PH02`); the step removed β€”
8536
+ on the core, or spine-core's `PhysicsConstraint.update` made a no-op β€” reads 0
8537
+ (`PH03`), and a constraint at mix 0 is named (`PH04`).
8538
+
8481
8539
  **The files.** `frames.json` and each `geometry.json` record the schedule as
8482
8540
  `environment`, its steps as read, `t`, `wind`, `gravity` in that order. The
8483
8541
  framing reads it too: a gust can carry a coat past the box a still rig fills, so
@@ -8696,6 +8754,131 @@ one scaled to nothing β€” named with the frame.
8696
8754
  **A reading, not a gate.** Whether a range is too wide is yours; Β§4.6 says which
8697
8755
  values move it.
8698
8756
 
8757
+ ### 8.8 Where two slots stop meeting β€” `render --seam`
8758
+
8759
+ A cut rig can tear in motion: two pieces painted to overlap at a joint pull apart
8760
+ and the background shows between them, or one swings clear of the piece it was
8761
+ tucked under and the overlap it carries shows where nothing was. Both pass every
8762
+ assertion and every check, and either can be intended β€” so `--seam` names the
8763
+ frames and decides nothing. On the selftest's probe (`pass_through_probe`,
8764
+ `fixtures/public.ts`: a skirt whose hip swings 30Β° and back over a leg that hangs
8765
+ inside it, and a second copy of the leg on the same bone):
8766
+
8767
+ ```bash
8768
+ bun cli.ts render --candidate path/to/spine --animation swing --seam skirt:leg,leg_front:leg [--skin …] [--fps 12]
8769
+ # rigc render
8770
+ # .. swing 25 frame(s), 2.000s + contact.png + seam.json (1 of 2 pair(s) open) -> …/swing
8771
+ # .. seam "swing": seam "skirt" | "leg" opens on 23 of 25 frame(s) read [1-23] at 12 fps, max 1606 px β€” gap on 13 frame(s) [6-18], worst frame 12 (t=1.0000s): 616 px (616.00 unitsΒ² at 1.0000 units a px) in world box x [-7.00, 19.00] y [105.99, 196.00] (26.00 Γ— 90.00); ghost on 23 frame(s) [1-23], worst frame 12 (t=1.0000s): 1606 px (1606.00 unitsΒ² at 1.0000 units a px) in world box x [-27.00, 41.00] y [104.99, 198.00] (68.00 Γ— 93.00)
8772
+ # .. seam "swing": seam "leg_front" | "leg" stays closed on all 25 frame(s) read at 12 fps β€” no gap, no ghost
8773
+ ```
8774
+
8775
+ The skirt swinging off the leg opens background between the leg's right edge and
8776
+ the skirt's swung side (the gap, widest at the top of the swing), and uncovers the
8777
+ top of the leg the skirt hid at rest (the ghost). The two copies of the leg move
8778
+ on one bone and never open. A pair with nothing to read says so β€”
8779
+ `seam "a" | "b": SKIP β€” the two drawings share no cell at the setup pose on any of
8780
+ the N frame(s) both draw, so there is no band to read` β€” and a closed pair says
8781
+ `stays closed on all N frame(s) read`; the two are never the same line.
8782
+
8783
+ **The frame of reference.** A seam is not read in the world: a limb that swings
8784
+ moves both of its pieces from where they were at the setup pose, and that is
8785
+ motion, not a tear. It is read **from each piece's own point of view**. A world
8786
+ point of a frame is carried back into a piece's setup pose by the affine map of
8787
+ that piece's triangle holding it as the frame poses it; outside the piece's mesh,
8788
+ by the map of the triangle it is least outside of (the largest smallest
8789
+ barycentric weight, the lowest index on a tie) β€” exact for a region and for any
8790
+ mesh one bone moves, the nearest affine piece of a skinned one. Two pieces that
8791
+ move together carry a point back to one place, so a pair moved whole reads nothing.
8792
+
8793
+ **Every number, and where it comes from.** Nothing here is a threshold the rig
8794
+ did not declare; each figure is the posed vertices (`--geometry`'s), the rest
8795
+ table's vertices, and the atlas alpha:
8796
+
8797
+ - **The cell** is one texel of the coarser of the two drawings at the setup pose,
8798
+ in world units β€” the step `--pass-through` steps by (Β§8.5) β€” on multiples of it
8799
+ from the world origin. `px` counts cells: on art drawn one texel to a unit, a
8800
+ cell is a world unit and a px is a pixel of the art.
8801
+ - **Drawn** is the renderer painting a cell's centre at **128 of 255** or more β€”
8802
+ the half-opacity line `--pass-through` draws outlines at, the frame's tint
8803
+ included, the setup pose drawn at the same tint (so a slot faded out is not a
8804
+ tear: it is faded in both). No other alpha enters.
8805
+ - **The band** is the cells both drawings draw at the setup pose β€” where the two
8806
+ were painted to overlap. It has **no width of its own**: it is the overlap the
8807
+ art states, dilated by nothing. A frame whose band is empty is not read; a pair
8808
+ whose band is empty on every frame both draw is a SKIP.
8809
+ - **gap** β€” a cell inside the convex hull of the band's cell centres, carried to
8810
+ the frame by both pieces (the region the joint occupies now, as both place it),
8811
+ that neither piece draws at the frame, and that at least one piece, carrying it
8812
+ back, finds drawn by either drawing at the setup pose: the two no longer cover
8813
+ what they covered together. The hull is the joint's own extent, which is what
8814
+ keeps a piece's far end swinging away from reading as a tear; the rest test is
8815
+ what keeps a band that is concave at the setup pose from reading its own notch.
8816
+ - **ghost** β€” a cell one piece draws at the frame and the other does not, which
8817
+ the drawing piece, carrying it back, lands in the band, and which the other,
8818
+ carrying it back, finds neither drawing at at the setup pose: the overlap swung
8819
+ clear of its partner.
8820
+
8821
+ Draw order does not enter: a cell is covered or bare whichever piece is in front.
8822
+
8823
+ **Its reach, stated.** A piece folded right back over its partner carries the band
8824
+ across the region it vacated, so a hull that wide reads the vacated cells its
8825
+ partner's view still finds drawn. And no size is too small to print: on
8826
+ `gallery/walk`, built as its README says, `--seam thigh_f:shin_f,arm_f:hand_f`
8827
+ reads gaps of 1–2 px on 5 of the 12 frames at the knee and of 1 px on 2 at the
8828
+ wrist β€” the wrist's, drawn at 20 pixels a unit, is the V where the two outlines
8829
+ meet at an angle, one cell deep. Whether that is a tear is yours.
8830
+
8831
+ **The file.** `seam.json`, `rigc-seam/1`, beside each set's frames β€” the contract
8832
+ a consumer reads it by. Every unit is in this table; the file's own `reading`
8833
+ block carries the definitions above in words.
8834
+
8835
+ | Field | Type | Unit | Meaning |
8836
+ | --- | --- | --- | --- |
8837
+ | `spec` | string | β€” | `"rigc-seam/1"` |
8838
+ | `animation` | string or `null` | β€” | the set's animation; `null` for a skeleton with none (its one frame is the setup pose) |
8839
+ | `skin` | string | β€” | the skin asked for; **absent** when none was |
8840
+ | `fps` | number | frames per second | the rate the set was sampled at |
8841
+ | `reading` | object | β€” | `read`, `cell`, `drawn`, `band`, `carried`, `gap`, `ghost`, `box`, `status`: the definitions, in words |
8842
+ | `pairs[]` | array | β€” | one per pair, in the flag's order |
8843
+ | `pairs[].a`, `pairs[].b` | string | β€” | the two slots, as the flag names them |
8844
+ | `pairs[].drawn` | integer | frames | frames on which both slots draw |
8845
+ | `pairs[].banded` | integer | frames | of those, frames whose two drawings share a cell at the setup pose β€” the frames read |
8846
+ | `pairs[].status` | `"read"` or `"skip"` | β€” | `skip` when `banded` is 0: nothing measured, which is **not** a seam that held |
8847
+ | `pairs[].opens` | integer[] | frame indices | every frame with a gap or a ghost, ascending |
8848
+ | `pairs[].maxPx` | integer | px (cells) | the most cells of either kind on any frame; 0 when none opens |
8849
+ | `pairs[].gap`, `pairs[].ghost` | object | β€” | one per kind, each `{ frames, spans, worst }` |
8850
+ | `<kind>.frames[]` | hit[] | β€” | only the frames with a cell of that kind, in frame order |
8851
+ | `<kind>.spans[]` | array | β€” | `<kind>.frames` as runs of consecutive indices: `{ frames: [first, last], times: [first, last], maxPx }` β€” frame indices, seconds, px |
8852
+ | `<kind>.worst` | hit or `null` | β€” | the frame with the most cells, the earliest on a tie; `null` when none |
8853
+ | hit `index` | integer | frame index | the frame β€” `f<index>.png`, zero-padded |
8854
+ | hit `time` | number | seconds | the frame's time |
8855
+ | hit `px` | integer | px (cells) | cells of that kind on the frame |
8856
+ | hit `cell` | number | world units | a cell's side |
8857
+ | hit `area` | number | world unitsΒ² | `px Γ— cellΒ²` |
8858
+ | hit `box` | `[minX, minY, maxX, maxY]` | world units, y up | the counted cells' whole squares, in Spine world |
8859
+
8860
+ A downstream warning is a projection of this file and needs nothing re-measured:
8861
+ one span of one kind is `{ pair: "<a>|<b>", frames: span.frames, times:
8862
+ span.times, maxPx: span.maxPx, kind }`, and one pair over one animation is
8863
+ `opens.length` frame(s) `[opens]`, max `maxPx` px β€” with `status: "skip"` the case
8864
+ to say SKIP for.
8865
+
8866
+ **Refused, nothing written:** a slot the skeleton does not have (with the slots it
8867
+ does), an entry without exactly one `:`, an empty entry or name, a slot paired with
8868
+ itself (a seam is between two drawings), a pair named twice in either order (the
8869
+ reading is symmetric, so `b:a` is `a:b` again), a slot that draws on no frame of
8870
+ the sets drawn, a slot a clip cuts (its drawing is no longer its attachment's, so
8871
+ it cannot be carried back), and `--slot`/`--hide` beside the flag, for
8872
+ `--pass-through`'s reason.
8873
+
8874
+ **Nothing else moves.** The PNGs, `contact.png`, `frames.json` and `geometry.json`
8875
+ are byte for byte what the render writes without the flag, both posers write the
8876
+ same file, and two runs write the same bytes (the selftest's `seam` suite).
8877
+
8878
+ **A reading, not a gate.** It never changes the exit status, whatever it finds. To
8879
+ look at a frame it names, draw that set over the box it printed:
8880
+ `render --animation <name> --viewport <x,y,w,h> --max 400`.
8881
+
8699
8882
  ---
8700
8883
 
8701
8884
  ## 9. Checking against the frames β€” `rigc check`
@@ -11522,6 +11705,7 @@ pixel (`IN09`).
11522
11705
  | What does this bone move? | `--show art,mesh,weights:<bone> --dim` | the bone's weight over every region and mesh it moves, as a heat: a pixel whose centre lies in a triangle is painted the ramp's colour at the blend of its corners' weights β€” blue near 0, yellow at 0.5, red at 1 (the stops are in the legend) β€” and a weight of 0 is not painted, so the art shows where the bone does not reach. A region and an unweighted mesh are moved whole by their slot's bone. The weights are read off `skeleton.json`, the file both posers skin from; a bone that weights nothing paints nothing and the run says so on a line of its own |
11523
11706
  | Does this part's edge follow that bone? | `--show art,weights:<bone>,mesh --overlay-slot <part> --zoom slot:<part>` | the part's own triangles and the bone's weight over them alone, over the whole painting. Where slots stack β€” a neck under the face, the hair, a collar β€” every mesh crossing the box is drawn and the heat is the union of every slot the bone moves, so the part's edge has no pixel of its own until the others are left out. The legend's `meshes[]` gives the part's weight for the bone at every vertex, so "does the top row follow the head" is read as numbers as well as colour |
11524
11707
  | Is this rig's density where its motion is? | `--show art,density --dim` | each region and mesh filled by its **ratio** β€” its triangles per world unitΒ² over the figure's β€” on one ramp, log-scaled and centred on 1Γ—: grey is as dense as the figure, pink to magenta denser (4Γ—, 16Γ—), light to dark green sparser (1/4Γ—, 1/16Γ—), clamped past the end stops (the stops are in the legend). A part's area is its posed mesh's own, the sum of its triangles' areas at the pose drawn, not the image's box; a region is two triangles over its quad. Where parts overlap the pixel is the part drawn last, as in the art, so a part under others is read with `--overlay-slot <part>`. The figure's totals are always over every slot the pose shows β€” the filter picks what is drawn and listed, never what the figure is β€” and the legend's `density` gives each slot's attachment, kind, vertices, triangles, hull, area, triangles per area, ratio and the bones it is weighted to, in draw order. A measurement, not a verdict: no bar is drawn, and `A13_MESH_BUDGET` stays the one gate on counts |
11708
+ | Does a joint tear in this animation? | `render --seam <a>:<b> --animation <name>` (Β§8.8) | One line per pair: the frames where background opens inside the joint (gap) or the overlap band of one piece shows past its partner (ghost), with the worst frame's cell count and world box; `seam.json` carries every frame and its time, and the runs of frames. A pair whose drawings never overlap is a SKIP, said by name. A reading, not a verdict β€” the exit status never moves |
11525
11709
  | Does anything fold in this animation? | `render --folds --animation <name>` (Β§8.3), then `inspect --show art,folds,mesh --animation <name> --at <the worst frame's time> --zoom folds` | `render --folds` prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count, which triangles, their world box at that frame and their setup-pose area. The view fills those triangles at the pose drawn β€” every pixel whose centre a folded triangle holds β€” in the colour the legend's `folds` states, and the legend's `folds[]` names, per mesh, the folded triangles by index with the same box and area, and the setup pose's degenerate and inverted ones, which are never counted. A fold is usually a sliver a few units across, invisible in a picture of the whole mesh: `--zoom folds` frames the picture to the folded triangles' box. A thin sliver still needs pixels β€” raise `--max` when the fill is a line. A reading, not a verdict |
11526
11710
  | How is this part triangulated? | `--show art,mesh,hull --zoom slot:<slot>` | each mesh's triangle edges, and over them its hull β€” vertex 0 to 1 to … to `hull βˆ’ 1` and back. Regions are not drawn: a region is not a mesh. The legend lists every vertex in world units and in pixels |
11527
11711
  | Which is drawn over which? | `--show art,slots` | each slot that shows a region or mesh numbered by its place in the draw order **at the pose drawn** (0 is drawn first), on a white box at the centre of its attachment's box β€” so a `drawOrder` key (Β§4.7) shows from the first frame `render` draws it on |
package/docs/INGEST.md CHANGED
@@ -191,6 +191,7 @@ rigc render
191
191
  .. atlas …/examples/spineboy/export/spineboy.atlas
192
192
  .. poser spine-core β€” no skeleton.model.json beside …/examples/spineboy/export/spineboy-pro.json β€” a Spine export, not a rigc build
193
193
  .. 200x186px at 8 fps, 1 set(s) -> …/render/sb
194
+ .. physics: none in this rig β€” it declares no physics constraint, so no frame steps one
194
195
  .. walk 9 frame(s), 1.000s + contact.png -> …/render/sb/walk@8fps
195
196
  rigc: wrote …/render/sb/frames.json
196
197
  ```
package/docs/MOTION.md CHANGED
@@ -1067,6 +1067,7 @@ rigc render --candidate spine --fps 24 --max 200
1067
1067
 
1068
1068
  ```
1069
1069
  .. 134x200px at 24 fps, 1 set(s) -> …/semaphore/render
1070
+ .. physics: none in this rig β€” it declares no physics constraint, so no frame steps one
1070
1071
  .. raise 14 frame(s), 0.542s + contact.png -> …/semaphore/render/raise@24fps
1071
1072
  ```
1072
1073
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/core",
3
- "version": "2.59.0",
3
+ "version": "2.61.0",
4
4
  "description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
5
5
  "type": "module",
6
6
  "bin": {