rig-parts 1.77.0 → 1.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/cli.ts +14 -7
- package/docs/AUTHORING.md +47 -21
- package/docs/MIGRATION.md +3 -3
- package/docs/STABILITY.md +14 -11
- package/package.json +2 -2
- package/skills/rig-parts/SKILL.md +1 -1
- package/src/assemble.ts +141 -8
- package/src/build.ts +11 -3
- package/src/check.ts +60 -2
- package/src/config.ts +22 -4
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,8 @@ 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
|
+
| `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"]`. |
|
|
124
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) |
|
|
125
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 |
|
|
126
127
|
| `meshes.<part>` | rig | **proposed**, then corrected. Generated modes below; `explicit` transfers an accepted mesh (see “An accepted mesh”). Exactly one mode is allowed. `grid` (the lattice: cell size, px — what `propose` writes: by default one spacing for the figure, round(H / 37) rig px with H the height of every part's box together, a part whose tenth of its short side is already coarser keeping that, and under `propose --grid-rule part` the reference's tenth of the part's short side clamped to 5..36, issue #265), `contour` (the outline mode, authored; the next row and "A contour mesh" below) or `auto` (the automatic mode, authored; "An automatic mesh" below) — two, or none, is `CONFIG_MESH_MODE`. In distance-generated modes: `r` (added to every distance before weighting: `w = 1/(d + r)²`), `segments` (a chain name, a bone name with a `tip`, or `[bone, [x0,y0], [x1,y1]]`), and, **authored**, optional, `exponent` (issue #161): a number above 0 that replaces the 2 in that rule, `w = 1/(d + r)^exponent` — a larger one hands each vertex more fully to its nearest bone; absent is 2 and changes no byte, a value that is not a number above 0 is `CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`, and a declared one is echoed as `exponent` in the mesh's `mesh_report.json` row; the exponent is the distance rule's, so declared beside `rule: "heat"` it is refused (`CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`). The segment list is the one authored decision about a layer: which bones may pull it. The slot's bone is the first segment's. Optional `rule` (issue #161): `"distance"` — the rule above, and what an absent `rule` means (every byte is the same) — or `"heat"`, bone heat over the part's own silhouette, in every mode: the lattice's art (alpha above 8, less any island a declared `stray` left out) with holes filled, or for `contour` and `auto` the silhouette their outline is traced from (that mode's art — alpha above 8 for `contour`, alpha 1 and above for `auto` — less any island a declared `stray` left out, holes filled, grown by its `margin`); each bound bone's sources are the silhouette pixels whose centre lies within 0.75 px of its segment (a pixel two bones reach goes to the nearer, a tie to the first listed), held at 1 for that bone and 0 for the others; the field is solved by row-major Gauss–Seidel over-relaxation to a sweep change under 1e-7 within 20000 sweeps; a vertex reads the nearest silhouette pixel and takes the same cap and floor (and `regions`) as the distance rule; `r` is not read. The mesh's `mesh_report.json` row gains `heat` (the constants, each bone's sources and sweeps, the residual). Any other value is `CONFIG_FIELD_TYPE`; a bound bone with no source pixel is `RIG_HEAT_BONE_SOURCE`, an island no bone has a source in `RIG_HEAT_ISLAND_SOURCE`, a solve past its sweep bound `RIG_HEAT_CONVERGED` — none is weighted 0. The rule is chosen per mesh, so `RIG_HEAT_BONE_SOURCE` names three remedies: move the segment onto the art, take the bone out of `segments`, or keep this part on the distance rule — the one to take when the bone anchors the part from outside its art (a torso bone under a neck), since the first moves a bone another part placed and the second drops the anchor (issue #303). On a lattice, `RIG_HEAT_ISLAND_SOURCE` on an island that is dust — at most 4 px and at most 0.1 % of the part's art px, `mesh-draft`'s line — names `meshes.<part>.stray` and the figure that leaves it out, with the value declared; an island above that line keeps the segment remedy, a real piece being a real authoring question. In every mode the mesh's `mesh_report.json` row's `bones` names the bones the emitted model binds for the attachment — the sorted set its weights in the written `rig.json` name, so a `turn` twin (`turn_<part>_<bone>`) or a stopped link's `<link>_d` where those rebind it, never the segment bone it stands for — the same list in every mode, `remesh --delta`'s included (issue #536) Optional `gather: false` keeps an exclusively bound non-segment limb rider at its authored draw position (issue #680); its binding and mesh geometry are unchanged. Remove the key for normal gathering; a non-rider is refused as `RIG_GATHER_RIDER`. |
|
|
@@ -2941,8 +2942,9 @@ world units. Other proposal modes (`--keypoints`, `--compare`, `--grid-rule`,
|
|
|
2941
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 |
|
|
2942
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 |
|
|
2943
2944
|
|
|
2944
|
-
Build through the normal gate, then use `rigc render --extremes`
|
|
2945
|
-
`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
|
|
2946
2948
|
inspect neck boundaries at native and 4x nearest-neighbour scale for tears or
|
|
2947
2949
|
ghosts. A green build alone does not certify the source art.
|
|
2948
2950
|
|
|
@@ -3169,7 +3171,7 @@ full loader before its assemble stage writes anything.
|
|
|
3169
3171
|
its own, or refused by name (issue #257; `CHECK_FLAGS_IN_BUILD` in
|
|
3170
3172
|
`src/build.ts` is the one list, and the `cli` suite holds it to `check`'s own
|
|
3171
3173
|
refusal): `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs` and
|
|
3172
|
-
`--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
|
|
3173
3175
|
`check --seam-pairs` writes on the same rig; `--out` is `build`'s (the check
|
|
3174
3176
|
writes under `<out>/check`); `--rig` and `--parts` are refused, because the
|
|
3175
3177
|
check reads the rig and the parts the build wrote (`<out>/rig`, `<out>`);
|
|
@@ -3182,6 +3184,22 @@ full loader before its assemble stage writes anything.
|
|
|
3182
3184
|
rig px, so every origin reads 0, each chain link's tip, length and direction its
|
|
3183
3185
|
config's, and every `<bone>_ctl` the rig stage inserted is the bone between a
|
|
3184
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.
|
|
3185
3203
|
10. **What a scene requires (optional).** When the motion a scene asks of the rig
|
|
3186
3204
|
is known — a hand that stays on a cup, a sleeve that takes half an arm's turn, a
|
|
3187
3205
|
joint that must stay inside a range — write it into a
|
|
@@ -3376,19 +3394,8 @@ from and what it measured), `rig.json` and `motion.json`, and from `build` the p
|
|
|
3376
3394
|
atlas — `check/build/skeleton.json`, `.atlas` and `.png` — that a Spine runtime
|
|
3377
3395
|
loads, with `check/contact.png` and the idle frames to look at.
|
|
3378
3396
|
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
For a tear, ghost, smudge, outline or seam, first select an appropriate existing
|
|
3382
|
-
See-through layer/run or alternate (for a collar baked into the neck, see
|
|
3383
|
-
[A neck that moves on its own](#a-neck-that-moves-on-its-own)); then measure a tighter See-through resolution,
|
|
3384
|
-
crop or settings if needed. Next measure the declared depth/turn role and, for a
|
|
3385
|
-
`drift:` line with a matching opaque behind holder, opt in with
|
|
3386
|
-
`assemble.yield_refused: true`. This config step only clears the original refused
|
|
3387
|
-
front alpha under the conditions in the field table; it does not paint a repair.
|
|
3388
|
-
Rebuild and inspect setup and the animation extremes. A polygon cut separates a
|
|
3389
|
-
piece that must move or change draw order independently; it is not the next way
|
|
3390
|
-
to conceal a visual defect. Measure those source/config alternatives before
|
|
3391
|
-
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).
|
|
3392
3399
|
|
|
3393
3400
|
### The correction loop (issue #359)
|
|
3394
3401
|
|
|
@@ -3486,11 +3493,12 @@ existing hardlinks are refused before artifact output; the ordinary mode names
|
|
|
3486
3493
|
| --- | --- | --- | --- |
|
|
3487
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*) |
|
|
3488
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 |
|
|
3489
|
-
| `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) |
|
|
3490
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 |
|
|
3491
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 |
|
|
3492
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` |
|
|
3493
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/` |
|
|
3494
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` |
|
|
3495
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 |
|
|
3496
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) |
|
|
@@ -3719,6 +3727,8 @@ and its declaration, and is green on both (`RG19`).
|
|
|
3719
3727
|
|
|
3720
3728
|
### Source before scissors
|
|
3721
3729
|
|
|
3730
|
+
<a id="source-before-scissors-issue-819"></a>
|
|
3731
|
+
|
|
3722
3732
|
**Scissors are for structure, never for repair.** `cuts`, `patches` and
|
|
3723
3733
|
`author:<label>` layers separate something that must move independently and
|
|
3724
3734
|
that See-through has no layer for: a joint, an accessory, or a rear piece that
|
|
@@ -3728,8 +3738,24 @@ problem. Follow this repair ladder, recording what each step measured:
|
|
|
3728
3738
|
1. Choose another See-through layer or run (`full` versus `head`, another tag,
|
|
3729
3739
|
or `alternates`). For a collar baked into the neck, follow
|
|
3730
3740
|
[A neck that moves on its own](#a-neck-that-moves-on-its-own).
|
|
3731
|
-
2. Re-run See-through with a tighter resolution or
|
|
3732
|
-
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.
|
|
3733
3759
|
|
|
3734
3760
|
A cut is appropriate for an independently moving accessory still inside its
|
|
3735
3761
|
parent's layer, two independently moving limbs in one layer, or a rear piece
|
package/docs/MIGRATION.md
CHANGED
|
@@ -54,7 +54,7 @@ installed the former upstream name beside this package installs nothing more:
|
|
|
54
54
|
|
|
55
55
|
### Scoped core dependency
|
|
56
56
|
|
|
57
|
-
The current dependency is `@rig-c/core` at `^2.
|
|
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
|
|
|
@@ -126,7 +129,7 @@ part name, `[i]` an entry of a list):
|
|
|
126
129
|
- **generation.control.skeleton**, the pose file it names when it is no built-in — required: `width`, `height`, `points`
|
|
127
130
|
- **seethrough** — required: `resolution`, `steps`, `seed`, `offload`; optional: `head_box`, `arm_split`
|
|
128
131
|
- **seethrough.arm_split** — required: `order`, and one of `x` or `line` (issue #439)
|
|
129
|
-
- **assemble** — required: `rig_scale`, `plan` (from plain `assemble` on); optional: `extend_below_crop`, `patches`, `cuts`, `alternates`, `head_face_underfill`, `yield_refused`
|
|
132
|
+
- **assemble** — required: `rig_scale`, `plan` (from plain `assemble` on); optional: `extend_below_crop`, `patches`, `cuts`, `alternates`, `head_face_underfill`, `yield_refused`, `keep_under_occluders`
|
|
130
133
|
- **assemble.alternates[i]** — required: `name`; optional: `source`, `full`, `head`, `parts`, `hides`, `head_box`, `cuts`, `skeleton`, `anchor`, `base_anchor`, `drawn`, `tips`, `front`, `layers`, `focus` (issues #454, #465, #543, #652, #664, #666, #719, #780). `focus` is an authored finite `[x, y]` in painting pixels, within its canvas (edges included), refused as CONFIG_SET_FOCUS before assemble output when invalid; only when present, `parts.json` records it with focus_source_size and `sets.json` emits it in setup-world coordinates through the actual rig resize and `src/coords.ts`. Absent focus emits no field or inferred value; changing/removing it requires reassembly
|
|
131
134
|
- **assemble.extend_below_crop[i]** — required: `part`, `run`, `tag`
|
|
132
135
|
- **assemble.patches[i]** — required: `name`, `box`, `alpha`, `draw`
|
|
@@ -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.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"ag-psd": "^31.0.2",
|
|
68
|
-
"@rig-c/core": "^2.
|
|
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,8 +241,10 @@ 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
|
+
/** Plan parts whose own samples survive source re-take beneath soft front cover. */
|
|
247
|
+
keepUnderOccluders?: readonly string[];
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
/**
|
|
@@ -360,8 +362,10 @@ export interface AssembleInput {
|
|
|
360
362
|
keySkeleton?: ControlSkeleton;
|
|
361
363
|
/** `assemble.head_face_underfill` (issue #492): composite `full:face` under `head:face` on the head canvas to fill transparent bare-skin holes. */
|
|
362
364
|
headFaceUnderfill?: boolean;
|
|
363
|
-
/** `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. */
|
|
364
366
|
yieldRefused?: boolean;
|
|
367
|
+
/** Plan parts whose own samples survive source re-take beneath soft front cover. */
|
|
368
|
+
keepUnderOccluders?: readonly string[];
|
|
365
369
|
/**
|
|
366
370
|
* Issue #587: rig pixels taken out of named parts once the cuts and the limbs are made, before the fringe pass and
|
|
367
371
|
* every figure a part's record holds — each entry clears `part`'s pixels (alpha, colour and region) where `mask` is
|
|
@@ -4384,8 +4388,12 @@ export interface DriftYieldReading {
|
|
|
4384
4388
|
from: string;
|
|
4385
4389
|
refusedCandidates: number;
|
|
4386
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;
|
|
4387
4393
|
/** Yielded refusals also accepted by projection closing; removed from the written projected count. */
|
|
4388
4394
|
yieldedTaken: number;
|
|
4395
|
+
/** Original refused pixels explicitly kept from their own layer (#825). */
|
|
4396
|
+
preserved?: number;
|
|
4389
4397
|
skipped: { merge: number; ring: number; notOpaque: number; noMatchingBehind: number };
|
|
4390
4398
|
/** Informational only: no fixed global-holder eligibility guard is used. */
|
|
4391
4399
|
originalGlobalHolderWouldSkip: number;
|
|
@@ -4393,7 +4401,24 @@ export interface DriftYieldReading {
|
|
|
4393
4401
|
finalHolders: Array<{ part: string | null; px: number }>;
|
|
4394
4402
|
}
|
|
4395
4403
|
|
|
4404
|
+
export interface SoftOcclusionReading {
|
|
4405
|
+
part: string;
|
|
4406
|
+
/** Config accepts original plan names, not derived cut/limb output names. */
|
|
4407
|
+
sourcePart?: string;
|
|
4408
|
+
from: string;
|
|
4409
|
+
changed: number;
|
|
4410
|
+
kept: number;
|
|
4411
|
+
front: Array<{ part: string; px: number }>;
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
/** Informational source/layer disagreement under actual soft front cover; no gate. */
|
|
4415
|
+
export function softOcclusionLine(r: SoftOcclusionReading): string {
|
|
4416
|
+
return `drift: "${r.part}" source re-take changed ${r.changed} own-layer px of ${r.from} under soft front cover (${r.front.map(q => `"${q.part}" ${q.px} px`).join(', ')}; alpha > 0 and < ${CORE_ALPHA}); ${r.kept > 0 ? `kept ${r.kept} original own-layer px (assemble.keep_under_occluders)` : `inspect its own amodal layer and motion extremes; to retain those samples ${r.sourcePart === undefined ? `resolve ${r.from} to its original assemble.plan part before setting assemble.keep_under_occluders` : `set assemble.keep_under_occluders: ["${r.sourcePart}"]`}`}; informational, not a drift-refusal or uncovered-hole gate`;
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4396
4419
|
export interface AssembleResult {
|
|
4420
|
+
/** Report-only source re-take under actual soft front support (#825). */
|
|
4421
|
+
softOcclusion?: SoftOcclusionReading[];
|
|
4397
4422
|
parts: PartsFile;
|
|
4398
4423
|
/** In plan order, each cropped to its record's box. */
|
|
4399
4424
|
images: PlacedPart[];
|
|
@@ -4662,6 +4687,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4662
4687
|
const patchIds = patches.map((_, i) => editId(`patches[${i}]`));
|
|
4663
4688
|
const layerWriters = new Map<string, Int32Array>();
|
|
4664
4689
|
const valueBaselines = new Map<string, ValueBaseline | null>();
|
|
4690
|
+
const cleanedSoftCover = new Map<string, Mask>();
|
|
4665
4691
|
// Extend-only rasters are still projected/merged, but are never emitted as own sources.
|
|
4666
4692
|
const readKeys = new Set(sourcePlan.map(([, run, tag]) => `${run}:${tag}`));
|
|
4667
4693
|
const rig = new Map<string, Raster>();
|
|
@@ -4735,6 +4761,17 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4735
4761
|
}
|
|
4736
4762
|
// Keep only samples that could equal a final opaque pixel; full raw and
|
|
4737
4763
|
// prepared normalization temporaries end with this layer's callback.
|
|
4764
|
+
// A low-alpha source fringe removed by cleanGhosts still contributed to
|
|
4765
|
+
// the painting. Retain its source footprint only for explicit opt-in;
|
|
4766
|
+
// final piece regions below still decide which side of a cut owns it.
|
|
4767
|
+
if ((input.keepUnderOccluders?.length ?? 0) > 0 && raw !== null && preparedWriter === 0) {
|
|
4768
|
+
let footprint: Mask | null = null;
|
|
4769
|
+
for (let p = 0; p < W * H; p++) if (raw.data[p * 4 + 3] > 0 && raw.data[p * 4 + 3] <= OPAQUE_ALPHA_ABOVE && rigLayers[i].data[p * 4 + 3] === 0) {
|
|
4770
|
+
footprint ??= newMask(W, H);
|
|
4771
|
+
footprint.data[p] = 1;
|
|
4772
|
+
}
|
|
4773
|
+
if (footprint !== null) cleanedSoftCover.set(key, footprint);
|
|
4774
|
+
}
|
|
4738
4775
|
valueBaselines.set(key, raw === null ? null : compactValueBaseline(raw));
|
|
4739
4776
|
return ids;
|
|
4740
4777
|
});
|
|
@@ -5170,7 +5207,66 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5170
5207
|
return { record: whole, image: b.r };
|
|
5171
5208
|
});
|
|
5172
5209
|
const featheredInOrder: Array<Mask | null> = order.map((d) => ('patch' in d ? null : (pieceAt(d).b.feathered ?? null)));
|
|
5173
|
-
|
|
5210
|
+
// #825: decide ownership on the final draw stack, after merges, cuts,
|
|
5211
|
+
// limb splits and exclusions. Restore only original own-layer samples:
|
|
5212
|
+
// no merge/ring, under-fill, cap, patch or authored replacement is borrowed.
|
|
5213
|
+
const wanted = new Set(input.keepUnderOccluders ?? []);
|
|
5214
|
+
refuseIfAny([...wanted].filter(name => !input.plan.some(([n]) => n === name)).map(name => ({ code: 'CONFIG_NAME_RESOLVES', object: 'config.assemble.keep_under_occluders', detail: `names "${name}"; a source part in assemble.plan is required` })));
|
|
5215
|
+
const sourceKeys = new Set(input.plan.filter(([name]) => wanted.has(name)).map(([, run, tag]) => `${run}:${tag}`));
|
|
5216
|
+
const keptRetake: Array<Mask | null> = order.map(() => null);
|
|
5217
|
+
const softOcclusion: SoftOcclusionReading[] = [];
|
|
5218
|
+
const keepInOrder = order.map((d, i): Mask | null => {
|
|
5219
|
+
const limb = 'limb' in d ? limbKeep[d.limb] : null;
|
|
5220
|
+
if ('patch' in d) return limb;
|
|
5221
|
+
const b = pieceAt(d).b, baseline = valueBaselines.get(b.key);
|
|
5222
|
+
if (baseline === undefined || baseline === null) return limb;
|
|
5223
|
+
const enabled = sourceKeys.has(b.key);
|
|
5224
|
+
const mask = enabled ? newMask(W, H) : limb;
|
|
5225
|
+
if (enabled && limb !== null) mask!.data.set(limb.data);
|
|
5226
|
+
const restored = enabled ? newMask(W, H) : null;
|
|
5227
|
+
const sourcePart = input.plan.find(([, run, tag]) => `${run}:${tag}` === b.key)?.[0];
|
|
5228
|
+
const reading: SoftOcclusionReading = { part: pieceAt(d).name, ...(sourcePart === undefined ? {} : { sourcePart }), from: b.key, changed: 0, kept: 0, front: [] };
|
|
5229
|
+
const holders = new Map<string, number>();
|
|
5230
|
+
for (let by = 0; by < baseline.image.height; by++) for (let bx = 0; bx < baseline.image.width; bx++) {
|
|
5231
|
+
const p = (baseline.y + by) * W + baseline.x + bx;
|
|
5232
|
+
if (b.trace.from[p] >= 0 || b.trace.ring.data[p] !== 0 || (b.region !== null && b.region.data[p] === 0)) continue;
|
|
5233
|
+
const x = p % W - baseline.x, y = Math.floor(p / W) - baseline.y;
|
|
5234
|
+
if (x < 0 || y < 0 || x >= baseline.image.width || y >= baseline.image.height) continue;
|
|
5235
|
+
const q = (y * baseline.image.width + x) * 4, raw = baseline.image.data;
|
|
5236
|
+
if (raw[q + 3] < CORE_ALPHA || b.r.data[p * 4 + 3] !== raw[q + 3]) continue;
|
|
5237
|
+
const writer = b.writers?.[p] ?? 0;
|
|
5238
|
+
if (!enabled && (writer !== AW.sourceProjection || rgbaEqual(b.r.data, p * 4, raw, q))) continue;
|
|
5239
|
+
if (writer !== AW.sourceProjection && !rgbaEqual(b.r.data, p * 4, raw, q)) continue;
|
|
5240
|
+
for (let j = stack.length - 1; j > i; j--) {
|
|
5241
|
+
let alpha = stack[j].image.data[p * 4 + 3];
|
|
5242
|
+
const frontSlot = order[j];
|
|
5243
|
+
if (alpha === 0 && enabled && !('patch' in frontSlot)) {
|
|
5244
|
+
const cover = pieceAt(frontSlot).b;
|
|
5245
|
+
if ((cover.region === null || cover.region.data[p] === 1) && cover.trace.from[p] < 0 && cover.trace.ring.data[p] === 0 && cleanedSoftCover.get(cover.key)?.data[p] === 1) alpha = OPAQUE_ALPHA_ABOVE;
|
|
5246
|
+
}
|
|
5247
|
+
if (alpha === 0) continue;
|
|
5248
|
+
if (alpha < CORE_ALPHA) {
|
|
5249
|
+
const different = !rgbaEqual(b.r.data, p * 4, raw, q);
|
|
5250
|
+
if (different) reading.changed++;
|
|
5251
|
+
if (different || enabled) holders.set(stack[j].record.name, (holders.get(stack[j].record.name) ?? 0) + 1);
|
|
5252
|
+
if (enabled) {
|
|
5253
|
+
b.r.data.set(raw.subarray(q, q + 4), p * 4);
|
|
5254
|
+
mask!.data[p] = 1;
|
|
5255
|
+
restored!.data[p] = 1;
|
|
5256
|
+
reading.kept++;
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
break;
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
keptRetake[i] = restored;
|
|
5263
|
+
if (reading.changed > 0 || reading.kept > 0) {
|
|
5264
|
+
reading.front = [...holders].map(([part, px]) => ({ part, px }));
|
|
5265
|
+
softOcclusion.push(reading);
|
|
5266
|
+
}
|
|
5267
|
+
return mask;
|
|
5268
|
+
});
|
|
5269
|
+
const retakeByName = new Map(order.flatMap((d, i) => 'patch' in d ? [] : [[pieceAt(d).name, keptRetake[i]] as const]));
|
|
5174
5270
|
const patchWriters = patches.map((_, i) => new Int32Array(W * H).fill(patchIds[i]));
|
|
5175
5271
|
const writersInOrder = order.map(d => 'patch' in d ? patchWriters[d.patch] : pieceAt(d).b.writers as Int32Array);
|
|
5176
5272
|
const pushedBy = pushBackFringe(stack, srcr, seamRule, seamRule === 'silhouette' ? silhouette : null, featheredInOrder, keepInOrder, input.turnMovingPair ?? null, (i, p) => { writersInOrder[i][p] = AW.fringeRestore; });
|
|
@@ -5222,11 +5318,12 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5222
5318
|
let yielded = 0;
|
|
5223
5319
|
if (input.yieldRefused === true) {
|
|
5224
5320
|
const mask = newMask(W, H);
|
|
5225
|
-
const reading: DriftYieldReading = { part: name, from: b.key, refusedCandidates: 0, yielded: 0, yieldedTaken: 0, skipped: { merge: 0, ring: 0, notOpaque: 0, noMatchingBehind: 0 }, originalGlobalHolderWouldSkip: 0, originalHolders: [], finalHolders: [] };
|
|
5321
|
+
const reading: DriftYieldReading = { part: name, from: b.key, refusedCandidates: 0, yielded: 0, yieldedRim: 0, yieldedTaken: 0, skipped: { merge: 0, ring: 0, notOpaque: 0, noMatchingBehind: 0 }, originalGlobalHolderWouldSkip: 0, originalHolders: [], finalHolders: [] };
|
|
5226
5322
|
const holders = new Map<string | null, number>();
|
|
5227
5323
|
for (let p = 0; p < W * H; p++) {
|
|
5228
5324
|
if (refused.data[p] !== 1) continue;
|
|
5229
5325
|
reading.refusedCandidates++;
|
|
5326
|
+
if (keptRetake[orderIdx]?.data[p] === 1) { reading.preserved = (reading.preserved ?? 0) + 1; continue; }
|
|
5230
5327
|
if (b.trace.from[p] >= 0) { reading.skipped.merge++; continue; }
|
|
5231
5328
|
if (b.trace.ring.data[p] !== 0) { reading.skipped.ring++; continue; }
|
|
5232
5329
|
if (b.r.data[p * 4 + 3] < CORE_ALPHA) { reading.skipped.notOpaque++; continue; }
|
|
@@ -5239,6 +5336,38 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5239
5336
|
b.r.data[p * 4 + 3] = 0;
|
|
5240
5337
|
yielded++;
|
|
5241
5338
|
}
|
|
5339
|
+
if (yielded > 0) {
|
|
5340
|
+
// Connectivity reads the original part support, including the opaque
|
|
5341
|
+
// band outside the eroded projection core. Only non-core rim pixels
|
|
5342
|
+
// may additionally yield: traversing an opaque pixel never clears it.
|
|
5343
|
+
// Merge/ring replacements, kept samples and another piece's region
|
|
5344
|
+
// cannot connect a rim to this source's refused body.
|
|
5345
|
+
const seen = new Uint8Array(W * H), queue = new Int32Array(W * H);
|
|
5346
|
+
let end = 0;
|
|
5347
|
+
for (let p = 0; p < W * H; p++) if (mask.data[p] === 1) { seen[p] = 1; queue[end++] = p; }
|
|
5348
|
+
for (let at = 0; at < end; at++) {
|
|
5349
|
+
const p = queue[at], x = p % W, y = Math.floor(p / W);
|
|
5350
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
|
|
5351
|
+
const nx = x + dx, ny = y + dy;
|
|
5352
|
+
if (nx < 0 || nx >= W || ny < 0 || ny >= H) continue;
|
|
5353
|
+
const q = ny * W + nx;
|
|
5354
|
+
if (seen[q] === 1) continue;
|
|
5355
|
+
seen[q] = 1;
|
|
5356
|
+
const alpha = b.r.data[q * 4 + 3];
|
|
5357
|
+
if (alpha === 0 || (b.region !== null && b.region.data[q] === 0) || keptRetake[orderIdx]?.data[q] === 1 || b.trace.from[q] >= 0 || b.trace.ring.data[q] !== 0) continue;
|
|
5358
|
+
queue[end++] = q;
|
|
5359
|
+
if (alpha >= CORE_ALPHA || behindAt[q] < 0) continue;
|
|
5360
|
+
const holder = holderAt[q] < 0 ? null : stackNames[holderAt[q]];
|
|
5361
|
+
holders.set(holder, (holders.get(holder) ?? 0) + 1);
|
|
5362
|
+
if (holderAt[q] !== orderIdx) reading.originalGlobalHolderWouldSkip++;
|
|
5363
|
+
mask.data[q] = 1;
|
|
5364
|
+
if (b.st.takenMask.data[q] === 1) reading.yieldedTaken++;
|
|
5365
|
+
b.r.data[q * 4 + 3] = 0;
|
|
5366
|
+
reading.yieldedRim++;
|
|
5367
|
+
yielded++;
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5242
5371
|
reading.yielded = yielded;
|
|
5243
5372
|
reading.originalHolders = [...holders].map(([part, px]) => ({ part, px }));
|
|
5244
5373
|
yields.set(name, { mask, reading });
|
|
@@ -5293,7 +5422,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5293
5422
|
const own = e < 0 ? st : (stats.get(extendKeys[e]) as ProjectionStats);
|
|
5294
5423
|
// A pixel outside the piece's region is a base pixel filled from a cut's under image (issue #193): occluded, never visible.
|
|
5295
5424
|
if (r.data[p * 4 + 3] > OPAQUE_ALPHA_ABOVE && own.clear.data[p] === 1 && (region === null || region.data[p] === 1)) visible.data[p] = 1;
|
|
5296
|
-
if (yields.get(name)?.mask.data[p] !== 1 && trace.ring.data[p] === 0 && own.takenMask.data[p] === 1 && (region === null || region.data[p] === 1)) projected.data[p] = 1;
|
|
5425
|
+
if (retakeByName.get(name)?.data[p] !== 1 && yields.get(name)?.mask.data[p] !== 1 && trace.ring.data[p] === 0 && own.takenMask.data[p] === 1 && (region === null || region.data[p] === 1)) projected.data[p] = 1;
|
|
5297
5426
|
}
|
|
5298
5427
|
const { x0, y0, x1, y1, opaque } = measureBox(r);
|
|
5299
5428
|
if (opaque === 0) {
|
|
@@ -5318,7 +5447,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5318
5447
|
visible_px: vc.visible_px,
|
|
5319
5448
|
occluded_px: vc.occluded_px,
|
|
5320
5449
|
projected_core_px: region === null ? st.core : count(st.coreMask, region),
|
|
5321
|
-
source_px_taken: (region === null ? st.taken : count(st.takenMask, region)) - (yields.get(name)?.reading.yieldedTaken ?? 0),
|
|
5450
|
+
source_px_taken: (region === null ? st.taken : count(st.takenMask, region)) - (yields.get(name)?.reading.yieldedTaken ?? 0) - (retakeByName.get(name) == null ? 0 : count(st.takenMask, retakeByName.get(name)!)),
|
|
5322
5451
|
visible_not_projected_px: vc.visible_not_projected_px,
|
|
5323
5452
|
refused_drift_px: region === null ? st.refused : count(st.refusedMask, region),
|
|
5324
5453
|
merged_px: merged,
|
|
@@ -5383,6 +5512,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5383
5512
|
bodyMinPx(frame),
|
|
5384
5513
|
);
|
|
5385
5514
|
return {
|
|
5515
|
+
...(softOcclusion.length === 0 ? {} : { softOcclusion }),
|
|
5386
5516
|
parts: {
|
|
5387
5517
|
rig_size: [W, H],
|
|
5388
5518
|
scale_rig_per_source: frame.S,
|
|
@@ -5526,7 +5656,7 @@ export function driftLine(d: DriftBodies): string {
|
|
|
5526
5656
|
`by the plan's draw order ${d.holders.map((h) => `"${h.part}" holds ${h.px} px`).join(', ')} of it (the top-most part at alpha >= ${CORE_ALPHA})${frontClause}${behindClause}; ` +
|
|
5527
5657
|
"informational: no gate reads this. Swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do): swap, rebuild and read this line again; if it moves, removing it needs different input layers. At rest, the seam pass repaints the holder's pixels that pass its rule from the painting" +
|
|
5528
5658
|
(d.yieldedPx === undefined
|
|
5529
|
-
? '; set assemble.yield_refused: true to yield original refused front pixels only where the top-most opaque behind holder matches the painting, before considering a cut'
|
|
5659
|
+
? '; set assemble.yield_refused: true to yield original refused front pixels and their connected non-core rims only where the top-most opaque behind holder matches the painting, before considering a cut'
|
|
5530
5660
|
: `; yielded ${d.yieldedPx} px to top-most opaque behind holders matching the painting (assemble.yield_refused: true; alpha only)`)
|
|
5531
5661
|
);
|
|
5532
5662
|
}
|
|
@@ -6228,8 +6358,10 @@ export function stageFields(
|
|
|
6228
6358
|
armSplit?: ArmSplit;
|
|
6229
6359
|
keySkeleton?: ControlSkeleton;
|
|
6230
6360
|
headFaceUnderfill?: boolean;
|
|
6231
|
-
/** `assemble.yield_refused`, default false; alpha-only
|
|
6361
|
+
/** `assemble.yield_refused`, default false; alpha-only transfer of refused cores and their connected non-core rims. */
|
|
6232
6362
|
yieldRefused?: boolean;
|
|
6363
|
+
/** Plan parts whose own samples survive source re-take beneath soft front cover. */
|
|
6364
|
+
keepUnderOccluders?: readonly string[];
|
|
6233
6365
|
turnMovingPair?: (a: string, b: string) => boolean;
|
|
6234
6366
|
} {
|
|
6235
6367
|
const problems: Problem[] = [];
|
|
@@ -6251,6 +6383,7 @@ export function stageFields(
|
|
|
6251
6383
|
...(keySkeleton === undefined ? {} : { keySkeleton }),
|
|
6252
6384
|
...(cfg.assemble.head_face_underfill === undefined ? {} : { headFaceUnderfill: cfg.assemble.head_face_underfill }),
|
|
6253
6385
|
...(cfg.assemble.yield_refused === undefined ? {} : { yieldRefused: cfg.assemble.yield_refused }),
|
|
6386
|
+
...(cfg.assemble.keep_under_occluders === undefined ? {} : { keepUnderOccluders: cfg.assemble.keep_under_occluders }),
|
|
6254
6387
|
...(turnMovingPair === null ? {} : { turnMovingPair }),
|
|
6255
6388
|
rigScale: cfg.assemble.rig_scale,
|
|
6256
6389
|
plan: cfg.assemble.plan,
|
package/src/build.ts
CHANGED
|
@@ -49,7 +49,7 @@ import { createHash } from 'node:crypto';
|
|
|
49
49
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
50
50
|
import { dirname, join, resolve } from 'node:path';
|
|
51
51
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
52
|
-
import { assemble, type AssembleResult, cutLines, type ExcludedCount, excludedLines, setExclusions, islandLines, setIslandSources, type IslandCount, hiddenLines, SINGLE_RUN_FACE_TAGS, singleRunHeadBox, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, measureRecomposite, type ProjectRule, recomposite, recompositeRecord, scenePhrase, type SeamRule, sourceInRig, stageFields, uncoveredLine } from './assemble.ts';
|
|
52
|
+
import { assemble, type AssembleResult, cutLines, type ExcludedCount, excludedLines, setExclusions, islandLines, setIslandSources, type IslandCount, hiddenLines, SINGLE_RUN_FACE_TAGS, singleRunHeadBox, softOcclusionLine, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, measureRecomposite, type ProjectRule, recomposite, recompositeRecord, scenePhrase, type SeamRule, sourceInRig, stageFields, uncoveredLine } from './assemble.ts';
|
|
53
53
|
import { allocationClause, type Reducer, stageBClause, unboundedClause } from './automesh.ts';
|
|
54
54
|
import { residualClause } from './autoenvelope.ts';
|
|
55
55
|
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, prepareComparison, type ReplayCandidate, runComparison, runPreparedComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
@@ -646,6 +646,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
646
646
|
capOwnership: input.setCapOwnership ?? 'target',
|
|
647
647
|
full: altRuns.full,
|
|
648
648
|
head: altRuns.head,
|
|
649
|
+
...(fields.keepUnderOccluders === undefined ? {} : { keepUnderOccluders: fields.keepUnderOccluders.filter(name => altPlanNames.has(name)) }),
|
|
649
650
|
resolution: fields.resolution,
|
|
650
651
|
headBox: altHeadBox,
|
|
651
652
|
rigScale: fields.rigScale,
|
|
@@ -811,6 +812,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
811
812
|
if (result.figures.uncoveredErrorPx > 0) log(holeKindsLine(result.holeKinds, scene));
|
|
812
813
|
for (const u of result.uncovered) log(uncoveredLine(u, scene));
|
|
813
814
|
for (const d of result.drift) log(driftLine(d));
|
|
815
|
+
for (const d of result.softOcclusion ?? []) log(softOcclusionLine(d));
|
|
814
816
|
// Issue #588: per subset set, one line per limb part cut from a layer of more than one island, then (issue #587) one
|
|
815
817
|
// per limb part and base part over it that took a pixel. None otherwise.
|
|
816
818
|
// Issue #609: then one per limb part cut from a layer of one island that also holds a limb the set does not carry.
|
|
@@ -1629,6 +1631,8 @@ export interface CheckStageInput {
|
|
|
1629
1631
|
seamPairs?: boolean;
|
|
1630
1632
|
/** `check --keep-stills` (issue #209): leave the setup-pose still and its `frames.json` under {@link STILL_DIR}. */
|
|
1631
1633
|
keepStills?: boolean;
|
|
1634
|
+
/** Skip the extra extremes renders and remove stale extremes outputs (issue #829). */
|
|
1635
|
+
noExtremes?: boolean;
|
|
1632
1636
|
/** `check --set <name>` (issue #454): measure one declared alternate set's setup-pose seam and recomposite figures. */
|
|
1633
1637
|
set?: string;
|
|
1634
1638
|
/** `check --all-sets` (issue #454): measure every declared alternate set's setup-pose seam and recomposite figures. */
|
|
@@ -1871,11 +1875,12 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1871
1875
|
const entryLine = versionLines.find((l) => l.startsWith('entry:'));
|
|
1872
1876
|
log(` ${rigcIdentity(bin, v)}${entryLine === undefined ? '' : `; ${entryLine}`}`);
|
|
1873
1877
|
const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
|
|
1874
|
-
const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true, input.set, input.allSets === true, input.environment, input.requirementsAs, input.config, input.windBand);
|
|
1878
|
+
const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true, input.set, input.allSets === true, input.environment, input.requirementsAs, input.config, input.windBand, input.noExtremes === true);
|
|
1875
1879
|
if (r.figures.environment !== undefined) log(` ${environmentLine(r.figures.environment)}`);
|
|
1876
1880
|
log(` gate rig-play (rigc ${packedBuildLabel(mode)}), verbatim:`);
|
|
1877
1881
|
for (const l of r.gateHtml) log(l);
|
|
1878
1882
|
for (const l of packLines(r)) log(` ${l}`);
|
|
1883
|
+
for (const line of r.extremes ?? []) log(` ${line}`);
|
|
1879
1884
|
const fig = r.figures;
|
|
1880
1885
|
if (r.idle === null) log(` loop: SKIP — ${fig.skipped?.loop ?? ''}`);
|
|
1881
1886
|
else log(` ${loopCheckLine(r.idle, fig.loop_max_diff, fig.loop_physics?.length ?? 0)}`);
|
|
@@ -2211,6 +2216,8 @@ export interface BuildInput {
|
|
|
2211
2216
|
seamPairs?: boolean;
|
|
2212
2217
|
/** `--keep-stills`, forwarded to the check as `check` takes it (issue #257): the setup-pose still under `check/setup_still/`; absent, nothing moves. */
|
|
2213
2218
|
keepStills?: boolean;
|
|
2219
|
+
/** Skip the extra extremes renders and remove stale extremes outputs (issue #829). */
|
|
2220
|
+
noExtremes?: boolean;
|
|
2214
2221
|
/** `--alt`, forwarded to the assemble stage as `assemble` takes it (issue #454). */
|
|
2215
2222
|
alts?: readonly AlternateCliInput[];
|
|
2216
2223
|
/** `--set <name>`, forwarded to the check as `check` takes it (issue #454). */
|
|
@@ -2247,6 +2254,7 @@ export const CHECK_FLAGS_IN_BUILD: Readonly<Record<string, CheckFlagInBuild>> =
|
|
|
2247
2254
|
'--all-sets': { as: 'forwarded' },
|
|
2248
2255
|
'--seam-pairs': { as: 'forwarded' },
|
|
2249
2256
|
'--keep-stills': { as: 'forwarded' },
|
|
2257
|
+
'--no-extremes': { as: 'forwarded' },
|
|
2250
2258
|
'--environment': { as: 'forwarded' },
|
|
2251
2259
|
'--wind-band': { as: 'forwarded' },
|
|
2252
2260
|
};
|
|
@@ -2412,7 +2420,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
2412
2420
|
|
|
2413
2421
|
let report: CheckReport;
|
|
2414
2422
|
try {
|
|
2415
|
-
report = stages.check({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(req === null ? {} : { requirements: req.read, requirementsAs: req.recorded }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true, ...(input.set === undefined ? {} : { set: input.set }), allSets: input.allSets === true, ...(input.environment === undefined ? {} : { environment: input.environment }), ...(input.windBand === undefined ? {} : { windBand: input.windBand }), ...(configRecord === null ? {} : { config: configRecord }) }, run.check, run.checkBin, prefixed('check'));
|
|
2423
|
+
report = stages.check({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(req === null ? {} : { requirements: req.read, requirementsAs: req.recorded }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true, noExtremes: input.noExtremes === true, ...(input.set === undefined ? {} : { set: input.set }), allSets: input.allSets === true, ...(input.environment === undefined ? {} : { environment: input.environment }), ...(input.windBand === undefined ? {} : { windBand: input.windBand }), ...(configRecord === null ? {} : { config: configRecord }) }, run.check, run.checkBin, prefixed('check'));
|
|
2416
2424
|
} catch (err) {
|
|
2417
2425
|
return refused('check', err);
|
|
2418
2426
|
}
|
package/src/check.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { baseAnchorMoves, type AnchorRegion } from './baseanchor.ts';
|
|
|
36
36
|
* | `idle_frames/` | `rigc render --animation idle --fps 12 --max 640 --geometry`: `frames.json` + `idle/f*.png` + `idle/geometry.json` (each mesh's skinned vertices, which `TEXTURE_STRETCH` reads, every attachment's posed triangles, which `TIP_OVER_ROOT` and both halves of `STILL_REGIONS_DARK` read since issues #118 and #123, and every bone's world transform, which the face half reads its head bone from) |
|
|
37
37
|
* | `contact.png` | rigc's own contact sheet of that render, copied out |
|
|
38
38
|
* | `motion_heat.png` | frame 0 in grey with each pixel's largest change across the idle in red |
|
|
39
|
+
* | `extremes/<anim>/extremes.png`, `extremes.json` | core min/max/diff observations at 12 fps, scale 1; static geometry writes a named SKIP and JSON only; `--no-extremes` removes stale diagnostics and runs no extra render |
|
|
39
40
|
* | `check.json` | the figures below and `PASS` |
|
|
40
41
|
* | `requirements/` | under `--requirements` only (issue #93): `as-declared/<animation>/`, and per follow `released/<name>/` and `full/<name>/` — rigc renders with `--geometry` ({@link measureRequirements}) |
|
|
41
42
|
*
|
|
@@ -2960,6 +2961,8 @@ export function requirementsNotPass(fig: Readonly<Record<string, unknown>>): str
|
|
|
2960
2961
|
}
|
|
2961
2962
|
|
|
2962
2963
|
export interface CheckReport {
|
|
2964
|
+
/** Core EXTREMES summaries and named SKIPs; diagnostics, never check.json or a bar. */
|
|
2965
|
+
extremes?: string[];
|
|
2963
2966
|
figures: CheckFigures;
|
|
2964
2967
|
gateHtml: string[];
|
|
2965
2968
|
pack: PackLine[];
|
|
@@ -5740,6 +5743,56 @@ export function windBandLines(line: WindBandLine, plant: WindBandPlant = {}): st
|
|
|
5740
5743
|
return [summary, ...breaking];
|
|
5741
5744
|
}
|
|
5742
5745
|
|
|
5746
|
+
/**
|
|
5747
|
+
* Issue #829: observations only. The fitted world box belongs to rigc; changing
|
|
5748
|
+
* its grid to one pixel per world unit makes the panels native, without a
|
|
5749
|
+
* second framing implementation. Each animation is rendered separately to
|
|
5750
|
+
* release its sampled poses before the next. Only the two observation files
|
|
5751
|
+
* survive; the ordinary frames and contact sheets are scratch.
|
|
5752
|
+
*/
|
|
5753
|
+
function renderExtremes(build: string, out: string, animations: readonly string[], frames: unknown, render: RigcRunner): string[] {
|
|
5754
|
+
if (animations.length === 0) return ['EXTREMES SKIP: CHECK_EXTREMES_NO_ANIMATIONS — no animation in motion.json'];
|
|
5755
|
+
const scratch = join(out, '_extremes');
|
|
5756
|
+
const dest = join(out, 'extremes');
|
|
5757
|
+
try {
|
|
5758
|
+
if (!isRecord(frames) || !isRecord(frames.viewport)) throw new Error('the existing render has no fitted viewport');
|
|
5759
|
+
const vp = frames.viewport;
|
|
5760
|
+
for (const key of ['x', 'y', 'width', 'height']) {
|
|
5761
|
+
const value = vp[key];
|
|
5762
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || ((key === 'width' || key === 'height') && value <= 0)) throw new Error(`fitted viewport.${key} is ${JSON.stringify(value)}`);
|
|
5763
|
+
}
|
|
5764
|
+
mkdirSync(scratch, { recursive: true });
|
|
5765
|
+
const grid = join(scratch, 'native.json');
|
|
5766
|
+
writeFileSync(grid, JSON.stringify({ spec: 'rigc-frames/1', viewport: { ...vp, scale: 1, pixelWidth: Math.max(1, Math.round(vp.width as number)), pixelHeight: Math.max(1, Math.round(vp.height as number)) } }));
|
|
5767
|
+
const lines: string[] = [];
|
|
5768
|
+
for (const animation of animations) {
|
|
5769
|
+
const rendered = join(scratch, 'render');
|
|
5770
|
+
const call = render(['render', '--candidate', build, '--animation', animation, '--fps', String(IDLE_FPS), '--viewport-from', grid, '--extremes', '--out', rendered]);
|
|
5771
|
+
if (call.status !== 0) throw new Error(`render --extremes ${JSON.stringify(animation)} exited ${call.status}: ${call.out.trim()}${rigcRanClause(call)}`);
|
|
5772
|
+
const summaries = call.out.split('\n').map(line => line.trim().replace(/^\.\.\s+/, '')).filter(line => line.startsWith('EXTREMES '));
|
|
5773
|
+
if (summaries.length === 0) throw new Error(`render --extremes ${JSON.stringify(animation)} printed no EXTREMES summary`);
|
|
5774
|
+
// The sidecar owns directory naming, including core's escaping of names.
|
|
5775
|
+
const sidecar = JSON.parse(readFileSync(join(rendered, 'frames.json'), 'utf8')) as { sets: Array<{ animation: string; dir: string }> };
|
|
5776
|
+
const set = sidecar.sets.find(row => row.animation === animation);
|
|
5777
|
+
if (set === undefined) throw new Error(`render --extremes has no set for ${JSON.stringify(animation)}`);
|
|
5778
|
+
const source = join(rendered, set.dir), target = join(dest, set.dir);
|
|
5779
|
+
mkdirSync(target, { recursive: true });
|
|
5780
|
+
copyFileSync(join(source, 'extremes.json'), join(target, 'extremes.json'));
|
|
5781
|
+
if (existsSync(join(source, 'extremes.png'))) copyFileSync(join(source, 'extremes.png'), join(target, 'extremes.png'));
|
|
5782
|
+
lines.push(...summaries);
|
|
5783
|
+
rmSync(rendered, { recursive: true, force: true });
|
|
5784
|
+
}
|
|
5785
|
+
return lines;
|
|
5786
|
+
} catch (err) {
|
|
5787
|
+
// Diagnostics never add a measured bar or change the gate's verdict. An
|
|
5788
|
+
// incomplete observation is explicitly unavailable, not a partial sheet set.
|
|
5789
|
+
rmSync(dest, { recursive: true, force: true });
|
|
5790
|
+
return [`EXTREMES SKIP: CHECK_EXTREMES_RENDER_FAILED — ${err instanceof Error ? err.message : String(err)}`];
|
|
5791
|
+
} finally {
|
|
5792
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
|
|
5743
5796
|
/**
|
|
5744
5797
|
* Run the whole check. Refuses (throws a PartsError) when the inputs are
|
|
5745
5798
|
* unreadable or a rigc step that everything after it depends on is red — the
|
|
@@ -5766,7 +5819,7 @@ export function windBandLines(line: WindBandLine, plant: WindBandPlant = {}): st
|
|
|
5766
5819
|
* MEASURABLE is a problem. Without it nothing here reads, writes or prints
|
|
5767
5820
|
* anything it did not before.
|
|
5768
5821
|
*/
|
|
5769
|
-
export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome?: string, mode: PackMode = DEFAULT_PACK_MODE, source?: string, requirements?: string, seamPairsAsked = false, keepStills = false, set?: string, allSets = false, environment?: string, requirementsAs: string | undefined = requirements, config?: InputFileRecord, windBand?: string): CheckReport {
|
|
5822
|
+
export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome?: string, mode: PackMode = DEFAULT_PACK_MODE, source?: string, requirements?: string, seamPairsAsked = false, keepStills = false, set?: string, allSets = false, environment?: string, requirementsAs: string | undefined = requirements, config?: InputFileRecord, windBand?: string, noExtremes = false): CheckReport {
|
|
5770
5823
|
let inp: CheckInputs;
|
|
5771
5824
|
let reqFile: RequirementsFile | null = null;
|
|
5772
5825
|
let reqRecord: RequirementsFileRecord | null = null;
|
|
@@ -5851,7 +5904,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
5851
5904
|
const foldDir = join(out, FOLD_SCRATCH);
|
|
5852
5905
|
const seamMotionDir = join(out, MOTION_SEAM_SCRATCH);
|
|
5853
5906
|
const windBandDir = join(out, WIND_BAND_SCRATCH);
|
|
5854
|
-
for (const d of [buildDir, idleDir, stillDir, isoDir, loopDir, seamDir, keptDir, join(out, 'setup_still_auto'), foldDir, seamMotionDir, windBandDir]) rmSync(d, { recursive: true, force: true });
|
|
5907
|
+
for (const d of [join(out, 'extremes'), join(out, '_extremes'), buildDir, idleDir, stillDir, isoDir, loopDir, seamDir, keptDir, join(out, 'setup_still_auto'), foldDir, seamMotionDir, windBandDir]) rmSync(d, { recursive: true, force: true });
|
|
5855
5908
|
const written: string[] = [];
|
|
5856
5909
|
const write = (name: string, data: string): void => {
|
|
5857
5910
|
writeFileSync(join(out, name), data);
|
|
@@ -5917,6 +5970,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
5917
5970
|
rmSync(windBandDir, { recursive: true, force: true });
|
|
5918
5971
|
}
|
|
5919
5972
|
}
|
|
5973
|
+
let extremesFrames: unknown = null;
|
|
5920
5974
|
let folds: FoldsLine | null = null;
|
|
5921
5975
|
const foldProblems: Problem[] = [];
|
|
5922
5976
|
// Issue #348: the feet the level line reads, off parts.json and rig.json; none, and nothing below moves.
|
|
@@ -6046,6 +6100,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6046
6100
|
level = levelLine(feet, inp.rig, levelReadings, fps, stripped.physics.length, levelProblems);
|
|
6047
6101
|
}
|
|
6048
6102
|
}
|
|
6103
|
+
if (!noExtremes && idle === null) extremesFrames = readJson(join(stripped === null ? foldDir : join(foldDir, 'on'), 'frames.json'), []);
|
|
6049
6104
|
} finally {
|
|
6050
6105
|
rmSync(foldDir, { recursive: true, force: true });
|
|
6051
6106
|
}
|
|
@@ -6611,6 +6666,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6611
6666
|
const publishCheck = (): void => write('check.json', `${JSON.stringify(figures, null, 1)}\n`);
|
|
6612
6667
|
if (motionSeam.worlds === undefined) publishCheck();
|
|
6613
6668
|
else publishMotionSeamWorlds(out, seamMotionDir, motionSeam.worlds, publishCheck);
|
|
6669
|
+
const extremes = noExtremes ? [] : renderExtremes(buildDir, out, animationNames, idle === null ? extremesFrames : readJson(join(idleDir, 'frames.json'), []), measured);
|
|
6670
|
+
if (existsSync(join(out, 'extremes'))) written.push('extremes/');
|
|
6614
6671
|
const bars: CheckReport['bars'] = { measured: ['gate'], skipped: [] };
|
|
6615
6672
|
const tally = (bar: string, reason: string | null): void => {
|
|
6616
6673
|
if (reason === null) bars.measured.push(bar);
|
|
@@ -6636,6 +6693,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6636
6693
|
return {
|
|
6637
6694
|
figures,
|
|
6638
6695
|
gateHtml,
|
|
6696
|
+
extremes,
|
|
6639
6697
|
pack,
|
|
6640
6698
|
packOpaque,
|
|
6641
6699
|
idle: idle === null || loop === null ? null : { frames: idle.frames.length, fps: idle.fps, duration: idle.duration, lastIndex: idle.frames[idle.frames.length - 1].index, loopAt: { x: loop.x, y: loop.y } },
|
package/src/config.ts
CHANGED
|
@@ -307,8 +307,10 @@ 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
|
+
/** Named plan parts retain their own pixels beneath a drawn-in-front soft occluder. */
|
|
313
|
+
keep_under_occluders?: string[];
|
|
312
314
|
}
|
|
313
315
|
|
|
314
316
|
export interface SingleBone {
|
|
@@ -1614,7 +1616,7 @@ export interface EarlyConfig {
|
|
|
1614
1616
|
depth?: DepthConfig;
|
|
1615
1617
|
key: string;
|
|
1616
1618
|
seethrough: SeeThrough;
|
|
1617
|
-
assemble: { rig_scale: number; head_face_underfill?: boolean; yield_refused?: boolean };
|
|
1619
|
+
assemble: { rig_scale: number; head_face_underfill?: boolean; yield_refused?: boolean; keep_under_occluders?: string[] };
|
|
1618
1620
|
}
|
|
1619
1621
|
|
|
1620
1622
|
/** What plain `assemble` reads: the early config plus the plan `--propose-plan` printed. */
|
|
@@ -1683,12 +1685,13 @@ function parseEarlyConfigFrom(raw: Json, door: EarlyDoor, base: string | null):
|
|
|
1683
1685
|
if (door === 'layers') {
|
|
1684
1686
|
if ('seethrough' in t) checkSeeThrough(c, t.seethrough);
|
|
1685
1687
|
if ('assemble' in t) {
|
|
1686
|
-
const a = c.object('config.assemble', t.assemble, sectionRequired(door, 'assemble'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused']);
|
|
1688
|
+
const a = c.object('config.assemble', t.assemble, sectionRequired(door, 'assemble'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders']);
|
|
1687
1689
|
if (a !== null) {
|
|
1688
1690
|
if ('rig_scale' in a) c.number('config.assemble.rig_scale', a.rig_scale, 'positive');
|
|
1689
1691
|
if ('head_face_underfill' in a && typeof a.head_face_underfill !== 'boolean') {
|
|
1690
1692
|
c.fail('CONFIG_FIELD_TYPE', 'config.assemble.head_face_underfill', `is ${show(a.head_face_underfill)}; true or false is required`);
|
|
1691
1693
|
}
|
|
1694
|
+
checkKeepUnderOccluders(c, a, null);
|
|
1692
1695
|
if ('yield_refused' in a && typeof a.yield_refused !== 'boolean') {
|
|
1693
1696
|
c.fail('CONFIG_FIELD_TYPE', 'config.assemble.yield_refused', `is ${show(a.yield_refused)}; true or false is required`);
|
|
1694
1697
|
}
|
|
@@ -2396,9 +2399,23 @@ function checkRunTag(c: Check, path: string, run: Json, tag: Json): void {
|
|
|
2396
2399
|
* out of the generic presence check so that its absence says which command
|
|
2397
2400
|
* writes one — the step a config without a plan has most likely skipped.
|
|
2398
2401
|
*/
|
|
2402
|
+
/** Validate at the early door too; resolve against the plan once that door reads it. */
|
|
2403
|
+
function checkKeepUnderOccluders(c: Check, a: Record<string, Json>, plan: readonly string[] | null): void {
|
|
2404
|
+
if (!('keep_under_occluders' in a)) return;
|
|
2405
|
+
const at = 'config.assemble.keep_under_occluders';
|
|
2406
|
+
if (!c.array(at, a.keep_under_occluders, false)) return;
|
|
2407
|
+
const seen = new Set<string>();
|
|
2408
|
+
(a.keep_under_occluders as Json[]).forEach((name, i) => {
|
|
2409
|
+
if (!c.string(`${at}[${i}]`, name)) return;
|
|
2410
|
+
if (seen.has(name)) c.fail('CONFIG_PART_UNIQUE', `${at}[${i}]`, `names "${name}" again; each part is named once`);
|
|
2411
|
+
seen.add(name);
|
|
2412
|
+
if (plan !== null && !plan.includes(name)) c.fail('CONFIG_NAME_RESOLVES', `${at}[${i}]`, `names "${name}"; a part named in assemble.plan is required — name the source part before cuts or limb splitting`);
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2399
2416
|
function checkAssemble(c: Check, v: Json, door: 'assemble' | 'full'): { parts: string[]; patches: string[]; cuts: string[] } {
|
|
2400
2417
|
const required = sectionRequired(door, 'assemble');
|
|
2401
|
-
const a = c.object('config.assemble', v, required.filter((k) => k !== 'plan'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused']);
|
|
2418
|
+
const a = c.object('config.assemble', v, required.filter((k) => k !== 'plan'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders']);
|
|
2402
2419
|
if (a === null) return { parts: [], patches: [], cuts: [] };
|
|
2403
2420
|
if (required.includes('plan') && !('plan' in a)) {
|
|
2404
2421
|
c.fail('CONFIG_FIELD_PRESENT', 'config.assemble.plan', 'is absent and required; `assemble --propose-plan` prints one from the two runs — paste its plan and extend_below_crop into config.assemble');
|
|
@@ -2432,6 +2449,7 @@ function checkAssemble(c: Check, v: Json, door: 'assemble' | 'full'): { parts: s
|
|
|
2432
2449
|
parts.push(name);
|
|
2433
2450
|
});
|
|
2434
2451
|
}
|
|
2452
|
+
checkKeepUnderOccluders(c, a, parts);
|
|
2435
2453
|
if ('extend_below_crop' in a && c.array(`${p}.extend_below_crop`, a.extend_below_crop, false)) {
|
|
2436
2454
|
(a.extend_below_crop as Json[]).forEach((entry, i) => {
|
|
2437
2455
|
const at = `${p}.extend_below_crop[${i}]`;
|