spine-rigc 0.14.0 → 0.14.1

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 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 — four complete rigs over art that ships with them
389
+ ## The gallery — five 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,17 @@ 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
+
406
+ <p align="center">
407
+ <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" />
408
+ </p>
409
+
410
+ <p align="center"><em>The portrait rig playing its three animations in one take — the turn is
411
+ the shot: both silhouette edges move apart, which a flat slide cannot do, because every
412
+ feature carries its own depth. Scene direction of this kind was Live2D's territory; the
413
+ split was authoring cost, and the cost is now one stated expression per key. Compiled and
414
+ rendered entirely by the published package.</em></p>
404
415
 
405
416
  ## Commands
406
417
 
@@ -419,7 +430,7 @@ commands take it and what its default is.
419
430
  | `preview --candidate <dir>` | one self-contained `.html` that plays it |
420
431
  | `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
421
432
  | `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 |
433
+ | `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
434
  | `diff <candidate.json> <reference.json>` | structural comparison of two skeletons, one ratio per measure and deliberately no combined score |
424
435
  | `check --candidate <dir> --frames <dir>` | the candidate against reference pictures — the only instrument here that can see a *wrong animation* |
425
436
  | `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
@@ -447,6 +458,7 @@ the art. Its shape is under
447
458
  | Document | For |
448
459
  | --- | --- |
