spine-rigc 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/cli.ts +31 -7
- package/docs/AUTHORING.md +177 -70
- package/docs/FACE.md +8 -7
- package/docs/INGEST.md +17 -6
- package/docs/SPEC_COVERAGE.md +2 -0
- package/package.json +1 -1
- package/src/chainfit.ts +64 -12
- package/src/compile.ts +432 -182
- package/src/deformgen.ts +14 -12
- package/src/diff.ts +24 -10
- package/src/mesh.ts +6 -1
- package/src/pose.ts +147 -35
- package/src/timelines.ts +37 -20
- package/src/trackgen.ts +3 -2
- package/src/transform.ts +18 -10
- package/src/types.ts +37 -2
- package/src/validate.ts +214 -50
package/README.md
CHANGED
|
@@ -533,9 +533,9 @@ first three work on any reference you have, and `bench` is a repository workflow
|
|
|
533
533
|
and `bun run fetch-examples`. The reasoning behind them is in
|
|
534
534
|
[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
|
|
535
535
|
|
|
536
|
-
`build` and `validate` both default to `--profile spine` — the
|
|
536
|
+
`build` and `validate` both default to `--profile spine` — the 34 validity rules, which
|
|
537
537
|
ask *is this valid Spine 4.3 that any runtime plays correctly?* `--profile spine-html`
|
|
538
|
-
adds all
|
|
538
|
+
adds all 49: the other 15 are one renderer's policy and one canvas budget's, and they
|
|
539
539
|
fire on perfectly correct editor-produced Spine data, which is why they are opt-in.
|
|
540
540
|
⇒ **That reason is about foreign data and does not carry to a rig you are authoring
|
|
541
541
|
yourself: author under `--profile spine-html` and read the extra 15 as findings, and
|
|
@@ -683,7 +683,7 @@ letting `A17` blame the editor for the harness's own doing.
|
|
|
683
683
|
| 📥 **[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 |
|
|
684
684
|
| 🤖 **[docs/PROMPTING.md](docs/PROMPTING.md)** | **handing the authoring to an AI agent** — the prompt clauses a measured pilot run paid for, and what you can leave unsaid. Ships in the package too |
|
|
685
685
|
| 🔬 **[docs/SPEC_COVERAGE.md](docs/SPEC_COVERAGE.md)** | Spine 4.3's full export surface against what rigc emits and what the official examples measurably use, with the ordered gap list |
|
|
686
|
-
| 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the
|
|
686
|
+
| 🎓 **[the benchmark dossier](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md)** | **why you can trust the output.** The yardstick, `diff` and `check` and what neither can see, the eight-rung ladder and the spineboy graduation exam, the run viewer, the 49 named assertions with their profiles, and the selftest that has watched every one of them fire. Repository material — it is not in the npm package |
|
|
687
687
|
| 📋 [LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) · [GATE.md](https://github.com/firejune/rigc/blob/main/docs/GATE.md) · [PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) | the live rung ledger, the clause statements a candidate is graded against, and how to run an agent through the ladder and score what comes back |
|
|
688
688
|
| 🗺️ [ROADMAP.md](https://github.com/firejune/rigc/blob/main/ROADMAP.md) | where this is going, and where it has been. What 1.0 has to mean before the number is claimed — conditions rather than a feature list, because direction here comes from what users hit |
|
|
689
689
|
| 📐 [CLAUDE.md](https://github.com/firejune/rigc/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. [CONTRIBUTING.md](https://github.com/firejune/rigc/blob/main/CONTRIBUTING.md) calls it worth ten minutes before a first patch. Repository material |
|
|
@@ -740,7 +740,7 @@ quality."* All six, with their verdicts, are in
|
|
|
740
740
|
[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
|
|
741
741
|
|
|
742
742
|
The whole dossier — the yardstick, `diff` and `check` and what neither of them can
|
|
743
|
-
see, every rung, the run viewer, the
|
|
743
|
+
see, every rung, the run viewer, the 49 assertions and the selftest behind them — is
|
|
744
744
|
[docs/BENCHMARK.md](https://github.com/firejune/rigc/blob/main/docs/BENCHMARK.md).
|
|
745
745
|
Live rung status is
|
|
746
746
|
[docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md).
|
package/cli.ts
CHANGED
|
@@ -86,7 +86,7 @@ import {
|
|
|
86
86
|
type AtlasRegion,
|
|
87
87
|
} from './src/atlas.ts';
|
|
88
88
|
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
89
|
-
import {
|
|
89
|
+
import { float32Step } from './src/timelines.ts';
|
|
90
90
|
import { findRung, RUNG_IDS, type RungSkeleton } from './src/ladder.ts';
|
|
91
91
|
import {
|
|
92
92
|
DEFAULT_MAX_RESIDUAL,
|
|
@@ -623,8 +623,30 @@ function meshFit(m: CompileResult['meshes'][number]): string {
|
|
|
623
623
|
// why, in the place the figures stood, and prints no figure.
|
|
624
624
|
if (m.fitWithheld !== undefined) return ` fit not measured: ${m.fitWithheld}. ${PAGE_GRID_UNLOCATED}`;
|
|
625
625
|
if (m.coverage === undefined) return '';
|
|
626
|
-
|
|
627
|
-
|
|
626
|
+
// A count of the plate's own cells, so on a `scale:` page it is texels and
|
|
627
|
+
// says so rather than borrowing the overshoot's unit beside it (issue #762).
|
|
628
|
+
const hole = m.holePixels ? `, enclosing ${m.holePixels}${m.pageScale === undefined ? 'px' : ' texel(s)'} of hole` : '';
|
|
629
|
+
return ` covers ${(m.coverage * 100).toFixed(2)}% of the art, reaching ${m.overshoot?.toFixed(2) ?? '?'}px past it${meshFitGrid(m)}${hole}`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* The grid a fit was taken on, when it is not the drawing's own (issue #762).
|
|
634
|
+
*
|
|
635
|
+
* The overshoot is stated in the drawing's pixels on every route — the unit an
|
|
636
|
+
* attachment's size is in — and on a page that declares a `scale:` other than
|
|
637
|
+
* 1 it was measured on the page's texels and divided by that scale. So it
|
|
638
|
+
* carries the coarser grid's step: on `scale: 0.5` a figure moves in steps of
|
|
639
|
+
* 2.00px of the drawing, and it need not equal the figure the page it was
|
|
640
|
+
* packed from reads except where the distance falls on whole texels. Said
|
|
641
|
+
* beside the figure, and nothing at all on a loose part or a page at scale 1,
|
|
642
|
+
* where the line is the one it always was.
|
|
643
|
+
*/
|
|
644
|
+
function meshFitGrid(m: CompileResult['meshes'][number]): string {
|
|
645
|
+
if (m.pageScale === undefined) return '';
|
|
646
|
+
return (
|
|
647
|
+
` (the drawing's pixels, measured on the page's texels at scale: ${m.pageScale} — a texel is ` +
|
|
648
|
+
`${(1 / m.pageScale).toFixed(2)}px of the drawing)`
|
|
649
|
+
);
|
|
628
650
|
}
|
|
629
651
|
|
|
630
652
|
/**
|
|
@@ -739,12 +761,14 @@ function deformKeyName(key: DeformKeyMeasure): string {
|
|
|
739
761
|
* The report's is the spec's own `t`; the survey's came back through
|
|
740
762
|
* `Float32Array`, because that is what `spine-core` reads a timeline's frames
|
|
741
763
|
* into — a key written `0.62` arrives as `0.6200000047683716`. So the tolerance
|
|
742
|
-
* is
|
|
743
|
-
*
|
|
744
|
-
*
|
|
764
|
+
* is one float32 step at this magnitude (`float32Step`): the compiler emits a
|
|
765
|
+
* key time as a float's name — the spec's own time when it names one, the float
|
|
766
|
+
* below it when it does not (issue #716) — so the loaded float is within one
|
|
767
|
+
* step of the spec's `t` either way, which is narrower than any key spacing the
|
|
768
|
+
* format can hold.
|
|
745
769
|
*/
|
|
746
770
|
function sameKeyTime(specTime: number, loaded: number): boolean {
|
|
747
|
-
return Math.abs(specTime - loaded) <=
|
|
771
|
+
return Math.abs(specTime - loaded) <= float32Step(loaded);
|
|
748
772
|
}
|
|
749
773
|
|
|
750
774
|
/**
|
package/docs/AUTHORING.md
CHANGED
|
@@ -169,7 +169,7 @@ What the flags mean:
|
|
|
169
169
|
| `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
|
|
170
170
|
| `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
|
|
171
171
|
| `--cuts` | the `cuts.json` `--cut` names. Required beside it — `--cut` alone is refused, with no guess at where the table lives |
|
|
172
|
-
| `--profile` | `spine` = the
|
|
172
|
+
| `--profile` | `spine` = the 34 validity rules (**the default**) · `spine-html` = all 49, opt-in |
|
|
173
173
|
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` is the one command that takes it more than once** — repeat it 2–4 times, one per pane, labelled A, B, C, D in the order given; everywhere else a repeat is a typo and is refused |
|
|
174
174
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview`, and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote`, so is a name that only *some* candidates have |
|
|
175
175
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
@@ -324,6 +324,14 @@ texel count beside it so both numbers are visible:
|
|
|
324
324
|
measures the PNG. Reach for `--atlas-in` when the pack is what you were handed, or
|
|
325
325
|
when drawing through the pack's own texels is the point.
|
|
326
326
|
|
|
327
|
+
📐 **What you make beside the art stays at the art's size**
|
|
328
|
+
([#762](https://github.com/firejune/rigc/issues/762)). A depth sheet or a soft
|
|
329
|
+
mask is read in the drawing's pixels on a `scale:` page as on loose parts — each
|
|
330
|
+
vertex's texel position over the stated scale — and a mesh fit's overshoot is
|
|
331
|
+
printed in them, with the texel it was measured on named beside it. What stays
|
|
332
|
+
in texels is what is taken off them: a `contour`'s trace, whose `margin` and
|
|
333
|
+
`tolerance` are applied on the texels there are (below).
|
|
334
|
+
|
|
327
335
|
🚨 **A page that declares a size it does not have is a different thing, and it is
|
|
328
336
|
refused** ([#715](https://github.com/firejune/rigc/issues/715)). The common shape
|
|
329
337
|
is a pack whose `4096x4096` pages ship as `2048x2048` PNGs with the atlas
|
|
@@ -387,11 +395,34 @@ that cannot be withheld, because its outline *is* its geometry: it is refused as
|
|
|
387
395
|
compile error carrying `A06`'s whole sentence — ratio and repair — on `explain`
|
|
388
396
|
and on `build` alike, where on `build` it arrives before the gate would have said
|
|
389
397
|
it. Carry out the repair and every figure comes back. ⚠️ They come back measured
|
|
390
|
-
on the **coarser** texels the page really has, so
|
|
391
|
-
|
|
392
|
-
restatement
|
|
393
|
-
|
|
394
|
-
|
|
398
|
+
on the **coarser** texels the page really has, so a figure that depends on the
|
|
399
|
+
grid need not equal the one the pack the page was halved from reads: on the same
|
|
400
|
+
fixture the `scale: 0.5` restatement traces the contour as 11 vertices where the
|
|
401
|
+
full-resolution page traced 15, because a trace runs on the texels there are and
|
|
402
|
+
its `margin` and `tolerance` are applied on them.
|
|
403
|
+
|
|
404
|
+
📐 **A fit's overshoot is stated in the drawing's pixels on every page**
|
|
405
|
+
([#762](https://github.com/firejune/rigc/issues/762)). It is a distance, and on a
|
|
406
|
+
page that declares a `scale:` it is taken on the page's texels — so it used to be
|
|
407
|
+
printed in them, under the same `px`: one mesh over one drawing read 16.00px on
|
|
408
|
+
the declared-size page, **8.00px** on its `scale: 0.5` restatement and **32.00px**
|
|
409
|
+
on a `scale: 2` one. It is now the texel distance over the scale the atlas states,
|
|
410
|
+
which is the unit an attachment's `width` is in and the one you drew in, and the
|
|
411
|
+
line says which grid it was taken on — a texel of a `scale: 0.5` page is 2.00px of
|
|
412
|
+
the drawing, so that is the step the figure moves in:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
# fan authored 9 vertices / 8 triangles (budget 200) bones=[fan] covers 100.00% of the art, reaching 16.00px
|
|
416
|
+
# past it (the drawing's pixels, measured on the page's texels at scale: 0.5 — a texel is 2.00px of the drawing)
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
All three pages read the fan at 16.00px, because its rim lands on whole texels of
|
|
420
|
+
each. A figure that does not is exact only to that step — the contour above reads
|
|
421
|
+
6.00px on the `scale: 0.5` page (3.00 texels) against the full page's 3.16px, a
|
|
422
|
+
different outline measured on a coarser grid, and the line's clause is what says
|
|
423
|
+
so. On a loose part and a page at scale 1 the texels are the drawing, and the line
|
|
424
|
+
is the one it always was. A `contour`'s hole count is a count of those cells and
|
|
425
|
+
says `texel(s)` on such a page.
|
|
395
426
|
|
|
396
427
|
🚨 **A page that is not a PNG is refused by name, before anything is compiled
|
|
397
428
|
against it** ([#732](https://github.com/firejune/rigc/issues/732)). rigc reads PNG
|
|
@@ -707,7 +738,10 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
|
707
738
|
count of vertices on undrawn texels — are replaced by `fit not measured: …` and
|
|
708
739
|
`… is not measured: …` naming the page and the ratio. A `contour` on such a page
|
|
709
740
|
is refused, since its outline is read off those texels. Why, the quoted lines,
|
|
710
|
-
and the repair: §0.2.
|
|
741
|
+
and the repair: §0.2. On a page that **does** declare a `scale:`, the overshoot
|
|
742
|
+
in that line is the drawing's pixels and the line names the texel it was taken
|
|
743
|
+
on (`… measured on the page's texels at scale: 0.5 — a texel is 2.00px of the
|
|
744
|
+
drawing`, [#762](https://github.com/firejune/rigc/issues/762)).
|
|
711
745
|
- **`diff`** compares two skeletons and reports **a ratio per measure** in six
|
|
712
746
|
sections (bones, slots, attachments, constraints, animations, events). It
|
|
713
747
|
deliberately does not combine them into a score: a rig with the right skeleton
|
|
@@ -922,7 +956,7 @@ one frame (1/60 s) is a compile error, and assertion `A09` re-checks it against
|
|
|
922
956
|
That frame of slack is for a duration declared *longer* than the motion — an
|
|
923
957
|
animation may hold its final pose. In the other direction there is no slack to give:
|
|
924
958
|
**no key may land past the declared duration**, and this is checked per timeline
|
|
925
|
-
rather than per animation, within
|
|
959
|
+
rather than per animation, within one float32 step of the duration. Both halves matter, and the second is not
|
|
926
960
|
the first with a smaller number — see §4.5.
|
|
927
961
|
|
|
928
962
|
**R8 — `from` needs a cut manifest.** `from.anchor` / `from.slotWindow` /
|
|
@@ -1092,7 +1126,7 @@ number where the box would be:
|
|
|
1092
1126
|
| --- | --- | --- |
|
|
1093
1127
|
| `build` | emits `x`/`y`/`width`/`height` | emits none of them |
|
|
1094
1128
|
| `A14_NO_FULL_FRAME_MESH` | fails a mesh as big as the stage | **SKIP**, by name |
|
|
1095
|
-
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | exempts the one image that covers the stage | exempts
|
|
1129
|
+
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | exempts the base plate a cut manifest names — the part whose window is the crop — and, on a build that names none, the one image that covers the stage | a manifest build exempts the plate it names, exactly as with one ([#770](https://github.com/firejune/rigc/issues/770)). A rig-spec build names no base plate, so an opaque part is refused with *"this skeleton declares no stage size to measure one against, and the build names no base plate"* and the two ways to decide it: a `skeleton` stage the plate covers, or *"build from a cut manifest, whose base plate is the part whose window is the crop"* |
|
|
1096
1130
|
| `diff` | `stage_present` and `stage_box` | `stage_present` 1/1 when neither side declares one (agreement), `stage_box` 0/0 |
|
|
1097
1131
|
| `explain` | `stage W x H` | `stage none declared` |
|
|
1098
1132
|
| `render` | frames the posed extent of every animation | the same frames, plus a line saying the viewport is the posed extent and no stage |
|
|
@@ -1869,7 +1903,7 @@ off a greyscale sheet in the part's own pixel grid:
|
|
|
1869
1903
|
|
|
1870
1904
|
| Field | Meaning |
|
|
1871
1905
|
| --- | --- |
|
|
1872
|
-
| `image` | **required.** The sheet, relative to the rig's `images` directory, and the **same pixel size as this attachment's `image`**. It is not packed into the atlas — it is a measurement rigc reads at compile time, not art anything draws |
|
|
1906
|
+
| `image` | **required.** The sheet, relative to the rig's `images` directory, and the **same pixel size as this attachment's `image`**. It is not packed into the atlas — it is a measurement rigc reads at compile time, not art anything draws. Under `--atlas-in` that is still the drawing's size, on a page that declares a `scale:` too: the sheet is read at each vertex's texel position over the scale the atlas states, so one sheet serves the loose parts and every pack of them ([#762](https://github.com/firejune/rigc/issues/762)) |
|
|
1873
1907
|
| `near` | **required.** `"white"` or `"black"` — which end of the range is closest to the viewer. Stated rather than defaulted: both conventions are in use, and a sheet read with the wrong one turns the part inside out with every gate still green |
|
|
1874
1908
|
| `zScale` | **required.** How many world units the map's full range spans, in the attachment's own units — the number `radius` used to carry. 8 bits of level say nothing about scale, so this is authored, never measured |
|
|
1875
1909
|
| `gamma`, `contrast`, `bias` | the tone curve applied to the nearness, defaults `1` / `1` / `0`. State them when a consumer's own renderer curves the same sheet, so the mesh and that renderer describe one surface |
|
|
@@ -1936,7 +1970,7 @@ bun cli.ts build --rig gallery/look/rig.json \
|
|
|
1936
1970
|
1st pct yaw +19.32° x1.000 of 80 / -19.32° x1.000 of 80 pitch +22.92° x1.000 of 102 / -26.94° x1.000 of 130
|
|
1937
1971
|
first to fold: yaw + at 19.32°, triangle 174 [119,138,139], the sheet steps 28.50 level(s) across it, which is 0.112 of the range this mesh sampled
|
|
1938
1972
|
MESH hair_lock_l grid 39 vertices / 48 triangles (budget 320) bones=[lock_l] attachments=[hair_lock_l]
|
|
1939
|
-
depth "lock_l_depth.png" 0c4eaeb36b7c5cac near=white zScale=64 z=[22.086275, 63.
|
|
1973
|
+
depth "lock_l_depth.png" 0c4eaeb36b7c5cac near=white zScale=64 z=[22.086275, 63.87451]
|
|
1940
1974
|
32 of 39 vertices sample a texel the part image does not draw — their z is the sheet's reading of somewhere the part is not
|
|
1941
1975
|
turn ceiling yaw +17.04° / -45.80° pitch +none / -none
|
|
1942
1976
|
1st pct yaw +unranked of 12 / -unranked of 36 pitch +none / -none
|
|
@@ -2034,6 +2068,8 @@ refuses it instead:
|
|
|
2034
2068
|
| a sheet cut to the art's alpha, on a **contour** | `does not cover 12 of the mesh's 12 vertices … A contour mesh puts every vertex ON the silhouette and pushes it out by the margin … Dilate the sheet past the mesh margin, or lower the margin.` |
|
|
2035
2069
|
| a sheet cut to the art's alpha, on a **grid** | `does not cover 36 of the mesh's 81 vertices … A grid spans the whole part window, corners included … Dilate the sheet to the window, or state "us"/"vs" that keep the lattice inside the art.` — the two topologies run out of sheet for different reasons, and the message says which |
|
|
2036
2070
|
| a sheet that is not the part's size | `the depth map … is 32x32 and the part is 64x64. A depth map is sampled in the part's own pixel grid` |
|
|
2071
|
+
| a sheet at the page's texel size, on a page that declares a `scale:` | `the depth map "lattice_texels.png" is 48x32 and the part is a 96x64 drawing — 48x32 texels on a page that declares scale: 0.5, which makes a texel 2px of the drawing. … so the sheet is 96x64, the size of the loose art, whatever the page holds.` — the part's own pixel grid is the drawing's on every route, and the message names the ratio and all three sizes, because the drawing's is in neither file |
|
|
2072
|
+
| a sheet cut to the art, on a `scale:` page | the coverage refusal above, with the vertex's position in the page's texels **and** `pixel (x, y) of the drawing-sized sheet` — the pixel of the file you made |
|
|
2037
2073
|
| a colour sheet | `the depth map … is not greyscale — pixel (0, 0) is rgb(10, 200, 10)` |
|
|
2038
2074
|
| `zScale` at or below 0 | `it is how many units the map's full range spans, so a positive number. To put the near end at the back, say "near": "black"` |
|
|
2039
2075
|
| `gamma` or `contrast` at or below 0 | `collapses the range onto the midpoint … so the map would describe a flat part` |
|
|
@@ -2101,7 +2137,7 @@ hanging sleeve.
|
|
|
2101
2137
|
| Field | Meaning |
|
|
2102
2138
|
| --- | --- |
|
|
2103
2139
|
| `bone` | **required.** The bone the region is carried by. It has to already exist — a bone a physics constraint targets is part of the skeleton, not a side effect of a mesh |
|
|
2104
|
-
| `mask` | **required.** A greyscale sheet in the part's own pixel grid: the level IS the weight, black still and white fully carried, sampled at each vertex. Alpha is not read — a transparent pixel is black |
|
|
2140
|
+
| `mask` | **required.** A greyscale sheet in the part's own pixel grid: the level IS the weight, black still and white fully carried, sampled at each vertex. Alpha is not read — a transparent pixel is black. The grid is the drawing's, as for a depth map: on a `scale:` page the mask is still the art's size ([#762](https://github.com/firejune/rigc/issues/762)) |
|
|
2105
2141
|
|
|
2106
2142
|
The remainder always stays on the slot bone, so every vertex closes at 1 by
|
|
2107
2143
|
construction rather than by `A20` catching it later. `build` and `explain` report
|
|
@@ -2131,7 +2167,7 @@ bone, must close at 1, and at least one must actually be carried.
|
|
|
2131
2167
|
| the slot's own bone | `moves nothing — a soft region needs a bone that can move independently` |
|
|
2132
2168
|
| a mask that is black everywhere | `carries no vertex of this mesh — every one of its 49 vertices samples black` |
|
|
2133
2169
|
| a colour mask | `is not greyscale — pixel (0, 0) is rgb(10, 200, 10)` |
|
|
2134
|
-
| a mask that is not the part's size | `is 48x32 and the part is 96x64` |
|
|
2170
|
+
| a mask that is not the part's size | `is 48x32 and the part is 96x64` — on a `scale:` page, `and the part is a 96x64 drawing — 48x32 texels on a page that declares scale: 0.5` |
|
|
2135
2171
|
| a mask that is not on disk | `the soft mask "x.png" is not at …` |
|
|
2136
2172
|
|
|
2137
2173
|
⭐ **One depth pass buys both, on one part.** A carried mesh has two bones on the
|
|
@@ -2987,10 +3023,11 @@ the mapping above and drives the bone to the value it names, rather than playing
|
|
|
2987
3023
|
the animation on a track while the slider sits at its neutral. §4.11.4 is what
|
|
2988
3024
|
that changes and why it matters at `mix: 1`.
|
|
2989
3025
|
|
|
2990
|
-
🔸 **`scale` is
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3026
|
+
🔸 **`scale` is emitted as its float32**, like every other number rigc writes, so
|
|
3027
|
+
`1/60` ships as `0.016666668` — 8e-8 relative (it was `0.016667`, 2e-5, on the
|
|
3028
|
+
six-decimal grid before issue #716). Invisible in the middle of the range; it
|
|
3029
|
+
shows at the top of it, where a 60° turn then applies at 1.00000008 s rather than
|
|
3030
|
+
1 s. With `loop: false` that is the last frame and harmless, with
|
|
2994
3031
|
`loop: true` it wraps to the start. When the range comes from a *measured* ceiling
|
|
2995
3032
|
— the turn ceiling `build` reports for a depth mesh (§3.4, `depth`) is the natural
|
|
2996
3033
|
one — pick `to`/`scale` so the endpoint lands **inside** the duration rather than
|
|
@@ -3561,26 +3598,33 @@ consumer's, decided by dressing the skeleton rather than by the animation.
|
|
|
3561
3598
|
Seconds, not frames: nothing requires a key to land on any frame grid, and a
|
|
3562
3599
|
reference rendered at some rate says nothing about where its keys are. Put keys
|
|
3563
3600
|
where the motion changes.
|
|
3564
|
-
- **Key times are
|
|
3565
|
-
|
|
3601
|
+
- **Key times are emitted as float32s like every other number, and never stored
|
|
3602
|
+
LATER than you wrote them.** Every emitted number is the shortest decimal naming
|
|
3603
|
+
its float32 — the text the editor writes, and the precision the runtime keeps,
|
|
3604
|
+
because `spine-core` reads a timeline's frames into a `Float32Array` (issue #716).
|
|
3605
|
+
A time that already names a float — `0.5`, `0.2`, the editor's `1.4333333` — is
|
|
3606
|
+
written as you wrote it. A time that does not — `2/12` computed in doubles, a key
|
|
3607
|
+
moved by `lag` or `stagger` — steps to the **largest float not above it**, never
|
|
3608
|
+
to nearest. A key time is a position against the sample grid a player will step,
|
|
3566
3609
|
and the two directions of a half-step error are not the same size. `2/12 s` and
|
|
3567
|
-
`5/30 s` are both 0.16666666
|
|
3568
|
-
|
|
3569
|
-
a whole frame late, with nothing raised. On a
|
|
3570
|
-
timeline always is) that is the wrong picture
|
|
3571
|
-
the spineboy run's muzzle flare fired a frame
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3610
|
+
`5/30 s` are both 0.16666666…, and the nearest float to that, 0.1666666716…, is
|
|
3611
|
+
*larger* than either, so a key stored there is applied at sample **3** of a 12 fps
|
|
3612
|
+
playback and not sample 2 — a whole frame late, with nothing raised. On a
|
|
3613
|
+
**stepped** timeline (an attachment timeline always is) that is the wrong picture
|
|
3614
|
+
rather than a slightly wrong value: the spineboy run's muzzle flare fired a frame
|
|
3615
|
+
late for exactly this, on the six-decimal grid rigc emitted until #716, until the
|
|
3616
|
+
run's own frame check caught it (issue #99). Stepping down cannot do that; the
|
|
3617
|
+
worst it can do is store a key one float early — 1.5e-8 s at 1/6 s — on the sample
|
|
3618
|
+
it was written for. ⚠️ What this does **not** protect you from is rounding your
|
|
3619
|
+
own times before you write them — write `2/12`, not `0.1667`, and let the compiler
|
|
3576
3620
|
do the quantising.
|
|
3577
|
-
- 🚨 **Nor does it protect a stepped key whose time
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
a stepped key there sits above the sample that was meant to see it. On an
|
|
3621
|
+
- 🚨 **Nor does it protect a stepped key whose time ALREADY names a float.**
|
|
3622
|
+
Such a time is written exactly as you wrote it, and the sampler does not arrive
|
|
3623
|
+
there: a player — and `sampleAnimation`, and therefore `check` — reaches sample *i*
|
|
3624
|
+
by accumulating `1/fps` *i* times, which for many *i* lands a few ULPs **below**
|
|
3625
|
+
`i/fps`. `2/12` is saved by the rule above precisely because it is *not* a float;
|
|
3626
|
+
`0.25`, `0.5`, `0.75`, `1` and every other multiple of `0.25 s` is one exactly,
|
|
3627
|
+
and a stepped key there sits above the sample that was meant to see it. On an
|
|
3584
3628
|
interpolated timeline that costs a few ULPs of value and nothing else. On a
|
|
3585
3629
|
**stepped** one it is the whole frame — and on the last sample it is the whole
|
|
3586
3630
|
event, because there is no later sample to catch it. Measured on rung 5's 6.5 s
|
|
@@ -3590,16 +3634,22 @@ consumer's, decided by dressing the skeleton rather than by the animation.
|
|
|
3590
3634
|
`6.499999999999994` — which read as a frame-change disagreement the pose series had
|
|
3591
3635
|
already fixed, and cost that run three builds
|
|
3592
3636
|
([`2026-08-26-rung5-1`](https://github.com/firejune/rigc/blob/main/bench/runs/2026-08-26-rung5-1/LOOP.md), §8). ⇒ **For a
|
|
3593
|
-
stepped timeline, write `T − 1e-6`
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3637
|
+
stepped timeline, write a time a little below `T` — `T − 1e-6` still works, at
|
|
3638
|
+
any magnitude — rather than `T`.** What makes it work is not the size of the step:
|
|
3639
|
+
any time below `T` is stored on a float below `T`, because a time that names a
|
|
3640
|
+
float is stored at that float and one that does not steps down, and the float
|
|
3641
|
+
below `T` is below the accumulated sample too (one float step is 3.0e-8 s at
|
|
3642
|
+
0.5 s and 4.8e-7 s at 6.5 s, against the few-ULP shortfall of the sampler). What
|
|
3643
|
+
bounds it from the other side is the frame: `T − 1e-6` cannot reach the previous
|
|
3644
|
+
sample, 83,333 µs away at 12 fps. One ULP late loses the frame. This is the same
|
|
3645
|
+
asymmetry the rule above turns on, one float further in.
|
|
3597
3646
|
- **No key may land past the animation's `duration`.** Nothing that plays the
|
|
3598
3647
|
animation for the duration it declares ever reaches such a key, so it is a
|
|
3599
3648
|
compile error — checked on **every timeline**, not just on the latest key in the
|
|
3600
|
-
animation
|
|
3601
|
-
|
|
3602
|
-
|
|
3649
|
+
animation, against the float the key is stored at. The tolerance is one float32
|
|
3650
|
+
step at the duration — 4.8e-7 s at 5 s, 3.8e-6 s at 32 s — which is the most a
|
|
3651
|
+
key you put exactly *on* a duration the float cannot hold is stored past it, so
|
|
3652
|
+
such a key is fine. R7's frame of slack does not apply in this direction and
|
|
3603
3653
|
would not see this: rung 6 rounded its key times to 4 dp somewhere upstream, its
|
|
3604
3654
|
one-frame reveal landed 0.000034 s past a 68/12 s duration, another track was
|
|
3605
3655
|
already sitting on the declared duration so the animation's *longest* key time
|
|
@@ -3787,7 +3837,7 @@ group members (the per-member values of one track, side by side — issue #295)
|
|
|
3787
3837
|
t = 0.20944 rad
|
|
3788
3838
|
cos t − 1 = -0.021852
|
|
3789
3839
|
sin t = 0.207912
|
|
3790
|
-
shift the parent carries = −carried·sin t = -35.
|
|
3840
|
+
shift the parent carries = −carried·sin t = -35.344986
|
|
3791
3841
|
eye_l 5.513083 <- -62 at depth 150
|
|
3792
3842
|
eye_r 2.803385 <- 62 at depth 150
|
|
3793
3843
|
brow_l 3.849789 <- -62 at depth 158
|
|
@@ -4086,7 +4136,11 @@ exactly what a mix that was 0 at setup needs said.
|
|
|
4086
4136
|
above 1 is a real editor idiom rather than a mistake.
|
|
4087
4137
|
- ⚠️ A mix is only read by the runtime if the constraint declares the matching
|
|
4088
4138
|
`properties` mapping (§3.5). Keying `mixScaleY` on a constraint that maps
|
|
4089
|
-
rotation only is dead data — legal, loaded, and it moves nothing.
|
|
4139
|
+
rotation only is dead data — legal, loaded, and it moves nothing. ⚠️ So is every
|
|
4140
|
+
mix a key **omits**, and that one looks like a rescue: the parser reads an
|
|
4141
|
+
omitted mix as 1, so a key of `mixRotate: 0` alone on a rotate-only constraint
|
|
4142
|
+
carries five mixes of 1 that nothing reads. `A48` judges only the mixes of the
|
|
4143
|
+
properties the constraint drives (§4.12).
|
|
4090
4144
|
- The refusals are §4.9's, with `transform` in place of `ik`.
|
|
4091
4145
|
|
|
4092
4146
|
### 4.11 `deform` — moving an attachment's vertices
|
|
@@ -4439,15 +4493,15 @@ bun cli.ts explain --rig gallery/portrait/rig.json \
|
|
|
4439
4493
|
t = 0.20944 rad
|
|
4440
4494
|
cos t − 1 = -0.021852
|
|
4441
4495
|
sin t = 0.207912
|
|
4442
|
-
centre shift = −radius·sin t = -35.
|
|
4443
|
-
25 vertices, largest offset 35.
|
|
4444
|
-
v 0 (-7.17493, 0) v 1 (-22.413595, 0) v 2 (-35.
|
|
4496
|
+
centre shift = −radius·sin t = -35.344986
|
|
4497
|
+
25 vertices, largest offset 35.344986px at vertex 2
|
|
4498
|
+
v 0 (-7.17493, 0) v 1 (-22.413595, 0) v 2 (-35.344986, 0) v 3 (-27.65817, 0)
|
|
4445
4499
|
v 4 (-14.255108, 0) v 5 (-14.255108, 0) v 6 (-14.255108, 0) v 7 (-14.255108, 0)
|
|
4446
|
-
v 8 (-14.255108, 0) v 9 (-27.
|
|
4500
|
+
v 8 (-14.255108, 0) v 9 (-27.65817, 0) v 10 (-35.344986, 0) v 11 (-22.413595, 0)
|
|
4447
4501
|
v 12 (-7.17493, 0) v 13 (-7.17493, 0) v 14 (-7.17493, 0) v 15 (-7.17493, 0)
|
|
4448
|
-
v 16 (-22.413595, 0) v 17 (-35.
|
|
4449
|
-
v 20 (-35.
|
|
4450
|
-
v 24 (-27.
|
|
4502
|
+
v 16 (-22.413595, 0) v 17 (-35.344986, 0) v 18 (-27.65817, 0) v 19 (-22.413595, 0)
|
|
4503
|
+
v 20 (-35.344986, 0) v 21 (-27.65817, 0) v 22 (-22.413595, 0) v 23 (-35.344986, 0)
|
|
4504
|
+
v 24 (-27.65817, 0)
|
|
4451
4505
|
```
|
|
4452
4506
|
|
|
4453
4507
|
The curve reads `stepped` where the spec says `"ease": "swell"`, and that is
|
|
@@ -4455,12 +4509,17 @@ The curve reads `stepped` where the spec says `"ease": "swell"`, and that is
|
|
|
4455
4509
|
offsets, so the segment between them would draw nothing and is written the way
|
|
4456
4510
|
the editor writes it.
|
|
4457
4511
|
|
|
4458
|
-
📌 **Float behaviour, stated.** The closed forms are evaluated in float64
|
|
4459
|
-
quantised
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
a
|
|
4463
|
-
|
|
4512
|
+
📌 **Float behaviour, stated.** The closed forms are evaluated in float64,
|
|
4513
|
+
quantised onto the model's own 1e-6 grid and emitted as that value's float32 name
|
|
4514
|
+
like every other number, so the same spec emits the same bytes and
|
|
4515
|
+
`A18_DETERMINISTIC_EMIT` proves it on a second compile. The grid is absolute on
|
|
4516
|
+
purpose: a model's identities — a wave sampled on its zero crossings, a whole
|
|
4517
|
+
revolution — are exact zeros float64 misses by ~1e-16, and the refusal of a key
|
|
4518
|
+
that states a deformation and evaluates to nothing is decided on it (issue #350);
|
|
4519
|
+
a float32 alone is relative and has no zero to land on. The runtime then loads
|
|
4520
|
+
the numbers into a `Float32Array`, which is equally true of a hand-written table —
|
|
4521
|
+
the difference the generator makes is that the numbers now agree with a stated
|
|
4522
|
+
model instead of with a transcription.
|
|
4464
4523
|
|
|
4465
4524
|
🔭 **Both adjacent asks have since landed.**
|
|
4466
4525
|
[#295](https://github.com/firejune/rigc/issues/295) was the same complaint about
|
|
@@ -4964,6 +5023,35 @@ constraint declaring `mixGlobal`, since only the physics family has one. The ref
|
|
|
4964
5023
|
says both halves — `path constraint "P" has mixRotate 0, mixX 0 and mixY 0 at setup
|
|
4965
5024
|
and none of the 2 animations keys its mix above 0; …` — and names both repairs.
|
|
4966
5025
|
|
|
5026
|
+
⚠️ **The same question of an `ik` and a `transform` constraint is `A47` and `A48`**
|
|
5027
|
+
([#765](https://github.com/firejune/rigc/issues/765)); until then no assertion asked
|
|
5028
|
+
it, and a rig resting either kind muted with nothing keying it gated green with 0
|
|
5029
|
+
failures. [measured] on generated fixtures, an ik at `mix` 0 and a transform at
|
|
5030
|
+
every mix 0, each with nothing keying it and each keyed to 0 only, pose every bone
|
|
5031
|
+
exactly where the same rig with no constraint does. They read the timelines through
|
|
5032
|
+
the same helper as `A23`/`A36`/`A37`, so a lifted Bezier between two keys of 0 is a
|
|
5033
|
+
rescue and a 0-only timeline is not, with two differences that are the runtime's:
|
|
5034
|
+
|
|
5035
|
+
- **Live is `!== 0`, not `> 0`.** `IkConstraint.update` returns on `mix === 0`, and
|
|
5036
|
+
a transform applies a property only when its own mix `!== 0`, so a negative mix
|
|
5037
|
+
runs the constraint inverted. [measured] five transform constraints across four of
|
|
5038
|
+
the editor's example exports (`6-arcs-pro`, `8-follow-through-pro-ball`,
|
|
5039
|
+
`sack-pro` twice, `spineboy-pro`) rest at `mixX` = `mixY` = −1 with nothing keying
|
|
5040
|
+
them, each moves its bones against the same constraint at every mix 0, and a
|
|
5041
|
+
`> 0` reading refuses all five. `A36`/`A37` still read `> 0`.
|
|
5042
|
+
- **A transform is judged on the mixes of the properties it drives.**
|
|
5043
|
+
`TransformConstraint.update` returns early only when all six mixes are 0, but a
|
|
5044
|
+
property is applied by its own mix, and a key omitting a mix reads it as 1
|
|
5045
|
+
(§4.10). [measured] a rotate-only transform keyed to `mixRotate: 0` alone — five
|
|
5046
|
+
mixes of 1 on the loaded timeline — and one keyed to `mixX: 1` both pose exactly
|
|
5047
|
+
where no constraint does, and both are refused. A transform whose `properties`
|
|
5048
|
+
name no `to` at all is refused with its own sentence: no mix it carries is read.
|
|
5049
|
+
|
|
5050
|
+
`ik constraint "reach" has mix 0 at setup and none of the 1 animation keys its mix
|
|
5051
|
+
above 0; update() returns on mix 0, so "upper" never reaches for "goal" — rest it
|
|
5052
|
+
above 0, or key its mix above 0 in an animation`. A rig resting at 0 and keyed up by
|
|
5053
|
+
the animation that needs it — spineboy's aim — is refused by neither.
|
|
5054
|
+
|
|
4967
5055
|
### 4.13 `sequence` — which frame of a numbered series shows
|
|
4968
5056
|
|
|
4969
5057
|
The other attachment timeline, beside `deform` and for the same reason: its key is
|
|
@@ -5411,7 +5499,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
|
|
|
5411
5499
|
| `A16_SKELETON_VERSION_4_3` | both | the `skeleton.spine` label is not on the 4.3 line (`4.3`, `4.3.N`, `4.3.N-suffix`) |
|
|
5412
5500
|
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) — as it is for `A06`, `A19` and `A27`; see `A07` ([#608](https://github.com/firejune/rigc/issues/608)) |
|
|
5413
5501
|
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
|
|
5414
|
-
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the
|
|
5502
|
+
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the base plate may be opaque, and which image that is is decided once for both routes ([#770](https://github.com/firejune/rigc/issues/770)): the plate the build names — on a cut manifest, the part whose window is the crop — and, only when it names none, an image at least the stage's size. The rig's statement comes first because the two can disagree: a stage stated small enough for an overlay to cover would otherwise exempt that overlay. A rig spec cannot name a base plate, so a stageless rig-spec build, and `validate <dir>` on a stageless skeleton, has nothing to decide it; an opaque part there is refused with *"nothing here decides which image that is: this skeleton declares no stage size to measure one against"* and the two ways to decide it — a `skeleton` stage the plate covers, or a cut manifest (for `validate`, the specs it was built from: `--rig`, `--motion` and the `--manifest`). Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art ([#266](https://github.com/firejune/rigc/issues/266)). ⚠️ **That scan states its verdict over the texels it READ, and never over texels that are not on the page** ([#705](https://github.com/firejune/rigc/issues/705)): a rectangle partly on its page is judged over the part that is on it, and the message carries both counts — `opaque in every one of the 77 texels of its 12x8 rectangle at -1,-1 … the other 19 of the 96 it declares are not on the page and are not measured here`. A rectangle with **no** texel on the page is reported **not measured** by name — the region, its rectangle, the page image's size, and the pointer to `A06`, which is the rule that judges a region's rectangle — and no verdict about opacity is printed at all. ⚠️ **A page whose IMAGE is not the size the atlas declares for it is the same non-measurement for every region on it** ([#715](https://github.com/firejune/rigc/issues/715)), and #705's clause does not cover that case: a page rescaled after packing leaves most rectangles partly on it, at coordinates that address a different part of the picture, so the scan came back with a confident verdict over texels nobody had located — on a two-region pack at a uniform 0.5 an opaque part's failure **disappeared**, the scan having found a transparent texel 32 texels away from it. The row names the page's two sizes and points at `A06`, which judges the page grid and prints the header that repairs it (§0.2). It stays a failure rather than becoming a SKIP because a SKIP is per ASSERTION: it would delete the verdicts on every other part of the same page, and an assertion cannot be skipped and failed at once without the report counting it twice. Before #705 the walk was silent about its own reach, so a part nobody could read printed *opaque in every one of its 12x8 texels* over zero of them, which is a refusal pointing at the wrong file: the art it names may be transparent, and the repair is the rectangle in `A06`'s row above. ⚠️ **A page file that cannot be read as PNG at all is the same non-measurement for every part on it** ([#732](https://github.com/firejune/rigc/issues/732)): one row per page naming its parts and pointing at `A06`, which names what the file is — where it used to print `threw: cannot decode PNG …: unexpected end of file`, an inflate error about a file that was never a PNG. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
5415
5503
|
| `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not ([#684](https://github.com/firejune/rigc/issues/684)). Fix the rig spec's `controls`/`chain`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
5416
5504
|
| `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1 |
|
|
5417
5505
|
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
@@ -5435,10 +5523,12 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
|
|
|
5435
5523
|
| `A40_SLIDERS_COMPOSE_ON_A_SHARED_TARGET` | both | two or more sliders whose animations key the same timeline, where a later one is not `additive` — it writes that property outright at `mix: 1` and every earlier slider on it is dead (§3.5.2). Also fires when the shared timeline **cannot** be applied additively (a slot colour, an attachment swap, a draw order, an ik mix, a path's `spacing`, most physics properties), where `"additive": true` is not the fix and one of the two has to go. ⭐ Which of the two it is, is **posed rather than read off `Timeline.additive`**: the shared timeline is applied twice with `add` set and the detail says what it did ([#655](https://github.com/firejune/rigc/issues/655) — two classes declare that flag falsely about themselves, so a path constraint's `mix` and a slider's `time` were refused although they compose). The detail names the bone or slot and the property, every slider keying it in `constraints` order with its flag, which one wins today, and the class that was posed. Four shapes are deliberately not findings: a slider below `mix: 1` or with its `mix` keyed (the apply is then a lerp from the current pose, not an overwrite), two `skinRequired` sliders no skin activates together, two sliders on different properties, and a shared timeline that writes **nothing a pose holds** — an `events` timeline fires no event under a slider (`firedEvents` is null), so neither slider has anything there for the other to erase. **SKIP** when fewer than two sliders are at full authority; a PASS means two were compared |
|
|
5436
5524
|
| `A41_PHYSICS_SURVIVES_EDITOR_ROUND_TRIP` | both | a physics constraint driving a component the **Spine editor** cannot hold, on a rig that declared `invariants.editorRoundTrip` (§3.7). The editor's physics model holds `x` and `y` only, with no cap on how many at once, so a constraint driving `rotate`, `scaleX` or `shearX` is imported, exported and handed back driving **nothing** — measured over three rigs and twelve constraints with the predictions written first ([#540](https://github.com/firejune/rigc/issues/540)). The detail names the constraint and each component. ⚠️ rigc's own output is correct — every runtime plays a rotation jiggle — so this is opt-in and the default is *not* silence: on a rig that declares nothing it **SKIPs**, and the SKIP names the constraint and the component anyway, so an author learns without having asked. Fix by driving the constraint in `x`/`y`, or by dropping the declaration if the rig never goes near the editor. Disjoint from `A23_PHYSICS_CONSTRAINT_EFFECTIVE` by construction: A23 refuses an **empty** driven set, which is what comes back from the editor, and this refuses a non-empty one that will not survive going in. **SKIP** also when the rig declares the editor and carries no physics constraint at all |
|
|
5437
5525
|
| `A42_DRIVEN_CONSTRAINTS_UPDATE_AFTER_THEIR_DRIVER` | both | a slider whose animation keys a property of a constraint **at or before it** in `constraints` (§3.5.2) — a slider's `mix` or `time`, an ik or transform mix, a path `position`, `spacing` or `mix`, any physics value. That array is the update order for every kind, and each constraint reads its own applied pose when its turn comes — `Slider.update` takes `mix` as the alpha it applies with and `time` as the time it applies at, `PhysicsConstraint.update` returns on `mix` 0 before reading the rest — so the key lands after the only read of it and `Posed.resetConstrained` discards it before the next frame: what the driven constraint drives is dead at every position of the driving dial, although its pose still holds the number ([#658](https://github.com/firejune/rigc/issues/658), [#665](https://github.com/firejune/rigc/issues/665)). The detail names the slider, the driven constraint with its kind, both array indices, the property, the runtime class whose `update` reads it, and the animation the key sits in. Fix by moving the driver earlier, or by keying that property from a slider that already is. **The two indices equal is the same failure**: a slider cannot key its own `mix` or `time`, and one muted at setup that keys its own `mix` up never applies anything at all — `A37` is silent there, because it asks whether *an* animation keys the mix and not which one. **Two shapes it deliberately leaves out**, both measured: a `physics` `reset` key, which fires on a crossed frame time and so never fires from a slider at all, in either order — the reorder would repair nothing; and a physics timeline naming no constraint, which is every physics constraint declaring that property global and IS refused for the ones already run. Disjoint from `A40` by construction: `A40` asks who writes a shared property last and excludes every slider whose `mix` is keyed, this asks whether anything reads what was written. **SKIP** when the skeleton declares no slider, and when no slider's animation keys a constraint property — that SKIP names any `reset` keys it found — a pass means a driver and a driven were compared |
|
|
5438
|
-
| `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (§3.3) or an `rgba2` / `rgb2` timeline (§4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** — it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** — `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` or `rgb2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. ⚠️ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. An `rgb2` key's light colour is compared over its three channels only: the light alpha is not its to state, and that it is left where it was is measured in the selftest (`S85`). **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` or `rgb2` — there is then no two-colour tint to read back |
|
|
5526
|
+
| `A43_TWO_COLOR_TINT_LOADS_AND_POSES_AS_WRITTEN` | both | a slot's `dark` (§3.3) or an `rgba2` / `rgb2` timeline (§4.4) that the runtime does not hold as the file states it. Three shapes, all of which parse in silence: a `dark` the slot reader **drops** — it takes the field through a truthiness test, so `""` is discarded without a word and the slot renders with one colour; a `dark` that is **not six hex digits** — `Color.setFromString` slices fixed offsets and stores whatever `parseInt` gives back, so `"4020"` loads a channel of `NaN`; and an `rgba2` or `rgb2` timeline on a slot with **no `dark` at all**, where the runtime allocates no dark colour and the first `state.apply` throws in the consumer's process. The keyed half is read by posing: the animation is stepped to each key's own time — at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298…`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before (a correct file was refused that way until [#771](https://github.com/firejune/rigc/issues/771)), and the posed `color` and `darkColor` are compared against the hex the key states, to half a quantisation step (`1/510`). The detail names the slot, the value found and the value required. ⚠️ The required value is parsed **here** and not through `Color.fromString`, because a check that read it out of the parser it is checking would agree with that parser whatever it did. `compile.ts` refuses the third shape outright in a rig rigc builds; this is the same fact held against a skeleton it did not write. An `rgb2` key's light colour is compared over its three channels only: the light alpha is not its to state, and that it is left where it was is measured in the selftest (`S85`). **SKIP** when no slot declares a `dark` and no animation keys an `rgba2` or `rgb2` — there is then no two-colour tint to read back |
|
|
5439
5527
|
| `A44_LINKED_MESH_STATES_NO_GEOMETRY_OF_ITS_OWN` | both | a **linked mesh** (§3.4) — `type: "linkedmesh"`, or a `type: "mesh"` carrying `source` — that also states `uvs`, `triangles`, `vertices`, `hull` or `edges`. The parser returns from the `source` branch before `readVertices` (`SkeletonJson.ts:582-586`), so those keys are read by **nothing at all** and `setSourceMesh` fills the attachment with the source's arrays instead: the file says one mesh and every runtime draws another, in silence. The detail names the attachment by skin, slot and placeholder, every key it states, the `source` and where the parser looks for it — the two defaults spelled out, because an omitted `skin` is the **default** skin rather than the one the link is written in — and the shape the keys describe beside the shape the attachment loaded. ⚠️ **`width`/`height` are not part of this.** `setSourceMesh` overwrites both with the source's, so they are as dead at runtime — but the parser reads them (`:569-570`), the format carries them on a link and rigc emits them, so refusing them would refuse every link rigc writes (§3.4). `compile.ts` refuses the same shape outright in a rig rigc builds (§5.1); this is that fact held against a skeleton it did not write, and `ingest` reports it as `ATTACHMENT_LINK_GEOMETRY` ([INGEST §2.0](INGEST.md)). **SKIP** when no attachment in the skeleton takes its geometry from another — which is almost every skeleton, so a pass here means a link was read ([#710](https://github.com/firejune/rigc/issues/710)) |
|
|
5440
|
-
| `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | both | an `rgb` or `alpha` timeline (§4.4) the runtime does not hold as the file states it, in one of two shapes that both parse in silence. **A channel keyed twice**: another colour timeline of the same slot in the same animation poses a channel this one poses — `rgba` beside `alpha` is the shape a converter leaves when it writes a separable `rgb` back as `rgba` next to the `alpha` it kept. Every colour timeline poses its channels at every time, the setup value before its first key included, so the one the file states later overwrites the other everywhere; the detail names both timelines, the channel, and which one survives. **A key not posed as written**: the animation is stepped to each key's own time and the posed r g b (for `rgb`, against the hex, to half a quantisation step) or alpha (for `alpha`, against `value`, whose absence the parser reads as 0) is compared — a colour that is not six hex digits loads as NaN, and a key whose time another key repeats is read by nothing. ⚠️ An `rgb` alone written as an `rgba` holding the setup alpha is **not** caught and cannot be from the file: it is a correct `rgba`, and the difference shows only under another track that moves the alpha. The loaded timeline class and the channels a separable timeline leaves alone are measured in the selftest (`S83`–`S85`) rather than here, because against the linked parser neither can come out wrong. The channel table is `SLOT_COLOR_CHANNELS` in `src/timelines.ts`, shared with the compiler's refusal and held to the runtime's own property ids (`S89`). **SKIP** when no animation keys an `rgb` or `alpha` — there is then no separable slot colour to read back |
|
|
5441
|
-
| `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | both | a **numbered series** (§3.4.3, §4.13) that the runtime does not show as the file states it. Every shape below loads without a word, measured on spine-core 4.3.13 ([#729](https://github.com/firejune/rigc/issues/729)). **The block**: a `sequence` with no `count` (`readSequence` reads 0, and the attachment holds no region) or a `setup` at or past `count` (`Sequence.resolveIndex` clamps it to the last frame). **The keys**: a `mode` outside the seven — `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` — loads as `hold`; an `index` that is fractional (`index << 4` truncates it) or past the end (clamped); an advancing mode at an effective delay of 0 (the parser carries a key's `delay` from the key before; `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so it never advances); a timeline on an attachment that carries no block (the parser gives every region a one-region series, so every mode shows it). **The pose**: every key is stepped to mid-frame sample times — enough to wrap every mode — and the region the slot shows is held to the frame the file's own statement gives, the arithmetic of `SequenceTimeline.applyToSlot` and the names of `Sequence.getPath` transcribed rather than read off the loaded timeline, so the check is not the runtime agreeing with itself. Before the first key the frame is `setup`. ⚠️ A sample where the slot shows another attachment is not compared, because the runtime writes nothing there; a timeline with no comparable sample is counted in `stats.sequenceSamplesUnshown`. `compile.ts` refuses every one of these shapes in a spec (§5.1); this is them held against a skeleton it did not write. **SKIP** when no attachment carries a `sequence` block and no animation keys a `sequence` timeline |
|
|
5528
|
+
| `A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN` | both | an `rgb` or `alpha` timeline (§4.4) the runtime does not hold as the file states it, in one of two shapes that both parse in silence. **A channel keyed twice**: another colour timeline of the same slot in the same animation poses a channel this one poses — `rgba` beside `alpha` is the shape a converter leaves when it writes a separable `rgb` back as `rgba` next to the `alpha` it kept. Every colour timeline poses its channels at every time, the setup value before its first key included, so the one the file states later overwrites the other everywhere; the detail names both timelines, the channel, and which one survives. **A key not posed as written**: the animation is stepped to each key's own time — at the key **as the runtime stores it**: spine-core keeps key times as 32-bit floats, so a key at `0.2` is posed at `0.20000000298…`, the later of the two, and not one float step before it, where a first key still shows the setup value and a stepped key the one before (a correct file was refused that way until [#771](https://github.com/firejune/rigc/issues/771)), and the posed r g b (for `rgb`, against the hex, to half a quantisation step) or alpha (for `alpha`, against `value`, whose absence the parser reads as 0) is compared — a colour that is not six hex digits loads as NaN, and a key whose time another key repeats is read by nothing. ⚠️ An `rgb` alone written as an `rgba` holding the setup alpha is **not** caught and cannot be from the file: it is a correct `rgba`, and the difference shows only under another track that moves the alpha. The loaded timeline class and the channels a separable timeline leaves alone are measured in the selftest (`S83`–`S85`) rather than here, because against the linked parser neither can come out wrong. The channel table is `SLOT_COLOR_CHANNELS` in `src/timelines.ts`, shared with the compiler's refusal and held to the runtime's own property ids (`S89`). **SKIP** when no animation keys an `rgb` or `alpha` — there is then no separable slot colour to read back |
|
|
5529
|
+
| `A46_SEQUENCE_ATTACHMENTS_SHOW_THE_FRAME_THE_FILE_STATES` | both | a **numbered series** (§3.4.3, §4.13) that the runtime does not show as the file states it. Every shape below loads without a word, measured on spine-core 4.3.13 ([#729](https://github.com/firejune/rigc/issues/729)). **The block**: a `sequence` with no `count` (`readSequence` reads 0, and the attachment holds no region) or a `setup` at or past `count` (`Sequence.resolveIndex` clamps it to the last frame). **The keys**: a `mode` outside the seven — `hold`, `once`, `loop`, `pingpong`, `onceReverse`, `loopReverse`, `pingpongReverse` — loads as `hold`; an `index` that is fractional (`index << 4` truncates it) or past the end (clamped); an advancing mode at an effective delay of 0 (the parser carries a key's `delay` from the key before; `(time - keyTime) / 0` is Infinity and `Infinity \| 0` is 0, so it never advances); a timeline on an attachment that carries no block (the parser gives every region a one-region series, so every mode shows it). **The pose**: every key is stepped to mid-frame sample times — enough to wrap every mode, and a `hold` key to its own time as the runtime stores it, a 32-bit float ([#771](https://github.com/firejune/rigc/issues/771)) — and the region the slot shows is held to the frame the file's own statement gives, the arithmetic of `SequenceTimeline.applyToSlot` and the names of `Sequence.getPath` transcribed rather than read off the loaded timeline, so the check is not the runtime agreeing with itself. Before the first key the frame is `setup`. ⚠️ A sample where the slot shows another attachment is not compared, because the runtime writes nothing there; a timeline with no comparable sample is counted in `stats.sequenceSamplesUnshown`. `compile.ts` refuses every one of these shapes in a spec (§5.1); this is them held against a skeleton it did not write. **SKIP** when no attachment carries a `sequence` block and no animation keys a `sequence` timeline |
|
|
5530
|
+
| `A47_IK_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | an ik constraint resting at `mix` 0 that no animation keys **away from 0** (§4.9, §4.12). `IkConstraint.update` returns on `mix === 0`, so it sits in the update cache and moves nothing. The keys are read the way `A23`/`A36`/`A37` read theirs — every value the loaded timeline poses on its `mix` channel, Bezier samples included — so a timeline keying 0 only is no rescue: [measured] it poses every bone exactly where the same rig with no constraint does, and before [#765](https://github.com/firejune/rigc/issues/765) it passed. Live is the runtime's `!== 0`, so a negative mix is not refused. `ik constraint "C" has mix 0 at setup and none of the 1 animation keys its mix above 0; update() returns on mix 0, so "upper" never reaches for "goal" — rest it above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no ik constraint |
|
|
5531
|
+
| `A48_TRANSFORM_CONSTRAINT_NOT_MUTED_THROUGHOUT` | both | a transform constraint none of whose mixes **for a property it drives** is away from 0 at setup or on any value an animation poses (§4.10, §4.12), or one whose `properties` name no `to` at all. A property is applied only when its own mix `!== 0`, and a key that omits a mix reads it as 1, so the six-mix early return of `TransformConstraint.update` would take a key of `mixRotate: 0` alone as a rescue — [measured] that key, and one keying `mixX` 1 on a rotate-only constraint, pose every bone exactly where no constraint does ([#765](https://github.com/firejune/rigc/issues/765)). A negative mix runs, and five transforms in the editor's example exports rest at −1. `transform constraint "C" drives rotate and has mixRotate 0 at setup, and none of the 1 animation keys its mix above 0; a mix is read only for a property the constraint drives, and update() skips each one at 0, so nothing ever moves "follower" — rest mixRotate above 0, or key its mix above 0 in an animation`. **SKIP** when the skeleton declares no transform constraint |
|
|
5442
5532
|
|
|
5443
5533
|
`both ◑` marks a mixed assertion: its validity half always runs and its policy
|
|
5444
5534
|
clauses are gated by profile.
|
|
@@ -7809,11 +7899,14 @@ under 4.3.26 (`[152.7006, 305.4012, 458.1019, 610.8025]`) and on an open one und
|
|
|
7809
7899
|
⚠️ **That last reading settles the model and cannot settle the spelling.** A
|
|
7810
7900
|
4-sample chord sum agrees with the runtime's forward difference to about **nine
|
|
7811
7901
|
significant digits** — *below* what float32 can hold, which is why both spellings
|
|
7812
|
-
reproduce both exports exactly, and
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7902
|
+
reproduce both exports exactly, and since issue #716 below what rigc's own file
|
|
7903
|
+
holds too, because rigc now writes each number as its float32. Under the six fixed
|
|
7904
|
+
decimals it wrote until then the file could tell them apart: on both rigs above
|
|
7905
|
+
they rounded apart on the **last** curve, where the running total has accumulated
|
|
7906
|
+
most — `610.802519` against `610.802520`, `1127.735817` against `1127.735818`. So
|
|
7907
|
+
the editor is the evidence for *what* is computed, and only `PathConstraint`
|
|
7908
|
+
itself is evidence for *how* — `PS67`/`PS68` compare the transcription with it at
|
|
7909
|
+
double precision.
|
|
7817
7910
|
|
|
7818
7911
|
⭐ **rigc emits the forward difference itself** since
|
|
7819
7912
|
[#560](https://github.com/firejune/rigc/issues/560) — `pathCurveLengths` in
|
|
@@ -7954,6 +8047,7 @@ Per part:
|
|
|
7954
8047
|
| --- | --- |
|
|
7955
8048
|
| `part`, `path`, `width`, `height` | the PNG, by the name every message uses |
|
|
7956
8049
|
| `placement` | the best placement found — `null` **only** for `empty-part` and `larger-than-canvas`, where nothing was searched |
|
|
8050
|
+
| `walls` | the walls of the search window `placement` stands **on**, whatever the verdict: `[]` when it settled inside, otherwise one `{ axis, edge, window }` per axis — `axis` is `scale` or `rotation`, `edge` is `floor` or `ceiling`, `window` is the flag's own `min,max`. A value on a wall is where the search was held, not where it came to rest — see §11.4 |
|
|
7957
8051
|
| `alternates` | other optima worth reporting, best first. Non-empty means the answer was not unique |
|
|
7958
8052
|
| `ambiguous` | at least one alternate is inside the ambiguity margin. **Choose with something this instrument cannot see** — anatomy, the other frame, or `rigc vote` |
|
|
7959
8053
|
| `rotationFree` | the part is self-similar under rotation, so `rotationDeg` is a placeholder and the value is yours |
|
|
@@ -8008,9 +8102,22 @@ holds nothing because it already contains every angle.
|
|
|
8008
8102
|
That is the case where the window is the first thing to move rather than the
|
|
8009
8103
|
frame or the threshold — eleven parts of one frame came back refused at
|
|
8010
8104
|
`scale=0.500` against art rendered at `0.311` per part pixel, and the message
|
|
8011
|
-
said only that the residual was above `--max-residual`.
|
|
8012
|
-
|
|
8013
|
-
|
|
8105
|
+
said only that the residual was above `--max-residual`. The sentence — with its
|
|
8106
|
+
*"may lie below"* — is printed on a refusal only.
|
|
8107
|
+
- 🔒 **An accepted placement that stopped on a wall says which wall too**, beside
|
|
8108
|
+
the value it holds on the console line and in `walls`:
|
|
8109
|
+
`PLACE head.png x= 45.5 y= 37.5 rot= 0.0° scale=2.000 (the ceiling of --scale 0.5,2) residual=0.0394 unexplained= 10%`
|
|
8110
|
+
is a part drawn at twice the scale of a frame rendered at 1.15 px/unit, whose
|
|
8111
|
+
truth is **2.30** — outside the window — and whose residual at the ceiling still
|
|
8112
|
+
cleared `--max-residual`. *On* a wall is not *near* one: the refinement is clamped
|
|
8113
|
+
to the window, so a value the window held **is** the bound, while a correct
|
|
8114
|
+
placement a window brackets closely settles strictly inside it and carries no
|
|
8115
|
+
mark. A window with no interior (`min === max`) marks nothing — being at its only
|
|
8116
|
+
value says nothing about the answer. ⚠️ The mark names the wall and not the side
|
|
8117
|
+
the truth is on: over the rendered example corpus, a floor held both truths below
|
|
8118
|
+
the window and parts shrunk into their own region whose truth was inside it. An
|
|
8119
|
+
empty `walls` is not evidence either way — a window that excludes the truth can
|
|
8120
|
+
still settle inside on another optimum, which is the caveat two bullets up.
|
|
8014
8121
|
- ⚠️ **A frame whose border has no dominant colour reports `background.unknown`.**
|
|
8015
8122
|
Every pixel then counts as material, the silhouette signal is gone, and the
|
|
8016
8123
|
residual is colour agreement alone. The report says so rather than being quietly
|
|
@@ -8258,7 +8365,7 @@ simply unused; a name the directory lacks is refused `no-part-image` by name.
|
|
|
8258
8365
|
| --- | --- |
|
|
8259
8366
|
| `--anchor <pose.json>` | use this `rigc pose` report instead of running one. Refused together with `--scale` / `--rotation`, which size the internal pass that then does not happen |
|
|
8260
8367
|
| `--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 |
|
|
8261
|
-
| `--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 |
|
|
8368
|
+
| `--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. The hinge step `3°` is a **ceiling** on the step, not the step: a window it does not divide is divided into whole steps no coarser than it, and `search` states the step that division produced, so `--hinge -20,20` prints `hinge -20°–20° step 2.857° (15 rungs)` and each chain part's note says it was searched `in 2.857° steps`. The full turn divides it exactly, so the default walks the same rungs it always did |
|
|
8262
8369
|
| `--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 |
|
|
8263
8370
|
| `--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 |
|
|
8264
8371
|
| `--max-residual <0..1>` | as §11, over the visible pixels (default `0.25`) |
|