@rig-c/core 2.40.1 → 2.42.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 +19 -0
- package/docs/AUTHORING.md +169 -2
- package/package.json +1 -1
- package/skills/rigc/SKILL.md +5 -0
- package/src/cli/core_commands.ts +116 -2
- package/src/cli/shared.ts +77 -2
- package/src/inspect.ts +1424 -0
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ installed.
|
|
|
41
41
|
| the same, and one texture instead of many | `rigc build --pack` | the parts arranged onto shared atlas pages, written beside the skeleton — losslessly, so the picture is the picture |
|
|
42
42
|
| a pack somebody already made | `rigc build --atlas-in` | the same skeleton, with every part resolved to a region of that atlas — or a named refusal, never a part that silently does not draw |
|
|
43
43
|
| a compiled rig | `rigc render` | every animation as PNG frames, plus one labelled contact sheet of the whole shot |
|
|
44
|
+
| a compiled rig and a question about its geometry | `rigc inspect` | one picture of its bones, axes, mesh edges and hulls, one bone's weights, the draw order, a world grid or a declared axis over the art — at the setup pose, one time or a sheet of several, zoomed to a bone or a slot, or beside a second build on one transform — and a legend that puts every mark back in world units |
|
|
44
45
|
| a compiled rig | `rigc preview` | one self-contained `.html` that plays it in Spine's own web player |
|
|
45
46
|
| two to four compiled rigs | `rigc vote` | one ballot page a human picks from, and the answer checked into a ledger |
|
|
46
47
|
| a picture of a key pose | `rigc pose` | where each loose part PNG sits in it, in spec coordinates — the movement between two poses is then yours to key ([docs/MOTION.md](docs/MOTION.md)) |
|
|
@@ -402,8 +403,26 @@ second package, no server.
|
|
|
402
403
|
```bash
|
|
403
404
|
rigc render --candidate spine # PNG frames + a contact sheet, in render/
|
|
404
405
|
rigc preview --candidate spine # one .html file that plays it: preview.html
|
|
406
|
+
rigc inspect --candidate spine --show art,bones,names --dim # inspect.png + inspect.json
|
|
405
407
|
```
|
|
406
408
|
|
|
409
|
+
**`inspect`** answers a question about geometry rather than appearance — *is this
|
|
410
|
+
bone where the art is*, *which way does it squash*, *where in rig units is this
|
|
411
|
+
point*, *is this axis held*. It draws the build's art, framed as `render` frames it,
|
|
412
|
+
and over it any of `bones` (origin to tip, a bone of length 0 marked as one),
|
|
413
|
+
`names` (one label per pixel), `axes` (each bone's world x and y), `mesh` and `hull`
|
|
414
|
+
(each mesh's edges and outline), `weights:<bone>` (what that bone moves, as a heat),
|
|
415
|
+
`slots` (the draw order at the pose), `grid` (in world units) and `declared`
|
|
416
|
+
(each `invariants.axes` line and the bones held to it), at the setup pose, at
|
|
417
|
+
`--animation <name> --at <s[,s…]>` — one picture, or a sheet — or zoomed with
|
|
418
|
+
`--zoom <bone|slot>[:pad]` or `--viewport x,y,w,h`; `--bone`/`--hide-bone` pick the
|
|
419
|
+
bones by glob, and `--compare <dir>` draws a second build beside the first on one
|
|
420
|
+
transform, to see what a change moved. Beside the PNG it writes a legend:
|
|
421
|
+
the world-to-pixel transform both ways, what each colour means, and every bone's
|
|
422
|
+
origin and tip in world units and in pixels, so a position read off the picture goes
|
|
423
|
+
straight back into the spec. [AUTHORING §13](docs/AUTHORING.md#13-asking-the-picture-a-question--rigc-inspect)
|
|
424
|
+
maps each question to its view.
|
|
425
|
+
|
|
407
426
|
**`render`** samples every animation at 12 fps and writes `render/<animation>/f0000.png…`
|
|
408
427
|
with a `contact.png` beside them — every frame of the shot as one labelled grid,
|
|
409
428
|
which is the picture to open first, because spacing is a comparison *across*
|
package/docs/AUTHORING.md
CHANGED
|
@@ -126,6 +126,12 @@ bun cli.ts preview --candidate path/to/spine # one .html that plays it in Spi
|
|
|
126
126
|
# ↳ a green build ends by naming this command for its own --out; repeat
|
|
127
127
|
# --candidate for a pane per candidate on one page
|
|
128
128
|
|
|
129
|
+
# …and when the question is about geometry — is this bone where the art is, which
|
|
130
|
+
# way does it squash — draw the bones over the art and read the legend beside it:
|
|
131
|
+
bun cli.ts inspect --candidate path/to/spine --show art,bones,names --dim --out look.png
|
|
132
|
+
# ↳ look.png and look.json: every bone's origin and tip in world units and in
|
|
133
|
+
# pixels, and the transform between them — §13
|
|
134
|
+
|
|
129
135
|
# …and where you have several green candidates and no instrument that separates
|
|
130
136
|
# them, ask a human — the one loop step this toolchain cannot run for you:
|
|
131
137
|
bun cli.ts vote --candidate path/to/spine-a --candidate path/to/spine-b # -> ballot.html
|
|
@@ -235,10 +241,11 @@ What the flags mean:
|
|
|
235
241
|
| `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
|
|
236
242
|
| `--profile` | `spine` = the 35 validity rules (**the default**) · `spine-html` = all 51, opt-in |
|
|
237
243
|
| `--report` | `build` and `repack`: also write the gate's report as a JSON document (`build-report/1`) to this file — every gate's `PASS`, `SKIP` and `FAIL` rows, its summary figures and stats line, which supplier judged it, and every `pack:` line's figures. Written when the command writes `--out` and when a gate is red, before the exit; a file already at the path is removed first, so a compile error or a refused repack leaves none. Never inside `--out`, refused by name. The printed lines are byte-identical with it and without it — **§5.3** |
|
|
238
|
-
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. A directory holding no `skeleton.json`, or a path with nothing at it, is refused `nothing at <path>`, exit 2, nothing written, by every command that takes a build — `validate <dir>` and a directory given to `diff` included (`RC27`). `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
244
|
+
| `--candidate` | `check`, `bench`, `render`, `inspect`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. A directory holding no `skeleton.json`, or a path with nothing at it, is refused `nothing at <path>`, exit 2, nothing written, by every command that takes a build — `validate <dir>` and a directory given to `diff` included (`RC27`). `--atlas <path>` names the atlas when it does not sit beside the skeleton. On `render` and `check` a rigc build whose `skeleton.model.json` is `rigc-compiled/3` or `/2` needs no atlas at all: with no `--atlas` and no atlas beside it, it is drawn from the document's `pages`, the same frames, geometry and figures as with it, and the `atlas` line says `— not there`. An atlas that is there is still read, and held to `pages`: one that differs (edited after the build) is drawn through spine-core, and the `poser` line names the first difference. A build drawn through an atlas that is not there — a `rigc-compiled/1` document, or a build the core refuses — is refused `nothing at <the atlas>: … is drawn through its atlas (<why>)`, exit 2, nothing written; a Spine export without its atlas is refused `nothing at <the atlas>` as it always was. A directory whose files are not one build — a `skeleton.json` or a `skeleton.atlas` from another build beside this one's `skeleton.model.json` — is drawn through spine-core where the runtime can load the pair, the `poser` line naming the two digests or the atlas's first difference; where it cannot, it is refused `<skeleton> does not load against <the atlas>: spine-core draws this pair (<the poser line's why>) and could not resolve it — "<the runtime's own words>". <dir> is not one build: …`, exit 2, nothing written (`RC18`) — another build's skeleton beside this build's atlas typically ends `"Region not found in atlas: <region> (attachment: <name>)"`. Rebuild, or put the build's own three files back beside each other. A Spine export beside an atlas it was not exported with is refused in the same words, ending `The skeleton and the atlas are not one pair`. An atlas copied from another build's directory usually stops earlier, at its pages — their paths are relative to the directory it was written in — and is refused naming the first one: `nothing at <the page>: <the atlas> names it as a page, and spine-core draws this pair through that atlas (<why>). … <dir> is not one build: …`, exit 2, nothing written (`RC18`); an export whose atlas names a page that is not there is refused the same way, ending `and the page has to be there`. A build moved or copied whole away from the directory it was built in stops at the same place, on either poser, and is refused naming the page and the file that names it: `nothing at <the page>: <the skeleton.model.json> names it as a page, and the core draws this build from it. A page path is relative to the build's directory, so a build moved or copied away from the directory it was built in names pages that are not here — build it again where it is, or build it with --copy-images, which writes its pages beside it` (under `--poser spine`, `<the atlas> names it as a page, and spine-core draws this pair through that atlas (--poser spine)` with the same ending), exit 2, nothing written (`RC21`); only where the core refused the files does a page refusal say the directory is not one build. `bench --bones` and `rigc bonedist` — on its `--candidate` and on its `--reference` — refuse a pair the runtime cannot load in the words above with `spine-core poses this pair (bonedist's candidate: …)` or `(bonedist's reference: …)`, exit 2, nothing written (`RC20`); they read no page image, so a build whose pages are elsewhere is measured as it is in place. A `skeleton.json` that is not JSON is refused `rigc: cannot read <the file>: <the parser's words> (line N, column N)`, exit 2, by `render`, `check`, `bench` and `bonedist` as by every other command that reads one (`RC22`). With the atlas gone, `check` on a `rigc-compiled/3` build reads its pages' `scale:` lines off the document, so its texture note and `check.json`'s `textureFrom.candidateScales` are what they are with the atlas; on a `/2` build its texture note says whether the atlas declares a `scale:` line is not read (that document does not state one), and `candidateScales` is `null` (`RC13`). **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
239
245
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
|
|
246
|
+
| `--show`, `--dim`, `--at`, `--zoom` | `inspect` only — what it draws, whether the art is faded, which times of `--animation` it poses and the bone or slot it frames to; §13 says each, with the refusals |
|
|
240
247
|
| `--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** |
|
|
241
|
-
| `--poser` | `render`, `check` and `explain`: `core` or `spine` — on `render` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`, `RC34`). On the entry that links none of spine-core — the installed `rigc` without the runtime beside it, `bun cli_core.ts` in a clone — that refusal ends on the route to the entry that does (``the entry that links it runs them — installed, the same `rigc` once @esotericsoftware/spine-core is installed beside the package; from a source checkout, `bun cli.ts` ``); a runtime that is installed and fails to load is refused without it, since installing it is not what that reader has to do. On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1), ending on the same route where the entry links none of it |
|
|
248
|
+
| `--poser` | `render`, `inspect`, `check` and `explain`: `core` or `spine` — on `render`, `inspect` and `check`, which implementation poses the frames (on `check`, the candidate's; the reference side is pixels either way). By default a **rigc build** (the `skeleton.model.json` `build` writes; its atlas, when one is named or present, the one beside it) is posed by rigc's own core and a **Spine export** by spine-core; the render prints a `poser` line saying which and why, and so does the check report (`check.json`'s `poser`, and the pictures' `frames.json` under `--out`; `bench --frames` prints the same line). A concave or an inverse clip is posed by the core, and so is a build rendered with no `--skin` whose default skin names a skin-required bone or constraint, or whose skins name no `default` one — posed as a fresh skeleton is, no skin's bones or constraints applied (the default skin's included) and every slot resolved through the default skin alone, so the second draws nothing and stops at `there is nothing to draw` (exit 2) under either poser (issue #1051). Where the input cannot carry the core, or the core refuses it, the render poses it through spine-core and the `poser` line says why, in one of these words: `no skeleton.model.json beside` (a Spine export) · `is not beside` (`--atlas` names an atlas outside the build's directory) · `does not describe` (a model document whose bones, slots or animations are not the skeleton's) · `is not the skeleton.json` (a `skeleton.json` whose SHA-256 is not the `spine.sha256` its model document records — edited or replaced after the build — both digests named) · `the polygon is not simple` (a clip polygon two of whose edges cross or touch, the slot named) · `depends on the runtime's previous pass` (a constraint moves a posed bone from a bone that a constraint writes into while that bone is inactive, all four named) · `--poser spine` (asked for). Any other refusal of the core's — an unreadable model document among them — reads `the core refused` and then the core's own words. `--poser core` on an input that cannot carry it is refused in the same words (exit 2, nothing written); `--poser spine` forces the runtime. A fallback whose pair the runtime cannot load — another build's `skeleton.json` or atlas beside this build's document, or an export beside an atlas it was not exported with — is not drawn: it is refused `… does not load against <the atlas>: spine-core draws this pair (<why>) and could not resolve it — "<the runtime's own words>"` — or, where the atlas names a page that is not there, `nothing at <the page>: …` — exit 2, nothing written, on `render`, `render --geometry`, `check` and `bench --frames` alike; under `--poser core` the same pair is that flag's refusal, in the words above (`RC18`; the `--candidate` row says what to change). The repository's selftest renders one probe per reason above and holds this list to the lines they print (`RC09`). Both posers draw the same frames, geometry and framing box to the bit on every corpus (issue #968), and `check` through either writes the same report, `check.json` and pictures but for the poser it names on every tree row. A rigc build the core poses reads nothing through spine-core on `render`, `render --geometry` and `check`, `--texture-from` included (and the check `bench --frames` folds in; `bench`'s own `validate`, like `build`'s, is the runtime's round trip by design): its bone tree, slot list and slot roster come off its model document; so do the order its animations and skins are listed in, its stage and its pages' `scale:` lines, on a `rigc-compiled/3` document (`editorOrder`, `stage`, `pages`) — a `/2` or `/1` document states none of those, so they come off its own `skeleton.json` and atlas, and the `poser` line then says `a rigc-compiled/2 document, which does not state the order its skins and animations are listed in or its stage, read from` and names both files; where each region sits and the page images it draws off the document's `pages` (a `rigc-compiled/1` document's off the atlas beside it, and the `poser` line then says `a rigc-compiled/1 document, which does not state where each region sits on its page: that is read from` and names the atlas); and a `--texture-from` atlas off rigc's own atlas reader. The runtime is loaded for a Spine export, `--poser spine` and a fallback the `poser` line names; a run that cannot load it there is refused naming the input and why it needs the runtime (exit 1; `RC11`, `RC13`, `RC34`). On the entry that links none of spine-core — the installed `rigc` without the runtime beside it, `bun cli_core.ts` in a clone — that refusal ends on the route to the entry that does (``the entry that links it runs them — installed, the same `rigc` once @esotericsoftware/spine-core is installed beside the package; from a source checkout, `bun cli.ts` ``); a runtime that is installed and fails to load is refused without it, since installing it is not what that reader has to do. On `explain` the flag chooses the `DEFORM` block's survey (§4.11.2): `core`, the default, reads the skeleton's structure — its deform timelines and their keys, curves and slots, the skin each is keyed under, the sliders — off the model document the compile writes and poses it through the core, and spine-core is not touched (`RC15`); `spine` parses and poses the Spine skeleton through spine-core. The two are the same survey to the byte on every corpus row (`DM12`). Where the core refuses the document, the survey falls back to spine-core and the block says so on a line of its own (`read and posed through spine-core: …`); `--poser core` there is refused instead (exit 2), and a run that cannot load the runtime where it needs it is refused naming the survey and why (exit 1), ending on the same route where the entry links none of it |
|
|
242
249
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
243
250
|
| `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
|
|
244
251
|
| `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
|
|
@@ -10048,3 +10055,163 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
|
|
|
10048
10055
|
is a given condition and once your spec states it there is nothing left to be
|
|
10049
10056
|
close to. Every threshold in the report is a reporting threshold. There is no pass
|
|
10050
10057
|
bar and `caveats` says so in the file.
|
|
10058
|
+
|
|
10059
|
+
## 13. Asking the picture a question — `rigc inspect`
|
|
10060
|
+
|
|
10061
|
+
The gate answers everything a number can say, and a geometric judgement is not
|
|
10062
|
+
one of those: *the bones around the head look odd* is a sentence about a picture.
|
|
10063
|
+
`inspect` draws that picture of a green build — the art, and over it the rig's own
|
|
10064
|
+
geometry at one moment — and writes a **legend** beside it that turns every mark
|
|
10065
|
+
back into world units. The picture is for reading; the legend is for acting on what
|
|
10066
|
+
was read, because a position taken off it goes straight into a spec.
|
|
10067
|
+
|
|
10068
|
+
```bash
|
|
10069
|
+
bun cli.ts inspect --candidate gallery/look/build --show art,bones,names --dim --max 640 --out look.png
|
|
10070
|
+
# rigc inspect
|
|
10071
|
+
# .. skeleton …/gallery/look/build/skeleton.json
|
|
10072
|
+
# .. atlas …/gallery/look/build/skeleton.atlas
|
|
10073
|
+
# .. poser rigc core — …/skeleton.model.json
|
|
10074
|
+
# .. view art,bones,names over dimmed art — the setup pose
|
|
10075
|
+
# .. box world x -35.2..835.2, y -35.2..915.2 at 0.6734 px per unit — 1 tile(s) of 586x640px (exact in the legend)
|
|
10076
|
+
# rigc: wrote …/look.png
|
|
10077
|
+
# rigc: wrote …/look.json
|
|
10078
|
+
```
|
|
10079
|
+
|
|
10080
|
+
It poses through the poser `render` would pose the candidate with, and the `poser`
|
|
10081
|
+
line says which — rigc's core for a build with its `skeleton.model.json`, spine-core
|
|
10082
|
+
for an export or a named fallback (the `--poser` row in §0). Without `--zoom` or
|
|
10083
|
+
`--viewport` the picture is framed exactly as `render` frames the build at the same
|
|
10084
|
+
`--max`, so with nothing drawn over it the art **is** `render`'s frame, pixel for
|
|
10085
|
+
pixel (`IN09`).
|
|
10086
|
+
|
|
10087
|
+
### 13.1 Which question, which view
|
|
10088
|
+
|
|
10089
|
+
| The question | The view | What to read |
|
|
10090
|
+
| --- | --- | --- |
|
|
10091
|
+
| Is this bone where the art is? | `--show art,bones,names --dim` | each bone as a line from its origin to its tip, the origin a filled square, the tip a hollow one; a bone of length 0 is a hollow 5x5 square around its origin, so a joint nobody gave a length is not mistaken for a point on another bone. Three fringe chains drawn as evenly spaced page verticals is the kind of thing this shows at a glance |
|
|
10092
|
+
| Which way does this bone squash? | `--show art,axes --zoom <bone>` | the bone's world **x** axis (red) and **y** axis (green), each 24 px long at a world scale of 1 — longer or shorter as the bone scales. A `scaley` key squashes along the green one; if that is the page's vertical on a face painted at an angle, the blink squashes along the page |
|
|
10093
|
+
| Where in rig units is this point? | `--show art,grid --zoom <bone or slot>` or `--viewport x,y,w,h` | grid lines at whole multiples of a step in **world units**, each labelled with its value, the lines through 0 darker. The legend's transform converts any pixel exactly; the grid is for reading by eye |
|
|
10094
|
+
| Is this axis declared, and held? | `--show art,declared,bones` | each `invariants.axes` line (§3.7) through its two bones' origins, drawn across the whole tile, and the bones it holds drawn in their own colour; the legend states each held bone's world x axis and how far off the line it lies, as `A51` measures it, at the pose drawn |
|
|
10095
|
+
| Did it break during the move? | any of the above `--animation <name> --at <s,s,…>` | one tile per time, left to right on one grid, each labelled with its time and frame |
|
|
10096
|
+
| 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 |
|
|
10097
|
+
| 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 |
|
|
10098
|
+
| 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 |
|
|
10099
|
+
| Did this change move anything? | `--compare <dir>` beside any view | the same view of a second build beside the first, at the same times, on **one** world box and one transform, each half labelled `A`/`B` with the poser that posed it. A mark on a different pixel in the two halves is at a different place in the two rigs |
|
|
10100
|
+
|
|
10101
|
+
### 13.2 Where it looks
|
|
10102
|
+
|
|
10103
|
+
- **The setup pose** by default.
|
|
10104
|
+
- **One time, or several** — `--animation <name> --at <s[,s…]>`. A time is a frame
|
|
10105
|
+
of the grid `render --fps <n>` draws the animation on (default 12): the pose at
|
|
10106
|
+
`--at 0.5` is `render`'s `f0006.png`, the same continuous trajectory stepped the
|
|
10107
|
+
same way, physics included. A time off that grid is refused naming the frames
|
|
10108
|
+
either side (`--at 0.3 is not on the 12 fps frame grid … the frames either side
|
|
10109
|
+
are 3 (0.2500s) and 4 (0.3333s)`); pass `--fps` for another grid. A time past the
|
|
10110
|
+
end is refused naming the animation's length. Several times are a sheet: tiles
|
|
10111
|
+
left to right, eight to a row, a one-pixel rule between them, all on one world box
|
|
10112
|
+
so a mark that moves between two tiles moved in the rig.
|
|
10113
|
+
- **The whole rig**, framed as `render` frames it; **a box** — `--viewport x,y,w,h`
|
|
10114
|
+
in world units, y up, `x,y` its bottom-left corner; or **one bone or slot** —
|
|
10115
|
+
`--zoom <name>[:<pad>]`, the bone's origin and tip or the slot's attachment
|
|
10116
|
+
vertices over every time drawn, padded by `<pad>` world units on every side
|
|
10117
|
+
(default a quarter of the box's longer side). A name that is both a bone and a slot
|
|
10118
|
+
is refused until it is written `bone:<name>` or `slot:<name>`; a bone of length 0
|
|
10119
|
+
with no pad is one point, and is refused asking for one.
|
|
10120
|
+
- **Two builds** — `--compare <dir | skeleton.json>`. The second build is loaded as
|
|
10121
|
+
the first is (its own poser, chosen by what it carries, on a `poser B:` line),
|
|
10122
|
+
posed at the same times, and drawn beside it: one time is two halves side by side,
|
|
10123
|
+
several are pairs, four to a row. The box is the one `--viewport` names, or the
|
|
10124
|
+
union of what `--zoom` names in **both** builds, or — with neither — the union of
|
|
10125
|
+
`render`'s framing of each, so a half is `render`'s frame of its build only where
|
|
10126
|
+
the two framings agree. A name either build lacks is refused naming that build.
|
|
10127
|
+
- `--max` is each tile's longer side in pixels (default 256), as on `render`.
|
|
10128
|
+
- `--dim` fades the art toward white (each channel keeps 0.4 of its distance from
|
|
10129
|
+
white) so what is drawn over it reads.
|
|
10130
|
+
|
|
10131
|
+
### 13.3 Labels, and which bones are drawn
|
|
10132
|
+
|
|
10133
|
+
- **One label per pixel.** Bones whose origins land on one pixel — a `_ctl` bone and
|
|
10134
|
+
its identity child, a chain's joint — are one label: the first by declaration
|
|
10135
|
+
order, then `+N` for the rest (`leg0_ctl +2`). The pixel is the unit because it is
|
|
10136
|
+
what overprints: two origins a third of a pixel apart print their names on each
|
|
10137
|
+
other as surely as two that coincide. The legend's `labels` lists every bone of
|
|
10138
|
+
each label, and `bones` keeps every bone.
|
|
10139
|
+
- **No label over another.** Labels are placed in declaration order (slot numbers
|
|
10140
|
+
first); one that would touch an earlier label moves down a text line at a time
|
|
10141
|
+
until it is clear, and a grey leader joins it to its origin. After 16 lines it
|
|
10142
|
+
stays where it began and the legend marks it `overlaps`.
|
|
10143
|
+
- **`--bone <glob[,glob…]>`** draws only the bones a pattern matches, and
|
|
10144
|
+
**`--hide-bone <glob[,glob…]>`** none of the ones it matches, applied after
|
|
10145
|
+
`--bone` — so `--bone 'bang_*' --hide-bone '*_ctl,*_px'` is the fringe chains
|
|
10146
|
+
without their control bones. A pattern is a glob — `*` any run of characters, `?`
|
|
10147
|
+
one, matched against the whole name — rather than a regular expression: bone names
|
|
10148
|
+
are identifiers, the patterns are comma-separated, and a glob needs no escaping in
|
|
10149
|
+
the shell. The filter is on `bones`, `axes` and `names`; every bone stays in the
|
|
10150
|
+
legend, and a left-out one says which flag left it out in `undrawn`. A pattern
|
|
10151
|
+
that matches no bone is refused naming the bones there are.
|
|
10152
|
+
|
|
10153
|
+
### 13.4 The legend
|
|
10154
|
+
|
|
10155
|
+
`<out>.json` beside `<out>.png`, `rigc-inspect/1`. What it states:
|
|
10156
|
+
|
|
10157
|
+
- `coordinates` — the two spaces and the transform, in both directions, as text:
|
|
10158
|
+
`px = x0 + (x - minX) * scale; py = y0 + (maxY - y) * scale` and
|
|
10159
|
+
`x = minX + (px - x0) / scale; y = maxY - (py - y0) / scale`. World is Spine's, y up;
|
|
10160
|
+
the picture is y down from its top-left corner, and pixel (i, j) covers
|
|
10161
|
+
`[i, i+1) x [j, j+1)` — a position's pixel is its floor.
|
|
10162
|
+
- `viewport` — the world box every tile is a picture of and its size in pixels;
|
|
10163
|
+
`size` — the whole picture's.
|
|
10164
|
+
- `tiles[]` — per tile: `animation`, `frame` and `time` (`frame / fps`), its `box` in
|
|
10165
|
+
the picture, its own `transform` (`minX`, `maxY`, `scale`, and `x0`, `y0`, its
|
|
10166
|
+
top-left pixel), and `bones[]` — every bone in declaration order with its
|
|
10167
|
+
`parent`, `length`, `zeroLength`, `origin` and `tip` each as `{ world, pixel }`,
|
|
10168
|
+
`undrawn` (why it is not drawn — its world matrix is 0 at this time, unposed by the
|
|
10169
|
+
skin or scaled to 0, or a `--bone`/`--hide-bone` pattern left it out — or `null`),
|
|
10170
|
+
and with `axes` shown, the two axis ends. With `grid`, the step and every line's
|
|
10171
|
+
value; with `declared`, each entry's line in degrees, its two points, and each held
|
|
10172
|
+
bone's angle and how far off the line it is. With `names`, `labels[]`: each label's
|
|
10173
|
+
`text`, the `bones` it stands for, its `anchor` pixel, the `box` it is drawn in,
|
|
10174
|
+
and whether it was `moved` or `overlaps`. With `mesh`, `hull` or `weights`,
|
|
10175
|
+
`meshes[]`: every region and mesh the pose shows, in draw order, with its `kind`,
|
|
10176
|
+
`vertexCount`, `triangleCount`, `hull` (a mesh's hull vertex count), its unique
|
|
10177
|
+
triangle `edges` as vertex pairs, every posed vertex in `world` and `pixel`, and
|
|
10178
|
+
with `weights` the bone's weight at each vertex. With `slots`, `slots[]`: each
|
|
10179
|
+
slot's `order`, `slot`, `attachment`, `bone`, its `anchor` and number's `box`.
|
|
10180
|
+
On a sheet or a compare, `label` — the tile's label text and box; with `--compare`,
|
|
10181
|
+
`build` — `A` or `B`.
|
|
10182
|
+
- `colours` — every colour the picture draws, as RGBA, with what it means.
|
|
10183
|
+
- `poser`, `show`, `dim`, `skin`, `animation`, `fps` and `framing` — what was drawn
|
|
10184
|
+
and how it was framed (`shot`, `zoom` with the bone or slot, the pad and whether it
|
|
10185
|
+
was stated, or `viewport`).
|
|
10186
|
+
- `weights` — with `weights:<bone>`: the bone, the ramp's stops as RGBA, the floor
|
|
10187
|
+
below which nothing is painted, and how a pixel's weight is read.
|
|
10188
|
+
- `boneFilter` — `--bone` and `--hide-bone` as given.
|
|
10189
|
+
- `builds` — with `--compare`: each build (`A`, `B`), the poser that posed it, and
|
|
10190
|
+
its `skeleton.json`'s sha256. `poser` is build `A`'s.
|
|
10191
|
+
|
|
10192
|
+
Every number is `JSON.stringify`'s, the double the pose produced, not a rounding of
|
|
10193
|
+
it. The same build and flags write the same bytes (`IN10`).
|
|
10194
|
+
|
|
10195
|
+
### 13.5 What it refuses
|
|
10196
|
+
|
|
10197
|
+
Each of these exits 2, writes nothing, and names what exists: a bone or slot the
|
|
10198
|
+
build does not have (both rosters listed), an animation or skin it does not declare,
|
|
10199
|
+
a `--show` word it does not draw, a flag it does not take, a time off the frame grid
|
|
10200
|
+
or past the end, `--at` without `--animation` and the reverse, `--zoom` beside
|
|
10201
|
+
`--viewport`, `--show declared` on a skeleton with no `skeleton.model.json`
|
|
10202
|
+
beside it whose `spine.sha256` is its own — a Spine export states no rig invariants —
|
|
10203
|
+
`weights` with no bone or with a bone the build lacks (the bones listed), a
|
|
10204
|
+
`--bone`/`--hide-bone` pattern that matches no bone (the bones listed), and, with
|
|
10205
|
+
`--compare`, any of these that holds of the second build, said as `the --compare
|
|
10206
|
+
build: …`.
|
|
10207
|
+
A rig that declares no `invariants.axes` is not refused: the picture draws nothing
|
|
10208
|
+
for `declared` and the run says so on a line of its own.
|
|
10209
|
+
|
|
10210
|
+
### 13.6 What it is not
|
|
10211
|
+
|
|
10212
|
+
- **Not a score.** Nothing in it is a pass bar; the picture is read, and what is read
|
|
10213
|
+
is acted on in the spec.
|
|
10214
|
+
- **Not a player.** It is one still, or a sheet of stills. A person watches
|
|
10215
|
+
`@rig-c/play` or `rigc preview`.
|
|
10216
|
+
- **Not a second poser.** Everything it draws is a pose `render` would have drawn at
|
|
10217
|
+
the same frame; where it disagrees with `render --geometry`, it is wrong (`IN04`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.42.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/skills/rigc/SKILL.md
CHANGED
|
@@ -74,6 +74,11 @@ row for each.
|
|
|
74
74
|
cover a feature the art shows — and answer each with `render --hide <slot>` (or
|
|
75
75
|
`--slot <slot,…>`), which draws the frame again without that part on the same
|
|
76
76
|
grid, so the two frames say which part a pixel is. AUTHORING §0 holds the three.
|
|
77
|
+
When the question is geometry — is this bone where the art is, which way does it
|
|
78
|
+
squash, where in rig units is this point — `rigc inspect --candidate <out> --show
|
|
79
|
+
art,bones,names --dim` draws the bones over the art and writes a legend beside the
|
|
80
|
+
picture that puts every mark back in world units; AUTHORING §13 maps each
|
|
81
|
+
question to its view.
|
|
77
82
|
5. `rigc check --candidate <out> --frames <dir>` when you have reference pictures
|
|
78
83
|
(`--out <dir>` writes the picture each of its numbers came from — open the worst).
|
|
79
84
|
Read its output from the top: the framing block says whether the figures under
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The bodies of the commands that reach nothing of spine-core — `explain`,
|
|
3
|
-
* `ingest`, `diff`, `check`, `render`, `pose`, `chainfit` and `skills` —
|
|
3
|
+
* `ingest`, `diff`, `check`, `render`, `inspect`, `pose`, `chainfit` and `skills` —
|
|
4
4
|
* moved here unchanged from `cli.ts` (issue #1052, step 4e of #380), with the
|
|
5
5
|
* helpers only they call. Both entries register them (`CORE_COMMAND_RUNS`):
|
|
6
6
|
* `cli.ts` beside the runtime's, `cli_core.ts` alone. And the body
|
|
@@ -30,12 +30,13 @@ import { ingest, INGEST_GUTTERS, type IngestFinding, IngestSpecRefused, type Ing
|
|
|
30
30
|
import { PARSER_DEFAULTS, parserReading } from '../keyorder.ts';
|
|
31
31
|
import { MODEL_DOCUMENT_FILE, modelDocument } from '../model.ts';
|
|
32
32
|
import { parseMotionSpec } from '../motion.ts';
|
|
33
|
+
import { type InspectCompare, type InspectFrame, InspectError, inspectCandidate, INSPECT_DEFAULT_FPS, legendText, readBonePatterns, readShow, readTimes, readZoom } from '../inspect.ts';
|
|
33
34
|
import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
34
35
|
import { BACKGROUND, contactSheet, type Frame, FRAMES_SIDECAR, FRAMES_SPEC, 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 } from '../render_shared.ts';
|
|
35
36
|
import { type CompileResult } from '../types.ts';
|
|
36
37
|
import { attachmentRegionJoins } from '../region_joins.ts';
|
|
37
38
|
import { cmdRepack } from './repack.ts';
|
|
38
|
-
import { ATLAS_ABSENT, COMMANDS, type CommandRun, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
39
|
+
import { ATLAS_ABSENT, COMMANDS, type CommandRun, modelTextBeside, resolveBuild, type BuildGate, runBuild, PACKAGE_ROOT, DEFAULT_CHAINFIT_OUT, DEFAULT_POSE_OUT, DEFAULT_SKILLS_DIR, ExplainError, meshBudget, meshDepthNote, meshFit, meshInfluenceNote, PAGE_GRID_UNLOCATED, readAnimationFlag, readJsonFile, readPoserFlag, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
39
40
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
40
41
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
41
42
|
|
|
@@ -648,6 +649,118 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
648
649
|
console.log(`rigc: wrote ${join(outRoot, FRAMES_SIDECAR)}`);
|
|
649
650
|
}
|
|
650
651
|
|
|
652
|
+
/** Where `inspect` writes when `--out` is not given, and the name it takes inside a directory `--out` names. */
|
|
653
|
+
export const DEFAULT_INSPECT_OUT = 'inspect.png';
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* inspect — one moment of a build drawn for an agent to read, and the legend
|
|
657
|
+
* that turns its pixels back into world units (issue #1357). The view is
|
|
658
|
+
* `src/inspect.ts`'s; this reads the flags, loads the candidate the way
|
|
659
|
+
* `render` loads it and writes the two files.
|
|
660
|
+
*
|
|
661
|
+
* ⚠️ A flag this command does not take is refused by name here rather than
|
|
662
|
+
* ignored: the parser accepts any `--word value`, and a view asked for with a
|
|
663
|
+
* flag that does nothing would be a picture of something other than what the
|
|
664
|
+
* caller asked about.
|
|
665
|
+
*/
|
|
666
|
+
export function cmdInspect(flags: Record<string, string>): void {
|
|
667
|
+
const doc = COMMANDS.find((c) => c.name === 'inspect');
|
|
668
|
+
const accepted = [...(doc?.flags ?? []), 'help'];
|
|
669
|
+
const stray = Object.keys(flags).filter((name) => !accepted.includes(name));
|
|
670
|
+
if (stray.length > 0) {
|
|
671
|
+
throw new InspectError(`inspect takes no --${stray.join(', --')}; it takes --${accepted.filter((f) => f !== 'help').join(', --')}`);
|
|
672
|
+
}
|
|
673
|
+
if (flags.candidate === undefined) {
|
|
674
|
+
throw new UsageError('inspect needs --candidate <dir | skeleton.json> — the directory `build --out` wrote');
|
|
675
|
+
}
|
|
676
|
+
if (flags.zoom !== undefined && flags.viewport !== undefined) {
|
|
677
|
+
throw new InspectError('--zoom and --viewport both choose the box the picture is of; give one of them');
|
|
678
|
+
}
|
|
679
|
+
const { show, weightsBone } = readShow(flags.show);
|
|
680
|
+
const include = readBonePatterns('bone', flags.bone);
|
|
681
|
+
const exclude = readBonePatterns('hide-bone', flags['hide-bone']);
|
|
682
|
+
const boneFilter = include === undefined && exclude === undefined ? undefined : { ...(include === undefined ? {} : { include }), ...(exclude === undefined ? {} : { exclude }) };
|
|
683
|
+
const times = readTimes(flags.at);
|
|
684
|
+
const fps = readPositiveNumber(flags, 'fps', INSPECT_DEFAULT_FPS, 1);
|
|
685
|
+
const maxSide = readPositiveNumber(flags, 'max', 256, 16);
|
|
686
|
+
let frame: InspectFrame = { kind: 'shot' };
|
|
687
|
+
if (flags.zoom !== undefined) frame = { kind: 'zoom', ...readZoom(flags.zoom) };
|
|
688
|
+
if (flags.viewport !== undefined) {
|
|
689
|
+
const parts = flags.viewport.split(',').map((s) => Number(s.trim()));
|
|
690
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) {
|
|
691
|
+
throw new UsageError('--viewport takes four numbers: <x>,<y>,<width>,<height> — the world box, y up');
|
|
692
|
+
}
|
|
693
|
+
if (parts[2] <= 0 || parts[3] <= 0) throw new UsageError('--viewport width and height must be positive');
|
|
694
|
+
frame = { kind: 'viewport', x: parts[0], y: parts[1], width: parts[2], height: parts[3] };
|
|
695
|
+
}
|
|
696
|
+
const target = resolve(flags.out ?? DEFAULT_INSPECT_OUT);
|
|
697
|
+
const out = existsSync(target) && statSync(target).isDirectory() ? join(target, DEFAULT_INSPECT_OUT) : target;
|
|
698
|
+
if (!out.endsWith('.png')) throw new InspectError(`--out ${flags.out}: the picture is a PNG, so --out names a .png file (its legend is written beside it as .json)`);
|
|
699
|
+
const legendPath = `${out.slice(0, -'.png'.length)}.json`;
|
|
700
|
+
|
|
701
|
+
const { skeletonPath, atlasPath, atlasText } = resolveDrawable(flags.candidate, flags.atlas);
|
|
702
|
+
const skeletonText = readSkeletonText(skeletonPath);
|
|
703
|
+
console.log('rigc inspect');
|
|
704
|
+
console.log(` .. skeleton ${skeletonPath}`);
|
|
705
|
+
console.log(` .. atlas ${atlasPath}${atlasText === null ? ` — not there (${ATLAS_ABSENT})` : ''}`);
|
|
706
|
+
// `render`'s loading, in `render`'s order (issue #1014): the poser is chosen before anything is read off the candidate.
|
|
707
|
+
const candidate = loadCandidate(
|
|
708
|
+
{ skeletonText, atlasText, atlasDir: dirname(atlasPath), label: skeletonPath },
|
|
709
|
+
{ skeleton: skeletonPath, atlas: atlasPath },
|
|
710
|
+
posersAsked(flags),
|
|
711
|
+
);
|
|
712
|
+
const animation = readAnimationFlag(flags, [...candidate.facts.animations]) ?? null;
|
|
713
|
+
readPoserFlag(flags);
|
|
714
|
+
refuseUnchosen(candidate.choice);
|
|
715
|
+
// `--compare`: the second build, loaded the way the first was — its own poser chosen by what it carries, and said.
|
|
716
|
+
let compare: InspectCompare | undefined;
|
|
717
|
+
if (flags.compare !== undefined) {
|
|
718
|
+
const other = resolveDrawable(flags.compare, undefined);
|
|
719
|
+
const otherText = readSkeletonText(other.skeletonPath);
|
|
720
|
+
console.log(` .. compare ${other.skeletonPath}`);
|
|
721
|
+
const otherCandidate = loadCandidate(
|
|
722
|
+
{ skeletonText: otherText, atlasText: other.atlasText, atlasDir: dirname(other.atlasPath), label: other.skeletonPath },
|
|
723
|
+
{ skeleton: other.skeletonPath, atlas: other.atlasPath },
|
|
724
|
+
posersAsked(flags),
|
|
725
|
+
);
|
|
726
|
+
refuseUnchosen(otherCandidate.choice);
|
|
727
|
+
compare = { candidate: otherCandidate, texts: { skeletonText: otherText, modelText: modelTextBeside(other.skeletonPath, otherText) } };
|
|
728
|
+
}
|
|
729
|
+
const result = inspectCandidate(
|
|
730
|
+
candidate,
|
|
731
|
+
{ skeletonText, modelText: modelTextBeside(skeletonPath, skeletonText) },
|
|
732
|
+
{
|
|
733
|
+
show,
|
|
734
|
+
...(weightsBone === undefined ? {} : { weightsBone }),
|
|
735
|
+
...(boneFilter === undefined ? {} : { boneFilter }),
|
|
736
|
+
dim: flags.dim !== undefined,
|
|
737
|
+
animation,
|
|
738
|
+
times,
|
|
739
|
+
fps,
|
|
740
|
+
skin: flags.skin,
|
|
741
|
+
frame,
|
|
742
|
+
maxSide,
|
|
743
|
+
},
|
|
744
|
+
{},
|
|
745
|
+
compare,
|
|
746
|
+
);
|
|
747
|
+
const { legend } = result;
|
|
748
|
+
console.log(` .. poser ${compare === undefined ? '' : 'A: '}${result.poserNote}`);
|
|
749
|
+
if (result.comparePoserNote !== undefined) console.log(` .. poser B: ${result.comparePoserNote}`);
|
|
750
|
+
const when = animation === null ? 'the setup pose' : `${animation} at ${legend.tiles.filter((t) => t.build !== 'B').map((t) => `${t.time.toFixed(4)}s (frame ${t.frame})`).join(', ')}, ${fps} fps grid`;
|
|
751
|
+
console.log(` .. view ${legend.show.join(',')}${legend.dim ? ' over dimmed art' : ''} — ${when}`);
|
|
752
|
+
const v = legend.viewport;
|
|
753
|
+
// Four places for reading; the legend carries every number exactly.
|
|
754
|
+
const f = (n: number): string => String(Number(n.toFixed(4)));
|
|
755
|
+
console.log(` .. box world x ${f(v.minX)}..${f(v.maxX)}, y ${f(v.minY)}..${f(v.maxY)} at ${f(v.scale)} px per unit — ${legend.tiles.length} tile(s) of ${v.width}x${v.height}px (exact in the legend)`);
|
|
756
|
+
for (const note of result.notes) console.log(` .. ${note}`);
|
|
757
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
758
|
+
result.plate.writePng(out);
|
|
759
|
+
writeFileSync(legendPath, legendText(legend));
|
|
760
|
+
console.log(`rigc: wrote ${out}`);
|
|
761
|
+
console.log(`rigc: wrote ${legendPath}`);
|
|
762
|
+
}
|
|
763
|
+
|
|
651
764
|
/** `--scale 0.5,2` / `--rotation -30,30` — a pair of numbers, low first. */
|
|
652
765
|
function readRange(flags: Record<string, string>, key: string): { low: number; high: number } | undefined {
|
|
653
766
|
const raw = flags[key];
|
|
@@ -1621,6 +1734,7 @@ export const CORE_COMMAND_RUNS: Readonly<Record<string, CommandRun>> = {
|
|
|
1621
1734
|
diff: ({ flags, lists, positional }) => cmdDiff(flags, lists, positional),
|
|
1622
1735
|
check: ({ flags }) => cmdCheck(flags),
|
|
1623
1736
|
render: ({ flags }) => cmdRender(flags),
|
|
1737
|
+
inspect: ({ flags }) => cmdInspect(flags),
|
|
1624
1738
|
pose: ({ flags }) => cmdPose(flags),
|
|
1625
1739
|
chainfit: ({ flags }) => cmdChainFit(flags),
|
|
1626
1740
|
skills: ({ flags, positional }) => cmdSkills(flags, positional),
|
package/src/cli/shared.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { BallotError } from '../ballot.ts';
|
|
|
56
56
|
import { ChainFitError } from '../chainfit.ts';
|
|
57
57
|
import { CheckError } from '../check.ts';
|
|
58
58
|
import { IngestError, type IngestStage } from '../ingest.ts';
|
|
59
|
+
import { InspectError, DEFAULT_SHOW, DIM_KEEP, INSPECT_DEFAULT_FPS, ON_GRID_FRAMES, SHOW_SPELLINGS } from '../inspect.ts';
|
|
59
60
|
import { NotAPngError } from '../png.ts';
|
|
60
61
|
import { PoseError } from '../pose.ts';
|
|
61
62
|
import { RepackError } from '../repack.ts';
|
|
@@ -125,7 +126,7 @@ export { PACKAGE_ROOT, readPackageMeta, readVersion };
|
|
|
125
126
|
* needs a value` (issue #328). `CLI10`/`CLI11` in `selftest.ts` now hold the two
|
|
126
127
|
* halves together by reading `--help` rather than by naming a flag.
|
|
127
128
|
*/
|
|
128
|
-
const BOOLEAN_FLAGS = new Set(['all-frames', 'all-bones', 'help', 'copy-images', 'again', 'pack', 'copy', 'geometry', 'accept-skeleton-differences', 'keep-pages']);
|
|
129
|
+
const BOOLEAN_FLAGS = new Set(['all-frames', 'all-bones', 'help', 'copy-images', 'again', 'pack', 'copy', 'geometry', 'accept-skeleton-differences', 'keep-pages', 'dim']);
|
|
129
130
|
|
|
130
131
|
/**
|
|
131
132
|
* The flags a command is allowed to spell more than once.
|
|
@@ -996,7 +997,7 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
996
997
|
"slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry — on the " +
|
|
997
998
|
'frames\' own grid and viewport. Not with --slot/--hide: the geometry is the whole pose whatever is drawn',
|
|
998
999
|
poser:
|
|
999
|
-
"`render` and `check`: which implementation poses the frames (on `check`, the candidate's) — `core` (rigc's own, reading the skeleton.model.json a " +
|
|
1000
|
+
"`render`, `inspect` and `check`: which implementation poses the frames (on `check`, the candidate's) — `core` (rigc's own, reading the skeleton.model.json a " +
|
|
1000
1001
|
'build writes beside the pair) or `spine` (spine-core). Default: `core` when that document and the atlas sit ' +
|
|
1001
1002
|
"beside the skeleton and the skeleton is the one the document records (spine.sha256), `spine` otherwise and wherever the core refuses the input by name; the `poser` " +
|
|
1002
1003
|
'line of the render or the check report says which and why. `--poser core` on an input that cannot carry it is refused by name. ' +
|
|
@@ -1061,6 +1062,36 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
1061
1062
|
'draw every slot but these, comma-separated — `--slot` the other way round, on the same grid, recorded and ' +
|
|
1062
1063
|
'refused the same way. Not with `--slot`: the two are one statement',
|
|
1063
1064
|
max: 'longest side of a rendered frame, in pixels (default 256)',
|
|
1065
|
+
show:
|
|
1066
|
+
`what \`inspect\` draws, comma-separated, from [${SHOW_SPELLINGS.join(', ')}] (default \`${DEFAULT_SHOW.join(',')}\`): art = the ` +
|
|
1067
|
+
"build's own frame; grid = lines at whole multiples of a step in world units, labelled; weights:<bone> = that bone's " +
|
|
1068
|
+
'weight over every region and mesh it moves, as a heat on a ramp the legend states; mesh = each mesh\'s triangle edges; ' +
|
|
1069
|
+
"hull = each mesh's hull outline; declared = each invariants.axes " +
|
|
1070
|
+
"line through its two bones' origins, its held bones drawn in their own colour; bones = each bone from origin to tip, " +
|
|
1071
|
+
'a bone of length 0 marked as one; axes = each bone\'s world x and y axes; slots = each slot\'s place in the posed ' +
|
|
1072
|
+
'draw order; names = the bones\' names, one label per pixel (bones sharing an origin pixel are "first +N"). Any other word ' +
|
|
1073
|
+
'is refused naming these',
|
|
1074
|
+
dim: `fade the art toward white (each channel keeps ${DIM_KEEP} of its distance from white), so what is drawn over it reads`,
|
|
1075
|
+
at:
|
|
1076
|
+
`the times of --animation to draw, in seconds, comma-separated — one is a picture, several a sheet of tiles on one ` +
|
|
1077
|
+
`grid in the order given. Each must be a frame of the --fps grid render draws the animation on (within ` +
|
|
1078
|
+
`${ON_GRID_FRAMES} of a frame), and is refused naming the frames either side otherwise; without --animation the ` +
|
|
1079
|
+
'picture is the setup pose and --at is refused',
|
|
1080
|
+
zoom:
|
|
1081
|
+
'frame the picture to one bone (origin to tip) or one slot (its attachment\'s vertices) over every time drawn, padded ' +
|
|
1082
|
+
'by <pad> world units on every side (default: a quarter of the box\'s longer side). A name that is both a bone and a ' +
|
|
1083
|
+
'slot is refused until written bone:<name> or slot:<name>; a name that is neither lists both rosters. Not with --viewport',
|
|
1084
|
+
bone:
|
|
1085
|
+
"draw only the bones these globs match (`*` any run of characters, `?` one, the whole name; comma-separated) — on `inspect`, " +
|
|
1086
|
+
"for bones, axes and names; every bone stays in the legend, a left-out one saying why in `undrawn`. A pattern that matches no " +
|
|
1087
|
+
'bone is refused naming the bones there are',
|
|
1088
|
+
'hide-bone':
|
|
1089
|
+
"draw none of the bones these globs match — `--bone` the other way round, applied after it, so `--bone 'bang_*' " +
|
|
1090
|
+
"--hide-bone '*_ctl'` is the fringe chains without their control bones",
|
|
1091
|
+
compare:
|
|
1092
|
+
'a second build to draw beside --candidate, the same view at the same times on one world box and one transform — the union ' +
|
|
1093
|
+
'of both framings unless --zoom or --viewport pins it — each half labelled with its build and the poser that posed it. A ' +
|
|
1094
|
+
'name, animation or skin the second build lacks is refused naming it',
|
|
1064
1095
|
record: 'a saved vote to check against its ballot and append to the ledger, instead of writing a ballot',
|
|
1065
1096
|
ballot: `the ballot the --record'd vote answers (default \`${DEFAULT_BALLOT}\`); its embedded manifest is what the vote is checked against`,
|
|
1066
1097
|
ledger: `the append-only JSONL the vote lands in (default \`${DEFAULT_LEDGER}\`)`,
|
|
@@ -1158,6 +1189,12 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
1158
1189
|
slot: '<name[,name…]>',
|
|
1159
1190
|
hide: '<name[,name…]>',
|
|
1160
1191
|
max: '<px>',
|
|
1192
|
+
show: '<view[,view…]>',
|
|
1193
|
+
at: '<s[,s…]>',
|
|
1194
|
+
zoom: '<name>[:<pad>]',
|
|
1195
|
+
bone: '<glob[,glob…]>',
|
|
1196
|
+
'hide-bone': '<glob[,glob…]>',
|
|
1197
|
+
compare: '<dir|skeleton.json>',
|
|
1161
1198
|
record: '<result.json>',
|
|
1162
1199
|
ballot: '<ballot.html>',
|
|
1163
1200
|
ledger: '<votes.jsonl>',
|
|
@@ -1536,6 +1573,38 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1536
1573
|
fps: { meaning: `frames per second to sample the animation at (default ${PROTOCOL_FPS})` },
|
|
1537
1574
|
},
|
|
1538
1575
|
},
|
|
1576
|
+
{
|
|
1577
|
+
name: 'inspect',
|
|
1578
|
+
runtime: false,
|
|
1579
|
+
spineFormat: false,
|
|
1580
|
+
usage: [
|
|
1581
|
+
'rigc inspect --candidate <dir | skeleton.json> [--show art,bones,…] [--dim] [--max 256] [--poser core|spine] [--out inspect.png] (the setup pose)',
|
|
1582
|
+
'rigc inspect … --animation <name> --at <s[,s…]> [--fps 12] [--skin <name>] (one time, or a sheet of several on one grid)',
|
|
1583
|
+
'rigc inspect … --zoom <bone|slot>[:<pad>] | --viewport <x,y,w,h> (a box, in world units)',
|
|
1584
|
+
"rigc inspect … --bone 'bang_*' --hide-bone '*_ctl' (which bones bones, axes and names draw)",
|
|
1585
|
+
'rigc inspect … --compare <dir | skeleton.json> (a second build beside the first, on one transform)',
|
|
1586
|
+
],
|
|
1587
|
+
flags: ['candidate', 'atlas', 'show', 'dim', 'animation', 'at', 'fps', 'skin', 'zoom', 'viewport', 'bone', 'hide-bone', 'compare', 'max', 'poser', 'out'],
|
|
1588
|
+
overrides: {
|
|
1589
|
+
animation: { meaning: 'the animation --at reads its times from (default: the setup pose, with no --at)' },
|
|
1590
|
+
fps: { meaning: `the frame grid --at is read on, which is the grid \`render --fps\` draws (default ${INSPECT_DEFAULT_FPS})` },
|
|
1591
|
+
viewport: { meaning: 'frame the picture to this world box, y up — x,y its bottom-left corner — instead of render\'s framing of the whole rig' },
|
|
1592
|
+
out: {
|
|
1593
|
+
value: '<file.png>',
|
|
1594
|
+
meaning:
|
|
1595
|
+
'the picture to write (default `inspect.png`; a directory means that name in it), and beside it the legend, the same ' +
|
|
1596
|
+
'path with .json for .png: the world-to-pixel transform both ways, what each colour means, and every bone\'s origin ' +
|
|
1597
|
+
'and tip in world units and in pixels',
|
|
1598
|
+
},
|
|
1599
|
+
},
|
|
1600
|
+
notes: [
|
|
1601
|
+
'inspect draws one moment of the build for an agent to read, and its legend says',
|
|
1602
|
+
'where every mark is in world units and in pixels. It poses through the poser',
|
|
1603
|
+
'render would pose the candidate with, and the poser line says which. Without',
|
|
1604
|
+
'--zoom or --viewport the picture is framed as render frames the build, so its',
|
|
1605
|
+
'art is render\'s frame at the same time and --max. Nothing in it is a score.',
|
|
1606
|
+
],
|
|
1607
|
+
},
|
|
1539
1608
|
{
|
|
1540
1609
|
name: 'preview',
|
|
1541
1610
|
runtime: { for: "its gate line is the round trip's, and it names the pages it embeds through spine-core's atlas reader" },
|
|
@@ -2704,6 +2773,12 @@ function refuse(err: unknown, command: string | undefined, USAGE: string, entry:
|
|
|
2704
2773
|
// Same kind as a PoseError, and printed the same way for the same reason: the
|
|
2705
2774
|
// messages name a path, a bone or an attachment, and reprinting the whole
|
|
2706
2775
|
// usage under them buries the one line that says what to change.
|
|
2776
|
+
// A view refused before anything was drawn (issue #1357): a name the build does not have, a time off render's
|
|
2777
|
+
// frame grid, a box with no size. The message lists what exists; the usage under it would bury that.
|
|
2778
|
+
if (err instanceof InspectError) {
|
|
2779
|
+
console.error(`rigc inspect: ${err.message}`);
|
|
2780
|
+
process.exit(2);
|
|
2781
|
+
}
|
|
2707
2782
|
if (err instanceof ChainFitError) {
|
|
2708
2783
|
console.error(`rigc chainfit: ${err.message}`);
|
|
2709
2784
|
process.exit(2);
|