449
460
  | 📘 **[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` |
461
+ | 🦴 **[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
462
  | 🎞️ **[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
463
  | 🙂 **[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
464
  | 📥 **[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.
@@ -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
@@ -4345,7 +4353,9 @@ themselves**, downward and by small amounts, most on the parts whose edges are i
4345
4353
  the frame. ⇒ A residual measured before that date and one measured after are not
4346
4354
  the same measurement; re-read a frame rather than comparing across it. The worked
4347
4355
  figures live in [MOTION.md](MOTION.md) §6 and
4348
- [BENCHMARK.md](BENCHMARK.md), each with its own re-baseline note.
4356
+ [BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)
4357
+ repository material, not shipped in the package — each with its own re-baseline
4358
+ note.
4349
4359
 
4350
4360
  ### 11.1 It measures an input, so nothing here is a score
4351
4361
 
@@ -4460,7 +4470,11 @@ two things.
4460
4470
  apologised for. The parts drawn after a part are what covers it, and the pixels
4461
4471
  they cover are **excluded** from that part's objective rather than charged to it.
4462
4472
  Every residual here is over the part's **visible** pixels, and every one comes
4463
- with the `visibleShare` it was computed on.
4473
+ with the `visibleShare` it was computed on. ⚠️ **`visibleShare` is a per-frame
4474
+ diagnostic and not a summary statistic** — it is measured through the fitted
4475
+ placements, so a median or a mean of it is not comparable across fits. §12.3 says
4476
+ what that costs and [the 2026-09-03 study](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
4477
+ measures it — repository material, not shipped in the package.
4464
4478
  - **Hierarchy and attachment geometry**, so the search collapses. A child bone
4465
4479
  whose parent is already placed does not have four degrees of freedom: the rig
4466
4480
  fixes its pivot, so what is left is **one hinge** about that pivot — plus a
@@ -4500,11 +4514,96 @@ the four a similarity has — and every descendant then follows from the rig. A
4500
4514
  **above** an anchor does not: recovering it would need to know what the link
4501
4515
  between them did, and that is precisely the unknown the anchor does not carry.
4502
4516
 
4503
- ⚠️ **So a limb with no trusted part on it or above it is refused `no-anchor`**, not
4504
- guessed at from a cousin. If a whole side of your figure comes back that way, the
4505
- repair is upstream: give `pose` a better frame, pin its `--scale`, or loosen
4517
+ ⚠️ **So a limb with no trusted part on it or above it is refused**, not guessed at
4518
+ from a cousin. If a whole side of your figure comes back that way, the repair is
4519
+ upstream: give `pose` a better frame, pin its `--scale`, or loosen
4506
4520
  `--anchor-residual` deliberately and read the consequences.
4507
4521
 
4522
+ ### 12.2b The inward step — two anchors bracket the bone between them
4523
+
4524
+ ⬆️ There is **one exception** to the paragraph above, and it is one shape.
4525
+
4526
+ One anchored descendant says nothing about the link above it — that is the
4527
+ sentence you just read, and it is still true. **Two** of them say something else
4528
+ entirely. A bone's world placement is four numbers, and a descendant's **pivot**
4529
+ depends on that bone and on your rig's own offsets and **not** on the
4530
+ descendant's own hinge. So each anchored descendant contributes two equations,
4531
+ two of them make four, and four equations fix four numbers. That is the whole
4532
+ geometry; there is no search and no window.
4533
+
4534
+ Such a bone comes back with **`role: "inward"`** and a `bone.inward` block, and
4535
+ the outward walk then **resumes from it** — so a subtree that was refused a moment
4536
+ ago is fitted normally, one hinge per link, with `bone.anchoredToRole` on every
4537
+ one of those placements reading `"inward"` to say what it rests on.
4538
+
4539
+ ⚠️ **Which means the inward step reaches exactly the bones that BRANCH.** A bone
4540
+ whose children form a single sub-chain can never be determined, however good the
4541
+ anchor below it is: two equations, four unknowns. That is `no-bracket`, and it is
4542
+ a fact about your rig's topology rather than about the frame. On the 2026-09-03
4543
+ spineboy candidate `torso` is the only bone in the whole rig that branches — and
4544
+ it is the bone that recorded 30 `no-anchor` frames, which is why this exists.
4545
+
4546
+ | Field on `bone.inward` | Meaning |
4547
+ | --- | --- |
4548
+ | `form` | `descendants` — the only form there is. Named so a future one is readable beside it |
4549
+ | `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 |
4550
+ | `redundancy` | equations beyond the four a similarity needs, `2 × determinants − 4`. **Read `disagreementPx` next to this and never without it** |
4551
+ | `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 |
4552
+ | `minLeverPx` | the floor that span had to clear (`--inward-lever`, default `8`) |
4553
+ | `disagreementPx` | the worst `offsetPx`: **the over-determination residual**, in frame pixels. `null` at `redundancy 0` |
4554
+ | `rejected[]` | anchored descendants that could **not** be used, each with the reason — named rather than silently dropped |
4555
+
4556
+ 🚨 **At `redundancy 0`, `disagreementPx` is `null` rather than `0`, and the
4557
+ difference is the whole point.** Two determinants supply exactly four numbers, so
4558
+ the solve fits its own two points exactly *whether or not your rig is right*. A
4559
+ zero there would be a measurement of nothing. One more anchored descendant on a
4560
+ third sub-chain is what makes a determination checkable at all — and that is the
4561
+ same philosophy as `pivotDisagreementPx`: the residual is not an error bar, it is
4562
+ your rig's joint offsets and the frame disagreeing by that much.
4563
+
4564
+ 🚨 **And `disagreementPx` says the determination disagrees with itself. It does
4565
+ NOT say which determinant is wrong.** The solve is least squares, so a
4566
+ displacement on one anchor spreads across every determinant near it. Measured on
4567
+ the chain-fit fixture, a deliberate 4 px error on one child came back as **1.95 px
4568
+ on a different child** and 1.74 px on the one that was moved, because those two
4569
+ sit 5 bone units apart while the third is 24 away — nothing in the arithmetic can
4570
+ tell a tight pair apart. Read the per-determinant `offsetPx` list as a *pattern*,
4571
+ and attribute with a second frame or with `pivotDisagreementPx` on the anchors
4572
+ themselves.
4573
+
4574
+ **What the step cannot see, stated as refusals it makes by name.**
4575
+
4576
+ - **`no-bracket`, one determinant.** Four numbers need four equations. The detail
4577
+ names the one it found.
4578
+ - **`no-bracket`, an unusable path.** A bone strictly between the two carries art
4579
+ (its hinge is a searched unknown), or your rig leaves its **scale** free (the
4580
+ *distance* across it is unknown), or its geometry is not a similarity at all.
4581
+ Each is named with the bone. ⚠️ `--stretch` frees every bone's scale, so it can
4582
+ turn a working bracket into a refused one — that is the flag telling the truth
4583
+ about what it made unknown.
4584
+ - **`no-bracket`, below the lever floor.** Two coincident pivots fix no direction.
4585
+ Nothing is printed here, unlike `occluded`: a rotation with no baseline is not a
4586
+ worse placement, it is not a placement.
4587
+ - **`no-anchor` still means what it meant** — nothing trusted on this limb, above
4588
+ it *or below it*. The split matters because the repair does: `no-bracket` wants
4589
+ one more anchor on a different sub-chain, `no-anchor` wants a better anchor pass.
4590
+ - **Determinants are ANCHORED bones and nothing else** (`inward.criterion.determinantsMustBeAnchored`).
4591
+ A bone the outward walk placed is sitting at whatever hinge your setup declares
4592
+ until it is fitted, and reading that as evidence would compound a guess into a
4593
+ placement.
4594
+ - **An anchored bone is never determined inward**, and a fitted hinge is never
4595
+ replaced by a geometric one. The step only reaches bones the outward walk left
4596
+ unplaced.
4597
+
4598
+ 🚫 **An `inward` placement is not a measurement of the bone it places.** Its
4599
+ evidence lives on the anchors below it. The bone's own `residual` and
4600
+ `visibleShare` say how much of the answer *the frame can independently confirm* —
4601
+ and **the visibility floor still refuses it** when the answer is one nothing in
4602
+ the picture can check. That is deliberate, and it is the second time this call has
4603
+ been made in `chainfit`: exempting a placement nothing searched was tried for
4604
+ anchors and reverted, because it prints a part nobody can see as READ. The floor
4605
+ is about what the picture can confirm, not about how the number was arrived at.
4606
+
4508
4607
  ### 12.3 What the report adds to a `pose` report
4509
4608
 
4510
4609
  The coordinate contract is **identical** to §11.2 — frame pixels, y down, origin
@@ -4515,27 +4614,64 @@ placement:
4515
4614
  | Field | Meaning |
4516
4615
  | --- | --- |
4517
4616
  | `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 |
4617
+ | `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
4618
  | `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 |
4619
+ | `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
4620
  | `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
4621
  | `localRotationDeg` | the bone's local rotation this implies, Spine degrees. The other half of the same answer |
4523
4622
  | `stretch` | the uniform scale on the bone; `1` where that DOF was not free |
4524
4623
  | `unexplained`, `offCanvas`, `footprint`, `bbox` | as §11.3, with `residual` and `unexplained` over the visible set and `offCanvas` over the whole part |
4525
4624
 
4625
+ 🚨 **`visibleShare` is a per-frame diagnostic. Do not take a median or a mean of it
4626
+ across frames without naming the fit.** Both halves of the fraction are downstream
4627
+ of the fit — the numerator because the occluders are stamped from wherever the
4628
+ later-drawn parts *currently sit* — so the quantity is fit-relative by
4629
+ construction, and measurably so. Measured over all 147 committed `ess` frames of
4630
+ `bench/reference/spineboy`, perturbing the anchor placements **inside `pose`'s own
4631
+ convergence band** (`src/pose.ts`'s level-0 polish `floor`: 0.05 px, 0.1°, 0.1 %
4632
+ scale — below which the fitter stops looking, so it cannot tell the two fits apart):
4633
+
4634
+ | | |
4635
+ | --- | ---: |
4636
+ | median \|Δ`visibleShare`\| | 0.0005 |
4637
+ | **p99** | **0.5592** |
4638
+ | max | 0.9401 |
4639
+ | readings that move by more than 0.10 | **5.71 %** |
4640
+ | the **corpus median** of one part's share, worst case | **0.3055 ↔ 0.5228** (`rear-foot`) |
4641
+
4642
+ ⚠️ **The distribution is bimodal, so the median of the swing is not a summary of
4643
+ it.** A reading is either exact to four decimals or somewhere else entirely, and
4644
+ which of the two a part is in **cannot be told by looking at the row**:
4645
+ `front-bracer` sits at a comfortable 0.73 and its corpus median moves 17 points,
4646
+ while `rear-bracer` sits at a 0.015 sliver and moves 0.0005.
4647
+
4648
+ 📌 **It is not a definitional edge, and `visibleShareAtFit` is not a way out.** Of the
4649
+ cells that swing by more than 0.10, **63 %** are the part's own placement having
4650
+ travelled more than a pixel or turned more than `AMBIGUITY_HINGE_DEG`, **32 %** are
4651
+ an occluder having relocated (median 42.6 px), and **0.5 %** are the mask changing
4652
+ while everything on the frame stood still. `visibleShareAtFit` is measured where
4653
+ the answer landed and is *less* steady, not more (p99 0.6588 against 0.5592). ⇒
4654
+ **The field is reporting a bistable fit faithfully.** What is safe: reading it beside
4655
+ its own residual, on its own frame — which is what it exists for. What is safe as a
4656
+ corpus statistic: a **maximum**, which saturates. Full method and evidence:
4657
+ [`bench/studies/2026-09-03-visibleshare`](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
4658
+ ([#323](https://github.com/firejune/rigc/issues/323)).
4659
+
4526
4660
  And per part:
4527
4661
 
4528
4662
  | Field | Meaning |
4529
4663
  | --- | --- |
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 |
4664
+ | `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` |
4665
+ | — | ⭐ **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
4666
  | — | 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
4667
  | `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
4668
  | `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
4669
  | `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
4670
  | `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
4671
  | `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
- | `refusal` | `{ reason, detail }` or `null`. Reasons: `occluded`, `no-match`, `no-anchor`, `empty-part`, `no-part-image`, `unsupported-geometry` |
4672
+ | `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 |
4673
+ | `bone.inward` | non-`null` only on a bone determined inward, and then it is the whole account of that determination — see §12.2b |
4674
+ | `refusal` | `{ reason, detail }` or `null`. Reasons: `occluded`, `no-match`, `no-anchor`, `no-bracket` (§12.2b), `empty-part`, `no-part-image`, `unsupported-geometry` |
4539
4675
 
4540
4676
  `⚠️ --images is not a part list here.` For `pose` every `.png` in the directory is
4541
4677
  a part; for `chainfit` **the candidate decides what the parts are** and the
@@ -4550,10 +4686,11 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
4550
4686
  | `--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
4687
  | `--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
4688
  | `--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 |
4689
+ | `--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
4690
  | `--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`) |
4691
+ | `--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
4692
  | `--anchor-residual <0..1>` | the residual a `pose` placement must be within to anchor (default `0.16`) |
4693
+ | `--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
4694
  | `--scale`, `--rotation` | passed to the **internal anchor pass**, meaning exactly what they mean to `pose` |
4558
4695
 
4559
4696
  ### 12.5 What it cannot see — read this before using the numbers
@@ -4569,6 +4706,15 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
4569
4706
  measurement on an occluded part**, by construction: one drops the covered pixels
4570
4707
  and the other charges them. Do not put them in one column. What *is* comparable
4571
4708
  is each against its own `visibleShare` / `unexplained`.
4709
+ - 🚨 **`visibleShare` cannot be averaged across frames or across fits.** It is
4710
+ measured through the fitted placements of the parts drawn over this one, so two
4711
+ fits `pose` itself cannot tell apart give it different values — measured: p99
4712
+ 0.56 and a worst corpus-median move of 22 points inside `pose`'s own polish
4713
+ floor. Read it per frame beside its own residual. If a corpus statistic is
4714
+ needed, a **maximum** is the one that survives (it saturates), and it should be
4715
+ quoted with its spread. §12.3 carries the figures;
4716
+ [`bench/studies/2026-09-03-visibleshare`](https://github.com/firejune/rigc/blob/main/bench/studies/2026-09-03-visibleshare/README.md)
4717
+ carries the method.
4572
4718
  - ⚠️ **Setup draw order, on one frame.** A `drawOrder` timeline reorders your slots
4573
4719
  at runtime and this cannot know the time, so a candidate that has one is masked in
4574
4720
  the order its setup pose declares. The report says so in `caveats` when it finds
@@ -4576,6 +4722,9 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
4576
4722
  - ⚠️ **The hinge is searched; the pivot is not.** Nothing here searches a bone's
4577
4723
  translation, so a bone you key `translate` on is reported `pivotFree` rather than
4578
4724
  solved.
4725
+ - ⬆️ **The inward step determines a bone; it does not measure one.** §12.2b is the
4726
+ whole account. The two things it structurally cannot do: attribute a
4727
+ disagreement to one determinant, and reach a bone that does not branch.
4579
4728
  - **A constraint moves bones after their local transforms compose.** With IK,
4580
4729
  transform, path or physics constraints in the candidate, a fitted
4581
4730
  `localRotationDeg` is still a placement but not necessarily a value you can key
package/docs/FACE.md CHANGED
@@ -25,6 +25,10 @@ hold its results.
25
25
  replaces a person's eye
26
26
  - A skeleton somebody else authored, and moving a pivot inside it:
27
27
  [INGEST.md](INGEST.md)
28
+ - The hierarchy underneath a face, as a general rule rather than this closed form:
29
+ [RIGGING.md](RIGGING.md) — §5 is why §3's `faceshift` is a bone at all and why a
30
+ breath's `chest` is a **sibling** of the plate it must not scale, and §4.4 is the
31
+ artless-parent pattern the shared-shift split is one instance of
28
32
  - The worked example every number below comes from:
29
33
  [`gallery/portrait`](https://github.com/firejune/rigc/tree/main/gallery/portrait),
30
34
  and its measurement half,
package/docs/INGEST.md CHANGED
@@ -35,6 +35,9 @@ invent one.
35
35
  - The coordinate contract, in one place: **AUTHORING §11.2** and
36
36
  [`src/transform.ts`](../src/transform.ts)
37
37
  - What the editor does when nobody tells it otherwise: **AUTHORING §10**
38
+ - Why the re-pivot in **§4.1** is shaped the way it is, and the child-bone row it
39
+ warns about worked on a bone that actually has one: [RIGGING.md](RIGGING.md) §3.
40
+ Its §2 is how to tell whether the pivot you are moving *to* is identified at all
38
41
  - What the format holds and what rigc covers, skeleton by skeleton:
39
42
  [SPEC_COVERAGE.md](SPEC_COVERAGE.md)
40
43
  - If you are the *person operating* an agent rather than the agent:
package/docs/MOTION.md CHANGED
@@ -25,6 +25,11 @@ toolchain and this page does not invent one.
25
25
  - The parts of that picture `pose` refuses because something is drawn over them,
26
26
  once a first candidate exists: **AUTHORING §12** (`rigc chainfit`). It reports the
27
27
  `rotate` key value each answer implies, which is the form this recipe wants them in
28
+ - If the **skeleton** is what you have to decide rather than the movement — how many
29
+ bones, where each pivot goes, what hangs off what, and which of those the frames
30
+ can check: [RIGGING.md](RIGGING.md). §3.9's pivot solve is the one piece of that
31
+ page this one already carries, and RIGGING §2 is what a wrong answer to it looks
32
+ like from inside a fit
28
33
  - If the movement is a **face** — a blink, a gaze, or a head turning off axis —
29
34
  everything on this page still applies, and [FACE.md](FACE.md) is the geometry it
30
35
  does not have: a turn is a projection rather than a pose, so its values are