spine-rigc 0.14.0 → 0.15.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 +21 -2
- package/cli.ts +33 -10
- package/docs/AUTHORING.md +232 -24
- package/docs/FACE.md +35 -0
- package/docs/INGEST.md +10 -6
- package/docs/MOTION.md +7 -1
- package/docs/RIGGING.md +1456 -0
- package/package.json +2 -1
- package/src/atlas.ts +130 -57
- package/src/chainfit.ts +707 -35
- package/src/deformgen.ts +124 -8
- package/src/trackgen.ts +12 -3
- package/src/validate.ts +128 -19
package/README.md
CHANGED
|
@@ -386,7 +386,7 @@ limits: [AUTHORING.md §11](docs/AUTHORING.md). The parts it refuses because
|
|
|
386
386
|
something is drawn over them are `rigc chainfit`'s, once a candidate exists —
|
|
387
387
|
[§12](docs/AUTHORING.md).
|
|
388
388
|
|
|
389
|
-
## The gallery —
|
|
389
|
+
## The gallery — six complete rigs over art that ships with them
|
|
390
390
|
|
|
391
391
|
Each directory in [`gallery/`](https://github.com/firejune/rigc/tree/main/gallery) is
|
|
392
392
|
one rig spec, one motion spec and the PNGs they name, small enough to read in one
|
|
@@ -401,6 +401,24 @@ was verified, and what writing it cost. Repository material: a clone and
|
|
|
401
401
|
| [`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash) | **`deform` timelines** | A ball squashed about its contact point and stretched along its travel, from two affine transforms the keys state rather than tabulate |
|
|
402
402
|
| [`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex) | **`contour` meshes** | A swallow-tailed banner and a serrated leaf: four meshes traced off their own alpha, waved by bone timelines and rippled by a `deform` |
|
|
403
403
|
| [`gallery/ride`](https://github.com/firejune/rigc/tree/main/gallery/ride) | `path` attachments + **path constraints** | A trolley coasting down a drawn rail and rolling back, driven by a `position` timeline, with `groups` + `stagger` keying the wheels and the ears |
|
|
404
|
+
| [`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait) | **deform `transform`** + `derive` group tracks | A 2.5D head turn: two meshes and six feature bones all keyed from one stated expression, `dx = x(cos t − 1) − z·sin t`, with the depths in the spec rather than a README |
|
|
405
|
+
| [`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod) | the **`pitch`** and **`wave`** transform kinds | A head bowing and two lop ears rippling, on three meshes each laid out for the closed form that moves it — a fold angle solved for before authoring, and a shear whose winding no amplitude can reverse |
|
|
406
|
+
|
|
407
|
+
<p align="center">
|
|
408
|
+
<img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/rigc-scene.gif" alt="A portrait rig breathing, glancing aside, then turning its head in 2.5D — hair and features sliding at different depths" width="600" />
|
|
409
|
+
</p>
|
|
410
|
+
|
|
411
|
+
<p align="center"><em>The portrait rig playing its three animations in one take — the turn is
|
|
412
|
+
the shot: both silhouette edges move apart, which a flat slide cannot do, because every
|
|
413
|
+
feature carries its own depth. Scene direction of this kind was Live2D's territory; the
|
|
414
|
+
split was authoring cost, and the cost is now one stated expression per key. Compiled and
|
|
415
|
+
rendered entirely by the published package.</em></p>
|
|
416
|
+
|
|
417
|
+
🎞️ **How the three films on this page were made** is kept with them, one directory per
|
|
418
|
+
film in [`films/`](https://github.com/firejune/rigc/tree/main/films) — a `run.sh` that
|
|
419
|
+
names every step, the assembler that cuts the shots and draws the type, and a README
|
|
420
|
+
saying what the film claims and which tool printed each figure on screen. Repository
|
|
421
|
+
material, like the gallery: a clone runs them.
|
|
404
422
|
|
|
405
423
|
## Commands
|
|
406
424
|
|
|
@@ -419,7 +437,7 @@ commands take it and what its default is.
|
|
|
419
437
|
| `preview --candidate <dir>` | one self-contained `.html` that plays it |
|
|
420
438
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
421
439
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
|
422
|
-
| `chainfit --candidate <dir> --images <dir> --frame <png>` | reads the parts `pose` refuses, through the candidate's own draw order and hierarchy: masked residuals over **visible** pixels, one hinge per child instead of four degrees of freedom, and the `rotate` key value each answer implies |
|
|
440
|
+
| `chainfit --candidate <dir> --images <dir> --frame <png>` | reads the parts `pose` refuses, through the candidate's own draw order and hierarchy: masked residuals over **visible** pixels, one hinge per child instead of four degrees of freedom, and the `rotate` key value each answer implies. A bone with two or more anchored descendants is **determined** rather than searched, and the residual that over-determination leaves is reported |
|
|
423
441
|
| `diff <candidate.json> <reference.json>` | structural comparison of two skeletons, one ratio per measure and deliberately no combined score |
|
|
424
442
|
| `check --candidate <dir> --frames <dir>` | the candidate against reference pictures — the only instrument here that can see a *wrong animation* |
|
|
425
443
|
| `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
|
|
@@ -447,6 +465,7 @@ the art. Its shape is under
|
|
|
447
465
|
| Document | For |
|
|
448
466
|
| --- | --- |
|
|
449
467
|
| 📘 **[docs/AUTHORING.md](docs/AUTHORING.md)** | **the format guide, and the one to read before writing a spec.** Both input files field by field with a complete minimal example each, every field with its Spine meaning, the rules that decide what is emitted, the build → read the report → fix → repeat loop, the map from every named failure to the file that has to change, and the features rigc refuses by name so you do not spend a loop discovering them. It travels **inside the npm package**, at `node_modules/spine-rigc/docs/AUTHORING.md` |
|
|
468
|
+
| 🦴 **[docs/RIGGING.md](docs/RIGGING.md)** | **authoring the hierarchy.** Where a bone goes and why the art is pushed out on an offset, why a pivot in the wrong place looks like a search failure and what identifies one, moving a pivot and the child row that gets forgotten, gauges, siblings-not-a-chain, what a chain can reach and how many links it needs, why a local key is not a world key, duplicate art at mirrored pivots, and constraints as structure. Every section is a stumble the run records hold more than once, ranked by how often. Ships in the package too |
|
|
450
469
|
| 🎞️ **[docs/MOTION.md](docs/MOTION.md)** | **the key-pose recipe.** How to get two poses, what a pair of poses does and does not fix, the in-betweening rules and where each comes from, and how to spread candidates so a ballot informs. Ships in the package too |
|
|
451
470
|
| 🙂 **[docs/FACE.md](docs/FACE.md)** | **authoring a face.** A blink, a gaze and a 2.5D head turn on plain Spine data: the one line of yaw arithmetic every number in a turn comes from, depth as the parameter you are actually authoring, where to put a grid's columns and the angle at which any grid folds, what foreshortens and what does not, channel allocation before the first key, and the three cliffs with their angles. Also the deform audit gap, demonstrated — a folded mesh gates green — and the differential check that works today |
|
|
452
471
|
| 📥 **[docs/INGEST.md](docs/INGEST.md)** | **working with a skeleton you did not author.** What every command can and cannot do with a foreign `skeleton.json`, reading it with the toolchain, transcription as the route that makes it yours, what each validator complaint means on an export, and the re-pivot/rename/extend recipes. Ships in the package too |
|
package/cli.ts
CHANGED
|
@@ -86,6 +86,7 @@ import {
|
|
|
86
86
|
ChainFitError,
|
|
87
87
|
DEFAULT_HINGE_MAX,
|
|
88
88
|
DEFAULT_HINGE_MIN,
|
|
89
|
+
DEFAULT_MIN_LEVER_PX,
|
|
89
90
|
DEFAULT_MIN_VISIBLE,
|
|
90
91
|
DEFAULT_PASSES,
|
|
91
92
|
estimateChainFit,
|
|
@@ -180,8 +181,19 @@ function repositoryUrl(): string {
|
|
|
180
181
|
* Listed by name rather than inferred from "the next argument looks like a
|
|
181
182
|
* flag": inferring it would turn `--out --json report.json` — a real typo, a
|
|
182
183
|
* missing value — into a silently accepted switch plus a stray positional.
|
|
184
|
+
*
|
|
185
|
+
* ⚠️ This set and `FLAG_VALUES` are two halves of one statement, and they are
|
|
186
|
+
* the halves a reader and the parser read separately: a flag absent from
|
|
187
|
+
* `FLAG_VALUES` is printed bare in every usage line and flag table, and a flag
|
|
188
|
+
* present here is the only kind the parser will accept bare. `all-bones` was in
|
|
189
|
+
* one half and not the other for two releases — documented bare in `bonedist`'s
|
|
190
|
+
* usage line, in the shared flag table, and in the hint `src/bonedist.ts` prints
|
|
191
|
+
* under a truncated bone table, while the parser fell through to the value
|
|
192
|
+
* branch and answered the caller who followed that hint with `rigc: --all-bones
|
|
193
|
+
* needs a value` (issue #328). `CLI10`/`CLI11` in `selftest.ts` now hold the two
|
|
194
|
+
* halves together by reading `--help` rather than by naming a flag.
|
|
183
195
|
*/
|
|
184
|
-
const BOOLEAN_FLAGS = new Set(['all-frames', 'help', 'copy-images', 'again', 'pack']);
|
|
196
|
+
const BOOLEAN_FLAGS = new Set(['all-frames', 'all-bones', 'help', 'copy-images', 'again', 'pack']);
|
|
185
197
|
|
|
186
198
|
/**
|
|
187
199
|
* The flags a command is allowed to spell more than once.
|
|
@@ -715,9 +727,10 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
715
727
|
const { label, opts } = resolveCut(flags);
|
|
716
728
|
const profile = readProfile(flags);
|
|
717
729
|
const packing = flags.pack !== undefined;
|
|
718
|
-
//
|
|
730
|
+
// Two combinations are refused rather than silently resolved, because in each
|
|
719
731
|
// one the two flags disagree about a single question and there is no answer
|
|
720
|
-
// that is not a guess about which the caller meant.
|
|
732
|
+
// that is not a guess about which the caller meant. (There were three until
|
|
733
|
+
// issue #266 — see the note below the second.)
|
|
721
734
|
if (packing && opts.atlasInPath !== undefined) {
|
|
722
735
|
throw new UsageError(
|
|
723
736
|
'--pack and --atlas-in are opposite directions through the same door: --pack MAKES an atlas out of the ' +
|
|
@@ -730,13 +743,12 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
730
743
|
'the loose part PNGs, which a packed atlas does not reference. Drop --copy-images',
|
|
731
744
|
);
|
|
732
745
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
746
|
+
// `--pack --profile spine-html` used to be the third refusal here, because
|
|
747
|
+
// A06's coverage clause was "one part per page" flat and a legitimate pack
|
|
748
|
+
// arrived at the gate reading as a defect. Since issue #266's second follow-up
|
|
749
|
+
// that clause is "one part per page OR a tiling page", so the combination is
|
|
750
|
+
// now a build like any other — and it is the only one that puts the renderer's
|
|
751
|
+
// own rulebook over shared-page sampling.
|
|
740
752
|
if (!packing) {
|
|
741
753
|
for (const name of ['page-size', 'padding'] as const) {
|
|
742
754
|
if (flags[name] !== undefined) throw new UsageError(`--${name} only means something with --pack`);
|
|
@@ -1407,6 +1419,11 @@ function cmdChainFit(flags: Record<string, string>): void {
|
|
|
1407
1419
|
if (!Number.isInteger(value) || value < 1 || value > 8) throw new UsageError('--passes must be a whole number in 1..8');
|
|
1408
1420
|
options.passes = value;
|
|
1409
1421
|
}
|
|
1422
|
+
if (flags['inward-lever'] !== undefined) {
|
|
1423
|
+
const value = Number(flags['inward-lever']);
|
|
1424
|
+
if (!Number.isFinite(value) || value < 0) throw new UsageError('--inward-lever must be a number of frame pixels, 0 or more');
|
|
1425
|
+
options.minLeverPx = value;
|
|
1426
|
+
}
|
|
1410
1427
|
if (flags['anchor-residual'] !== undefined) {
|
|
1411
1428
|
const value = Number(flags['anchor-residual']);
|
|
1412
1429
|
if (!Number.isFinite(value) || value <= 0 || value > 1) throw new UsageError('--anchor-residual must be a number in (0, 1]');
|
|
@@ -2300,6 +2317,10 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
2300
2317
|
'anchor-residual':
|
|
2301
2318
|
`the residual a \`pose\` placement must be within to anchor a chain (default ${ANCHOR_MAX_RESIDUAL}, with ` +
|
|
2302
2319
|
`unexplained ≤ ${ANCHOR_MAX_UNEXPLAINED} and unambiguous — the 2026-09-03 measurement run's own clean-frame criterion)`,
|
|
2320
|
+
'inward-lever':
|
|
2321
|
+
`how far apart, in frame pixels, two anchored descendants have to sit before the rotation they determine is ` +
|
|
2322
|
+
`printed (default ${DEFAULT_MIN_LEVER_PX}); below it the bone is refused \`no-bracket\` naming the measured ` +
|
|
2323
|
+
'lever, because an angle read across a short lever turns a half-pixel anchor error into several degrees',
|
|
2303
2324
|
animation: 'which animation to show; the default is every one for `render` and the first for `preview`',
|
|
2304
2325
|
max: 'longest side of a rendered frame, in pixels (default 256)',
|
|
2305
2326
|
record: 'a saved vote to check against its ballot and append to the ledger, instead of writing a ballot',
|
|
@@ -2344,6 +2365,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
2344
2365
|
'min-visible': '<0..1>',
|
|
2345
2366
|
passes: '<n>',
|
|
2346
2367
|
'anchor-residual': '<0..1>',
|
|
2368
|
+
'inward-lever': '<px>',
|
|
2347
2369
|
animation: '<name>',
|
|
2348
2370
|
max: '<px>',
|
|
2349
2371
|
record: '<result.json>',
|
|
@@ -2496,6 +2518,7 @@ const COMMANDS: CommandDoc[] = [
|
|
|
2496
2518
|
'max-residual',
|
|
2497
2519
|
'passes',
|
|
2498
2520
|
'anchor-residual',
|
|
2521
|
+
'inward-lever',
|
|
2499
2522
|
'scale',
|
|
2500
2523
|
'rotation',
|
|
2501
2524
|
'out',
|
package/docs/AUTHORING.md
CHANGED
|
@@ -30,6 +30,14 @@ that can see that, and a run that skips it has verified nothing about the motion
|
|
|
30
30
|
unlinked** for the same reason as the line above: it cites `SPEC_COVERAGE.md` and
|
|
31
31
|
the stored transcriptions, both of which are on the ladder run's forbidden list. Its
|
|
32
32
|
reader was handed a compiled skeleton rather than art and a brief
|
|
33
|
+
- Deciding the **hierarchy** itself — how many bones, where each pivot goes, what
|
|
34
|
+
hangs off what, what a chain can reach, and which of those the frames can check:
|
|
35
|
+
`docs/RIGGING.md`. 🚫 **Not an authoring input, and deliberately unlinked** for
|
|
36
|
+
the same reason as the two lines below: it is a mining of the recorded runs, so it
|
|
37
|
+
cites their `LOOP.md` files, and following a citation out of an allowed surface is
|
|
38
|
+
a leak by another route. Its rules are the ones **§8.1**, **§10.3** and
|
|
39
|
+
**MOTION §3.9** already state, which are allowed reading; what that page adds is
|
|
40
|
+
provenance and worked demonstrations, for a maintainer
|
|
33
41
|
- Reproducing a shot you were given as pictures: **§8**, and read it *before* you
|
|
34
42
|
start measuring rather than after; **§8.1** if the figure has more joints than you
|
|
35
43
|
can measure one at a time; then **§9** for the loop that closes it
|
|
@@ -188,10 +196,16 @@ unweighted meshes, packed atlases) — reach for it when you are shipping into
|
|
|
188
196
|
*that* project, not to be thorough. A report always prints which profile ran and
|
|
189
197
|
lists what that profile left out, on `PROF` lines.
|
|
190
198
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
🆕 *Packed atlases* used to be in that list twice over: `--pack --profile
|
|
200
|
+
spine-html` was **refused by name**, because `A06`'s coverage clause said "one
|
|
201
|
+
part per page" flat and rigc's own pack could not satisfy it. Since
|
|
202
|
+
[#266](https://github.com/firejune/rigc/issues/266) that clause is **one part per
|
|
203
|
+
page OR a tiling page**, so the combination is an ordinary build — and it is the
|
|
204
|
+
only one that puts the renderer's own rulebook over shared-page sampling. What a
|
|
205
|
+
*tiling* page has to satisfy is stated where the clause is, §7's `A06` row: every
|
|
206
|
+
region wholly inside the page it names, and no two regions on one page
|
|
207
|
+
overlapping. Rotation is still refused, and that is a separate clause about
|
|
208
|
+
rigc's packer never turning a region.
|
|
195
209
|
|
|
196
210
|
### 0.1 Packing the parts onto shared pages — `--pack`
|
|
197
211
|
|
|
@@ -1447,7 +1461,7 @@ is that a **depth** is the decision while a **residual** is not.
|
|
|
1447
1461
|
| `kind` | Reads | `translatex` / `translatey` | `scalex` / `scaley` | Worked case |
|
|
1448
1462
|
| --- | --- | --- | --- | --- |
|
|
1449
1463
|
| `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` |
|
|
1450
|
-
| `pitch` | each member's setup `y` | the same expression with `y` for `x` — a nod | the same |
|
|
1464
|
+
| `pitch` | each member's setup `y` | the same expression with `y` for `x` — a nod | the same | `gallery/nod` |
|
|
1451
1465
|
|
|
1452
1466
|
⭐ **The `property` says which half of the turn a key is.** A turn does two
|
|
1453
1467
|
things to a rigid part on a curved surface: it moves it, and it narrows it. Those
|
|
@@ -1456,7 +1470,19 @@ which one the track is — so a `derive` on a property its kind has no projectio
|
|
|
1456
1470
|
onto is refused by name rather than quietly driven by the wrong half.
|
|
1457
1471
|
|
|
1458
1472
|
**The parameters.** `degrees` and `depth` are required; `depth` is
|
|
1459
|
-
`{ "member": z, … }` on a group track and one number on a bone track.
|
|
1473
|
+
`{ "member": z, … }` on a group track and one number on a bone track.
|
|
1474
|
+
|
|
1475
|
+
⚠️ **`z` runs toward the viewer (FACE §1), so a larger depth is nearer.** A nose
|
|
1476
|
+
in front of the skull surface takes a **bigger** number than the socket beside
|
|
1477
|
+
it, and a **negative** depth is behind the axis — which is what makes the back of
|
|
1478
|
+
a head swing the other way (FACE §2). That sign is the one parameter here no
|
|
1479
|
+
assertion can check, so the closed form is the arbiter: a part with
|
|
1480
|
+
`depth > carried` gets a **negative** residual, and FACE §3 makes exactly that
|
|
1481
|
+
the nose diagnostic — *if the nose's residual is not negative, the depths are
|
|
1482
|
+
wrong*. ([#351](https://github.com/firejune/rigc/issues/351) was this sentence
|
|
1483
|
+
missing here and stated backwards in the field reference.)
|
|
1484
|
+
|
|
1485
|
+
`carried`
|
|
1460
1486
|
(default 0) is **the depth whose shift a parent bone already applies** — FACE
|
|
1461
1487
|
§3's shared-shift split, stated: put a bone at the plate's own origin, key
|
|
1462
1488
|
`−carried·sin t` there, and each member then keys only its residual. That split
|
|
@@ -1935,7 +1961,9 @@ where the remedy is a line you own. `A35` does **not** refuse it: it is pointed
|
|
|
1935
1961
|
other people's files, and a rule stricter than the runtime tells its reader to go
|
|
1936
1962
|
and break correct data.
|
|
1937
1963
|
|
|
1938
|
-
🖼️ **Worked examples, and they use a deform for
|
|
1964
|
+
🖼️ **Worked examples, and they use a deform for four different things** — all
|
|
1965
|
+
four are repository material rather than part of the published package, so the
|
|
1966
|
+
links go to GitHub.
|
|
1939
1967
|
[`gallery/squash`](https://github.com/firejune/rigc/tree/main/gallery/squash) — a 9-vertex ball squashed about its contact point,
|
|
1940
1968
|
from the two affine transforms its keys now **state**.
|
|
1941
1969
|
[`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait) — a 2.5D head turn, where the keys are the
|
|
@@ -1943,6 +1971,11 @@ from the two affine transforms its keys now **state**.
|
|
|
1943
1971
|
placed to sample a cosine, and a measured account of the angle past which the
|
|
1944
1972
|
mesh folds. [`gallery/flex`](https://github.com/firejune/rigc/tree/main/gallery/flex) — a leaf whose blade bends on a
|
|
1945
1973
|
`contour` mesh no bone can bend, and the measurement that picked the model.
|
|
1974
|
+
[`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod) — the
|
|
1975
|
+
projection on the **other** axis (a `pitch`, §4.11.1), and a **travelling wave**
|
|
1976
|
+
whose only moving parameter is `phase`: each of its three meshes is laid out for
|
|
1977
|
+
the model that bends it, so the rows are the argument rather than the vertex
|
|
1978
|
+
count.
|
|
1946
1979
|
|
|
1947
1980
|
📘 **[FACE.md](FACE.md) is the recipe for that second case**, and it is where the
|
|
1948
1981
|
grid questions this section leaves to its reader are answered: where to put the
|
|
@@ -1988,9 +2021,9 @@ example needed it:
|
|
|
1988
2021
|
| `kind` | Parameters | What it evaluates | Worked case |
|
|
1989
2022
|
| --- | --- | --- | --- |
|
|
1990
2023
|
| `yaw` | `radius`, `degrees`, `about` | `dx = (x−about)·(cos t − 1) − z·sin t`, `z = √(radius² − (x−about)²)` — the 2.5D turn (FACE §1) | `gallery/portrait` |
|
|
1991
|
-
| `pitch` | the same | the same expression with `y` for `x` — a nod rather than a turn |
|
|
2024
|
+
| `pitch` | the same | the same expression with `y` for `x` — a nod rather than a turn | `gallery/nod` |
|
|
1992
2025
|
| `affine` | `scale`, `about` | `dx = (sx−1)·(x−ax)`, `dy = (sy−1)·(y−ay)` — a scale about a fixed point | `gallery/squash` |
|
|
1993
|
-
| `wave` | `amplitude`, `wavelength`, `phase`, `along`, `axis` | `d = amplitude · sin(2π·along/wavelength + phase)` |
|
|
2026
|
+
| `wave` | `amplitude`, `wavelength`, `phase`, `along`, `axis` | `d = amplitude · sin(2π·along/wavelength + phase)` | `gallery/nod` |
|
|
1994
2027
|
| `bend` | `amount`, `from`, `to`, `power`, `along`, `axis` | `d = amount · u^power`, `u = (along − from)/(to − from)` | `gallery/flex` |
|
|
1995
2028
|
|
|
1996
2029
|
`along` names the coordinate a wave or a bend reads and `axis` the one it
|
|
@@ -2000,7 +2033,7 @@ axis is a stretch and `affine` states that. `power: 1` is an affine shear and
|
|
|
2000
2033
|
instead of tilting. `about` defaults to 0 (or `[0, 0]`), `phase` to 0 and `power`
|
|
2001
2034
|
to 2; nothing else has a default.
|
|
2002
2035
|
|
|
2003
|
-
|
|
2036
|
+
Six things this construct is bounded by, and each one is a refusal rather than a
|
|
2004
2037
|
convention:
|
|
2005
2038
|
|
|
2006
2039
|
**It covers every vertex, always.** A transform is a model of the attachment, not
|
|
@@ -2031,6 +2064,22 @@ consequences. One kind is the exception and it is an exception with a proof:
|
|
|
2031
2064
|
`affine` refuses a determinant at or below zero, and above zero a positive
|
|
2032
2065
|
determinant means no triangle **can** reverse.
|
|
2033
2066
|
|
|
2067
|
+
**A model that evaluates to nothing is refused too**
|
|
2068
|
+
([#350](https://github.com/firejune/rigc/issues/350)). Every parameter can be
|
|
2069
|
+
individually legal and the model still come out as a **run of zeros** — a `wave`
|
|
2070
|
+
whose wavelength puts every vertex on a zero crossing, a `bend` over a span the
|
|
2071
|
+
part barely enters. The key then claims a deformation, emits the identity and
|
|
2072
|
+
gates green: `A35` is right that the run fits and `A39` is right that no triangle
|
|
2073
|
+
moved, so the compiler is the only place it can be said. ⭐ **What distinguishes
|
|
2074
|
+
it is where the identity is stated.** A key that *means* the setup pose says so
|
|
2075
|
+
in its own parameters — `degrees: 0` (or any whole revolution), `amplitude: 0`,
|
|
2076
|
+
`amount: 0`, `scale: [1, 1]` — or carries no run at all, and those compile. The
|
|
2077
|
+
refused pair is parameters that state a deformation beside an evaluation that is
|
|
2078
|
+
the identity. The message names the vertex count, the largest value the closed
|
|
2079
|
+
form reached before quantising, and the measured fact behind the usual cause: for
|
|
2080
|
+
a wave, the closest two distinct coordinates it read and the ratio the wavelength
|
|
2081
|
+
makes against them.
|
|
2082
|
+
|
|
2034
2083
|
**It is auditable.** `explain` prints the model, the scalars the closed form
|
|
2035
2084
|
derived from it, and every offset it produced — the emitted ones, not a second
|
|
2036
2085
|
evaluation:
|
|
@@ -2146,6 +2195,15 @@ inverted build is the case worth reading — `A39` passes it (correctly: nothing
|
|
|
2146
2195
|
reverses), and the block is what says `x1.362834` where the model's own table
|
|
2147
2196
|
says `x1.319121`, with no reference render anywhere.
|
|
2148
2197
|
|
|
2198
|
+
📘 **[`gallery/nod`](https://github.com/firejune/rigc/tree/main/gallery/nod)'s
|
|
2199
|
+
README is a second reading of the same block** (repository material, hence the
|
|
2200
|
+
GitHub link), and it is the one where the figures are checked from two directions
|
|
2201
|
+
at once. Its `pitch` band ratios are *derived* from the mesh's own row table and
|
|
2202
|
+
*measured* off the posed vertices, and the two agree to six decimals. Its `wave`
|
|
2203
|
+
keys then report an area ratio of `1.000000 ± 1e-6` at **every** amplitude — not
|
|
2204
|
+
a measurement but a **proof** showing up as one, because a wave that reads `y`
|
|
2205
|
+
and displaces `x` over row-major quads preserves every signed area exactly.
|
|
2206
|
+
|
|
2149
2207
|
---
|
|
2150
2208
|
|
|
2151
2209
|
### 4.12 `path` and `slider` timelines — tracks, not their own groups
|
|
@@ -2302,6 +2360,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
2302
2360
|
| `deform … (t=…): … has no single space to evaluate it in` | §4.11.1 — the attachment's vertices are in several bind spaces; key the control bone, or write the pairs with `offset` |
|
|
2303
2361
|
| `deform … (t=…): transform yaw has radius R, and vertex v sits at x=… past it` | §4.11.1 — the cylinder has no surface there; raise the radius to where the part sits |
|
|
2304
2362
|
| `deform … (t=…): transform affine has scale […], whose determinant is …` | §4.11.1 — at or below zero the map reverses every triangle |
|
|
2363
|
+
| `deform … (t=…): transform <kind> states …, and every one of this attachment's N vertices evaluates to an offset of 0` | §4.11.1 — the parameters state a deformation and the geometry sampled it to nothing; the message names the measured cause. A key that means the setup pose states the identity in its parameters, or carries no run |
|
|
2305
2364
|
| `vertexCount is N, which is not a multiple of 3` | §3.4 — a path's vertices are knots and handles read in groups of three: `3(K + 1)` open, `3K` closed |
|
|
2306
2365
|
| `vertexCount is N and an open path needs at least 6` | §3.4 — an open path drops its first and last point, so it needs six for one curve |
|
|
2307
2366
|
| `"lengths" is not authored — rigc measures the setup arc length of each curve` | §3.4 — delete the array; it is a measurement of the vertices above it |
|
|
@@ -2347,7 +2406,7 @@ The report prints one line per assertion:
|
|
|
2347
2406
|
| `A03_REGION_WIDTH_HEIGHT_FINITE` | both | a region loaded `NaN` or a non-positive size — the attachment has no `image` and no `width`/`height` |
|
|
2348
2407
|
| `A04_MESH_TRIANGLES_AND_ENCODING` | both | authored mesh geometry: triangle count not a multiple of 3, an index out of range, or a `vertices` length that disagrees with `uvs` (the weighted/unweighted trap) |
|
|
2349
2408
|
| `A05_CURVE_ARRAY_LENGTH` | both | a raw `curve` with the wrong number of values, a non-finite number in one, or a curve on a timeline that cannot take one. Four numbers **per value channel** |
|
|
2350
|
-
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk. Under `spine-html` also: `pma`, rotation, and a region that
|
|
2409
|
+
| `A06_ATLAS_PAGE_SIZE_MATCHES_PNG` | both ◑ | the atlas `size:` disagrees with the PNG on disk. Under `spine-html` also: `pma`, rotation, and a page that is neither **one part covering it exactly** (the unpacked convention) nor a **tiling** — a page whose regions all sit inside it and none of which overlap ([#266](https://github.com/firejune/rigc/issues/266)). A packed atlas therefore gates under this profile; what the message names is the region that runs off its page, or the pair that shares texels |
|
|
2351
2410
|
| `A07_ATLAS_TEXT_SHAPE` | both | atlas text: a region name with stray whitespace, or a blank line splitting a page block. rigc writes the atlas, so this means a hand-edited file |
|
|
2352
2411
|
| `A08_REGION_NAMES_MATCH_ATTACHMENTS` | both ◑ | an attachment resolves to a region the atlas does not have — usually a `path`/`image` basename mismatch. Under `spine-html` the placeholder and the region name must also be *identical* |
|
|
2353
2412
|
| `A09_ANIMATION_DURATION_MATCHES_SPEC` | both | the loaded duration ≠ the declared one, or the two sides disagree about which animations exist (R7). Asymmetric by design: a frame of slack for an animation that ends early, and none worth the name for a key *past* the declared end, which is the same rule §4.5 states at compile time — held here against a skeleton the compiler never saw. **SKIP** when neither side has an animation at all — a static rig has no duration |
|
|
@@ -2360,7 +2419,7 @@ The report prints one line per assertion:
|
|
|
2360
2419
|
| `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
|
|
2361
2420
|
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out` |
|
|
2362
2421
|
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
|
|
2363
|
-
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the full-stage base plate may be opaque. Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art |
|
|
2422
|
+
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the full-stage base plate may be opaque. Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art ([#266](https://github.com/firejune/rigc/issues/266)) |
|
|
2364
2423
|
| `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, or a binding at weight 0 |
|
|
2365
2424
|
| `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1 |
|
|
2366
2425
|
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count |
|
|
@@ -2765,6 +2824,14 @@ measurement, while segment lengths taken off a folded pose are an estimate — s
|
|
|
2765
2824
|
disagree, suspect the estimate. And do this **per chain, before its first fit**, because
|
|
2766
2825
|
the surgery to fix it invalidates every pose already fitted with the short chain.
|
|
2767
2826
|
|
|
2827
|
+
⚠️ **The paragraph above is written for a chain that is too *short*, and the check bites
|
|
2828
|
+
in both directions.** A chain that is too *long* fails differently — the fits converge,
|
|
2829
|
+
every residual is ordinary, and the figure splays to absorb the excess — which is why
|
|
2830
|
+
reading only this direction has twice sent a run looking for the wrong defect.
|
|
2831
|
+
[RIGGING.md](RIGGING.md) §6.2 carries the too-long case with its record and its cheap
|
|
2832
|
+
detector, and §6.3 carries the refusal that goes with it: an excess names a
|
|
2833
|
+
disagreement between a chain and a shot, and it does not say which of the two is wrong.
|
|
2834
|
+
|
|
2768
2835
|
**Re-fit the setup pose against frames drawn from every shot, not against one.** Every
|
|
2769
2836
|
animation is measured from the setup pose, so an error in it is an error in all of
|
|
2770
2837
|
them — and it is exactly the error one frame cannot show you. Fit an attachment's
|
|
@@ -4345,7 +4412,9 @@ themselves**, downward and by small amounts, most on the parts whose edges are i
|
|
|
4345
4412
|
the frame. ⇒ A residual measured before that date and one measured after are not
|
|
4346
4413
|
the same measurement; re-read a frame rather than comparing across it. The worked
|
|
4347
4414
|
figures live in [MOTION.md](MOTION.md) §6 and
|
|
4348
|
-
[BENCHMARK.md](BENCHMARK.md)
|
|
4415
|
+
[BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md) —
|
|
4416
|
+
repository material, not shipped in the package — each with its own re-baseline
|
|
4417
|
+
note.
|
|
4349
4418
|
|
|
4350
4419
|
### 11.1 It measures an input, so nothing here is a score
|
|
4351
4420
|
|
|
@@ -4460,7 +4529,11 @@ two things.
|
|
|
4460
4529
|
apologised for. The parts drawn after a part are what covers it, and the pixels
|
|
4461
4530
|
they cover are **excluded** from that part's objective rather than charged to it.
|
|
4462
4531
|
Every residual here is over the part's **visible** pixels, and every one comes
|
|
4463
|
-
with the `visibleShare` it was computed on.
|
|
4532
|
+
with the `visibleShare` it was computed on. ⚠️ **`visibleShare` is a per-frame
|
|
4533
|
+
diagnostic and not a summary statistic** — it is measured through the fitted
|
|
4534
|
+
placements, so a median or a mean of it is not comparable across fits. §12.3 says
|
|
4535
|
+
what that costs and [the 2026-09-03 study](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
|
|
4536
|
+
measures it — repository material, not shipped in the package.
|
|
4464
4537
|
- **Hierarchy and attachment geometry**, so the search collapses. A child bone
|
|
4465
4538
|
whose parent is already placed does not have four degrees of freedom: the rig
|
|
4466
4539
|
fixes its pivot, so what is left is **one hinge** about that pivot — plus a
|
|
@@ -4500,11 +4573,96 @@ the four a similarity has — and every descendant then follows from the rig. A
|
|
|
4500
4573
|
**above** an anchor does not: recovering it would need to know what the link
|
|
4501
4574
|
between them did, and that is precisely the unknown the anchor does not carry.
|
|
4502
4575
|
|
|
4503
|
-
⚠️ **So a limb with no trusted part on it or above it is refused
|
|
4504
|
-
|
|
4505
|
-
|
|
4576
|
+
⚠️ **So a limb with no trusted part on it or above it is refused**, not guessed at
|
|
4577
|
+
from a cousin. If a whole side of your figure comes back that way, the repair is
|
|
4578
|
+
upstream: give `pose` a better frame, pin its `--scale`, or loosen
|
|
4506
4579
|
`--anchor-residual` deliberately and read the consequences.
|
|
4507
4580
|
|
|
4581
|
+
### 12.2b The inward step — two anchors bracket the bone between them
|
|
4582
|
+
|
|
4583
|
+
⬆️ There is **one exception** to the paragraph above, and it is one shape.
|
|
4584
|
+
|
|
4585
|
+
One anchored descendant says nothing about the link above it — that is the
|
|
4586
|
+
sentence you just read, and it is still true. **Two** of them say something else
|
|
4587
|
+
entirely. A bone's world placement is four numbers, and a descendant's **pivot**
|
|
4588
|
+
depends on that bone and on your rig's own offsets and **not** on the
|
|
4589
|
+
descendant's own hinge. So each anchored descendant contributes two equations,
|
|
4590
|
+
two of them make four, and four equations fix four numbers. That is the whole
|
|
4591
|
+
geometry; there is no search and no window.
|
|
4592
|
+
|
|
4593
|
+
Such a bone comes back with **`role: "inward"`** and a `bone.inward` block, and
|
|
4594
|
+
the outward walk then **resumes from it** — so a subtree that was refused a moment
|
|
4595
|
+
ago is fitted normally, one hinge per link, with `bone.anchoredToRole` on every
|
|
4596
|
+
one of those placements reading `"inward"` to say what it rests on.
|
|
4597
|
+
|
|
4598
|
+
⚠️ **Which means the inward step reaches exactly the bones that BRANCH.** A bone
|
|
4599
|
+
whose children form a single sub-chain can never be determined, however good the
|
|
4600
|
+
anchor below it is: two equations, four unknowns. That is `no-bracket`, and it is
|
|
4601
|
+
a fact about your rig's topology rather than about the frame. On the 2026-09-03
|
|
4602
|
+
spineboy candidate `torso` is the only bone in the whole rig that branches — and
|
|
4603
|
+
it is the bone that recorded 30 `no-anchor` frames, which is why this exists.
|
|
4604
|
+
|
|
4605
|
+
| Field on `bone.inward` | Meaning |
|
|
4606
|
+
| --- | --- |
|
|
4607
|
+
| `form` | `descendants` — the only form there is. Named so a future one is readable beside it |
|
|
4608
|
+
| `determinants[]` | the anchored bones the four numbers were read from: each one's `bone`, the `part` its anchor came from, its `leverPx` from the determined bone, its `offsetPx`, and the art-less bones `carried` through to reach it |
|
|
4609
|
+
| `redundancy` | equations beyond the four a similarity needs, `2 × determinants − 4`. **Read `disagreementPx` next to this and never without it** |
|
|
4610
|
+
| `leverPx` | the widest frame-pixel span between two determinant pivots — what the rotation was read *across*. A short lever turns a half-pixel anchor error into several degrees |
|
|
4611
|
+
| `minLeverPx` | the floor that span had to clear (`--inward-lever`, default `8`) |
|
|
4612
|
+
| `disagreementPx` | the worst `offsetPx`: **the over-determination residual**, in frame pixels. `null` at `redundancy 0` |
|
|
4613
|
+
| `rejected[]` | anchored descendants that could **not** be used, each with the reason — named rather than silently dropped |
|
|
4614
|
+
|
|
4615
|
+
🚨 **At `redundancy 0`, `disagreementPx` is `null` rather than `0`, and the
|
|
4616
|
+
difference is the whole point.** Two determinants supply exactly four numbers, so
|
|
4617
|
+
the solve fits its own two points exactly *whether or not your rig is right*. A
|
|
4618
|
+
zero there would be a measurement of nothing. One more anchored descendant on a
|
|
4619
|
+
third sub-chain is what makes a determination checkable at all — and that is the
|
|
4620
|
+
same philosophy as `pivotDisagreementPx`: the residual is not an error bar, it is
|
|
4621
|
+
your rig's joint offsets and the frame disagreeing by that much.
|
|
4622
|
+
|
|
4623
|
+
🚨 **And `disagreementPx` says the determination disagrees with itself. It does
|
|
4624
|
+
NOT say which determinant is wrong.** The solve is least squares, so a
|
|
4625
|
+
displacement on one anchor spreads across every determinant near it. Measured on
|
|
4626
|
+
the chain-fit fixture, a deliberate 4 px error on one child came back as **1.95 px
|
|
4627
|
+
on a different child** and 1.74 px on the one that was moved, because those two
|
|
4628
|
+
sit 5 bone units apart while the third is 24 away — nothing in the arithmetic can
|
|
4629
|
+
tell a tight pair apart. Read the per-determinant `offsetPx` list as a *pattern*,
|
|
4630
|
+
and attribute with a second frame or with `pivotDisagreementPx` on the anchors
|
|
4631
|
+
themselves.
|
|
4632
|
+
|
|
4633
|
+
**What the step cannot see, stated as refusals it makes by name.**
|
|
4634
|
+
|
|
4635
|
+
- **`no-bracket`, one determinant.** Four numbers need four equations. The detail
|
|
4636
|
+
names the one it found.
|
|
4637
|
+
- **`no-bracket`, an unusable path.** A bone strictly between the two carries art
|
|
4638
|
+
(its hinge is a searched unknown), or your rig leaves its **scale** free (the
|
|
4639
|
+
*distance* across it is unknown), or its geometry is not a similarity at all.
|
|
4640
|
+
Each is named with the bone. ⚠️ `--stretch` frees every bone's scale, so it can
|
|
4641
|
+
turn a working bracket into a refused one — that is the flag telling the truth
|
|
4642
|
+
about what it made unknown.
|
|
4643
|
+
- **`no-bracket`, below the lever floor.** Two coincident pivots fix no direction.
|
|
4644
|
+
Nothing is printed here, unlike `occluded`: a rotation with no baseline is not a
|
|
4645
|
+
worse placement, it is not a placement.
|
|
4646
|
+
- **`no-anchor` still means what it meant** — nothing trusted on this limb, above
|
|
4647
|
+
it *or below it*. The split matters because the repair does: `no-bracket` wants
|
|
4648
|
+
one more anchor on a different sub-chain, `no-anchor` wants a better anchor pass.
|
|
4649
|
+
- **Determinants are ANCHORED bones and nothing else** (`inward.criterion.determinantsMustBeAnchored`).
|
|
4650
|
+
A bone the outward walk placed is sitting at whatever hinge your setup declares
|
|
4651
|
+
until it is fitted, and reading that as evidence would compound a guess into a
|
|
4652
|
+
placement.
|
|
4653
|
+
- **An anchored bone is never determined inward**, and a fitted hinge is never
|
|
4654
|
+
replaced by a geometric one. The step only reaches bones the outward walk left
|
|
4655
|
+
unplaced.
|
|
4656
|
+
|
|
4657
|
+
🚫 **An `inward` placement is not a measurement of the bone it places.** Its
|
|
4658
|
+
evidence lives on the anchors below it. The bone's own `residual` and
|
|
4659
|
+
`visibleShare` say how much of the answer *the frame can independently confirm* —
|
|
4660
|
+
and **the visibility floor still refuses it** when the answer is one nothing in
|
|
4661
|
+
the picture can check. That is deliberate, and it is the second time this call has
|
|
4662
|
+
been made in `chainfit`: exempting a placement nothing searched was tried for
|
|
4663
|
+
anchors and reverted, because it prints a part nobody can see as READ. The floor
|
|
4664
|
+
is about what the picture can confirm, not about how the number was arrived at.
|
|
4665
|
+
|
|
4508
4666
|
### 12.3 What the report adds to a `pose` report
|
|
4509
4667
|
|
|
4510
4668
|
The coordinate contract is **identical** to §11.2 — frame pixels, y down, origin
|
|
@@ -4515,27 +4673,64 @@ placement:
|
|
|
4515
4673
|
| Field | Meaning |
|
|
4516
4674
|
| --- | --- |
|
|
4517
4675
|
| `residual` | the same objective as §11, over the part's **visible** pixels only: covered pixels are dropped from both sums rather than charged. **Not the same number as `pose`'s on an occluded part**, and never to be read without the next field |
|
|
4518
|
-
| `visibleShare` | the share of the part's own alpha weight the residual was computed on. A low residual on a `0.08` share is a confident statement about a sliver |
|
|
4676
|
+
| `visibleShare` | the share of the part's own alpha weight the residual was computed on. A low residual on a `0.08` share is a confident statement about a sliver. ⚠️ **Per frame, not per corpus** — see the caution below the table |
|
|
4519
4677
|
| `scoredPixels` | how many part pixels that share actually is |
|
|
4520
|
-
| `visibleShareAtFit` | the share recomputed **where the answer landed**, rather than where the visible set was frozen. Far from `visibleShare` means the fit moved out of its own measurement; `--passes` is the repair |
|
|
4678
|
+
| `visibleShareAtFit` | the share recomputed **where the answer landed**, rather than where the visible set was frozen. Far from `visibleShare` means the fit moved out of its own measurement; `--passes` is the repair. ⚠️ Not the steadier of the two — measured, below |
|
|
4521
4679
|
| `hingeDeg` | ⭐ the searched degree of freedom, in **Spine** degrees relative to the bone's setup rotation — **the value a `rotate` key would carry**. `null` on an anchor whose own parent is unplaced, where the quantity does not exist |
|
|
4522
4680
|
| `localRotationDeg` | the bone's local rotation this implies, Spine degrees. The other half of the same answer |
|
|
4523
4681
|
| `stretch` | the uniform scale on the bone; `1` where that DOF was not free |
|
|
4524
4682
|
| `unexplained`, `offCanvas`, `footprint`, `bbox` | as §11.3, with `residual` and `unexplained` over the visible set and `offCanvas` over the whole part |
|
|
4525
4683
|
|
|
4684
|
+
🚨 **`visibleShare` is a per-frame diagnostic. Do not take a median or a mean of it
|
|
4685
|
+
across frames without naming the fit.** Both halves of the fraction are downstream
|
|
4686
|
+
of the fit — the numerator because the occluders are stamped from wherever the
|
|
4687
|
+
later-drawn parts *currently sit* — so the quantity is fit-relative by
|
|
4688
|
+
construction, and measurably so. Measured over all 147 committed `ess` frames of
|
|
4689
|
+
`bench/reference/spineboy`, perturbing the anchor placements **inside `pose`'s own
|
|
4690
|
+
convergence band** (`src/pose.ts`'s level-0 polish `floor`: 0.05 px, 0.1°, 0.1 %
|
|
4691
|
+
scale — below which the fitter stops looking, so it cannot tell the two fits apart):
|
|
4692
|
+
|
|
4693
|
+
| | |
|
|
4694
|
+
| --- | ---: |
|
|
4695
|
+
| median \|Δ`visibleShare`\| | 0.0005 |
|
|
4696
|
+
| **p99** | **0.5592** |
|
|
4697
|
+
| max | 0.9401 |
|
|
4698
|
+
| readings that move by more than 0.10 | **5.71 %** |
|
|
4699
|
+
| the **corpus median** of one part's share, worst case | **0.3055 ↔ 0.5228** (`rear-foot`) |
|
|
4700
|
+
|
|
4701
|
+
⚠️ **The distribution is bimodal, so the median of the swing is not a summary of
|
|
4702
|
+
it.** A reading is either exact to four decimals or somewhere else entirely, and
|
|
4703
|
+
which of the two a part is in **cannot be told by looking at the row**:
|
|
4704
|
+
`front-bracer` sits at a comfortable 0.73 and its corpus median moves 17 points,
|
|
4705
|
+
while `rear-bracer` sits at a 0.015 sliver and moves 0.0005.
|
|
4706
|
+
|
|
4707
|
+
📌 **It is not a definitional edge, and `visibleShareAtFit` is not a way out.** Of the
|
|
4708
|
+
cells that swing by more than 0.10, **63 %** are the part's own placement having
|
|
4709
|
+
travelled more than a pixel or turned more than `AMBIGUITY_HINGE_DEG`, **32 %** are
|
|
4710
|
+
an occluder having relocated (median 42.6 px), and **0.5 %** are the mask changing
|
|
4711
|
+
while everything on the frame stood still. `visibleShareAtFit` is measured where
|
|
4712
|
+
the answer landed and is *less* steady, not more (p99 0.6588 against 0.5592). ⇒
|
|
4713
|
+
**The field is reporting a bistable fit faithfully.** What is safe: reading it beside
|
|
4714
|
+
its own residual, on its own frame — which is what it exists for. What is safe as a
|
|
4715
|
+
corpus statistic: a **maximum**, which saturates. Full method and evidence:
|
|
4716
|
+
[`bench/studies/2026-09-03-visibleshare`](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
|
|
4717
|
+
([#323](https://github.com/firejune/rigc/issues/323)).
|
|
4718
|
+
|
|
4526
4719
|
And per part:
|
|
4527
4720
|
|
|
4528
4721
|
| Field | Meaning |
|
|
4529
4722
|
| --- | --- |
|
|
4530
|
-
| `role` | `anchor` (taken from the anchor pass, not re-fitted), `chain` (fitted through the rig), `unplaced` |
|
|
4531
|
-
| — | ⭐ **A refused ANCHOR is not a contradiction, and it is the most useful row in the table.** The anchor pass judged that placement over the part's *whole* footprint — all `pose` can see, and blind to what covers it — while this instrument has just measured how much of the part is visible at all. Both readings are true. A refused anchor means *the placement may well be right and the confirmation is missing*, and every part whose `anchoredTo` names that bone rests on it. Measured on the 2026-09-03 corpus, `rear-bracer` clears `pose`'s criterion on 81 of 147 frames at a median visible share of **0.1%** — suppressing the refusal there was tried and prints that as READ. ⚠️ **That pair of numbers is on the pre-[#306](https://github.com/firejune/rigc/issues/306) objective** and has not been re-derived: the study is the 2026-09-03 run's own, over its own candidate, and re-running it is a run-scale job rather than a docs edit. A four-frame spot check under #306 (`--min-visible 0`, committed `ess` frames `idle/f0000`, `run/f0002`, `walk/f0004`, `aim/f0000`) moved `rear-bracer`'s anchor residual *down* on all four — 0.1564→0.1555, 0.1519→0.1513, 0.1493→0.1488, 0.1537→0.1534 — and flipped its eligibility on none, while `visibleShare` moved materially on one of the four (0.36→0.89). ⇒ Read the **shape** of the row, not those two digits |
|
|
4723
|
+
| `role` | `anchor` (taken from the anchor pass, not re-fitted), `chain` (fitted through the rig), `inward` (**determined** from two or more anchored descendants, with nothing searched — §12.2b), `unplaced` |
|
|
4724
|
+
| — | ⭐ **A refused ANCHOR is not a contradiction, and it is the most useful row in the table.** The anchor pass judged that placement over the part's *whole* footprint — all `pose` can see, and blind to what covers it — while this instrument has just measured how much of the part is visible at all. Both readings are true. A refused anchor means *the placement may well be right and the confirmation is missing*, and every part whose `anchoredTo` names that bone rests on it. Measured on the 2026-09-03 corpus, `rear-bracer` clears `pose`'s criterion on 81 of 147 frames at a median visible share of **0.1%** — suppressing the refusal there was tried and prints that as READ. ⚠️ **That pair of numbers is on the pre-[#306](https://github.com/firejune/rigc/issues/306) objective** and has not been re-derived: the study is the 2026-09-03 run's own, over its own candidate, and re-running it is a run-scale job rather than a docs edit. A four-frame spot check under #306 (`--min-visible 0`, committed `ess` frames `idle/f0000`, `run/f0002`, `walk/f0004`, `aim/f0000`) moved `rear-bracer`'s anchor residual *down* on all four — 0.1564→0.1555, 0.1519→0.1513, 0.1493→0.1488, 0.1537→0.1534 — and flipped its eligibility on none, while `visibleShare` moved materially on one of the four (0.36→0.89). ⇒ Read the **shape** of the row, not those two digits. 🚨 **And that median in particular is one of the ones the 2026-09-03 study found unsafe**: `rear-bracer`'s share swings by **0.87** on `idle/f0001` inside `pose`'s own convergence band when the chain anchors on `pose`'s criterion — the basis this figure was taken on. The count and the shape are the reading; the 0.1 % is not a number |
|
|
4532
4725
|
| — | The **other** parts on an anchored bone are refused on their own numbers too, and there they mean something different again: their placement is the **rig's** prediction from that anchor, so their residual is a measurement of the rig (a goggle plate that will not sit on the head it is parented to shows up exactly here) |
|
|
4533
4726
|
| `bone` | the bone this hangs off: its `parent`, its `setupRotationDeg`, its `depth` in links from the anchor, `anchoredTo`, the `dof` searched, the `window` taken, the other parts `sharedWith` it on that bone, and `carriedBones` |
|
|
4534
4727
|
| `bone.dof.pivotFree` | your candidate keys a `translate` timeline on this bone, so the arc this answer sits on has a centre the rig itself moves. The placement is still read off pixels; `localRotationDeg` alone will not reproduce it |
|
|
4535
4728
|
| `bone.carriedBones` | bones between the anchor and here that carry nothing scoreable. Their hinge could not be fitted, their setup rotation was carried through, and every number below them inherits that |
|
|
4536
4729
|
| `bone.pivotDisagreementPx` | anchored bones only: how far the chain's own prediction of this bone's pivot is from where the anchor put it. **This is the one direct measurement of your rig against the picture** — a large value says the joint offset you declared is not the joint the frame shows |
|
|
4537
4730
|
| `anchorVerdict` | what the anchor pass made of this same part: `residual`, `unexplained`, `ambiguous`, `eligible`. ⭐ `eligible: false` beside a `chain` placement is **a part the chain bought** |
|
|
4538
|
-
| `
|
|
4731
|
+
| `bone.anchoredToRole` | whether the trunk this hangs off was `anchor` (read off the picture) or `inward` (determined from two anchors below it). ⭐ **The field to check before quoting anything hung off an inward trunk** — that subtree inherits the determination's own uncertainty, and `bone.inward.disagreementPx` on the trunk bone is where it is priced |
|
|
4732
|
+
| `bone.inward` | non-`null` only on a bone determined inward, and then it is the whole account of that determination — see §12.2b |
|
|
4733
|
+
| `refusal` | `{ reason, detail }` or `null`. Reasons: `occluded`, `no-match`, `no-anchor`, `no-bracket` (§12.2b), `empty-part`, `no-part-image`, `unsupported-geometry` |
|
|
4539
4734
|
|
|
4540
4735
|
`⚠️ --images is not a part list here.` For `pose` every `.png` in the directory is
|
|
4541
4736
|
a part; for `chainfit` **the candidate decides what the parts are** and the
|
|
@@ -4550,10 +4745,11 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
|
|
|
4550
4745
|
| `--atlas` | **refused by name.** Every other `--candidate` command takes it, so trying it here is reasonable — but the part art comes from `--images` and the skeleton is all this needs of the candidate, so a flag that silently did nothing would be worse than one that says why |
|
|
4551
4746
|
| `--hinge <min,max>` | the window each child's local rotation is searched over, in Spine degrees about its setup value. Default `-180,180` — **a full turn, on purpose**: one degree of freedom is cheap enough to sweep exhaustively, and §11.4's warning about a window that does not contain the truth applies here too |
|
|
4552
4747
|
| `--stretch <ratio>` | also search a uniform bone scale, this ratio either way. Without it, stretch is searched **only where your own animations key a `scale` timeline on that bone** — a rig that never scales a bone is a rig saying that bone does not stretch |
|
|
4553
|
-
| `--min-visible <0..1>` | below this visible share a placement is refused `occluded` instead of reported flat (default `0.25`). A reporting threshold, not a pass bar; the placement is still in the JSON |
|
|
4748
|
+
| `--min-visible <0..1>` | below this visible share a placement is refused `occluded` instead of reported flat (default `0.25`). A reporting threshold, not a pass bar; the placement is still in the JSON. ⚠️ **It is not inert, though**: a bone whose frozen share is under this floor gets one *unmasked* look before its visible set is fixed, so the flag also changes **where parts land** and not only which rows are refused. Two runs at different `--min-visible` are two fits, and their shares are not one column |
|
|
4554
4749
|
| `--max-residual <0..1>` | as §11, over the visible pixels (default `0.25`) |
|
|
4555
|
-
| `--passes <n>` | how many times the masks are rebuilt from the answers and the fit rerun (default `2`) |
|
|
4750
|
+
| `--passes <n>` | how many times the masks are rebuilt from the answers and the fit rerun (default `2`). ⚠️ **It buys convergence of the mask onto the answer, and it costs determinacy.** Each pass re-seeds the hinge search on the previous pass's answer, so two nearby inputs that fell into different basins on pass 1 are *further* apart after pass 2. Measured over the 147 committed `ess` frames, the share of readings whose `visibleShare` moves by more than 0.10 under a perturbation inside `pose`'s convergence band runs **0.78 % → 5.45 % → 15.17 %** at `--passes` 1 → 2 → 4. Raise it to settle a `visibleShareAtFit` drift on one frame; do not raise it expecting steadier numbers across runs |
|
|
4556
4751
|
| `--anchor-residual <0..1>` | the residual a `pose` placement must be within to anchor (default `0.16`) |
|
|
4752
|
+
| `--inward-lever <px>` | how far apart two anchored descendants must sit before the rotation they determine is printed (default `8`). Derived, not picked: a pivot error of ε px across a lever of L px is an angle error of about ε/L radians, so half a pixel inside 3° needs 9.5 px. Below it, `no-bracket` names the measured lever |
|
|
4557
4753
|
| `--scale`, `--rotation` | passed to the **internal anchor pass**, meaning exactly what they mean to `pose` |
|
|
4558
4754
|
|
|
4559
4755
|
### 12.5 What it cannot see — read this before using the numbers
|
|
@@ -4569,6 +4765,15 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
|
|
|
4569
4765
|
measurement on an occluded part**, by construction: one drops the covered pixels
|
|
4570
4766
|
and the other charges them. Do not put them in one column. What *is* comparable
|
|
4571
4767
|
is each against its own `visibleShare` / `unexplained`.
|
|
4768
|
+
- 🚨 **`visibleShare` cannot be averaged across frames or across fits.** It is
|
|
4769
|
+
measured through the fitted placements of the parts drawn over this one, so two
|
|
4770
|
+
fits `pose` itself cannot tell apart give it different values — measured: p99
|
|
4771
|
+
0.56 and a worst corpus-median move of 22 points inside `pose`'s own polish
|
|
4772
|
+
floor. Read it per frame beside its own residual. If a corpus statistic is
|
|
4773
|
+
needed, a **maximum** is the one that survives (it saturates), and it should be
|
|
4774
|
+
quoted with its spread. §12.3 carries the figures;
|
|
4775
|
+
[`bench/studies/2026-09-03-visibleshare`](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
|
|
4776
|
+
carries the method.
|
|
4572
4777
|
- ⚠️ **Setup draw order, on one frame.** A `drawOrder` timeline reorders your slots
|
|
4573
4778
|
at runtime and this cannot know the time, so a candidate that has one is masked in
|
|
4574
4779
|
the order its setup pose declares. The report says so in `caveats` when it finds
|
|
@@ -4576,6 +4781,9 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
|
|
|
4576
4781
|
- ⚠️ **The hinge is searched; the pivot is not.** Nothing here searches a bone's
|
|
4577
4782
|
translation, so a bone you key `translate` on is reported `pivotFree` rather than
|
|
4578
4783
|
solved.
|
|
4784
|
+
- ⬆️ **The inward step determines a bone; it does not measure one.** §12.2b is the
|
|
4785
|
+
whole account. The two things it structurally cannot do: attribute a
|
|
4786
|
+
disagreement to one determinant, and reach a bone that does not branch.
|
|
4579
4787
|
- **A constraint moves bones after their local transforms compose.** With IK,
|
|
4580
4788
|
transform, path or physics constraints in the candidate, a fitted
|
|
4581
4789
|
`localRotationDeg` is still a placement but not necessarily a value you can key
|