@rig-c/core 2.69.0 → 2.71.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 +29 -9
- package/bin/rigc.cjs +2 -2
- package/docs/AUTHORING.md +279 -9
- 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/assertions/report.ts +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/compile.ts +48 -0
- package/src/core/animation.ts +56 -24
- package/src/core/index.ts +62 -1
- package/src/core/raw.ts +38 -12
- package/src/core/tracks.ts +23 -8
- package/src/core/vertices.ts +182 -44
- package/src/deformgen.ts +23 -7
- package/src/depth.ts +17 -4
- package/src/motion.ts +4 -4
- package/src/render_shared.ts +19 -0
- package/src/runtime/curves.ts +106 -0
- package/src/runtime/index.ts +80 -0
- package/src/runtime/model.ts +719 -0
- package/src/runtime/pose.ts +1014 -0
- package/src/seam.ts +6 -2
- package/src/sheet.ts +920 -0
- package/src/trackgen.ts +27 -4
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
|
|
@@ -616,7 +636,7 @@ commands take it and what its default is.
|
|
|
616
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 |
|
|
617
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)) |
|
|
618
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 |
|
|
619
|
-
| `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 |
|
|
620
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` |
|
|
621
641
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
622
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,14 +266,14 @@ 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) |
|
|
252
273
|
| `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
|
|
253
274
|
| `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
|
|
254
275
|
| `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
|
|
255
|
-
| `--profile` | `spine` = the 36 validity rules (**the default**) · `rig-play` = all 50, opt-in. `spine-html`, the profile's former name, is read as `rig-play` until the next
|
|
276
|
+
| `--profile` | `spine` = the 36 validity rules (**the default**) · `rig-play` = all 50, opt-in. `spine-html`, the profile's former name, is read as `rig-play` until the next major release and prints `rigc: --profile spine-html is deprecated and reads as --profile rig-play, the same rules; it is accepted until the next major release — pass --profile rig-play` on stderr |
|
|
256
277
|
| `--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** |
|
|
257
278
|
| `--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 |
|
|
258
279
|
| `--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 |
|
|
@@ -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 |
|
|
@@ -5196,7 +5219,7 @@ is that a **depth** is the decision while a **residual** is not.
|
|
|
5196
5219
|
|
|
5197
5220
|
| `kind` | Reads | `translatex` / `translatey` | `scalex` / `scaley` | Worked case |
|
|
5198
5221
|
| --- | --- | --- | --- | --- |
|
|
5199
|
-
| `yaw` | each member's setup `x` | `d = (x−about)·(cos t − 1) − (depth − carried)·sin t` — FACE §3 | `cos(α − t)/cos α`, `α = atan2(x−about, depth)` — FACE §5 | `gallery/portrait` |
|
|
5222
|
+
| `yaw` | each member's setup `x` | `d = (x−about)·(cos t − 1) − (depth − carried)·sin t` (or `−(depth − carried)·sin t` with `"narrowing": false`) — FACE §3 | `cos(α − t)/cos α`, `α = atan2(x−about, depth)` — FACE §5 | `gallery/portrait` |
|
|
5200
5223
|
| `pitch` | each member's setup `y` | the same expression with `y` for `x` — a nod | the same | `gallery/nod` |
|
|
5201
5224
|
|
|
5202
5225
|
⭐ **The `property` says which half of the turn a key is.** A turn does two
|
|
@@ -5225,6 +5248,15 @@ is an **auditing** decision before it is a rigging one: a residual is 1–6 unit
|
|
|
5225
5248
|
where a total is 30–40, and nobody can eyeball a wrong sign in the second.
|
|
5226
5249
|
`about` (default 0) is where the axis crosses the driving coordinate, in the
|
|
5227
5250
|
members' shared parent's space.
|
|
5251
|
+
`narrowing` (default `true`) says whether the displacement includes the
|
|
5252
|
+
foreshortening of the member's setup position `(x−about)·(cos t − 1)` (or `y`).
|
|
5253
|
+
With `"narrowing": false`, the displacement evaluates `d = −(depth − carried)·sin t`
|
|
5254
|
+
alone — for a rig whose plate proportions are held at their front-view width
|
|
5255
|
+
across the turn rather than narrowed toward the axis (§4.11.1). It belongs on the
|
|
5256
|
+
displacement projection (`translatex` / `translatey`) only; stating `narrowing` on
|
|
5257
|
+
a `scalex` or `scaley` track is refused by name (`CE_TRACK_NARROWING_PROJECTION`),
|
|
5258
|
+
because a foreshortening track *is* the narrowing — to turn without narrowing a
|
|
5259
|
+
plate's scale, omit that track.
|
|
5228
5260
|
|
|
5229
5261
|
Six things the construct is bounded by, and each one is a refusal:
|
|
5230
5262
|
|
|
@@ -6171,7 +6203,7 @@ example needed it:
|
|
|
6171
6203
|
|
|
6172
6204
|
| `kind` | Parameters | What it evaluates | Worked case |
|
|
6173
6205
|
| --- | --- | --- | --- |
|
|
6174
|
-
| `yaw` | `radius` **or** `depth`, `degrees`, `about` | `dx = (x−about)·(cos t − 1) − z·sin t
|
|
6206
|
+
| `yaw` | `radius` **or** `depth`, `degrees`, `about`, `narrowing` | `dx = (x−about)·(cos t − 1) − z·sin t` (or `dx = −z·sin t` with `"narrowing": false`), with `z = √(radius² − (x−about)²)` from a cylinder or `z` read per vertex off a depth map — the 2.5D turn (FACE §1) | `gallery/portrait` |
|
|
6175
6207
|
| `pitch` | the same | the same expression with `y` for `x` — a nod rather than a turn | `gallery/nod` |
|
|
6176
6208
|
| `affine` | `scale`, `about` | `dx = (sx−1)·(x−ax)`, `dy = (sy−1)·(y−ay)` — a scale about a fixed point | `gallery/squash` |
|
|
6177
6209
|
| `wave` | `amplitude`, `wavelength`, `phase`, `along`, `axis` | `d = amplitude · sin(2π·along/wavelength + phase)` | `gallery/nod` |
|
|
@@ -6181,8 +6213,28 @@ example needed it:
|
|
|
6181
6213
|
displaces; they cannot be the same coordinate, because reading and displacing one
|
|
6182
6214
|
axis is a stretch and `affine` states that. `power: 1` is an affine shear and
|
|
6183
6215
|
`power: 2` is a cantilever — flat at `from`, so a part held at one end bends
|
|
6184
|
-
instead of tilting. `about` defaults to 0 (or `[0, 0]`), `
|
|
6185
|
-
to 2; nothing else has a default.
|
|
6216
|
+
instead of tilting. `about` defaults to 0 (or `[0, 0]`), `narrowing` to `true`,
|
|
6217
|
+
`phase` to 0 and `power` to 2; nothing else has a default.
|
|
6218
|
+
|
|
6219
|
+
**`"narrowing": false` drops the setup-position foreshortening `(u−about)·(cos t − 1)` and keeps the depth shift `−z·sin t` alone.**
|
|
6220
|
+
A rigid 3D cylinder narrows across the screen as `cos t`, pulling every vertex
|
|
6221
|
+
inward by `(x−about)·(cos t − 1)` on top of its depth shift `−z·sin t`. On a
|
|
6222
|
+
cut-out rig whose facial plates are drawn to keep their front-view proportions
|
|
6223
|
+
across a turn — features translating by `−(depth − carried)·sin t` without
|
|
6224
|
+
foreshortening their setup `x` or `y` — that inward pull squashes the outer cheek
|
|
6225
|
+
while the features riding over it do not narrow to match. Setting `"narrowing": false`
|
|
6226
|
+
on `yaw` or `pitch` (both here and on `derive` in §4.5.1) states that choice
|
|
6227
|
+
in the model rather than forcing the author back to a raw `vertices` array.
|
|
6228
|
+
With the `cos t` term absent, a triangle's deformed twice-signed area is
|
|
6229
|
+
`2A(t) = 2A₀ − 2A_axis·sin t` instead of `2A₀·cos t − 2A_axis·sin t`, so it
|
|
6230
|
+
crosses zero at `sin t = A₀ / A_axis` (`asin(|A₀ / A_axis|)` when `|A₀ / A_axis| < 1`,
|
|
6231
|
+
and never on `(0, 90°)` when `|A₀ / A_axis| ≥ 1`) rather than `tan t = A₀ / A_axis`
|
|
6232
|
+
(`atan(|A₀ / A_axis|)`). When a deform key on a depth-mapped mesh declares
|
|
6233
|
+
`"narrowing": false`, `build` and `explain` recompute that axis's turn ceiling
|
|
6234
|
+
(§3.4.5) with `asin`, so the reported ceiling stays the exact angle `A39` fires at;
|
|
6235
|
+
mixing `"narrowing": false` and the default (`true`) on the same attachment and axis
|
|
6236
|
+
is refused (`CE_COMPILE_DEFORM_TURN_NARROWING_MIXED`), because one attachment and axis
|
|
6237
|
+
cannot have both the `asin` and `atan` ceilings at once.
|
|
6186
6238
|
|
|
6187
6239
|
Six things this construct is bounded by, and each one is a refusal rather than a
|
|
6188
6240
|
convention:
|
|
@@ -7433,6 +7485,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7433
7485
|
| <a id="CE_COMPILE_DEFORM_FROM_VERTEX_INVALID"></a>`CE_COMPILE_DEFORM_FROM_VERTEX_INVALID` | “<value> (t=<value>): fromVertex is <value>; it is a vertex index, so a whole number ≥ 0” | State fromVertex as a whole vertex index at least zero. |
|
|
7434
7486
|
| <a id="CE_COMPILE_DEFORM_FROM_VERTEX_OUTSIDE_ATTACHMENT"></a>`CE_COMPILE_DEFORM_FROM_VERTEX_OUTSIDE_ATTACHMENT` | “<value> (t=<value>): fromVertex <value> plus <value> vertex offset(s) runs to vertex <value><value>, and the attachment has <value>” | Keep fromVertex plus the covered vertex count within the attachment. |
|
|
7435
7487
|
| <a id="CE_COMPILE_DEFORM_WEIGHTED_FROM_VERTEX_AMBIGUOUS"></a>`CE_COMPILE_DEFORM_WEIGHTED_FROM_VERTEX_AMBIGUOUS` | “<value> (t=<value>): "fromVertex" counts VERTICES, and this attachment is weighted — its deform array holds one x, y pair per bone INFLUENCE, and vertex <value> has <value> of them. One offset per vertex is not a thing that array can hold: the world offset of a multi-bone vertex is the weighted sum of a per-bone offset in each bone's own bind space, so rigc will not guess one for you. Either key the control bone instead, or write the bind-space pairs yourself and start the run with "offset" (vertex <value> starts at deform index <value>).” | Key the control bone or author each bone influence's bind-space pair using offset. |
|
|
7488
|
+
| <a id="CE_COMPILE_DEFORM_TURN_NARROWING_MIXED"></a>`CE_COMPILE_DEFORM_TURN_NARROWING_MIXED` | “deform <turnTriple> keys "<turnAxis>" with narrowing=false in animation "<turnFalseAnim>" and with narrowing=true in animation "<turnTrueAnim>". An attachment's turn ceiling on one axis is either asin(\|A₀ / A_axis\|) (without narrowing) or atan(\|A₀ / A_axis\|) (with narrowing) — mixing both on one attachment and axis would report one ceiling where two apply. Use one "narrowing" setting across every "<turnAxis>" key on <turnTriple>.”<br>Also: “<turnDeformSkin>/<turnTrack.slot>/<turnTrack.attachment>”<br>Here the values named in the message include: “default”. | Use one `"narrowing"` setting (`true` or `false`) across every deform turn key on the same attachment and axis. |
|
|
7436
7489
|
| <a id="CE_COMPILE_TRACK_TARGETS_MULTIPLE"></a>`CE_COMPILE_TRACK_TARGETS_MULTIPLE` | “animation "<value>": a track names more than one target (<value>); the list is <value> and a track names exactly one”<br>Also: “, ”<br>Here the values named in the message include: “/”, “bone”, “slot”, “group”. | Name exactly one target field on a track. |
|
|
7437
7490
|
| <a id="CE_COMPILE_CONSTRAINT_TRACK_PROPERTY_UNKNOWN"></a>`CE_COMPILE_CONSTRAINT_TRACK_PROPERTY_UNKNOWN` | “animation "<value>": <value> has no timeline "<value>" (it has: <value>)”<br>Also: “, ”<br>“path constraint”<br>“physics constraint”<br>“{group "<value>" / <value> "<value>"}”<br>Here the values named in the message include: “slider”. | Use a timeline property supported by this constraint family. |
|
|
7438
7491
|
| <a id="CE_COMPILE_ANIMATION_GROUP_UNKNOWN"></a>`CE_COMPILE_ANIMATION_GROUP_UNKNOWN` | “animation "<value>": unknown group "<value>"” | Declare the motion group or correct its name. |
|
|
@@ -7600,8 +7653,8 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7600
7653
|
| <a id="CE_DEFORM_WAVELENGTH_ZERO"></a>`CE_DEFORM_WAVELENGTH_ZERO` | “<where>: transform wave has wavelength 0; one period cannot be zero long” | State a non-zero wavelength. |
|
|
7601
7654
|
| <a id="CE_DEFORM_BEND_POWER_WHOLE"></a>`CE_DEFORM_BEND_POWER_WHOLE` | “<where>: transform bend has power <power>; it is a whole number ≥ 1 (1 is an affine shear, 2 a cantilever that is flat at "from"). A fractional power is not evaluated because it has no value on the side of "from" the part does not reach.” | Use a whole bend power of at least 1. |
|
|
7602
7655
|
| <a id="CE_DEFORM_BEND_ENDS_EQUAL"></a>`CE_DEFORM_BEND_ENDS_EQUAL` | “<where>: transform bend has from <from> and to <to>; they are the two ends of the bend, so they differ” | Give bend from and to different values. |
|
|
7603
|
-
| <a id="CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY"></a>`CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY` | “<where>: transform <kind> states <stated>, and every one of this attachment's <count> vertices evaluates to an offset of 0 — the largest value the closed form reached at any of them is <band.toExponential(3)>, which quantises to 0 on the 1e-6 grid a model is evaluated on. So the key states a deformation and emits the identity, and nothing downstream can tell it apart from a key that meant the setup pose. <sampledTo>. A key that MEANS the identity states it in its own parameters (<identitySpelling>) or carries no run at all.”<br>“A scale about a fixed point moves nothing that SITS on it, so every vertex this attachment has lies at about=[<about[0]>, <about[1]>] on the axis the scale changes — the geometry has collapsed onto the point the key holds still”<br>“amount 0”<br>“amount=<amount> from=<from> to=<to> power=<power> along=<an> axis=<axis === 0 ? 'x' : 'y'>”<br>“amplitude 0”<br>“amplitude=<amplitude> wavelength=<wavelength> phase=<phase> along=<an> axis=<axis === 0 ? 'x' : 'y'>”<br>“degrees 0”<br>“scale [1, 1]”<br>“scale=[<scale[0]>, <scale[1]>]<a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`>”<br>“{Every vertex sits at <an>=<distinct[0]>, so one value of the sinusoid covers the whole attachment and this phase is where that one value crosses zero. A wave needs the coordinate it reads to VARY across the geometry; a part that displaces as a whole is a bone, not a deform / The closest two distinct <an> coordinates in this attachment are <round(gap)> apart, and a sinusoid has to be sampled to exist: a wavelength of at least 4x that (<round(4 * gap)>) to read as a wave at all and 8x (<round(8 * gap)>) to read as a curve, where this key states <round(wavelength / gap)>x. At 2x every sample lands on the same pair of phases, and at the zero crossings that pair is (0, 0). `gallery/nod`'s README carries that rule and the measured table behind it}”<br>“{Every vertex sits at <an>=<from>, which is "from" — the end the bend is anchored at, where the displacement is 0 by construction. The span the key names does not cross the part it is keyed on / The furthest any vertex reaches into the span is u=<round(reach)> of 1, and u^<power> of that is <(reach ** power).toExponential(3)> — so the part occupies only the flat end of the curve. Move "to" to where the geometry actually ends, or lower the power}”<br>“{The turn is <degrees>°, and a projection of it can only vanish where every vertex shares one <kind === 'yaw' ? 'x' : 'y'> AND one depth — check the depth map's sampled range in the mesh report, which is 0 wide when the sheet is flat where this part sits / The turn is <degrees>°, and a projection of it can only vanish where every vertex shares one <kind === 'yaw' ? 'x' : 'y'> — check that this attachment's setup geometry is the shape the radius says it is}”<br>“{depth=true degrees=<degrees><t.about === undefined ? '' : ` about=${about}`> / radius=<radius> degrees=<degrees><t.about === undefined ? '' : ` about=${about}`>}”<br>Also: “axis=[<value>, <value>] from=<value> to=<value> shape=<value>”<br>“mask=<value>”<br>“<value> envelope(<value>)”<br>Here the values named in the message include: “linear”, “smooth”. | Use parameters with a measured non-zero result, explicitly state identity parameters, or omit the run. |
|
|
7604
|
-
| <a id="CE_DEFORM_ENVELOPE_ZERO_WEIGHT"></a>`CE_DEFORM_ENVELOPE_ZERO_WEIGHT` | “<where>: transform <kind> states <stated> and moves <plain.filter((d, i) => i % 2 === 0 && (d !== 0 \|\| plain[i + 1] !== 0)).length> of <count> vertices, and envelope <weighed.stated> weighs every one of them 0 — so the key states a deformation and emits the identity. <weighed.reach>”<br>“The mask "<mask>" weighs <carried> of <count> vertices above 0.”<br>“axis=[<axis[0]>, <axis[1]>] from=<from> to=<to> shape=<shape>”<br>“mask=<mask>”<br>“{The attachment has no vertices. / Along [<axis[0]>, <axis[1]>] this attachment's vertices sit at <round(lo)> to <round(hi)>, and the ramp rises from <from> (weight 0) to <to> (weight 1) — move "from" and "to" onto the part.}”<br>Also: “amplitude=<value> wavelength=<value> phase=<value> along=<value> axis=<value>”<br>“{depth=true degrees=<value><value> / radius=<value> degrees=<value><value>}”<br>“{ / about=[<value>, <value>]}”<br>“amount=<value> from=<value> to=<value> power=<value> along=<value> axis=<value>”<br>“<value> envelope(<value>)”<br>“scale=[<value>, <value>]<value>”<br>Here the values named in the message include: “linear”, “smooth”. | Place the envelope ramp or mask where it weights moved vertices above 0, or remove the ineffective key. |
|
|
7656
|
+
| <a id="CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY"></a>`CE_DEFORM_TRANSFORM_QUANTISED_IDENTITY` | “<where>: transform <kind> states <stated>, and every one of this attachment's <count> vertices evaluates to an offset of 0 — the largest value the closed form reached at any of them is <band.toExponential(3)>, which quantises to 0 on the 1e-6 grid a model is evaluated on. So the key states a deformation and emits the identity, and nothing downstream can tell it apart from a key that meant the setup pose. <sampledTo>. A key that MEANS the identity states it in its own parameters (<identitySpelling>) or carries no run at all.”<br>“A scale about a fixed point moves nothing that SITS on it, so every vertex this attachment has lies at about=[<about[0]>, <about[1]>] on the axis the scale changes — the geometry has collapsed onto the point the key holds still”<br>“amount 0”<br>“amount=<amount> from=<from> to=<to> power=<power> along=<an> axis=<axis === 0 ? 'x' : 'y'>”<br>“amplitude 0”<br>“amplitude=<amplitude> wavelength=<wavelength> phase=<phase> along=<an> axis=<axis === 0 ? 'x' : 'y'>”<br>“degrees 0”<br>“scale [1, 1]”<br>“scale=[<scale[0]>, <scale[1]>]<a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`>”<br>“{Every vertex sits at <an>=<distinct[0]>, so one value of the sinusoid covers the whole attachment and this phase is where that one value crosses zero. A wave needs the coordinate it reads to VARY across the geometry; a part that displaces as a whole is a bone, not a deform / The closest two distinct <an> coordinates in this attachment are <round(gap)> apart, and a sinusoid has to be sampled to exist: a wavelength of at least 4x that (<round(4 * gap)>) to read as a wave at all and 8x (<round(8 * gap)>) to read as a curve, where this key states <round(wavelength / gap)>x. At 2x every sample lands on the same pair of phases, and at the zero crossings that pair is (0, 0). `gallery/nod`'s README carries that rule and the measured table behind it}”<br>“{Every vertex sits at <an>=<from>, which is "from" — the end the bend is anchored at, where the displacement is 0 by construction. The span the key names does not cross the part it is keyed on / The furthest any vertex reaches into the span is u=<round(reach)> of 1, and u^<power> of that is <(reach ** power).toExponential(3)> — so the part occupies only the flat end of the curve. Move "to" to where the geometry actually ends, or lower the power}”<br>“{The turn is <degrees>°, and a projection of it can only vanish where every vertex shares one <kind === 'yaw' ? 'x' : 'y'> AND one depth — check the depth map's sampled range in the mesh report, which is 0 wide when the sheet is flat where this part sits / The turn is <degrees>°, and a projection of it can only vanish where every vertex shares one <kind === 'yaw' ? 'x' : 'y'> — check that this attachment's setup geometry is the shape the radius says it is}”<br>“{depth=true degrees=<degrees><t.about === undefined ? '' : ` about=${about}`><narrowingNote> / radius=<radius> degrees=<degrees><t.about === undefined ? '' : ` about=${about}`><narrowingNote>}”<br>“{ / narrowing=false}”<br>Also: “axis=[<value>, <value>] from=<value> to=<value> shape=<value>”<br>“mask=<value>”<br>“<value> envelope(<value>)”<br>Here the values named in the message include: “linear”, “smooth”. | Use parameters with a measured non-zero result, explicitly state identity parameters, or omit the run. |
|
|
7657
|
+
| <a id="CE_DEFORM_ENVELOPE_ZERO_WEIGHT"></a>`CE_DEFORM_ENVELOPE_ZERO_WEIGHT` | “<where>: transform <kind> states <stated> and moves <plain.filter((d, i) => i % 2 === 0 && (d !== 0 \|\| plain[i + 1] !== 0)).length> of <count> vertices, and envelope <weighed.stated> weighs every one of them 0 — so the key states a deformation and emits the identity. <weighed.reach>”<br>“The mask "<mask>" weighs <carried> of <count> vertices above 0.”<br>“axis=[<axis[0]>, <axis[1]>] from=<from> to=<to> shape=<shape>”<br>“mask=<mask>”<br>“{The attachment has no vertices. / Along [<axis[0]>, <axis[1]>] this attachment's vertices sit at <round(lo)> to <round(hi)>, and the ramp rises from <from> (weight 0) to <to> (weight 1) — move "from" and "to" onto the part.}”<br>Also: “amplitude=<value> wavelength=<value> phase=<value> along=<value> axis=<value>”<br>“{depth=true degrees=<value><value><value> / radius=<value> degrees=<value><value><value>}”<br>“{ / narrowing=false}”<br>“{ / about=[<value>, <value>]}”<br>“amount=<value> from=<value> to=<value> power=<value> along=<value> axis=<value>”<br>“<value> envelope(<value>)”<br>“scale=[<value>, <value>]<value>”<br>Here the values named in the message include: “linear”, “smooth”. | Place the envelope ramp or mask where it weights moved vertices above 0, or remove the ineffective key. |
|
|
7605
7658
|
| <a id="CE_DEFORM_ENVELOPE_NO_EFFECT"></a>`CE_DEFORM_ENVELOPE_NO_EFFECT` | “<where>: transform <kind> states envelope <weighed.stated>, and it changes no emitted number: every vertex the model moves takes weight 1 (<weighed.full> of <count> vertices at 1, <weighed.zero> at 0), so the run is the un-enveloped one and the envelope is a field a reader would credit for an output it did not shape. <weighed.reach>”<br>“The mask "<mask>" weighs <carried> of <count> vertices above 0.”<br>“axis=[<axis[0]>, <axis[1]>] from=<from> to=<to> shape=<shape>”<br>“mask=<mask>”<br>“{The attachment has no vertices. / Along [<axis[0]>, <axis[1]>] this attachment's vertices sit at <round(lo)> to <round(hi)>, and the ramp rises from <from> (weight 0) to <to> (weight 1) — move "from" and "to" onto the part.}”<br>Here the values named in the message include: “linear”, “smooth”. | Remove the ineffective envelope, or place its transition over vertices the model moves. |
|
|
7606
7659
|
| <a id="CE_DEFORM_ENVELOPE_OBJECT"></a>`CE_DEFORM_ENVELOPE_OBJECT` | “<where>: transform field "envelope" is <JSON.stringify(envelope)>; it is an object — a ramp `{ "axis", "from", "to", "shape" }` or a mask `{ "mask" }`” | Write envelope as a ramp object or a mask object. |
|
|
7607
7660
|
| <a id="CE_DEFORM_ENVELOPE_MASK_RAMP_CONFLICT"></a>`CE_DEFORM_ENVELOPE_MASK_RAMP_CONFLICT` | “<where>: transform field "envelope" states "mask" <JSON.stringify(e.mask)> beside the ramp's <rampStated.map((f) => `"${f}"`).join(', ')>. They are two answers to one question — what weight each vertex takes — and a key carrying both leaves a reader unable to say which one the output came from. Drop one.”<br>Here the values named in the message include: “axis”, “shape”. | Choose envelope.mask or the ramp fields and remove the other form. |
|
|
@@ -7626,6 +7679,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7626
7679
|
| <a id="CE_TRACK_DERIVE_OBJECT"></a>`CE_TRACK_DERIVE_OBJECT` | “<where>: "derive" is <JSON.stringify(derive)>; it is an object with a "kind"” | Write derive as an object with a kind. |
|
|
7627
7680
|
| <a id="CE_TRACK_DERIVE_KIND"></a>`CE_TRACK_DERIVE_KIND` | “<where>: "derive" has kind <JSON.stringify(kind)>; this spec evaluates <TRACK_DERIVE_KINDS.join(', ')>. A model that is none of those is still authorable as a per-member "v" map, or as one track per member.”<br>Here the values named in the message include: “yaw”, “pitch”. | Use a supported derive kind, a per-member v map, or one authored track per member. |
|
|
7628
7681
|
| <a id="CE_TRACK_CARRIED_PROJECTION"></a>`CE_TRACK_CARRIED_PROJECTION` | “<where>: derive <kind> states carried=<JSON.stringify(turn.carried)> on a "<property>" track, and the foreshortening reads no depth difference — it is cos(α − t)/cos α, where α is the member's own angle off the axis. "carried" belongs on the "<projections.shift>" track, whose shared shift is the thing it names.” | Put carried on the displacement projection rather than the foreshortening track. |
|
|
7682
|
+
| <a id="CE_TRACK_NARROWING_PROJECTION"></a>`CE_TRACK_NARROWING_PROJECTION` | “<where>: derive <kind> states narrowing=<JSON.stringify(turn.narrowing)> on a "<property>" track, and a "<property>" track IS the foreshortening — its whole output is cos(α − t)/cos α. "narrowing" belongs on the "<projections.shift>" track, where it drops (<projections.coordinate>−about)·(cos t − 1) from the displacement; to turn without narrowing the plate's scale, omit the "<property>" track.” | Put narrowing on the displacement projection rather than the foreshortening track, or omit the foreshortening track. |
|
|
7629
7683
|
| <a id="CE_TRACK_FORESHORTEN_DEPTH"></a>`CE_TRACK_FORESHORTEN_DEPTH` | “<where>: derive <kind> projects onto "<property>" and member "<member.name>" states depth <depth>. Foreshortening is cos(α − t)/cos α with α = atan2(coordinate, depth), which needs the part to be IN FRONT of the axis; at or behind it there is no front surface to narrow. A part behind the axis still takes the "<projections.shift>" projection, where a negative depth is exactly what swings it the other way.” | State positive depth for the foreshortening projection; negative depth belongs to displacement. |
|
|
7630
7684
|
| <a id="CE_TRACK_FORESHORTEN_EDGE_ON"></a>`CE_TRACK_FORESHORTEN_EDGE_ON` | “<where>: derive <kind> turns member "<member.name>" (<projections.coordinate>=<u>, depth=<depth>) past its own edge: α = <round(alpha)> rad and the turn is <round(rad)> rad, so cos(α − t) is <round(Math.cos(alpha - rad))> and the scale would be <round(scale)>. A scale at or below 0 mirrors the drawing rather than narrowing it. The part has turned edge-on — the turn is past what this construction carries, and FACE §8 is the page that picks a construction from the angle.” | Keep the turn within this construction's positive scale range or choose the construction described in FACE §8. |
|
|
7631
7685
|
| <a id="CE_TRACK_PROPERTY_PROJECTION"></a>`CE_TRACK_PROPERTY_PROJECTION` | “<where>: derive <kind> has no projection onto "<property>". It projects onto "<p.shift>" (the displacement, FACE §3) and "<p.foreshorten>" (the narrowing, FACE §5), and onto nothing else — a turn about one axis moves nothing along it, so a paired "translate"/"scale" would need a second channel this model does not state. State the axis, or write the values as a per-member "v" map.” | Use the derive kind's printed displacement or foreshortening property, or a per-member v map. |
|
|
@@ -7695,7 +7749,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7695
7749
|
|
|
7696
7750
|
| Code | Fixed message text and alternatives | What to change |
|
|
7697
7751
|
| --- | --- | --- |
|
|
7698
|
-
| <a id="CE_KEYS_UNKNOWN_FIELD"></a>`CE_KEYS_UNKNOWN_FIELD` | “<where>: <what> has <strays.length === 1 ? 'a key' : `${strays.length} keys`> this compiler does not read: <named.join(', ')>. Nothing reads such a key, so it would be dropped from the emitted skeleton in silence — fix the spelling or remove it. {This shape has no fields at all. / Known here: <[...known].sort().join(', ')>.}”<br>“{"<key>" / "<key>" (did you mean <near.map((n) => `"${n}"`).join(', ')>?)}”<br>Also: “skin "<value>" slot "<value>" attachment "<value>"”<br>“<value> generator.bones[<value>]”<br>“{constraint "<value>" / constraints[<value>]}”<br>“<value>.transform.envelope”<br>“<value> weights[<value>][<value>]”<br>“invariants.deformMayFold[<value>]”<br>“animations."<value>"”<br>“<value>.offsets[<value>]”<br>“{stop "<value>" / stops[<value>]}”<br>“<value> properties."<value>".to."<value>"”<br>“this rig spec”<br>“<value>.derive”<br>“setup."<value>"”<br>“physics."<value>"”<br>“<value>'s "lower"”<br>“<value> "sequence"”<br>“<value> generator.bias”<br>“<value> generator.depth”<br>“<value>.events[<value>]”<br>“<value>.tracks[<value>]”<br>“invariants.axes[<value>]”<br>“<value> generator.falloff”<br>“<value>.drawOrder[<value>]”<br>“<value> generator.influence”<br>“invariants.detached[<value>]”<br>“{slot "<value>" / slots[<value>]}”<br>“invariants.consumerDrivenMix[<value>]”<br>“event "<value>"”<br>“<value>'s "from"”<br>“this motion spec”<br>“{absent / <value>}”<br>“{region / <value>}”<br>“<value>.keys[<value>]”<br>“<value> generator.soft”<br>“{linkedmesh / <value>}”<br>“<value>.deform[<value>]”<br>“<value>.sequence[<value>]”<br>“<value> generator (<value>)”<br>“{bone "<value>" / bones[<value>]}”<br>Here the values named in the message include: “uvs”, “mixX”, “blend”, “scaleX”, “groups”, “radius”, “images”, “axisBone”, “maxBones”, “massBone”, “softness”, “timelines”, “archetype”, “rotateMode”, “windGlobal”, “localTarget”, “spacingMode”, “positionMode”, “inertiaGlobal”, “dampingGlobal”, “gravityGlobal”, “strengthGlobal”, “skeleton.stageBox”, “invariants.idleDrivesMeshes”, “ik”, “lag”, “why”, “cut”, “fps”, “icon”, “dark”, “cols”, “kind”, “hull”, “cell”, “path”, “loop”, “mask”, “ease”, “ramp”, “note”, “mode”, “size”, “rows”, “mixY”, “alpha”, “pairs”, “inner”, “curve”, “limit”, “edges”, “float”, “start”, “audio”, “along”, “clamp”, “width”, “skins”, “links”, “chain”, “phase”, “about”, “power”, “count”, “color”, “gamma”, “index”, “delay”, “shearY”, “source”, “convex”, “target”, “amount”, “volume”, “shearX”, “slider”, “string”, “center”, “parent”, “closed”, “zScale”, “inward”, “margin”, “across”, “height”, “digits”, “scaleY”, “anchor”, “degrees”, “inverse”, “stretch”, “easings”, “against”, “default”, “inherit”, “lengths”, “carried”, “balance”, “stagger”, “outward”, “through”, “vertices”, “axis_deg”, “contrast”, “compress”, “notUnder”, “property”, “duration”, “controls”, “additive”, “rotation”, “triangles”, “mixScaleY”, “mixScaleX”, “mixShearY”, “tolerance”, “mixRotate”, “meshSlots”, “minWeight”, “mixGlobal”, “amplitude”, “massGlobal”, “meshCenter”, “fromVertex”, “slotWindow”, “wavelength”, “"skeleton"”, “attachments”, “vertexCount”, “maxVertices”, “localSource”, “"invariants"”, “bendPositive”, “boneIndexing”, “constantSpeed”, “meshTriangles”, “referenceScale”. | Fix each printed stray key's spelling using the suggested or known names, or remove it. |
|
|
7752
|
+
| <a id="CE_KEYS_UNKNOWN_FIELD"></a>`CE_KEYS_UNKNOWN_FIELD` | “<where>: <what> has <strays.length === 1 ? 'a key' : `${strays.length} keys`> this compiler does not read: <named.join(', ')>. Nothing reads such a key, so it would be dropped from the emitted skeleton in silence — fix the spelling or remove it. {This shape has no fields at all. / Known here: <[...known].sort().join(', ')>.}”<br>“{"<key>" / "<key>" (did you mean <near.map((n) => `"${n}"`).join(', ')>?)}”<br>Also: “skin "<value>" slot "<value>" attachment "<value>"”<br>“<value> generator.bones[<value>]”<br>“{constraint "<value>" / constraints[<value>]}”<br>“<value>.transform.envelope”<br>“<value> weights[<value>][<value>]”<br>“invariants.deformMayFold[<value>]”<br>“animations."<value>"”<br>“<value>.offsets[<value>]”<br>“{stop "<value>" / stops[<value>]}”<br>“<value> properties."<value>".to."<value>"”<br>“this rig spec”<br>“<value>.derive”<br>“setup."<value>"”<br>“physics."<value>"”<br>“<value>'s "lower"”<br>“<value> "sequence"”<br>“<value> generator.bias”<br>“<value> generator.depth”<br>“<value>.events[<value>]”<br>“<value>.tracks[<value>]”<br>“invariants.axes[<value>]”<br>“<value> generator.falloff”<br>“<value>.drawOrder[<value>]”<br>“<value> generator.influence”<br>“invariants.detached[<value>]”<br>“{slot "<value>" / slots[<value>]}”<br>“invariants.consumerDrivenMix[<value>]”<br>“event "<value>"”<br>“<value>'s "from"”<br>“this motion spec”<br>“{absent / <value>}”<br>“{region / <value>}”<br>“<value>.keys[<value>]”<br>“<value> generator.soft”<br>“{linkedmesh / <value>}”<br>“<value>.deform[<value>]”<br>“<value>.sequence[<value>]”<br>“<value> generator (<value>)”<br>“{bone "<value>" / bones[<value>]}”<br>Here the values named in the message include: “uvs”, “mixX”, “blend”, “scaleX”, “groups”, “radius”, “images”, “axisBone”, “maxBones”, “massBone”, “softness”, “timelines”, “archetype”, “rotateMode”, “windGlobal”, “localTarget”, “spacingMode”, “positionMode”, “inertiaGlobal”, “dampingGlobal”, “gravityGlobal”, “strengthGlobal”, “skeleton.stageBox”, “invariants.idleDrivesMeshes”, “ik”, “lag”, “why”, “cut”, “fps”, “icon”, “dark”, “cols”, “kind”, “hull”, “cell”, “path”, “loop”, “mask”, “ease”, “ramp”, “note”, “mode”, “size”, “rows”, “mixY”, “alpha”, “pairs”, “inner”, “curve”, “limit”, “edges”, “float”, “start”, “audio”, “along”, “clamp”, “width”, “skins”, “links”, “chain”, “phase”, “about”, “power”, “count”, “color”, “gamma”, “index”, “delay”, “shearY”, “source”, “convex”, “target”, “amount”, “volume”, “shearX”, “slider”, “string”, “center”, “parent”, “closed”, “zScale”, “inward”, “margin”, “across”, “height”, “digits”, “scaleY”, “anchor”, “degrees”, “inverse”, “stretch”, “easings”, “against”, “default”, “inherit”, “lengths”, “carried”, “balance”, “stagger”, “outward”, “through”, “vertices”, “axis_deg”, “contrast”, “compress”, “notUnder”, “property”, “duration”, “controls”, “additive”, “rotation”, “triangles”, “mixScaleY”, “mixScaleX”, “mixShearY”, “tolerance”, “mixRotate”, “meshSlots”, “minWeight”, “mixGlobal”, “amplitude”, “massGlobal”, “meshCenter”, “fromVertex”, “slotWindow”, “wavelength”, “"skeleton"”, “attachments”, “vertexCount”, “maxVertices”, “localSource”, “"invariants"”, “bendPositive”, “boneIndexing”, “constantSpeed”, “meshTriangles”, “referenceScale”, “narrowing”. | Fix each printed stray key's spelling using the suggested or known names, or remove it. |
|
|
7699
7753
|
| <a id="CE_KEYS_NUMBER_FLOAT32_FINITE"></a>`CE_KEYS_NUMBER_FLOAT32_FINITE` | “<where>: <place(path)> is <String(node)>; a number in this file is finite at float32 precision, at most ±<String(FLOAT32_MAX)>, because the skeleton is written as float32 — past that it would be emitted as Infinity, which JSON writes as null, a value the file never stated.<why>”<br>“{ / { This one is finite as a double and has no float32 but Infinity. / JSON has no spelling for Infinity: a literal past ±1.7976931348623157e308, such as 1e309, parses to it.}}” | State a finite number whose float32 rounding stays finite; inspect the printed path and magnitude. |
|
|
7700
7754
|
| <a id="CE_KEYS_FIELD_VALUE_TYPE"></a>`CE_KEYS_FIELD_VALUE_TYPE` | “<where>: <visit.name([key, ...wrong.tail])> is <typeFound(wrong.value)>; <wrong.required> is required”<br>“a boolean (true or false)”<br>“a number”<br>“a number or null”<br>“a string”<br>“a string or null”<br>“an array of arrays of numbers”<br>“an array of numbers”<br>“an array of numbers, or null”<br>“an array of strings”<br>“an object whose every value is a string or null”<br>“an object whose every value is an array of numbers”<br>“an object whose every value is an array of strings” | Replace the named field or element with the required JSON type printed in the message. |
|
|
7701
7755
|
| <a id="CE_KEYS_ENUM_VALUE"></a>`CE_KEYS_ENUM_VALUE` | “<where>: <visit.name([key])> is <JSON.stringify(value) ?? String(value)>; one of <rule.set.map((name) => JSON.stringify(name)).join(', ')> — <rule.readAs>”<br>“anything else was read as a ribbon by the mesh checks and as a ring by the control-bone reader and the geometry builder, so the refusal it got named a fault the part did not have”<br>“anything else was read as no rotation source, and the bone was emitted without the setup rotation it asked for”<br>“anything else was read as the default "name" in silence: a weighted "vertices" run was refused as unflagged, and every other attachment built byte for byte as if the key were absent” | Choose one of the printed names for the named field; the readAs explanation describes why another spelling changes its meaning. |
|
|
@@ -9037,6 +9091,23 @@ Draw order does not enter: a cell is covered or bare whichever piece is in front
|
|
|
9037
9091
|
An owner says which piece drew a ghost cell; it does not say which piece is over
|
|
9038
9092
|
or under the other, or whether that exposure should fail a consumer's policy.
|
|
9039
9093
|
|
|
9094
|
+
**Outside the overlap hull is outside the gap reading.** Both `gap` and its
|
|
9095
|
+
`gapOpen` partition are judged only inside the carried setup-overlap hull. Art
|
|
9096
|
+
that reaches beyond a joint disc, such as a drape over an elbow, can break
|
|
9097
|
+
outside that hull without a gap being reported. A gap of 0 is never a statement
|
|
9098
|
+
that the seam is closed outside the hull; inspect `ghost` and the actual frames
|
|
9099
|
+
too. Ghost independently names one-sided exposure of the carried setup overlap,
|
|
9100
|
+
not every visible break in the silhouette.
|
|
9101
|
+
|
|
9102
|
+
In the public [draped-sleeve fixture](https://github.com/rig-c/parts/blob/0628d7bc311a2b548edf7a11e8407546357d4b4d/fixtures/hybrid_limb.ts),
|
|
9103
|
+
measured on core `f273358` at both −30° and +30°, the hard cut reads `gap = 0`,
|
|
9104
|
+
`gapOpen = 0`, `ghost = 8` cells; the hybrid's two interfaces read 0 in all three
|
|
9105
|
+
classes. The [cell overlay](https://github.com/rig-c/core/blob/570a2954747780a2169fa6036eebe53f3ea66f2a/bench/studies/2026-10-25-seam-reach/evidence/cut-endpoint-cell-overlay.png)
|
|
9106
|
+
shows the notch and flap excluded outside the hull, while ghost names different
|
|
9107
|
+
cells on the joint rim. The [measurement receipt](https://github.com/rig-c/core/blob/570a2954747780a2169fa6036eebe53f3ea66f2a/bench/studies/2026-10-25-seam-reach/evidence/receipt.json)
|
|
9108
|
+
pins the inputs, commands and frame digest; these are seam cells, not display
|
|
9109
|
+
pixels. See [#1575](https://github.com/rig-c/core/issues/1575) for the reading.
|
|
9110
|
+
|
|
9040
9111
|
**Its reach, stated.** A piece folded right back over its partner carries the band
|
|
9041
9112
|
across the region it vacated, so a hull that wide reads the vacated cells its
|
|
9042
9113
|
partner's view still finds drawn. And no size is too small to print: on
|
|
@@ -9209,6 +9280,118 @@ is a defect is yours; when it is, the remedy is a `drawOrder` key in the motion
|
|
|
9209
9280
|
spec (or the slot's place in the rig's `slots`), and the next render says whether
|
|
9210
9281
|
the line moved.
|
|
9211
9282
|
|
|
9283
|
+
### 8.10 Sprite sheets from a render — `render --sheet`
|
|
9284
|
+
|
|
9285
|
+
```bash
|
|
9286
|
+
bun cli.ts render --candidate path/to/build --sheet [--sheet-format json-hash] [--page-edges pot|free] [--animation idle] [--fps 12] [--max 256]
|
|
9287
|
+
```
|
|
9288
|
+
|
|
9289
|
+
Packs each rendered animation set into a deterministic, transparent sprite sheet
|
|
9290
|
+
beside the diagnostic frames, with a TexturePacker JSON Hash descriptor per page
|
|
9291
|
+
and a rigc manifest stating the source identities, provenance, and coordinate
|
|
9292
|
+
contract.
|
|
9293
|
+
|
|
9294
|
+
**What it writes per set (`<out>/<set>/`):**
|
|
9295
|
+
|
|
9296
|
+
- `sheet.png` and `sheet.json` (plus `sheet2.png`, `sheet2.json`, … when a set
|
|
9297
|
+
spills across `2048 × 2048` pages) — RGBA PNG pages rendered over transparent
|
|
9298
|
+
`[0, 0, 0, 0]` (`alphaMode: "straight"`), packed through `packAtlas` with a
|
|
9299
|
+
fixed `2048` page ceiling and `2`-px clamped edge extrusion around every cell.
|
|
9300
|
+
`--page-edges pot` (the default) rounds each page edge up to a power of two;
|
|
9301
|
+
`--page-edges free` sizes each page tightly to the packed cells plus their
|
|
9302
|
+
`2`-px border.
|
|
9303
|
+
- `sheet-manifest.json` (`spec: "rigc-sheet/1"`) — the set's machine-readable
|
|
9304
|
+
provenance and coordinate contract: `format` (`"json-hash"`), `namespace`,
|
|
9305
|
+
`source` (`{ skeleton: { file, sha256 }, model: { file, sha256 } | null }`,
|
|
9306
|
+
recording the relative filenames and SHA-256 digests of the `skeleton.json`
|
|
9307
|
+
and `skeleton.model.json` the sheet was rendered from, with no paths),
|
|
9308
|
+
`animation` (`null` on a setup-pose set), optional `skin` / `slots` / `hidden` /
|
|
9309
|
+
`environment` / `pinned`, `fps`, `duration`, `sampled`, `endpointPolicy`
|
|
9310
|
+
(`"inclusive-grid"`), `alphaMode` (`"straight"`), `filterPolicy`
|
|
9311
|
+
(`{ recommended: "linear", paddingPx: 2, extrusion: "clamp-edge" }`),
|
|
9312
|
+
`pageEdges`, `pageSize` (`2048`), `padding` (`2`), `sourceSize` (`{ w, h }`),
|
|
9313
|
+
`viewport`, `pivot` (`{ world: [0, 0], canvasPx: { x, y }, anchor: { x, y } }`),
|
|
9314
|
+
`pages[]` (`{ index, image, table, width, height, occupancy, frames }`),
|
|
9315
|
+
and `frames[]` in chronological `0 .. N - 1` order (`{ index, key, time, page, image, table, empty, frame, spriteSourceSize, sourceSize, anchor }`).
|
|
9316
|
+
- `frames.json` gains `sheet` on each set entry (`manifest`, `format`,
|
|
9317
|
+
`pageEdges`, `pages`, `frames`, `emptyFrames`).
|
|
9318
|
+
|
|
9319
|
+
**What does not move.** The diagnostic `f####.png` frames and `contact.png` stay
|
|
9320
|
+
drawn over `BACKGROUND` (`[232, 232, 232, 255]`) and are byte for byte what `render`
|
|
9321
|
+
writes without `--sheet`; stripping `sheet` from each set in `frames.json`
|
|
9322
|
+
leaves the plain render's `frames.json` to the byte (`SH02`). Two runs and both
|
|
9323
|
+
posers (`--poser core` and `--poser spine`) write the same `sheet*.png`,
|
|
9324
|
+
`sheet*.json`, and `sheet-manifest.json` byte for byte (`SH03`), with no
|
|
9325
|
+
timestamps, host strings, or absolute paths.
|
|
9326
|
+
|
|
9327
|
+
**Trimming, empty frames, and `alpha > 0`.** Every frame is rendered at the
|
|
9328
|
+
set's full canvas (`sourceSize: { w: viewport.width, h: viewport.height }`) over
|
|
9329
|
+
straight-alpha `[0, 0, 0, 0]` and trimmed to the minimal bounding box of pixels
|
|
9330
|
+
with `alpha > 0` (`alpha >= 1`), preserving `alpha === 1` feather edges (`SH13`)
|
|
9331
|
+
and un-premultiplied RGB channels (`SH14`). When every pixel on a frame has
|
|
9332
|
+
`alpha === 0` (for instance, when `--hide` or `--slot` hides every visible slot
|
|
9333
|
+
on that step), the frame is emitted as a `1 × 1` transparent `[0, 0, 0, 0]`
|
|
9334
|
+
sentinel cell (`empty: true`, `trimmed: true`,
|
|
9335
|
+
`spriteSourceSize: { x: 0, y: 0, w: 1, h: 1 }`, `sourceSize` preserved) so the
|
|
9336
|
+
animation's frame count and timing stay intact and reconstructing the frame
|
|
9337
|
+
yields an all-zero full canvas (`SH12`).
|
|
9338
|
+
|
|
9339
|
+
**Shared world `(0, 0)` anchor and multi-page layout.**
|
|
9340
|
+
|
|
9341
|
+
- Each entry in `frames` carries `"anchor": { "x": ax, "y": ay }`, where
|
|
9342
|
+
`canvasPx = projector(viewport)(0, 0)` projects Spine world `(0, 0)` into
|
|
9343
|
+
untrimmed canvas pixel coordinates and `ax = canvasPx.x / viewport.width`,
|
|
9344
|
+
`ay = canvasPx.y / viewport.height`. When a pinned `--viewport` excludes world
|
|
9345
|
+
`(0, 0)`, `ax` and `ay` are preserved un-clamped outside `[0, 1]` so placing a
|
|
9346
|
+
sprite at `(worldOriginX, worldOriginY)` still lands every pixel at its exact
|
|
9347
|
+
world coordinate (`SH15`).
|
|
9348
|
+
- Each `sheet*.json` carries `frames`, `animations`, and `meta` (`app: "rig-c"`,
|
|
9349
|
+
`format: "RGBA8888"`, `image`, `size: { w, h }` as a single object and never an
|
|
9350
|
+
array, and `scale: "1"`). When a set spills across multiple pages, `packAtlas`
|
|
9351
|
+
assigns trimmed cells to pages by descending area rather than by frame time:
|
|
9352
|
+
each page's `frames` and `animations[<namespace>]` list only the frames that
|
|
9353
|
+
landed on that page (ordered chronologically within that page), `meta.related_multi_packs`
|
|
9354
|
+
names the sibling descriptor files (`SH16`), and the full set's playback order
|
|
9355
|
+
across all pages comes from `sheet-manifest.json`'s `frames[]` (`0 .. N - 1`).
|
|
9356
|
+
|
|
9357
|
+
**Collision-free frame keys and sampling policy.** Frame keys are
|
|
9358
|
+
`<rig>/<set>[/<variants>]/f<NNNN>` (for example, `squash/bounce/f0000` at the
|
|
9359
|
+
default `12` fps or `squash/bounce@24fps/skin=alt/f0000` at `24` fps under
|
|
9360
|
+
`--skin alt`), where `<rig>` is derived from `rig.archetype` in
|
|
9361
|
+
`skeleton.model.json` (the rig spec's `name`, never the build output folder) and
|
|
9362
|
+
`<set>` is `frameSetDir(animation, fps)`, so rendering one animation at different
|
|
9363
|
+
rates or copying a build into differently named folders never collides or moves
|
|
9364
|
+
a byte (`SH04`, `SH17`, `SH19`, `SH20`). Sampling follows `render`'s closed grid
|
|
9365
|
+
(`[0, duration]` at `Math.round(duration * fps) + 1` frames, recorded as
|
|
9366
|
+
`endpointPolicy: "inclusive-grid"`): whether a looping playback drops the
|
|
9367
|
+
closing endpoint frame or plays the full grid is a scene playback decision left
|
|
9368
|
+
to the consumer.
|
|
9369
|
+
|
|
9370
|
+
**Self-check, external reader verification, and refusals.** Before any file is
|
|
9371
|
+
written, `verifySheetPack`
|
|
9372
|
+
reconstructs every frame from the packed `sheet*.png` pages and `sheet*.json`
|
|
9373
|
+
descriptors onto a blank `sourceSize.w × sourceSize.h` canvas and checks that
|
|
9374
|
+
every reconstructed canvas is byte-identical (`0` differing RGBA bytes) to the
|
|
9375
|
+
untrimmed transparent render (`SH05`), that every cell's `2`-px border is
|
|
9376
|
+
clamped edge extrusion (`SH06`), that no two padded cells overlap (`SH07`–`SH11`),
|
|
9377
|
+
and that `source` records relative filenames and valid 64-hex SHA-256 digests (`SH21`).
|
|
9378
|
+
On a source checkout, `bun tools/sheet_readers.ts --sheet <dir>` loads a packed
|
|
9379
|
+
set (single-page or multi-page via `meta.related_multi_packs`) in headless
|
|
9380
|
+
Playwright Chromium through the pinned `pixi.js@8.22.0` (`PIXI.Assets.load` /
|
|
9381
|
+
`PIXI.Spritesheet`) and `phaser@3.90.0` (`Phaser.Textures.Parsers.JSONHash` /
|
|
9382
|
+
`addAtlasJSONHash`) loaders and checks cut rectangles, `sourceSize` and
|
|
9383
|
+
`spriteSourceSize`, `defaultAnchor` / `customPivot` alignment at
|
|
9384
|
+
`pivot.canvasPx` (`0` px drift; in Phaser 3.86+, set `roundPixels: false` on the
|
|
9385
|
+
game or camera when `pivot.canvasPx` is sub-pixel so `CanvasRenderer` does not
|
|
9386
|
+
round the sprite translation before adding `-displayOrigin + frame.{x,y}`),
|
|
9387
|
+
chronological playback order, multipage membership, and straight-alpha rendering
|
|
9388
|
+
over light (`[232, 232, 232, 255]`) and dark (`[28, 28, 32, 255]`) backgrounds;
|
|
9389
|
+
`SH22`–`SH25` hold its host-side refusals.
|
|
9390
|
+
`--sheet-format` with an unknown value or without `--sheet`, `--page-edges` on
|
|
9391
|
+
`render` without `--sheet` or with an unknown value, and any frame whose trimmed
|
|
9392
|
+
bounds plus its `2`-px border on each side exceed `2048` (`w + 4 > 2048` or
|
|
9393
|
+
`h + 4 > 2048`) are refused by name, exit 2, with nothing written (`SH17`).
|
|
9394
|
+
|
|
9212
9395
|
---
|
|
9213
9396
|
|
|
9214
9397
|
## 9. Checking against the frames — `rigc check`
|
|
@@ -12307,3 +12490,90 @@ calls, with the independent linked mesh selected, agree exactly. The API's
|
|
|
12307
12490
|
nonselected output is the stored source geometry through the unchanged setup
|
|
12308
12491
|
matrices; it does not borrow a different selection's deform. The control
|
|
12309
12492
|
checks both results and the source of the difference, without an epsilon.
|
|
12493
|
+
|
|
12494
|
+
## 15. Posing in place for a player — the runtime poser (stage 0)
|
|
12495
|
+
|
|
12496
|
+
rigc poses a model document twice over, on purpose (issue #1591). The **oracle**
|
|
12497
|
+
(`src/core/`) is the reference: each pose is a pure function of the document,
|
|
12498
|
+
the skin and the time, returned as a fresh, name-keyed, detached record, and it
|
|
12499
|
+
is the poser `tools/core_gate.ts` holds to spine-core 4.3.13 at tolerance 0. The
|
|
12500
|
+
**runtime poser** (`src/runtime/`) is for a player drawing many skeletons a
|
|
12501
|
+
frame: it resolves every name to an index once at load and overwrites typed
|
|
12502
|
+
arrays in place, so a steady-state frame allocates nothing. It computes every
|
|
12503
|
+
value with the oracle's own formulas in the oracle's order of operations, and it
|
|
12504
|
+
is held equal to the oracle at tolerance 0 rather than trusted; the oracle is not
|
|
12505
|
+
changed for it. It is imported from `rig-c/runtime`, an **experimental** entry
|
|
12506
|
+
(RELEASING.md, *The import surface*): not promised until it stabilises, so a
|
|
12507
|
+
minor may reshape it and a dependant pins an exact version.
|
|
12508
|
+
|
|
12509
|
+
```ts
|
|
12510
|
+
import { readModel, underNoSkin } from 'rig-c/core';
|
|
12511
|
+
import { compileRuntime, RuntimePose, RuntimeRefusal } from 'rig-c/runtime'; // experimental
|
|
12512
|
+
|
|
12513
|
+
const view = underNoSkin(readModel(modelText));
|
|
12514
|
+
const model = compileRuntime(view, { ignore: ['events'] }); // throws RuntimeRefusal, every missing construct named
|
|
12515
|
+
const pose = new RuntimePose(model); // every buffer allocated here, once
|
|
12516
|
+
pose.setAnimation('walk'); // a refused animation throws its RuntimeRefusal
|
|
12517
|
+
pose.step(1 / 60); // or pose.apply(t) at an animation time
|
|
12518
|
+
pose.computeVertices();
|
|
12519
|
+
// pose.world: 6 numbers per bone (a, b, c, d, worldX, worldY — W_A … W_Y), y up
|
|
12520
|
+
// pose.light: 4 per slot; pose.shown: per slot an index into model.attachments, or −1
|
|
12521
|
+
// pose.vertices: model.attachments[i].count numbers at model.attachments[i].offset
|
|
12522
|
+
```
|
|
12523
|
+
|
|
12524
|
+
Each `model.attachments[i]` also carries readonly `skin` and `placeholder`: the
|
|
12525
|
+
exact resolved table identity `(skin, model.slots[attachment.slot].name, placeholder)`.
|
|
12526
|
+
The display `name` may be shared by different placeholders; it is never a table
|
|
12527
|
+
key to guess the source from. These load-time strings do not change the pose path.
|
|
12528
|
+
|
|
12529
|
+
| | what stage 0 poses |
|
|
12530
|
+
| --- | --- |
|
|
12531
|
+
| bones | local and world transforms, all five inherit modes, skin-required bones under the view's skin (an inactive bone is zeros, as the oracle poses it) |
|
|
12532
|
+
| timelines | `rotate`, `translate(x/y)`, `scale(x/y)`, `shear(x/y)`; a slot's `attachment`, `rgba`, `rgb` and `alpha`; ik and transform timelines; linear, stepped and Bézier keys, each read from a per-pose key cursor |
|
|
12533
|
+
| constraints | one- and two-bone ik; world-space, absolute transform constraints; the document's order, each followed by the bones below what it moved posed again |
|
|
12534
|
+
| vertices | regions, meshes, weighted meshes and linked meshes, written into the pose's buffer |
|
|
12535
|
+
| time | one animation at full weight from the setup pose, at a time (`apply`) or stepped as a looping track (`step`, the track time wrapped by the duration as `openTrack` wraps it) |
|
|
12536
|
+
|
|
12537
|
+
**Everything else is refused at load, by name.** `RuntimeRefusal.missing` lists
|
|
12538
|
+
every construct met, each `{ feature, detail, animation }`, and the message
|
|
12539
|
+
reads `not posed by the runtime poser (stage 0), N construct(s) missing:
|
|
12540
|
+
<feature>: <detail>; …`. A model-level feature refuses the whole model; an
|
|
12541
|
+
animation-level one refuses that animation alone (`model.refused`;
|
|
12542
|
+
`model.animation(name)` and `setAnimation(name)` throw its refusal) and every
|
|
12543
|
+
other animation is posed. Fall back to the oracle (`openTrack`) for what is
|
|
12544
|
+
refused: nothing is posed approximately.
|
|
12545
|
+
|
|
12546
|
+
| feature | refuses | what it is |
|
|
12547
|
+
| --- | --- | --- |
|
|
12548
|
+
| `skin` | the model | a skin other than `default` |
|
|
12549
|
+
| `path-constraint`, `physics-constraint`, `slider-constraint` | the model | a constraint of that kind |
|
|
12550
|
+
| `local-transform`, `additive-transform` | the model | a transform constraint with `localSource` or `localTarget`, or `additive` |
|
|
12551
|
+
| `inactive-constrained-bone` | the model | an applied ik or transform constraint naming a bone the view leaves inactive (what the oracle writes there depends on the pass before) |
|
|
12552
|
+
| `noscale-world-target` | the model | a world-space transform onto a `noScale` or `noScaleOrReflection` bone, whose local values would be read back in that mode |
|
|
12553
|
+
| `clipping`, `null-atlas-region` | the model when shown at setup, else the animation that keys it | a clipping attachment; a region whose atlas rectangle the model states as `null` |
|
|
12554
|
+
| `skin-conflict` | as above | a placeholder the skins fill differently |
|
|
12555
|
+
| `oracle-absent` | the model | a pose the oracle itself leaves out (`constraintsAbsentWhy`) |
|
|
12556
|
+
| `inherit-timeline`, `deform-timeline`, `sequence-timeline`, `draw-order-timeline`, `dark-colour-timeline` | the animation | a timeline of that kind (`dark-colour` is `rgba2` / `rgb2`) |
|
|
12557
|
+
| `events` | the animation, unless `ignore: ['events']` | event keys. Events move no pose, so a consumer that reads none may ask for them to be ignored and is handed a poser that fires none; without the request a dropped cue would be silent |
|
|
12558
|
+
|
|
12559
|
+
A call it cannot answer throws `RuntimeInputError`, naming the call:
|
|
12560
|
+
`apply: no animation is set (setAnimation first)`, `animation "x" is not one of
|
|
12561
|
+
this document's [...]`, `apply: the time NaN is not finite`.
|
|
12562
|
+
|
|
12563
|
+
**The gate.** `tools/core_gate.ts` runs a third dumper (`runtimeDump` in
|
|
12564
|
+
`tools/pose_oracle.ts`) over every row it builds, at the oracle's samples, and
|
|
12565
|
+
compares its document with the oracle's in full doubles at tolerance 0 twice:
|
|
12566
|
+
through `compare` and cell by cell (which reads the bones `compare`'s
|
|
12567
|
+
ill-conditioned rule leaves out). One `RUNTIME` line per row reads `IDENTICAL`
|
|
12568
|
+
(covered whole and equal), `SKIP` (refused at load, or animations or events left
|
|
12569
|
+
out — every one named), `DIFF` (the first value that differs) or `REFUSED`; then
|
|
12570
|
+
one `RUNTIME SKIP feature <name>` line per feature with the rows it refused, a
|
|
12571
|
+
`HOLE` line when no row is covered whole or nothing was compared, and `RUNTIME
|
|
12572
|
+
GREEN|RED`, counting rows covered whole, covered in part and refused at load, and
|
|
12573
|
+
the poses and values compared. A `DIFF` or `REFUSED` turns the run RED; a SKIP
|
|
12574
|
+
never counts as a pass. The page UVs are the atlas's and are compared by neither
|
|
12575
|
+
side here. The selftest's `runtime-poser` suite holds the gate to its plants (a
|
|
12576
|
+
1e-6° bone perturbation, a weighted binding bound to the wrong bone index, a
|
|
12577
|
+
refused feature smuggled through — each caught by name), a seeded population of
|
|
12578
|
+
every construct above, the looping step against `openTrack`, and a steady-state
|
|
12579
|
+
frame measured at 0 bytes with the collector off.
|