@rig-c/core 2.82.1 → 2.82.2
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 +45 -43
- package/docs/AUTHORING.md +36 -20
- package/docs/INGEST.md +3 -3
- package/docs/MOTION.md +21 -11
- package/package.json +1 -1
- package/src/pose.ts +209 -147
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ name. Both remain published alongside it with the same product files.
|
|
|
26
26
|
|
|
27
27
|
<p align="center"><em>Fourteen part PNGs drawn from scratch for this repo, one rig spec, one motion spec — the assembly,
|
|
28
28
|
the breathing and the wave are all rigc-compiled Spine animations, rendered with
|
|
29
|
-
<code>rigc render</code>. A <a href="https://raw.githubusercontent.com/rig-c/core/main/assets/rigc-demo@2x.gif">2x
|
|
29
|
+
<code>rigc render</code>. A <a href="https://raw.githubusercontent.com/rig-c/core/main/assets/rigc-demo@2x.gif">2x assembly film</a> (1200x810) sits beside it.</em></p>
|
|
30
30
|
|
|
31
31
|
Loose part PNGs and two small JSON files in; **Spine 4.3 skeleton data out** — a
|
|
32
32
|
`skeleton.json` and a `skeleton.atlas` that load in any Spine runtime and **import
|
|
@@ -44,7 +44,7 @@ installed.
|
|
|
44
44
|
| a compiled rig and a question about its geometry | `rigc inspect` | one picture of its bones, axes, mesh edges and hulls, one bone's weights, the triangles folded over, the draw order, a world grid or a declared axis over the art — at the setup pose, one time or a sheet of several, zoomed to a bone or a slot, or beside a second build on one transform — and a legend that puts every mark back in world units |
|
|
45
45
|
| a compiled rig | `rigc preview` | one self-contained `.html` that plays it in Spine's own web player |
|
|
46
46
|
| two to four compiled rigs | `rigc vote` | one ballot page a human picks from, and the answer checked into a ledger |
|
|
47
|
-
| a picture of a key pose | `rigc pose` | where each loose part PNG sits in it, in spec coordinates — the movement between two poses is then yours to key ([
|
|
47
|
+
| a picture of a key pose | `rigc pose` | where each loose part PNG sits in it, in spec coordinates — the movement between two poses is then yours to key ([the motion recipe](docs/MOTION.md)) |
|
|
48
48
|
| the same picture, and a rig | `rigc chainfit` | the parts `pose` refuses because something is drawn over them — read through the candidate's own draw order and hierarchy, with the share of each part the answer was measured on |
|
|
49
49
|
|
|
50
50
|
Everything in that table needs Bun and this package — `preview` and `vote` also
|
|
@@ -54,8 +54,8 @@ reference art, no art pipeline, no server.
|
|
|
54
54
|
### Depth-driven rigging
|
|
55
55
|
|
|
56
56
|
Explicit depth, which a drawing alone does not state, makes facial features
|
|
57
|
-
shift by different amounts during a small head turn: [
|
|
58
|
-
uses per-part depths, while [
|
|
57
|
+
shift by different amounts during a small head turn: [the portrait depth example](https://github.com/rig-c/core/tree/main/gallery/portrait)
|
|
58
|
+
uses per-part depths, while [the mesh-depth look example](https://github.com/rig-c/core/tree/main/gallery/look)
|
|
59
59
|
samples a depth map at the mesh vertices.
|
|
60
60
|
With [`@rig-c/parts`' depth fields](https://github.com/rig-c/parts/blob/main/docs/AUTHORING.md#per-layer-depth-sheets-773),
|
|
61
61
|
See-through's per-layer sheets are bound explicitly with `near` and `zScale`,
|
|
@@ -106,7 +106,7 @@ it cuts both ways:
|
|
|
106
106
|
|
|
107
107
|
### Licensing, stated plainly
|
|
108
108
|
|
|
109
|
-
See [
|
|
109
|
+
See [the licensing notice](NOTICE.md) for licensing and third-party terms.
|
|
110
110
|
|
|
111
111
|
The problem rigc is aimed at is narrow. An agent asked to author a rig has no way
|
|
112
112
|
to tell whether it succeeded: Spine's JSON parser accepts a great deal of nonsense
|
|
@@ -190,7 +190,7 @@ reference frames this project renders from them, which **are** committed, each
|
|
|
190
190
|
example's own `license.txt` beside them under the redistribution grant those files
|
|
191
191
|
carry; the images stay **non-commercial only**. The reasoning is in
|
|
192
192
|
[`bench/reference/README.md`](https://github.com/rig-c/core/blob/main/bench/reference/README.md)
|
|
193
|
-
and the terms in [
|
|
193
|
+
and the terms in [the reference-image notice](NOTICE.md). It needs a clone and `bun run
|
|
194
194
|
fetch-examples`, and says so by name when the corpus is absent. `check` is not one
|
|
195
195
|
of them: it reads whatever frames you point it at, so it runs from the installed
|
|
196
196
|
package on pictures of your own — which is what *Where to go next* below tells you
|
|
@@ -446,7 +446,7 @@ origin and tip in world units and in pixels, so a position read off the picture
|
|
|
446
446
|
straight back into the spec. [AUTHORING §13](docs/AUTHORING.md#13-asking-the-picture-a-question--rigc-inspect)
|
|
447
447
|
maps each question to its view.
|
|
448
448
|
|
|
449
|
-
**`render`** writes PNG frames and a contact sheet; **`preview`** writes a playable HTML file — see [
|
|
449
|
+
**`render`** writes PNG frames and a contact sheet; **`preview`** writes a playable HTML file — see [the render and preview reference](docs/AUTHORING.md#0-the-loop).
|
|
450
450
|
|
|
451
451
|
**7. Let someone choose.** Sooner or later you will have two builds that both pass
|
|
452
452
|
the gate and no instrument that can separate them. `vote` puts them in one page
|
|
@@ -466,7 +466,7 @@ that does not belong to it. See
|
|
|
466
466
|
|
|
467
467
|
**Where to go next.**
|
|
468
468
|
|
|
469
|
-
- 📘 **[
|
|
469
|
+
- 📘 **[the authoring guide](docs/AUTHORING.md)** is the real guide — both files
|
|
470
470
|
field by field, the emission rules, every named failure mapped to the file that
|
|
471
471
|
has to change, and §8–§9 for reproducing a shot you were given as pictures. It
|
|
472
472
|
ships inside the npm package too, at
|
|
@@ -490,11 +490,11 @@ that does not belong to it. See
|
|
|
490
490
|
it, so a figure is read against that rather than against a number from a page;
|
|
491
491
|
AUTHORING.md §9.2 says what the two halves of that bound are and why the floor
|
|
492
492
|
is not zero.
|
|
493
|
-
- [
|
|
493
|
+
- [the benchmark ladder](https://github.com/rig-c/core/blob/main/docs/LADDER.md) is the benchmark: the same job, from a brief
|
|
494
494
|
and rendered frames, scored. [docs/PILOT.md](https://github.com/rig-c/core/blob/main/docs/PILOT.md) is how to run an
|
|
495
495
|
agent through it and score what comes back.
|
|
496
496
|
- 🤖 **Handing the authoring to an AI agent?**
|
|
497
|
-
[
|
|
497
|
+
[the prompting guide](docs/PROMPTING.md) is the operator's page — the six prompt
|
|
498
498
|
clauses a measured pilot run paid for, and what you can leave unsaid.
|
|
499
499
|
|
|
500
500
|
## See what you built, and let someone choose
|
|
@@ -514,7 +514,7 @@ a rig compiles green, because green says nothing at all about the picture.
|
|
|
514
514
|
conditions, <code>rigc pose</code> reads where every part sits in each picture, two
|
|
515
515
|
candidate in-betweenings are compiled from the same pair — and a real
|
|
516
516
|
<code>rigc vote</code> ballot picks the winner, because the movement between the poses
|
|
517
|
-
is the one thing no instrument here will grade. A <a href="https://raw.githubusercontent.com/rig-c/core/main/assets/rigc-keypose@2x.gif">2x
|
|
517
|
+
is the one thing no instrument here will grade. A <a href="https://raw.githubusercontent.com/rig-c/core/main/assets/rigc-keypose@2x.gif">2x key-pose film</a> (1200x810) sits beside it.</em></p>
|
|
518
518
|
|
|
519
519
|
Three properties of `vote` are worth stating, because they are what make its ledger
|
|
520
520
|
usable by the next agent rather than by a reader: **a tie is a recorded outcome, not a
|
|
@@ -525,7 +525,7 @@ anywhere; and **every line carries a reason code** from a closed enumeration tha
|
|
|
525
525
|
enforced, so *"tie, because this one is better"* is refused.
|
|
526
526
|
|
|
527
527
|
🎞️ **Authoring the movement those pages show you** — key poses, in-betweening, and how
|
|
528
|
-
to spread candidates so a ballot informs — is [
|
|
528
|
+
to spread candidates so a ballot informs — is [the motion authoring guide](docs/MOTION.md).
|
|
529
529
|
|
|
530
530
|
📐 **`rigc pose --images parts/ --frame poseA.png` runs the other way.** Every command
|
|
531
531
|
above takes something you authored and tells you about it; this one takes a **picture
|
|
@@ -568,7 +568,7 @@ while <code>gaze</code> and <code>turn</code> return to rest, so the hand-offs m
|
|
|
568
568
|
differing pixels — and the composing is the consumer's. Authorable on plain Spine 4.3, no
|
|
569
569
|
plugin, no runtime patch; what the turn costs is authoring rather than runtime capability,
|
|
570
570
|
and that cost is one stated expression per key. Compiled and rendered entirely by the
|
|
571
|
-
published package. A <a href="https://raw.githubusercontent.com/rig-c/core/main/assets/rigc-scene@2x.gif">2x
|
|
571
|
+
published package. A <a href="https://raw.githubusercontent.com/rig-c/core/main/assets/rigc-scene@2x.gif">2x portrait film</a> (1200x810) sits beside it.</em></p>
|
|
572
572
|
|
|
573
573
|
🎞️ **How the three films on this page were made** is kept with them, one directory per
|
|
574
574
|
film in [`films/`](https://github.com/rig-c/core/tree/main/films) — a `run.sh` that
|
|
@@ -582,32 +582,34 @@ material, like the gallery: a clone runs them.
|
|
|
582
582
|
|
|
583
583
|
| Command | Does |
|
|
584
584
|
| --- | --- |
|
|
585
|
-
| `build` | Compile, gate and write the rig. [
|
|
586
|
-
| `validate` | Re-gate artifacts already on disk. [
|
|
587
|
-
| `repack` | Repack a build from its output. [
|
|
588
|
-
| `ingest` | Read a skeleton back into specs. [
|
|
589
|
-
| `split` | Separate disconnected mesh pieces. [
|
|
590
|
-
| `explain` | Print the compiled rig as a table. [
|
|
591
|
-
| `render` | Write PNG frames and a contact sheet. [
|
|
592
|
-
| `preview` | Write a playable HTML preview. [
|
|
593
|
-
| `vote` | Make a ballot and record its answer. [
|
|
594
|
-
| `pose` | Read part placements from a picture. [
|
|
595
|
-
| `chainfit` | Read occluded parts through a candidate rig. [
|
|
596
|
-
| `diff` | Compare two skeletons structurally. [
|
|
597
|
-
| `bonedist` | Measure per-bone world-transform distance. [
|
|
598
|
-
| `check` | Compare a candidate against reference pictures. [
|
|
599
|
-
| `bench` | Run a rung of the benchmark ladder. [
|
|
600
|
-
| `guide` | Read a named failure or guide section. [
|
|
601
|
-
| `skills install` | Install the packaged agent skills. [
|
|
602
|
-
| `review` | Hand a build to the review package. [
|
|
603
|
-
|
|
604
|
-
###
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
585
|
+
| `build` | Compile, gate and write the rig. [`build` in the CLI reference](docs/AUTHORING.md#cmd-build) |
|
|
586
|
+
| `validate` | Re-gate artifacts already on disk. [`validate` in the CLI reference](docs/AUTHORING.md#cmd-validate) |
|
|
587
|
+
| `repack` | Repack a build from its output. [`repack` in the CLI reference](docs/AUTHORING.md#cmd-repack) |
|
|
588
|
+
| `ingest` | Read a skeleton back into specs. [`ingest` in the CLI reference](docs/AUTHORING.md#cmd-ingest) |
|
|
589
|
+
| `split` | Separate disconnected mesh pieces. [`split` in the CLI reference](docs/AUTHORING.md#cmd-split) |
|
|
590
|
+
| `explain` | Print the compiled rig as a table. [`explain` in the CLI reference](docs/AUTHORING.md#cmd-explain) |
|
|
591
|
+
| `render` | Write PNG frames and a contact sheet. [`render` in the CLI reference](docs/AUTHORING.md#cmd-render) |
|
|
592
|
+
| `preview` | Write a playable HTML preview. [`preview` in the CLI reference](docs/AUTHORING.md#cmd-preview) |
|
|
593
|
+
| `vote` | Make a ballot and record its answer. [`vote` in the CLI reference](docs/AUTHORING.md#cmd-vote) |
|
|
594
|
+
| `pose` | Read part placements from a picture. [`pose` in the CLI reference](docs/AUTHORING.md#cmd-pose) |
|
|
595
|
+
| `chainfit` | Read occluded parts through a candidate rig. [`chainfit` in the CLI reference](docs/AUTHORING.md#cmd-chainfit) |
|
|
596
|
+
| `diff` | Compare two skeletons structurally. [`diff` in the CLI reference](docs/AUTHORING.md#cmd-diff) |
|
|
597
|
+
| `bonedist` | Measure per-bone world-transform distance. [`bonedist` in the CLI reference](docs/AUTHORING.md#cmd-bonedist) |
|
|
598
|
+
| `check` | Compare a candidate against reference pictures. [`check` in the CLI reference](docs/AUTHORING.md#cmd-check) |
|
|
599
|
+
| `bench` | Run a rung of the benchmark ladder. [`bench` in the CLI reference](docs/AUTHORING.md#cmd-bench) |
|
|
600
|
+
| `guide` | Read a named failure or guide section. [`guide` in the CLI reference](docs/AUTHORING.md#cmd-guide) |
|
|
601
|
+
| `skills install` | Install the packaged agent skills. [`skills install` in the CLI reference](docs/AUTHORING.md#cmd-skills-install) |
|
|
602
|
+
| `review` | Hand a build to the review package. [`review` in the CLI reference](docs/AUTHORING.md#cmd-review) |
|
|
603
|
+
|
|
604
|
+
### Existing skeletons and the editor
|
|
605
|
+
|
|
606
|
+
<a id="starting-from-a-skeleton-you-already-have"></a>
|
|
607
|
+
<a id="the-editor-round-trip--for-a-licence-holder-never-in-ci"></a>
|
|
608
|
+
|
|
609
|
+
For a skeleton you already have, [the ingest-and-rebuild loop](docs/INGEST.md#starting-from-a-skeleton-you-already-have)
|
|
610
|
+
turns a Spine 4.3 export into rig and motion specs, then checks that rebuilding them reproduces it.
|
|
611
|
+
For a licence holder, [the editor round trip](docs/INGEST.md#the-editor-round-trip--for-a-licence-holder-never-in-ci)
|
|
612
|
+
imports a build into the Spine editor and checks the export against that build with the gate, structural diff and rendered frames; it never runs in CI.
|
|
611
613
|
|
|
612
614
|
## Documentation
|
|
613
615
|
|
|
@@ -625,7 +627,7 @@ See [INGEST: The editor round trip](docs/INGEST.md#the-editor-round-trip--for-a-
|
|
|
625
627
|
| 🧬 [GENERATIONS.md](https://github.com/rig-c/core/blob/main/docs/GENERATIONS.md) | **Spine data from another generation.** Why a 3.8–4.2 file read as 4.3 fails in silence, the policy that follows (detect from `skeleton.spine`, never guess, play on the matching runtime), what `A16` and `ingest` do with such a file, and the editor as the migration path. Repository material |
|
|
626
628
|
| 📓 [CASES.md](https://github.com/rig-c/core/blob/main/docs/CASES.md) | **converting a Live2D model to Spine, and verifying the conversion.** The method — the source's own player headless, parts from its texture along the drawables, motion baked as deform keys, reference frames first, `check` — run once end to end on two Live2D sample models with no editor and no converter, and verified against the source's renderer and Spine's own runtime: the three instruments, the two rigc defects verifying it led to and v1.2.1 fixed, and where the method stops. Figures only — the models' licence keeps every asset out. Repository material |
|
|
627
629
|
| 🗺️ [ROADMAP.md](https://github.com/rig-c/core/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 had to mean before the number was claimed, and what it was claimed on — conditions rather than a feature list, because direction here comes from what users hit |
|
|
628
|
-
| 📐 [CLAUDE.md](https://github.com/rig-c/core/blob/main/CLAUDE.md) | **the doctrine** — why the validator's messages are the product, why nothing reaches disk before green, why no number is ever invented, and what a change has to keep. [
|
|
630
|
+
| 📐 [CLAUDE.md](https://github.com/rig-c/core/blob/main/CLAUDE.md) | **the doctrine** — why the validator's messages are the product, why nothing reaches disk before green, why no number is ever invented, and what a change has to keep. [the contribution guide](https://github.com/rig-c/core/blob/main/CONTRIBUTING.md) calls it worth ten minutes before a first patch. Repository material |
|
|
629
631
|
|
|
630
632
|
[Dated survey](https://github.com/rig-c/core/blob/main/docs/SURVEY_2026-08-22.md).
|
|
631
633
|
|
|
@@ -649,7 +651,7 @@ attributability is **measured** to be capped below the bar reads down when every
|
|
|
649
651
|
observable about it is independently verified strict. Every other rung and the
|
|
650
652
|
graduation exam pass on the clause itself. Recompiling a stored candidate reproduces its
|
|
651
653
|
record **to the digit within one gate**; across an instrument change the digits move,
|
|
652
|
-
and [
|
|
654
|
+
and [the gate re-inspection ledger](https://github.com/rig-c/core/blob/main/docs/LADDER.md) records
|
|
653
655
|
where. The standing figures quoted above are from its *gate-v2.4 re-inspection*, which
|
|
654
656
|
is the current sweep.
|
|
655
657
|
|
|
@@ -668,13 +670,13 @@ a from-zero run is a tooling-progress measurement rather than a re-climb, which
|
|
|
668
670
|
why the certification above is scoped to tool + guide + protocol. One of those
|
|
669
671
|
attempts states the residual in its own words: *"in motion it is not at editor
|
|
670
672
|
quality."* All six, with their verdicts, are in
|
|
671
|
-
[
|
|
673
|
+
[the from-zero attempt records](https://github.com/rig-c/core/blob/main/docs/LADDER.md).
|
|
672
674
|
|
|
673
675
|
The whole dossier — the yardstick, `diff` and `check` and what neither of them can
|
|
674
676
|
see, every rung, the run viewer, the 50 assertions and the selftest behind them — is
|
|
675
677
|
[docs/BENCHMARK.md](https://github.com/rig-c/core/blob/main/docs/BENCHMARK.md).
|
|
676
678
|
Live rung status is
|
|
677
|
-
[
|
|
679
|
+
[recorded in the ladder ledger](https://github.com/rig-c/core/blob/main/docs/LADDER.md).
|
|
678
680
|
|
|
679
681
|
## Contributing
|
|
680
682
|
|
package/docs/AUTHORING.md
CHANGED
|
@@ -120,27 +120,27 @@ commands take it and what its default is.
|
|
|
120
120
|
|
|
121
121
|
| Command | Does |
|
|
122
122
|
| --- | --- |
|
|
123
|
-
| `build --rig … --motion … --out …` | compiles, gates, and **writes only if the gate is green**. `--images <dir>` says where the rig spec's `image` names resolve, `--manifest` adds measured art, and `--copy-images` copies every page PNG into `--out` so the directory is self-contained, with `skeleton.images` pointing at it so the editor's import finds the parts |
|
|
123
|
+
| `build --rig … --motion … --out …` | compiles, gates, and **writes only if the gate is green**. `--images <dir>` says where the rig spec's `image` names resolve, `--manifest` adds measured art, and `--copy-images` copies every page PNG into `--out` so the directory is self-contained, with `skeleton.images` pointing at it so the editor's import finds the parts <a id="cmd-build"></a> |
|
|
124
124
|
| `build … --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` — losslessly, and gated a second time as the pair that ships. `--page-size`, `--padding`, `--page-edges` and `--pack-shape` tune it — `--pack-shape polygon` lets a neighbour sit inside a mesh's rectangle where its hull is not, and with `--page-edges free` costs 12–56 times `rect`'s pack (3–11 s against 0.2–0.3 s on 90–144 parts over two to four pages; past 5 s from about 60 parts, a second or so below 45 on one page — AUTHORING §0.1) |
|
|
125
125
|
| `build … --atlas-in <file.atlas>` | the same build with every part resolved to a **region of an existing pack** instead of a loose PNG; a name the atlas lacks, a size the spec disagrees with or a rectangle off its page is refused by name |
|
|
126
|
-
| `validate <dir>` | re-gates artifacts already on disk |
|
|
127
|
-
| `repack <dir> --out <dir>` | a packed build **repacked from its own output** — `skeleton.json`, `skeleton.atlas` and the pages, with no parts kept — under the packing flags `build --pack` takes, so a packer improvement reaches a build whose parts are gone. Every region is lifted off its page, the skeleton read back with `ingest`, and `build --pack` run over the lifted parts with the same gate; it writes only after every region is shown pixel-identical to the input's, the skeleton byte-identical and the gate green, and refuses by name an atlas it cannot lift exactly. A skeleton the rebuild does not reproduce byte for byte — a build written before 2.2.0, whose header box was its stage — is refused naming every differing path, and written, every path printed, only under `--accept-skeleton-differences`. `--stage-box <slot>` reads the stage from the box a build carries for it (`skeleton.stageBox`), the one way the three files alone carry the stage exactly ([AUTHORING §0.4](../docs/AUTHORING.md)) |
|
|
126
|
+
| `validate <dir>` | re-gates artifacts already on disk <a id="cmd-validate"></a> |
|
|
127
|
+
| `repack <dir> --out <dir>` | a packed build **repacked from its own output** — `skeleton.json`, `skeleton.atlas` and the pages, with no parts kept — under the packing flags `build --pack` takes, so a packer improvement reaches a build whose parts are gone. Every region is lifted off its page, the skeleton read back with `ingest`, and `build --pack` run over the lifted parts with the same gate; it writes only after every region is shown pixel-identical to the input's, the skeleton byte-identical and the gate green, and refuses by name an atlas it cannot lift exactly. A skeleton the rebuild does not reproduce byte for byte — a build written before 2.2.0, whose header box was its stage — is refused naming every differing path, and written, every path printed, only under `--accept-skeleton-differences`. `--stage-box <slot>` reads the stage from the box a build carries for it (`skeleton.stageBox`), the one way the three files alone carry the stage exactly ([AUTHORING §0.4](../docs/AUTHORING.md)) <a id="cmd-repack"></a> |
|
|
128
128
|
| `repack <dir> --out <dir> --keep-pages` | **`skeleton.model.json` alone, for the pages already in place** — for a player that reads the document beside an atlas and pages that already shipped. The same read and stage rules, then `build --atlas-in` over the input atlas in place of the lift and the pack; it writes the document, and nothing else, only after its `pages` are shown to state the atlas as it stands, the rebuilt skeleton byte-identical to the one on disk (so `spine.sha256` names it — no acceptance flag here) and the gate green. `--out` is usually the build's own directory and must not already hold a `skeleton.model.json`; a page or region line the document has no field for is refused by name ([AUTHORING §0.4](../docs/AUTHORING.md)) |
|
|
129
|
-
| `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
|
|
130
|
-
| `split --rig … --motion … --slot <name> --out <dir>` | the answer to a mesh refused because its triangles are **several pieces that share no vertex** — two legs after the triangles bridging them were deleted. It reads the two specs and writes them back into an absent or empty `--out` with one mesh per piece: piece 1 stays in the slot, piece k gets a new slot `<slot>_<k>` after it, each with the original's uvs and weights unchanged, its triangles renumbered outline first where the hull rule needs it, and every deform key re-sliced to the new indices — plus `split.json`, where every original vertex and triangle went. One piece is said so and nothing is written; what it cannot carry is refused by name ([AUTHORING §3.4.11](../docs/AUTHORING.md)) |
|
|
131
|
-
| `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
|
|
132
|
-
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings ([AUTHORING §8.8](../docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--extremes` adds `extremes.json` and a native six-panel `extremes.png` (setup, geometry-pair min/max and three premultiplied RGBA diffs with a legend), plus one `EXTREMES` line per set. Per-channel minima/maxima and the farthest original-vertex geometry pair are distinct readings; valid channel extrema survive a geometry/image SKIP, with channel refusals reported separately; still or uncomparable geometry reports a named SKIP without changing render exit status. `--extremes-only` writes the same `extremes.json` and readable `extremes.png` bytes while skipping ordinary frame rasterization/encoding, `contact.png` and `frames.json`; it implies `--extremes` and takes no other diagnostic or sheet output flags. Look at min, max and diff before a person does ([AUTHORING §8.11](../docs/AUTHORING.md#811-min-max-and-diff-before-a-person-looks--render---extremes)). `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene. `--sheet [--sheet-format json-hash] [--page-edges pot\|free]` also packs each set's straight-alpha transparent frames into `sheet.png` + `sheet.json` (TexturePacker JSON Hash with a shared world-origin `anchor` and `2`-px clamped edge extrusion, verified through `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`) and `sheet-manifest.json` (`rigc-sheet/1`), leaving diagnostic `f####.png` and `contact.png` unchanged ([AUTHORING §8.10](../docs/AUTHORING.md#810-sprite-sheets-from-a-render--render---sheet)) — a self-contained sprite object per animation set; when animations play and how a scene is composed remains the consumer's |
|
|
133
|
-
| `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` |
|
|
134
|
-
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
135
|
-
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
|
136
|
-
| `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 |
|
|
137
|
-
| `diff <candidate.json> <reference.json>` | structural comparison of two skeletons, one ratio per measure and deliberately no combined score |
|
|
138
|
-
| `bonedist --candidate … --reference … --bones …` | per-frame, per-bone world-transform distance against another skeleton — the ladder's stage 3, run on its own. `--bones <correspondence.json \| identity>` is required rather than defaulted: a candidate is entitled to its own bone names, so the pairing is stated |
|
|
139
|
-
| `check --candidate <dir> --frames <dir> [--out <dir>]` | the candidate against reference pictures — the only instrument here that can see a *wrong animation*. `--out` also writes, for every frame the table lists, the picture its figures came from: reference, candidate, difference and overlay at native size |
|
|
140
|
-
| `bench <rung> --candidate <dir>` | one rung of the benchmark ladder |
|
|
141
|
-
| `guide <RULE>` / `guide §<n>[.<m>[.<k>]] [--all]` | the guide's row for the rule a failure line names — every `FAIL` line, `vote --record` refusal, `ingest` finding and coded compile error ends with ` (see rigc guide <RULE>)`; only legacy uncoded library errors point at `§5.1` — or a section's heading, own prose and subsection index, read from the installed `docs/AUTHORING.md` when it runs. `--all` includes the subsection bodies; a leaf prints its full text either way. Sizes describe full ledger spans. `--all` with a rule is refused, exit 2; an unknown name is refused with the nearest real ones, exit 1 |
|
|
142
|
-
| `skills install [--dir …] [--copy]` | links every agent skill the package ships into `.agents/skills` (or `--dir`) as relative symlinks, or copies them with `--copy` — where Codex, Gemini CLI and Antigravity look. A second run has nothing to do; an entry already there that is not this command's is refused by name and nothing is written |
|
|
143
|
-
| `review <dir> […]` | the human review of a build, handed to the `rig-review` bin of `@rig-c/review` installed beside the package; rigc does not depend on it and never installs it. `rigc review <dir> [--reference <video>] [--animation <name>] [--compare <dir>]` is `rig-review open <dir> …` with those flags 1:1 and prints the loopback URL to give a person; a first argument that is one of review's own words (`open`, `check`, `feedback`, `control`, `strip`, `compare`) or starts with `-` (`--help`, `--`) is handed over untouched. stdout is review's byte for byte, stderr review's plus the launcher's own refusals, the exit code review's. Refused by name, exit 2, before the bin starts: `REVIEW_OUTPUT_MISSING <path>` (the target is not a directory); `REVIEW_NOT_INSTALLED` with two lines, `bun add -d @rig-c/review` and `bunx @rig-c/review open <dir> …` carrying the flags given — the same on a terminal and off, with nothing installed or written; `REVIEW_BUN_TOO_OLD: Bun <found>, @rig-c/review needs <required>`, the floor read off the installed package's `engines.bun`; and an empty argument (`""`) under a Bun that drops one (Bun 1.2.0, measured by a probe), `rigc review: argument N is empty, and Bun <version>, the Bun running this, drops an empty argument when it starts a process — …`. docs/AUTHORING.md lists every line |
|
|
129
|
+
| `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all <a id="cmd-ingest"></a> |
|
|
130
|
+
| `split --rig … --motion … --slot <name> --out <dir>` | the answer to a mesh refused because its triangles are **several pieces that share no vertex** — two legs after the triangles bridging them were deleted. It reads the two specs and writes them back into an absent or empty `--out` with one mesh per piece: piece 1 stays in the slot, piece k gets a new slot `<slot>_<k>` after it, each with the original's uvs and weights unchanged, its triangles renumbered outline first where the hull rule needs it, and every deform key re-sliced to the new indices — plus `split.json`, where every original vertex and triangle went. One piece is said so and nothing is written; what it cannot carry is refused by name ([AUTHORING §3.4.11](../docs/AUTHORING.md)) <a id="cmd-split"></a> |
|
|
131
|
+
| `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong <a id="cmd-explain"></a> |
|
|
132
|
+
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`, one directory per set: `<anim>@<fps>fps/` rather than `<anim>/` at any rate but the default — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid. `--folds` adds a `folds.json` per set and prints one line per mesh with a triangle turned over against the setup pose: the frames, the worst frame's count and which triangles, their world box and their setup-pose area. `--pass-through <cloth>:<leg>` adds a `pass-through.json` per set and prints the frames where a cloth drawn over a leg has its side inside the leg's drawn outline, with the depth and the world box — a reading, not a gate. `--seam <slot>:<slot>` adds a `seam.json` per set and prints, per pair, the frames where two slots that meet open background inside the joint (a gap) or show the overlap band of one past the other (a ghost), with the worst frame's cell count and world box; `ghostA` and `ghostB` partition the same ghost by which named pair member drew it, each with its own frames, spans and worst; `gapOpen` and `gapCovered` partition `gap` by zero versus positive rendered alpha from the whole skeleton, including faint and clipped drawings ([AUTHORING §8.8](../docs/AUTHORING.md#88-where-two-slots-stop-meeting--render---seam)) — a reading, not a gate; the exit status never moves. `--overlaps` adds an `overlaps.json` per set and prints one `OVERLAP "<under>" under "<over>"` line per pair of slots where the one drawn later covers the earlier one and the motion moves it — the frames, and the most pixels covered and the frame of them — with an outline plate per covered frame; the render reports what was drawn, and the draw-order key is the author's remedy — a reading, not a gate. `--extremes` adds `extremes.json` and a native six-panel `extremes.png` (setup, geometry-pair min/max and three premultiplied RGBA diffs with a legend), plus one `EXTREMES` line per set. Per-channel minima/maxima and the farthest original-vertex geometry pair are distinct readings; valid channel extrema survive a geometry/image SKIP, with channel refusals reported separately; still or uncomparable geometry reports a named SKIP without changing render exit status. `--extremes-only` writes the same `extremes.json` and readable `extremes.png` bytes while skipping ordinary frame rasterization/encoding, `contact.png` and `frames.json`; it implies `--extremes` and takes no other diagnostic or sheet output flags. Look at min, max and diff before a person does ([AUTHORING §8.11](../docs/AUTHORING.md#811-min-max-and-diff-before-a-person-looks--render---extremes)). `--angle-range <bone>:<relative-to>` prints how far one bone turns against another over each set: the lowest and highest angle and the frames they fall on — a reading, not a gate. `--viewport <x,y,w,h>` or `--viewport-from <frames.json>` draws over a given box instead of a fitted one, so two builds land on one pixel grid; a slot that reaches past it is named by set and frame, and `frames.json` records it. `--environment <schedule.json>` draws the rig under the wind and gravity its physics answers, in time — the same frames on the core and on spine-core; an inspection of the response the rig states, not a scene. `--sheet [--sheet-format json-hash] [--page-edges pot\|free]` also packs each set's straight-alpha transparent frames into `sheet.png` + `sheet.json` (TexturePacker JSON Hash with a shared world-origin `anchor` and `2`-px clamped edge extrusion, verified through `pixi.js@8.22.0` and `phaser@3.90.0` by `tools/sheet_readers.ts`) and `sheet-manifest.json` (`rigc-sheet/1`), leaving diagnostic `f####.png` and `contact.png` unchanged ([AUTHORING §8.10](../docs/AUTHORING.md#810-sprite-sheets-from-a-render--render---sheet)) — a self-contained sprite object per animation set; when animations play and how a scene is composed remains the consumer's <a id="cmd-render"></a> |
|
|
133
|
+
| `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` <a id="cmd-preview"></a> |
|
|
134
|
+
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` <a id="cmd-vote"></a> |
|
|
135
|
+
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture <a id="cmd-pose"></a> |
|
|
136
|
+
| `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 <a id="cmd-chainfit"></a> |
|
|
137
|
+
| `diff <candidate.json> <reference.json>` | structural comparison of two skeletons, one ratio per measure and deliberately no combined score <a id="cmd-diff"></a> |
|
|
138
|
+
| `bonedist --candidate … --reference … --bones …` | per-frame, per-bone world-transform distance against another skeleton — the ladder's stage 3, run on its own. `--bones <correspondence.json \| identity>` is required rather than defaulted: a candidate is entitled to its own bone names, so the pairing is stated <a id="cmd-bonedist"></a> |
|
|
139
|
+
| `check --candidate <dir> --frames <dir> [--out <dir>]` | the candidate against reference pictures — the only instrument here that can see a *wrong animation*. `--out` also writes, for every frame the table lists, the picture its figures came from: reference, candidate, difference and overlay at native size <a id="cmd-check"></a> |
|
|
140
|
+
| `bench <rung> --candidate <dir>` | one rung of the benchmark ladder <a id="cmd-bench"></a> |
|
|
141
|
+
| `guide <RULE>` / `guide §<n>[.<m>[.<k>]] [--all]` | the guide's row for the rule a failure line names — every `FAIL` line, `vote --record` refusal, `ingest` finding and coded compile error ends with ` (see rigc guide <RULE>)`; only legacy uncoded library errors point at `§5.1` — or a section's heading, own prose and subsection index, read from the installed `docs/AUTHORING.md` when it runs. `--all` includes the subsection bodies; a leaf prints its full text either way. Sizes describe full ledger spans. `--all` with a rule is refused, exit 2; an unknown name is refused with the nearest real ones, exit 1 <a id="cmd-guide"></a> |
|
|
142
|
+
| `skills install [--dir …] [--copy]` | links every agent skill the package ships into `.agents/skills` (or `--dir`) as relative symlinks, or copies them with `--copy` — where Codex, Gemini CLI and Antigravity look. A second run has nothing to do; an entry already there that is not this command's is refused by name and nothing is written <a id="cmd-skills-install"></a> |
|
|
143
|
+
| `review <dir> […]` | the human review of a build, handed to the `rig-review` bin of `@rig-c/review` installed beside the package; rigc does not depend on it and never installs it. `rigc review <dir> [--reference <video>] [--animation <name>] [--compare <dir>]` is `rig-review open <dir> …` with those flags 1:1 and prints the loopback URL to give a person; a first argument that is one of review's own words (`open`, `check`, `feedback`, `control`, `strip`, `compare`) or starts with `-` (`--help`, `--`) is handed over untouched. stdout is review's byte for byte, stderr review's plus the launcher's own refusals, the exit code review's. Refused by name, exit 2, before the bin starts: `REVIEW_OUTPUT_MISSING <path>` (the target is not a directory); `REVIEW_NOT_INSTALLED` with two lines, `bun add -d @rig-c/review` and `bunx @rig-c/review open <dir> …` carrying the flags given — the same on a terminal and off, with nothing installed or written; `REVIEW_BUN_TOO_OLD: Bun <found>, @rig-c/review needs <required>`, the floor read off the installed package's `engines.bun`; and an empty argument (`""`) under a Bun that drops one (Bun 1.2.0, measured by a probe), `rigc review: argument N is empty, and Bun <version>, the Bun running this, drops an empty argument when it starts a process — …`. docs/AUTHORING.md lists every line <a id="cmd-review"></a> |
|
|
144
144
|
|
|
145
145
|
`diff`, `bonedist`, `check` and `bench` measure against something you were given; the
|
|
146
146
|
first three work on any reference you have, and `bench` is a repository workflow that needs a clone
|
|
@@ -12026,7 +12026,7 @@ ambiguous one ends with a sentence saying which reading its figures support:
|
|
|
12026
12026
|
|
|
12027
12027
|
- **below the floor** — a 40x28 part of one flat colour, cut from a plain block it fits anywhere inside:
|
|
12028
12028
|
|
|
12029
|
-
> part is 40x28 px (span 40 frame px, opaque 1) with texture 0, detail 0;
|
|
12029
|
+
> part is 40x28 px (span 40 frame px, opaque 1) with texture 0, detail 0; 34 candidate(s), best 0.0000, next 0.0000, spread 0.0000 — below the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up): pose placed 7% of the measured parts this plain, so this part is too plain for pose to tell its placements apart; that says nothing about whether the cut is right
|
|
12030
12030
|
|
|
12031
12031
|
- **above the floor** — a 200x150 black-and-white checker the frame does not hold:
|
|
12032
12032
|
|
|
@@ -12034,7 +12034,7 @@ ambiguous one ends with a sentence saying which reading its figures support:
|
|
|
12034
12034
|
|
|
12035
12035
|
- **above the floor, one size in two places** — two copies of an 18x34 checker 0.6 of its span and 6° apart, the shape a left/right pair cut from one drawing reads as:
|
|
12036
12036
|
|
|
12037
|
-
> part is 18x34 px (span 36.2 frame px, opaque 1) with texture 0.0221, detail 0.752;
|
|
12037
|
+
> part is 18x34 px (span 36.2 frame px, opaque 1) with texture 0.0221, detail 0.752; 40 candidate(s), best 0.0160, next 0.0162, spread 0.0002 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; every placement in the margin is at the best's scale (within 0.30%, bar 2%) and in another place (the nearest 0.62 span(s) away, bar 0.45), which is the part — or a near-copy, such as a left/right pair cut from one drawing — drawn more than once: read the placements as an unordered set and order it across frames (MOTION.md §2.2 🔀); a --rotation window that excludes one place chooses the answer rather than measuring it
|
|
12038
12038
|
|
|
12039
12039
|
Read the figures, not only the verdict. *Below* says plainness alone explains the
|
|
12040
12040
|
failure: the cut may be right, and `pose` cannot tell. *Above* says it does not,
|
|
@@ -12124,6 +12124,22 @@ two anchors — and only 3 had it ranked under the old cutoff of three. The fine
|
|
|
12124
12124
|
took the grid from 185 to 227 trials found and those misses from 65 to 14, for about
|
|
12125
12125
|
10–14% more CPU over the grid (615 s of user time before it, 678 s and 704 s on two runs after).
|
|
12126
12126
|
|
|
12127
|
+
For repeated parts, an independent coverage lane also keeps up to five **new**
|
|
12128
|
+
places per rung. It uses the same fields and stride, but bounds suppression by
|
|
12129
|
+
half the material box's **short** side at that rung's scale: a long thin part's
|
|
12130
|
+
half-length cannot erase a different nearby position. Positions already covered
|
|
12131
|
+
by the original lane do not consume that extra budget. The coverage lane keeps
|
|
12132
|
+
its coarse pool through the first finer, rotation-regridded level before comparing
|
|
12133
|
+
scales together; only then does it narrow to the usual refinement budget. Both
|
|
12134
|
+
completed pools use the same full-resolution objective, dedupe and ambiguity
|
|
12135
|
+
margin. More coverage can expose an ambiguity; it does not prove that every
|
|
12136
|
+
visible copy, or every subpixel placement, will be recovered. Public repeated-part
|
|
12137
|
+
controls hold the final reported position, rotation and scale, not merely a coarse
|
|
12138
|
+
seed. The programmatic `coarsePolicy: 'legacy-only'` seam reproduces the original
|
|
12139
|
+
search for those negative controls; the CLI uses short-support coverage. An unknown
|
|
12140
|
+
runtime `coarsePolicy` is refused by name with the supplied value and both accepted
|
|
12141
|
+
policies; omit it to use the current search.
|
|
12142
|
+
|
|
12127
12143
|
The refinement then polishes each place down the pyramid, and at full resolution a
|
|
12128
12144
|
polish that has stopped tries its scale **up by half a coarse rung** (2^(1/6)), re-fits
|
|
12129
12145
|
the position there, and restarts if that scores better; fifteen places survive each
|
package/docs/INGEST.md
CHANGED
|
@@ -1405,11 +1405,11 @@ rigc pose
|
|
|
1405
1405
|
.. ground rgb(232, 232, 232) over 100% of the border ring
|
|
1406
1406
|
.. parts …/packed-only (1 png)
|
|
1407
1407
|
.. search scale 0.5–2 in 7 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
|
|
1408
|
-
PLACE 3-timing-and-spacing.png x= 324.
|
|
1408
|
+
PLACE 3-timing-and-spacing.png x= 324.6 y= 189.8 rot= -91.1° scale=0.625 residual=0.2080 unexplained= 29%
|
|
1409
1409
|
found on a 14x7 anchor grid, step 4 at 16x reduction
|
|
1410
1410
|
```
|
|
1411
1411
|
|
|
1412
|
-
⚠️ **`residual=0.
|
|
1412
|
+
⚠️ **`residual=0.2080` is *under* the default 0.25 refusal bar**, so nothing refused
|
|
1413
1413
|
it, and `PLACE` rather than `AMBIG` means nothing flagged it either. With the same frame
|
|
1414
1414
|
and the two real loose parts, the answer is what it should be:
|
|
1415
1415
|
|
|
@@ -1424,7 +1424,7 @@ rigc pose
|
|
|
1424
1424
|
.. ground rgb(232, 232, 232) over 100% of the border ring
|
|
1425
1425
|
.. parts …/examples/3-timing-and-spacing/images (2 png)
|
|
1426
1426
|
.. search scale 0.3–0.6 in 4 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
|
|
1427
|
-
PLACE pendulum.png x= 319.
|
|
1427
|
+
PLACE pendulum.png x= 319.4 y= 214.4 rot= -88.9° scale=0.412 residual=0.0421 unexplained= 7%
|
|
1428
1428
|
found on a 28x13 anchor grid, step 2 at 16x reduction
|
|
1429
1429
|
PLACE square.png x= 437.1 y= 343.1 rot= 0.0° scale=0.410 residual=0.0334 unexplained= 4%
|
|
1430
1430
|
found on a 113x51 anchor grid, step 2 at 4x reduction
|
package/docs/MOTION.md
CHANGED
|
@@ -818,20 +818,22 @@ rigc pose
|
|
|
818
818
|
found on a 20x25 anchor grid, step 4 at 2x reduction
|
|
819
819
|
PLACE flag.png x= 104.7 y= 156.1 rot= 84.3° scale=0.955 residual=0.0135 unexplained= 0%
|
|
820
820
|
found on a 40x50 anchor grid, step 2 at 2x reduction
|
|
821
|
-
AMBIG post.png x=
|
|
821
|
+
AMBIG post.png x= 80.0 y= 148.6 rot= -0.1° scale=0.982 residual=0.0587 unexplained= 16%
|
|
822
822
|
found on a 14x17 anchor grid, step 6 at 2x reduction
|
|
823
|
-
alt 2: x=
|
|
824
|
-
|
|
823
|
+
alt 2: x= 78.7 y= 164.0 rot= -0.6° scale=0.573 residual=0.0612 unexplained= 19%
|
|
824
|
+
alt 3: x= 79.0 y= 141.7 rot= -0.5° scale=0.691 residual=0.0687 unexplained= 25%
|
|
825
|
+
alt 4: x= 78.4 y= 135.5 rot= -0.5° scale=0.500 residual=0.0702 unexplained= 23%
|
|
826
|
+
ambiguous: part is 14x96 px (span 94.3 frame px, opaque 1) with texture 0.0146, detail 1.406; 50 candidate(s), best 0.0587, next 0.0612, spread 0.0025 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; either the frame holds more than one place this part fits, or every candidate missed the true one — a narrower --scale or --rotation window around what you know of the part tells the two apart
|
|
825
827
|
```
|
|
826
828
|
|
|
827
|
-
⚠️ **The post came back `AMBIG`: a best at scale 0.
|
|
828
|
-
|
|
829
|
+
⚠️ **The post came back `AMBIG`: a best at scale 0.982 and its next alternate at 0.573,
|
|
830
|
+
15.4 px lower, trailing it by a spread of 0.0025.** That is §2.2's caveat in the open. The post
|
|
829
831
|
is a long part with most of its area in one colour, so a shrunken copy sitting inside the
|
|
830
832
|
real post explains those pixels nearly as well, and the report's own `ambiguous:` line
|
|
831
833
|
says size and texture do not account for how close the two come. The report cannot say
|
|
832
|
-
which one is true, and it says so: it names
|
|
834
|
+
which one is true, and it says so: it names the alternatives and asks for a narrower window. The
|
|
833
835
|
picture can. The arm and the flag read 0.970 and 0.955, so the picture is at the art's
|
|
834
|
-
own resolution, and a post at 0.
|
|
836
|
+
own resolution, and a post at 0.573 would be drawn at about 0.6 of the size of everything
|
|
835
837
|
bolted to it. ⇒ Narrow, and say so:
|
|
836
838
|
|
|
837
839
|
```bash
|
|
@@ -861,12 +863,19 @@ rigc pose
|
|
|
861
863
|
.. search scale 0.85–1.2 in 2 step(s) · rotation -180°–180° in 24 step(s) of 15° · refuse above residual 0.25
|
|
862
864
|
PLACE arm.png x= 102.5 y= 94.8 rot= -18.3° scale=0.974 residual=0.0305 unexplained= 4%
|
|
863
865
|
found on a 20x25 anchor grid, step 4 at 2x reduction
|
|
864
|
-
PLACE flag.png x= 137.4 y= 86.0 rot= -6.
|
|
866
|
+
PLACE flag.png x= 137.4 y= 86.0 rot= -6.4° scale=0.958 residual=0.0134 unexplained= 0%
|
|
865
867
|
found on a 40x50 anchor grid, step 2 at 2x reduction
|
|
866
868
|
PLACE post.png x= 80.0 y= 148.0 rot= 0.0° scale=0.995 residual=0.0394 unexplained= 9%
|
|
867
869
|
found on a 14x17 anchor grid, step 6 at 2x reduction
|
|
868
870
|
```
|
|
869
871
|
|
|
872
|
+
The worked derivation and recipe below keep their recorded rounded authoring inputs:
|
|
873
|
+
pose A as printed here, and pose B with the flag at screen rotation −6.6° and scale
|
|
874
|
+
0.956. The current rerun above reads −6.4° and 0.958 for that flag. The 0.2° rotation
|
|
875
|
+
and 0.002 scale differences are changes in the estimator's reading; the fixed inputs
|
|
876
|
+
used to derive this worked rig and its motion are stated here so that the recipe does
|
|
877
|
+
not silently change with a rerun.
|
|
878
|
+
|
|
870
879
|
Three things to read out of that pair, none of which is a score:
|
|
871
880
|
|
|
872
881
|
- **Scale.** All six readings sit in 0.956–0.995, a spread of about 4 %. The one part
|
|
@@ -887,7 +896,8 @@ Three things to read out of that pair, none of which is a score:
|
|
|
887
896
|
|
|
888
897
|
### 3. Convert, and derive the rig
|
|
889
898
|
|
|
890
|
-
`cropToSpineY(y, 200) = 200 − y` and
|
|
899
|
+
For those recorded authoring inputs, `cropToSpineY(y, 200) = 200 − y` and
|
|
900
|
+
`screenToSpineDegrees(d) = −d`, both from
|
|
891
901
|
[`src/transform.ts`](../src/transform.ts) (§2.2 — do not open-code either):
|
|
892
902
|
|
|
893
903
|
| | pose A, Spine world | pose B, Spine world |
|
|
@@ -1042,8 +1052,8 @@ which is which is worth being able to point at:
|
|
|
1042
1052
|
|
|
1043
1053
|
| Key | Where it came from |
|
|
1044
1054
|
| --- | --- |
|
|
1045
|
-
| arm `t: 0` = −61.9, flag `t: 0` = −22.2 | **given** — pose A, converted. Untouched, per §3.6 |
|
|
1046
|
-
| arm `t: 0.55` = 18.3, flag `t: 0.55` = −11.7 | **given** — pose B. The flag's is `6.6 − 18.3`: both world rotations converted first, then differenced, because a child's track carries a **local** rotation under a rotated parent |
|
|
1055
|
+
| arm `t: 0` = −61.9, flag `t: 0` = −22.2 | **given** — the recorded pose A reading, converted. Untouched, per §3.6 |
|
|
1056
|
+
| arm `t: 0.55` = 18.3, flag `t: 0.55` = −11.7 | **given** — the recorded pose B reading. The flag's is `6.6 − 18.3`: both world rotations converted first, then differenced, because a child's track carries a **local** rotation under a rotated parent |
|
|
1047
1057
|
| arm `t: 0.07` = −66.4 | §3.6 — 4.5° against an 80° excursion (5.6 %), at 13 % of the duration |
|
|
1048
1058
|
| arm `t: 0.32` = 24.7 | §3.8 — 6.4° past the end value (8 %), at 58 % of the duration |
|
|
1049
1059
|
| flag `t: 0.09`, `t: 0.47` | §3.7 — the flag's extreme lands at 85 % of the duration against the arm's 58 %, an offset of **+27 %**, the middle of its row's band, and it drags the other way first. Not the top of the band: at 35 % (`t: 0.51`) the crossing and the settle below would share 0.04 s, and the 24 fps render would still show the flag at its extreme one frame before the last |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.82.
|
|
3
|
+
"version": "2.82.2",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/pose.ts
CHANGED
|
@@ -835,6 +835,8 @@ export interface PoseOptions {
|
|
|
835
835
|
* stride fraction in force in `search.coarse.strideFraction`.
|
|
836
836
|
*/
|
|
837
837
|
coarseGrid?: PoseCoarseGrid;
|
|
838
|
+
/** Historical-search control for instruments; the CLI uses short-support coverage. */
|
|
839
|
+
coarsePolicy?: 'legacy-only' | 'short-support';
|
|
838
840
|
}
|
|
839
841
|
|
|
840
842
|
/** A search candidate as the trace states it: where the part image's centre would land, frame pixels. */
|
|
@@ -900,6 +902,8 @@ export interface PoseTrace {
|
|
|
900
902
|
levels: PoseTraceLevel[];
|
|
901
903
|
/** Per part, every candidate that reached the report, with its every-pixel residual, best first. */
|
|
902
904
|
measured: { part: string; candidates: PoseTraceCandidate[] }[];
|
|
905
|
+
/** The independent coverage lane; `levels` retains the original lane's pyramid. */
|
|
906
|
+
supplemental?: { part: string; levels: PoseTraceLevel[] }[];
|
|
903
907
|
}
|
|
904
908
|
|
|
905
909
|
// ---------------------------------------------------------------------------
|
|
@@ -1388,8 +1392,15 @@ function coarseCellResidual(level: Level, s: Samples, scale: number, rotDeg: num
|
|
|
1388
1392
|
* exactly two such cells — and the accepted list then keeps them apart so the
|
|
1389
1393
|
* refinement budget is not spent twice on one hill.
|
|
1390
1394
|
*/
|
|
1391
|
-
function
|
|
1392
|
-
|
|
1395
|
+
export function poseCoarseMinima(
|
|
1396
|
+
field: CoarseField,
|
|
1397
|
+
radius: number,
|
|
1398
|
+
keep: number,
|
|
1399
|
+
support = Infinity,
|
|
1400
|
+
covered: readonly Candidate[] = [],
|
|
1401
|
+
observation?: { accepted: number; covered: number; spacingChecks?: number },
|
|
1402
|
+
): Candidate[] {
|
|
1403
|
+
const found: { candidate: Candidate; gx: number; gy: number }[] = [];
|
|
1393
1404
|
for (let gy = 0; gy < field.rows; gy++) {
|
|
1394
1405
|
for (let gx = 0; gx < field.cols; gx++) {
|
|
1395
1406
|
const here = field.residual[gy * field.cols + gx];
|
|
@@ -1400,7 +1411,7 @@ function localMinima(field: CoarseField, radius: number, keep: number): Candidat
|
|
|
1400
1411
|
const nx = gx + dx;
|
|
1401
1412
|
const ny = gy + dy;
|
|
1402
1413
|
if (nx < 0 || ny < 0 || nx >= field.cols || ny >= field.rows) continue;
|
|
1403
|
-
if (field.residual[ny * field.cols + nx] < here) {
|
|
1414
|
+
if (hypot2(dx * field.stride, dy * field.stride) <= support && field.residual[ny * field.cols + nx] < here) {
|
|
1404
1415
|
minimal = false;
|
|
1405
1416
|
break;
|
|
1406
1417
|
}
|
|
@@ -1408,23 +1419,45 @@ function localMinima(field: CoarseField, radius: number, keep: number): Candidat
|
|
|
1408
1419
|
}
|
|
1409
1420
|
if (!minimal) continue;
|
|
1410
1421
|
const cell = gy * field.cols + gx;
|
|
1411
|
-
found.push({
|
|
1422
|
+
found.push({ gx, gy, candidate: {
|
|
1412
1423
|
cx: gx * field.stride + field.stride / 2,
|
|
1413
1424
|
cy: gy * field.stride + field.stride / 2,
|
|
1414
1425
|
rotDeg: field.rotDeg[cell],
|
|
1415
1426
|
scale: field.scale,
|
|
1416
1427
|
residual: here,
|
|
1417
|
-
});
|
|
1428
|
+
} });
|
|
1418
1429
|
}
|
|
1419
1430
|
}
|
|
1420
|
-
found.sort((a, b) => a.residual - b.residual);
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1431
|
+
found.sort((a, b) => a.candidate.residual - b.candidate.residual);
|
|
1432
|
+
const spacing = Math.min(radius * field.stride, support);
|
|
1433
|
+
const gridRadius = spacing / field.stride;
|
|
1434
|
+
const bucketSpan = Math.max(1, Math.floor(gridRadius));
|
|
1435
|
+
const reach = Math.ceil(gridRadius / bucketSpan);
|
|
1436
|
+
const accepted = new Map<string, Candidate[]>();
|
|
1437
|
+
const novel: Candidate[] = [];
|
|
1438
|
+
for (const entry of found) {
|
|
1439
|
+
if (novel.length >= keep) break;
|
|
1440
|
+
const cand = entry.candidate;
|
|
1441
|
+
const bx = Math.floor(entry.gx / bucketSpan), by = Math.floor(entry.gy / bucketSpan);
|
|
1442
|
+
let overlaps = false;
|
|
1443
|
+
for (let y = by - reach; y <= by + reach && !overlaps; y++) {
|
|
1444
|
+
for (let x = bx - reach; x <= bx + reach && !overlaps; x++) {
|
|
1445
|
+
for (const a of accepted.get(`${x}:${y}`) ?? []) {
|
|
1446
|
+
if (observation !== undefined) observation.spacingChecks = (observation.spacingChecks ?? 0) + 1;
|
|
1447
|
+
if (hypot2(a.cx - cand.cx, a.cy - cand.cy) <= spacing) { overlaps = true; break; }
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (overlaps) continue;
|
|
1452
|
+
const key = `${bx}:${by}`, bucket = accepted.get(key) ?? [];
|
|
1453
|
+
bucket.push(cand);
|
|
1454
|
+
accepted.set(key, bucket);
|
|
1455
|
+
if (observation !== undefined) observation.accepted++;
|
|
1456
|
+
if (covered.some((b) => hypot2(cand.cx - b.cx, cand.cy - b.cy) <= support)) {
|
|
1457
|
+
if (observation !== undefined) observation.covered++;
|
|
1458
|
+
} else novel.push(cand);
|
|
1426
1459
|
}
|
|
1427
|
-
return
|
|
1460
|
+
return novel;
|
|
1428
1461
|
}
|
|
1429
1462
|
|
|
1430
1463
|
/**
|
|
@@ -1859,6 +1892,9 @@ export function partFiles(imagesDir: string, exclude: string): string[] {
|
|
|
1859
1892
|
// ---------------------------------------------------------------------------
|
|
1860
1893
|
|
|
1861
1894
|
export function estimatePose(options: PoseOptions): PoseReport {
|
|
1895
|
+
if (options.coarsePolicy !== undefined && options.coarsePolicy !== 'legacy-only' && options.coarsePolicy !== 'short-support') {
|
|
1896
|
+
throw new PoseError(`coarsePolicy ${JSON.stringify(options.coarsePolicy)}: expected "short-support" or "legacy-only" — omit it for the current search`);
|
|
1897
|
+
}
|
|
1862
1898
|
const framePath = resolve(options.framePath);
|
|
1863
1899
|
if (!existsSync(framePath)) throw new PoseError(`no pose frame at ${framePath}`);
|
|
1864
1900
|
let frame: Plate;
|
|
@@ -1959,6 +1995,7 @@ export function estimatePose(options: PoseOptions): PoseReport {
|
|
|
1959
1995
|
{ min: scaleMin, max: scaleMax },
|
|
1960
1996
|
rotationBounds,
|
|
1961
1997
|
options.coarseGrid ?? COARSE_GRID,
|
|
1998
|
+
options.coarsePolicy ?? 'short-support',
|
|
1962
1999
|
options.trace,
|
|
1963
2000
|
),
|
|
1964
2001
|
);
|
|
@@ -1977,6 +2014,7 @@ function placePart(
|
|
|
1977
2014
|
scaleBounds: { min: number; max: number },
|
|
1978
2015
|
rotationBounds: { min: number; max: number; wraps: boolean },
|
|
1979
2016
|
coarseGrid: PoseCoarseGrid,
|
|
2017
|
+
coarsePolicy: 'legacy-only' | 'short-support',
|
|
1980
2018
|
trace?: PoseTrace,
|
|
1981
2019
|
): PosePart {
|
|
1982
2020
|
const scaleMin = scaleBounds.min;
|
|
@@ -2114,11 +2152,23 @@ function placePart(
|
|
|
2114
2152
|
const stride = Math.max(1, Math.round(spanAtCoarse * coarseGrid.strideFraction));
|
|
2115
2153
|
const coarseSamples = samplesFor(coarse.reduction, COARSE_SAMPLES);
|
|
2116
2154
|
let candidates: Candidate[] = [];
|
|
2155
|
+
const supplementalSeeds: Candidate[] = [];
|
|
2117
2156
|
let grid = { reduction: coarse.reduction, cols: 0, rows: 0, stride };
|
|
2118
2157
|
for (const scale of scales) {
|
|
2119
2158
|
const field = coarseScan(coarse, coarseSamples, scale, searchRotations, stride);
|
|
2120
2159
|
grid = { reduction: coarse.reduction, cols: field.cols, rows: field.rows, stride };
|
|
2121
|
-
|
|
2160
|
+
const legacy = poseCoarseMinima(field, coarseGrid.suppressionCells, coarseGrid.minimaPerScale);
|
|
2161
|
+
candidates.push(...legacy);
|
|
2162
|
+
if (coarsePolicy === 'short-support') {
|
|
2163
|
+
// An isotropic half-LONG-span neighborhood can suppress another position
|
|
2164
|
+
// along a thin part. Its inscribed material disk supplies a rotation-free
|
|
2165
|
+
// bound, in level pixels, derived from the actual rung scale (issue #1649).
|
|
2166
|
+
const support = thickness * scale / (2 * coarse.reduction);
|
|
2167
|
+
const extra = poseCoarseMinima(field, coarseGrid.suppressionCells, coarseGrid.minimaPerScale, support, legacy);
|
|
2168
|
+
// Charge the independent budget AFTER excluding already represented
|
|
2169
|
+
// positions; charging it first spends its slots on seeds then discarded.
|
|
2170
|
+
supplementalSeeds.push(...extra);
|
|
2171
|
+
}
|
|
2122
2172
|
}
|
|
2123
2173
|
base.coarse = grid;
|
|
2124
2174
|
candidates.sort((a, b) => a.residual - b.residual);
|
|
@@ -2140,147 +2190,159 @@ function placePart(
|
|
|
2140
2190
|
// that arm is one half of the two-identical-limbs answer the whole instrument
|
|
2141
2191
|
// exists to report. Re-gridding the ladder at the first level with real detail
|
|
2142
2192
|
// brought it back at +35.
|
|
2143
|
-
const
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
grid
|
|
2193
|
+
const refineLane = (initial: Candidate[], supplemental = false, levelTrace?: PoseTraceLevel[]): Candidate[] => {
|
|
2194
|
+
let candidates = initial;
|
|
2195
|
+
const branchLevel = Math.max(0, coarseIndex - 1);
|
|
2196
|
+
/** How many rotations survive the re-grid at the branch level, per position. */
|
|
2197
|
+
const BRANCH_ROTATIONS = 3;
|
|
2198
|
+
let rotStep = rotationFree ? 0 : COARSE_ROTATION_STEP / 2;
|
|
2199
|
+
for (let li = coarseIndex; li >= 0; li--) {
|
|
2200
|
+
const level = levels[li];
|
|
2201
|
+
const plate = plates[li];
|
|
2202
|
+
const smooth = li !== coarseIndex;
|
|
2203
|
+
const branch = li === branchLevel;
|
|
2204
|
+
// Coarse scale scores are not comparable. Give the coverage pool its
|
|
2205
|
+
// first detailed/rotation-regridded reading before ranking scales together.
|
|
2206
|
+
const keep = li === coarseIndex || (supplemental && li === branchLevel) ? scales.length * coarseGrid.minimaPerScale : REFINE_CANDIDATES;
|
|
2207
|
+
const s = samplesFor(level.reduction, li === 0 ? POLISH_SAMPLES : REFINE_SAMPLES);
|
|
2208
|
+
// The coarse pass only sampled every `stride` pixels, so entering the
|
|
2209
|
+
// refinement the anchor can be half a stride out; the first polish gets a
|
|
2210
|
+
// step big enough to cross that rather than a step that assumes a pixel.
|
|
2211
|
+
const step = { translate: li === coarseIndex ? Math.max(1.5, stride) : 1.5, rotate: rotStep, scale: 0.08 };
|
|
2212
|
+
const floor =
|
|
2213
|
+
li === 0 ? { translate: 0.05, rotate: 0.1, scale: 0.001 } : { translate: 0.25, rotate: 0.5, scale: 0.01 };
|
|
2214
|
+
const seeds: Candidate[] = [];
|
|
2215
|
+
for (const start of candidates.slice(0, keep)) {
|
|
2216
|
+
if (branch && !rotationFree) {
|
|
2217
|
+
const grid: Candidate[] = [];
|
|
2218
|
+
for (const rotDeg of searchRotations) {
|
|
2219
|
+
const probe: Candidate = { ...start, rotDeg, residual: 0 };
|
|
2220
|
+
probe.residual = residualAt(level, plate, s, probe, smooth);
|
|
2221
|
+
grid.push(probe);
|
|
2222
|
+
}
|
|
2223
|
+
grid.sort((a, b) => a.residual - b.residual);
|
|
2224
|
+
// One position and one scale throughout, so this dedupe is a spread over
|
|
2225
|
+
// rotation alone: an angle within 25 degrees of one already kept is the
|
|
2226
|
+
// same basin under a slightly different name.
|
|
2227
|
+
seeds.push(...dedupe(grid, 1, 25, Infinity).slice(0, BRANCH_ROTATIONS));
|
|
2228
|
+
continue;
|
|
2168
2229
|
}
|
|
2169
|
-
|
|
2170
|
-
// One position and one scale throughout, so this dedupe is a spread over
|
|
2171
|
-
// rotation alone: an angle within 25 degrees of one already kept is the
|
|
2172
|
-
// same basin under a slightly different name.
|
|
2173
|
-
seeds.push(...dedupe(grid, 1, 25, Infinity).slice(0, BRANCH_ROTATIONS));
|
|
2174
|
-
continue;
|
|
2230
|
+
seeds.push(start);
|
|
2175
2231
|
}
|
|
2176
|
-
seeds.
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
residual: c.residual,
|
|
2203
|
-
};
|
|
2204
|
-
};
|
|
2205
|
-
let probeResidual: number | null = null;
|
|
2206
|
-
if (trace.probe !== undefined) {
|
|
2207
|
-
const q = trace.probe;
|
|
2208
|
-
const cos = Math.cos(q.rotationDeg * DEG) * q.scale;
|
|
2209
|
-
const sin = Math.sin(q.rotationDeg * DEG) * q.scale;
|
|
2210
|
-
const at: Candidate = {
|
|
2211
|
-
cx: (q.x - (ou * cos - ov * sin)) / level.reduction,
|
|
2212
|
-
cy: (q.y - (ou * sin + ov * cos)) / level.reduction,
|
|
2213
|
-
rotDeg: q.rotationDeg,
|
|
2214
|
-
scale: q.scale,
|
|
2215
|
-
residual: 0,
|
|
2232
|
+
const paths = seeds.map((): { start: number; steps: PoseTraceStep[] } => ({ start: 0, steps: [] }));
|
|
2233
|
+
candidates = seeds.map((seed, i) =>
|
|
2234
|
+
polish(level, plate, s, seed, step, floor, smooth, scaleBounds, rotationBounds, li === 0 ? POLISH_SCALE_ESCAPES : [], trace === undefined ? undefined : paths[i]),
|
|
2235
|
+
);
|
|
2236
|
+
const polished = candidates.slice();
|
|
2237
|
+
candidates.sort((a, b) => a.residual - b.residual);
|
|
2238
|
+
// ⚠️ Eight branches that walked to one optimum are one candidate, not eight —
|
|
2239
|
+
// and the radius has to scale with the PART rather than be a pixel count.
|
|
2240
|
+
// Narrowing by residual alone lets near-copies of the best hill fill the
|
|
2241
|
+
// budget and crowd the SECOND hill out, which is exactly the answer this
|
|
2242
|
+
// instrument exists to keep: with a fixed one-pixel radius the fixture lost
|
|
2243
|
+
// one of its two identical arms.
|
|
2244
|
+
candidates = dedupe(candidates, Math.max(1, (0.2 * span * scaleReference) / level.reduction), 5, 1.03);
|
|
2245
|
+
if (trace !== undefined && levelTrace !== undefined) {
|
|
2246
|
+
const ou = part.width / 2 - anchorX;
|
|
2247
|
+
const ov = part.height / 2 - anchorY;
|
|
2248
|
+
const said = (c: Candidate): PoseTraceCandidate => {
|
|
2249
|
+
const cos = Math.cos(c.rotDeg * DEG) * c.scale;
|
|
2250
|
+
const sin = Math.sin(c.rotDeg * DEG) * c.scale;
|
|
2251
|
+
return {
|
|
2252
|
+
x: c.cx * level.reduction + ou * cos - ov * sin,
|
|
2253
|
+
y: c.cy * level.reduction + ou * sin + ov * cos,
|
|
2254
|
+
rotationDeg: normaliseDegrees(c.rotDeg),
|
|
2255
|
+
scale: c.scale,
|
|
2256
|
+
residual: c.residual,
|
|
2257
|
+
};
|
|
2216
2258
|
};
|
|
2217
|
-
probeResidual
|
|
2259
|
+
let probeResidual: number | null = null;
|
|
2260
|
+
if (trace.probe !== undefined) {
|
|
2261
|
+
const q = trace.probe;
|
|
2262
|
+
const cos = Math.cos(q.rotationDeg * DEG) * q.scale;
|
|
2263
|
+
const sin = Math.sin(q.rotationDeg * DEG) * q.scale;
|
|
2264
|
+
const at: Candidate = {
|
|
2265
|
+
cx: (q.x - (ou * cos - ov * sin)) / level.reduction,
|
|
2266
|
+
cy: (q.y - (ou * sin + ov * cos)) / level.reduction,
|
|
2267
|
+
rotDeg: q.rotationDeg,
|
|
2268
|
+
scale: q.scale,
|
|
2269
|
+
residual: 0,
|
|
2270
|
+
};
|
|
2271
|
+
probeResidual = residualAt(level, plate, s, at, smooth);
|
|
2272
|
+
}
|
|
2273
|
+
levelTrace.push({
|
|
2274
|
+
part: name,
|
|
2275
|
+
level: li,
|
|
2276
|
+
reduction: level.reduction,
|
|
2277
|
+
keep,
|
|
2278
|
+
seeds: seeds.map(said),
|
|
2279
|
+
starts: paths.map((p) => p.start),
|
|
2280
|
+
paths: paths.map((p) => p.steps),
|
|
2281
|
+
polished: polished.map(said),
|
|
2282
|
+
out: candidates.map(said),
|
|
2283
|
+
probeResidual,
|
|
2284
|
+
coarseRotations:
|
|
2285
|
+
li === coarseIndex
|
|
2286
|
+
? {
|
|
2287
|
+
ladder: searchRotations.slice(),
|
|
2288
|
+
residuals: seeds.map((seed) =>
|
|
2289
|
+
searchRotations.map((rotDeg) => coarseCellResidual(level, coarseSamples, seed.scale, rotDeg, seed.cx, seed.cy)),
|
|
2290
|
+
),
|
|
2291
|
+
}
|
|
2292
|
+
: null,
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
if (li > 0) {
|
|
2296
|
+
candidates = candidates.map((c) => ({ ...c, cx: c.cx * 2, cy: c.cy * 2 }));
|
|
2297
|
+
// A rotation-free part keeps its step at zero all the way down, so the
|
|
2298
|
+
// polish never touches an angle that means nothing and the report's 0° is
|
|
2299
|
+
// the placeholder it says it is rather than a wandered-to number.
|
|
2300
|
+
if (!rotationFree) rotStep = Math.max(1, rotStep / 2);
|
|
2218
2301
|
}
|
|
2219
|
-
trace.levels.push({
|
|
2220
|
-
part: name,
|
|
2221
|
-
level: li,
|
|
2222
|
-
reduction: level.reduction,
|
|
2223
|
-
keep,
|
|
2224
|
-
seeds: seeds.map(said),
|
|
2225
|
-
starts: paths.map((p) => p.start),
|
|
2226
|
-
paths: paths.map((p) => p.steps),
|
|
2227
|
-
polished: polished.map(said),
|
|
2228
|
-
out: candidates.map(said),
|
|
2229
|
-
probeResidual,
|
|
2230
|
-
coarseRotations:
|
|
2231
|
-
li === coarseIndex
|
|
2232
|
-
? {
|
|
2233
|
-
ladder: searchRotations.slice(),
|
|
2234
|
-
residuals: seeds.map((seed) =>
|
|
2235
|
-
searchRotations.map((rotDeg) => coarseCellResidual(level, coarseSamples, seed.scale, rotDeg, seed.cx, seed.cy)),
|
|
2236
|
-
),
|
|
2237
|
-
}
|
|
2238
|
-
: null,
|
|
2239
|
-
});
|
|
2240
|
-
}
|
|
2241
|
-
if (li > 0) {
|
|
2242
|
-
candidates = candidates.map((c) => ({ ...c, cx: c.cx * 2, cy: c.cy * 2 }));
|
|
2243
|
-
// A rotation-free part keeps its step at zero all the way down, so the
|
|
2244
|
-
// polish never touches an angle that means nothing and the report's 0° is
|
|
2245
|
-
// the placeholder it says it is rather than a wandered-to number.
|
|
2246
|
-
if (!rotationFree) rotStep = Math.max(1, rotStep / 2);
|
|
2247
2302
|
}
|
|
2248
|
-
}
|
|
2249
2303
|
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2304
|
+
// The one rotation family the translation scan cannot see: a part that is its
|
|
2305
|
+
// own mirror after a quarter or a half turn sits in the SAME place at more than
|
|
2306
|
+
// one angle, so the field records only whichever won. Probe them explicitly.
|
|
2307
|
+
//
|
|
2308
|
+
// 🚨 Only the turns the window contains, and this is the other half of #719's
|
|
2309
|
+
// measurement. A quarter turn off is a SEED, not a ladder rung — so under
|
|
2310
|
+
// `--rotation -5,5` it entered the answer from outside a window the report was
|
|
2311
|
+
// calling the search, and the candidate who ran the exam read `rot=91.2°`
|
|
2312
|
+
// under `rotation -5°–5°`. A caller who bounds the rotation has said the part
|
|
2313
|
+
// is not a quarter turn over; the honest response is not to look there rather
|
|
2314
|
+
// than to look and report it.
|
|
2315
|
+
if (!rotationFree && candidates.length > 0) {
|
|
2316
|
+
const primary = candidates[0];
|
|
2317
|
+
const s = samplesFor(1, POLISH_SAMPLES);
|
|
2318
|
+
for (const turn of [90, 180, 270]) {
|
|
2319
|
+
const turned = primary.rotDeg + turn;
|
|
2320
|
+
if (!rotationBounds.wraps && (turned < rotationBounds.min - 1e-9 || turned > rotationBounds.max + 1e-9)) continue;
|
|
2321
|
+
candidates.push(
|
|
2322
|
+
polish(
|
|
2323
|
+
levels[0],
|
|
2324
|
+
plates[0],
|
|
2325
|
+
s,
|
|
2326
|
+
{ ...primary, rotDeg: turned },
|
|
2327
|
+
{ translate: 1.5, rotate: 4, scale: 0.04 },
|
|
2328
|
+
{ translate: 0.05, rotate: 0.1, scale: 0.001 },
|
|
2329
|
+
true,
|
|
2330
|
+
scaleBounds,
|
|
2331
|
+
rotationBounds,
|
|
2332
|
+
[],
|
|
2333
|
+
),
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
candidates.sort((a, b) => a.residual - b.residual);
|
|
2281
2337
|
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2338
|
+
|
|
2339
|
+
return candidates;
|
|
2340
|
+
};
|
|
2341
|
+
const baselineCandidates = refineLane(candidates, false, trace?.levels);
|
|
2342
|
+
supplementalSeeds.sort((a, b) => a.residual - b.residual);
|
|
2343
|
+
const extraTrace: PoseTraceLevel[] | undefined = trace === undefined ? undefined : [];
|
|
2344
|
+
if (trace !== undefined && extraTrace !== undefined) (trace.supplemental ??= []).push({ part: name, levels: extraTrace });
|
|
2345
|
+
candidates = [...baselineCandidates, ...refineLane(supplementalSeeds, true, extraTrace)];
|
|
2284
2346
|
|
|
2285
2347
|
// Measured at full resolution over every pixel, then de-duplicated: two
|
|
2286
2348
|
// candidates that walked to the same optimum are one answer, not two.
|