rig-parts 1.64.0 → 1.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -272,7 +272,7 @@ Use the graph's node classes and the host's `/object_info` when checking the ada
272
272
  ```sh
273
273
  rig-parts inputs --source painting.png --config config.json --out inputs # st_input_full.png; the config needs assemble.rig_scale
274
274
  # See-through on st_input_full.png (external, or `rig-parts comfy seethrough`) -> layers/full
275
- rig-parts layers layers/full # every layer: box, opaque px, depth, plausibility figures; WARN lines
275
+ rig-parts layers layers/full # drawn(a>8)/opaque(a>=250) boxes, CORE body-group height in input-canvas px, WARNs
276
276
  rig-parts propose --head-box --full layers/full --canvas 1664x2432
277
277
  # -> seethrough.head_box into config.json
278
278
  rig-parts inputs --source painting.png --config config.json --out inputs # now st_input_head.png too
@@ -291,7 +291,8 @@ rig-parts propose --parts work/rig --source painting.png --out work
291
291
  # (limbs only with --keypoints, constraints only with --cloth, turn only with --full/--head;
292
292
  # a limbs block left out is refused next)
293
293
  rig-parts propose --parts work/rig --source painting.png --out work --from-config config.json
294
- # -> LINT lines; exit 1 while any remain
294
+ # -> LINT lines; exit 1 while any remain (takes --parts, --source, --out, --from-config and optional --keypoints [--person] only;
295
+ # proposes nothing, so --full, --head, --cloth, --write-config, --wrist-snap, --grid-rule, --only-missing and --compare are refused)
295
296
  rig-parts build --config config.json --source painting.png --full layers/full --head layers/head --out out --loop
296
297
  # -> read out/check/check.json; every FAIL line names what has to change
297
298
  ```
@@ -333,7 +334,7 @@ agent skill.
333
334
  | `inputs --source <png> --config <json> --out <dir>` | the two images See-through is fed: the painting on a white square, and the head box's crop once the config has one |
334
335
  | `comfy paint --config --out [--host]` | optional: generate the painting on a ComfyUI box from the config's `generation` block |
335
336
  | `comfy seethrough --image --out [--host]` | optional: run the ComfyUI See-through wrapper on one image and write the form `layers` reads |
336
- | `layers <dir \| layers.json \| file.psd>` | print every layer of a decomposition: draw order, name, tag group, box, size, opaque pixels, depth, and its translucent, background and area figures; a `WARN` line for a layer `--propose-plan` will leave out |
337
+ | `layers <dir \| layers.json \| file.psd>` | keep the existing columns (`opaque_px` still means alpha >8) and append source-alpha boxes `drawn(a>8)` / `opaque(a>=250)` with exclusive right/bottom; name absent boxes, report the existing body-group union height at **CORE_ALPHA (>=250)** in **input-canvas px**, and show its >8 union separately; no anatomical height, ownership/erosion or painting registration is inferred ([units and rig-scale guidance](docs/AUTHORING.md#4-the-command-order)) |
337
338
  | `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
338
339
  | `assemble --propose-plan … [--write-config <config>]` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule, naming in a note any full-run body no planned part covers, and naming every layer no row takes with why (empty, taken from the other run only, a plausibility rule; `nose`, never planned, with the row that lets `propose` read the head's pitch) |
339
340
  | `assemble --source --full --head --config --out [--seam] [--project]` | merge the two runs into rig-space parts, `parts.json`, the recomposite and its error map, and list the uncovered holes |
@@ -341,7 +342,7 @@ agent skill.
341
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 |
342
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)) |
343
344
  | `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) |
344
- | `propose … --from-config <config>` | draw and LINT the config's current bones (by the joints too, given `--keypoints`) |
345
+ | `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`) |
345
346
  | `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) |
346
347
  | `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after rig-c's gate is green; each mesh is in the mode its config entry names — `grid` (the lattice, what `propose` writes), `contour` (the traced outline with declared interior points) or `auto` (the contour mesh at alpha 1 and above, reduced and locally refined by rig-c's `reduceMesh` under the author's declared bounds, written only when its motion against the unreduced source on the idle passes the author's `motion` bounds through rig-c's `compareMeshesInMotion`; [AUTHORING §3](docs/AUTHORING.md)); every chain link is turned along its chain and carries its `length`, so a physics constraint added later has a lever, with every offset under it in the turned frame so nothing moves; `--idle-keys` says whether the idle's keys on mesh-driving bones go through `<bone>_ctl` parents (default) or stay on the bones with `invariants.idleDrivesMeshes` declared; the config's `constraints` go to rigc as written, and a two-bone ik over keyed links needs `--idle-keys direct` (on `rig` or `build`) |
