@rig-c/core 2.58.1 → 2.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/docs/AUTHORING.md +145 -1
- package/package.json +1 -1
- package/src/cli/core_commands.ts +140 -5
- package/src/cli/shared.ts +60 -3
- package/src/mesh.ts +17 -0
- package/src/passthrough.ts +3 -3
- package/src/seam.ts +576 -0
- package/src/stop.ts +33 -3
package/README.md
CHANGED
|
@@ -603,7 +603,7 @@ commands take it and what its default is.
|
|
|
603
603
|
| `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
604
|
| `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
605
|
| `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 |
|
|
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. `--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
607
|
| `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
608
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
609
609
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
|
@@ -614,6 +614,7 @@ commands take it and what its default is.
|
|
|
614
614
|
| `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
|
|
615
615
|
| `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
616
|
| `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 |
|
|
617
|
+
| `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
618
|
|
|
618
619
|
`diff`, `bonedist`, `check` and `bench` measure against something you were given; the
|
|
619
620
|
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]
|
|
@@ -4518,6 +4525,17 @@ rest; a frame parent scaled unevenly, sheared or reflected, or such a bone
|
|
|
4518
4525
|
between two links; a link below a bone a later stop generates; a `from` bone in
|
|
4519
4526
|
the angles' path.
|
|
4520
4527
|
|
|
4528
|
+
On the first link that bound is |rest| + `outward`, where rest is the angle
|
|
4529
|
+
between the direction the link hangs in and the aim from its root at
|
|
4530
|
+
`<chain>_touch`, so where the point sits decides it as much as `outward` does.
|
|
4531
|
+
The refusal names the link, its root's world position and hang, the point's world
|
|
4532
|
+
position, rest, the end and the sum, and says what to move: `<chain>_touch` to
|
|
4533
|
+
within 180° − `outward` of the link's hang direction — below the root, for a link
|
|
4534
|
+
that hangs straight down and `outward` up to 90° — or `outward` down to
|
|
4535
|
+
180° − |rest|. A leg that rises from behind the cloth, as on a prone figure,
|
|
4536
|
+
runs its local +x up the screen, so `at` at its far end lands above the chain's
|
|
4537
|
+
root; put it at the knee, below the root (`LG34`).
|
|
4538
|
+
|
|
4521
4539
|
The stop decides nothing about *when* — it is what the object can do. Read it
|
|
4522
4540
|
with `render --pass-through <cloth>:<leg>` (§8.5) on the motion that swung
|
|
4523
4541
|
through before.
|
|
@@ -7352,7 +7370,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7352
7370
|
| <a id="CE_STOP_LINK_UNDRIVEN"></a>`CE_STOP_LINK_UNDRIVEN` | “<stop.who>: links[<k>] "<link>" 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
7371
|
| <a id="CE_STOP_LINK_PARENT_CHAIN"></a>`CE_STOP_LINK_PARENT_CHAIN` | “<stop.who>: links[<k>] "<links[k]>" does not hang below "<names.drawn[k - 1]>", the drawn link of links[<k - 1>] — 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
7372
|
| <a id="CE_STOP_AGAINST_MOVES_WITH_STOP"></a>`CE_STOP_AGAINST_MOVES_WITH_STOP` | “<stop.who>: "against" "<stop.against>" hangs below "<under>", 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` | “<stop.who>: "<field><end>" <range[end]> puts <what>'s bound at <value>°, past ±<BOUND>° — 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”. |
|
|
7373
|
+
| <a id="CE_STOP_LOCAL_BOUND_RANGE"></a>`CE_STOP_LOCAL_BOUND_RANGE` | “<stop.who>: "<field><end>" <range[end]> puts <what>'s bound at <value>°, past ±<BOUND>° — 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: “. "<stop.links[0]>" hangs at <at.hang>° from its root at [<at.root>], and "<stop.names.point>" ("at" [<stop.at>] on "<stop.against>") sits at [<at.point>] — Spine world, y up, at the setup pose — so the aim from that root to the point lies <off>° off the hang (rest <rest>°): \|rest\| <off>° + "<end>" <e>° is <sum>°”, then one of “, above <BOUND>°. Move "<stop.names.point>" to within <BOUND − e>° (<BOUND>° − "<end>") of the direction "<stop.links[0]>" hangs in from its root, <at.hang>°, or state "<end>" at most <BOUND − off>°”, “, above <BOUND>°, and no point lowers it: \|rest\| is above 0° for every point a stop accepts. State "<end>" below <BOUND>°”, “, below -<BOUND>°. State "<end>" at least <−BOUND − off>°”<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
7374
|
| <a id="CE_STOP_DEPENDENCY_ORDER"></a>`CE_STOP_DEPENDENCY_ORDER` | “<who>: "<tip>" hangs below "<name>", which a later stop generates — declare that stop first, so its angles are known when this one is read”<br>Also: “<value>: stop "<value>"” | Declare the stop generating the parent before the stop depending on it. |
|
|
7357
7375
|
| <a id="CE_STOP_ANCESTOR_MANIFEST_POSITION"></a>`CE_STOP_ANCESTOR_MANIFEST_POSITION` | “<who>: "<name>" 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: “<value>: stop "<value>"” | State x, y and rotation for the named setup-pose ancestor instead of from. |
|
|
7358
7376
|
| <a id="CE_STOP_FRAME_PARENT_TRANSFORM"></a>`CE_STOP_FRAME_PARENT_TRANSFORM` | “<who>: "<parent>", 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: “<value>: stop "<value>"” | Use a frame parent with rotation and even positive scale, without shear or reflection. |
|
|
@@ -8696,6 +8714,131 @@ one scaled to nothing — named with the frame.
|
|
|
8696
8714
|
**A reading, not a gate.** Whether a range is too wide is yours; §4.6 says which
|
|
8697
8715
|
values move it.
|
|
8698
8716
|
|
|
8717
|
+
### 8.8 Where two slots stop meeting — `render --seam`
|
|
8718
|
+
|
|
8719
|
+
A cut rig can tear in motion: two pieces painted to overlap at a joint pull apart
|
|
8720
|
+
and the background shows between them, or one swings clear of the piece it was
|
|
8721
|
+
tucked under and the overlap it carries shows where nothing was. Both pass every
|
|
8722
|
+
assertion and every check, and either can be intended — so `--seam` names the
|
|
8723
|
+
frames and decides nothing. On the selftest's probe (`pass_through_probe`,
|
|
8724
|
+
`fixtures/public.ts`: a skirt whose hip swings 30° and back over a leg that hangs
|
|
8725
|
+
inside it, and a second copy of the leg on the same bone):
|
|
8726
|
+
|
|
8727
|
+
```bash
|
|
8728
|
+
bun cli.ts render --candidate path/to/spine --animation swing --seam skirt:leg,leg_front:leg [--skin …] [--fps 12]
|
|
8729
|
+
# rigc render
|
|
8730
|
+
# .. swing 25 frame(s), 2.000s + contact.png + seam.json (1 of 2 pair(s) open) -> …/swing
|
|
8731
|
+
# .. 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)
|
|
8732
|
+
# .. seam "swing": seam "leg_front" | "leg" stays closed on all 25 frame(s) read at 12 fps — no gap, no ghost
|
|
8733
|
+
```
|
|
8734
|
+
|
|
8735
|
+
The skirt swinging off the leg opens background between the leg's right edge and
|
|
8736
|
+
the skirt's swung side (the gap, widest at the top of the swing), and uncovers the
|
|
8737
|
+
top of the leg the skirt hid at rest (the ghost). The two copies of the leg move
|
|
8738
|
+
on one bone and never open. A pair with nothing to read says so —
|
|
8739
|
+
`seam "a" | "b": SKIP — the two drawings share no cell at the setup pose on any of
|
|
8740
|
+
the N frame(s) both draw, so there is no band to read` — and a closed pair says
|
|
8741
|
+
`stays closed on all N frame(s) read`; the two are never the same line.
|
|
8742
|
+
|
|
8743
|
+
**The frame of reference.** A seam is not read in the world: a limb that swings
|
|
8744
|
+
moves both of its pieces from where they were at the setup pose, and that is
|
|
8745
|
+
motion, not a tear. It is read **from each piece's own point of view**. A world
|
|
8746
|
+
point of a frame is carried back into a piece's setup pose by the affine map of
|
|
8747
|
+
that piece's triangle holding it as the frame poses it; outside the piece's mesh,
|
|
8748
|
+
by the map of the triangle it is least outside of (the largest smallest
|
|
8749
|
+
barycentric weight, the lowest index on a tie) — exact for a region and for any
|
|
8750
|
+
mesh one bone moves, the nearest affine piece of a skinned one. Two pieces that
|
|
8751
|
+
move together carry a point back to one place, so a pair moved whole reads nothing.
|
|
8752
|
+
|
|
8753
|
+
**Every number, and where it comes from.** Nothing here is a threshold the rig
|
|
8754
|
+
did not declare; each figure is the posed vertices (`--geometry`'s), the rest
|
|
8755
|
+
table's vertices, and the atlas alpha:
|
|
8756
|
+
|
|
8757
|
+
- **The cell** is one texel of the coarser of the two drawings at the setup pose,
|
|
8758
|
+
in world units — the step `--pass-through` steps by (§8.5) — on multiples of it
|
|
8759
|
+
from the world origin. `px` counts cells: on art drawn one texel to a unit, a
|
|
8760
|
+
cell is a world unit and a px is a pixel of the art.
|
|
8761
|
+
- **Drawn** is the renderer painting a cell's centre at **128 of 255** or more —
|
|
8762
|
+
the half-opacity line `--pass-through` draws outlines at, the frame's tint
|
|
8763
|
+
included, the setup pose drawn at the same tint (so a slot faded out is not a
|
|
8764
|
+
tear: it is faded in both). No other alpha enters.
|
|
8765
|
+
- **The band** is the cells both drawings draw at the setup pose — where the two
|
|
8766
|
+
were painted to overlap. It has **no width of its own**: it is the overlap the
|
|
8767
|
+
art states, dilated by nothing. A frame whose band is empty is not read; a pair
|
|
8768
|
+
whose band is empty on every frame both draw is a SKIP.
|
|
8769
|
+
- **gap** — a cell inside the convex hull of the band's cell centres, carried to
|
|
8770
|
+
the frame by both pieces (the region the joint occupies now, as both place it),
|
|
8771
|
+
that neither piece draws at the frame, and that at least one piece, carrying it
|
|
8772
|
+
back, finds drawn by either drawing at the setup pose: the two no longer cover
|
|
8773
|
+
what they covered together. The hull is the joint's own extent, which is what
|
|
8774
|
+
keeps a piece's far end swinging away from reading as a tear; the rest test is
|
|
8775
|
+
what keeps a band that is concave at the setup pose from reading its own notch.
|
|
8776
|
+
- **ghost** — a cell one piece draws at the frame and the other does not, which
|
|
8777
|
+
the drawing piece, carrying it back, lands in the band, and which the other,
|
|
8778
|
+
carrying it back, finds neither drawing at at the setup pose: the overlap swung
|
|
8779
|
+
clear of its partner.
|
|
8780
|
+
|
|
8781
|
+
Draw order does not enter: a cell is covered or bare whichever piece is in front.
|
|
8782
|
+
|
|
8783
|
+
**Its reach, stated.** A piece folded right back over its partner carries the band
|
|
8784
|
+
across the region it vacated, so a hull that wide reads the vacated cells its
|
|
8785
|
+
partner's view still finds drawn. And no size is too small to print: on
|
|
8786
|
+
`gallery/walk`, built as its README says, `--seam thigh_f:shin_f,arm_f:hand_f`
|
|
8787
|
+
reads gaps of 1–2 px on 5 of the 12 frames at the knee and of 1 px on 2 at the
|
|
8788
|
+
wrist — the wrist's, drawn at 20 pixels a unit, is the V where the two outlines
|
|
8789
|
+
meet at an angle, one cell deep. Whether that is a tear is yours.
|
|
8790
|
+
|
|
8791
|
+
**The file.** `seam.json`, `rigc-seam/1`, beside each set's frames — the contract
|
|
8792
|
+
a consumer reads it by. Every unit is in this table; the file's own `reading`
|
|
8793
|
+
block carries the definitions above in words.
|
|
8794
|
+
|
|
8795
|
+
| Field | Type | Unit | Meaning |
|
|
8796
|
+
| --- | --- | --- | --- |
|
|
8797
|
+
| `spec` | string | — | `"rigc-seam/1"` |
|
|
8798
|
+
| `animation` | string or `null` | — | the set's animation; `null` for a skeleton with none (its one frame is the setup pose) |
|
|
8799
|
+
| `skin` | string | — | the skin asked for; **absent** when none was |
|
|
8800
|
+
| `fps` | number | frames per second | the rate the set was sampled at |
|
|
8801
|
+
| `reading` | object | — | `read`, `cell`, `drawn`, `band`, `carried`, `gap`, `ghost`, `box`, `status`: the definitions, in words |
|
|
8802
|
+
| `pairs[]` | array | — | one per pair, in the flag's order |
|
|
8803
|
+
| `pairs[].a`, `pairs[].b` | string | — | the two slots, as the flag names them |
|
|
8804
|
+
| `pairs[].drawn` | integer | frames | frames on which both slots draw |
|
|
8805
|
+
| `pairs[].banded` | integer | frames | of those, frames whose two drawings share a cell at the setup pose — the frames read |
|
|
8806
|
+
| `pairs[].status` | `"read"` or `"skip"` | — | `skip` when `banded` is 0: nothing measured, which is **not** a seam that held |
|
|
8807
|
+
| `pairs[].opens` | integer[] | frame indices | every frame with a gap or a ghost, ascending |
|
|
8808
|
+
| `pairs[].maxPx` | integer | px (cells) | the most cells of either kind on any frame; 0 when none opens |
|
|
8809
|
+
| `pairs[].gap`, `pairs[].ghost` | object | — | one per kind, each `{ frames, spans, worst }` |
|
|
8810
|
+
| `<kind>.frames[]` | hit[] | — | only the frames with a cell of that kind, in frame order |
|
|
8811
|
+
| `<kind>.spans[]` | array | — | `<kind>.frames` as runs of consecutive indices: `{ frames: [first, last], times: [first, last], maxPx }` — frame indices, seconds, px |
|
|
8812
|
+
| `<kind>.worst` | hit or `null` | — | the frame with the most cells, the earliest on a tie; `null` when none |
|
|
8813
|
+
| hit `index` | integer | frame index | the frame — `f<index>.png`, zero-padded |
|
|
8814
|
+
| hit `time` | number | seconds | the frame's time |
|
|
8815
|
+
| hit `px` | integer | px (cells) | cells of that kind on the frame |
|
|
8816
|
+
| hit `cell` | number | world units | a cell's side |
|
|
8817
|
+
| hit `area` | number | world units² | `px × cell²` |
|
|
8818
|
+
| hit `box` | `[minX, minY, maxX, maxY]` | world units, y up | the counted cells' whole squares, in Spine world |
|
|
8819
|
+
|
|
8820
|
+
A downstream warning is a projection of this file and needs nothing re-measured:
|
|
8821
|
+
one span of one kind is `{ pair: "<a>|<b>", frames: span.frames, times:
|
|
8822
|
+
span.times, maxPx: span.maxPx, kind }`, and one pair over one animation is
|
|
8823
|
+
`opens.length` frame(s) `[opens]`, max `maxPx` px — with `status: "skip"` the case
|
|
8824
|
+
to say SKIP for.
|
|
8825
|
+
|
|
8826
|
+
**Refused, nothing written:** a slot the skeleton does not have (with the slots it
|
|
8827
|
+
does), an entry without exactly one `:`, an empty entry or name, a slot paired with
|
|
8828
|
+
itself (a seam is between two drawings), a pair named twice in either order (the
|
|
8829
|
+
reading is symmetric, so `b:a` is `a:b` again), a slot that draws on no frame of
|
|
8830
|
+
the sets drawn, a slot a clip cuts (its drawing is no longer its attachment's, so
|
|
8831
|
+
it cannot be carried back), and `--slot`/`--hide` beside the flag, for
|
|
8832
|
+
`--pass-through`'s reason.
|
|
8833
|
+
|
|
8834
|
+
**Nothing else moves.** The PNGs, `contact.png`, `frames.json` and `geometry.json`
|
|
8835
|
+
are byte for byte what the render writes without the flag, both posers write the
|
|
8836
|
+
same file, and two runs write the same bytes (the selftest's `seam` suite).
|
|
8837
|
+
|
|
8838
|
+
**A reading, not a gate.** It never changes the exit status, whatever it finds. To
|
|
8839
|
+
look at a frame it names, draw that set over the box it printed:
|
|
8840
|
+
`render --animation <name> --viewport <x,y,w,h> --max 400`.
|
|
8841
|
+
|
|
8699
8842
|
---
|
|
8700
8843
|
|
|
8701
8844
|
## 9. Checking against the frames — `rigc check`
|
|
@@ -11522,6 +11665,7 @@ pixel (`IN09`).
|
|
|
11522
11665
|
| 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
11666
|
| 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
11667
|
| 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 |
|
|
11668
|
+
| 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
11669
|
| 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
11670
|
| 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
11671
|
| 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.60.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": {
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -38,6 +38,7 @@ import { FOLDS_FILE, foldLine, foldsFileOf, foldsOf, foldsText, setupFaultLine }
|
|
|
38
38
|
import { AngleRangeError, angleRangeLine, type AngleRangeReading, angleRangesOf, type BonePair, readBonePairs } from '../anglerange.ts';
|
|
39
39
|
import { outsideLine, outsideViewport, pinnedRecord, readViewportBox, viewportOfFramesFile, ViewportError } from '../viewportpin.ts';
|
|
40
40
|
import { PASS_THROUGH_FILE, passFrameLine, type PassPair, passSummaryLine, passThroughFileOf, passThroughOf, passThroughText, PassThroughError, readPassPairs } from '../passthrough.ts';
|
|
41
|
+
import { readSeamPairs, SEAM_FILE, seamFileOf, seamLine, SeamError, type SeamPair, seamsOf, seamText } from '../seam.ts';
|
|
41
42
|
import { environmentCollisions, environmentFrame, EnvironmentError, environmentResponse, readEnvironmentSchedule, type EnvironmentStep } from '../environment.ts';
|
|
42
43
|
import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
43
44
|
import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, frameSetDir, type FrameSet, type FramesSidecar, framingViewport, GEOMETRY_FILE, geometryFileOf, geometryText, loadCandidate, POSER_NAMES, type PoserName, PROTOCOL_FPS, refuseUnchosen, renderFrame, sampleAll, sampleAnimation, SETUP_POSE_DIR, SHEET_FILE, SHEET_TILE, sidecarViewport, type SkeletonFacts, type SlotSubset, SlotSubsetError, throughPoser, type Viewport, viewportFor } from '../render_shared.ts';
|
|
@@ -45,9 +46,10 @@ import { type CompileResult } from '../types.ts';
|
|
|
45
46
|
import { attachmentRegionJoins } from '../region_joins.ts';
|
|
46
47
|
import { cmdRepack } from './repack.ts';
|
|
47
48
|
import { GUIDE_FILE, GUIDE_RULE_TABLES_ELSEWHERE, guideAnswer, refusalLine } from '../guide.ts';
|
|
48
|
-
import { ATLAS_ABSENT, COMMANDS, type CommandRun, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, GuideError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
49
|
+
import { ATLAS_ABSENT, COMMANDS, type CommandRun, DelegateError, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, GuideError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
49
50
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
50
51
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
52
|
+
import { spawnSync } from 'node:child_process';
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> = {
|
|
@@ -561,6 +563,23 @@ function readPassThroughFlag(raw: string | undefined, facts: SkeletonFacts): Pas
|
|
|
561
563
|
}
|
|
562
564
|
}
|
|
563
565
|
|
|
566
|
+
/**
|
|
567
|
+
* `--seam <a>:<b>[,…]` read against the skeleton's slots (issue #1456,
|
|
568
|
+
* `../seam.ts`), or refused as a usage error before anything is posed: a
|
|
569
|
+
* malformed list, an empty name, a slot the skeleton does not have, a slot paired
|
|
570
|
+
* with itself, a pair named twice in either order — each by its entry.
|
|
571
|
+
* `undefined` without the flag.
|
|
572
|
+
*/
|
|
573
|
+
function readSeamFlag(raw: string | undefined, facts: SkeletonFacts): SeamPair[] | undefined {
|
|
574
|
+
if (raw === undefined) return undefined;
|
|
575
|
+
try {
|
|
576
|
+
return readSeamPairs(raw, facts.slots.map((slot) => slot.name));
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (err instanceof SeamError) throw new UsageError(err.message);
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
564
583
|
/**
|
|
565
584
|
* `--angle-range`'s pairs, read against the skeleton's bones (issue #1414,
|
|
566
585
|
* `../anglerange.ts`), or refused as a usage error before anything is posed: a
|
|
@@ -685,6 +704,21 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
685
704
|
}
|
|
686
705
|
}
|
|
687
706
|
|
|
707
|
+
// The seam reading (issue #1456) is its own flag for --folds' reason, and refused beside a subset for --pass-through's:
|
|
708
|
+
// it reads the two slots as the whole rig draws them.
|
|
709
|
+
const seam = flags.seam;
|
|
710
|
+
if (seam !== undefined) {
|
|
711
|
+
for (const flag of ['slot', 'hide'] as const) {
|
|
712
|
+
if (flags[flag] !== undefined) {
|
|
713
|
+
throw new UsageError(
|
|
714
|
+
`--seam takes no --${flag}: it reads the two slots as the whole rig draws them, which a subset of the drawn slots ` +
|
|
715
|
+
`would change. Run \`rigc render --seam\` on the whole rig for the reading, and --${flag} ${flags[flag]} ` +
|
|
716
|
+
'without --seam for the pictures — both land on the same grid',
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
688
722
|
console.log('rigc render');
|
|
689
723
|
console.log(` .. skeleton ${skeletonPath}`);
|
|
690
724
|
console.log(` .. atlas ${atlasPath}${atlasText === null ? ` — not there (${ATLAS_ABSENT})` : ''}`);
|
|
@@ -703,6 +737,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
703
737
|
const skin = readSkinFlag(flags, [...facts.skins]);
|
|
704
738
|
const subset = readSlotSubsetFlags(flags, facts, skin);
|
|
705
739
|
const passPairs = readPassThroughFlag(passThrough, facts);
|
|
740
|
+
const seamPairs = readSeamFlag(seam, facts);
|
|
706
741
|
// The angle reading (issue #1414) reads bones, which a subset of the drawn slots does not change, so it takes --slot/--hide.
|
|
707
742
|
const anglePairs = readAngleRangeFlag(flags['angle-range'], facts);
|
|
708
743
|
// One object, so the framing and the frames cannot be posed under two
|
|
@@ -774,7 +809,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
774
809
|
// the export's grid is this frame set's by construction.
|
|
775
810
|
// The bones ride along for --geometry and --angle-range, the whole attachments for --geometry and the two readings off them.
|
|
776
811
|
const wantsBones = geometry || anglePairs !== undefined;
|
|
777
|
-
const wantsGeometry = geometry || folds || passPairs !== undefined;
|
|
812
|
+
const wantsGeometry = geometry || folds || passPairs !== undefined || seamPairs !== undefined;
|
|
778
813
|
const sampling = !wantsBones && !wantsGeometry ? pose : { ...pose, ...(wantsBones ? { bones: true } : {}), ...(wantsGeometry ? { geometry: true } : {}) };
|
|
779
814
|
const sampled: Map<string, Frame[]> =
|
|
780
815
|
only === undefined
|
|
@@ -828,6 +863,34 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
828
863
|
}
|
|
829
864
|
}
|
|
830
865
|
}
|
|
866
|
+
// The seam reading, off the very pieces the frames are drawn from — one per set, every pair the flag names. A pair
|
|
867
|
+
// whose slot draws on no frame of any set drawn is refused, as --pass-through refuses it, rather than read as a SKIP.
|
|
868
|
+
const seamReadings = new Map<string, ReturnType<typeof seamFileOf>>();
|
|
869
|
+
if (seamPairs !== undefined) {
|
|
870
|
+
const draws = new Set<string>();
|
|
871
|
+
for (const frames of sampled.values()) for (const frame of frames) for (const piece of frame.pieces) draws.add(piece.slot);
|
|
872
|
+
for (const pair of seamPairs) {
|
|
873
|
+
for (const slot of [pair.a, pair.b]) {
|
|
874
|
+
if (!draws.has(slot)) {
|
|
875
|
+
throw new UsageError(
|
|
876
|
+
`--seam: slot ${JSON.stringify(slot)} draws on no frame of [${[...sampled.keys()].join(', ')}]${skin === undefined ? '' : ` under skin ${JSON.stringify(skin)}`} ` +
|
|
877
|
+
'— it shows no region or mesh there, so it has no drawing to read a seam of',
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
for (const [name, frames] of sampled) {
|
|
883
|
+
const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
|
|
884
|
+
const shown = frames.map((frame) => frame.attachments ?? []);
|
|
885
|
+
const read = frames.map((frame) => ({ index: frame.index, time: frame.time, pieces: frame.pieces, attachments: frame.attachments ?? [] }));
|
|
886
|
+
try {
|
|
887
|
+
seamReadings.set(name, seamFileOf(animation, fps, skin, seamsOf(seamPairs, poser.rest(skin, shown), read, pages)));
|
|
888
|
+
} catch (err) {
|
|
889
|
+
if (err instanceof SeamError) throw new UsageError(`--seam, ${JSON.stringify(name)}: ${err.message}`);
|
|
890
|
+
throw err;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
831
894
|
// The angle reading, off the very bones the frames were posed with — one per set, every pair the flag names.
|
|
832
895
|
const angleReadings = new Map<string, AngleRangeReading[]>();
|
|
833
896
|
if (anglePairs !== undefined) {
|
|
@@ -841,9 +904,9 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
841
904
|
}
|
|
842
905
|
}
|
|
843
906
|
}
|
|
844
|
-
return { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, angleReadings };
|
|
907
|
+
return { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings };
|
|
845
908
|
});
|
|
846
|
-
const { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, angleReadings } = posed.value;
|
|
909
|
+
const { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings } = posed.value;
|
|
847
910
|
console.log(` .. poser ${posed.note}`);
|
|
848
911
|
if (environment !== undefined) {
|
|
849
912
|
const response = environmentResponse(skeletonText);
|
|
@@ -893,6 +956,8 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
893
956
|
if (foldReading !== undefined) writeFileSync(join(dir, FOLDS_FILE), foldsText(foldReading));
|
|
894
957
|
const passReading = passReadings.get(name);
|
|
895
958
|
if (passReading !== undefined) writeFileSync(join(dir, PASS_THROUGH_FILE), passThroughText(passReading));
|
|
959
|
+
const seamReading = seamReadings.get(name);
|
|
960
|
+
if (seamReading !== undefined) writeFileSync(join(dir, SEAM_FILE), seamText(seamReading));
|
|
896
961
|
const duration = frames[frames.length - 1].time;
|
|
897
962
|
sets.push({
|
|
898
963
|
dir: dirName,
|
|
@@ -908,7 +973,9 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
908
973
|
const foldNote = foldReading === undefined ? '' : ` + ${FOLDS_FILE} (${folding} of ${foldReading.meshes.length} mesh(es) fold)`;
|
|
909
974
|
const passing = passReading?.pairs.filter((p) => p.frames.length > 0).length ?? 0;
|
|
910
975
|
const passNote = passReading === undefined ? '' : ` + ${PASS_THROUGH_FILE} (${passing} of ${passReading.pairs.length} pair(s) pass through)`;
|
|
911
|
-
const
|
|
976
|
+
const opening = seamReading?.pairs.filter((p) => p.opens.length > 0).length ?? 0;
|
|
977
|
+
const seamNote = seamReading === undefined ? '' : ` + ${SEAM_FILE} (${opening} of ${seamReading.pairs.length} pair(s) open)`;
|
|
978
|
+
const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}${foldNote}${passNote}${seamNote}`;
|
|
912
979
|
console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${extras} -> ${dir}`);
|
|
913
980
|
for (const mesh of foldReading?.meshes ?? []) {
|
|
914
981
|
const key = `${mesh.slot}\u0000${mesh.attachment}`;
|
|
@@ -922,6 +989,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
922
989
|
console.log(` .. pass ${passSummaryLine(passReading?.animation ?? null, fps, pair)}`);
|
|
923
990
|
for (const frame of pair.frames) console.log(` .. pass ${passFrameLine(passReading?.animation ?? null, pair, frame)}`);
|
|
924
991
|
}
|
|
992
|
+
for (const pair of seamReading?.pairs ?? []) console.log(` .. seam ${seamLine(seamReading?.animation ?? null, fps, pair)}`);
|
|
925
993
|
for (const reading of angleReadings.get(name) ?? []) {
|
|
926
994
|
console.log(` .. angle ${angleRangeLine(name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name, fps, reading)}`);
|
|
927
995
|
}
|
|
@@ -2103,6 +2171,72 @@ export const MODEL_AND_TEXT_GATE: BuildGate = {
|
|
|
2103
2171
|
look: (outDir) => `rigc: look at it: rigc render --candidate ${outDir}`,
|
|
2104
2172
|
};
|
|
2105
2173
|
|
|
2174
|
+
/**
|
|
2175
|
+
* Where a delegated package's `package.json` resolves, or `null` (issue #1457).
|
|
2176
|
+
*
|
|
2177
|
+
* ⚖️ Node's `node_modules` lookup — `<dir>/node_modules/<package>` for the
|
|
2178
|
+
* directory and then each of its ancestors, nearest first — from two places in
|
|
2179
|
+
* this order, the first answer winning:
|
|
2180
|
+
*
|
|
2181
|
+
* 1. the working directory — the project `npm i -D <package>` installed it
|
|
2182
|
+
* into, which is what the refusal tells a caller to run, so a project's
|
|
2183
|
+
* own install is the one its command line reaches;
|
|
2184
|
+
* 2. this package's root — where an install of rig-c sits beside it in the
|
|
2185
|
+
* same `node_modules` and the command was run from somewhere else.
|
|
2186
|
+
*
|
|
2187
|
+
* 🚨 Walked here rather than asked of the runtime's resolver: Bun's resolves a
|
|
2188
|
+
* package it cannot find, from a directory with no `node_modules` above it, by
|
|
2189
|
+
* installing it on demand into its global cache — measured, a directory with
|
|
2190
|
+
* none resolved `@rig-c/review` there — so "not installed" would never be
|
|
2191
|
+
* refused, and asking would reach the network. `NODE_PATH` and the global
|
|
2192
|
+
* folders are not read: what the refusal tells a caller to run never puts a
|
|
2193
|
+
* package there. The bin is read off the `package.json` found, not resolved.
|
|
2194
|
+
*/
|
|
2195
|
+
function resolveDelegate(name: string): string | null {
|
|
2196
|
+
for (const from of [process.cwd(), PACKAGE_ROOT]) {
|
|
2197
|
+
for (let dir = resolve(from); ; dir = dirname(dir)) {
|
|
2198
|
+
const manifest = join(dir, 'node_modules', name, 'package.json');
|
|
2199
|
+
if (existsSync(manifest)) return manifest;
|
|
2200
|
+
if (dirname(dir) === dir) break;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return null;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
/**
|
|
2207
|
+
* A delegating command's body (`CommandDoc.delegate`, issue #1457): resolve the
|
|
2208
|
+
* package's bin and run it under the Bun running this — the bin is a Bun script
|
|
2209
|
+
* — with every argument untouched and stdio inherited, then exit with its code,
|
|
2210
|
+
* or die by its signal the way `bin/rigc.cjs` does, so a caller reads what it
|
|
2211
|
+
* would have read running the bin itself. Not installed, it is refused by name
|
|
2212
|
+
* with the one command that installs it, exit 2, and nothing else is printed.
|
|
2213
|
+
*/
|
|
2214
|
+
function cmdDelegate(command: string, raw: readonly string[]): void {
|
|
2215
|
+
const delegate = COMMANDS.find((c) => c.name === command)?.delegate;
|
|
2216
|
+
if (delegate === undefined) throw new Error(`internal: ${command} names no delegate`);
|
|
2217
|
+
const manifest = resolveDelegate(delegate.package);
|
|
2218
|
+
if (manifest === null) throw new DelegateError(`${delegate.package} is not installed — npm i -D ${delegate.package}`, 2);
|
|
2219
|
+
const bins = (readJsonFile(manifest) as { bin?: unknown }).bin;
|
|
2220
|
+
const relativeBin = typeof bins === 'object' && bins !== null ? (bins as Record<string, unknown>)[delegate.bin] : undefined;
|
|
2221
|
+
const bin = typeof relativeBin === 'string' ? join(dirname(manifest), relativeBin) : null;
|
|
2222
|
+
if (bin === null || !existsSync(bin)) {
|
|
2223
|
+
throw new DelegateError(
|
|
2224
|
+
`${delegate.package} at ${dirname(manifest)} ${bin === null ? `declares no "${delegate.bin}" bin` : `declares the "${delegate.bin}" bin ${String(relativeBin)}, which is not there`} — npm i -D ${delegate.package}`,
|
|
2225
|
+
2,
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
const ran = spawnSync(process.execPath, [bin, ...raw], { stdio: 'inherit' });
|
|
2229
|
+
// ⚠️ The exit and the signal are read before `error`: Bun 1.2.0 — the declared minimum — sets `error` on every
|
|
2230
|
+
// child that exits non-zero, so reading it first refused review's own exit 7 as "could not run". `error` is a
|
|
2231
|
+
// launch that failed only where the child left neither a status nor a signal.
|
|
2232
|
+
if (typeof ran.status === 'number') process.exit(ran.status);
|
|
2233
|
+
if (ran.signal !== null) {
|
|
2234
|
+
process.kill(process.pid, ran.signal);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
throw new DelegateError(`could not run ${bin}: ${ran.error?.message ?? 'it left no exit status and no signal'}`, 1);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2106
2240
|
/**
|
|
2107
2241
|
* split — one authored mesh's pieces, proposed as one mesh each (issue #1414).
|
|
2108
2242
|
*
|
|
@@ -2182,4 +2316,5 @@ export const CORE_COMMAND_RUNS: Readonly<Record<string, CommandRun>> = {
|
|
|
2182
2316
|
chainfit: ({ flags }) => cmdChainFit(flags),
|
|
2183
2317
|
guide: ({ flags, positional }) => cmdGuide(flags, positional),
|
|
2184
2318
|
skills: ({ flags, positional }) => cmdSkills(flags, positional),
|
|
2319
|
+
review: ({ raw }) => cmdDelegate('review', raw),
|
|
2185
2320
|
};
|
package/src/cli/shared.ts
CHANGED
|
@@ -38,7 +38,8 @@ import { checkAgainstFrames, type CheckOptions, CheckPlates, type CheckReport }
|
|
|
38
38
|
import { compile, CompileError, droppedStateReason, headerBoundsOf, type CompileOptions } from '../compile.ts';
|
|
39
39
|
import { depthStepLevels, type FoldLimit, type TurnCeiling } from '../depth.ts';
|
|
40
40
|
import { FOLDS_FILE } from '../folds.ts';
|
|
41
|
-
import { PASS_THROUGH_FILE, PASS_THROUGH_RUN } from '../passthrough.ts';
|
|
41
|
+
import { DRAWN_ALPHA, PASS_THROUGH_FILE, PASS_THROUGH_RUN } from '../passthrough.ts';
|
|
42
|
+
import { SEAM_FILE } from '../seam.ts';
|
|
42
43
|
import { readViewportBox, ViewportError } from '../viewportpin.ts';
|
|
43
44
|
import { parseJsonWithPosition } from '../json-position.ts';
|
|
44
45
|
import { RUNG_IDS } from '../ladder.ts';
|
|
@@ -955,6 +956,21 @@ export class SkillsInstallError extends Error {}
|
|
|
955
956
|
/** `guide` asked for a rule or a section the guide does not have (issue #1397): a refusal by `GUIDE_RULE_KNOWN` or `GUIDE_SECTION_KNOWN`, whose message is the whole line. Exit 1. */
|
|
956
957
|
export class GuideError extends Error {}
|
|
957
958
|
|
|
959
|
+
/**
|
|
960
|
+
* A delegating command (`CommandDoc.delegate`, issue #1457) that could not hand
|
|
961
|
+
* over: the package it delegates to does not resolve, or resolves without the
|
|
962
|
+
* bin it names. Nothing ran, so the exit is `status` (2) and the message is the
|
|
963
|
+
* whole of what is printed after `rigc <command>: `.
|
|
964
|
+
*/
|
|
965
|
+
export class DelegateError extends Error {
|
|
966
|
+
constructor(
|
|
967
|
+
message: string,
|
|
968
|
+
readonly status: number,
|
|
969
|
+
) {
|
|
970
|
+
super(message);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
958
974
|
// ---------------------------------------------------------------------------
|
|
959
975
|
// usage / per-command help
|
|
960
976
|
// ---------------------------------------------------------------------------
|
|
@@ -1023,6 +1039,13 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1023
1039
|
'world units, one texel of the coarser drawing to a step — each with the depth and the world box. One line per pair, and one per frame that passes through. ' +
|
|
1024
1040
|
'The order is the declaration: the first name must draw over the second for a frame to be read. A reading, not a gate. ' +
|
|
1025
1041
|
'An unknown slot, a slot that draws on no frame, a malformed list and a pair named twice are refused by name. Not with --slot/--hide',
|
|
1042
|
+
seam:
|
|
1043
|
+
`also read, for each <slot>:<slot> pair, where the two drawings stop meeting, and write ${SEAM_FILE} into each frame directory: ` +
|
|
1044
|
+
'per pair, the frames with a gap — background inside the hull of the band the two drawings share at the setup pose, carried to the frame by both — ' +
|
|
1045
|
+
'and the frames with a ghost — the band of one drawing showing where, from its partner\'s point of view, nothing was drawn at the setup pose — each with ' +
|
|
1046
|
+
`its cell count, area and world box. Cells are one texel of the coarser drawing, drawn at ${DRAWN_ALPHA} of 255 or more. One line per pair; a pair whose ` +
|
|
1047
|
+
'drawings share no cell at the setup pose is a SKIP. A reading, not a gate: the exit status never moves. An unknown slot, a slot with itself, ' +
|
|
1048
|
+
'a slot that draws on no frame, a malformed list and a pair named twice in either order are refused by name. Not with --slot/--hide',
|
|
1026
1049
|
'angle-range':
|
|
1027
1050
|
'also read, for each <bone>:<relative-to> pair, the range of the first bone\'s world direction against the second\'s over each set\'s ' +
|
|
1028
1051
|
'frames — its rotationX less the other\'s, followed from frame to frame so a sweep past ±180° reads as itself — and print one line per ' +
|
|
@@ -1234,6 +1257,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1234
1257
|
poser: 'core|spine',
|
|
1235
1258
|
environment: '<schedule.json>',
|
|
1236
1259
|
'pass-through': '<cloth>:<leg>[,<cloth>:<leg>…]',
|
|
1260
|
+
seam: '<slot>:<slot>[,<slot>:<slot>…]',
|
|
1237
1261
|
'angle-range': '<bone>:<relative-to>[,…]',
|
|
1238
1262
|
'viewport-from': '<frames.json|dir>',
|
|
1239
1263
|
reference: '<dir|skeleton.json>',
|
|
@@ -1365,6 +1389,15 @@ interface CommandDoc {
|
|
|
1365
1389
|
* `diff`) can be moved to another side in one place.
|
|
1366
1390
|
*/
|
|
1367
1391
|
spineFormat: boolean;
|
|
1392
|
+
/**
|
|
1393
|
+
* A command whose body is another package's bin (issue #1457): the dispatch
|
|
1394
|
+
* hands it every argument after the name untouched — nothing is parsed as a
|
|
1395
|
+
* flag, `--help` included, so its page is the delegate's own — and the
|
|
1396
|
+
* delegate's exit code is the command's. `flags` is empty for that reason:
|
|
1397
|
+
* rigc documents no flag of a program it does not own. A control that reads
|
|
1398
|
+
* a command's flag table off `--help` reads it off the commands without this.
|
|
1399
|
+
*/
|
|
1400
|
+
delegate?: { package: string; bin: string };
|
|
1368
1401
|
}
|
|
1369
1402
|
|
|
1370
1403
|
export const COMMANDS: CommandDoc[] = [
|
|
@@ -1686,10 +1719,11 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1686
1719
|
'rigc render … --slot <name[,name…]> | --hide <name[,name…]> (a subset of the slots, on the whole rig\'s grid)',
|
|
1687
1720
|
'rigc render … --environment <schedule.json> (the wind and gravity the physics answers, in time)',
|
|
1688
1721
|
'rigc render … --pass-through <cloth>:<leg>[,…] (the frames where a cloth drawn over a leg passes through it)',
|
|
1722
|
+
'rigc render … --seam <slot>:<slot>[,…] (the frames where two slots that meet open a gap or show a ghost)',
|
|
1689
1723
|
'rigc render … --angle-range <bone>:<relative-to>[,…] (how far a bone turns against another, per animation)',
|
|
1690
1724
|
'rigc render … --viewport <x,y,w,h> | --viewport-from <frames.json> (a box given, not fitted: two renders on one pixel grid)',
|
|
1691
1725
|
],
|
|
1692
|
-
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'pass-through', 'angle-range', 'environment', 'poser', 'out'],
|
|
1726
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'viewport', 'viewport-from', 'geometry', 'folds', 'pass-through', 'seam', 'angle-range', 'environment', 'poser', 'out'],
|
|
1693
1727
|
overrides: {
|
|
1694
1728
|
out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
|
|
1695
1729
|
fps: {
|
|
@@ -1891,6 +1925,16 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1891
1925
|
'"Install it into your agent").',
|
|
1892
1926
|
],
|
|
1893
1927
|
},
|
|
1928
|
+
{
|
|
1929
|
+
name: 'review',
|
|
1930
|
+
runtime: false,
|
|
1931
|
+
spineFormat: false,
|
|
1932
|
+
delegate: { package: '@rig-c/review', bin: 'rig-review' },
|
|
1933
|
+
usage: [
|
|
1934
|
+
"rigc review <dir> [...] (@rig-c/review's rig-review, installed beside: every argument handed over untouched, its exit code returned — `rigc review open <dir>` prints a loopback URL to give a person, `rigc review --help` is rig-review's own usage)",
|
|
1935
|
+
],
|
|
1936
|
+
flags: [],
|
|
1937
|
+
},
|
|
1894
1938
|
];
|
|
1895
1939
|
|
|
1896
1940
|
/** Every command's name, in the order the usage lists them — what the full entry dispatches over. */
|
|
@@ -2731,6 +2775,8 @@ export interface CommandArgs {
|
|
|
2731
2775
|
flags: Record<string, string>;
|
|
2732
2776
|
lists: Record<string, string[]>;
|
|
2733
2777
|
positional: string[];
|
|
2778
|
+
/** Every argument after the command's name, as given — what a delegating command (`CommandDoc.delegate`) hands over. */
|
|
2779
|
+
raw: readonly string[];
|
|
2734
2780
|
}
|
|
2735
2781
|
|
|
2736
2782
|
/** One command's body, as an entry registers it. */
|
|
@@ -2931,13 +2977,18 @@ export function runCli(entry: CliEntry, argv: readonly string[]): void {
|
|
|
2931
2977
|
if (elsewhere !== undefined) throw commandNeedsRuntime(elsewhere, known, rest);
|
|
2932
2978
|
throw new UsageError(`unknown command: ${command}`);
|
|
2933
2979
|
}
|
|
2980
|
+
// A delegating command reads nothing here: not a flag, not `--help` (issue #1457).
|
|
2981
|
+
if (docs.find((doc) => doc.name === command)?.delegate !== undefined) {
|
|
2982
|
+
entry.runs[command]({ flags: {}, lists: {}, positional: [], raw: rest });
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2934
2985
|
|
|
2935
2986
|
const { flags, lists, positional } = parseArgs([...rest], REPEATABLE_FLAGS[command]);
|
|
2936
2987
|
if (flags.help !== undefined) {
|
|
2937
2988
|
console.log(commandHelp(command, docs));
|
|
2938
2989
|
process.exit(0);
|
|
2939
2990
|
}
|
|
2940
|
-
entry.runs[command]({ flags, lists, positional });
|
|
2991
|
+
entry.runs[command]({ flags, lists, positional, raw: rest });
|
|
2941
2992
|
} catch (err) {
|
|
2942
2993
|
refuse(err, command, USAGE, entry);
|
|
2943
2994
|
}
|
|
@@ -3107,5 +3158,11 @@ function refuse(err: unknown, command: string | undefined, USAGE: string, entry:
|
|
|
3107
3158
|
console.error(err.message);
|
|
3108
3159
|
process.exit(1);
|
|
3109
3160
|
}
|
|
3161
|
+
// A delegate that could not be handed the run (issue #1457): one line naming the package and what to run, nothing else —
|
|
3162
|
+
// the usage under it would bury the one command that fixes it.
|
|
3163
|
+
if (err instanceof DelegateError) {
|
|
3164
|
+
console.error(`rigc ${command}: ${err.message}`);
|
|
3165
|
+
process.exit(err.status);
|
|
3166
|
+
}
|
|
3110
3167
|
throw err;
|
|
3111
3168
|
}
|