rig-parts 1.9.1 → 1.10.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
@@ -283,12 +283,13 @@ agent skill.
283
283
  | `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
284
284
  | `propose --parts --source --out [--compare <config>]` | 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 |
285
285
  | `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)) |
286
+ | `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) |
286
287
  | `propose … --from-config <config>` | draw and LINT the config's current bones (by the joints too, given `--keypoints`) |
287
288
  | `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) |
288
289
  | `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`) |
289
290
  | `check --rig --out [--parts] [--source] [--requirements]` | build packed, gated under `spine-html`, 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*) |
290
291
  | `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
291
- | `build --config --source --full --head --out [--seam] [--project] [--loop] [--lead-in <periods>] [--requirements] [--idle-keys ctl\|direct]` | assemble, rig and check in one process, stopping at the first refusal; `--requirements` is 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 |
292
+ | `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 |
292
293
  | `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*) |
293
294
 
294
295
  `rig-parts --help` has every flag. Exit codes: 0 done, 1 input refused (every
package/cli.ts CHANGED
@@ -16,7 +16,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, wri
16
16
  import { tmpdir } from 'node:os';
17
17
  import { basename, dirname, extname, join } from 'node:path';
18
18
  import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
19
- import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
19
+ import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage, suppliedCheckFlag } from './src/build.ts';
20
20
  import { ACCEPTABLE_BARS, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, NO_PARTS_SENTENCE, SEAM_MEAN_BAR, SOURCE_SENTENCE, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
21
21
  import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
22
22
  import { type CharacterConfig, loadConfig, loadEarlyConfig, loadPaintConfig } from './src/config.ts';
@@ -26,13 +26,15 @@ import { makeInputs } from './src/inputs.ts';
26
26
  import { idleKeysOf, isIdleKeys } from './src/rig.ts';
27
27
  import { figuresPhrase, implausibleRules, layerFigures, type LayerSet, pct, readLayers, ruleSummary, times } from './src/layers.ts';
28
28
  import { checkImageSize, choosePerson, KEYPOINTS_SPEC, loadKeypoints, type RigJoints, toRigJoints } from './src/keypoints.ts';
29
- import { basisLines, checkProposal, compare, compareLines, draftTurn, drawLandmarks, FRINGE_STEP_PX, HIP_MIN_FRACTION, lint, lintLine, type PartSet, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
29
+ import { basisLines, checkProposal, compare, compareLines, DEFAULT_GRID_RULE, draftTurn, drawLandmarks, FIGURE_GRID_DIVISOR, FRINGE_STEP_PX, GRID_RULES, type GridRule, HIP_MIN_FRACTION, lint, lintLine, type PartSet, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
30
30
  import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, serializeBasis } from './src/diagnostics.ts';
31
+ import { BEND_DEGREES, BEND_WORST_SHARE, DUST_MAX_PX, DUST_MAX_SHARE, IDLE_BOUND_PX, MESH_DRAFT_REPORT, MESH_DRAFT_SPEC, meshDraftStage, MORE_TRIANGLES_FLAG, TOLERANCE_FIGURE_DIVISOR } from './src/meshdraft.ts';
31
32
  import { readPng, writePng } from './src/raster/png.ts';
32
33
  import { REQUIREMENTS_SENTENCE } from './src/requirements.ts';
33
34
  import { composeStage, IMAGE_SEP, PLATE, PROVENANCES, SCENE_REPORT_FILE, SCENE_REPORT_SPEC, SCENE_SPEC } from './src/scene.ts';
34
35
  import { buildSheet, defaultCaption, type Tile, tilesFrom } from './src/sheet.ts';
35
36
  import { loadComparison, requiredProblems, structureLines } from './src/structure.ts';
37
+ import { authorLayerLine } from './src/tags.ts';
36
38
 
37
39
  const EXIT_OK = 0;
38
40
  const EXIT_REFUSED = 1;
@@ -62,9 +64,11 @@ usage:
62
64
  (alpha below 128), background share (min channel above 235) and area as a
63
65
  multiple of the rest of the figure (the other layers' union). A layer that
64
66
  crosses a plausibility rule gets a WARN line naming the rule and the bar;
65
- --propose-plan leaves it out. Refuses, by name, a missing file, an
66
- unknown tag, a PNG whose size is not its box, and anything else outside
67
- the input contract — never a WARN.
67
+ --propose-plan leaves it out. A layer named author:<label> is one the
68
+ author made by hand: its group is author, and a line says it has no
69
+ role. Refuses, by name, a missing file, an unknown tag, a PNG whose
70
+ size is not its box, and anything else outside the input contract —
71
+ never a WARN.
68
72
 
69
73
  rig-parts sheet --source <painting.png> --layers <path> [--layers <path> ...]
70
74
  --out <sheet.png> [--cell <px>] [--cols <n>]
@@ -77,7 +81,8 @@ usage:
77
81
  rig-parts propose --parts <dir> --source <painting.png> --out <dir> [--compare <config.json>]
78
82
  Propose bones, meshes, regions and an idle from the assembled parts (<dir>
79
83
  holds parts.json and parts/). Roles come from each part's See-through tag,
80
- never its name. Writes <out>/proposal.json (config-shaped: bones, meshes,
84
+ never its name; a part from an author:<label> layer has no role and
85
+ rides the nearest trunk bone until the config names one. Writes <out>/proposal.json (config-shaped: bones, meshes,
81
86
  regions, motion with its blink — and a blink.still cut for a lash that
82
87
  reaches far above its eyewhite, when a clear row allows one; no blink
83
88
  when no part is an eyewhite, and no brows in it when no part is an
@@ -149,6 +154,16 @@ usage:
149
154
  or a layer with no depth is refused by name. Without either flag nothing
150
155
  changes.
151
156
 
157
+ rig-parts propose … [--grid-rule figure|part]
158
+ How each drafted lattice's spacing (meshes.<part>.grid) is chosen.
159
+ figure, the default: round(H / ${FIGURE_GRID_DIVISOR}) rig px for every lattice, H the
160
+ height of every part's box together, and a part whose own spacing is
161
+ already coarser keeps it; a note says which parts took it, that it was
162
+ calibrated on three figures of similar size, and what it costs (the
163
+ idle moved against the finer lattice, which no bar reads). part: the
164
+ reference's rule, a tenth of the part's short side clamped to 5..36.
165
+ r is the part rule's under both.
166
+
152
167
  rig-parts propose --parts <dir> --source <painting.png> --out <dir> --from-config <config.json>
153
168
  Draw the config's CURRENT bones instead (<out>/render/landmarks_config.png
154
169
  and _head) and LINT them (by the joints too, given --keypoints), with the
@@ -226,6 +241,42 @@ usage:
226
241
  rig-c 1.3.0 or later and makes A15 a SKIP that prints its cost (the
227
242
  stage prints that SKIP line). The pose is the same to one level of float
228
243
  rounding, and so is the per-frame mesh work (AUTHORING §5).
244
+ rig-parts mesh-draft --config <config.json> --parts <dir> --out <dir> [--requirements <file.json>]
245
+ [--idle-keys ctl|direct] [--page-edges pot|free] [--pack-shape rect|polygon]
246
+ [--more-triangles]
247
+ Draft a traced outline around a coarse grid for every lattice mesh the
248
+ config states, and keep it only where it holds against that part's
249
+ lattice (issue #265). The config is read, never written: --out receives
250
+ config.json — the input's own text with each drafted part's meshes entry
251
+ replaced, every other byte as read — and ${MESH_DRAFT_REPORT} (spec
252
+ "${MESH_DRAFT_SPEC}": the rules, both checks' bars, and per part its
253
+ lattice and candidate triangles and overdraw, the five readings, each
254
+ with its margin to its line, and the note). H is the height of the union of the parts' boxes; the candidate is
255
+ contour {tolerance T, margin T, spacing S, maxEdge S, stray} with
256
+ T = max(1, round(H / ${TOLERANCE_FIGURE_DIVISOR})) and S = round(H / ${FIGURE_GRID_DIVISOR}) (a figure whose S is 0 is
257
+ refused, MESH_DRAFT_FIGURE), stray the largest dust island (at most
258
+ ${DUST_MAX_PX} px and ${DUST_MAX_SHARE * 100} % of the part's art); a part with any other extra island
259
+ keeps its lattice. The config as written must build and check PASS first
260
+ (MESH_DRAFT_LATTICE_GREEN). Then each candidate is held to its lattice:
261
+ every check bar that reads the part passes on the drafted rig; within
262
+ ${IDLE_BOUND_PX} rig px of the lattice over the idle; at the bend set (every chain
263
+ link the part's segments name turned ${BEND_DEGREES} degrees, both ways) no more
264
+ inverted triangles than the lattice and the worst sample within
265
+ ${BEND_WORST_SHARE * 100} % of the lattice's, both against a denser lattice of the same
266
+ part; and no more triangles than the lattice — every motion reading
267
+ rig-c's (check, and compareMeshesInMotion over three unpacked gate
268
+ builds). The lattice is the config's own grid entry: for a proposed
269
+ config, the figure's spacing propose drafts. The gate is a judgment
270
+ relative to that lattice, not an absolute quality: when the lattice
271
+ coarsens, the reference moves with it. --more-triangles drafts a
272
+ candidate that holds the four motion readings even with more triangles
273
+ than its lattice, and the report records each part's triangle and
274
+ overdraw difference. A part that passes is drafted, its note saying
275
+ what it was measured against; one that fails keeps its entry, and the
276
+ report names each failed reading and by how much. Written only when the
277
+ drafted config's check is PASS. --requirements, --idle-keys,
278
+ --page-edges and --pack-shape are as for rig and check.
279
+
229
280
  rig-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>]
230
281
  [--page-edges pot|free] [--pack-shape rect|polygon]
231
282
  [--requirements <file.json>] [--seam-pairs] [--keep-stills]
@@ -436,7 +487,7 @@ usage:
436
487
  --head <dir|psd> --out <dir> [--seam near-white|silhouette]
437
488
  [--project core|visible] [--page-edges pot|free]
438
489
  [--pack-shape rect|polygon] [--loop] [--requirements <file.json>]
439
- [--idle-keys ctl|direct] [--lead-in <periods>]
490
+ [--idle-keys ctl|direct] [--lead-in <periods>] [--seam-pairs] [--keep-stills]
440
491
  assemble, then rig, then check, in one process, each stage's own lines
441
492
  printed under [assemble], [rig] and [check]; the first stage that refuses
442
493
  stops the build with its own FAIL lines. The config must already carry
@@ -459,6 +510,13 @@ usage:
459
510
  and check's artifact, take them). --requirements is forwarded to check
460
511
  (as for check); the file's own shape is read before assemble runs, and
461
512
  its names resolve against the rig in the check, before it builds.
513
+ --seam-pairs and --keep-stills are forwarded to check (as for check:
514
+ the seam split by pair on a pass too, under check/seam/; the setup-pose
515
+ still under check/setup_still/). check's --rig and --parts are refused:
516
+ build's check reads the rig and the parts it wrote, <out>/rig and <out>.
517
+ build's --source is the painting assemble reads and is not handed to
518
+ the check, so check.json carries no SETUP_POSE_VS_SOURCE; run check
519
+ --rig <out>/rig --parts <out> --source <painting> for that line.
462
520
  --idle-keys is forwarded to the rig stage (as for rig) and defaults to
463
521
  ctl; the first line names it when it is direct. A config with a
464
522
  two-bone ik over chain links the idle keys needs --idle-keys direct:
@@ -568,6 +626,8 @@ function printLayerTable(set: LayerSet): void {
568
626
  for (const r of rows) console.log(line(r));
569
627
  const painted = set.layers.filter((l) => l.opaquePx > 0).length;
570
628
  console.log(` ${set.layers.length} layer(s): ${painted} with opaque pixels, ${set.layers.length - painted} with none`);
629
+ // Issue #256: a layer the author made is said to be one, so it never reads as See-through's. None, no line.
630
+ for (const l of set.layers) if (l.tag.group === 'author') console.log(` ${authorLayerLine(l.name)}`);
571
631
  // A reader refuses nothing on plausibility: it says what --propose-plan will leave out, and why.
572
632
  let warned = 0;
573
633
  for (const f of figures) {
@@ -587,7 +647,7 @@ function printLayerTable(set: LayerSet): void {
587
647
  const TAKES = {
588
648
  sheet: { valued: ['--source', '--layers', '--out', '--cell', '--cols'], switches: [] },
589
649
  rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
590
- propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap'], switches: ['--head-box'] },
650
+ propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule'], switches: ['--head-box'] },
591
651
  compare: { valued: ['--left', '--right', '--map'], switches: [] },
592
652
  check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements'], switches: ['--seam-pairs', '--keep-stills'] },
593
653
  loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
@@ -595,8 +655,9 @@ const TAKES = {
595
655
  'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll'], switches: ['--offload', '--lama', '--nf4'] },
596
656
  'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
597
657
  inputs: { valued: ['--source', '--config', '--out'], switches: [] },
598
- build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in'], switches: ['--loop'] },
658
+ build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in'], switches: ['--loop', '--seam-pairs', '--keep-stills'] },
599
659
  compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
660
+ 'mesh-draft': { valued: ['--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: ['--more-triangles'] },
600
661
  } as const satisfies Record<string, { valued: readonly string[]; switches: readonly string[] }>;
601
662
 
602
663
  type Command = keyof typeof TAKES;
@@ -891,6 +952,11 @@ function cmdPropose(args: string[]): number {
891
952
  }
892
953
  wristSnap = Number(snapArg);
893
954
  }
955
+ // Issue #265: the drafted lattice's spacing; the figure's unless the reference's per-part rule is asked for.
956
+ const ruleArg = flags.get('--grid-rule');
957
+ if (ruleArg !== undefined && flags.has('--from-config')) return usage('--grid-rule chooses the drafted spacing; --from-config proposes nothing');
958
+ if (ruleArg !== undefined && !(GRID_RULES as readonly string[]).includes(ruleArg)) return usage(`--grid-rule ${ruleArg}; one of ${GRID_RULES.join(', ')} is required`);
959
+ const gridRule = (ruleArg ?? DEFAULT_GRID_RULE) as GridRule;
894
960
  const partsDir = existsSync(partsArg) && statSync(partsArg).isFile() ? dirname(partsArg) : partsArg;
895
961
  if (!existsSync(source)) {
896
962
  throw new PartsError([{ code: 'PROPOSE_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is drawn under the overlay' }]);
@@ -918,7 +984,7 @@ function cmdPropose(args: string[]): number {
918
984
  }
919
985
  const cmpPath = flags.get('--compare');
920
986
  const cfg = cmpPath === undefined ? null : loadConfig(cmpPath);
921
- const { proposal: drafted, basis } = proposeWithBasis(P, joints, wristSnap);
987
+ const { proposal: drafted, basis } = proposeWithBasis(P, joints, wristSnap, gridRule);
922
988
  const fullRun = flags.get('--full');
923
989
  const headRun = flags.get('--head');
924
990
  const prop =
@@ -1175,14 +1241,19 @@ function cmdInputs(args: string[]): number {
1175
1241
 
1176
1242
  function cmdBuild(args: string[]): number {
1177
1243
  const flags = new Map<string, string>();
1178
- let loop = false;
1244
+ // The bare switches build takes: --loop its own, --seam-pairs and --keep-stills check's, forwarded (issue #257).
1245
+ const on = new Set<string>();
1246
+ const switches: readonly string[] = TAKES.build.switches;
1179
1247
  for (let i = 0; i < args.length; i++) {
1180
1248
  const flag = args[i];
1181
- if (flag === '--loop') {
1182
- if (loop) return usage('--loop is given twice');
1183
- loop = true;
1249
+ if (switches.includes(flag)) {
1250
+ if (on.has(flag)) return usage(`${flag} is given twice`);
1251
+ on.add(flag);
1184
1252
  continue;
1185
1253
  }
1254
+ // A check flag whose value build supplies itself (--rig, --parts) is refused with what build reads instead (issue #257).
1255
+ const supplied = suppliedCheckFlag(flag);
1256
+ if (supplied !== null) return usage(supplied);
1186
1257
  if (!takesValued('build', flag)) return usage(notTaken('build', flag));
1187
1258
  const value = args[i + 1];
1188
1259
  if (value === undefined) return usage(`${flag} needs a value`);
@@ -1201,6 +1272,7 @@ function cmdBuild(args: string[]): number {
1201
1272
  if (!isPackShape(shape)) return usage(shape);
1202
1273
  const keys = idleKeysOf(flags.get('--idle-keys'));
1203
1274
  if (!isIdleKeys(keys)) return usage(keys);
1275
+ const loop = on.has('--loop');
1204
1276
  const leadText = flags.get('--lead-in');
1205
1277
  if (leadText !== undefined && !/^[1-9][0-9]*$/.test(leadText)) return usage(`--lead-in ${leadText}; a whole number of idle periods, 1 or more, is required`);
1206
1278
  if (leadText !== undefined && !loop) return usage('--lead-in needs --loop; it says which frames the loop is encoded from');
@@ -1214,7 +1286,7 @@ function cmdBuild(args: string[]): number {
1214
1286
  const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-build-'));
1215
1287
  try {
1216
1288
  const r = build(
1217
- { 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) }) },
1289
+ { 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') },
1218
1290
  { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
1219
1291
  console.log,
1220
1292
  );
@@ -1248,6 +1320,46 @@ function cmdCompose(args: string[]): number {
1248
1320
  }
1249
1321
  }
1250
1322
 
1323
+ function cmdMeshDraft(args: string[]): number {
1324
+ // The one switch mesh-draft takes (#295): taken out before the valued flags are read.
1325
+ const more = args.filter((a) => a === MORE_TRIANGLES_FLAG).length;
1326
+ if (more > 1) return usage(`${MORE_TRIANGLES_FLAG} is given twice`);
1327
+ const f = flags(
1328
+ args.filter((a) => a !== MORE_TRIANGLES_FLAG),
1329
+ 'mesh-draft',
1330
+ ['--config', '--parts', '--out'],
1331
+ );
1332
+ if (typeof f === 'string') return usage(f);
1333
+ const keys = idleKeysOf(f.get('--idle-keys'));
1334
+ if (!isIdleKeys(keys)) return usage(keys);
1335
+ const edges = pageEdgesOf(f.get('--page-edges'));
1336
+ if (!isPageEdges(edges)) return usage(edges);
1337
+ const shape = packShapeOf(f.get('--pack-shape'));
1338
+ if (!isPackShape(shape)) return usage(shape);
1339
+ let bin: string;
1340
+ try {
1341
+ bin = findRigc(import.meta.dir, process.env.PATH ?? '');
1342
+ } catch (err) {
1343
+ return printRefusal(err);
1344
+ }
1345
+ const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-mesh-draft-'));
1346
+ try {
1347
+ const req = f.get('--requirements');
1348
+ meshDraftStage(
1349
+ { config: f.get('--config') as string, parts: f.get('--parts') as string, out: f.get('--out') as string, idleKeys: keys, pageEdges: edges, packShape: shape, ...(req === undefined ? {} : { requirements: req }), ...(more === 1 ? { moreTriangles: true } : {}) },
1350
+ rigcRunner(bin),
1351
+ bin,
1352
+ scratch,
1353
+ console.log,
1354
+ );
1355
+ return EXIT_OK;
1356
+ } catch (err) {
1357
+ return printRefusal(err);
1358
+ } finally {
1359
+ rmSync(scratch, { recursive: true, force: true });
1360
+ }
1361
+ }
1362
+
1251
1363
  function main(argv: string[]): number | Promise<number> {
1252
1364
  const [command, ...rest] = argv;
1253
1365
  if (command === undefined || command === '--help' || command === '-h' || command === 'help') {
@@ -1270,6 +1382,7 @@ function main(argv: string[]): number | Promise<number> {
1270
1382
  if (command === 'comfy') return cmdComfy(rest);
1271
1383
  if (command === 'build') return cmdBuild(rest);
1272
1384
  if (command === 'compose') return cmdCompose(rest);
1385
+ if (command === 'mesh-draft') return cmdMeshDraft(rest);
1273
1386
  const later = LATER.find(([name]) => name === command);
1274
1387
  if (later !== undefined) {
1275
1388
  console.log(` FAIL NOT_IMPLEMENTED: \`rig-parts ${command}\` (${later[1]}) is not implemented in this version, ${version()}`);