347
348
  | `check --rig --out [--parts] [--source] [--requirements]` | build packed, gated under `rig-play`, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json`; on a rig spec with no `parts.json` or no `idle` (a merged rig, issue #77) the gate still runs and every line that needs the missing input says SKIP by name; `--source <painting.png>` adds `SETUP_POSE_VS_SOURCE`, the setup pose against the painting by assemble's recomposite figures; `--requirements <scene.json>` measures what a scene declares of the motion — a contact, a follow fraction, an aim, a joint range, a mesh's stretch, the seam between two parts over the motion — each against the author's own bar, from rig-c's posed frames, one line each, PASS, FAIL or NOT MEASURABLE (AUTHORING §7, *Declared requirements*) |
package/cli.ts CHANGED
@@ -28,7 +28,7 @@ import { GUIDE_DOC, type GuideAnswer, guideRule, guideSection } from './src/guid
28
28
  import { proposeHeadBox } from './src/headbox.ts';
29
29
  import { makeInputs } from './src/inputs.ts';
30
30
  import { idleKeysOf, isIdleKeys } from './src/rig.ts';
31
- import { depthSkipLine, exportLayers, figuresPhrase, hiddenLayerLine, implausibleRules, layerFigures, type LayerSet, layerSetDiff, pct, readLayers, readWrapperLayers, ruleSummary, templateLayerNames, templatePsdBytes, times } from './src/layers.ts';
31
+ import { bodyCoreBox, bodyOpaqueBox, CORE_ALPHA, depthSkipLine, exportLayers, figuresPhrase, hiddenLayerLine, implausibleRules, layerCoreBox, layerFigures, layerOpaqueBox, type LayerSet, layerSetDiff, type OpaqueBox, OPAQUE_ALPHA_ABOVE, pct, readLayers, readWrapperLayers, ruleSummary, templateLayerNames, templatePsdBytes, times } from './src/layers.ts';
32
32
  import { checkImageSize, choosePerson, KEYPOINTS_SPEC, loadKeypoints, type RigJoints, toRigJoints } from './src/keypoints.ts';
33
33
  import { basisLines, branchSqueezeLine, checkProposal, compare, compareLines, DEFAULT_GRID_RULE, draftTurn, drawLandmarks, FIGURE_GRID_DIVISOR, FRINGE_STEP_PX, GRID_RULES, type GridRule, HIP_MIN_FRACTION, type DraftLintFinding, lint, lintLine, type PartSet, PROPOSAL_CONFIG_SECTIONS, proposalAsConfig, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
34
34
  import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, legChainChoice, serializeBasis } from './src/diagnostics.ts';
@@ -79,6 +79,11 @@ usage:
79
79
  where something covers it: listed apart, never drawn, never a part. An
80
80
  input with no depth (a PSD, or depth_median null on every layer, whose
81
81
  manifest order is then the draw order) prints one SKIP depth line.
82
+ Trailing boxes name both source-alpha cuts: drawn(a>8), and opaque
83
+ (a>=250, CORE_ALPHA). The legacy opaque_px still counts alpha above 8.
84
+ Right/bottom are exclusive, in INPUT-CANVAS px; absent boxes are named.
85
+ The body-group union height uses alpha >=250, with the >8 union shown
86
+ separately: no cleanup, ownership, anatomical height or painting map.
82
87
  Refuses, by name, a missing file, an unknown tag, a PNG whose
83
88
  size is not its box, and anything else outside the input contract —
84
89
  never a WARN.
@@ -268,9 +273,10 @@ usage:
268
273
 
269
274
  rig-parts propose --parts <dir> --source <painting.png> --out <dir> --from-config <config.json>
270
275
  Draw the config's CURRENT bones instead (<out>/render/landmarks_config.png
271
- and _head) and LINT them (by the joints too, given --keypoints), with the
272
- coverage lines after them; no ${BASIS_FILE}, as nothing is proposed.
273
- Exits 1 when any LINT line is printed.
276
+ and _head) and LINT them (by the joints too, given --keypoints [--person]), with the
277
+ coverage lines after them; no ${BASIS_FILE}, as nothing is proposed (takes
278
+ --parts, --source, --out, --from-config and optional --keypoints [--person]
279
+ only). Exits 1 when any LINT line is printed.
274
280
 
275
281
  rig-parts propose … --write-config <config.json>
276
282
  Without --cloth, update an existing config with the whole proposal's
@@ -416,7 +422,8 @@ usage:
416
422
  rig-parts mesh-stats --config <config.json> --parts <dir>
417
423
  Print the vertices and triangles of every mesh the config states, as the
418
424
  rig stage would make them from the parts in --parts (the directory
419
- holding parts.json and parts/<name>.png), and their total, in seconds:
425
+ holding parts.json and parts/ — work/rig from assemble --out work, or
426
+ out from build --out out), and their total, in seconds:
420
427
  the rig stage's own mesh construction, without rigc, without assemble
421
428
  and without check (issue #566). One line per mesh in parts.json's order,
422
429
  "mesh-stats: <part> — <mode> <V> vertices, <T> triangles" (mode lattice,
@@ -523,6 +530,7 @@ usage:
523
530
  [--page-edges pot|free] [--pack-shape rect|polygon]
524
531
  [--requirements <file.json>] [--seam-pairs] [--keep-stills]
525
532
  [--set <name> | --all-sets] [--environment <schedule.json>]
533
+ [--wind-band <spec.json>]
526
534
  Build, gate, render and measure a rig through rig-c's CLI (the rigc at
527
535
  node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
528
536
  a rig spec, which rigc build compiles, not a compiled skeleton.json.
@@ -671,6 +679,14 @@ usage:
671
679
  response or no band is SKIP; no pair or animation gives no worlds.
672
680
  Nonphysics output is unchanged. In rig-play environment(null) returns
673
681
  to the default world, not still air (AUTHORING, Physics seam environments).
682
+ --wind-band <spec.json> sweeps steady wind [x, 0], gravity [0, 1] at t = 0
683
+ over [min, max] in step increments through rigc render --seam (spec
684
+ "rig-parts-wind-band/1"; every field required: spec, min, max, step, fps,
685
+ max_gap, max_ghost, pairs; refused by CHECK_WIND_BAND before anything is
686
+ written). Prints WIND_BAND: REPORTED with the steady-wind band and one
687
+ WIND_BAND: line per pair that breaks inside [min, max], and records
688
+ WIND_BAND in check.json. Steady wind only; it does not measure gusts or
689
+ transient overshoot (AUTHORING §6, Steady-wind seam band).
674
690
 
675
691
  rig-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
676
692
  Encode a frame set rigc render wrote (its --out directory, or the set
@@ -788,7 +804,7 @@ usage:
788
804
  [--pack-shape rect|polygon] [--loop] [--requirements <file.json>]
789
805
  [--idle-keys ctl|direct] [--lead-in <periods>] [--seam-pairs] [--keep-stills]
790
806
  [--alt <name>=<source>:<full>[:<head>]] [--set <name> | --all-sets]
791
- [--environment <schedule.json>]
807
+ [--environment <schedule.json>] [--wind-band <spec.json>]
792
808
  assemble, then rig, then check, in one process, each stage's own lines
793
809
  printed under [assemble], [rig] and [check]; the first stage that refuses
794
810
  stops the build with its own FAIL lines. The config must already carry
@@ -819,6 +835,9 @@ usage:
819
835
  --environment is forwarded to check (as for check); the schedule is read
820
836
  by rig-c's reader before assemble runs, so a malformed one is refused,
821
837
  CHECK_ENVIRONMENT, under [assemble] before anything is assembled.
838
+ --wind-band is forwarded to check (as for check); the spec is read before
839
+ assemble runs, so a malformed one is refused, CHECK_WIND_BAND, under
840
+ [assemble] before anything is assembled.
822
841
  --seam-pairs and --keep-stills are forwarded to check (as for check:
823
842
  the seam split by pair on a pass too, under check/seam/; the setup-pose
824
843
  still under check/setup_still/). check's --rig and --parts are refused:
@@ -981,10 +1000,29 @@ function printLayerTable(set: LayerSet): void {
981
1000
  const head = ['order', 'name', 'group', 'left,top', 'right,bottom', 'size', 'opaque_px', 'depth', 'translucent', 'background', 'area'];
982
1001
  const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
983
1002
  const line = (cells: string[]): string => ` ${cells.map((c, i) => c.padEnd(widths[i])).join(' ')}`.trimEnd();
984
- console.log(line(head));
985
- for (const r of rows) console.log(line(r));
1003
+ // Keep every legacy line byte-for-byte as a prefix; size the trailing fields separately.
1004
+ const legacy = [line(head), ...rows.map(line)];
1005
+ const legacyWidth = Math.max(...legacy.map(l => l.length));
1006
+ const drawn = `drawn(a>${OPAQUE_ALPHA_ABOVE})`;
1007
+ const opaque = `opaque(a>=${CORE_ALPHA})`;
1008
+ const boxes = set.layers.map(l => [layerOpaqueBox(l), layerCoreBox(l)] as const);
1009
+ const opaqueHead = [drawn, opaque].flatMap(label => [`${label}_left,top`, `${label}_right,bottom`, `${label}_size`]);
1010
+ const opaqueRows = boxes.map(pair => pair.flatMap(b => b === null ? ['none', 'none', 'none'] : [`${b.left},${b.top}`, `${b.right},${b.bottom}`, `${b.right - b.left}x${b.bottom - b.top}`]));
1011
+ const opaqueWidths = opaqueHead.map((h, i) => Math.max(h.length, ...opaqueRows.map(r => r[i].length)));
1012
+ for (const [i, cells] of [opaqueHead, ...opaqueRows].entries()) {
1013
+ console.log(`${legacy[i].padEnd(legacyWidth)} ${cells.map((c, j) => c.padEnd(opaqueWidths[j])).join(' ')}`.trimEnd());
1014
+ }
986
1015
  const painted = set.layers.filter((l) => l.opaquePx > 0).length;
987
1016
  console.log(` ${set.layers.length} layer(s): ${painted} with opaque pixels, ${set.layers.length - painted} with none`);
1017
+ console.log(` source-alpha boxes: ${drawn}, ${opaque}; legacy opaque_px remains alpha > ${OPAQUE_ALPHA_ABOVE}; INPUT-CANVAS px; right/bottom exclusive`);
1018
+ for (const [i, [drawnBox, coreBox]] of boxes.entries()) {
1019
+ if (drawnBox === null) console.log(` layer "${set.layers[i].name}": no ${drawn} box (no pixels with alpha > ${OPAQUE_ALPHA_ABOVE})`);
1020
+ if (coreBox === null) console.log(` layer "${set.layers[i].name}": no ${opaque} box (no pixels with alpha >= ${CORE_ALPHA})${drawnBox === null ? '' : `; ${drawn} box is present`}`);
1021
+ }
1022
+ const bodyBounds = (body: OpaqueBox | null): string => body === null ? 'none; height unavailable INPUT-CANVAS px' : `${body.left},${body.top} to ${body.right},${body.bottom}; size ${body.right - body.left}x${body.bottom - body.top}; height ${body.bottom - body.top} INPUT-CANVAS px`;
1023
+ console.log(` body-group union [${opaque}]: ${bodyBounds(bodyCoreBox(set))}; source alpha >= ${CORE_ALPHA} only, not an owned/eroded core or anatomical height`);
1024
+ console.log(` body-group union [${drawn}]: ${bodyBounds(bodyOpaqueBox(set))}; source alpha > ${OPAQUE_ALPHA_ABOVE} only; not an anatomical height`);
1025
+ console.log(' body-group population: drawn group=body (includes hair/head/wings/objects; excludes face/headwear, author and hidden art); before cleanup, resampling or painting registration');
988
1026
  // Issue #256: a layer the author made is said to be one, so it never reads as See-through's. None, no line.
989
1027
  for (const l of set.layers) if (l.tag.group === 'author') console.log(` ${authorLayerLine(l.name)}`);
990
1028
  // Issue #359: a hidden-art layer is listed apart, never in the drawn table. None, no line.
@@ -1014,13 +1052,13 @@ const TAKES = {
1014
1052
  rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
1015
1053
  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'] },
1016
1054
  compare: { valued: ['--left', '--right', '--map'], switches: [] },
1017
- check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
1055
+ check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment', '--wind-band'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
1018
1056
  loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
1019
1057
  assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config'], switches: ['--propose-plan'] },
1020
1058
  'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll'], switches: ['--offload', '--lama', '--nf4'] },
1021
1059
  'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
1022
1060
  inputs: { valued: ['--source', '--config', '--out'], switches: [] },
1023
- build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in', '--alt', '--set', '--environment'], switches: ['--loop', '--seam-pairs', '--keep-stills', '--all-sets'] },
1061
+ 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'] },
1024
1062
  compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
1025
1063
  'mesh-draft': { valued: ['--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: ['--more-triangles'] },
1026
1064
  'mesh-stats': { valued: ['--config', '--parts'], switches: [] },
@@ -1271,7 +1309,7 @@ function cmdCheck(args: string[]): number {
1271
1309
  try {
1272
1310
  const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
1273
1311
  const r = checkStage(
1274
- { 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 } : {}) },
1312
+ { 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 } : {}) },
1275
1313
  rigcRunner(bin),
1276
1314
  bin,
1277
1315
  console.log,
@@ -1339,11 +1377,12 @@ function cmdPropose(args: string[]): number {
1339
1377
  }
1340
1378
  // Issue #671: --only-missing adds entries to an existing config for the parts it attaches by neither section; it
1341
1379
  // belongs to the ordinary proposal's --write-config alone.
1380
+ const fromConfigTakes = '(takes --parts, --source, --out, --from-config and optional --keypoints [--person] only)';
1342
1381
  if (onlyMissing) {
1343
1382
  const mode = 'propose --parts … --write-config <config.json> --only-missing';
1344
1383
  if (headBox) return usage(`--only-missing belongs to ${mode}; --head-box writes seethrough.head_box only`);
1345
1384
  if (!flags.has('--parts')) return usage(`--only-missing belongs to ${mode}; give --parts <dir>, the parts the entries are drafted from`);
1346
- if (flags.has('--from-config')) return usage(`--only-missing belongs to ${mode}; --from-config proposes nothing`);
1385
+ if (flags.has('--from-config')) return usage(`--only-missing belongs to ${mode}; --from-config proposes nothing ${fromConfigTakes}`);
1347
1386
  if (flags.has('--cloth')) return usage(`--only-missing belongs to ${mode}; --cloth writes a new config with the recipe's entries spliced in`);
