rig-parts 1.3.0 → 1.5.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
@@ -288,7 +288,7 @@ agent skill.
288
288
  | `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*) |
289
289
  | `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
290
290
  | `build --config --source --full --head --out [--seam] [--project] [--loop] [--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 |
291
- | `compose --scene <scene.json> --out [--requirements]` | bind several characters' green builds into one rig: 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*) |
291
+ | `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*) |
292
292
 
293
293
  `rig-parts --help` has every flag. Exit codes: 0 done, 1 input refused (every
294
294
  reason is a FAIL line), 2 a usage error or a command this version does not implement.
package/cli.ts CHANGED
@@ -17,16 +17,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
19
  import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
20
- import { 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';
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
- import { type CharacterConfig, loadConfig, loadEarlyConfig } from './src/config.ts';
22
+ import { type CharacterConfig, loadConfig, loadEarlyConfig, loadPaintConfig } from './src/config.ts';
23
23
  import { PartsError, problemLine } from './src/errors.ts';
24
24
  import { proposeHeadBox } from './src/headbox.ts';
25
25
  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, drawLandmarks, HIP_MIN_FRACTION, lint, lintLine, type PartSet, proposeWithBasis, readPartSet, serializeProposal } from './src/propose.ts';
29
+ import { basisLines, checkProposal, compare, compareLines, drawLandmarks, HIP_MIN_FRACTION, lint, lintLine, type PartSet, proposeWithBasis, readPartSet, serializeProposal, WRIST_ON_PART_PX } from './src/propose.ts';
30
30
  import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, serializeBasis } from './src/diagnostics.ts';
31
31
  import { readPng, writePng } from './src/raster/png.ts';
32
32
  import { REQUIREMENTS_SENTENCE } from './src/requirements.ts';
@@ -108,7 +108,7 @@ usage:
108
108
  bone differs (origin, tip, parent; how far, in px). No confidence is
109
109
  computed. It prints "wrote ${BASIS_FILE} (…)" after the coverage lines.
110
110
 
111
- rig-parts propose … [--keypoints <keypoints.json> [--person <id>]]
111
+ rig-parts propose … [--keypoints <keypoints.json> [--person <id>] [--wrist-snap <px>]]
112
112
  Read the figure's pose from one explicit file (spec "${KEYPOINTS_SPEC}"):
113
113
  its space (painting-px, origin top-left, y down — another is refused),
114
114
  the image's width and height (held to --source's, KEYPOINTS_IMAGE_SIZE),
@@ -124,7 +124,11 @@ usage:
124
124
  the rule. One note per joint says which. LINT then reads the torso along
125
125
  the neck -> hips line and each sleeve chain along shoulder -> wrist
126
126
  instead of by screen y, and prints the rule set it used first. Without
127
- --keypoints nothing changes.
127
+ --keypoints nothing changes. A wrist holds a part when it lies within
128
+ ${WRIST_ON_PART_PX} px of its art (the larger of the x and y gaps);
129
+ --wrist-snap <px>, an integer above ${WRIST_ON_PART_PX}, binds a wrist on no
130
+ part to the one nearest part within <px>, said in a "wrist snap:" note
131
+ (with --keypoints, not with --from-config). Without it nothing changes.
128
132
 
129
133
  rig-parts propose --parts <dir> --source <painting.png> --out <dir> --from-config <config.json>
130
134
  Draw the config's CURRENT bones instead (<out>/render/landmarks_config.png
@@ -205,7 +209,7 @@ usage:
205
209
  rounding, and so is the per-frame mesh work (AUTHORING §5).
206
210
  rig-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>]
207
211
  [--page-edges pot|free] [--pack-shape rect|polygon]
208
- [--requirements <file.json>]
212
+ [--requirements <file.json>] [--seam-pairs] [--keep-stills]
209
213
  Build, gate, render and measure a rig through rig-c's CLI (the rigc at
210
214
  node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
211
215
  a rig spec, which rigc build compiles, not a compiled skeleton.json.
