@rig-c/core 2.68.0 → 2.70.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 +39 -9
- package/bin/rigc.cjs +2 -2
- package/docs/AUTHORING.md +198 -1
- package/package.json +5 -2
- package/skills/rigc/SKILL.md +1 -1
- package/skills/rigc-face/SKILL.md +1 -1
- package/skills/rigc-ingest/SKILL.md +1 -1
- package/skills/rigc-motion/SKILL.md +1 -1
- package/skills/rigc-rigging/SKILL.md +1 -1
- package/src/atlas.ts +6 -1
- package/src/cli/core_commands.ts +68 -6
- package/src/cli/shared.ts +30 -2
- package/src/cli/write_whole.ts +1 -1
- package/src/render_read.ts +55 -0
- package/src/render_shared.ts +19 -0
- package/src/seam.ts +6 -2
- package/src/sheet.ts +920 -0
package/README.md
CHANGED
|
@@ -137,15 +137,35 @@ writes them into `--out`; `--atlas-in` builds against a pack somebody else made.
|
|
|
137
137
|
Both are opt-in and both are narrow — no trimming, no rotation, no scaling — and
|
|
138
138
|
[AUTHORING §0.1–§0.2](docs/AUTHORING.md) states the limits before you hit them.
|
|
139
139
|
|
|
140
|
-
rigc runs on [Bun](https://bun.sh).
|
|
141
|
-
Bun runs them, so there is no build step
|
|
142
|
-
repository it was cut from. What is
|
|
143
|
-
install into an empty directory, build,
|
|
144
|
-
|
|
145
|
-
`installs-matrix` jobs in
|
|
140
|
+
rigc runs on [Bun](https://bun.sh) (`engines.bun` declares `>=1.4.0`). The
|
|
141
|
+
package ships its TypeScript sources and Bun runs them, so there is no build step
|
|
142
|
+
and no `dist/` that can drift from the repository it was cut from. What is
|
|
143
|
+
measured is the install itself — pack, install into an empty directory, build,
|
|
144
|
+
render — on Linux, macOS and Windows runners on the Bun `setup-bun` resolves that
|
|
145
|
+
day (the `installs` and `installs-matrix` jobs in
|
|
146
146
|
[`ci.yml`](https://github.com/rig-c/core/blob/main/.github/workflows/ci.yml),
|
|
147
|
-
repository material, not in the npm package); a platform
|
|
148
|
-
|
|
147
|
+
repository material, not in the npm package); a platform whose leg is red there
|
|
148
|
+
is not one the package is known to run on. The declared `>=1.4.0` floor itself is
|
|
149
|
+
not pinned in CI — older `1.4.x` releases are untested until a report arrives.
|
|
150
|
+
|
|
151
|
+
From a source checkout, `bun run verify:nova -- --label "<issue + purpose>"
|
|
152
|
+
--owner "<who>" --session "<session id>" [--bun <version>]` sends the full
|
|
153
|
+
selftest to the Nova pool (`NOVA_RUN` names the runner, or `nova-run.sh` is on
|
|
154
|
+
PATH). `--bun 1.4.0` selects that exact stable release. Before upload, the hand
|
|
155
|
+
fetches Bun's official `SHASUMS256.txt` and checks that the Linux x64 baseline
|
|
156
|
+
archive is available; on the pool it downloads the pinned archive, verifies its
|
|
157
|
+
SHA-256, checks its executable's version, and puts that private Bun first on PATH
|
|
158
|
+
for corpus fetch and selftest. The remote command's first line is
|
|
159
|
+
`verify:nova: Bun 1.4.0`. The temporary runtime is removed on exit. Without
|
|
160
|
+
`--bun`, the pool's Bun and existing command are unchanged; dependency installation
|
|
161
|
+
still uses the pool runner's installation step. The selftest's exit is returned
|
|
162
|
+
unchanged: selecting a runtime does not promise a green run. Memory ceilings
|
|
163
|
+
remain those of that platform and Bun major.minor's tracked row; a missing
|
|
164
|
+
measurement remains a reported HOLE. Malformed, duplicate or missing
|
|
165
|
+
versions, unavailable releases/assets and unreadable checksums are refused by
|
|
166
|
+
name with exit 2 before upload. Use a published exact `X.Y.Z` (no `v` prefix,
|
|
167
|
+
alias, range or prerelease), or retry a failed network fetch. This is repository
|
|
168
|
+
verification tooling, not an installed `rigc` command or a change to CI's matrix.
|
|
149
169
|
|
|
150
170
|
**The npm package is `rig-c`; the command it installs is `rigc`.** npm
|
|
151
171
|
refuses the name `rigc` as too similar to packages that already exist, so the
|
|
@@ -186,6 +206,16 @@ of them: it reads whatever frames you point it at, so it runs from the installed
|
|
|
186
206
|
package on pictures of your own — which is what *Where to go next* below tells you
|
|
187
207
|
to do with it, and it is the one instrument here that can see a wrong animation.
|
|
188
208
|
|
|
209
|
+
Programmatic checked consumers can import `rig-c/render-shared`: the official
|
|
210
|
+
candidate chooser (`loadCandidate`, `throughPoser`), setup and animation samplers
|
|
211
|
+
(`sampleSetupPose`, `sampleAnimation`) and raster reads (`pageFor`, `projector`,
|
|
212
|
+
`rasterisePiece`, `renderFrame`), with their argument/result types and named
|
|
213
|
+
failures. It loads without the optional Spine runtime; a Spine input requires
|
|
214
|
+
explicitly loading `rig-c/render`, whose eager runtime dependency is unchanged.
|
|
215
|
+
The promised scope and planned first minor release are in
|
|
216
|
+
[RELEASING.md, The import surface](https://github.com/rig-c/core/blob/main/RELEASING.md#the-import-surface);
|
|
217
|
+
[AUTHORING §0.5](docs/AUTHORING.md#05-reading-a-build-from-code) shows the call chain.
|
|
218
|
+
|
|
189
219
|
### Install it into your agent
|
|
190
220
|
|
|
191
221
|
The guides under [Documentation](#documentation) also ship as
|
|
@@ -606,7 +636,7 @@ commands take it and what its default is.
|
|
|
606
636
|
| `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 |
|
|
607
637
|
| `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)) |
|
|
608
638
|
| `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 |
|
|
609
|
-
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings ([AUTHORING §8.8](docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — 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 |
|
|
639
|
+
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings ([AUTHORING §8.8](docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — 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. `--sheet [--sheet-format json-hash] [--page-edges pot\|free]` also packs each set's straight-alpha transparent frames into `sheet.png` + `sheet.json` (TexturePacker JSON Hash with a shared world-origin `anchor` and `2`-px clamped edge extrusion, verified through `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`) and `sheet-manifest.json` (`rigc-sheet/1`), leaving diagnostic `f####.png` and `contact.png` unchanged ([AUTHORING §8.10](docs/AUTHORING.md#810-sprite-sheets-from-a-render--render---sheet)) — a self-contained sprite object per animation set; when animations play and how a scene is composed remains the consumer's |
|
|
610
640
|
| `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` |
|
|
611
641
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
612
642
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
package/bin/rigc.cjs
CHANGED
|
@@ -56,8 +56,8 @@ const entry = present === null ? 'cli_core.ts' : 'cli.ts';
|
|
|
56
56
|
const args = process.argv.slice(2);
|
|
57
57
|
const result = spawnSync('bun', [path.join(root, entry), ...args], { stdio: 'inherit' });
|
|
58
58
|
|
|
59
|
-
// ⚠️ The exit and the signal are read before `error` (issue #1488): Bun 1.2.0
|
|
60
|
-
// the declared minimum, and what
|
|
59
|
+
// ⚠️ The exit and the signal are read before `error` (issue #1488): Bun 1.2.0
|
|
60
|
+
// (then the declared minimum, and what ran this file as `bun bin/rigc.cjs`) sets
|
|
61
61
|
// `error` on every child that exits non-zero, so reading it first answered the
|
|
62
62
|
// CLI's own refusal (exit 2) with "could not launch bun" and exit 1. `error` is
|
|
63
63
|
// a launch that failed only where the child left neither a status nor a signal.
|
package/docs/AUTHORING.md
CHANGED
|
@@ -75,6 +75,27 @@ which is also repository material and not in the published package,
|
|
|
75
75
|
*What a run may read* — the prompt that starts a run quotes them
|
|
76
76
|
outright, which is the copy that binds.
|
|
77
77
|
|
|
78
|
+
Repository maintainers can run the full selftest on Nova with
|
|
79
|
+
`bun run verify:nova -- --label "<issue + purpose>" --owner "<who>"
|
|
80
|
+
--session "<session id>" [--bun <version>]`. This is checkout tooling, not an
|
|
81
|
+
installed `rigc` command. `NOVA_RUN` names `nova-run.sh`, or it is found on PATH.
|
|
82
|
+
Without `--bun` the pool's runtime and command are unchanged. With `--bun 1.4.0`,
|
|
83
|
+
the remote command's first line reads `verify:nova: Bun 1.4.0`; both corpus fetch
|
|
84
|
+
and selftest use that privately downloaded runtime, removed on exit. The runner's
|
|
85
|
+
dependency installation step still uses the pool's Bun.
|
|
86
|
+
The selftest's exit is returned unchanged, including a failure under the chosen
|
|
87
|
+
runtime. Memory ceilings still use that platform and Bun major.minor's tracked
|
|
88
|
+
row; selecting a version neither raises a ceiling nor turns a missing measurement
|
|
89
|
+
(a reported HOLE) into a pass.
|
|
90
|
+
|
|
91
|
+
| `verify:nova` refusal (exit 2) | Repair |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `--bun needs a value`, `--bun was given more than once`, or `--bun "<value>": expected an exact stable version X.Y.Z` | State one published exact stable release, e.g. `--bun 1.4.0`; aliases, prefixes, ranges, prereleases and whitespace are refused before upload |
|
|
94
|
+
| `--bun "<version>": cannot fetch …` | The official release manifest or Linux x64 baseline archive is unavailable, or its fetch failed. Choose a published release with that asset or retry with network access to github.com. The pool runner has not been started |
|
|
95
|
+
| `--bun "<version>": Bun's SHASUMS256.txt must name exactly one SHA-256 …` | The official manifest has no unambiguous valid digest for that archive; choose a release whose manifest names it. Refused before upload |
|
|
96
|
+
| `--bun <version>: pinned runtime preparation failed; selftest was not started` | On the pool, check the download, SHA-256, archive extraction and Linux x64 host. The pinned archive must match Bun's official manifest; do not bypass the check |
|
|
97
|
+
| `--bun <version>: downloaded Bun reports <found>; selftest was not started` | The downloaded executable is not the requested runtime; retry a verified official release. No selftest ran under the wrong version |
|
|
98
|
+
|
|
78
99
|
## The vocabulary is Spine's
|
|
79
100
|
|
|
80
101
|
Wherever rigc has no better abstraction it uses **Spine 4.3's own concept, its own
|
|
@@ -245,7 +266,7 @@ What the flags mean:
|
|
|
245
266
|
| `--pack` | `build` only: arrange every part onto **shared** atlas page(s), written into `--out` as real PNGs, instead of one page per part. Lossless — nothing is resampled, trimmed or rotated. The default is one page per part — **§0.1** |
|
|
246
267
|
| `--page-size` | `build --pack` only: the largest page edge (default `2048`). A ceiling, not the size: page edges are powers of two and the one written is the smallest that holds the pack — **§0.1**. `repack` takes it with the same meaning — **§0.4** |
|
|
247
268
|
| `--padding` | `build --pack` only: the gutter each region reserves on every side (default `2`), filled by extending the region's own edge pixels outwards. `0` is not a legal-but-tight choice, it is bleed — **§0.1**. `repack` takes it with the same meaning — **§0.4** |
|
|
248
|
-
| `--page-edges` | `build --pack` only: `pot` (default) or `free`. `pot` keeps both page edges powers of two; `free` sizes the page to the parts — a smaller page, at the cost of region attachments sampling within one least significant bit of the loose build instead of exactly. Any other value is refused by name — **§0.1**. `repack` takes it with the same meaning — **§0.4** |
|
|
269
|
+
| `--page-edges` | `build --pack` only: `pot` (default) or `free`. `pot` keeps both page edges powers of two; `free` sizes the page to the parts — a smaller page, at the cost of region attachments sampling within one least significant bit of the loose build instead of exactly. Any other value is refused by name — **§0.1**. `repack` takes it with the same meaning — **§0.4**. `render --sheet` also takes `pot` (default) or `free` to size the packed sheet pages (`pot` power-of-two edges up to `2048` vs `free` tight edges up to `2048`), and is refused on `render` without `--sheet` — **§8.10** |
|
|
249
270
|
| `--pack-shape` | `build --pack` only: `rect` (default) or `polygon`. `rect` keeps every region's cell apart; `polygon` packs a region that only meshes draw by its emitted hull, so a neighbour may sit inside its rectangle where the hull is not, with the padding kept between footprints. A region attachment's footprint stays its rectangle. Any other value is refused by name, and so is the flag without `--pack` — **§0.1**. `repack` takes it with the same meaning, and always packs — **§0.4** |
|
|
250
271
|
| `--atlas-in` | `build` and `explain`: resolve every part against the **regions of a pre-packed `.atlas`** instead of against loose PNGs. Region geometry is read from the file and sizes are descaled by the page's `scale:`; `build` re-emits the atlas into `--out`, re-anchored, and `explain` writes nothing and poses through it — **§0.2**. On `explain` it is the flag that makes a **size-only** spec readable at all (`ingest --art none`), because posing resolves every attachment against an atlas; without it that pair is refused by name rather than thrown through (§5.1) |
|
|
251
272
|
| `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** — every `.png` in it is a part, in name order. For `chainfit` it is only where each attachment's image name **resolves**: the candidate decides what the parts are, so extra PNGs are unused and a missing name is refused by name (§12.3) |
|
|
@@ -263,6 +284,8 @@ What the flags mean:
|
|
|
263
284
|
| `--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** |
|
|
264
285
|
| `--overlaps` | `render` only: also read **which slot each frame drew over which**, and write an `overlaps.json` into each frame directory: per frame, every pair of slots where the slot drawn later covers pixels the earlier one drew (drawn at 128 of 255 or more, on the frames' own pixels), with the pixel count and the pixel box. One `OVERLAP` line per pair that covers anything — its frames as runs, how many of the set, and the most pixels and the frame of them — and one line counting the pairs that are layering the motion does not move. Each frame with a moving pair also gets an **outline plate**, `overlaps-f<NNNN>.png`: every slot in a moving pair as its contour in its own colour, dashed where a slot drawn later covers it, named at its top. The render reports what was drawn; who should be in front is the author's, and the draw-order key in the motion spec is the remedy. A reading, not a gate. Every other file is byte for byte what the render writes without it, and `frames.json` gains each set's `overlaps` record. Refused beside `--slot`/`--hide` — **§8.9** |
|
|
265
286
|
| `--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** |
|
|
287
|
+
| `--sheet` | `render` only: also pack each rendered animation set as a straight-alpha transparent sprite sheet (`sheet.png` + `sheet.json`, plus `sheet2.png` / `sheet2.json` on multi-page spill, and `sheet-manifest.json` (`rigc-sheet/1`)) inside its set directory, and record `sheet` on each set in `frames.json`. Diagnostic `f####.png` and `contact.png` stay byte for byte what the render writes without it — **§8.10** |
|
|
288
|
+
| `--sheet-format` | `render --sheet` only: `json-hash` (default, TexturePacker JSON Hash with `frames`, `animations` and `meta`, verified against `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`). Refused without `--sheet` or on an unknown value, exit 2, nothing written — **§8.10** |
|
|
266
289
|
| `--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 |
|
|
267
290
|
| `--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** |
|
|
268
291
|
| `--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 |
|
|
@@ -1617,6 +1640,68 @@ written`, followed by `rigc repack: nothing was written to <out>; the work direc
|
|
|
1617
1640
|
|
|
1618
1641
|
---
|
|
1619
1642
|
|
|
1643
|
+
### 0.5 Reading a build from code
|
|
1644
|
+
|
|
1645
|
+
The named `rig-c/render-shared` entry promises the official renderer reads for
|
|
1646
|
+
checked consumers (issue #1561). It is planned for the next minor, 2.69.0; wait
|
|
1647
|
+
for the measured release before using `>=2.69.0 <3.0.0`. It requires Bun and the
|
|
1648
|
+
candidate's real files, but imports no optional Spine runtime. The existing
|
|
1649
|
+
`rig-c/render` entry still eagerly imports that runtime.
|
|
1650
|
+
|
|
1651
|
+
```ts
|
|
1652
|
+
import { readFileSync } from 'node:fs';
|
|
1653
|
+
import { join } from 'node:path';
|
|
1654
|
+
import {
|
|
1655
|
+
loadCandidate, throughPoser, sampleSetupPose, sampleAnimation,
|
|
1656
|
+
pageFor, projector, rasterisePiece, renderFrame,
|
|
1657
|
+
type Viewport,
|
|
1658
|
+
} from 'rig-c/render-shared';
|
|
1659
|
+
|
|
1660
|
+
const dir = 'build';
|
|
1661
|
+
const skeleton = join(dir, 'skeleton.json');
|
|
1662
|
+
const atlas = join(dir, 'skeleton.atlas');
|
|
1663
|
+
const candidate = loadCandidate({
|
|
1664
|
+
skeletonText: readFileSync(skeleton, 'utf8'),
|
|
1665
|
+
atlasText: readFileSync(atlas, 'utf8'),
|
|
1666
|
+
atlasDir: dir, label: skeleton,
|
|
1667
|
+
}, { skeleton, atlas }, undefined);
|
|
1668
|
+
const read = throughPoser(candidate.choice, (poser, roster) => ({
|
|
1669
|
+
setup: sampleSetupPose(poser, { bones: true, geometry: true })[0],
|
|
1670
|
+
frames: sampleAnimation(poser, 'wave', 12),
|
|
1671
|
+
skins: roster.skins,
|
|
1672
|
+
}));
|
|
1673
|
+
// The consumer supplies the world box and pixel grid it intends to measure.
|
|
1674
|
+
const viewport: Viewport = {
|
|
1675
|
+
minX: 0, minY: 0, maxX: 256, maxY: 256,
|
|
1676
|
+
scale: 1, width: 256, height: 256,
|
|
1677
|
+
};
|
|
1678
|
+
const project = projector(viewport);
|
|
1679
|
+
for (const piece of read.value.setup.pieces) {
|
|
1680
|
+
rasterisePiece(pageFor(candidate.pages, piece), piece, project, viewport,
|
|
1681
|
+
(x, y, r, g, b, a) => { /* read each covered sample */ });
|
|
1682
|
+
}
|
|
1683
|
+
const image = renderFrame(read.value.setup, candidate.pages, viewport, [0, 0, 0, 0]);
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
The animation name and viewport above belong to the consumer; the loader does
|
|
1687
|
+
not invent them. The chooser checks the model document beside the skeleton,
|
|
1688
|
+
its `spine.sha256`, atlas placement, page files and bone/slot/animation rosters.
|
|
1689
|
+
A /3 document can be loaded with `atlasText: null` when its atlas is absent.
|
|
1690
|
+
`throughPoser` returns `poser` and `note` with its value. Keep its callback free
|
|
1691
|
+
of writes: an explicitly registered runtime fallback can rerun it.
|
|
1692
|
+
|
|
1693
|
+
The entry also exports the original `CandidateAtlasError` (missing required
|
|
1694
|
+
atlas), `CandidatePairError` (unloadable pair or missing page),
|
|
1695
|
+
`SpineRuntimeError` (input needs an unregistered or unusable Spine side),
|
|
1696
|
+
`PoserChoiceError` (a forced-core pose refusal), `SlotSubsetError` (invalid
|
|
1697
|
+
slot subset) and `CoreInputError` (core input/pose refusal). The chooser's
|
|
1698
|
+
mismatch reason remains in the refusal: no silent substitute is selected by
|
|
1699
|
+
this entry. Unknown animation and missing `pageFor` page are `Error`s naming
|
|
1700
|
+
the requested name and available names. A forced-core flag keeps the existing
|
|
1701
|
+
deferred choice check; it alone does not prove `candidate.choice.core` exists.
|
|
1702
|
+
To use a Spine export or `forced: 'spine'`, install the runtime and explicitly
|
|
1703
|
+
import `rig-c/render` first; importing `render-shared` never registers it.
|
|
1704
|
+
|
|
1620
1705
|
## 1. The two files
|
|
1621
1706
|
|
|
1622
1707
|
### 1.1 A complete minimal rig spec
|
|
@@ -9147,6 +9232,118 @@ is a defect is yours; when it is, the remedy is a `drawOrder` key in the motion
|
|
|
9147
9232
|
spec (or the slot's place in the rig's `slots`), and the next render says whether
|
|
9148
9233
|
the line moved.
|
|
9149
9234
|
|
|
9235
|
+
### 8.10 Sprite sheets from a render — `render --sheet`
|
|
9236
|
+
|
|
9237
|
+
```bash
|
|
9238
|
+
bun cli.ts render --candidate path/to/build --sheet [--sheet-format json-hash] [--page-edges pot|free] [--animation idle] [--fps 12] [--max 256]
|
|
9239
|
+
```
|
|
9240
|
+
|
|
9241
|
+
Packs each rendered animation set into a deterministic, transparent sprite sheet
|
|
9242
|
+
beside the diagnostic frames, with a TexturePacker JSON Hash descriptor per page
|
|
9243
|
+
and a rigc manifest stating the source identities, provenance, and coordinate
|
|
9244
|
+
contract.
|
|
9245
|
+
|
|
9246
|
+
**What it writes per set (`<out>/<set>/`):**
|
|
9247
|
+
|
|
9248
|
+
- `sheet.png` and `sheet.json` (plus `sheet2.png`, `sheet2.json`, … when a set
|
|
9249
|
+
spills across `2048 × 2048` pages) — RGBA PNG pages rendered over transparent
|
|
9250
|
+
`[0, 0, 0, 0]` (`alphaMode: "straight"`), packed through `packAtlas` with a
|
|
9251
|
+
fixed `2048` page ceiling and `2`-px clamped edge extrusion around every cell.
|
|
9252
|
+
`--page-edges pot` (the default) rounds each page edge up to a power of two;
|
|
9253
|
+
`--page-edges free` sizes each page tightly to the packed cells plus their
|
|
9254
|
+
`2`-px border.
|
|
9255
|
+
- `sheet-manifest.json` (`spec: "rigc-sheet/1"`) — the set's machine-readable
|
|
9256
|
+
provenance and coordinate contract: `format` (`"json-hash"`), `namespace`,
|
|
9257
|
+
`source` (`{ skeleton: { file, sha256 }, model: { file, sha256 } | null }`,
|
|
9258
|
+
recording the relative filenames and SHA-256 digests of the `skeleton.json`
|
|
9259
|
+
and `skeleton.model.json` the sheet was rendered from, with no paths),
|
|
9260
|
+
`animation` (`null` on a setup-pose set), optional `skin` / `slots` / `hidden` /
|
|
9261
|
+
`environment` / `pinned`, `fps`, `duration`, `sampled`, `endpointPolicy`
|
|
9262
|
+
(`"inclusive-grid"`), `alphaMode` (`"straight"`), `filterPolicy`
|
|
9263
|
+
(`{ recommended: "linear", paddingPx: 2, extrusion: "clamp-edge" }`),
|
|
9264
|
+
`pageEdges`, `pageSize` (`2048`), `padding` (`2`), `sourceSize` (`{ w, h }`),
|
|
9265
|
+
`viewport`, `pivot` (`{ world: [0, 0], canvasPx: { x, y }, anchor: { x, y } }`),
|
|
9266
|
+
`pages[]` (`{ index, image, table, width, height, occupancy, frames }`),
|
|
9267
|
+
and `frames[]` in chronological `0 .. N - 1` order (`{ index, key, time, page, image, table, empty, frame, spriteSourceSize, sourceSize, anchor }`).
|
|
9268
|
+
- `frames.json` gains `sheet` on each set entry (`manifest`, `format`,
|
|
9269
|
+
`pageEdges`, `pages`, `frames`, `emptyFrames`).
|
|
9270
|
+
|
|
9271
|
+
**What does not move.** The diagnostic `f####.png` frames and `contact.png` stay
|
|
9272
|
+
drawn over `BACKGROUND` (`[232, 232, 232, 255]`) and are byte for byte what `render`
|
|
9273
|
+
writes without `--sheet`; stripping `sheet` from each set in `frames.json`
|
|
9274
|
+
leaves the plain render's `frames.json` to the byte (`SH02`). Two runs and both
|
|
9275
|
+
posers (`--poser core` and `--poser spine`) write the same `sheet*.png`,
|
|
9276
|
+
`sheet*.json`, and `sheet-manifest.json` byte for byte (`SH03`), with no
|
|
9277
|
+
timestamps, host strings, or absolute paths.
|
|
9278
|
+
|
|
9279
|
+
**Trimming, empty frames, and `alpha > 0`.** Every frame is rendered at the
|
|
9280
|
+
set's full canvas (`sourceSize: { w: viewport.width, h: viewport.height }`) over
|
|
9281
|
+
straight-alpha `[0, 0, 0, 0]` and trimmed to the minimal bounding box of pixels
|
|
9282
|
+
with `alpha > 0` (`alpha >= 1`), preserving `alpha === 1` feather edges (`SH13`)
|
|
9283
|
+
and un-premultiplied RGB channels (`SH14`). When every pixel on a frame has
|
|
9284
|
+
`alpha === 0` (for instance, when `--hide` or `--slot` hides every visible slot
|
|
9285
|
+
on that step), the frame is emitted as a `1 × 1` transparent `[0, 0, 0, 0]`
|
|
9286
|
+
sentinel cell (`empty: true`, `trimmed: true`,
|
|
9287
|
+
`spriteSourceSize: { x: 0, y: 0, w: 1, h: 1 }`, `sourceSize` preserved) so the
|
|
9288
|
+
animation's frame count and timing stay intact and reconstructing the frame
|
|
9289
|
+
yields an all-zero full canvas (`SH12`).
|
|
9290
|
+
|
|
9291
|
+
**Shared world `(0, 0)` anchor and multi-page layout.**
|
|
9292
|
+
|
|
9293
|
+
- Each entry in `frames` carries `"anchor": { "x": ax, "y": ay }`, where
|
|
9294
|
+
`canvasPx = projector(viewport)(0, 0)` projects Spine world `(0, 0)` into
|
|
9295
|
+
untrimmed canvas pixel coordinates and `ax = canvasPx.x / viewport.width`,
|
|
9296
|
+
`ay = canvasPx.y / viewport.height`. When a pinned `--viewport` excludes world
|
|
9297
|
+
`(0, 0)`, `ax` and `ay` are preserved un-clamped outside `[0, 1]` so placing a
|
|
9298
|
+
sprite at `(worldOriginX, worldOriginY)` still lands every pixel at its exact
|
|
9299
|
+
world coordinate (`SH15`).
|
|
9300
|
+
- Each `sheet*.json` carries `frames`, `animations`, and `meta` (`app: "rig-c"`,
|
|
9301
|
+
`format: "RGBA8888"`, `image`, `size: { w, h }` as a single object and never an
|
|
9302
|
+
array, and `scale: "1"`). When a set spills across multiple pages, `packAtlas`
|
|
9303
|
+
assigns trimmed cells to pages by descending area rather than by frame time:
|
|
9304
|
+
each page's `frames` and `animations[<namespace>]` list only the frames that
|
|
9305
|
+
landed on that page (ordered chronologically within that page), `meta.related_multi_packs`
|
|
9306
|
+
names the sibling descriptor files (`SH16`), and the full set's playback order
|
|
9307
|
+
across all pages comes from `sheet-manifest.json`'s `frames[]` (`0 .. N - 1`).
|
|
9308
|
+
|
|
9309
|
+
**Collision-free frame keys and sampling policy.** Frame keys are
|
|
9310
|
+
`<rig>/<set>[/<variants>]/f<NNNN>` (for example, `squash/bounce/f0000` at the
|
|
9311
|
+
default `12` fps or `squash/bounce@24fps/skin=alt/f0000` at `24` fps under
|
|
9312
|
+
`--skin alt`), where `<rig>` is derived from `rig.archetype` in
|
|
9313
|
+
`skeleton.model.json` (the rig spec's `name`, never the build output folder) and
|
|
9314
|
+
`<set>` is `frameSetDir(animation, fps)`, so rendering one animation at different
|
|
9315
|
+
rates or copying a build into differently named folders never collides or moves
|
|
9316
|
+
a byte (`SH04`, `SH17`, `SH19`, `SH20`). Sampling follows `render`'s closed grid
|
|
9317
|
+
(`[0, duration]` at `Math.round(duration * fps) + 1` frames, recorded as
|
|
9318
|
+
`endpointPolicy: "inclusive-grid"`): whether a looping playback drops the
|
|
9319
|
+
closing endpoint frame or plays the full grid is a scene playback decision left
|
|
9320
|
+
to the consumer.
|
|
9321
|
+
|
|
9322
|
+
**Self-check, external reader verification, and refusals.** Before any file is
|
|
9323
|
+
written, `verifySheetPack`
|
|
9324
|
+
reconstructs every frame from the packed `sheet*.png` pages and `sheet*.json`
|
|
9325
|
+
descriptors onto a blank `sourceSize.w × sourceSize.h` canvas and checks that
|
|
9326
|
+
every reconstructed canvas is byte-identical (`0` differing RGBA bytes) to the
|
|
9327
|
+
untrimmed transparent render (`SH05`), that every cell's `2`-px border is
|
|
9328
|
+
clamped edge extrusion (`SH06`), that no two padded cells overlap (`SH07`–`SH11`),
|
|
9329
|
+
and that `source` records relative filenames and valid 64-hex SHA-256 digests (`SH21`).
|
|
9330
|
+
On a source checkout, `bun tools/sheet_readers.ts --sheet <dir>` loads a packed
|
|
9331
|
+
set (single-page or multi-page via `meta.related_multi_packs`) in headless
|
|
9332
|
+
Playwright Chromium through the pinned `pixi.js@8.22.0` (`PIXI.Assets.load` /
|
|
9333
|
+
`PIXI.Spritesheet`) and `phaser@3.90.0` (`Phaser.Textures.Parsers.JSONHash` /
|
|
9334
|
+
`addAtlasJSONHash`) loaders and checks cut rectangles, `sourceSize` and
|
|
9335
|
+
`spriteSourceSize`, `defaultAnchor` / `customPivot` alignment at
|
|
9336
|
+
`pivot.canvasPx` (`0` px drift; in Phaser 3.86+, set `roundPixels: false` on the
|
|
9337
|
+
game or camera when `pivot.canvasPx` is sub-pixel so `CanvasRenderer` does not
|
|
9338
|
+
round the sprite translation before adding `-displayOrigin + frame.{x,y}`),
|
|
9339
|
+
chronological playback order, multipage membership, and straight-alpha rendering
|
|
9340
|
+
over light (`[232, 232, 232, 255]`) and dark (`[28, 28, 32, 255]`) backgrounds;
|
|
9341
|
+
`SH22`–`SH25` hold its host-side refusals.
|
|
9342
|
+
`--sheet-format` with an unknown value or without `--sheet`, `--page-edges` on
|
|
9343
|
+
`render` without `--sheet` or with an unknown value, and any frame whose trimmed
|
|
9344
|
+
bounds plus its `2`-px border on each side exceed `2048` (`w + 4 > 2048` or
|
|
9345
|
+
`h + 4 > 2048`) are refused by name, exit 2, with nothing written (`SH17`).
|
|
9346
|
+
|
|
9150
9347
|
---
|
|
9151
9348
|
|
|
9152
9349
|
## 9. Checking against the frames — `rigc check`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.70.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": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"./font5x7": "./tools/font5x7.ts",
|
|
12
12
|
"./transform": "./src/transform.ts",
|
|
13
13
|
"./render": "./src/render.ts",
|
|
14
|
+
"./render-shared": "./src/render_read.ts",
|
|
14
15
|
"./png": "./src/png.ts",
|
|
15
16
|
"./compile": "./src/compile.ts",
|
|
16
17
|
"./rig": "./src/rig.ts",
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"./*": "./*.ts"
|
|
32
33
|
},
|
|
33
34
|
"engines": {
|
|
34
|
-
"bun": ">=1.
|
|
35
|
+
"bun": ">=1.4.0"
|
|
35
36
|
},
|
|
36
37
|
"license": "MIT",
|
|
37
38
|
"author": "firejune",
|
|
@@ -105,6 +106,8 @@
|
|
|
105
106
|
"@esotericsoftware/spine-core": "4.3.13",
|
|
106
107
|
"@types/bun": "^1.4.0",
|
|
107
108
|
"eslint": "^10.9.0",
|
|
109
|
+
"phaser": "3.90.0",
|
|
110
|
+
"pixi.js": "8.22.0",
|
|
108
111
|
"spine-html": "^0.4.1",
|
|
109
112
|
"typescript": "^5.9.3",
|
|
110
113
|
"typescript-eslint": "^8.67.0",
|
package/skills/rigc/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rigc
|
|
3
3
|
description: Author, build and validate Spine 4.3 skeleton data (skeleton.json plus its .atlas) from loose part PNGs with rigc, the rig compiler that verifies its own output with named assertions before writing it. Use for any request to make a Spine rig or Spine animation from PNG parts, or where the source is a Live2D, Unity or video model whose pictures you can render, to run or read rigc build, validate, render, preview, check or vote, or to write or fix a *.rig.json or *.motion.json spec; it says which shipped guide to open for the need at hand. Not for Live2D conversion, cutting an illustration into parts, or real-time face tracking.
|
|
4
4
|
license: MIT
|
|
5
|
-
compatibility: Requires Bun 1.
|
|
5
|
+
compatibility: Requires Bun 1.4 or later. The tool is the npm package rig-c (bunx rig-c, or bun add -d rig-c); the command it installs is rigc.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# rigc — a rig compiler for agents
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rigc-face
|
|
3
3
|
description: Author a face on plain Spine data with rigc — a blink, a gaze shift, a breathing portrait and a head turn a few degrees off axis, built from deform timelines and per-part parallax. Use when the request is a talking or living portrait, a standing character, an expression or a head turn, such as "rig this face", "make the portrait blink and look around" or "turn the head". Not for Live2D file conversion, cutting a face illustration into parts, or VTuber-style real-time face tracking.
|
|
4
4
|
license: MIT
|
|
5
|
-
compatibility: Requires Bun 1.
|
|
5
|
+
compatibility: Requires Bun 1.4 or later and the npm package rig-c.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Face — a turn, a gaze and a blink
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rigc-ingest
|
|
3
3
|
description: Work with a Spine skeleton.json somebody else authored — exported from the Spine editor or another tool — using rigc. Read and validate it, understand a complaint rigc raised about it, decompile it into rigc specs with `rigc ingest`, normalise, re-pivot or rename it, and extend it with an animation it does not have. Use when the input is an existing skeleton.json with its .atlas and page images rather than loose part PNGs. Not for Live2D file conversion or runtime tracking.
|
|
4
4
|
license: MIT
|
|
5
|
-
compatibility: Requires Bun 1.
|
|
5
|
+
compatibility: Requires Bun 1.4 or later and the npm package rig-c.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Ingest — a skeleton you did not author
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rigc-motion
|
|
3
3
|
description: Author a Spine animation with rigc from key poses — an idle, a loop, a move from one picture to another — with timing and spacing, ease in and out, anticipation, arcs, overlap, follow-through, squash and stretch, and candidate variants a person can choose between. Use when the request is a movement on an existing or planned rig, in the animator's words too: "animate this rig", "make it breathe", "go from pose A to pose B", "make it feel heavier", "the cape should follow through". Not for Live2D, separating an image into parts, or real-time tracking.
|
|
4
4
|
license: MIT
|
|
5
|
-
compatibility: Requires Bun 1.
|
|
5
|
+
compatibility: Requires Bun 1.4 or later and the npm package rig-c.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Motion — what goes between two poses
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rigc-rigging
|
|
3
3
|
description: Decide a Spine rig's hierarchy with rigc — how many bones, where each pivot sits, what hangs off what, offsets, chains and what a chain can reach, siblings versus chains, constraints as structure — and which of those decisions the reference frames can check. Use when the request is a skeleton from loose part PNGs, such as "rig these parts", "make a Spine skeleton" or "where do the joints go", before any motion is authored. Not for Live2D, cutting an illustration into parts, or VTuber-style tracking.
|
|
4
4
|
license: MIT
|
|
5
|
-
compatibility: Requires Bun 1.
|
|
5
|
+
compatibility: Requires Bun 1.4 or later and the npm package rig-c.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Rigging — the hierarchy itself
|
package/src/atlas.ts
CHANGED
|
@@ -775,6 +775,11 @@ export interface PackInput {
|
|
|
775
775
|
absPath: string;
|
|
776
776
|
width: number;
|
|
777
777
|
height: number;
|
|
778
|
+
/**
|
|
779
|
+
* Already-drawn pixels (`render --sheet`), used in place of `readPlate(absPath)`
|
|
780
|
+
* when given so an in-memory crop is not written out only to be read back in.
|
|
781
|
+
*/
|
|
782
|
+
plate?: Plate;
|
|
778
783
|
/**
|
|
779
784
|
* What the region's attachments draw (`packFootprints`), read only under
|
|
780
785
|
* `shape: 'polygon'`. Absent means the rectangle.
|
|
@@ -2535,7 +2540,7 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
2535
2540
|
const owners: Array<{ source: Plate; place: Placement; shape: CellShape }> = [];
|
|
2536
2541
|
for (const place of onPage) {
|
|
2537
2542
|
const input = byRegion.get(place.region)!;
|
|
2538
|
-
const source = readPlate(input.absPath);
|
|
2543
|
+
const source = input.plate ?? readPlate(input.absPath);
|
|
2539
2544
|
if (source.width !== input.width || source.height !== input.height) {
|
|
2540
2545
|
// The size in the atlas came from the PNG's IHDR (`readPngInfo`); this is
|
|
2541
2546
|
// the decoded image. They disagreeing means the file changed between the
|
package/src/cli/core_commands.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { outsideLine, outsideViewport, pinnedRecord, readViewportBox, viewportOf
|
|
|
40
40
|
import { PASS_THROUGH_FILE, passFrameLine, type PassPair, passSummaryLine, passThroughFileOf, passThroughOf, passThroughText, PassThroughError, readPassPairs } from '../passthrough.ts';
|
|
41
41
|
import type { Plate } from '../../tools/plate.ts';
|
|
42
42
|
import { OVERLAPS_FILE, OverlapError, overlapLine, overlapPlate, overlapsFileOf, overlapsOf, overlapsPlateFile, overlapsText, plateParticipants, staticLine } from '../overlaps.ts';
|
|
43
|
+
import { buildSheetSet, SHEET_MANIFEST_FILE, type SheetSetPack, SheetUsageError, sheetRigNameOf, sheetSidecarRecord, sheetSourceOf } from '../sheet.ts';
|
|
43
44
|
import { readSeamPairs, SEAM_FILE, seamFileOf, seamLine, SeamError, type SeamPair, seamsOf, seamText } from '../seam.ts';
|
|
44
45
|
import { environmentCollisions, environmentFrame, EnvironmentError, environmentResponse, readEnvironmentSchedule, type EnvironmentStep } from '../environment.ts';
|
|
45
46
|
import { estimatePose, poseLines, type PoseOptions } from '../pose.ts';
|
|
@@ -49,7 +50,7 @@ import { attachmentRegionJoins } from '../region_joins.ts';
|
|
|
49
50
|
import { cmdRepack } from './repack.ts';
|
|
50
51
|
import { writeWhole } from './write_whole.ts';
|
|
51
52
|
import { GUIDE_FILE, GUIDE_RULE_TABLES_ELSEWHERE, guideAnswer, refusalLine } from '../guide.ts';
|
|
52
|
-
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';
|
|
53
|
+
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, readPageEdges, readPoserFlag, readSheetFormat, readSkeletonText, readVersion, resolveCut, resolveDrawable, runCheck, SkillsInstallError, STAGELESS_FRAMING, UsageError, writeJson, documentStageBeside } from './shared.ts';
|
|
53
54
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
54
55
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
55
56
|
import { spawnSync } from 'node:child_process';
|
|
@@ -656,6 +657,18 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
656
657
|
const maxSide = readPositiveNumber(flags, 'max', 256, 16);
|
|
657
658
|
// A box given rather than fitted (issue #1414), read before a line is printed, so a malformed one is refused first.
|
|
658
659
|
const given = readGivenViewport(flags);
|
|
660
|
+
const wantsSheet = flags.sheet !== undefined;
|
|
661
|
+
if (flags['sheet-format'] !== undefined && !wantsSheet) {
|
|
662
|
+
throw new UsageError('--sheet-format takes --sheet: pass --sheet alongside --sheet-format <format>, or drop --sheet-format');
|
|
663
|
+
}
|
|
664
|
+
if (flags['page-edges'] !== undefined && !wantsSheet) {
|
|
665
|
+
throw new UsageError(
|
|
666
|
+
'--page-edges on render takes --sheet: without --sheet, render writes loose f####.png frames and contact.png, ' +
|
|
667
|
+
'which have no packed atlas pages. Pass --sheet or drop --page-edges',
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
const sheetFormat = wantsSheet ? readSheetFormat(flags) : undefined;
|
|
671
|
+
const sheetPageEdges = wantsSheet ? readPageEdges(flags) : undefined;
|
|
659
672
|
const outRoot = resolve(flags.out ?? 'render');
|
|
660
673
|
const geometry = flags.geometry !== undefined;
|
|
661
674
|
// The fold reading (issue #1383) is its own flag rather than a part of --geometry, so a caller that asked for
|
|
@@ -953,9 +966,45 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
953
966
|
}
|
|
954
967
|
}
|
|
955
968
|
}
|
|
956
|
-
|
|
969
|
+
const sheetPacks = new Map<string, SheetSetPack>();
|
|
970
|
+
if (wantsSheet) {
|
|
971
|
+
const modelDocText = modelTextBeside(skeletonPath, skeletonText) ?? null;
|
|
972
|
+
const rigName = sheetRigNameOf(skeletonPath, modelDocText, skeletonText);
|
|
973
|
+
const source = sheetSourceOf(skeletonPath, skeletonText, modelDocText);
|
|
974
|
+
const pinned = given === undefined ? undefined : pinnedRecord(given.by, outsideViewport(sampled, viewport));
|
|
975
|
+
for (const [name, frames] of sampled) {
|
|
976
|
+
const animation = name === SETUP_POSE_DIR && facts.animations.length === 0 ? null : name;
|
|
977
|
+
const setName = frameSetDir(name, fps);
|
|
978
|
+
try {
|
|
979
|
+
sheetPacks.set(
|
|
980
|
+
name,
|
|
981
|
+
buildSheetSet({
|
|
982
|
+
rigName,
|
|
983
|
+
setName,
|
|
984
|
+
animation,
|
|
985
|
+
fps,
|
|
986
|
+
maxSide,
|
|
987
|
+
viewport,
|
|
988
|
+
frames,
|
|
989
|
+
atlasPages: pages,
|
|
990
|
+
source,
|
|
991
|
+
format: sheetFormat,
|
|
992
|
+
pageEdges: sheetPageEdges,
|
|
993
|
+
skin,
|
|
994
|
+
subset,
|
|
995
|
+
environment: environment?.steps,
|
|
996
|
+
pinned,
|
|
997
|
+
}),
|
|
998
|
+
);
|
|
999
|
+
} catch (err) {
|
|
1000
|
+
if (err instanceof SheetUsageError) throw new UsageError(err.message);
|
|
1001
|
+
throw err;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings, overlapReadings, sheetPacks };
|
|
957
1006
|
});
|
|
958
|
-
const { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings, overlapReadings } = posed.value;
|
|
1007
|
+
const { viewport, fitted, sampled, geometryTexts, foldReadings, passReadings, seamReadings, angleReadings, overlapReadings, sheetPacks } = posed.value;
|
|
959
1008
|
console.log(` .. poser ${posed.note}`);
|
|
960
1009
|
if (environment !== undefined) {
|
|
961
1010
|
const response = environmentResponse(skeletonText);
|
|
@@ -1014,6 +1063,14 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1014
1063
|
writeFileSync(join(dir, OVERLAPS_FILE), overlapsText(overlapReading.file));
|
|
1015
1064
|
for (const [index, plate] of overlapReading.plates) plate.writePng(join(dir, overlapsPlateFile(index)));
|
|
1016
1065
|
}
|
|
1066
|
+
const sheetPack = sheetPacks.get(name);
|
|
1067
|
+
if (sheetPack !== undefined) {
|
|
1068
|
+
for (const page of sheetPack.pages) {
|
|
1069
|
+
page.plate.writePng(join(dir, page.image));
|
|
1070
|
+
writeFileSync(join(dir, page.table), page.jsonText);
|
|
1071
|
+
}
|
|
1072
|
+
writeFileSync(join(dir, SHEET_MANIFEST_FILE), sheetPack.manifestText);
|
|
1073
|
+
}
|
|
1017
1074
|
const duration = frames[frames.length - 1].time;
|
|
1018
1075
|
const physics = physicsDeclared.length === 0 ? undefined : physicsSteppedOf(frames, fps);
|
|
1019
1076
|
sets.push({
|
|
@@ -1026,6 +1083,7 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1026
1083
|
duration,
|
|
1027
1084
|
...(physics === undefined ? {} : { physics: { stepped: physics.stepped.length, fps } }),
|
|
1028
1085
|
...(overlapReading === undefined ? {} : { overlaps: overlapsRecord(overlapReading.file, overlapReading.plates.size) }),
|
|
1086
|
+
...(sheetPack === undefined ? {} : { sheet: sheetSidecarRecord(sheetPack) }),
|
|
1029
1087
|
});
|
|
1030
1088
|
const how = frames.length === 1 ? 'a single pose' : `${duration.toFixed(3)}s`;
|
|
1031
1089
|
const folding = foldReading?.meshes.filter((m) => m.worst !== null).length ?? 0;
|
|
@@ -1038,7 +1096,11 @@ export function cmdRender(flags: Record<string, string>): void {
|
|
|
1038
1096
|
overlapReading === undefined
|
|
1039
1097
|
? ''
|
|
1040
1098
|
: ` + ${OVERLAPS_FILE} (${overlapReading.file.pairs.filter((p) => p.varies).length} moving pair(s), ${overlapReading.plates.size} outline plate(s))`;
|
|
1041
|
-
const
|
|
1099
|
+
const sheetNote =
|
|
1100
|
+
sheetPack === undefined
|
|
1101
|
+
? ''
|
|
1102
|
+
: ` + ${sheetPack.pages.map((p) => `${p.image} + ${p.table}`).join(' + ')} + ${SHEET_MANIFEST_FILE} (${sheetPack.pages.length} page(s), ${sheetPack.pageEdges})`;
|
|
1103
|
+
const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${sheetNote}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}${foldNote}${passNote}${seamNote}${overlapNote}`;
|
|
1042
1104
|
console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${extras} -> ${dir}`);
|
|
1043
1105
|
if (physics !== undefined) console.log(` .. ${physicsLine(physics.stepped, physicsDeclared, fps, frames.length)}`);
|
|
1044
1106
|
for (const mesh of foldReading?.meshes ?? []) {
|
|
@@ -2313,7 +2375,7 @@ function resolveDelegate(name: string): string | null {
|
|
|
2313
2375
|
|
|
2314
2376
|
/**
|
|
2315
2377
|
* Whether the Bun running this hands an empty argument to a process it starts
|
|
2316
|
-
* (issue #1465). Bun 1.2.0
|
|
2378
|
+
* (issue #1465). Bun 1.2.0 (then the declared minimum) does not: `["a", "", "b"]`
|
|
2317
2379
|
* reaches the child as `["a", "b"]`, through `spawnSync` and `Bun.spawnSync`
|
|
2318
2380
|
* alike and whatever the child is; 1.4.2 hands it over. That is measured here
|
|
2319
2381
|
* rather than read off a version number, by starting this Bun twice with
|
|
@@ -2466,7 +2528,7 @@ function cmdDelegate(command: string, raw: readonly string[]): void {
|
|
|
2466
2528
|
// `rigc review -- <dir>` reached the bin as `<dir>` (measured, issue #1486). One given here is the one dropped, and
|
|
2467
2529
|
// the arguments after it arrive as they were.
|
|
2468
2530
|
const ran = spawnSync(process.execPath, [bin, '--', ...argv], { stdio: 'inherit' });
|
|
2469
|
-
// ⚠️ The exit and the signal are read before `error`: Bun 1.2.0
|
|
2531
|
+
// ⚠️ The exit and the signal are read before `error`: Bun 1.2.0 (then the declared minimum) sets `error` on every
|
|
2470
2532
|
// child that exits non-zero, so reading it first refused review's own exit 7 as "could not run". `error` is a
|
|
2471
2533
|
// launch that failed only where the child left neither a status nor a signal.
|
|
2472
2534
|
if (typeof ran.status === 'number') process.exit(ran.status);
|