1348
1387
  if (!flags.has('--write-config')) return usage(`--only-missing belongs to ${mode}; give --write-config <config.json>, the existing config the entries are added to`);
1349
1388
  }
@@ -1388,17 +1427,17 @@ function cmdPropose(args: string[]): number {
1388
1427
  if (partsArg === undefined) return usage('propose needs --parts <dir>, the directory holding parts.json and parts/');
1389
1428
  if (source === undefined) return usage('propose needs --source <painting.png>, drawn under the overlay');
1390
1429
  if (out === undefined) return usage('propose needs --out <dir>');
1391
- if (flags.has('--from-config') && flags.has('--compare')) return usage('--from-config and --compare are two modes; give one');
1430
+ if (flags.has('--from-config') && flags.has('--compare')) return usage(`--from-config and --compare are two modes; give one (--from-config ${fromConfigTakes.slice(1, -1)})`);
1392
1431
  if (flags.has('--person') && !flags.has('--keypoints')) return usage('--person names a person in the --keypoints file; give --keypoints <file>');
1393
1432
  // Issue #251: the See-through runs the turn control's depths are read from; without them nothing is drafted.
1394
- if (flags.has('--from-config') && (flags.has('--full') || flags.has('--head'))) return usage('--full and --head draft the turn control while proposing; --from-config proposes nothing');
1433
+ if (flags.has('--from-config') && (flags.has('--full') || flags.has('--head'))) return usage(`--full and --head draft the turn control while proposing; --from-config proposes nothing ${fromConfigTakes}`);
1395
1434
  // Issue #199: a declared tolerance for a wrist on no handwear part. Whole px above the built-in one: the distance is
1396
1435
  // counted in whole px, so a fraction would bind what its floor binds, and a value at or under the built-in binds nothing.
1397
1436
  const snapArg = flags.get('--wrist-snap');
1398
1437
  let wristSnap: number | null = null;
1399
1438
  if (snapArg !== undefined) {
1400
1439
  if (!flags.has('--keypoints')) return usage('--wrist-snap binds a wrist joint to a handwear part; give --keypoints <file>');
1401
- if (flags.has('--from-config')) return usage('--wrist-snap binds a wrist while proposing; --from-config proposes nothing');
1440
+ if (flags.has('--from-config')) return usage(`--wrist-snap binds a wrist while proposing; --from-config proposes nothing ${fromConfigTakes}`);
1402
1441
  if (!/^[0-9]+$/.test(snapArg) || Number(snapArg) <= WRIST_ON_PART_PX) {
1403
1442
  return usage(`--wrist-snap ${snapArg} is not an integer above ${WRIST_ON_PART_PX}, the px within which a wrist already holds a part`);
1404
1443
  }
@@ -1406,15 +1445,15 @@ function cmdPropose(args: string[]): number {
1406
1445
  }
1407
1446
  // Issue #265: the drafted lattice's spacing; the figure's unless the reference's per-part rule is asked for.
1408
1447
  const ruleArg = flags.get('--grid-rule');
1409
- if (ruleArg !== undefined && flags.has('--from-config')) return usage('--grid-rule chooses the drafted spacing; --from-config proposes nothing');
1448
+ if (ruleArg !== undefined && flags.has('--from-config')) return usage(`--grid-rule chooses the drafted spacing; --from-config proposes nothing ${fromConfigTakes}`);
1410
1449
  if (ruleArg !== undefined && !(GRID_RULES as readonly string[]).includes(ruleArg)) return usage(`--grid-rule ${ruleArg}; one of ${GRID_RULES.join(', ')} is required`);
1411
1450
  const gridRule = (ruleArg ?? DEFAULT_GRID_RULE) as GridRule;
1412
1451
  // Issue #364: the cloth and hair recipe, read from a config's cloth block; without it nothing changes.
1413
1452
  const clothArg = flags.get('--cloth');
1414
- if (clothArg !== undefined && flags.has('--from-config')) return usage('--cloth drafts trees while proposing; --from-config proposes nothing');
1453
+ if (clothArg !== undefined && flags.has('--from-config')) return usage(`--cloth drafts trees while proposing; --from-config proposes nothing ${fromConfigTakes}`);
1415
1454
  // Issue #537: the --cloth config's own text with what its recipe drafts spliced in, beside proposal.json.
1416
1455
  const writeArg = flags.get('--write-config');
1417
- if (writeArg !== undefined && flags.has('--from-config')) return usage('--write-config writes what the recipe drafts into its config; --from-config proposes nothing');
1456
+ if (writeArg !== undefined && flags.has('--from-config')) return usage(`--write-config writes what the recipe drafts into its config; --from-config proposes nothing ${fromConfigTakes}`);
1418
1457
  // Issue #571: the config the recipe is changed from; an entry equal to its recipe's draft is the recipe's, not a hand edit.
1419
1458
  const fromArg = flags.get('--from');
1420
1459
  if (fromArg !== undefined && writeArg === undefined) return usage('--from names the config the recipe is changed from, for --write-config; give --write-config <out.json>');
@@ -1422,7 +1461,7 @@ function cmdPropose(args: string[]): number {
1422
1461
  if (writeArg !== undefined && clothArg !== undefined) {
1423
1462
  const ours = ['proposal.json', BASIS_FILE, 'render/landmarks.png', 'render/landmarks_head.png'].map((f) => resolve(out, f));
1424
1463
  if (existsSync(writeArg) || ours.includes(resolve(writeArg))) {
1425
- throw new PartsError([{ code: 'PROPOSE_WRITE_CONFIG_FRESH', object: writeArg, detail: existsSync(writeArg) ? 'exists; the config is written to a new file, never over the --cloth config or an earlier one' : `is a file this run writes under ${out}; name another file` }]);
1464
+ throw new PartsError([{ code: 'PROPOSE_WRITE_CONFIG_FRESH', object: writeArg, detail: existsSync(writeArg) ? 'exists; the config is written to a new file, never over the --cloth config or an earlier one (unlike assemble --propose-plan and propose without --cloth, which update an existing --config in place)' : `is a file this run writes under ${out}; name another file` }]);
1426
1465
  }
1427
1466
  separateConfigWriteTarget({ path: resolve(writeArg) }, ours, 'PROPOSE_WRITE_CONFIG_FRESH');
1428
1467
  }
@@ -1630,7 +1669,7 @@ function cmdAssemble(args: string[]): number {
1630
1669
  const [source, full, config] = ['--source', '--full', '--config'].map((f) => flags.get(f) as string);
1631
1670
  const head = flags.get('--head') ?? null;
1632
1671
  const writePath = flags.get('--write-config');
1633
- if (writePath !== undefined && resolve(writePath) !== resolve(config)) return usage('--write-config must name the same file as --config for assemble --propose-plan');
1672
+ if (writePath !== undefined && resolve(writePath) !== resolve(config)) return usage('--write-config must name the same file as --config for assemble --propose-plan (assemble --propose-plan updates --config in place; only propose --cloth --write-config writes to a new file)');
1634
1673
  let staged: StagedConfigWrite | null = null;
1635
1674
  try {
1636
1675
  if (propose) {
@@ -1857,7 +1896,7 @@ function cmdBuild(args: string[]): number {
1857
1896
  const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-build-'));
1858
1897
  try {
1859
1898
  const r = build(
1860
- { 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 } : {}) },
1899
+ { 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 } : {}) },
1861
1900
  { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate'), stages: {
1862
1901
  assemble: (input, outputs) => runBuildProcess({ stage: 'assemble', bin, input, outputs }, join(scratch, 'workers')),
1863
1902
  rig: (input, _rigc, gateScratch) => runBuildProcess({ stage: 'rig', bin, input, scratch: gateScratch }, join(scratch, 'workers')),