rig-parts 1.78.0 → 1.79.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/cli.ts +14 -7
- package/docs/AUTHORING.md +54 -32
- package/docs/MIGRATION.md +3 -3
- package/docs/STABILITY.md +13 -10
- package/package.json +2 -2
- package/skills/rig-parts/SKILL.md +1 -1
- package/src/assemble.ts +44 -5
- package/src/build.ts +12 -3
- package/src/check.ts +60 -2
- package/src/config.ts +1 -1
- package/src/depthsheets.ts +9 -3
- package/src/neckdraft.ts +1 -0
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
|
|
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.
|
|
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.
|
|
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.
|
|
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`,
|
|
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`
|
|
2946
|
-
`turn` and `tilt`. Read setup, min,
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
@@ -5757,7 +5772,14 @@ Assembly uses the actual run registration, source-copy/composite coordinates,
|
|
|
5757
5772
|
merge ownership and final crop. Scalar bilinear sampling at pixel centres is
|
|
5758
5773
|
quantized half up at each intermediate copy/composite grid, then again at
|
|
5759
5774
|
final part registration. PAD4 repeats the resulting u8 edge values. There is no
|
|
5760
|
-
universal half-level total-error bound across those operators.
|
|
5775
|
+
universal half-level total-error bound across those operators. The drawing warp rounds its inverse coordinates to
|
|
5776
|
+
1/32 px and uses bilinear or bicubic support; scalar sampling keeps its existing
|
|
5777
|
+
unrounded inverse map plus the half-pixel centre. Visible final-grid samples
|
|
5778
|
+
less than one source pixel outside a registered crop retain the edge texel.
|
|
5779
|
+
At one pixel or more outside on either axis, `DEPTH_PART_SUPPORT` still refuses.
|
|
5780
|
+
Assembly logs and neck-draft notes report the clamped visible sample count per
|
|
5781
|
+
part, counting a corner once. Transparent cells are excluded from that count.
|
|
5782
|
+
No source sheet is padded or inferred. No art-color
|
|
5761
5783
|
premultiplication or local min/max rescaling is applied. The rig sheet receives the drawing's PAD4 with
|
|
5762
5784
|
explicit rectangular edge clamping. Original source hashes, rectangles, medians
|
|
5763
5785
|
and declared units accompany the optional `parts.json` part `depth` reference.
|
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.
|
|
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,
|
|
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
|
|
76
|
-
flags
|
|
77
|
-
|
|
78
|
-
|
|
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.
|
|
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.
|
|
3
|
+
"version": "1.79.1",
|
|
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.
|
|
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
|
|
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
|
|
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). */
|
|
@@ -4443,6 +4445,8 @@ export interface AssembleResult {
|
|
|
4443
4445
|
seamRule: SeamRule;
|
|
4444
4446
|
projectRule: ProjectRule;
|
|
4445
4447
|
depthReadings?: Array<{ from: string; reading: import('./depthsheets.ts').DepthReading }>;
|
|
4448
|
+
/** Visible registered samples clamped within <1 source pixel of a crop. */
|
|
4449
|
+
depthClamps?: Array<{ part: string; samples: number }>;
|
|
4446
4450
|
/** Issue #492: head-canvas pixels filled by `config.assemble.head_face_underfill: true` when > 0. */
|
|
4447
4451
|
headFaceUnderfillPx?: number;
|
|
4448
4452
|
/** Issue #587: what each `exclude` entry took, in entry order; empty without one. */
|
|
@@ -5316,7 +5320,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5316
5320
|
let yielded = 0;
|
|
5317
5321
|
if (input.yieldRefused === true) {
|
|
5318
5322
|
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: [] };
|
|
5323
|
+
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
5324
|
const holders = new Map<string | null, number>();
|
|
5321
5325
|
for (let p = 0; p < W * H; p++) {
|
|
5322
5326
|
if (refused.data[p] !== 1) continue;
|
|
@@ -5334,6 +5338,38 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5334
5338
|
b.r.data[p * 4 + 3] = 0;
|
|
5335
5339
|
yielded++;
|
|
5336
5340
|
}
|
|
5341
|
+
if (yielded > 0) {
|
|
5342
|
+
// Connectivity reads the original part support, including the opaque
|
|
5343
|
+
// band outside the eroded projection core. Only non-core rim pixels
|
|
5344
|
+
// may additionally yield: traversing an opaque pixel never clears it.
|
|
5345
|
+
// Merge/ring replacements, kept samples and another piece's region
|
|
5346
|
+
// cannot connect a rim to this source's refused body.
|
|
5347
|
+
const seen = new Uint8Array(W * H), queue = new Int32Array(W * H);
|
|
5348
|
+
let end = 0;
|
|
5349
|
+
for (let p = 0; p < W * H; p++) if (mask.data[p] === 1) { seen[p] = 1; queue[end++] = p; }
|
|
5350
|
+
for (let at = 0; at < end; at++) {
|
|
5351
|
+
const p = queue[at], x = p % W, y = Math.floor(p / W);
|
|
5352
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
|
5353
|
+
const nx = x + dx, ny = y + dy;
|
|
5354
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H) continue;
|
|
5355
|
+
const q = ny * W + nx;
|
|
5356
|
+
if (seen[q] === 1) continue;
|
|
5357
|
+
seen[q] = 1;
|
|
5358
|
+
const alpha = b.r.data[q * 4 + 3];
|
|
5359
|
+
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;
|
|
5360
|
+
queue[end++] = q;
|
|
5361
|
+
if (alpha >= CORE_ALPHA || behindAt[q] < 0) continue;
|
|
5362
|
+
const holder = holderAt[q] < 0 ? null : stackNames[holderAt[q]];
|
|
5363
|
+
holders.set(holder, (holders.get(holder) ?? 0) + 1);
|
|
5364
|
+
if (holderAt[q] !== orderIdx) reading.originalGlobalHolderWouldSkip++;
|
|
5365
|
+
mask.data[q] = 1;
|
|
5366
|
+
if (b.st.takenMask.data[q] === 1) reading.yieldedTaken++;
|
|
5367
|
+
b.r.data[q * 4 + 3] = 0;
|
|
5368
|
+
reading.yieldedRim++;
|
|
5369
|
+
yielded++;
|
|
5370
|
+
}
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5337
5373
|
reading.yielded = yielded;
|
|
5338
5374
|
reading.originalHolders = [...holders].map(([part, px]) => ({ part, px }));
|
|
5339
5375
|
yields.set(name, { mask, reading });
|
|
@@ -5363,6 +5399,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5363
5399
|
});
|
|
5364
5400
|
|
|
5365
5401
|
const planned: Array<PlacedPart | null> = plan.map(() => null);
|
|
5402
|
+
const depthClamps: NonNullable<AssembleResult['depthClamps']> = [];
|
|
5366
5403
|
const cutPlaced: Array<PlacedPart | null> = cuts.map(() => null);
|
|
5367
5404
|
// A cut's piece records the plan part it was cut from (issue #230): parts.json is in draw order, so a piece drawn
|
|
5368
5405
|
// "back" comes before its base, and nothing else in the file says which of the two is the piece.
|
|
@@ -5438,6 +5475,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5438
5475
|
...((limb?.cap?.image_px ?? 0) > 0 || (limb?.cap?.lower_px ?? 0) > 0 ? { unsupported: `cap copies ${(limb?.cap?.image_px ?? 0) + (limb?.cap?.lower_px ?? 0)} pixels from a separate cap image whose scalar grid is undeclared` } : {}),
|
|
5439
5476
|
});
|
|
5440
5477
|
if (scalar !== undefined) record.depth = scalar.reference;
|
|
5478
|
+
if (scalar !== undefined && scalar.clampedSamples > 0) depthClamps.push({ part: name, samples: scalar.clampedSamples });
|
|
5441
5479
|
set({ record, image, ...(scalar === undefined ? {} : { depthImage: scalar.image }) });
|
|
5442
5480
|
});
|
|
5443
5481
|
refuseIfAny(empty);
|
|
@@ -5494,6 +5532,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5494
5532
|
},
|
|
5495
5533
|
images: placed,
|
|
5496
5534
|
...(registeredDepth.size === 0 ? {} : { depthReadings: ([['full', depthFullInput], ...(single ? [] : [['head', depthHeadInput]])] as Array<['full' | 'head', LayerSet]>).flatMap(([run, set]) => set.layers.flatMap(layer => layer.depthSheet === undefined ? [] : [{ from: `${run}:${layer.name}`, reading: layer.depthSheet.reading }])) }),
|
|
5535
|
+
...(depthClamps.length === 0 ? {} : { depthClamps }),
|
|
5497
5536
|
recomposite: can,
|
|
5498
5537
|
errorMap: recompositeErrorMap(can, srcr, placed, inside),
|
|
5499
5538
|
figure: inside === null ? null : figureImage(inside, W, H),
|
|
@@ -5622,7 +5661,7 @@ export function driftLine(d: DriftBodies): string {
|
|
|
5622
5661
|
`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
5662
|
"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
5663
|
(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'
|
|
5664
|
+
? '; 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
5665
|
: `; yielded ${d.yieldedPx} px to top-most opaque behind holders matching the painting (assemble.yield_refused: true; alpha only)`)
|
|
5627
5666
|
);
|
|
5628
5667
|
}
|
|
@@ -6324,7 +6363,7 @@ export function stageFields(
|
|
|
6324
6363
|
armSplit?: ArmSplit;
|
|
6325
6364
|
keySkeleton?: ControlSkeleton;
|
|
6326
6365
|
headFaceUnderfill?: boolean;
|
|
6327
|
-
/** `assemble.yield_refused`, default false; alpha-only
|
|
6366
|
+
/** `assemble.yield_refused`, default false; alpha-only transfer of refused cores and their connected non-core rims. */
|
|
6328
6367
|
yieldRefused?: boolean;
|
|
6329
6368
|
/** Plan parts whose own samples survive source re-take beneath soft front cover. */
|
|
6330
6369
|
keepUnderOccluders?: readonly string[];
|
package/src/build.ts
CHANGED
|
@@ -384,7 +384,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
const altProblems: Problem[] = [];
|
|
387
|
-
const altBuilt: Array<{ record: AlternatePartsRecord; images: Array<{ name: string; image: Raster }>; excluded: ExcludedCount[]; islands: IslandCount[]; splits: SplitCount[]; capSplits: SplitCount[] }> = [];
|
|
387
|
+
const altBuilt: Array<{ record: AlternatePartsRecord; images: Array<{ name: string; image: Raster }>; excluded: ExcludedCount[]; islands: IslandCount[]; splits: SplitCount[]; capSplits: SplitCount[]; depthClamps: AssembleResult['depthClamps'] }> = [];
|
|
388
388
|
const baseSlots = result.parts.parts.map((p) => p.name);
|
|
389
389
|
// Issue #664: each limb segment part with a declared front slot -> the part its front slot follows.
|
|
390
390
|
const frontAfter = new Map<string, string>();
|
|
@@ -724,6 +724,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
724
724
|
islands: altRes.islands,
|
|
725
725
|
splits: altRes.splits,
|
|
726
726
|
capSplits: altRes.capSplits,
|
|
727
|
+
depthClamps: altRes.depthClamps,
|
|
727
728
|
});
|
|
728
729
|
}
|
|
729
730
|
const emittedAssets = [
|
|
@@ -762,6 +763,8 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
762
763
|
if (depthImage !== undefined && record.depth !== undefined) writePng(join(outs.partsDir, record.depth.image), depthImage);
|
|
763
764
|
}
|
|
764
765
|
for (const row of result.depthReadings ?? []) log(` depth reading ${row.from}: ${row.reading.relation}; ordering median ${row.reading.orderingMedian}, source-sheet median ${row.reading.sheetMedian} (half-level quantization; reading only, polarity declared)`);
|
|
766
|
+
for (const row of result.depthClamps ?? []) log(` depth edge clamp ${row.part}: ${row.samples} visible samples less than 1 source pixel outside the crop; edge texels retained`);
|
|
767
|
+
for (const b of altBuilt) for (const row of b.depthClamps ?? []) log(` set "${b.record.name}" depth edge clamp ${row.part}: ${row.samples} visible samples less than 1 source pixel outside the crop; edge texels retained`);
|
|
765
768
|
for (const b of altBuilt) {
|
|
766
769
|
for (const { name, image } of b.images) writePng(join(outs.partsDir, `${name}.png`), image);
|
|
767
770
|
}
|
|
@@ -1631,6 +1634,8 @@ export interface CheckStageInput {
|
|
|
1631
1634
|
seamPairs?: boolean;
|
|
1632
1635
|
/** `check --keep-stills` (issue #209): leave the setup-pose still and its `frames.json` under {@link STILL_DIR}. */
|
|
1633
1636
|
keepStills?: boolean;
|
|
1637
|
+
/** Skip the extra extremes renders and remove stale extremes outputs (issue #829). */
|
|
1638
|
+
noExtremes?: boolean;
|
|
1634
1639
|
/** `check --set <name>` (issue #454): measure one declared alternate set's setup-pose seam and recomposite figures. */
|
|
1635
1640
|
set?: string;
|
|
1636
1641
|
/** `check --all-sets` (issue #454): measure every declared alternate set's setup-pose seam and recomposite figures. */
|
|
@@ -1873,11 +1878,12 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1873
1878
|
const entryLine = versionLines.find((l) => l.startsWith('entry:'));
|
|
1874
1879
|
log(` ${rigcIdentity(bin, v)}${entryLine === undefined ? '' : `; ${entryLine}`}`);
|
|
1875
1880
|
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);
|
|
1881
|
+
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
1882
|
if (r.figures.environment !== undefined) log(` ${environmentLine(r.figures.environment)}`);
|
|
1878
1883
|
log(` gate rig-play (rigc ${packedBuildLabel(mode)}), verbatim:`);
|
|
1879
1884
|
for (const l of r.gateHtml) log(l);
|
|
1880
1885
|
for (const l of packLines(r)) log(` ${l}`);
|
|
1886
|
+
for (const line of r.extremes ?? []) log(` ${line}`);
|
|
1881
1887
|
const fig = r.figures;
|
|
1882
1888
|
if (r.idle === null) log(` loop: SKIP — ${fig.skipped?.loop ?? ''}`);
|
|
1883
1889
|
else log(` ${loopCheckLine(r.idle, fig.loop_max_diff, fig.loop_physics?.length ?? 0)}`);
|
|
@@ -2213,6 +2219,8 @@ export interface BuildInput {
|
|
|
2213
2219
|
seamPairs?: boolean;
|
|
2214
2220
|
/** `--keep-stills`, forwarded to the check as `check` takes it (issue #257): the setup-pose still under `check/setup_still/`; absent, nothing moves. */
|
|
2215
2221
|
keepStills?: boolean;
|
|
2222
|
+
/** Skip the extra extremes renders and remove stale extremes outputs (issue #829). */
|
|
2223
|
+
noExtremes?: boolean;
|
|
2216
2224
|
/** `--alt`, forwarded to the assemble stage as `assemble` takes it (issue #454). */
|
|
2217
2225
|
alts?: readonly AlternateCliInput[];
|
|
2218
2226
|
/** `--set <name>`, forwarded to the check as `check` takes it (issue #454). */
|
|
@@ -2249,6 +2257,7 @@ export const CHECK_FLAGS_IN_BUILD: Readonly<Record<string, CheckFlagInBuild>> =
|
|
|
2249
2257
|
'--all-sets': { as: 'forwarded' },
|
|
2250
2258
|
'--seam-pairs': { as: 'forwarded' },
|
|
2251
2259
|
'--keep-stills': { as: 'forwarded' },
|
|
2260
|
+
'--no-extremes': { as: 'forwarded' },
|
|
2252
2261
|
'--environment': { as: 'forwarded' },
|
|
2253
2262
|
'--wind-band': { as: 'forwarded' },
|
|
2254
2263
|
};
|
|
@@ -2414,7 +2423,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
2414
2423
|
|
|
2415
2424
|
let report: CheckReport;
|
|
2416
2425
|
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'));
|
|
2426
|
+
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
2427
|
} catch (err) {
|
|
2419
2428
|
return refused('check', err);
|
|
2420
2429
|
}
|
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
|
|
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[];
|
package/src/depthsheets.ts
CHANGED
|
@@ -207,6 +207,10 @@ export interface DepthTransport {
|
|
|
207
207
|
}
|
|
208
208
|
/** Resample on the FINAL unpadded part grid. Visible synthesized/unsupported
|
|
209
209
|
* material refuses; transparent cells and later PAD4 use explicit edge clamp.
|
|
210
|
+
* Registered art filtering can reach less than one source pixel beyond its
|
|
211
|
+
* crop. Keep that edge texel; a visible sample >=1px outside still refuses.
|
|
212
|
+
* clampedSamples counts visible samples outside the half-open crop, once per
|
|
213
|
+
* output pixel even at a corner. It is a reading, not emitted provenance.
|
|
210
214
|
* No geometry, art pixels, source field or calibration are changed here.
|
|
211
215
|
*/
|
|
212
216
|
export function transportPartDepth(
|
|
@@ -214,13 +218,14 @@ export function transportPartDepth(
|
|
|
214
218
|
art: Raster,
|
|
215
219
|
registered: ReadonlyMap<string, RegisteredDepth>,
|
|
216
220
|
transport: DepthTransport,
|
|
217
|
-
): { reference: PartDepthReference; image: Raster } | undefined {
|
|
221
|
+
): { reference: PartDepthReference; image: Raster; clampedSamples: number } | undefined {
|
|
218
222
|
const keys = [part.from, ...transport.extend];
|
|
219
223
|
const initial = keys.map(k => registered.get(k)).find((s): s is RegisteredDepth => s !== undefined);
|
|
220
224
|
if (initial === undefined) return undefined;
|
|
221
225
|
const fail = (detail: string): never => { throw new PartsError([{ code: 'DEPTH_PART_SUPPORT', object: `part "${part.name}"`, detail }]); };
|
|
222
226
|
if (transport.unsupported !== undefined) fail(`${transport.unsupported}; an exact source-bound scalar transport is required`);
|
|
223
227
|
const image = newRaster(part.w, part.h), used = new Map<string, RegisteredDepth>();
|
|
228
|
+
let clampedSamples = 0;
|
|
224
229
|
for (let y = 0; y < part.h; y++) for (let x = 0; x < part.w; x++) {
|
|
225
230
|
const rx = part.x + x, ry = part.y + y, pos = ry * transport.width + rx;
|
|
226
231
|
const visible = art.data[(y * part.w + x) * 4 + 3] > 0;
|
|
@@ -233,12 +238,13 @@ export function transportPartDepth(
|
|
|
233
238
|
const sourceKey = source === undefined ? keys.find(k => registered.get(k) === initial) as string : key;
|
|
234
239
|
const run = sourceKey.startsWith('head:') ? 'head' : 'full';
|
|
235
240
|
const m = transport.map(run), px = (rx - m.tx) / m.sx - input.layer.left + 0.5, py = (ry - m.ty) / m.sx - input.layer.top + 0.5;
|
|
236
|
-
if (visible && (px
|
|
241
|
+
if (visible && (px <= -1 || py <= -1 || px >= input.sheet.pixels.width + 1 || py >= input.sheet.pixels.height + 1)) fail(`rig pixel (${rx},${ry}) maps to source (${px},${py}) outside ${input.sheet.pixels.width}x${input.sheet.pixels.height}; source art filtering/generated material has no declared depth there`);
|
|
242
|
+
if (visible && (px < 0 || py < 0 || px >= input.sheet.pixels.width || py >= input.sheet.pixels.height)) clampedSamples++;
|
|
237
243
|
const level = sampleDepthLevel(input.sheet.pixels, px, py);
|
|
238
244
|
image.data.set([level, level, level, 255], (y * part.w + x) * 4);
|
|
239
245
|
if (visible && source !== undefined) used.set(key, source);
|
|
240
246
|
}
|
|
241
|
-
return { image, reference: {
|
|
247
|
+
return { image, clampedSamples, reference: {
|
|
242
248
|
image: `${part.name}_depth.png`, near: initial.near, zScale: initial.zScale,
|
|
243
249
|
sources: [...new Map([...used].flatMap(([from, s]) => s.sheet.sources ?? [{ from, image: s.sheet.file, sha256: s.sheet.sha256, crop: [s.layer.left, s.layer.top, s.layer.right, s.layer.bottom] as [number, number, number, number], reading: s.sheet.reading }]).map(s => [`${s.from}:${s.sha256}`, s])).values()],
|
|
244
250
|
} };
|
package/src/neckdraft.ts
CHANGED
|
@@ -51,6 +51,7 @@ export function draftIndependentNeck(input: { config: CharacterConfig; source: R
|
|
|
51
51
|
body.roles[part] = 'unit';
|
|
52
52
|
// Stage fields now see the new role before any color/fringe ownership operation.
|
|
53
53
|
const assembled = assemble({ source: input.source, full: input.full, head: input.head, ...stageFields(cfg), under: input.under, caps: input.caps, seamRule: 'near-white', projectRule: 'core', depth: cfg.depth, depthBase: input.base });
|
|
54
|
+
for (const row of assembled.depthClamps ?? []) notes.push(`depth edge clamp ${row.part}: ${row.samples} visible samples less than 1 source pixel outside the crop; edge texels retained`);
|
|
54
55
|
const rec = assembled.parts.parts.find(p => p.name === part)!;
|
|
55
56
|
const image = assembled.images.find(p => p.record.name === part)!.image;
|
|
56
57
|
const own = assembled.images.find(p => p.record.name === part)!;
|