rig-parts 1.78.0 → 1.79.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 CHANGED
@@ -341,6 +341,7 @@ agent skill.
341
341
  | `propose --head-box --full <run> --canvas WxH [--no-back-hair] [--write-config <config>]` | propose `seethrough.head_box` from the full run, held inside the painting; `--no-back-hair` leaves the back-hair layer out of that proposal |
342
342
  | `propose --parts --source --out [--compare <config>] [--write-config <config> [--only-missing]]` | propose bones, meshes, regions and an idle; draw the overlay; print, per bone, which check read it or that none did and why (its role, its kind of segment), and write `basis.json` beside the proposal — what each bone rests on (a joint, a part's mask, a ratio, other bones) and, with `--compare`, whether the config's bone differs |
343
343
  | `propose … --keypoints <file> [--person <id>]` | read a posed figure's joints from one explicit file (space, image size, people, each joint observed, occluded or missing, its source): a joint with a position places the neck, hip, chest or a sleeve chain as given, a note says how every joint was used or which rule stood in, and LINT reads the torso and sleeves along the joints instead of by screen y ([AUTHORING §3](docs/AUTHORING.md)) |
344
+ | `propose … --full <run> --head <run> --write-config <config>` | [draft an independent neck](docs/AUTHORING.md#a-neck-that-moves-on-its-own) in an existing sampled body turn and reassemble its full-run source under `<out>/rig` |
344
345
  | `propose … --grid-rule figure\|part` | how each drafted lattice's spacing is chosen: `figure` (the default) one spacing for the figure, round(H / 37) rig px, a part already coarser keeping its own, with a note on its calibration and cost; `part` the reference's rule, a tenth of the part's short side (the oracle examples are proposed under it) |
345
346
  | `propose --parts --source --out --from-config <config> [--keypoints <file> [--person <id>]]` | draw and LINT the config's current bones (by the joints too, given `--keypoints`); proposes nothing, so takes only these flags (not `--full`, `--head`, `--cloth`, `--write-config`, `--wrist-snap`, `--grid-rule`, `--only-missing` or `--compare`) |
346
347
  | `compare --left <file> --right <file> [--map <bonemap.json>]` | compare two skeletons — a config, a proposal or a `rig.json` — bone by bone: origin, parent, tip, length and direction per pair, the bones neither side pairs, the required ones missing, and each side's roles read off the spec; no frame is guessed, and nothing pairs by resemblance (`propose --compare` reads origins only) |
@@ -349,6 +350,11 @@ agent skill.
349
350
  | `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
350
351
  | `build --config --source --full --head --out [--seam] [--project] [--loop] [--lead-in <periods>] [--requirements] [--seam-pairs] [--keep-stills] [--idle-keys ctl\|direct]` | assemble, rig and check in one process, stopping at the first refusal; `--requirements`, `--seam-pairs` and `--keep-stills` are forwarded to check, `--idle-keys` to rig; `--lead-in` (with `--loop`) encodes the loop from the last of n + 1 idle periods played back to back, for a rig whose physics does not end where the render starts it |
351
352
  | `compose --scene <scene.json> --out [--requirements] [--accept]` | bind several characters' green builds into one rig (or, under `--accept`, builds failing only the bars named — for looking, not shipping): every name prefixed `<id>:` under one shared root, each character's offset added at its first-level bones, an optional background plate (its provenance recorded, judged by nothing) drawn first, and the draw order between the characters' slots as the scene file declares it — nothing inferred, nothing scaled; gated through rig-c as `rig` is, then checked ([AUTHORING §7](docs/AUTHORING.md), *Composing several characters*) |
353
+ | `mesh-draft --config <json> --parts <dir> --out <dir>` | draft contour meshes for configured lattices, keeping each only where it holds against its lattice through rig-c; write the revised config and measurement report |
354
+ | `mesh-stats --config <json> --parts <dir>` | print each configured mesh's vertex and triangle counts and their total through the rig stage's construction; write nothing |
355
+ | `painting --keypoints <json> [--person <id>] <png>` | before See-through, read the painting against its declared joints and refuse named background, joint or limb-gap problems; write nothing |
356
+ | `remesh --rig <dir> --config <json> --parts <dir> --part <name> --out <dir>` | with exactly one of `--mesh <entry.json>` or `--delta <mesh-edit.json>`, re-fit one part of a gated rig into a fresh revision, preserving the input |
357
+ | `retune --rig <dir> --config <json> --parts <dir> --out <dir>` | splice the config's constraints and idle onto a gated rig in a fresh revision, carrying its attachments over as bytes |
352
358
  | `guide <RULE \| §<n> \| §<n>.<m> \| §<n>.<m>.<k>>` | print the one row of [docs/AUTHORING.md](docs/AUTHORING.md) that names a rule (a refusal and the bar it enforces, `CHECK_<LINE>` and `<LINE>`, print together), or one section, `###` subsection or `####` under it, read from the installed file when it runs |
353
359
 
354
360
  `rig-parts --help` has every flag. Exit codes: 0 done, 1 input refused (every
@@ -362,10 +368,14 @@ fields, the files a build writes and their spec ids, the environment, and what t
362
368
  package relies on in rig-c — and what is not, is [docs/STABILITY.md](docs/STABILITY.md).
363
369
  Coming from 0.16.0: [docs/MIGRATION.md](docs/MIGRATION.md).
364
370
 
365
- `--write-config <config.json>` saves the three proposals above into an existing
371
+ `--write-config <config.json>` saves the proposals above into an existing
366
372
  regular config file: `seethrough.head_box`, `assemble.plan` plus
367
373
  `assemble.extend_below_crop`, or the present config sections of an ordinary
368
- proposal. For `assemble --propose-plan`, the destination must name `--config`
374
+ proposal. The [neck draft](docs/AUTHORING.md#a-neck-that-moves-on-its-own)
375
+ (`propose … --full --head --write-config`) is another writer: for an existing
376
+ `turn.body` config with one neck plan row, it selects the full-run neck, drafts a
377
+ rigid unit and its depth, and reassembles parts under `<out>/rig`.
378
+ For `assemble --propose-plan`, the destination must name `--config`
369
379
  itself; a different destination is a usage error. Other fields and absent optional
370
380
  sections remain; proposal notes are not copied. Changed whole values are spliced
371
381
  into the original text, preserving untouched bytes. Stage loaders validate the candidate, including `proposed_rig_scale`,
@@ -509,7 +519,7 @@ as this port wrote it in issue #227).
509
519
 
510
520
  ## Requirements
511
521
 
512
- [Bun](https://bun.sh) 1.2 or later. `npm i -D @rig-c/parts` installs the
522
+ [Bun](https://bun.sh) 1.4 or later (`engines.bun >=1.4.0`; CI tests latest only). `npm i -D @rig-c/parts` installs the
513
523
  `rig-parts` command (`npm install -g @rig-c/parts` puts it on `PATH`); it hands off
514
524
  to Bun and says so in one sentence if Bun is not on `PATH`. **`@rig-c/parts` is the
515
525
  package's canonical name; versions 1.0.0-rc.1 to 1.5.3 were published as `rig-parts`, the convenience install name, and
@@ -519,8 +529,8 @@ beside `rig-parts` for the transition, the same launcher under both names. On np
519
529
  both compatibility names stay published beside `@rig-c/parts` at every version, as aliases
520
530
  carrying the same files with only the `name` in `package.json` changed, so a
521
531
  project that depends on either keeps receiving every release; a new
522
- install names `@rig-c/parts` ([RELEASING.md](RELEASING.md), *The registry side*). rig-c
523
- comes with it as a dependency. No Spine runtime does: since
532
+ install names `@rig-c/parts` ([RELEASING.md](RELEASING.md), *The registry side*). `@rig-c/core`
533
+ comes with it as a dependency, providing the `rigc` command. No Spine runtime does: since
524
534
  rig-c 2.0 the installed `rigc` gates every build with its own validator over the
525
535
  compiled model document, and `rigc --version` says `entry: cli_core.ts —
526
536
  @esotericsoftware/spine-core absent`. With `@esotericsoftware/spine-core` installed
package/cli.ts CHANGED
@@ -539,7 +539,7 @@ usage:
539
539
  same input), refused as RETUNE_INPUT_CHECK_BOUND,
540
540
  RETUNE_INPUT_CHECK_FILE and RETUNE_INPUT_CHECK_MOVED.
541
541
 
542
- rig-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>]
542
+ rig-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>] [--no-extremes]
543
543
  [--page-edges pot|free] [--pack-shape rect|polygon]
544
544
  [--requirements <file.json>] [--seam-pairs] [--keep-stills]
545
545
  [--set <name> | --all-sets] [--environment <schedule.json>]
@@ -556,6 +556,10 @@ usage:
556
556
  verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
557
557
  --geometry: the frames and geometry.json, the skinned vertices and
558
558
  every bone's world transform per frame),
559
+ extremes/<anim>/extremes.png and extremes.json (core's min/max/diff
560
+ observations at 12 fps and scale 1; EXTREMES lines name frames/distance
561
+ or core's SKIP reason). --no-extremes skips these extra renders and
562
+ removes stale extremes/. Observations never change the bars or verdict.
559
563
  contact.png, motion_heat.png and check.json (with rigc_entry, the rigc
560
564
  entry that gated the build, and pack_mode, the two pack flags). PASS needs every gate summary
561
565
  "0 failed", the seam (setup pose vs the flat composite of parts/) at mean
@@ -819,7 +823,7 @@ usage:
819
823
  [--head <dir|psd>] --out <dir> [--seam near-white|silhouette]
820
824
  [--project core|visible] [--page-edges pot|free]
821
825
  [--pack-shape rect|polygon] [--loop] [--requirements <file.json>]
822
- [--idle-keys ctl|direct] [--lead-in <periods>] [--seam-pairs] [--keep-stills]
826
+ [--idle-keys ctl|direct] [--lead-in <periods>] [--seam-pairs] [--keep-stills] [--no-extremes]
823
827
  [--alt <name>=<source>:<full>[:<head>]] [--set <name> | --all-sets]
824
828
  [--environment <schedule.json>] [--wind-band <spec.json>]
825
829
  assemble, then rig, then check, in one process, each stage's own lines
@@ -855,6 +859,7 @@ usage:
855
859
  --wind-band is forwarded to check (as for check); the spec is read before
856
860
  assemble runs, so a malformed one is refused, CHECK_WIND_BAND, under
857
861
  [assemble] before anything is assembled.
862
+ --no-extremes is forwarded to check to skip its diagnostic renders.
858
863
  --seam-pairs and --keep-stills are forwarded to check (as for check:
859
864
  the seam split by pair on a pass too, under check/seam/; the setup-pose
860
865
  still under check/setup_still/). check's --rig and --parts are refused:
@@ -1069,13 +1074,13 @@ const TAKES = {
1069
1074
  rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
1070
1075
  propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule', '--cloth', '--write-config', '--from'], switches: ['--head-box', '--no-back-hair', '--only-missing'] },
1071
1076
  compare: { valued: ['--left', '--right', '--map'], switches: [] },
1072
- check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment', '--wind-band'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
1077
+ check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment', '--wind-band'], switches: ['--seam-pairs', '--keep-stills', '--all-sets', '--no-extremes'] },
1073
1078
  loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
1074
1079
  assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config'], switches: ['--propose-plan'] },
1075
1080
  'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll', '--config', '--depth-run'], switches: ['--offload', '--lama', '--nf4'] },
1076
1081
  'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
1077
1082
  inputs: { valued: ['--source', '--config', '--out'], switches: [] },
1078
- build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in', '--alt', '--set', '--environment', '--wind-band'], switches: ['--loop', '--seam-pairs', '--keep-stills', '--all-sets'] },
1083
+ build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in', '--alt', '--set', '--environment', '--wind-band'], switches: ['--loop', '--seam-pairs', '--keep-stills', '--all-sets', '--no-extremes'] },
1079
1084
  compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
1080
1085
  'mesh-draft': { valued: ['--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: ['--more-triangles'] },
1081
1086
  'mesh-stats': { valued: ['--config', '--parts'], switches: [] },
@@ -1316,10 +1321,12 @@ function cmdCheck(args: string[]): number {
1316
1321
  if (seamPairs > 1) return usage('--seam-pairs is given twice');
1317
1322
  const keepStills = args.filter((a) => a === '--keep-stills').length;
1318
1323
  if (keepStills > 1) return usage('--keep-stills is given twice');
1324
+ const noExtremes = args.filter((a) => a === '--no-extremes').length;
1325
+ if (noExtremes > 1) return usage('--no-extremes is given twice');
1319
1326
  const allSets = args.filter((a) => a === '--all-sets').length;
1320
1327
  if (allSets > 1) return usage('--all-sets is given twice');
1321
1328
  const f = flags(
1322
- args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills' && a !== '--all-sets'),
1329
+ args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills' && a !== '--all-sets' && a !== '--no-extremes'),
1323
1330
  'check',
1324
1331
  ['--rig', '--out'],
1325
1332
  );
@@ -1334,7 +1341,7 @@ function cmdCheck(args: string[]): number {
1334
1341
  try {
1335
1342
  const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
1336
1343
  const r = checkStage(
1337
- { rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1, ...(f.has('--set') ? { set: f.get('--set') as string } : {}), allSets: allSets === 1, ...(f.has('--environment') ? { environment: f.get('--environment') as string } : {}), ...(f.has('--wind-band') ? { windBand: f.get('--wind-band') as string } : {}) },
1344
+ { rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1, noExtremes: noExtremes === 1, ...(f.has('--set') ? { set: f.get('--set') as string } : {}), allSets: allSets === 1, ...(f.has('--environment') ? { environment: f.get('--environment') as string } : {}), ...(f.has('--wind-band') ? { windBand: f.get('--wind-band') as string } : {}) },
1338
1345
  rigcRunner(bin),
1339
1346
  bin,
1340
1347
  console.log,
@@ -1936,7 +1943,7 @@ function cmdBuild(args: string[]): number {
1936
1943
  const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-build-'));
1937
1944
  try {
1938
1945
  const r = build(
1939
- { config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges, packShape: shape, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), idleKeys: keys, ...(leadText === undefined ? {} : { leadIn: Number(leadText) }), seamPairs: on.has('--seam-pairs'), keepStills: on.has('--keep-stills'), ...(alts.length === 0 ? {} : { alts }), ...(flags.has('--set') ? { set: flags.get('--set') as string } : {}), allSets: on.has('--all-sets'), ...(flags.has('--environment') ? { environment: flags.get('--environment') as string } : {}), ...(flags.has('--wind-band') ? { windBand: flags.get('--wind-band') as string } : {}) },
1946
+ { config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges, packShape: shape, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), idleKeys: keys, ...(leadText === undefined ? {} : { leadIn: Number(leadText) }), seamPairs: on.has('--seam-pairs'), keepStills: on.has('--keep-stills'), noExtremes: on.has('--no-extremes'), ...(alts.length === 0 ? {} : { alts }), ...(flags.has('--set') ? { set: flags.get('--set') as string } : {}), allSets: on.has('--all-sets'), ...(flags.has('--environment') ? { environment: flags.get('--environment') as string } : {}), ...(flags.has('--wind-band') ? { windBand: flags.get('--wind-band') as string } : {}) },
1940
1947
  { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate'), stages: {
1941
1948
  assemble: (input, outputs) => runBuildProcess({ stage: 'assemble', bin, input, outputs }, join(scratch, 'workers')),
1942
1949
  rig: (input, _rigc, gateScratch) => runBuildProcess({ stage: 'rig', bin, input, scratch: gateScratch }, join(scratch, 'workers')),
package/docs/AUTHORING.md CHANGED
@@ -40,7 +40,7 @@ with `flattenRig(built)`, not with the built file.
40
40
 
41
41
  | what | why | how |
42
42
  | --- | --- | --- |
43
- | [Bun](https://bun.sh) 1.2 or later | rig-parts and rig-c are Bun programs | `npm install -g @rig-c/parts` installs the command; it says so if Bun is missing |
43
+ | [Bun](https://bun.sh) 1.4 or later (`engines.bun >=1.4.0`; CI tests latest only) | rig-parts and rig-c are Bun programs | `npm install -g @rig-c/parts` installs the command; it says so if Bun is missing |
44
44
  | [@rig-c/core](https://www.npmjs.com/package/@rig-c/core) | compiles, gates, packs and renders every rig; rig-parts writes no Spine data itself | installed with rig-parts as the dependency `@rig-c/core` (`node_modules/.bin/rigc`); with no Spine runtime beside it, it gates with its own validator, and `rigc --version` says `entry: cli_core.ts` |
45
45
  | [See-through](https://github.com/shitagaki-lab/see-through), somewhere | the layer decomposition is the input | any route in README *Getting See-through layers*; it runs twice per character, outside this tool |
46
46
  | a GPU, wherever See-through runs | See-through is a diffusion model | nothing in rig-parts itself uses a GPU |
@@ -120,7 +120,7 @@ read by no CPU stage).
120
120
  | `assemble.cuts` | assemble; `meshes`/`regions` keys may name the new parts too, and must (each is a part like any other) | **authored**, optional (issue #170): `[{from, into, polygon, draw, overlap, under, under_fill}]`, `under` and `under_fill` optional — a structural separation under [Source before scissors](#source-before-scissors), cut from a plan part's layer by the author's polygon: the pixels of plan part `from` whose **centre** lies inside `polygon` (even-odd; a centre exactly on an edge is outside) leave `from` and make the new part `into`, colour and alpha, so every pixel of the layer ends in exactly one of the two. `polygon` is `[[x, y], …]`, at least three points, non-negative integer **rig** pixels — the space of `parts.json` and of the patches' boxes. The file that shows the layer in those coordinates is `assemble`'s own `rig/parts/<from>.png`, placed at its `parts.json` `x`, `y` (the PNG's pixel `(i, j)` is rig pixel `(x + i, y + j)`), or `render/recomposite_rig.png`, the whole rig 1:1; the contact sheets (`sheet`) are scaled and carry no coordinates. `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`, as for a patch (to keep the setup pose, draw the piece immediately in front of `from` — `{"before": "<the plan part after from>"}`, or `"front"` when `from` is the last plan part; `{"before": "<from>"}` draws it behind `from`, which `under` refuses as `ASSEMBLE_UNDER_DRAW` because the fill would hide it). `from` must be a plan part (`CONFIG_NAME_RESOLVES`; a patch or another cut's piece is refused; a part a limb takes may be cut where no segment of the limb holds it, issue #564, §5 *Limbs*), `into` a new name (`CONFIG_PART_UNIQUE`), the polygon three or more integer points (`CONFIG_FIELD_TYPE`). The piece's `parts.json` record carries `from`'s `<run>:<tag>` and its own counts, each split from the layer's by where the pixel lies; it is a mesh or a region under its own name, and the coverage rule holds it like a plan part. What the config cannot know the stage refuses: a polygon point past the rig (`ASSEMBLE_CUT_INSIDE`), two cuts of one part sharing a pixel (`ASSEMBLE_CUT_OVERLAP`), a cut that takes none of the layer's opaque pixels or leaves `from` none (`ASSEMBLE_CUT_PIXELS`), a cut of a part a limb takes that takes a pixel a segment of the limb holds (`ASSEMBLE_CUT_CROSSES_LIMB`). `overlap` is required, a non-negative integer of rig pixels with no default (`CONFIG_FIELD_TYPE`): the **band** — the polygon's pixels within `overlap` px of its edge by Chebyshev distance (the polygon's mask less its erosion by a `2 * overlap + 1` square) — stays in `from` as well as going to the piece, so the piece's resampled edge has art under it; the counts stay a strict partition and the band is the base's extra copy, printed on the `cut:` line. At `0` the two abut and `check`'s seam bar judges the edge between them (`CHECK_SEAM_WITHIN_BAR`, §5). A band only covers the edge at rest: a piece that swings well clear of where it was painted shows the hole it leaves, and a thin piece's band shows a ghost of the piece. `under` (issue #193), optional, is the author's image of what lies under the piece: a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE` unless a non-empty string), in rig pixels over the polygon's **bounding box** — the least and greatest polygon x and y, so the image is `max x − min x` by `max y − min y` and its pixel `(i, j)` is rig pixel `(min x + i, min y + j)`. Where the cut removed pixels from `from` — the piece's pixels (alpha above 0) less the band — `from` takes the image's pixel, colour and alpha wherever the image's alpha is at least the alpha the cut removed there (**filled**); elsewhere `from` stays empty there (**left empty**): a transparent pixel is the author's statement that nothing lies under it (the background, or a part drawn behind), and a less opaque one is not half-filled. The image is read nowhere else, so its pixels over the band, over the box outside the polygon or where the layer had no art never change `from`. The `cut:` line then ends `; under <n> px filled, <m> px left empty` (`n + m` is every removed pixel), so an unintended hole is a number in the log, not a guess; the filled pixels count in `from`'s `opaque_px` and `occluded_px` (the piece is drawn over them), never as visible or projected. The stage refuses: a file that is not there (`ASSEMBLE_UNDER_PRESENT`) or not a PNG (`ASSEMBLE_UNDER_DECODES`), an image not exactly the box's size (`ASSEMBLE_UNDER_SIZE`, both sizes given), an image that fills none of the removed pixels — all transparent there, or another file (`ASSEMBLE_UNDER_EMPTY`; to leave the whole hole, drop `under`), and a piece drawn behind `from` (`ASSEMBLE_UNDER_DRAW`: the fill would hide it; draw it `"front"` or `{"before": "<the plan part after from>"}`). Producing the image is the author's, by whatever means; absent, `from` keeps only its band. `under_fill` (issue #360), optional, is the one value `"colour"` (`CONFIG_FIELD_TYPE` on any other): the same pixels are filled and left empty, and a filled pixel takes the image's **colour** at the alpha the cut removed there instead of the image's alpha, so `from`'s alpha is the uncut layer's wherever the image fills and its silhouette does not grow — an opaque image over a soft edge leaves the edge soft. It applies to the part's hidden-art layer as well as to a file; a cut with neither is refused (`ASSEMBLE_UNDER_FILL_SOURCE`). The `cut:` line's filled count then says how many took the colour only: `; under 190 px filled (190 colour only, at the alpha the cut removed), 0 px left empty`. Absent, a filled pixel takes the image's colour and alpha, as above |
121
121
  | `assemble.alternates` | assemble, rig, check (`--set`, `--all-sets`) | **authored**, optional (issues #454, #465, #472, #473, #476): `[{name, source?, full?, head?, head_box?, parts?, hides?, cuts?, skeleton?, anchor?, base_anchor?, drawn?, tips?, front?}]` — alternate painting sets assembled into `<out>/rig/parts/<set>_<part>.png` (`parts.json` `alternates`), attached on each covered slot in `rig.json` as `<set>_<part>` beside the base attachment, and indexed in `<out>/rig/sets.json` (`rig-parts-sets/1`: `sets.<set>.slots`, `.hides`, `.drawn` when present, and `.threshold: {yaw: 15, pitch: 12}` when the set replaces `face` on a rig with `turn` or `turn`/`tilt` slider animations). `source`, `full` and `head` may instead be given on the CLI with `--alt <name>=<source>:<full>[:<head>]`. Windows absolute drive paths such as `C:\paint\painting.png` are accepted as written; each segment's leading drive colon belongs to that path, while other colons still separate `source`, `full` and `head`. `parts` lists the subset of parts the set replaces (default: every part not in `hides`); a **subset set** (issue #473, e.g. mouth visemes `a`, `i`, `u` with `"parts": ["mouth"]`) cuts only its covered parts from the alternate runs (refusing with `ASSEMBLE_ALT_INPUT_PRESENT` if a covered part's layer is missing or empty), writes only `{slot: "<set>_<slot>"}` in `sets.json` so `@rig-c/play`'s `setSet(name)` switches those slots while leaving every other slot on its base attachment (and `setSet(null)` restores the base pose), may be keyed on a slot attachment track in `motion.animations_from` as `{"t": …, "set": "<name>" \| null}` (`CONFIG_SET_COVERS_SLOT` if `"set"` is undeclared or does not cover the track's slot), and under `check --set <name>` counts `ANIMATIONS_DO_NOT_FOLD` only on its own covered slots; `hides` lists slots the set hides (`null` in `sets.json`). A **gesture subset set** (issue #476) covers a limb chain declared in `config.limbs.<limb>` (all of its segment `part`s; with `drawn` alone, cut at the base's joints posed to the set's own `drawn` angles by `poseBoneEntriesForDrawn`, each segment keeping the base's length; with `skeleton` (issue #539), cut at the skeleton's own joint positions by `placeBoneEntriesForSkeleton` — each covered bone the body-18 table names (`sleeve_*`, `arm_*0`/`arm_*1`, `leg_*0`/`leg_*1`) at its skeleton origin with the skeleton segment's own length and direction (`drawn` overriding the direction of a bone it names), and a covered bone the table does not name (a hand link `arm_r2`, a single `foot_r`) placed from its parent's new place — its base offset from the parent, stretched along the parent by the parent's length change and turned by its turn, so a link or a foot at the parent's end lands on the skeleton's end — with its base length and the drawn or base direction, so a foreshortened shin or forearm is cut where it is painted; naming the unsplit `from` layer, covering only some segments of the limb, or declaring polygon `cuts` on a limb set is refused with `CONFIG_LIMB_SET_JOINTS`) and carries `drawn` (or `skeleton`) for the covered bones only (giving `drawn` for an uncovered bone is refused with `CONFIG_SET_COVERS_BONE` / `RIG_SET_COVERS_BONE`; uncovered bones keep the base's `drawn`). A subset set's limb part takes no pixel that a base part drawn after it, which the set neither covers nor hides, covers at setup (issue #587): the set's attachment is drawn in the base slot's place, so that base part is drawn over it and what the set's painting shows there is that base part (shorts over a leg set's thigh top, which See-through synthesised in the set's leg layer); `assemble` clears those pixels — the base part's PNG at alpha above 8, at its `parts.json` x, y, on the shared canvas — from the set's part once its limbs are cut, before any count — its `merged_px` and its cap's record too (issue #603): a cap pixel it clears leaves the count it was in (`layer_px`, `image_px`, `upper_px`, `lower_px`, `lower_kept_px`) for the cap's `excluded_px`, written only when above 0, so every count is the written PNG's — and prints `LINT set "<set>" <part>: <n> px under <base part> excluded` per part and base part that took a pixel (n counts alpha above 8); a set that carries that base part itself (`"parts": ["thigh_l", "shin_l", "bottomwear"]`) excludes nothing and writes what it wrote before. A subset set that names one limb side of a layer more limbs take (issue #588: `"parts": ["thigh_r", "shin_r"]` where `leg_r` and `leg_l` both take the one `legs` layer) would put every pixel of that layer on its one limb, so each of its parts would hold a piece of both legs; when the set's layer holds more than one island (8-connected pixels with alpha above 8) of at least 1 % of its largest (issue #608: the ghost clean-up's own share; a smaller island is a speck, which goes with the island nearest it — the one holding the pixel centre nearest one of the speck's, none when two are equally near — and is never an island of its own), each segment part keeps the island its joints fall on — its bone's origin and the next segment's bone's origin, at the set's own bones, a joint on the island of a pixel whose square holds the point, and a joint on no island on the one island whose nearest pixel centre lies within the radius of the cap cut at that joint (`cap.radius`: an ankle just past the legwear's edge, on the shoe) — and drops the rest, once its limbs are cut and before the exclusion above, and `assemble` prints `LINT set "<set>" <part>: <layer part> holds <n> islands (8-connected, alpha above 8)[, and <s> speck(s) under 1 % of the largest (<f> px), each with the island nearest it], kept the one under <bone> [x, y] and <bone> [x, y][ (<e> px off it, within its cap's radius <r>)] (<k> px), dropped <m> (<d> px)` per part (k and d count the part's pixels above alpha 8, m the islands it lost one from, and `dropped none` when it lost none); a joint on no island with none or two within that radius, or with no cap cut at it (a limb's first joint), and a segment whose joints fall on two, are refused with `ASSEMBLE_ALT_LIMB_ISLAND` rather than guessed. A layer of one island — specks aside — may hold both legs over each other (a folded shin across the standing leg; issue #609): each of the set's segment parts then keeps the pixels nearer its own limb's segment line — the polyline of only its segments taking that source, their bone origins and the last segment's supplied tip or following segment's joint (issue #638) — than the other limb's, both in the set's pose, and gives the rest away, once its limbs are cut and before the exclusion above; a pixel whose two distances differ by 0.5 px or less (the overlap band: half a pixel, the reach by which a pixel centre is read as on a line) goes to the limb whose part there is drawn later in the base's draw order, and the `LINT set` line above counts what each part kept, gave and held in the band. The other limb stands where the set's `skeleton` places it — its keypoints place every limb bone of the body-18 table, the other leg's as well as the set's own (its `drawn` and `drawnLength` stay the covered bones') — and never at the base's place: a set that does not place it (a `drawn` set, which may not name a bone it does not cover, or a set with neither) is refused with `ASSEMBLE_ALT_LIMB_POSE` when its layer has art where the base draws that limb, and when it has none nothing says the layer holds it and the layer is cut as before; two limbs whose segment lines run closer than the limb's half-width over most of where their art can meet are refused with `ASSEMBLE_ALT_LIMB_OVERLAP`. A set that carries every limb of the layer is cut as before. For a subset set, where a limb's own source layer holds art (`alpha > 8`) under another layer of that run drawn in front (`alpha > 8`, by the projection's plan/depth order), the limb keeps its own layer pixels through projection, limb cutting, island/nearest-line assignment, base-footprint exclusion, fringe clearing and seam correction (issue #637). Those pixels are occluded, never counted as painting projection; no base footprint or painting colour replaces them. After source-art island/nearest-line assignment, a source-transparent cap pixel is assigned before protection, in both doors: its centre is compared against only the segments taking the recipient's source, at the set's supplied bones (ankle-to-toe for a separate foot source), with the existing 0.5 px overlap band and base draw order. A missing last endpoint is refused with `ASSEMBLE_ALT_LIMB_POSE`; an actual following joint may supply that endpoint, never a guessed point. An unposed other limb with base art beneath these cap pixels is likewise refused; without such overlap the existing compatible path retains the cap without inventing the other limb's pose. A pixel cleared by this assignment is never restored. `CAP_FROM_UPPER` records a copy made before assignment and alone proves no ownership: only a retained copy is protected beneath the upper segment, which is drawn over it. The source-art island selection itself is unchanged. Other retained cap pixels (from the layer or a supplied cap image) are protected under an actual front layer even where the lower source is transparent. Protection follows each assigned piece through exclusion, fringe clearing and seam correction; it never restores a pixel ownership removed. Cap pixels removed during ownership leave their original counts for `excluded_px`, just as excluded pixels do, so the cap ledger describes the written PNG. Target assignment reconciles this ledger even when there are no source-transparent cap candidates and when a direct caller does not request cover protection. Unprotected source pixels still obey the base-footprint exclusion. Missing limb/cap art is never extended or guessed. Base builds and uncovered, unexcluded sets keep their existing bytes. `skeleton` (a pose file path relative to the config's directory, `{"width": W, "height": H, "points": {…}}` with all 18 body-18 keypoints on the canvas, validated by the same rules as `generation.control.skeleton`: `CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON`, `CONFIG_FIELD_TYPE`, `CONFIG_KEY_KNOWN`, `CONFIG_KEY_UNIQUE`, `CONFIG_POSE_CANVAS`, `CONFIG_POSE_KEYPOINTS`, `CONFIG_POSE_IN_CANVAS`) or `drawn` (`{<bone>: <worldDegrees>}`, each bone resolved against `bones`, `CONFIG_NAME_RESOLVES`) declares the posed angles the alternate was painted at (issue #465); when `skeleton` is given, `assemble` derives `drawn` from its 18 keypoints (filtered to covered bones on a subset set, with `drawn` overriding any bone named in both), records both `skeleton` (scaled by `assemble.rig_scale` to rig px) and `drawn` in `parts.json` `alternates` — and, when the skeleton placed the set's limb bones (issue #539), `drawnLength` (`{<bone>: <ratio>}`, each covered table bone's skeleton segment length over its base length, 6 places) and `registration` (`{offset: [dx, dy], scale}`: the similarity without rotation, about the canvas origin, that maps the skeleton's torso — the mean of its shoulders `r_shoulder`/`l_shoulder` to the mean of its hips `r_hip`/`l_hip`, read where the base declares the bone rooted there, `sleeve_*0`/`arm_*0` and `leg_*0` — onto those base bones' origins, 6 places), both also written to `sets.json`; the set's painting must be registered, its torso within 1 rig px and 1 % in scale of the base's, or the set is refused by name before any joint is cut (`ASSEMBLE_ALT_SKELETON_REGISTERED`, with the offset and scale the painting would need) — and the `rig` stage writes `<set>_*` region and mesh attachments in the bone-local frame of `poseRigBonesForDrawn(bones, drawn)` (weighting limb segments directly from `skeleton`'s joint endpoints when `skeleton` is present) and emits `drawn` in `sets.json` so `rig-pose` and `check --set <name>` / `--all-sets` pose the bones at `drawn` without rotating the already-posed art a second time. `front` (issue #664) lists covered limb segment parts the set draws on their **front slot** instead of the base slot: each must be in the set's `parts` and not its `hides`, and listed in some `config.limbs.<limb>.front.parts` (`CONFIG_SET_FRONT`); the set then maps `<part>_front` (never `<part>`) to `<set>_<part>` in `parts.json` and `sets.json` and hides `<part>` (and `<part>_still`), its #587 exclusion counts only base parts drawn after the front slot's target, and keying that part's set on a slot track is refused (`CONFIG_SET_FRONT_KEYED`) — *Front slots (issue #664)* below `layers` optionally maps covered, nonhidden output part names to layer names in their existing source run (issue #666); for example `"layers": {"upperarm_r": "author:raised", "forearm_r": "author:raised"}`. Limb cuts, caps, islands and base exclusions read the selected layer; the base plan stays unchanged. |
122
122
  | `assemble.head_face_underfill` | assemble, `assemble --propose-plan` | **authored**, optional (issue #492): `boolean` (`CONFIG_FIELD_TYPE` on any other). When both runs exist and `full:face` has at least `200` opaque run px (`PROPOSE_MIN_PX`), the stage maps `full:face` into the head canvas via `fullLayerToHead` and measures how much of its hole-filled silhouette (`alpha > 8`) is covered by `head:face` (`alpha > 8`). A head-run `face` covering less than `75 %` (`HEAD_FACE_COVERAGE_FLOOR`, measured `0.0 %..48.6 %` on hollow/omitted head runs vs `84.8 %..99.6 %` on clean head runs across the 4 public examples and 9-standee corpus) is refused with `SEETHROUGH_HEAD_RUN_HOLLOW` unless `head_face_underfill: true` is declared. When `head_face_underfill: true` is declared and `head:face` exists (`opaqueRunPx >= 200`), `fullLayerToHead(full:face)` is composited under `head:face` on the head canvas (`alpha = max(headAlpha, fullAlpha)`, RGB blended by `headAlpha / 255`) before `head:face` is placed in rig space (`underfill=<N>px(full:face)` on the `face` part log line and a `face: head_face_underfill composited …` note in `assemble --propose-plan`). The fill restores transparent pixels only and leaves the cut's own edge where it is — a hollow whose edge is not under hair stays visible; follow [Source before scissors](#source-before-scissors) for its source remedy. When `head:face` has fewer than `200` opaque pixels, `SEETHROUGH_HEAD_RUN_HOLLOW` still fires even under `head_face_underfill: true` because the head run omitted the face layer and cannot supply a jawline |
123
- | `assemble.yield_refused` | assemble | **authored**, optional boolean, default `false` (issue #819; other types: `CONFIG_FIELD_TYPE`). With `true`, only an original front projection-core pixel refused at max-channel drift > `DRIFT_LIMIT` (90) can yield: its original source pixel must still be opaque, and the actual top-most opaque holder below it (alpha >= `CORE_ALPHA`, 250), after earlier yields, must have colour within 90 of the painting. A deeper matching part cannot bypass an intervening opaque mismatch. This setting also applies to alternate sets. Its alpha becomes 0; RGB is not repainted. Merge/ring replacements are not original refused pixels. Front is relative to the matching part behind, so original refusals in another run are eligible even when a different part is the global top holder. The reading proceeds back to front against the current behind pixels; alpha changes do not repaint or create a new matching colour. `yieldReadings` records skipped merge/ring/nonopaque/no-match counts and original/final global holders; a yielded pixel also taken by projection closing is omitted from the written `source_px_taken` count. Existing cuts/patches and thresholds are unchanged. The `drift:` line reports `yielded N px` per affected part, even below the drift-body size bar; with the setting off it names `assemble.yield_refused: true` as the conditional remedy. Rebuild and inspect the newly exposed layer in motion before considering any cut; a mismatching or absent behind holder cannot be repaired by this setting. |
123
+ | `assemble.yield_refused` | assemble | **authored**, optional boolean, default `false` (issue #819; other types: `CONFIG_FIELD_TYPE`). With `true`, an original front projection-core pixel refused at max-channel drift > `DRIFT_LIMIT` (90) can yield when its original source pixel is still opaque. Non-core rim pixels (0 < alpha < `CORE_ALPHA`, 250) on the same 8-connected part body can then yield too (issue #832). Connectivity reads the part support before clearing: the opaque border outside the eroded projection core connects the body to its rim but is not itself newly cleared. A separate rim island without a yielded core seed is unchanged. At every cleared pixel, the actual top-most opaque holder below it (alpha >= 250), after earlier yields, must have colour within 90 of the painting. A deeper matching part cannot bypass an intervening opaque mismatch. This setting also applies to alternate sets. Its alpha becomes 0; RGB is not repainted. Merge/ring replacements, samples explicitly kept by `keep_under_occluders`, and pixels outside a derived piece cannot connect a rim or be newly yielded. Front is relative to the matching part behind, so original refusals in another run are eligible even when a different part is the global top holder. The reading proceeds back to front against the current behind pixels; alpha changes do not repaint or create a new matching colour. `yieldReadings` records `yieldedRim` as the rim subset of total `yielded`, skipped core merge/ring/nonopaque/no-match counts and original/final global holders; a yielded pixel also taken by projection closing is omitted from the written `source_px_taken` count. Existing cuts/patches and thresholds are unchanged. The `drift:` line reports `yielded N px` per affected part, even below the drift-body size bar; with the setting off it names `assemble.yield_refused: true` as the conditional remedy. Rebuild and inspect the newly exposed layer in motion before considering any cut; a mismatching or absent behind holder cannot be repaired by this setting. |
124
124
  | `assemble.keep_under_occluders` | assemble | **authored**, optional array of unique source part names from `assemble.plan`, default `[]` (issue #825). For each named part, keep its own See-through RGBA where the front-most covering part in the final draw stack has alpha greater than 0 and below `CORE_ALPHA` (250), including a cover in the other run. For an explicitly named part, original alpha 1–8 fringe removed by ghost cleanup also counts as source cover, within its final piece region: those pixels still contributed to the painting. Only original opaque own-layer samples are restored after source projection/closing and protected from later seam re-take; merged/rim, cap, under-fill and authored replacement pixels are excluded. This keeps their existing provenance and makes no holes; fully opaque cover and absent cover keep the old path. Cuts and limb pieces inherit the source part's protection. Unknown names: `CONFIG_NAME_RESOLVES`; duplicates: `CONFIG_PART_UNIQUE`; invalid shape: `CONFIG_FIELD_TYPE`. Inspect the part's own amodal layer first: this handle preserves its existing content and cannot complete a missing pixel. Example: `"keep_under_occluders": ["shin_r"]`. |
125
125
  | `limbs` | assemble (the cut and the caps), rig (through `regions`), check (`LIMB_ONE_BONE`) | **proposed** by `propose --keypoints` (issue #349), then checked: `{<limb>: {segments: [{part, from, bone}, {part, from, bone, joint, range?, cap?, edge?, cap_shape?, cut_band?, hybrid?}, …], split?, layout?, front?}}`, the upper segment first. The rule is *Limbs: cut at their joints* below the table. `part` is a new part (`CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` against the plan, patches, cuts and every other limb part); `from` a plan part or a cut's piece (`CONFIG_NAME_RESOLVES`) — the layer the segment's pixels come from, which is cut into the limb parts and is not itself made: a `meshes` or `regions` entry for it is `CONFIG_NAME_RESOLVES`, and the segments of one limb that take one part are consecutive (`CONFIG_LIMB_FROM_RUN`); `bone` a declared bone, each segment's the child of the one before (`CONFIG_LIMB_BONES_CHAIN`); the upper segment names no `joint`, `range`, `cap`, `edge`, `cap_shape` or `cut_band` (`CONFIG_KEY_KNOWN`), every other names its `joint` (`CONFIG_FIELD_PRESENT`) — the joint is its bone's origin; `range` is `[min, max]` degrees from rest with `min <= 0 <= max` (`CONFIG_LIMB_RANGE`), which `assemble` writes into the lower part's `parts.json` record and `check` poses the joint at — its two ends and its middle — reading the seam there (`SEAM_OVER_RANGE`, issue #360; rig-c needs no rotation limit); `cap` a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE`); `edge` `"feather"` (issue #360), the one value it takes, and `cap_shape` `"disc"` (issue #360) or `"ball"` (issue #579) (`CONFIG_FIELD_TYPE` on any other) — the cut's geometry at that joint, *Limbs: cut at their joints* below; absent, the cut is hard and the cap the layer's half-disc of half the limb's width; `cut_band` a whole number of px, 1 or more (`CONFIG_FIELD_TYPE`), on a segment that takes the segment above's part — between two layers nothing is cut, so a band there is `CONFIG_KEY_KNOWN` — places the cut within that many px of the joint line (issue #360, *Limbs: cut at their joints*); absent, the cut is the joint line. Each limb part is attached like any part — `regions.<part>`, which must be the segment's `bone` (`CONFIG_LIMB_BONE`), or a mesh, which `check` holds to that bone alone (`LIMB_ONE_BONE`). With `limbs`, `assemble` reads `bones` too (`CONFIG_FIELD_PRESENT` without it). Absent, nothing is cut at a joint and no byte of any output moves. Optional `front` (issue #664), `{parts: [<own segment part>, …], after: "<produced part>"}`, gives each named segment a second slot `<part>_front`, empty at setup, placed after `after`'s slot family, for a gesture set to draw on (`CONFIG_FRONT_SCOPE`, `CONFIG_FRONT_NAME`, `CONFIG_FRONT_LAYOUT`; *Front slots (issue #664)* below) |
126
126
  | `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip, link_inherit?}` whose links are named `<chain>0 … <chain>n`. Parents come before children. In `rig.json` each link is **turned along its chain** — `rotation` is the direction from its origin to the next link's (the last link's to `tip`) and `length` that distance (issue #73), so a physics constraint added downstream finds a lever; under `--idle-keys ctl` the link's `<link>_ctl` carries the turn and the length and the link sits at local rotation 0 beneath it. A single bone with a `tip` is turned the same way, towards its tip, with that distance as its length (issue #239); a tip on the bone's own origin is refused (`RIG_TIP_DISTINCT`). The five face bones `eye_r`, `eye_l`, `brow_r`, `brow_l` and `mouth` (by name, when one has no tip of its own) are turned to the eye line — the line through the origins of `eye_r` and `eye_l` as `bones` writes them, read image-left to image-right, drawn when parts.json holds both an `eyewhite-r` and an `eyewhite-l` part — so the blink's `scaley` squashes across the eye line on a tilted head; move an eye bone and the line moves with it. With one eyewhite or none, or an eye bone missing, the rig stage's `face axis:` line says so and they keep the page's axes. A bone under a face bone keeps the face's axis. With the line, `rig.json` also declares `invariants.axes` — one entry, `"face"`, through `eye_r` and `eye_l`, holding the turned face bones (and their controls) — so rig-c's gate holds the build to the line; the line is the one rigc draws through the same two bones, so the tolerance is only the float floor — the largest gap the emitted (float32) bones have to it, rounded up to 6 places plus one 6-place unit — and the `face axis:` line prints it. With the line the rig stage also adds `head_up` (issue #250): a slotless bone under `head`, at head's origin, turned to the eye line + 90 degrees and as long as the eye midpoint lies from head's origin along that axis — the head's frame, which a rule that needs it reads; `head` keeps its own frame, so no key on it or under it changes meaning, and a config bone of that name is refused (`RIG_HEAD_UP_NAME_FREE`). A face bone with its own `tip` is held too (issue #258), in a second entry, `"face_tips"`, when its tip lies along the line within whole-pixel rounding — a tip written in whole pixels sits at most sqrt(2)/2 px off the point of the line it stands for, so its direction may sit up to asin((sqrt(2)/2) / length) degrees off — with its tolerance measured the same way; a tip further off is the bone's own axis, and the `face axis:` line names it with its gap and that bound. A failure is rig-c's `A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` line, which names the bone and the fix (rig-c's `docs/AUTHORING.md` §3.7 and §5.2); with no line no entry is written and A51 reports SKIP. Every region on a turned bone is turned back, so the setup pose does not move. Any other single bone keeps world rotation 0; one whose parent is a link is turned back upright. A single bone may declare `inherit` (issue #348): what it takes from its parent's transform, one of rig-c's own mode names exactly as rig-c's table spells them — `normal`, `onlyTranslation`, `noRotationOrReflection`, `noScale`, `noScaleOrReflection` (rig-c's `RigBone.inherit`) — written to that bone in `rig.json` as declared; any other spelling, `"NoScale"` included (the runtime would fold its first letter; nothing here folds or maps), is refused (`CONFIG_BONE_INHERIT`), and a chain takes no `inherit` (`CONFIG_KEY_KNOWN`). A chain may instead declare `link_inherit` (issue #369), an object mapping its own expanded link names to those same modes, for example `"link_inherit": {"leg_r1": "noScale"}`. Only the named link changes; omitted links inherit normally. A key outside this chain is `CONFIG_NAME_RESOLVES`, with this chain's valid link names in the detail; a non-object or non-string mode is `CONFIG_FIELD_TYPE`, and another mode spelling is `CONFIG_BONE_INHERIT`. The mode is forwarded to the expanded link, or to its control under `--idle-keys ctl`, by the same rule as a single bone. This is an authored opt-in: `propose` does not add it. Its setup rotation is the turn from where rig-c's own poser puts the bone with no rotation of its own under that mode, so it still stands upright (or along its tip) at the setup pose. Where the idle gives the bone a `<bone>_ctl`, the mode goes on the control: the control is the bone the parent's rotation reaches, and the bone under it inherits normally, so the idle's keys moved onto the control still turn it. A foot that does not take its shin's rotation (`noRotationOrReflection` or `onlyTranslation`) keeps its shoe level while the leg is posed (`FEET_STAY_LEVEL`, §7); which mode is the author's. `noScale` keeps a shin or shoe's shape under a scaled parent but still takes the parent's rotation: a shoe that must also stay level needs a separate rotation constraint, for example a transform constraint from an unrotated bone. This does not change the proposed feet's existing inheritance or `FEET_STAY_LEVEL` default. An automatic mesh weighted through a bone with a mode other than `normal` gets no derived amplitude (its row names the bone) and no residual envelope (rig-c's `SKINNING_RANGE_UNSUPPORTED`). Absent, `rig.json` carries no `inherit` and no byte moves |
@@ -2942,8 +2942,9 @@ world units. Other proposal modes (`--keypoints`, `--compare`, `--grid-rule`,
2942
2942
  | <a id="PROPOSE_NECK_OUT_FRESH"></a>`PROPOSE_NECK_OUT_FRESH` | output rig directory already exists or aliases input parts | choose a fresh output directory |
2943
2943
  | <a id="PROPOSE_NECK_MODE"></a>`PROPOSE_NECK_MODE` | incompatible proposal mode requested | follow this recipe and supply the named input; the refusal writes no config |
2944
2944
 
2945
- Build through the normal gate, then use `rigc render --extremes` on `body_turn`,
2946
- `turn` and `tilt`. Read setup, min, max and absolute diff at native scale, then
2945
+ Build through the normal gate, then use `rigc render --extremes` (requires
2946
+ `@rig-c/core` 2.81.0 or later) on `body_turn`, `turn` and `tilt`. Read setup, min,
2947
+ max and absolute diff at native scale, then
2947
2948
  inspect neck boundaries at native and 4x nearest-neighbour scale for tears or
2948
2949
  ghosts. A green build alone does not certify the source art.
2949
2950
 
@@ -3170,7 +3171,7 @@ full loader before its assemble stage writes anything.
3170
3171
  its own, or refused by name (issue #257; `CHECK_FLAGS_IN_BUILD` in
3171
3172
  `src/build.ts` is the one list, and the `cli` suite holds it to `check`'s own
3172
3173
  refusal): `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs` and
3173
- `--keep-stills` are forwarded, so `build --seam-pairs` writes the rows
3174
+ `--keep-stills` and `--no-extremes` are forwarded, so `build --seam-pairs` writes the rows
3174
3175
  `check --seam-pairs` writes on the same rig; `--out` is `build`'s (the check
3175
3176
  writes under `<out>/check`); `--rig` and `--parts` are refused, because the
3176
3177
  check reads the rig and the parts the build wrote (`<out>/rig`, `<out>`);
@@ -3183,6 +3184,22 @@ full loader before its assemble stage writes anything.
3183
3184
  rig px, so every origin reads 0, each chain link's tip, length and direction its
3184
3185
  config's, and every `<bone>_ctl` the rig stage inserted is the bone between a
3185
3186
  keyed mesh bone and its parent (§5).
3187
+ **Before a person looks**, open every `out/check/extremes/<anim>/extremes.png`
3188
+ after `check` (or the check inside `build`). Read setup, the geometry pair's
3189
+ min/max, and `|max - min|` at native scale. Crop every changed part boundary
3190
+ at native size and 4x nearest-neighbour; label it **tear** (background or
3191
+ another layer showing through a joint), **ghost** (a doubled line or pixels
3192
+ staying/moving where they should not), or **clean**, and explain the visible
3193
+ evidence for each label. Metrics alone miss baked lines and doubled edges;
3194
+ the agent does this pre-review before asking a human to inspect the figure.
3195
+ These are visual labels by the agent, never automatic classifications.
3196
+ Core's `EXTREMES` lines name the geometry-pair frames and distance; channel
3197
+ extrema/method are in the unchanged core JSON. Static geometry or unavailable
3198
+ observations report named SKIPs with JSON and no sheet; record the reason,
3199
+ never call the missing picture clean. Diagnostics do not change `check.json`,
3200
+ its bars or verdict. A failed build gate produces no extremes. `--no-extremes`
3201
+ on `check` or `build` skips every extra render and removes any stale
3202
+ `extremes/` from that output directory for a fast gate loop.
3186
3203
  10. **What a scene requires (optional).** When the motion a scene asks of the rig
3187
3204
  is known — a hand that stays on a cup, a sleeve that takes half an arm's turn, a
3188
3205
  joint that must stay inside a range — write it into a
@@ -3377,29 +3394,8 @@ from and what it measured), `rig.json` and `motion.json`, and from `build` the p
3377
3394
  atlas — `check/build/skeleton.json`, `.atlas` and `.png` — that a Spine runtime
3378
3395
  loads, with `check/contact.png` and the idle frames to look at.
3379
3396
 
3380
- ### Source before scissors (issue #819)
3381
-
3382
- For a tear, ghost, smudge, outline or seam, first select an appropriate existing
3383
- See-through layer/run or alternate (for a collar baked into the neck, see
3384
- [A neck that moves on its own](#a-neck-that-moves-on-its-own)); then measure a tighter See-through resolution,
3385
- crop or settings if needed. Next measure the declared depth/turn role and, for a
3386
- `drift:` line with a matching opaque behind holder, opt in with
3387
- `assemble.yield_refused: true`. This config step only clears the original refused
3388
- front alpha under the conditions in the field table; it does not paint a repair.
3389
- For a moving part under a soft hem, inspect its own amodal See-through layer;
3390
- if the painting's blended hem is re-taken onto otherwise complete hidden art,
3391
- set `assemble.keep_under_occluders: ["<source-part>"]` at this config step.
3392
- An accepted re-take need not produce a drift refusal or an uncovered hole.
3393
- The informational `drift: "<part>" source re-take changed N own-layer px`
3394
- line reads actual final soft front support and original own samples. It names
3395
- `assemble.keep_under_occluders` as the remedy and, when enabled, reports the
3396
- kept count. This is a source/layer disagreement reading, not a visual defect
3397
- gate; verify the own layer and the rendered motion before choosing the handle.
3398
- `yield_refused` addresses a different ownership case.
3399
- Rebuild and inspect setup and the animation extremes. A polygon cut separates a
3400
- piece that must move or change draw order independently; it is not the next way
3401
- to conceal a visual defect. Measure those source/config alternatives before
3402
- proposing a cut for a defect.
3397
+ For source/config repairs and the measurement order before a structural edit,
3398
+ follow [Source before scissors](#source-before-scissors).
3403
3399
 
3404
3400
  ### The correction loop (issue #359)
3405
3401
 
@@ -3497,11 +3493,12 @@ existing hardlinks are refused before artifact output; the ordinary mode names
3497
3493
  | --- | --- | --- | --- |
3498
3494
  | `layers` | the table, then one `author-made: layer "author:<label>" — …` line per layer the author made (group `author` in the table; §5, *A layer the author made*), then the `WARN` lines | every tag the plan will need has opaque pixels; one `face` in the head run; `0 WARN line(s)` | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`); a `WARN PLAN_LAYER_…` line (§6, *Plausibility*) |
3499
3495
  | `sheet` of both runs | the tile list (and the sheet, if you can see) | eyes, irises, lashes and brows as left/right pairs in the head run | a head box that cut off an ornament: move `head_box`, re-run the head crop |
3500
- | `assemble` | one line per part — its counts (`op=` opaque, `vis=` visible, `src=` taken / candidates, `unproj=` visible but not projected, `drift=<n> (<p>% of core)`, `merged=`, `seam=`; *parts.json*, below). `drift=` is `refused_drift_px` in rig px and its share of the part's candidates, `projected_core_px`, which it is a subset of (issue #314); no bar reads either. On the four public examples the share runs to 94.5 % (`scarf` `hair_back`, 479 of 507) and the count to 26,472 (`scarf` `topwear`, 22.3 %), and the seam override repaints refused top-most pixels afterwards, so a large count says where See-through's colour was not taken, not that the part is unlike the painting: the `drift:` line below is the one that names a body of them another part holds. Then the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…)[, "<holder>" drawn in front of "<part>"][; the painting is within 90 of "<behind>" (<px> px at alpha >= 250, drawn behind "<part>")]; informational: no gate reads this. Swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do): swap, rebuild and read this line again; if it moves, removing it needs different input layers. At rest, the seam pass repaints the holder's pixels that pass its rule from the painting` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order or that a part drawn behind it matches within 90 of the painting (issues #255, #351, #758) — that layer is not what the painting shows there; no gate reads this line, swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do) — swap, rebuild and read this line again, and if it moves, removing it needs different input layers — and at rest the seam pass repaints the holder's pixels that pass its rule from the painting (none on the five public paintings), then, per subset alternate set, one `LINT set "<set>" <part>: <layer part> holds <n> islands (8-connected, alpha above 8)[, and <s> speck(s) under 1 % of the largest (<f> px), each with the island nearest it], kept the one under <joints> (<k> px), dropped <m> (<d> px)` line per limb part cut from a layer of more than one island when the set names one limb side of it (issue #588; issue #608: a joint taken to the nearest island reads `<bone> [x, y] (<e> px off it, within its cap's radius <r>)`, and a part that lost no pixel `dropped none`), then one `LINT set "<set>" <part>: <layer part> holds <limb> and <other limb> in one island (8-connected, alpha above 8), split by the nearest segment line at the set's bones: kept <k> px (<share>), gave <m> px (<share>) to <other limb>; <b> px in the overlap band (nearer one line than the other by 0.5 px or less) went to the limb drawn in front, <bk> kept and <bg> given` line per limb part cut from a layer of one island that a limb the set does not carry also takes (issue #609; k, m and b count the part's pixels above alpha 8, each share of the part's pixels before the split, 3 places) and one `LINT set "<set>" <part>: <n> px under <base part> excluded` line per subset alternate set's limb part and base part drawn over it that the set does not replace which took a pixel (issue #587, `assemble.alternates` above; none on the public examples, which declare no alternate set); and look at `render/recomposite_error_rig.png`. The opt-in `assemble.yield_refused` adds `yielded N px` to each affected part's drift line (including a yielded refusal smaller than the body bar); with it off the line names `assemble.yield_refused: true` as the conditional config remedy before any cut. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then — with `config.limbs` (issue #349) — one `limb cut: "<part>" opaque <n> = "<segment part>" <px> + …` line per part a limb takes and one `limb cap:` line per joint, its cap's pixels from the cap image, from the layer and missing, and its box (under `cap_shape: "ball"`, issue #579, then the lower half's pixels written, left as painted and missing), followed on a short cap by one `LINT "<part>" cap at [x, y] … short_px left <n>, right <m>: no layer gives them …` line (issue #557) — from the upper segment where it gave a pixel (under `cap_shape: "disc"` always), under `edge: "feather"` the pixels faded, under `cut_band` the line the cut was placed on and the stroke pixels it and the joint line cross (issue #360) (§3, *Limbs: cut at their joints*), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `reference-sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `reference-demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (reference-demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. Use [Source before scissors](#source-before-scissors) for the source ladder; a missing independently moving structural piece can use a painting patch (below) |
3496
+ | `assemble` | one line per part — its counts (`op=` opaque, `vis=` visible, `src=` taken / candidates, `unproj=` visible but not projected, `drift=<n> (<p>% of core)`, `merged=`, `seam=`; *parts.json*, below). `drift=` is `refused_drift_px` in rig px and its share of the part's candidates, `projected_core_px`, which it is a subset of (issue #314); no bar reads either. On the four public examples the share runs to 94.5 % (`scarf` `hair_back`, 479 of 507) and the count to 26,472 (`scarf` `topwear`, 22.3 %), and the seam override repaints refused top-most pixels afterwards, so a large count says where See-through's colour was not taken, not that the part is unlike the painting: the `drift:` line below is the one that names a body of them another part holds. After the part lines, `provenance:` per part and `provenance total:` report the value partition, followed by `ownership:` per cut for structural moves, independent of RGBA equality; alternate sets get their own `set "<set>"`-prefixed lines ([the two axes](#source-before-scissors)). The author-made/cut/limb/fringe lines described below precede the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…)[, "<holder>" drawn in front of "<part>"][; the painting is within 90 of "<behind>" (<px> px at alpha >= 250, drawn behind "<part>")]; informational: no gate reads this. Swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do): swap, rebuild and read this line again; if it moves, removing it needs different input layers. At rest, the seam pass repaints the holder's pixels that pass its rule from the painting` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order or that a part drawn behind it matches within 90 of the painting (issues #255, #351, #758) — that layer is not what the painting shows there; no gate reads this line, swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do) — swap, rebuild and read this line again, and if it moves, removing it needs different input layers — and at rest the seam pass repaints the holder's pixels that pass its rule from the painting (none on the five public paintings), then informational `drift: "<part>" source re-take changed N own-layer px` lines under soft front cover, naming `assemble.keep_under_occluders` and its kept count when enabled ([source ladder](#source-before-scissors)), then, per subset alternate set, one `LINT set "<set>" <part>: <layer part> holds <n> islands (8-connected, alpha above 8)[, and <s> speck(s) under 1 % of the largest (<f> px), each with the island nearest it], kept the one under <joints> (<k> px), dropped <m> (<d> px)` line per limb part cut from a layer of more than one island when the set names one limb side of it (issue #588; issue #608: a joint taken to the nearest island reads `<bone> [x, y] (<e> px off it, within its cap's radius <r>)`, and a part that lost no pixel `dropped none`), then one `LINT set "<set>" <part>: <layer part> holds <limb> and <other limb> in one island (8-connected, alpha above 8), split by the nearest segment line at the set's bones: kept <k> px (<share>), gave <m> px (<share>) to <other limb>; <b> px in the overlap band (nearer one line than the other by 0.5 px or less) went to the limb drawn in front, <bk> kept and <bg> given` line per limb part cut from a layer of one island that a limb the set does not carry also takes (issue #609; k, m and b count the part's pixels above alpha 8, each share of the part's pixels before the split, 3 places) and one `LINT set "<set>" <part>: <n> px under <base part> excluded` line per subset alternate set's limb part and base part drawn over it that the set does not replace which took a pixel (issue #587, `assemble.alternates` above; none on the public examples, which declare no alternate set); and look at `render/recomposite_error_rig.png`. The opt-in `assemble.yield_refused` adds `yielded N px` to each affected part's drift line (including a yielded refusal smaller than the body bar); with it off the line names `assemble.yield_refused: true` as the conditional config remedy before any cut. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then — with `config.limbs` (issue #349) — one `limb cut: "<part>" opaque <n> = "<segment part>" <px> + …` line per part a limb takes and one `limb cap:` line per joint, its cap's pixels from the cap image, from the layer and missing, and its box (under `cap_shape: "ball"`, issue #579, then the lower half's pixels written, left as painted and missing), followed on a short cap by one `LINT "<part>" cap at [x, y] … short_px left <n>, right <m>: no layer gives them …` line (issue #557) — from the upper segment where it gave a pixel (under `cap_shape: "disc"` always), under `edge: "feather"` the pixels faded, under `cut_band` the line the cut was placed on and the stroke pixels it and the joint line cross (issue #360) (§3, *Limbs: cut at their joints*), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `reference-sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `reference-demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (reference-demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. Use [Source before scissors](#source-before-scissors) for the source ladder; a missing independently moving structural piece can use a painting patch (below) |
3501
3497
  | `propose` | `note:` lines, `LINT` lines, the `coverage` lines, `landmarks.png`, `basis.json` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter; the `coverage:` summary says how many bones that verdict covers — a `not checked` bone was read by no check (its line says why), and a `ratio` or `fallback` bone in `basis.json` is a guess | a link off the art (a bone on the background) — move it onto the layer; `LINT <link> at … runs to … N degrees off the head's down axis …` — a fringe link that does not lean with the head (§3, the fringe in the head's frame): move it along the lock it drives; `LINT <bone> under <parent>: amplitude <a> below the link above's <b> in the keyed chain …` — under `--cloth`, a keyed sibling the recipe left the only keyed child of its parent, which `CHAIN_LAG` would refuse (issue #572, §3 *A keyed sibling left behind*): raise the field it names to `<b>` or more in size; `LINT hairback_… on mesh '<part>' (side <r\|l>): <N> tree(s) for <M> visible hair mass(es) (mass 0 columns x=<minX>..<maxX>, y=<minY>..<maxY>; …)` — back-hair cloth trees whose count does not match the side's visible hair masses (`max(1, floor(w / (4 * grid)) + 1)` per mass) or whose link or tip leaves its mass's column range (issues #445, #461): omit `chains` on `cloth."back hair"` or draft trees per visible hair mass inside its columns; `LINT <a_limb> and <b_limb> on <part> (<from>) cross at [x, y] with no per-limb split: …` — two limbs sharing one layer cross in an X without a per-limb split (issue #446): declare `config.limbs.<covering>.split` (`{ "line": [[x, y], …] }` or `{ "edge": "painting" }`, §3 *Which limb*); `LINT foot_<s> at [x, y], the <s>_ankle joint and leg_<s>1's tip, lies on none of leg_<s>'s art within 1 px along its joint line (the nearest of it on that line is the pixel [x, y], D px from the joint): …` — an ankle keypoint off its leg's art on the line `assemble` sizes the ankle's cap by (issue #517): `propose` drafts every leg without its ankle cut, the shoe layer stays one part, and nothing is moved onto the art; move that ankle keypoint onto the leg's or the shoe's art and re-run `propose` (`(the line crosses none of it)` when no art lies on that line at all); `LINT stop on <chain> (<firstLink> at [x0, y0]) against <against> (at [ax, ay]) places <chain>_touch at [tx, ty], above <firstLink>'s root (rest R deg, \|rest\| + outward B deg): …` — a stop's contact point `<chain>_touch` lies above `links[0]`'s root in rig px or pushes `\|rest\| + outward` past `180°` (issue #447, §2 `stops`): stop against the descending limb bone below `firstLink`, set `links` to start at a link below `<chain>_touch`, or omit the stop on a rising limb; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head`; a `no face part: face box derived from …` note (always the first) — `head`, `neck` and every face-height scale come from a box guessed off the hair and neck (§3): check `head` and `neck` on `landmarks_head.png` and move them; when it adds `the blink shuts the eyes over no face part`, read `BLINK_NO_HOLE` after `check` — an eye with no face under it can open a hole (the `reference-sample` with only its face removed: 587 px). Under `--keypoints`: the `lint rule set:` line first (joints, or screen and why), then the joint notes — `missing: the rule "…" placed bone X instead` is a bone still guessed; `not used — …` names why an arm's joints did not place its sleeve (for a wrist on no part, the nearest part and the `--wrist-snap` that would bind it); a `wrist snap: …` note is a wrist bound past 15 px by the flag — check that chain's tip on `landmarks.png`; a `one handwear part holds both wrists` note is two chains on one mesh — use another layer/run first; when no separate layer exists, a structural `assemble.cuts` entry separates the independently moving arms ([Source before scissors](#source-before-scissors)); `LINT chest at … is not between the neck joint …`, `LINT hip at … is not past chest …`, `LINT hip at … is nearer the neck joint …` and `LINT <chain><i> at … does not advance from <s>_shoulder toward <s>_wrist` are the joint-frame lines: move that bone between, past or along the joints they name |
3502
3498
  | `compare` | one line per pair (origin, parent, tip, length, direction — or SKIP and why), then the per-row summary, the unmapped bones of each side, `required:`, each side's roles and the `controls:` line | proposal against the config you corrected: the rows you changed and no others. Config against the `rig.json` `build` writes for it: measured on both examples, every origin within 6.19e-7 px (the rig's offsets are written to 6 places) and every chain link's tip and length within 4.99e-4 px (`length` is written to 3), printed to three places as `0.000` (a signed figure as `+0.000` or `-0.000`); `parent … is the right's ancestor at depth 2, <bone>_ctl between` for every keyed mesh bone, no `DIFFERENT` and no `NOT MAPPED` | `parent DIFFERENT: left a, right b` — a bone hangs from another parent than the other side's; `frames not related` — a rig with no stage (`skeleton.width`/`height`), two configs at different `rig_scale`: the distance rows SKIP, so declare the frame in a `--map` file if the author knows it; `FAIL STRUCTURE_REQUIRED_PRESENT` — a bone the map requires is missing through its pairs |
3503
3499
  | `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`); in `rig.json`, each chain link's (or its `_ctl`'s) `length` and `rotation` | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages); every link's `length` the distance to the next link and `rotation` its direction (Spine degrees, counter-clockwise, y up, local to the parent), every offset under it — a child bone's `x, y`, a weight's bind `x, y`, a region's `x, y` — in that turned frame, and a region on a link carrying `rotation` that turns it back upright. Nothing moved: `flattenRig` (`src/rig.ts`) turns every offset back and gives the unturned numbers | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
3504
3500
  | `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:` (and `loop physics:` when the rig declares a physics constraint, §7), `seam:`, the six judgement lines, then — when `motion.json` holds an animation, the idle included — `ANIMATIONS_DO_NOT_FOLD` and one `folds` line per folding mesh, rigc's own (issues #320, #333), then — where a `footwear` part rides a bone that hangs from another — `FEET_STAY_LEVEL` and one `foot` line per such part (issue #348), then — when `parts.json` names a `handwear` part and there is an idle — `STRESS_OFF_CHAIN` (issue #347), then — where `parts.json` holds a limb segment's part — `LIMB_ONE_BONE` and one `limb` line per such part (issue #349), then — where a limb record carries a `range` — `SEAM_OVER_RANGE` and one `joint` line per joint and angle (issue #360), then `SEAM_OVER_MOTION` — a SKIP naming why on a rig that declares no neighbour pair, else the pairs read in every animation; existing internal-joint and explicit-seam pairs retain their `WARN seam` policy (issue #493), while proven body-root tears print `FAIL seam` and fail a counted bar (issue #686), then `CLOTH_SETTLES` — a SKIP naming why on a rig with no physics constraint, else the chains read and one `WARN chain` line per chain whose tip comes to rest before the idle's loop length (issue #501, a warning and never a bar), `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the bars measured — nine, ten with the fold bar, one more each with the feet-level bar, the stress bar, the limb bar, the range bar and, where proven body-root pairs exist, the body-root seam bar — and names the ones that said SKIP, then — where `SEAM_OVER_MOTION` read a pair — how many warnings it printed, `; N warning(s) (SEAM_OVER_MOTION, not a bar)` — with `CLOTH_SETTLES` named beside it, and N their sum, where the rig declares a physics constraint — which no bar counts | `check: PASS; 11 of 11 bar(s) measured, 0 skipped` on `demo` and `scarf`, whose sleeves each ride one chain; `check: PASS; 10 of 11 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN)` on `reference-demo` and `reference-sample`, whose one `sleeves` mesh rides both sleeve chains (issue #347) — no example writes `FEET_STAY_LEVEL`, every shoe riding `root`; a judgement line SKIP only where the character lacks what it reads; on a rig spec with no `parts.json` or no `idle` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), PASS with the skipped bars named | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6); `CHECK_ANIMATIONS_DO_NOT_FOLD` — a clip turns a mesh over (§6); `CHECK_STRESS_OFF_CHAIN` — a sleeve weighted off its chain (§6); `CHECK_LIMB_ONE_BONE` — a limb part on a second bone (§6); `CHECK_SEAM_OVER_RANGE` — a joint that opens when bent (§6); `CHECK_SEAM_OVER_MOTION` — a proven body-root seam opens over the existing floor (§6) |
3501
+ | `check` / `build`: extremes observations | `extremes/<anim>/extremes.png` and `extremes.json` beside the other check outputs, generated by core `render --extremes` at 12 fps on its fitted world box at scale 1; one `EXTREMES` geometry-pair frame/distance summary per animation | open the native setup/min/max/diffs and label every changed boundary tear, ghost or clean before human review (step 9) | core's named static/observation SKIPs retain JSON without a PNG; diagnostic failure is `CHECK_EXTREMES_RENDER_FAILED` SKIP and leaves the measured verdict unchanged; `--no-extremes` runs no extra render and removes stale `extremes/` |
3505
3502
  | `check --requirements` (and `build --requirements`) | after the lines above, one line per declared requirement, `NAME: PASS`, `FAIL` or `NOT MEASURABLE — …` with its figures, the bar as declared and the worst frame; then `requirements: N declared — M measured (P PASS, F FAIL), K NOT MEASURABLE; not declared: <kinds>`; `check.json`'s `requirements` block, first its `file` record (issue #556); the renders under `requirements/` (§7, *Declared requirements*) — `build` reads `config.requirements` in place of the flag when it is not given | every line PASS and `0 NOT MEASURABLE`; a follow's `released_copy` says whether rigc's consumer-driven door was taken on the throwaway copy; a `stretch` line's `held_by` (issue #321), right after `worst`, names the bones the worst edge's two vertices hang on and their idle chains, in `CHECK_TEXTURE_STRETCH`'s words (§6) and by the same function, on a PASS as on a FAIL | `CHECK_REQUIREMENT_MET` (a figure past its bar; the line names the frame) or `CHECK_REQUIREMENT_MEASURABLE` (the quantity is undefined in the frames: a tip or an axis on a bone of length 0, an aim with no line, a follow no frame of which reaches its least drive); the last line then reads `check: FAIL — B bar(s) not met, R declared requirement(s) not PASS` |
3506
3503
  | `compose` | the `[compose]` lines: one per character (its offset, its scale, its placed bounds, the shift and how many first-level bones, regions on `root` and weights bound to `root` it moved, its bone, slot, track and constraint counts, its `rig_scale` and idle), the declared order and the slot count it expands to, rigc's gate lines; then the `[check]` lines as for `check`, and `scene.json` | both gate summaries `0 failed`, `check: PASS` with the bars that measured and the ones that said SKIP named — on a composed rig `4 of 9`: the gate, the loop, `CHAIN_LAG` and `TEXTURE_STRETCH` (there is no `parts.json`; each character's own `build` measured all nine). `scene.json`'s `order.slots` is the order the compiled skeleton draws | a `SCENE_*` refusal (§6, *compose*) — the scene file, a build, or the order; `COMPOSE_RIGC_GREEN` — rigc's own line, quoted |
3507
3504
  | `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line, or, when the end frame is not frame 0, the jump line: its max \|d\| and px over 40, then why — under `build`, `the rig holds N physics constraint(s), and rigc's render starts every one at rest at frame 0` with the remedy `build --loop --lead-in <n>`, or `the rig holds no physics constraint, so the idle itself does not close` (`CHECK_LOOP_CLOSES`); under `loop --frames`, which reads frames alone, both causes (issue #325); under `build --lead-in`, the lead-in line in its place (the frames kept and the jump at the wrap); each file's line, then `loop: idle.png N B (lossless); idle-indexed.png N B (max …, mean …); idle.gif N B (max …, mean …)` | `f0048.png equals f0000.png byte for byte, so it is dropped` | `LOOP_ENCODE` (§6) |
@@ -3730,6 +3727,8 @@ and its declaration, and is green on both (`RG19`).
3730
3727
 
3731
3728
  ### Source before scissors
3732
3729
 
3730
+ <a id="source-before-scissors-issue-819"></a>
3731
+
3733
3732
  **Scissors are for structure, never for repair.** `cuts`, `patches` and
3734
3733
  `author:<label>` layers separate something that must move independently and
3735
3734
  that See-through has no layer for: a joint, an accessory, or a rear piece that
@@ -3739,8 +3738,24 @@ problem. Follow this repair ladder, recording what each step measured:
3739
3738
  1. Choose another See-through layer or run (`full` versus `head`, another tag,
3740
3739
  or `alternates`). For a collar baked into the neck, follow
3741
3740
  [A neck that moves on its own](#a-neck-that-moves-on-its-own).
3742
- 2. Re-run See-through with a tighter resolution or crop.
3743
- 3. Correct the declared depth or role in config.
3741
+ 2. Re-run See-through with a tighter resolution, crop or settings.
3742
+ 3. Correct the declared depth or turn role in config.
3743
+ 4. For a `drift:` line reporting original projection refusals and a matching
3744
+ opaque behind holder, measure
3745
+ `assemble.yield_refused: true` (issue #819). It clears original refused front
3746
+ cores and their connected non-core rim alpha (issue #832) under the conditions
3747
+ in the field table; it does not paint a repair.
3748
+ 5. For a moving part under a soft hem, inspect its own amodal See-through layer.
3749
+ If the painting's blended hem is re-taken onto otherwise complete hidden art,
3750
+ measure `assemble.keep_under_occluders: ["<source-part>"]`. The informational
3751
+ `drift: "<part>" source re-take changed N own-layer px` line reads actual final
3752
+ soft front support and original own samples; with the handle enabled it reports
3753
+ the kept count. An accepted re-take need not produce a drift refusal or an
3754
+ uncovered hole. This is a source/layer disagreement reading, not a visual defect
3755
+ gate; `yield_refused` addresses a different ownership case.
3756
+
3757
+ Rebuild and inspect setup and the animation extremes after each source/config
3758
+ change. Measure those alternatives before proposing a cut for a defect.
3744
3759
 
3745
3760
  A cut is appropriate for an independently moving accessory still inside its
3746
3761
  parent's layer, two independently moving limbs in one layer, or a rear piece
package/docs/MIGRATION.md CHANGED
@@ -54,7 +54,7 @@ installed the former upstream name beside this package installs nothing more:
54
54
 
55
55
  ### Scoped core dependency
56
56
 
57
- The current dependency is `@rig-c/core` at `^2.72.0`, replacing the literal
57
+ The current dependency is `@rig-c/core` at `^2.81.0`, replacing the literal
58
58
  `rig-c` dependency described for earlier versions above. The alias remains
59
59
  published. Imports retain their subpaths: `rig-c/mesh` becomes
60
60
  `@rig-c/core/mesh`, and `rig-c/core` becomes `@rig-c/core/core`. The command
@@ -151,7 +151,7 @@ nothing else.
151
151
  | --- | --- | --- |
152
152
  | npm package | `rig-parts` | `@rig-c/parts`; `rig-parts` and the alias `spine-parts` are published beside it at every version as aliases, the same files with only the `name` in `package.json` changed |
153
153
  | command | `rig-parts`, and the alias `spine-parts` on the same launcher | unchanged |
154
- | repository | `firejune/rig-parts` (GitHub redirects it) | `rig-c/parts`, beside `rig-c/core` (npm `rig-c`) and `rig-c/play` |
154
+ | repository | `firejune/rig-parts` (GitHub redirects it) | `rig-c/parts`, beside `rig-c/core` (npm `@rig-c/core`, alias `rig-c`) and `rig-c/play` |
155
155
  | spec ids, the config, the bytes a build writes | | unchanged |
156
156
 
157
157
  What to do: install with `npm i -D @rig-c/parts` (or `npm install -g @rig-c/parts`);
@@ -337,7 +337,7 @@ bent wants a cap that covers the turn.
337
337
  Issue #493 reads every declared neighbour pair — each limb's two segments at a joint
338
338
  (`config.limbs`), and each `--requirements` `seam`'s two parts — in every animation
339
339
  `motion.json` holds, the idle included, by rig-c's seam reading (`rigc render --seam`,
340
- rig-c 2.60.0, now the dependency's floor). It is a warning, never a bar: a seam that
340
+ rig-c 2.60.0, then the dependency's floor). It is a warning, never a bar: a seam that
341
341
  opens in motion may be intended, so no exit status, no `check: PASS` and no bar count
342
342
  moves (AUTHORING §6, `SEAM_OVER_MOTION`).
343
343
 
package/docs/STABILITY.md CHANGED
@@ -49,7 +49,7 @@ defaults are the help's too, and none is guessed from the art.
49
49
  <!-- stable:commands -->
50
50
  | command | flags |
51
51
  | --- | --- |
52
- | `layers` | one positional path: a wrapper directory, a `layers.json` or a `.psd`; with it, optional `--export` (a directory to write the wrapper form into, issue #359); or `--template` with `--canvas` and no path |
52
+ | `layers` | one positional path: a wrapper directory, a `layers.json` or a `.psd`; with it, optional `--export` (a directory to write the wrapper form into, issue #359); or `--template` with `--canvas` and no path; or a positional head run with `--full`, `--source`, `--config` together for `PLAN_HEAD_GARMENT_OVERLAP` (issue #826) |
53
53
  | `sheet` | `--source`, `--layers` (repeatable), `--out`; optional `--cell`, `--cols` |
54
54
  | `inputs` | `--source`, `--config`, `--out` |
55
55
  | `comfy` | `paint`: `--config`, `--out`; optional `--seeds`, `--seed0`, `--host`, `--wait`, `--timeout`, `--poll` |
@@ -57,11 +57,12 @@ defaults are the help's too, and none is guessed from the art.
57
57
  | `assemble` | `--propose-plan` with `--source`, `--full`, `--config`, optional `--head`, `--write-config` (must name `--config` itself, issue #356); or `--source`, `--full`, `--config`, `--out`, optional `--head` (without it, one run: issue #359), `--seam` (near-white, silhouette), `--project` (core, visible), `--alt` (repeatable `<name>=<source>:<full>[:<head>]`, issue #454) |
58
58
  | `propose` | `--head-box` with `--full`, `--canvas`; optional `--no-back-hair`, `--write-config` (existing config, issue #356) |
59
59
  | `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`), `--full`, `--head` (the turn control's depths; not with `--from-config`), `--grid-rule` (`figure` or `part`, the drafted lattice's spacing; not with `--from-config`), `--cloth` (a config whose `cloth` block is the recipe, issue #364; not with `--from-config`), `--write-config` (existing config for an ordinary proposal, issue #356; a new file with `--cloth`, issue #537; not with `--from-config` — additive: without it nothing `propose` writes changes), `--from` (the config the recipe is changed from, only with `--write-config`, issue #571 — additive: without it nothing `--write-config` writes changes), `--only-missing` (only with `--parts` and `--write-config`, not with `--cloth` or `--from-config`, issue #671 — additive: without it nothing `--write-config` writes changes) |
60
+ | `propose` | `--parts`, `--source`, `--out`, `--full`, `--head`, `--write-config` together: the [neck draft](AUTHORING.md#a-neck-that-moves-on-its-own) for an existing config with `turn.body` and one neck plan row; reassembles under `<out>/rig`, preserves the sampled turn and drafts a rigid neck unit (issue #826) |
60
61
  | `compare` | `--left`, `--right`; optional `--map` |
61
62
  | `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
62
- | `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--set` or `--all-sets` (issue #454), `--environment` (issue #538, additive), `--wind-band` (issue #590, additive) |
63
+ | `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--no-extremes` (issue #829), `--set` or `--all-sets` (issue #454), `--environment` (issue #538, additive), `--wind-band` (issue #590, additive) |
63
64
  | `loop` | `--frames`, `--out`; optional `--palette` |
64
- | `build` | `--config`, `--source`, `--full`, `--out`; optional `--head` (as for `assemble`), `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--lead-in`, `--requirements`, `--idle-keys`, `--seam-pairs`, `--keep-stills`, `--alt` (as for `assemble`, issue #454), `--set` or `--all-sets` (as for `check`, issue #454), `--environment` (as for `check`, issue #538, additive), `--wind-band` (as for `check`, issue #590, additive) |
65
+ | `build` | `--config`, `--source`, `--full`, `--out`; optional `--head` (as for `assemble`), `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--lead-in`, `--requirements`, `--idle-keys`, `--seam-pairs`, `--keep-stills`, `--no-extremes` (issue #829), `--alt` (as for `assemble`, issue #454), `--set` or `--all-sets` (as for `check`, issue #454), `--environment` (as for `check`, issue #538, additive), `--wind-band` (as for `check`, issue #590, additive) |
65
66
  | `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
66
67
  | `mesh-draft` | `--config`, `--parts`, `--out`; optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon), `--more-triangles` |
67
68
  | `mesh-stats` | `--config`, `--parts` (issue #566, additive: prints, writes nothing) |
@@ -72,10 +73,12 @@ defaults are the help's too, and none is guessed from the art.
72
73
  | (none) | `--version`, `--help` |
73
74
  <!-- /stable:commands -->
74
75
 
75
- `TY13` holds the first column to the commands `cli.ts` dispatches and the set of
76
- flags named anywhere in the table to the set of flags `cli.ts` spells. It holds the
77
- names, not which command takes which flag: that pairing is the CLI's, and the `cli`
78
- suite's controls read it from the help.
76
+ `TY13` holds the first column to the commands `cli.ts` dispatches, the set of
77
+ flags in the table to the flags `cli.ts` spells, and each command's listed flags
78
+ to its parser's `TAKES` row in both directions. Repeated proposal modes share one
79
+ command's flag union; `comfy paint` and `comfy seethrough` are checked separately.
80
+ Required combinations within a mode remain the CLI's; the `cli` suite's controls
81
+ read them from the help.
79
82
 
80
83
  ## The config contract
81
84
 
@@ -209,7 +212,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
209
212
 
210
213
  | file | written by | what is stable |
211
214
  | --- | --- | --- |
212
- | `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, `short_px` (`left`, `right`) after it only when `missing_px` is above 0 (issue #557, additive), `excluded_px` after them only on a subset set's limb part whose cap the set exclusion took a pixel of (issue #603, additive: each pixel taken out of the count it was in, so the cap's counts are the written PNG's), then `shape` and `upper_px` under the segment's `cap_shape` — `upper_px` without the shape only when the upper segment gave a pixel (issue #557, additive) — and after them `lower_px`, `lower_kept_px` and `lower_missing_px` under `cap_shape: "ball"` (issue #579, additive), and `edge`, `feather_radius` and `feathered_px` under its `edge`, and `cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and `stroke_luma_max` (when the band has a split) under its `cut_band`, issue #360), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px` — on a subset set's limb part counted after the set exclusion, issue #603 — `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242); `single_run_pad` (`file_size`, `side`, `left`, `top`) last, only for one run whose file is not the `seethrough.resolution` square (issue #366) |
215
+ | `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, `short_px` (`left`, `right`) after it only when `missing_px` is above 0 (issue #557, additive), `excluded_px` after them only on a subset set's limb part whose cap the set exclusion took a pixel of (issue #603, additive: each pixel taken out of the count it was in, so the cap's counts are the written PNG's), then `shape` and `upper_px` under the segment's `cap_shape` — `upper_px` without the shape only when the upper segment gave a pixel (issue #557, additive) — and after them `lower_px`, `lower_kept_px` and `lower_missing_px` under `cap_shape: "ball"` (issue #579, additive), and `edge`, `feather_radius` and `feathered_px` under its `edge`, and `cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and `stroke_luma_max` (when the band has a split) under its `cut_band`, issue #360), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px` — on a subset set's limb part counted after the set exclusion, issue #603 — `seam_override_px`, and `fringe_pushed_back` when not empty); per-part `provenance` (`unchanged_layer_px`, `hand_px`, `automatic_px`, `edits`, `automatic`, additive, issue #823); optional top-level `motion_seams`, `alternates` and `cut_ownership` (one row per cut, independent of RGBA equality, issue #823); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242); `single_run_pad` (`file_size`, `side`, `left`, `top`) last, only for one run whose file is not the `seethrough.resolution` square (issue #366) |
213
216
  | `rig/rig.json`, `rig/motion.json`, `rig/images/` | `rig`, `build`, `compose` | rig-c's rig spec and motion spec, `spec` `rigc-rig/1` and `rigc-motion/1`: the format is rig-c's, and this package writes only what rig-c's parser accepts; a bone carries `inherit` only where config.bones declares one, rig-c's mode name as written (issue #348) |
214
217
  | `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones` (the sorted names the attachment's weights bind in the written `rig.json`, in every mode — since 1.49.1 (issue #536) a rig with `turn` or `stops` names the twins and `<link>_d` bones there, where the row named the segment bones they replace: a fix of a wrong name, not a format change; a rig with neither writes the bytes it wrote), `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); or `mode` "explicit" with `image_sha256` and `region_sha256` (the verified guards; counts and coverage are measured from the supplied mesh, no legacy generator report is adopted); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins`, and `faint` only when an island was left out as faint — only when it declares `contour.bridges`); or `mode` "influence" with `influence` (as declared), `reach` (`farthest`, `at`, `nearest`, `capped`, `widest`) and `lattice` (rig-c's `SegmentsLatticeReport`: `cols`, `rows`, `artCells`, `keptCells`, `passes`, `islands`) (issue #378); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
215
218
  | `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40`, and `step_px` (`left`, `right`) only on a joint whose cap record carries `short_px` (issue #557, additive) — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493 retains existing pair warnings; issue #686 gates GAP on proven body-root pairs only (gap-only; mixed ghost remains INFO until rig-c/core#1555 supplies owners): `status` — `WARN`, `CLEAR`, `SKIP` or `FAIL` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, optional `root` (`limb`, `bone`, `parent`) on a proven body-root pair, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`, and only on new-root rows stock `gapOpen` plus UNDER-owned `ghostA`/`ghostB` cells selected by resolved per-frame draw order gate their summed count against the unchanged strict one-cell floor on the original raw kind boxes (their union when both kinds contribute), never narrowed partition boxes, while `gapCovered` and OVER-owned ghost cells are excluded (issue #711); `cells` retains partition counts, UNDER owner and counted total, `worst` retains frame/count/cell/box, and `roots` retains status, mode `gap-open+under-ghost`, ghost_owner `resolved-draw-order`, pairs, failed, reason and optional unmeasured bindings; eligible candidates whose valid setup bands are all zero retain a named unmeasured root and SKIP bar, including when no pair is selected; measured root FAIL wins over SKIP, while measured PASS plus an unmeasured root remains SKIP; every eligible body drawing with stock setup `banded > 0` is selected, including both when a parent is shared; issue #708 resolves a body slot on its exact generated turn child to the same named carrier only after unique identity-bone/control/per-part-transform proof, without changing pair identity, setup selection or the direct-carrier path; setup-only observation preserves the authored motion and default drawing, root-free inputs add no setup compile/render, and no animation remains SKIP rather than a measured zero; existing internal pair warnings and their exits are unchanged; issue #591 adds optional `worlds` on physics rigs with readable declared pairs and animations: `default` and `still`, each `wind`, `gravity`, `status` (`REPORTED` or `SKIP`), `reason` on SKIP, `dir` only when rendered (relative to check.json: `seam_motion/default` or `seam_motion/still`, the complete retained render with frames.json and each animation's raw seam.json), and `rows` — pair, animation, status, drawn, banded, `gap` and `ghost`, each `max_px` and `worst` only; worst carries `index`, `px`, `cell`, `box`. Per-frame hits stay in the raw sidecars. These raw reference readings add no bar or warning and leave the custom environment reading unchanged), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `WIND_BAND` after it under `--wind-band` only (issue #590, additive: `status` `"REPORTED"`, `spec` `"rig-parts-wind-band/1"`, `path` as given, `sha256`, `steady_wind_only` `true`, `gravity` `[0, 1]`, `min`, `max`, `step`, `steps`, `fps`, `animations`, `max_gap`, `max_ghost`, `holds_from`, `holds_to`, `pairs_holding_all`, and `pairs` — each `pair`, `a`, `b`, `holds_all`, `holds_from`, `holds_to`, `first_break_below`, `first_break_above`, `worst_gap`, `worst_ghost`, `steps`), `skipped`, `config` under `build` only (issue #607, additive: `path` as `--config` was typed, `sha256` of the bytes the build read; `check` and `compose` read no config and write none), a `requirements` block under `--requirements` (or `build`'s `config.requirements`) — `file` first from issue #556, additive: `path` as typed or declared, `sha256`, `keys` — and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes. Missing stock partitions or resolved owner are named refusals, never zero. No whole-box exemption or consumer compositor remains; internal warnings and reference raw readings are unchanged. |
@@ -217,7 +220,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
217
220
  | the template `.psd` | `layers --template` | one empty layer per name `templateLayerNames` (`src/layers.ts`) holds, bottom first; the same canvas writes the same bytes |
218
221
  | `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `constraints` with `--cloth` (issue #364: a physics constraint per tree link named `<bone>_physics`, a dangling end's `<bone>_translate`, and a skirt swing's transforms `<swing>_<tree>`), `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes` |
219
222
  | `basis.json` | `propose` | spec `spine-parts-basis/1` |
220
- | an existing config named by `--write-config` | `propose --head-box`, `assemble --propose-plan`, ordinary `propose` | issue #356, additive: `assemble --propose-plan` must update `--config` itself; only the selected proposed fields are replaced by whole-value text splices, preserving untouched bytes; absent optional proposal sections and other fields remain; the stage loader validates before atomic replacement, relative paths keep their base, an unchanged update preserves bytes and mtime, and stderr lists changes; no multi-artifact transaction is promised; with `--only-missing` (issue #671) only a `meshes.<part>` or `regions.<part>` entry for a proposed part the config attaches by neither is added, and an `assemble.plan` row for such a part the 1.37.0 arm split makes that the plan does not name, every existing row and entry and every other byte as read |
223
+ | an existing config named by `--write-config` | `propose --head-box`, `assemble --propose-plan`, ordinary `propose`, sampled-turn neck draft (`propose … --full --head --write-config`, issue #826) | issue #356, additive: `assemble --propose-plan` must update `--config` itself; only the selected proposed fields are replaced by whole-value text splices, preserving untouched bytes; absent optional proposal sections and other fields remain; the stage loader validates before atomic replacement, relative paths keep their base, an unchanged update preserves bytes and mtime, and stderr lists changes; no multi-artifact transaction is promised; with `--only-missing` (issue #671) only a `meshes.<part>` or `regions.<part>` entry for a proposed part the config attaches by neither is added, and an `assemble.plan` row for such a part the 1.37.0 arm split makes that the plan does not name, every existing row and entry and every other byte as read |
221
224
  | a new `--write-config` file | `propose --cloth … --write-config` | complete checked splice text published without overwrite; an intervening creator is refused, while cleanup failure after publication may leave the complete new file present; intentional partial configs retain their existing contract; the `--cloth` config read, byte for byte, but for the entries the recipe drafts (issue #537), measured against the same parts drafted without it: `meshes.<part>` and `regions.<part>` for each part the recipe moves, and `bones`, `constraints` and `motion.tracks` entry by entry by name (issue #552) — an entry the recipe drafts replaced or inserted, one it replaces removed, every other entry kept with its bytes and its place, and a hand edit to an entry the recipe owns refused by name (`PROPOSE_WRITE_CONFIG_CONFLICT`) rather than dropped; a section the input lacks is inserted; with `--from <previous config>` (issue #571) an entry equal to the previous recipe's draft is the recipe's as well |
222
225
  | `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw`, and `faint` only when an island was left out as faint — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto`, `contour`, `influence` or `explicit` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
223
226
  | `rig.json`, `motion.json`, `mesh_report.json`, `images/`, `remesh.json` | `remesh` | the input revision's files, `rig.json` and `mesh_report.json` with the part's attachment and row replaced (under `--delta` the row is the input's with its counts, `bones` and influences recomputed and `mode` "delta"), every other file byte for byte; `remesh.json` is spec `rig-parts-remesh/1`: `request` (`part`, `mesh`, `mesh_sha256`, `idle_keys`, `page_edges`, `pack_shape`; under `--delta`, issue #526: `part`, `delta` — the mesh edit as read — `delta_sha256`, `ops`, `applied` — `moved`, `added`, `removed`, `constrained` — `idle_keys`, `page_edges`, `pack_shape`), `versions` (`parts`, `rigc`), `input` (`rig` — `path`, `bytes`, `sha256` per file — `config_sha256`, `parts_json_sha256`), `baseline` (`reproduced`, `not_compared`; under `--delta` `reproduced` holds one sentence: no config is built, the input is the baseline), `target` (`before`, `after`: `vertices`, `triangles`, `hull`), `preserved` (`identical`, `spliced`), `revision` (per file as `input.rig`), `check` (`input`, `revision`: `pass`, `measured`, `not_met`, `skipped`, `physics`, then `line` and `status` — issue #541, additive, the spec stays `rig-parts-remesh/1`: `line` is the last line `check` prints on that tree, byte for byte, and `status` every block's status as that tree's `check.json` records it, by name in the file's order — `gate`, `loop` and `seam` read from their figures, every other block its own `status`, a block the file does not write absent; `scope`) |
@@ -300,7 +303,7 @@ in the package.
300
303
 
301
304
  ## rig-c
302
305
 
303
- `dependencies` takes `@rig-c/core` `^2.72.0`: 2.72.0 or any later 2.x. Bun 1.4 or later
306
+ `dependencies` takes `@rig-c/core` `^2.81.0`: 2.81.0 or any later 2.x. Bun 1.4 or later
304
307
  is the declared floor (`engines.bun` `>=1.4.0`; older 1.4 releases are not tested in
305
308
  CI, which runs `latest`, and compatibility with an older 1.4 release is handled when
306
309
  reported as an issue). What this package relies on in rig-c, every import
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-parts",
3
- "version": "1.78.0",
3
+ "version": "1.79.0",
4
4
  "description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "ag-psd": "^31.0.2",
68
- "@rig-c/core": "^2.72.0"
68
+ "@rig-c/core": "^2.81.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@esotericsoftware/spine-core": "4.3.13",
@@ -2,7 +2,7 @@
2
2
  name: rig-parts
3
3
  description: Turn one anime character painting and its See-through layer decomposition into a verified 2D character rig (Spine 4.3 skeleton data) — measured parts, weighted meshes, bone chains and a looping idle — with rig-parts, which gates everything through rig-c before it is written. Use for a request to rig or animate a single character painting, to check a painting before See-through (rig-parts painting), to run or read rig-parts layers, sheet, assemble, propose, rig, check, loop or build, or to write or fix a rig-parts config.json. Not for cutting art into parts by hand, for authoring a rig from loose PNGs (that is the rigc skill), for lip-sync or swapping a part's image, or for running See-through itself.
4
4
  license: MIT
5
- compatibility: Requires Bun 1.4 or later (declared in engines.bun >=1.4.0; CI tests latest only). The tool is the npm package @rig-c/parts (npm i -D @rig-c/parts, or bun add -d @rig-c/parts), whose command is rig-parts; rig-c is installed with it. See-through is run separately.
5
+ compatibility: Requires Bun 1.4 or later (declared in engines.bun >=1.4.0; CI tests latest only). The tool is the npm package @rig-c/parts (npm i -D @rig-c/parts, or bun add -d @rig-c/parts), whose command is rig-parts; @rig-c/core (the rigc command) is installed with it. See-through is run separately.
6
6
  ---
7
7
 
8
8
  # rig-parts — a painting to a Spine rig, for agents
package/src/assemble.ts CHANGED
@@ -241,7 +241,7 @@ export interface Geometry {
241
241
  keySkeleton?: ControlSkeleton;
242
242
  /** `assemble.head_face_underfill` (issue #492): composite `full:face` under `head:face` on the head canvas to fill transparent bare-skin holes. */
243
243
  headFaceUnderfill?: boolean;
244
- /** `assemble.yield_refused`, default false; alpha-only ownership transfer from original refused core pixels. */
244
+ /** `assemble.yield_refused`, default false; alpha-only transfer of refused cores and their connected non-core rims. */
245
245
  yieldRefused?: boolean;
246
246
  /** Plan parts whose own samples survive source re-take beneath soft front cover. */
247
247
  keepUnderOccluders?: readonly string[];
@@ -362,7 +362,7 @@ export interface AssembleInput {
362
362
  keySkeleton?: ControlSkeleton;
363
363
  /** `assemble.head_face_underfill` (issue #492): composite `full:face` under `head:face` on the head canvas to fill transparent bare-skin holes. */
364
364
  headFaceUnderfill?: boolean;
365
- /** `assemble.yield_refused`, default false; alpha-only ownership transfer from original refused core pixels. */
365
+ /** `assemble.yield_refused`, default false; alpha-only transfer of refused cores and their connected non-core rims. */
366
366
  yieldRefused?: boolean;
367
367
  /** Plan parts whose own samples survive source re-take beneath soft front cover. */
368
368
  keepUnderOccluders?: readonly string[];
@@ -4388,6 +4388,8 @@ export interface DriftYieldReading {
4388
4388
  from: string;
4389
4389
  refusedCandidates: number;
4390
4390
  yielded: number;
4391
+ /** Non-core pixels on an original 8-connected part body seeded by a yielded core. Included in yielded. */
4392
+ yieldedRim: number;
4391
4393
  /** Yielded refusals also accepted by projection closing; removed from the written projected count. */
4392
4394
  yieldedTaken: number;
4393
4395
  /** Original refused pixels explicitly kept from their own layer (#825). */
@@ -5316,7 +5318,7 @@ export function assemble(input: AssembleInput): AssembleResult {
5316
5318
  let yielded = 0;
5317
5319
  if (input.yieldRefused === true) {
5318
5320
  const mask = newMask(W, H);
5319
- const reading: DriftYieldReading = { part: name, from: b.key, refusedCandidates: 0, yielded: 0, yieldedTaken: 0, skipped: { merge: 0, ring: 0, notOpaque: 0, noMatchingBehind: 0 }, originalGlobalHolderWouldSkip: 0, originalHolders: [], finalHolders: [] };
5321
+ const reading: DriftYieldReading = { part: name, from: b.key, refusedCandidates: 0, yielded: 0, yieldedRim: 0, yieldedTaken: 0, skipped: { merge: 0, ring: 0, notOpaque: 0, noMatchingBehind: 0 }, originalGlobalHolderWouldSkip: 0, originalHolders: [], finalHolders: [] };
5320
5322
  const holders = new Map<string | null, number>();
5321
5323
  for (let p = 0; p < W * H; p++) {
5322
5324
  if (refused.data[p] !== 1) continue;
@@ -5334,6 +5336,38 @@ export function assemble(input: AssembleInput): AssembleResult {
5334
5336
  b.r.data[p * 4 + 3] = 0;
5335
5337
  yielded++;
5336
5338
  }
5339
+ if (yielded > 0) {
5340
+ // Connectivity reads the original part support, including the opaque
5341
+ // band outside the eroded projection core. Only non-core rim pixels
5342
+ // may additionally yield: traversing an opaque pixel never clears it.
5343
+ // Merge/ring replacements, kept samples and another piece's region
5344
+ // cannot connect a rim to this source's refused body.
5345
+ const seen = new Uint8Array(W * H), queue = new Int32Array(W * H);
5346
+ let end = 0;
5347
+ for (let p = 0; p < W * H; p++) if (mask.data[p] === 1) { seen[p] = 1; queue[end++] = p; }
5348
+ for (let at = 0; at < end; at++) {
5349
+ const p = queue[at], x = p % W, y = Math.floor(p / W);
5350
+ for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
5351
+ const nx = x + dx, ny = y + dy;
5352
+ if (nx < 0 || nx >= W || ny < 0 || ny >= H) continue;
5353
+ const q = ny * W + nx;
5354
+ if (seen[q] === 1) continue;
5355
+ seen[q] = 1;
5356
+ const alpha = b.r.data[q * 4 + 3];
5357
+ if (alpha === 0 || (b.region !== null && b.region.data[q] === 0) || keptRetake[orderIdx]?.data[q] === 1 || b.trace.from[q] >= 0 || b.trace.ring.data[q] !== 0) continue;
5358
+ queue[end++] = q;
5359
+ if (alpha >= CORE_ALPHA || behindAt[q] < 0) continue;
5360
+ const holder = holderAt[q] < 0 ? null : stackNames[holderAt[q]];
5361
+ holders.set(holder, (holders.get(holder) ?? 0) + 1);
5362
+ if (holderAt[q] !== orderIdx) reading.originalGlobalHolderWouldSkip++;
5363
+ mask.data[q] = 1;
5364
+ if (b.st.takenMask.data[q] === 1) reading.yieldedTaken++;
5365
+ b.r.data[q * 4 + 3] = 0;
5366
+ reading.yieldedRim++;
5367
+ yielded++;
5368
+ }
5369
+ }
5370
+ }
5337
5371
  reading.yielded = yielded;
5338
5372
  reading.originalHolders = [...holders].map(([part, px]) => ({ part, px }));
5339
5373
  yields.set(name, { mask, reading });
@@ -5622,7 +5656,7 @@ export function driftLine(d: DriftBodies): string {
5622
5656
  `by the plan's draw order ${d.holders.map((h) => `"${h.part}" holds ${h.px} px`).join(', ')} of it (the top-most part at alpha >= ${CORE_ALPHA})${frontClause}${behindClause}; ` +
5623
5657
  "informational: no gate reads this. Swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do): swap, rebuild and read this line again; if it moves, removing it needs different input layers. At rest, the seam pass repaints the holder's pixels that pass its rule from the painting" +
5624
5658
  (d.yieldedPx === undefined
5625
- ? '; set assemble.yield_refused: true to yield original refused front pixels only where the top-most opaque behind holder matches the painting, before considering a cut'
5659
+ ? '; set assemble.yield_refused: true to yield original refused front pixels and their connected non-core rims only where the top-most opaque behind holder matches the painting, before considering a cut'
5626
5660
  : `; yielded ${d.yieldedPx} px to top-most opaque behind holders matching the painting (assemble.yield_refused: true; alpha only)`)
5627
5661
  );
5628
5662
  }
@@ -6324,7 +6358,7 @@ export function stageFields(
6324
6358
  armSplit?: ArmSplit;
6325
6359
  keySkeleton?: ControlSkeleton;
6326
6360
  headFaceUnderfill?: boolean;
6327
- /** `assemble.yield_refused`, default false; alpha-only ownership transfer from original refused core pixels. */
6361
+ /** `assemble.yield_refused`, default false; alpha-only transfer of refused cores and their connected non-core rims. */
6328
6362
  yieldRefused?: boolean;
6329
6363
  /** Plan parts whose own samples survive source re-take beneath soft front cover. */
6330
6364
  keepUnderOccluders?: readonly string[];
package/src/build.ts CHANGED
@@ -1631,6 +1631,8 @@ export interface CheckStageInput {
1631
1631
  seamPairs?: boolean;
1632
1632
  /** `check --keep-stills` (issue #209): leave the setup-pose still and its `frames.json` under {@link STILL_DIR}. */
1633
1633
  keepStills?: boolean;
1634
+ /** Skip the extra extremes renders and remove stale extremes outputs (issue #829). */
1635
+ noExtremes?: boolean;
1634
1636
  /** `check --set <name>` (issue #454): measure one declared alternate set's setup-pose seam and recomposite figures. */
1635
1637
  set?: string;
1636
1638
  /** `check --all-sets` (issue #454): measure every declared alternate set's setup-pose seam and recomposite figures. */
@@ -1873,11 +1875,12 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
1873
1875
  const entryLine = versionLines.find((l) => l.startsWith('entry:'));
1874
1876
  log(` ${rigcIdentity(bin, v)}${entryLine === undefined ? '' : `; ${entryLine}`}`);
1875
1877
  const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
1876
- const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true, input.set, input.allSets === true, input.environment, input.requirementsAs, input.config, input.windBand);
1878
+ const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true, input.set, input.allSets === true, input.environment, input.requirementsAs, input.config, input.windBand, input.noExtremes === true);
1877
1879
  if (r.figures.environment !== undefined) log(` ${environmentLine(r.figures.environment)}`);
1878
1880
  log(` gate rig-play (rigc ${packedBuildLabel(mode)}), verbatim:`);
1879
1881
  for (const l of r.gateHtml) log(l);
1880
1882
  for (const l of packLines(r)) log(` ${l}`);
1883
+ for (const line of r.extremes ?? []) log(` ${line}`);
1881
1884
  const fig = r.figures;
1882
1885
  if (r.idle === null) log(` loop: SKIP — ${fig.skipped?.loop ?? ''}`);
1883
1886
  else log(` ${loopCheckLine(r.idle, fig.loop_max_diff, fig.loop_physics?.length ?? 0)}`);
@@ -2213,6 +2216,8 @@ export interface BuildInput {
2213
2216
  seamPairs?: boolean;
2214
2217
  /** `--keep-stills`, forwarded to the check as `check` takes it (issue #257): the setup-pose still under `check/setup_still/`; absent, nothing moves. */
2215
2218
  keepStills?: boolean;
2219
+ /** Skip the extra extremes renders and remove stale extremes outputs (issue #829). */
2220
+ noExtremes?: boolean;
2216
2221
  /** `--alt`, forwarded to the assemble stage as `assemble` takes it (issue #454). */
2217
2222
  alts?: readonly AlternateCliInput[];
2218
2223
  /** `--set <name>`, forwarded to the check as `check` takes it (issue #454). */
@@ -2249,6 +2254,7 @@ export const CHECK_FLAGS_IN_BUILD: Readonly<Record<string, CheckFlagInBuild>> =
2249
2254
  '--all-sets': { as: 'forwarded' },
2250
2255
  '--seam-pairs': { as: 'forwarded' },
2251
2256
  '--keep-stills': { as: 'forwarded' },
2257
+ '--no-extremes': { as: 'forwarded' },
2252
2258
  '--environment': { as: 'forwarded' },
2253
2259
  '--wind-band': { as: 'forwarded' },
2254
2260
  };
@@ -2414,7 +2420,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
2414
2420
 
2415
2421
  let report: CheckReport;
2416
2422
  try {
2417
- report = stages.check({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(req === null ? {} : { requirements: req.read, requirementsAs: req.recorded }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true, ...(input.set === undefined ? {} : { set: input.set }), allSets: input.allSets === true, ...(input.environment === undefined ? {} : { environment: input.environment }), ...(input.windBand === undefined ? {} : { windBand: input.windBand }), ...(configRecord === null ? {} : { config: configRecord }) }, run.check, run.checkBin, prefixed('check'));
2423
+ report = stages.check({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(req === null ? {} : { requirements: req.read, requirementsAs: req.recorded }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true, noExtremes: input.noExtremes === true, ...(input.set === undefined ? {} : { set: input.set }), allSets: input.allSets === true, ...(input.environment === undefined ? {} : { environment: input.environment }), ...(input.windBand === undefined ? {} : { windBand: input.windBand }), ...(configRecord === null ? {} : { config: configRecord }) }, run.check, run.checkBin, prefixed('check'));
2418
2424
  } catch (err) {
2419
2425
  return refused('check', err);
2420
2426
  }
package/src/check.ts CHANGED
@@ -36,6 +36,7 @@ import { baseAnchorMoves, type AnchorRegion } from './baseanchor.ts';
36
36
  * | `idle_frames/` | `rigc render --animation idle --fps 12 --max 640 --geometry`: `frames.json` + `idle/f*.png` + `idle/geometry.json` (each mesh's skinned vertices, which `TEXTURE_STRETCH` reads, every attachment's posed triangles, which `TIP_OVER_ROOT` and both halves of `STILL_REGIONS_DARK` read since issues #118 and #123, and every bone's world transform, which the face half reads its head bone from) |
37
37
  * | `contact.png` | rigc's own contact sheet of that render, copied out |
38
38
  * | `motion_heat.png` | frame 0 in grey with each pixel's largest change across the idle in red |
39
+ * | `extremes/<anim>/extremes.png`, `extremes.json` | core min/max/diff observations at 12 fps, scale 1; static geometry writes a named SKIP and JSON only; `--no-extremes` removes stale diagnostics and runs no extra render |
39
40
  * | `check.json` | the figures below and `PASS` |
40
41
  * | `requirements/` | under `--requirements` only (issue #93): `as-declared/<animation>/`, and per follow `released/<name>/` and `full/<name>/` — rigc renders with `--geometry` ({@link measureRequirements}) |
41
42
  *
@@ -2960,6 +2961,8 @@ export function requirementsNotPass(fig: Readonly<Record<string, unknown>>): str
2960
2961
  }
2961
2962
 
2962
2963
  export interface CheckReport {
2964
+ /** Core EXTREMES summaries and named SKIPs; diagnostics, never check.json or a bar. */
2965
+ extremes?: string[];
2963
2966
  figures: CheckFigures;
2964
2967
  gateHtml: string[];
2965
2968
  pack: PackLine[];
@@ -5740,6 +5743,56 @@ export function windBandLines(line: WindBandLine, plant: WindBandPlant = {}): st
5740
5743
  return [summary, ...breaking];
5741
5744
  }
5742
5745
 
5746
+ /**
5747
+ * Issue #829: observations only. The fitted world box belongs to rigc; changing
5748
+ * its grid to one pixel per world unit makes the panels native, without a
5749
+ * second framing implementation. Each animation is rendered separately to
5750
+ * release its sampled poses before the next. Only the two observation files
5751
+ * survive; the ordinary frames and contact sheets are scratch.
5752
+ */
5753
+ function renderExtremes(build: string, out: string, animations: readonly string[], frames: unknown, render: RigcRunner): string[] {
5754
+ if (animations.length === 0) return ['EXTREMES SKIP: CHECK_EXTREMES_NO_ANIMATIONS — no animation in motion.json'];
5755
+ const scratch = join(out, '_extremes');
5756
+ const dest = join(out, 'extremes');
5757
+ try {
5758
+ if (!isRecord(frames) || !isRecord(frames.viewport)) throw new Error('the existing render has no fitted viewport');
5759
+ const vp = frames.viewport;
5760
+ for (const key of ['x', 'y', 'width', 'height']) {
5761
+ const value = vp[key];
5762
+ if (typeof value !== 'number' || !Number.isFinite(value) || ((key === 'width' || key === 'height') && value <= 0)) throw new Error(`fitted viewport.${key} is ${JSON.stringify(value)}`);
5763
+ }
5764
+ mkdirSync(scratch, { recursive: true });
5765
+ const grid = join(scratch, 'native.json');
5766
+ writeFileSync(grid, JSON.stringify({ spec: 'rigc-frames/1', viewport: { ...vp, scale: 1, pixelWidth: Math.max(1, Math.round(vp.width as number)), pixelHeight: Math.max(1, Math.round(vp.height as number)) } }));
5767
+ const lines: string[] = [];
5768
+ for (const animation of animations) {
5769
+ const rendered = join(scratch, 'render');
5770
+ const call = render(['render', '--candidate', build, '--animation', animation, '--fps', String(IDLE_FPS), '--viewport-from', grid, '--extremes', '--out', rendered]);
5771
+ if (call.status !== 0) throw new Error(`render --extremes ${JSON.stringify(animation)} exited ${call.status}: ${call.out.trim()}${rigcRanClause(call)}`);
5772
+ const summaries = call.out.split('\n').map(line => line.trim().replace(/^\.\.\s+/, '')).filter(line => line.startsWith('EXTREMES '));
5773
+ if (summaries.length === 0) throw new Error(`render --extremes ${JSON.stringify(animation)} printed no EXTREMES summary`);
5774
+ // The sidecar owns directory naming, including core's escaping of names.
5775
+ const sidecar = JSON.parse(readFileSync(join(rendered, 'frames.json'), 'utf8')) as { sets: Array<{ animation: string; dir: string }> };
5776
+ const set = sidecar.sets.find(row => row.animation === animation);
5777
+ if (set === undefined) throw new Error(`render --extremes has no set for ${JSON.stringify(animation)}`);
5778
+ const source = join(rendered, set.dir), target = join(dest, set.dir);
5779
+ mkdirSync(target, { recursive: true });
5780
+ copyFileSync(join(source, 'extremes.json'), join(target, 'extremes.json'));
5781
+ if (existsSync(join(source, 'extremes.png'))) copyFileSync(join(source, 'extremes.png'), join(target, 'extremes.png'));
5782
+ lines.push(...summaries);
5783
+ rmSync(rendered, { recursive: true, force: true });
5784
+ }
5785
+ return lines;
5786
+ } catch (err) {
5787
+ // Diagnostics never add a measured bar or change the gate's verdict. An
5788
+ // incomplete observation is explicitly unavailable, not a partial sheet set.
5789
+ rmSync(dest, { recursive: true, force: true });
5790
+ return [`EXTREMES SKIP: CHECK_EXTREMES_RENDER_FAILED — ${err instanceof Error ? err.message : String(err)}`];
5791
+ } finally {
5792
+ rmSync(scratch, { recursive: true, force: true });
5793
+ }
5794
+ }
5795
+
5743
5796
  /**
5744
5797
  * Run the whole check. Refuses (throws a PartsError) when the inputs are
5745
5798
  * unreadable or a rigc step that everything after it depends on is red — the
@@ -5766,7 +5819,7 @@ export function windBandLines(line: WindBandLine, plant: WindBandPlant = {}): st
5766
5819
  * MEASURABLE is a problem. Without it nothing here reads, writes or prints
5767
5820
  * anything it did not before.
5768
5821
  */
5769
- export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome?: string, mode: PackMode = DEFAULT_PACK_MODE, source?: string, requirements?: string, seamPairsAsked = false, keepStills = false, set?: string, allSets = false, environment?: string, requirementsAs: string | undefined = requirements, config?: InputFileRecord, windBand?: string): CheckReport {
5822
+ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome?: string, mode: PackMode = DEFAULT_PACK_MODE, source?: string, requirements?: string, seamPairsAsked = false, keepStills = false, set?: string, allSets = false, environment?: string, requirementsAs: string | undefined = requirements, config?: InputFileRecord, windBand?: string, noExtremes = false): CheckReport {
5770
5823
  let inp: CheckInputs;
5771
5824
  let reqFile: RequirementsFile | null = null;
5772
5825
  let reqRecord: RequirementsFileRecord | null = null;
@@ -5851,7 +5904,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
5851
5904
  const foldDir = join(out, FOLD_SCRATCH);
5852
5905
  const seamMotionDir = join(out, MOTION_SEAM_SCRATCH);
5853
5906
  const windBandDir = join(out, WIND_BAND_SCRATCH);
5854
- for (const d of [buildDir, idleDir, stillDir, isoDir, loopDir, seamDir, keptDir, join(out, 'setup_still_auto'), foldDir, seamMotionDir, windBandDir]) rmSync(d, { recursive: true, force: true });
5907
+ for (const d of [join(out, 'extremes'), join(out, '_extremes'), buildDir, idleDir, stillDir, isoDir, loopDir, seamDir, keptDir, join(out, 'setup_still_auto'), foldDir, seamMotionDir, windBandDir]) rmSync(d, { recursive: true, force: true });
5855
5908
  const written: string[] = [];
5856
5909
  const write = (name: string, data: string): void => {
5857
5910
  writeFileSync(join(out, name), data);
@@ -5917,6 +5970,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
5917
5970
  rmSync(windBandDir, { recursive: true, force: true });
5918
5971
  }
5919
5972
  }
5973
+ let extremesFrames: unknown = null;
5920
5974
  let folds: FoldsLine | null = null;
5921
5975
  const foldProblems: Problem[] = [];
5922
5976
  // Issue #348: the feet the level line reads, off parts.json and rig.json; none, and nothing below moves.
@@ -6046,6 +6100,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
6046
6100
  level = levelLine(feet, inp.rig, levelReadings, fps, stripped.physics.length, levelProblems);
6047
6101
  }
6048
6102
  }
6103
+ if (!noExtremes && idle === null) extremesFrames = readJson(join(stripped === null ? foldDir : join(foldDir, 'on'), 'frames.json'), []);
6049
6104
  } finally {
6050
6105
  rmSync(foldDir, { recursive: true, force: true });
6051
6106
  }
@@ -6611,6 +6666,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
6611
6666
  const publishCheck = (): void => write('check.json', `${JSON.stringify(figures, null, 1)}\n`);
6612
6667
  if (motionSeam.worlds === undefined) publishCheck();
6613
6668
  else publishMotionSeamWorlds(out, seamMotionDir, motionSeam.worlds, publishCheck);
6669
+ const extremes = noExtremes ? [] : renderExtremes(buildDir, out, animationNames, idle === null ? extremesFrames : readJson(join(idleDir, 'frames.json'), []), measured);
6670
+ if (existsSync(join(out, 'extremes'))) written.push('extremes/');
6614
6671
  const bars: CheckReport['bars'] = { measured: ['gate'], skipped: [] };
6615
6672
  const tally = (bar: string, reason: string | null): void => {
6616
6673
  if (reason === null) bars.measured.push(bar);
@@ -6636,6 +6693,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
6636
6693
  return {
6637
6694
  figures,
6638
6695
  gateHtml,
6696
+ extremes,
6639
6697
  pack,
6640
6698
  packOpaque,
6641
6699
  idle: idle === null || loop === null ? null : { frames: idle.frames.length, fps: idle.fps, duration: idle.duration, lastIndex: idle.frames[idle.frames.length - 1].index, loopAt: { x: loop.x, y: loop.y } },
package/src/config.ts CHANGED
@@ -307,7 +307,7 @@ export interface Assemble {
307
307
  alternates?: AlternateSetConfig[];
308
308
  /** Issue #492: composite full:face under head:face on the head canvas to fill transparent holes inside the face silhouette. */
309
309
  head_face_underfill?: boolean;
310
- /** Opt-in: yield original drift-refused front pixels to an opaque behind holder matching the painting. */
310
+ /** Opt-in: yield original drift-refused front cores and connected non-core rims to an opaque behind holder matching the painting. */
311
311
  yield_refused?: boolean;
312
312
  /** Named plan parts retain their own pixels beneath a drawn-in-front soft occluder. */
313
313
  keep_under_occluders?: string[];