@@ -222,7 +226,13 @@ usage:
222
226
  entry that gated the build, and pack_mode, the two pack flags). PASS needs every gate summary
223
227
  "0 failed", the seam (setup pose vs the flat composite of parts/) at mean
224
228
  |d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
225
- frame at t = duration) at max |d| 0. Then six judgement lines, each in
229
+ frame at t = duration) at max |d| 0. A failing seam, or any seam under
230
+ --seam-pairs, is split by the pair of parts each counted pixel lies
231
+ between: one "seam pair" line each, worst first, with its count, frame
232
+ and rig boxes and a picture under seam/, and check.json's seam_pairs
233
+ (AUTHORING §7). --keep-stills leaves the setup-pose still the seam reads,
234
+ rigc's render directory with its frames.json (viewport and background),
235
+ under setup_still/. Then six judgement lines, each in
226
236
  check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
227
237
  bars (AUTHORING §7): BREATH_VISIBLE (the topwear moves, the footwear does
228
238
  not, each rendered alone), BLINK_NO_HOLE (the setup pose with the blink
@@ -421,13 +431,18 @@ usage:
421
431
  rigc refuses the pair a control splits, and the RIG_RIGC_GREEN line
422
432
  names the flag on the command that ran.
423
433
 
424
- rig-parts compose --scene <scene.json> --out <dir> [--requirements <file.json>]
434
+ rig-parts compose --scene <scene.json> --out <dir> [--requirements <file.json>] [--accept <bar>[,<bar>...]]
425
435
  Bind several finished characters into one rig. The scene file (spec
426
436
  "${SCENE_SPEC}") states the canvas {width, height}, an optional plate
427
437
  {image, provenance: ${PROVENANCES.join(' | ')}, note?}, the characters
428
- [{id, build, offset: [x, y]}] — each build the --out of a green
438
+ [{id, build, offset: [x, y], scale?}] — each build the --out of a green
429
439
  \`rig-parts build\` (its check/check.json PASS), each offset a
430
- translation from its rig px to canvas px — and order: the draw order,
440
+ translation from its rig px to canvas px, each scale (a positive
441
+ number, 1 when absent) the character's size about its stage's top-left
442
+ corner: every length it carries is scaled as Spine's loader scales a
443
+ skeleton (bones, regions, weight binds, constraint lengths, translate
444
+ keys), and a physics wind or gravity, which no per-character value
445
+ can scale, is refused (SCENE_SCALE_EXACT) — and order: the draw order,
431
446
  back to front, of character ids (that character's remaining slots, in
432
447
  its own order) and "<id>:<slot>" entries, every slot exactly once.
433
448
  Paths are relative to the scene file. Every bone, slot, attachment,
@@ -453,7 +468,7 @@ usage:
453
468
  rig-c as rig gates its own (build --profile spine-html --pack, the
454
469
  compile and the packed pages on disk); --out receives rig/ (rig.json,
455
470
  motion.json, images/) and ${SCENE_REPORT_FILE} (spec "${SCENE_REPORT_SPEC}":
456
- every setting, each character's offset, bounds and shift (and what it
471
+ every setting, each character's offset, scale, bounds and shift (and what it
457
472
  moved),
458
473
  the plate's provenance, the declared and the composed order) only when
459
474
  it is green; then check runs over rig/ into check/, as on a rig it did
@@ -461,8 +476,14 @@ usage:
461
476
  TIP_OVER_ROOT, STILL_REGIONS_DARK and RECOMPOSITE_HOLES say SKIP and
462
477
  why), --requirements forwarded. Nothing is inferred: not the order, not
463
478
  an overlap, not a scale, not a person; the plate's provenance and the
464
- order are the author's and judged by nothing. Exit 0 when the check is
465
- PASS, 1 otherwise.
479
+ order are the author's and judged by nothing. --accept (or a
480
+ character's "accept": [...] in the scene file; the two lists are
481
+ joined, the command line's applying to every character) takes a build
482
+ whose own check failed only the bars named — ${ACCEPTABLE_BARS.join(', ')};
483
+ the gate, a declared requirement and an unknown name are refused
484
+ (SCENE_ACCEPT_KNOWN). It is for looking, not shipping: the [compose]
485
+ line and ${SCENE_REPORT_FILE} name what was accepted, and the run exits 1.
486
+ Exit 0 when the check is PASS and nothing was accepted, 1 otherwise.
466
487
 
467
488
  rig-parts --version
468
489
  rig-parts --help
@@ -688,7 +709,17 @@ function flags(args: string[], known: readonly string[], command: string, option
688
709
  }
689
710
 
690
711
  function cmdCheck(args: string[]): number {
691
- const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--source', '--page-edges', '--pack-shape', '--requirements']);
712
+ // The two switches check takes (issues #202, #209): they carry no value, so they are taken out before the valued flags are read.
713
+ const seamPairs = args.filter((a) => a === '--seam-pairs').length;
714
+ if (seamPairs > 1) return usage('--seam-pairs is given twice');
715
+ const keepStills = args.filter((a) => a === '--keep-stills').length;
716
+ if (keepStills > 1) return usage('--keep-stills is given twice');
717
+ const f = flags(
718
+ args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills'),
719
+ ['--rig', '--out'],
720
+ 'check',
721
+ ['--parts', '--source', '--page-edges', '--pack-shape', '--requirements'],
722
+ );
692
723
  if (typeof f === 'string') return usage(f);
693
724
  const rig = f.get('--rig') as string;
694
725
  const out = f.get('--out') as string;
@@ -698,7 +729,12 @@ function cmdCheck(args: string[]): number {
698
729
  if (!isPackShape(shape)) return usage(shape);
699
730
  try {
700
731
  const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
701
- const r = checkStage({ rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements') }, rigcRunner(bin), bin, console.log);
732
+ const r = checkStage(
733
+ { rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1 },
734
+ rigcRunner(bin),
735
+ bin,
736
+ console.log,
737
+ );
702
738
  return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
703
739
  } catch (err) {
704
740
  return printRefusal(err);
@@ -732,7 +768,7 @@ function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes:
732
768
  function cmdPropose(args: string[]): number {
733
769
  const flags = new Map<string, string>();
734
770
  let headBox = false;
735
- const valued = ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--canvas', '--keypoints', '--person'];
771
+ const valued = ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--canvas', '--keypoints', '--person', '--wrist-snap'];
736
772
  for (let i = 0; i < args.length; i++) {
737
773
  const flag = args[i];
738
774
  if (flag === '--head-box') {
@@ -775,6 +811,18 @@ function cmdPropose(args: string[]): number {
775
811
  if (out === undefined) return usage('propose needs --out <dir>');
776
812
  if (flags.has('--from-config') && flags.has('--compare')) return usage('--from-config and --compare are two modes; give one');
777
813
  if (flags.has('--person') && !flags.has('--keypoints')) return usage('--person names a person in the --keypoints file; give --keypoints <file>');
814
+ // Issue #199: a declared tolerance for a wrist on no handwear part. Whole px above the built-in one: the distance is
815
+ // counted in whole px, so a fraction would bind what its floor binds, and a value at or under the built-in binds nothing.
816
+ const snapArg = flags.get('--wrist-snap');
817
+ let wristSnap: number | null = null;
818
+ if (snapArg !== undefined) {
819
+ if (!flags.has('--keypoints')) return usage('--wrist-snap binds a wrist joint to a handwear part; give --keypoints <file>');
820
+ if (flags.has('--from-config')) return usage('--wrist-snap binds a wrist while proposing; --from-config proposes nothing');
821
+ if (!/^[0-9]+$/.test(snapArg) || Number(snapArg) <= WRIST_ON_PART_PX) {
822
+ return usage(`--wrist-snap ${snapArg} is not an integer above ${WRIST_ON_PART_PX}, the px within which a wrist already holds a part`);
823
+ }
824
+ wristSnap = Number(snapArg);
825
+ }
778
826
  const partsDir = existsSync(partsArg) && statSync(partsArg).isFile() ? dirname(partsArg) : partsArg;
779
827
  if (!existsSync(source)) {
780
828
  throw new PartsError([{ code: 'PROPOSE_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is drawn under the overlay' }]);
@@ -802,7 +850,7 @@ function cmdPropose(args: string[]): number {
802
850
  }
803
851
  const cmpPath = flags.get('--compare');
804
852
  const cfg = cmpPath === undefined ? null : loadConfig(cmpPath);
805
- const { proposal: prop, basis } = proposeWithBasis(P, joints);
853
+ const { proposal: prop, basis } = proposeWithBasis(P, joints, wristSnap);
806
854
  // Emit only after green: a proposal the config loader would refuse is not written, nor a basis record that does not hold to it.
807
855
  checkProposal(P, prop);
808
856
  const bfile = basisFile(P, prop, basis, cfg === null || cmpPath === undefined ? null : { where: cmpPath, config: cfg });
@@ -978,7 +1026,8 @@ async function cmdComfy(args: string[]): Promise<number> {
978
1026
  try {
979
1027
  // The config is a local file, so it is answered before any host is: a
980
1028
  // config comfy paint cannot paint from is refused with no box named.
981
- const cfg = sub === 'paint' ? loadEarlyConfig(configPath as string, 'paint') : null;
1029
+ const paint = sub === 'paint' ? loadPaintConfig(configPath as string) : null;
1030
+ const cfg = paint === null ? null : paint.config;
982
1031
  const host = resolveHost(v.get('--host'), process.env.COMFY_HOST);
983
1032
  const client = new ComfyClient(host, { poll: poll as number, request: 30 });
984
1033
  if (sub === 'seethrough') {
@@ -1017,7 +1066,7 @@ async function cmdComfy(args: string[]): Promise<number> {
1017
1066
  if (typeof seed0 === 'string') return usage(seed0);
1018
1067
  console.log(`rig-parts comfy paint: ${cfg.key} -> ${out}`);
1019
1068
  console.log(` ${seeds} seed(s) from ${seed0}${v.has('--seed0') ? '' : ' (generation.seed)'}; wait <= ${wait} s per seed, timeout ${timeout} s`);
1020
- const done = await runPainting(client, { config: cfg, out, seeds, seed0, wait: wait as number, timeout: timeout as number }, (l) => console.log(l));
1069
+ const done = await runPainting(client, { config: cfg, pose: paint === null ? null : paint.pose, out, seeds, seed0, wait: wait as number, timeout: timeout as number }, (l) => console.log(l));
1021
1070
  console.log(` wrote ${done.length} painting(s): ${done.map((d) => `${d.file} ${d.size[0]}x${d.size[1]} in ${d.elapsed.toFixed(1)} s`).join(', ')}; GPU job(s) ended`);
1022
1071
  return EXIT_OK;
1023
1072
  } catch (err) {
@@ -1105,7 +1154,7 @@ function cmdBuild(args: string[]): number {
1105
1154
  }
1106
1155
 
1107
1156
  function cmdCompose(args: string[]): number {
1108
- const f = flags(args, ['--scene', '--out'], 'compose', ['--requirements']);
1157
+ const f = flags(args, ['--scene', '--out'], 'compose', ['--requirements', '--accept']);
1109
1158
  if (typeof f === 'string') return usage(f);
1110
1159
  let bin: string;
1111
1160
  try {
@@ -1116,8 +1165,9 @@ function cmdCompose(args: string[]): number {
1116
1165
  const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-compose-'));
1117
1166
  try {
1118
1167
  const req = f.get('--requirements');
1168
+ const accept = f.get('--accept');
1119
1169
  const r = composeStage(
1120
- { scene: f.get('--scene') as string, out: f.get('--out') as string, ...(req === undefined ? {} : { requirements: req }) },
1170
+ { scene: f.get('--scene') as string, out: f.get('--out') as string, ...(req === undefined ? {} : { requirements: req }), ...(accept === undefined ? {} : { accept: accept.split(',') }) },
1121
1171
  { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'gate') },
1122
1172
  console.log,
1123
1173
  );