rig-parts 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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>] [--seam-pairs]
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.
@@ -226,7 +230,9 @@ usage:
226
230
  --seam-pairs, is split by the pair of parts each counted pixel lies
227
231
  between: one "seam pair" line each, worst first, with its count, frame
228
232
  and rig boxes and a picture under seam/, and check.json's seam_pairs
229
- (AUTHORING §7). Then six judgement lines, each in
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
230
236
  check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
231
237
  bars (AUTHORING §7): BREATH_VISIBLE (the topwear moves, the footwear does
232
238
  not, each rendered alone), BLINK_NO_HOLE (the setup pose with the blink
@@ -425,7 +431,7 @@ usage:
425
431
  rigc refuses the pair a control splits, and the RIG_RIGC_GREEN line
426
432
  names the flag on the command that ran.
427
433
 
428
- 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>...]]
429
435
  Bind several finished characters into one rig. The scene file (spec
430
436
  "${SCENE_SPEC}") states the canvas {width, height}, an optional plate
431
437
  {image, provenance: ${PROVENANCES.join(' | ')}, note?}, the characters
@@ -470,8 +476,14 @@ usage:
470
476
  TIP_OVER_ROOT, STILL_REGIONS_DARK and RECOMPOSITE_HOLES say SKIP and
471
477
  why), --requirements forwarded. Nothing is inferred: not the order, not
472
478
  an overlap, not a scale, not a person; the plate's provenance and the
473
- order are the author's and judged by nothing. Exit 0 when the check is
474
- 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.
475
487
 
476
488
  rig-parts --version
477
489
  rig-parts --help
@@ -697,11 +709,13 @@ function flags(args: string[], known: readonly string[], command: string, option
697
709
  }
698
710
 
699
711
  function cmdCheck(args: string[]): number {
700
- // The one switch check takes (issue #202): it carries no value, so it is taken out before the valued flags are read.
712
+ // The two switches check takes (issues #202, #209): they carry no value, so they are taken out before the valued flags are read.
701
713
  const seamPairs = args.filter((a) => a === '--seam-pairs').length;
702
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');
703
717
  const f = flags(
704
- args.filter((a) => a !== '--seam-pairs'),
718
+ args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills'),
705
719
  ['--rig', '--out'],
706
720
  'check',
707
721
  ['--parts', '--source', '--page-edges', '--pack-shape', '--requirements'],
@@ -716,7 +730,7 @@ function cmdCheck(args: string[]): number {
716
730
  try {
717
731
  const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
718
732
  const r = checkStage(
719
- { rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1 },
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 },
720
734
  rigcRunner(bin),
721
735
  bin,
722
736
  console.log,
@@ -754,7 +768,7 @@ function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes:
754
768
  function cmdPropose(args: string[]): number {
755
769
  const flags = new Map<string, string>();
756
770
  let headBox = false;
757
- 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'];
758
772
  for (let i = 0; i < args.length; i++) {
759
773
  const flag = args[i];
760
774
  if (flag === '--head-box') {
@@ -797,6 +811,18 @@ function cmdPropose(args: string[]): number {
797
811
  if (out === undefined) return usage('propose needs --out <dir>');
798
812
  if (flags.has('--from-config') && flags.has('--compare')) return usage('--from-config and --compare are two modes; give one');
799
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
+ }
800
826
  const partsDir = existsSync(partsArg) && statSync(partsArg).isFile() ? dirname(partsArg) : partsArg;
801
827
  if (!existsSync(source)) {
802
828
  throw new PartsError([{ code: 'PROPOSE_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is drawn under the overlay' }]);
@@ -824,7 +850,7 @@ function cmdPropose(args: string[]): number {
824
850
  }
825
851
  const cmpPath = flags.get('--compare');
826
852
  const cfg = cmpPath === undefined ? null : loadConfig(cmpPath);
827
- const { proposal: prop, basis } = proposeWithBasis(P, joints);
853
+ const { proposal: prop, basis } = proposeWithBasis(P, joints, wristSnap);
828
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.
829
855
  checkProposal(P, prop);
830
856
  const bfile = basisFile(P, prop, basis, cfg === null || cmpPath === undefined ? null : { where: cmpPath, config: cfg });
@@ -1000,7 +1026,8 @@ async function cmdComfy(args: string[]): Promise<number> {
1000
1026
  try {
1001
1027
  // The config is a local file, so it is answered before any host is: a
1002
1028
  // config comfy paint cannot paint from is refused with no box named.
1003
- 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;
1004
1031
  const host = resolveHost(v.get('--host'), process.env.COMFY_HOST);
1005
1032
  const client = new ComfyClient(host, { poll: poll as number, request: 30 });
1006
1033
  if (sub === 'seethrough') {
@@ -1039,7 +1066,7 @@ async function cmdComfy(args: string[]): Promise<number> {
1039
1066
  if (typeof seed0 === 'string') return usage(seed0);
1040
1067
  console.log(`rig-parts comfy paint: ${cfg.key} -> ${out}`);
1041
1068
  console.log(` ${seeds} seed(s) from ${seed0}${v.has('--seed0') ? '' : ' (generation.seed)'}; wait <= ${wait} s per seed, timeout ${timeout} s`);
1042
- 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));
1043
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`);
1044
1071
  return EXIT_OK;
1045
1072
  } catch (err) {
@@ -1127,7 +1154,7 @@ function cmdBuild(args: string[]): number {
1127
1154
  }
1128
1155
 
1129
1156
  function cmdCompose(args: string[]): number {
1130
- const f = flags(args, ['--scene', '--out'], 'compose', ['--requirements']);
1157
+ const f = flags(args, ['--scene', '--out'], 'compose', ['--requirements', '--accept']);
1131
1158
  if (typeof f === 'string') return usage(f);
1132
1159
  let bin: string;
1133
1160
  try {
@@ -1138,8 +1165,9 @@ function cmdCompose(args: string[]): number {
1138
1165
  const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-compose-'));
1139
1166
  try {
1140
1167
  const req = f.get('--requirements');
1168
+ const accept = f.get('--accept');
1141
1169
  const r = composeStage(
1142
- { 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(',') }) },
1143
1171
  { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'gate') },
1144
1172
  console.log,
1145
1173
  );
package/docs/AUTHORING.md CHANGED
@@ -75,6 +75,7 @@ read by no CPU stage).
75
75
  | --- | --- | --- |
76
76
  | `key` | every stage's report | authored; the examples use the directory name |
77
77
  | `generation.*` | the optional `comfy` adapter only; no CPU stage | recorded: checkpoint, LoRAs, sampler, prompt parts, `latent`, `seed`, `control` |
78
+ | `generation.control.skeleton` | `comfy paint` — the OpenPose control image, drawn at `latent` | **authored**: a built-in standing pose, `"stand_sides"` or `"stand_clasp"`, or (issue #198) any other string, read as a path relative to the config file's directory to a pose file `{"width": W, "height": H, "points": {"nose": [x, y], "neck": [x, y], …}}` — `width` x `height` is the canvas and must be `generation.latent` (the points are not scaled; `CONFIG_POSE_CANVAS`), and `points` holds the 18 body-18 keypoints of `src/skeleton.ts` by name (`nose`, `neck`, `r_shoulder`, `r_elbow`, `r_wrist`, `l_shoulder`, `l_elbow`, `l_wrist`, `r_hip`, `r_knee`, `r_ankle`, `l_hip`, `l_knee`, `l_ankle`, `r_eye`, `l_eye`, `r_ear`, `l_ear`; `r`/`l` the subject's sides), each `[x, y]` in canvas px, x right, y down, `0 <= x <= W`, `0 <= y <= H`. The image is drawn from the file exactly as from a built-in (same renderer, same size), written as `control_pose-<first 12 hex of its sha256>.png`. A pose file carries no words, so `generation.pose` is required beside it (`CONFIG_FIELD_PRESENT`). The file is read wherever the config is read from a path, every problem at once: no such file — a misspelt built-in lands here too — or one that cannot be read (`CONFIG_FILE_PRESENT`), not JSON (`CONFIG_IS_JSON`), not an object, a size that is not a whole positive px count, a point that is not two numbers (`CONFIG_FIELD_TYPE`), a key besides the three (`CONFIG_KEY_KNOWN`) or written twice (`CONFIG_KEY_UNIQUE`), `points` holding other than the 18 names (`CONFIG_POSE_KEYPOINTS`, with the count found and required), a point off the canvas (`CONFIG_POSE_IN_CANVAS`, naming the point and the canvas size) |
78
79
  | `seethrough.resolution` | assemble — each run's canvas must be `resolution` square (`ASSEMBLE_RUN_CANVAS`) | recorded from the See-through runs; 1024 in both examples |
79
80
  | `seethrough.steps`, `.seed`, `.offload` | no CPU stage | recorded from the runs |
80
81
  | `seethrough.head_box` | inputs (cuts the head image there; optional at the first call); assemble — where the head run sits on the painting | **proposed** by `propose --head-box` from the full run, square, source pixels, shifted inside the painting when needed; the head run must then be fed exactly that crop |
@@ -86,7 +87,7 @@ read by no CPU stage).
86
87
  | `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip}` 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 is never turned; one whose parent is a link is turned back upright |
87
88
  | `meshes.<part>` | rig | **proposed**, then corrected. Exactly one of `grid` (the lattice: cell size, px — what `propose` writes), `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 every mode: `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) 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 |
88
89
  | `meshes.<part>.contour` | rig | **authored**, optional (issue #84): `{tolerance, margin, spacing, budget?, stray?, regions?}`. `tolerance` — rig-c's Douglas–Peucker tolerance on the traced outline, px, 0 or more; `margin` — how far the silhouette grows before it is traced, px: 0, or 1 or more (a pixel joins when its centre lies within `margin` of an art pixel's centre; a value above 0 and below 1 adds nothing and is refused, `CONFIG_FIELD_TYPE`); `spacing` — the background interior spacing, px, above 0; `budget` — the most vertices the mesh may have (refused above it, nothing thinned); `stray` — the largest island, in art pixels, that may be left out of the mesh (absent: none is); `regions` — local deformation regions, each `{name, shape, bone, spacing, band}` with `shape` `"circle"` (`cx, cy, r`) or `"polygon"` (`points`, 3 or more): `bone` is the region's control bone (any bone `bones` declares), `spacing` the interior spacing inside the region and its band, `band` the width the bone's weight falls across. Every length and position is **rig px**, like every other in the config; the part image is the rig less `(x − 4, y − 4)` (its box less the pad), a translation, so a length is the same number in both. A region's `cx`, `cy`, `r`, `band` and `points` are multiples of 1/256 px (`CONFIG_FIELD_TYPE` otherwise); region names are unique per mesh (`CONFIG_REGION_NAME_UNIQUE`). The alpha threshold is not a field: art is alpha above 8, the lattice's and `check`'s |
89
- | `meshes.<part>.ribs` | rig | **authored**, optional (issue #188), in the `contour` and `auto` modes: `{<chain>: {stations}, …}`, each key a chain this mesh's `segments` name. Each link of the chain gets a row of vertices across the part at its origin and at `stations` points evenly between its origin and its end (`stations`, a whole number, 0 or more, required; a chain of L links carries L × (stations + 1) ribs) — so a strand can fold where its chain bends; the rule is "Ribs along a chain" under "A contour mesh". Absent places none and changes no byte. Refused: a lattice mesh with ribs (`CONFIG_RIBS_MODE` — its vertices are its grid's corners), a key that is not one of the mesh's chains (`CONFIG_NAME_RESOLVES`), an object naming no chain or a `stations` that is not a whole number from 0 (`CONFIG_FIELD_TYPE`); at the rig stage `CONTOUR_RIB_ART`, `CONTOUR_RIB_LINK`, `CONTOUR_RIB_CROSSING`, `CONTOUR_RIB_EDGE` (the contour refusals). The mesh's `mesh_report.json` row gains `ribs` — the declaration, the ribs placed and the vertices they hold — and its contour report (the source's, in `auto`) lists each rib's vertices |
90
+ | `meshes.<part>.ribs` | rig | **authored**, optional (issue #188), in the `contour` and `auto` modes: `{<chain>: {stations}, …}`, each key a chain this mesh's `segments` name. Each link of the chain gets a row of vertices across the part at its origin and at `stations` points evenly between its origin and its end (`stations`, a whole number, 0 or more, required; a chain of L links carries L × (stations + 1) ribs), each rib a row of vertices across the part, kept as a row of locked edges; the rule is "Ribs along a chain" under "A contour mesh". Absent places none and changes no byte. Refused: a lattice mesh with ribs (`CONFIG_RIBS_MODE` — its vertices are its grid's corners), a key that is not one of the mesh's chains (`CONFIG_NAME_RESOLVES`), an object naming no chain or a `stations` that is not a whole number from 0 (`CONFIG_FIELD_TYPE`); at the rig stage `CONTOUR_RIB_ART`, `CONTOUR_RIB_LINK`, `CONTOUR_RIB_CROSSING`, `CONTOUR_RIB_EDGE` (the contour refusals). The mesh's `mesh_report.json` row gains `ribs` — the declaration, the ribs placed and the vertices they hold — and its contour report (the source's, in `auto`) lists each rib's vertices |
90
91
  | `regions.<part>` | rig | **proposed**: the bone a rigid part rides. Every plan part is exactly one of a mesh or a region (`CONFIG_PART_ATTACHED`) |
91
92
  | `motion.duration` | rig; check (the loop is measured at this time) | proposed as 4 s; a whole number of 1/12 s ticks, because `check` renders at 12 fps |
92
93
  | `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`). A bone property is keyed by one track (`CONFIG_BONE_PROPERTY_KEYED_ONCE`): a chain keys `rotate` on every link, the blink's `eyes` group `scaley` and its `brows` group `translatey` on every member, so a single track on any of those is a second track on it — rigc would refuse it one stage later, `animation "idle" has two tracks on eye.scaley`. A chain link is turned along its chain (the `bones` row), so a `translatex`/`translatey` key on a bone whose parent is a turned link, or a `scale`/`shear` key on a turned link, would move or stretch along the link and not along the picture's axis: the rig stage refuses it (`RIG_KEY_FRAME_UNTURNED`); `rotate` is keyed on any bone |
@@ -151,7 +152,11 @@ swing, hair of another shape, whether an accessory swings — is yours to add, a
151
152
  **A posed figure: `propose --keypoints`.** The proposer's ratios assume a
152
153
  standing figure: the hip below the chest, sleeves hanging from the shoulders. A
153
154
  seated, reclining or otherwise posed figure is told where its joints are with one
154
- explicit file (issue #75, `src/keypoints.ts`):
155
+ explicit file (issue #75, `src/keypoints.ts`). A pose file named by
156
+ `generation.control.skeleton` is not that file and is not offered as its default:
157
+ the control skeleton is what was asked of the model, not where the joints ended up
158
+ (a consumer measured a generated picture not following the control's raised hand),
159
+ so `keypoints.json` is read off the generated picture:
155
160
 
156
161
  ```json
157
162
  {
@@ -186,9 +191,37 @@ What a joint with a position places, as given — no ratio is applied over it:
186
191
  | `neck` | `neck` | the rule: half way from the chin to the neck part's bottom (or 0.12 face heights below the chin), on the eye axis |
187
192
  | `hip` | the midpoint of `r_hip` and `l_hip` — a point between two joints, said as that | the hip rule (bottomwear top, the waist, or 0.32 of the figure); one hip alone is said to be unused |
188
193
  | `chest` | its rule, half way from neck to hip, taken along the line between the placed ends | the rule as before when neither end is a joint |
189
- | `sleeve_<s>` | `<s>_shoulder` → `<s>_elbow` (two links), tip at `<s>_wrist`, with the two-blob rule's first two amplitudes and its chest stub laid along the upper arm — when every handwear part over 500 px holds one such wrist within 15 px of its art | the sleeve rule places every sleeve chain, and each arm joint's note says why |
194
+ | `sleeve_<s>` | `<s>_shoulder` → `<s>_elbow` (two links), tip at `<s>_wrist`, with the two-blob rule's first two amplitudes and its chest stub laid along the upper arm — when every handwear part over 500 px holds one such wrist within 15 px of its art (the larger of the x and y gaps, the LINT's own tolerance), or is bound one by `--wrist-snap` (below) | the sleeve rule places every sleeve chain, and each arm joint's note says why: for a wrist on no part, `… lies on the art of no handwear part within 15 px, so it does not say which sleeve it ends; the nearest is <part>, <d> px from its art by the larger of the x and y gaps (--wrist-snap <d> would bind it)` — `(beyond --wrist-snap <px>)` when the flag was given and fell short, and `the nearest are <a> and <b>, each <d> px …, so no tolerance says which` when two parts tie |
190
195
  | `eye_<s>`, `head`, the face box | not moved: they are measured off the parts | the note prints the eye joint's distance from the eye bone, with no bar |
191
196
 
197
+ **`--wrist-snap <px>`** (issue #199), with `--keypoints` only, declares a tolerance
198
+ for a wrist that lies on no handwear part within the built-in 15 px — hands folded
199
+ together, or a sleeve painted over the hand. `<px>` is an integer above 15 (the
200
+ distance is counted in whole px, so a fraction would bind exactly what its floor
201
+ binds, and 15 or less binds nothing the built-in does not); anything else is refused
202
+ as a usage error naming the value, as is the flag without `--keypoints` or with
203
+ `--from-config`, which proposes nothing. Such a wrist is measured to every handwear
204
+ part's art by the larger of the x and y gaps — the measure of the built-in 15 px —
205
+ and bound to the one nearest part when it is at most `<px>` away; two parts tied
206
+ nearest bind neither. A bound wrist's joint lines say `(the wrist is bound to <part>
207
+ by --wrist-snap)`, and a note says which wrist went to which part and how far:
208
+
209
+ wrist snap: r_wrist at [x, y] lies on no handwear part within 15 px; --wrist-snap <px> bound it to <part>, the nearest, <d> px from its art by the larger of the x and y gaps
210
+
211
+ A wrist already on a part is not moved by the flag, and without the flag nothing
212
+ changes, byte for byte. Choosing the tolerance automatically is not done: the note
213
+ for an unbound wrist prints the distance that would have bound it.
214
+
215
+ **One handwear part holding both wrists** — two sleeves and the hands decomposed as
216
+ one piece, not clasped at the midline — gets both sleeve chains on its one mesh, as
217
+ before, and a note says so rather than leaving two chains to share the part
218
+ silently:
219
+
220
+ one handwear part holds both wrists: r_wrist and l_wrist both end on <part>, so chains sleeve_r and sleeve_l share its mesh and its art bends with both arms; to separate them, cut one sleeve out of <part> with an assemble.cuts entry (a polygon round it) and propose again
221
+
222
+ The `assemble.cuts` entry (§3; §5, *Cutting a piece out of a layer*) splits the part; the
223
+ proposer does not.
224
+
192
225
  Every other joint (`nose`, knees, ankles, ears; the eyes with no eyewhite) is named
193
226
  with its state in one note: no bone is authored from it. Every joint the proposer
194
227
  could use gets its own note — `used as given for bone X`, `used as the producer's
@@ -371,11 +404,10 @@ contour refusal in a run is listed together; nothing falls back to the lattice:
371
404
  | `CONTOUR_RIB_CROSSING` | two ribs cross, touch or share an end (both named, with their ends) | fewer `stations`, or a chain that bends less sharply inside a wide part |
372
405
  | `CONTOUR_RIB_EDGE` | a rib's consecutive vertices could not be joined by an edge (no edge across them could be flipped) — a guard: the rule keeps every other point off a rib | report it |
373
406
 
374
- **Ribs along a chain** (`meshes.<part>.ribs`, issue #188). A long thin part that hangs from a
375
- chain — a tie, a strand of hair, a scarf end — bends at the chain's joints, and a mesh with no
376
- row of vertices across the strand there cannot fold at the joint: the bend is taken up by
377
- whatever triangles happen to span it. For each chain `ribs` names, `src/contour.ts`
378
- (`placeRibs`) places, in part-image px:
407
+ **Ribs along a chain** (`meshes.<part>.ribs`, issue #188). A rib is a row of vertices across
408
+ the part at a joint of a chain the mesh is bound to, or at a station along one of its links;
409
+ what a rib does to how the part deforms is not claimed. For each chain `ribs` names,
410
+ `src/contour.ts` (`placeRibs`) places, in part-image px:
379
411
 
380
412
  - **where**: on each link k (from `points[k]` to `points[k + 1]`, the last to the tip) a rib at
381
413
  its origin and at `points[k] + m/(stations + 1) · (end − origin)`, m = 1..`stations`; the tip
@@ -525,7 +557,11 @@ unreduced source on the rig's idle and passes the author's bounds (step 4). The
525
557
  moves some of the bones the part is weighted to and not all of them (a key on their shared
526
558
  ancestor, or on the one bone a single-bone part binds, moves every vertex by one affine map and
527
559
  would read 0 over nothing). Constraints are not read for this: a part only a constraint moves
528
- is refused, never passed.
560
+ is refused, never passed — unless its author declares the motion, `stimulus` (issue #196,
561
+ the field table below and *A part only physics moves* after the physics recipe): each
562
+ declared bend is two more frames, every link of the chain turned by +`degrees` and by
563
+ −`degrees`, walked in both builds at time 0 beside the idle's, and its links count as keys
564
+ by the rule above.
529
565
  5. **The acceptance loop** (rig-c 2.24.0, rigc#1266 mechanism 2; `src/autoreplay.ts`) — only
530
566
  when step 4 refuses the full reduction on motion (`AUTO_MESH_MOTION`). rig-c reports every
531
567
  accepted **operation** of the call (`acceptedAt`: N entries `{ step, kind, count,
@@ -676,6 +712,7 @@ wall time goes to standard error, not into the table).
676
712
  | `boundaryRuns` | optional, `{ maxVertices }`, a whole number 2 or more, no default: each removal pass first tries to replace a run of 2 to `maxVertices` consecutive source-hull vertices with one chord, as one step held to every declared row (below) |
677
713
  | `retriangulate` | optional, `"delaunay"`: once the reduction ends, the kept vertices are re-triangulated by Delaunay flips, taken whole only when every declared row still passes (below) |
678
714
  | `removalOrder` | optional, `"deformation-load"`: the single removals are tried in ascending predicted load instead of ascending source index (below) |
715
+ | `stimulus` | optional (issue #196), `{ "chain", "degrees" }` or a non-empty list of them: a bend the motion gate measures the part against, for a part no idle key deforms (*A part only physics moves*, after the physics recipe). `chain` is a chain this mesh's `segments` name, each named once; `degrees` a number above 0. Each link of the chain is turned by +`degrees` and by −`degrees` about its joint — a `rotate` key on the link, so the chain curls — in two animations of duration 0 (`stimulus <chain> +<degrees>`, `stimulus <chain> -<degrees>`) that only the comparison's builds carry: the reference and a candidate built from the written rig with them added (`build --profile spine-html` without `--pack`, one more gated build per part); `rig/motion.json` never holds them. Each is walked at time 0, right after the physics reset, so the pose is the declared bend and no constraint has moved it; the idle is walked as before, so where idle keys also move the part both are measured, against the one `motion` bound. The acceptance loop's probes walk them too, as `selection` beside the grid frames. The row echoes `stimulus` (chain, degrees, links, the two animation names); absent, nothing is added and every byte is the same. A chain the segments do not name is `CONFIG_NAME_RESOLVES`, `degrees` that is not a number above 0 `CONFIG_FIELD_TYPE`, both at the field; an animation of `motion.animations_from` already called by one of the two names is `AUTO_MESH_MOTION_INPUT`. The amplitude (*The allocation rows*) is still derived from the idle alone |
679
716
 
680
717
  **A bound declared absent.** `maxOvershoot` and `maxUndercut`, in `sourceBounds` and in
681
718
  `targets.artFit`, each take a number of px 0 or more or `null`. `null` means *measured and
@@ -868,7 +905,7 @@ safe is the author's to choose here.
868
905
  | `AUTO_MESH_INPUT` | rig-c refused the call's input by throwing (a protected vertex the source does not have), in its words and code |
869
906
  | `AUTO_MESH_TERMINATION` | rig-c returned no mesh: `invalid-input` (the source fails its own `sourceBounds`, a region it refuses, protected influences over the cap), `unsupported-topology`, or `budget-exhausted` with `none-met-the-targets`; or, in the acceptance loop, a replay that does not end as rig-c's contract promises (`replayed-to-accepted-step` at the step asked, `candidatesTried` the full run's `acceptedAt[n − 1].step`, its `acceptedAt` the full run's first n operations, each the same `step`, `kind`, `count` and `sourceVertices`) |
870
907
  | `AUTO_MESH_ACCEPTED` | rig-c returned a mesh that is not accepted; the detail names every declared row not passing and the constraint that stopped it |
871
- | `AUTO_MESH_NO_STIMULUS` | the idle keys no bone that moves some of the part's bound bones against the others, so no frame deforms it — missing stimulus is not a pass; put the part in `contour` or `grid` mode, or key a bone it binds |
908
+ | `AUTO_MESH_NO_STIMULUS` | neither the idle nor a declared `stimulus` moves some of the part's bound bones against the others, so no frame deforms it — the detail names the bones with no motion against each other and the field to declare; missing stimulus is not a pass; declare `meshes.<part>.auto.stimulus`, put the part in `contour` or `grid` mode, or key a bone it binds |
872
909
  | `AUTO_MESH_MOTION` | the reduced mesh against its source on the idle is not accepted and the acceptance loop wrote no step in its place: "no reduction passes the motion bound" (the full result's failing rows and every probe named; under `motion.selection`, "… among the tested candidates", with `maxProbes` and the termination), or the chosen step passes on the grid frames and not on the whole idle (its selection and held-out values and worst frames named); when the source itself is not accepted against itself, the full result's rows as before |
873
910
  | `AUTO_MESH_SELECTION_BUDGET` | `motion.selection.maxProbes` is below ⌈log₂(N − I)⌉, the bisection the multi-interval search runs whole first; N, I and the number required named; nothing replayed |
874
911
  | `AUTO_MESH_SELECTION_ROLES` | a multi-interval choice whose probes read a frame the final comparison holds out, or one it does not declare `selection`; the frames named, the held-out claim withdrawn |
@@ -993,6 +1030,33 @@ into the landing), and both overshoot after it, 3.38 and 1.38 rig px (frames 21
993
1030
  `CHECK_TEXTURE_STRETCH: mesh "bottomwear" triangle 13 (vertices 5 183 6), edge 6-5, idle frame 13 — the edge is 0.233 times its rest length (max(ratio, 1/ratio) 4.29); <= 1.926544 is required`
994
1031
  and `topwear` 2.597; with the values tracked now it reads 1.697 (`topwear`) and 1.566
995
1032
  (`bottomwear`). Tune the constraint against the animation that drives it, not the idle.
1033
+
1034
+ #### A part only physics moves, in the automatic mode
1035
+
1036
+ A tie, a strand or a hem whose chain only a `physics` constraint turns has no idle key that
1037
+ deforms it, so `auto` refuses it, `AUTO_MESH_NO_STIMULUS`, naming the bones with no motion and
1038
+ the field: the motion gate measures against motion, and the package derives none from a
1039
+ constraint. Declare the bend in `meshes.<part>.auto.stimulus` (issue #196, the field table in
1040
+ *An automatic mesh*):
1041
+
1042
+ ```jsonc
1043
+ "scarf": { "auto": { …, "motion": { "maxLocalDeformation": 1 },
1044
+ "stimulus": { "chain": "robe_r", "degrees": 9 } },
1045
+ "r": 6, "segments": [ "robe_r" ] }
1046
+ ```
1047
+
1048
+ Choose `degrees` as the largest bend, per link, that the physics is expected to reach in the
1049
+ animations that drive it: the mesh is held only to what was declared, so a smaller bend passes a
1050
+ mesh that tears at the real one, and nothing here checks the declaration against the
1051
+ constraint. The recipe for a thin strand only physics moves is therefore two steps: put its
1052
+ chain on the strand's own art, and declare the stimulus. The chain's position decides whether
1053
+ the motion bound can be held at all: on the scarf strap cut out of the sweater, with the chain
1054
+ on the strap (the tracked example's), the automatic mesh passes at the default
1055
+ `maxLocalDeformation` 1 with 68 vertices (the contour mesh's 133); with the chain beside the
1056
+ strap, as the proposal first placed it, the same part is refused `AUTO_MESH_MOTION` at 1 and
1057
+ passes only at 3, with its hull alone (31 vertices) — a hull cannot hold a bend the chain does
1058
+ not run through. Raising the bound is the author's choice, not the package's; moving the chain
1059
+ is the fix.
996
1060
  - **An added animation can move the idle's figures.** The render's viewport is fitted over
997
1061
  every animation, and the idle frames share it, so an animation that travels further than the
998
1062
  idle redraws the idle at another size: adding `hop` to `sample` narrowed the idle render from
@@ -1134,7 +1198,7 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
1134
1198
  | `layers` | the table, 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*) |
1135
1199
  | `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 |
1136
1200
  | `assemble` | one line per part, 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, …)`; and look at `render/recomposite_error_rig.png`. Between the part lines and the totals, 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 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: `sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `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 (the 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. When neither run holds it at all, an `assemble.patches` entry cuts it from the painting (below) |
1137
- | `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 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 `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; `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 |
1201
+ | `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 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 `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 — cut the part with `assemble.cuts` to separate them; `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 |
1138
1202
  | `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 |
1139
1203
  | `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` |
1140
1204
  | `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, `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the nine bars measured and names the ones that said SKIP | `check: PASS; 9 of 9 bar(s) measured, 0 skipped` on the examples, and 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) |
@@ -1553,6 +1617,7 @@ See-through with another seed.
1553
1617
  | `COMFY_HISTORY_OUTPUTS`, `COMFY_VIEW_PRESENT`, `COMFY_MANIFEST_NAMED`, `COMFY_MANIFEST_IS_THIS_RUN`, `COMFY_LAYER_NAME` | the job's outputs are missing, unreadable, belong to another run, or name a layer that is not a plain file name | the wrapper's version on the box; report it |
1554
1618
  | `COMFY_IMAGE_PRESENT`, `COMFY_OUT_EMPTY`, `COMFY_OUT_FREE` | no `--image`, an `--out` that already holds files, or a seed whose painting is already on disk | `--image`, `--out`, `--seed0` |
1555
1619
  | `CONFIG_FIELD_PRESENT` on `config.generation`, `COMFY_PAINTING_SIZE` | `comfy paint` was given a config with no `generation` block (the line names the fields it holds), or the painting that came back is not twice the latent | `config.generation` |
1620
+ | `CONFIG_POSE_CANVAS`, `CONFIG_POSE_KEYPOINTS`, `CONFIG_POSE_IN_CANVAS` | the pose file `generation.control.skeleton` names states another canvas than `generation.latent`, holds other than the 18 body-18 keypoints, or puts one off its canvas | the pose file (§3, the `generation.control.skeleton` row) |
1556
1621
 
1557
1622
  ### assemble
1558
1623
 
@@ -1656,7 +1721,8 @@ and stay absent).
1656
1721
  | `SCENE_ID_UNIQUE`, `SCENE_ID_FORM` | an `id` repeated, empty, or holding `:` — the separator of a prefixed name — or `/`, `\` or a leading `.`, because an id begins every image file of its character | another id |
1657
1722
  | `SCENE_PLATE_PROVENANCE`, `SCENE_PLATE_PRESENT`, `SCENE_PLATE_SIZE` | the plate's `provenance` is not `observed`, `generated` or `unknown`; its image is missing or not a PNG; it is not the canvas's size (it is drawn at canvas (0, 0) and never scaled) | the plate |
1658
1723
  | `SCENE_BUILD_PRESENT` | a build directory, or a file `build` writes in it (`parts.json`, `rig/rig.json`, `rig/motion.json` with an `idle`, an image its skin names, `check/check.json`), is missing | run `build` for that character |
1659
- | `SCENE_BUILD_GREEN` | the build's own `check/check.json` does not say `PASS` and `gate_spine_html_green` | finish that character first: compose binds finished characters |
1724
+ | `SCENE_BUILD_GREEN` | the build's own `check/check.json` does not say `PASS` and `gate_spine_html_green`; under an accept list (`--accept`, `characters[].accept`), it failed a bar the list does not name, its gate is red, a declared requirement is not PASS, or it names no failing bar — the bars not met and the ones accepted are listed | finish that character first: compose binds finished characters. To look at the scene meanwhile, name the failing bars (§7, *Composing several characters*) |
1725
+ | `SCENE_ACCEPT_KNOWN` | an `--accept` name, or one in a character's `accept`, is not a bar `check` holds a build to: a problem code (`CHECK_LOOP_CLOSES`) where the bar's name (`loop`) belongs, a misspelling, or `gate`, which is rig-c's and never accepted | one of the names the message lists |
1660
1726
  | `SCENE_BUILD_FIELD_KNOWN` | a build's `rig.json` or `motion.json` carries a key compose does not know how to prefix or place, or a root with a transform of its own | a rig `build` wrote; a rig written by something else is `check`'s to measure (§7, *Measuring a rig rig-parts did not assemble*), not compose's to merge |
1661
1727
  | `SCENE_RIG_SCALE_AGREES` | the characters' `parts.json` `scale_rig_per_source` differ (each named) | one `assemble.rig_scale` for every character; `characters[].scale` sizes a placed character, it does not make two `rig_scale`s one |
1662
1728
  | `SCENE_SCALE_EXACT` | a character at a `scale` other than 1 carries something no per-character value can scale: a physics constraint with a nonzero `wind` or `gravity` (Spine applies both through the skeleton's `referenceScale`, which the composed skeleton holds once for every character), a track whose `property` compose does not know to be a length or not, or a translate key whose `v` is neither a list of numbers nor a map of them | `scale` 1 for that character, or rebuild it at another `assemble.rig_scale` |
@@ -1719,7 +1785,12 @@ assemble*, at the end of this section):
1719
1785
  line runs along the scarf's red edge [observed, issue #202]. A wider band moves
1720
1786
  neither; the polygon's edge does — run it through art of one colour on both
1721
1787
  sides, in both copies, which `rig/parts/<from>.png` and `rig/parts/<into>.png`
1722
- show pixel for pixel;
1788
+ show pixel for pixel. The composite meets the frame pixel centre to pixel
1789
+ centre: rig pixel index u lands on frame index s·u + t + 0.5·(s − 1), `s` the
1790
+ scale the `seam:` line prints (issue #209; the reference implementation's map
1791
+ lacks the last term, so the two agree only at s = 1). `check --keep-stills`
1792
+ leaves that frame — rigc's render directory, its `frames.json` (viewport,
1793
+ background) beside the PNG — under `check/setup_still/`;
1723
1794
  - **loop**: idle frame 0 against the frame at `t = duration`: max |d| exactly 0. The bar
1724
1795
  measures the keys (issue #183): a rig that declares a physics constraint — a `rig.json`
1725
1796
  `constraints` entry of `type` `"physics"`, or an entry of `motion.json`'s `physics`
@@ -2036,7 +2107,7 @@ rig directory, copied away from its `parts.json`, reads `4 of 9`: the gate, the
2036
2107
 
2037
2108
  ### Composing several characters (`compose`, issue #74)
2038
2109
 
2039
- `rig-parts compose --scene <scene.json> --out <dir> [--requirements <file.json>]`
2110
+ `rig-parts compose --scene <scene.json> --out <dir> [--requirements <file.json>] [--accept <bar>[,<bar>…]]`
2040
2111
  binds characters this package already built into one rig. Each character runs the
2041
2112
  single-character path (§4, steps 0–9) on its own, already isolated input to a green
2042
2113
  `build` first; compose reads what that build wrote and changes nothing in it.
@@ -2059,6 +2130,7 @@ single-character path (§4, steps 0–9) on its own, already isolated input to a
2059
2130
  | `characters[].build` | a `build --out` directory (relative to the scene file) whose own `check/check.json` is green |
2060
2131
  | `characters[].offset` | `[x, y]`, a translation from the build's rig px to canvas px. Every character is built at one `assemble.rig_scale`, so its rig px are canvas px at scale 1 |
2061
2132
  | `characters[].scale` | optional, a positive number; absent is 1, and 1 composes to the bytes a scene without the key writes. The character's size on the canvas, about its stage's top-left corner: a rig px `p` lands at canvas px `scale * p + offset` (issue #197). A character scaled up past its art is drawn soft — the art is not resampled; rebuild it at a higher `assemble.rig_scale` if that matters |
2133
+ | `characters[].accept` | optional, a list of bar names: the bars this character's own check may have failed and still be composed (issue #203). `compose --accept <bar>[,<bar>…]` is the same list for every character; the two are joined, so the command line applies to every character and a character's list adds to it. Each name is one of the bars `check`'s summary names, less the gate — `loop`, `seam`, `BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`; anything else is `SCENE_ACCEPT_KNOWN`. A build is taken when every bar its `check.json` failed is named; its gate red, a declared requirement not PASS, or a bar not named is `SCENE_BUILD_GREEN` as before. **It is for looking, not for shipping:** the `[compose]` line says which failing bars were accepted, `scene.json` records them as the character's `accepted`, the composed rig's own check measures them again and reports them as it finds them, and the run exits 1 even when that check is PASS (a composed rig has no `parts.json`, so the seam and four judgement lines say SKIP there). A list that accepts nothing — every build green — composes to the bytes a scene without one writes |
2062
2134
  | `order` | the draw order, back to front: a character id (its slots not named elsewhere, in its own order) or one `<id>:<slot>`. Every slot of every character is drawn exactly once. Interleaving is written by naming slots: `["a", "b:arm_l", "b"]` draws `b`'s left arm between `a` and the rest of `b` |
2063
2135
 
2064
2136
  What is composed:
@@ -2128,7 +2200,8 @@ What is composed:
2128
2200
  image, provenance, note, bone, slot, size, `judged_by: "nothing"`), per character
2129
2201
  its build, offset, `scale` (only when it is not 1), `rig_size`, `rig_scale`, placed bounds (the union of its part
2130
2202
  boxes, canvas px), shift and what it moved (`shifted`: the first-level bones, the
2131
- regions on `root`, the count of weights bound to `root`), and its counts, and the
2203
+ regions on `root`, the count of weights bound to `root`), its counts and `accepted`
2204
+ (the failing bars of its own check that an accept list took, only when there are any), and the
2132
2205
  order —
2133
2206
  as declared and as the slot list it expands to, `judged_by: "nothing"`.
2134
2207
 
package/docs/STABILITY.md CHANGED
@@ -52,13 +52,13 @@ defaults are the help's too, and none is guessed from the art.
52
52
  | `comfy` | `seethrough`: `--image`, `--out`; optional `--resolution`, `--steps`, `--seed`, `--prefix`, `--offload`, `--lama`, `--nf4`, `--host`, `--wait`, `--timeout`, `--poll` |
53
53
  | `assemble` | `--propose-plan` with `--source`, `--full`, `--head`, `--config`; or `--source`, `--full`, `--head`, `--config`, `--out`, optional `--seam` (near-white, silhouette), `--project` (core, visible) |
54
54
  | `propose` | `--head-box` with `--full`, `--canvas` |
55
- | `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` (with `--keypoints`) |
55
+ | `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`) |
56
56
  | `compare` | `--left`, `--right`; optional `--map` |
57
57
  | `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
58
- | `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs` |
58
+ | `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills` |
59
59
  | `loop` | `--frames`, `--out`; optional `--palette` |
60
60
  | `build` | `--config`, `--source`, `--full`, `--head`, `--out`; optional `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--requirements`, `--idle-keys` |
61
- | `compose` | `--scene`, `--out`; optional `--requirements` |
61
+ | `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
62
62
  | (none) | `--version`, `--help` |
63
63
  <!-- /stable:commands -->
64
64
 
@@ -104,6 +104,7 @@ part name, `[i]` an entry of a list):
104
104
  - **generation.loras[i]** — required: `name`, `strength`; optional: `strength_clip`
105
105
  - **generation.sampler** — required: `steps`, `cfg`, `sampler`, `scheduler`
106
106
  - **generation.control** — required: `skeleton`, `strength`, `end_percent`; optional: `model`
107
+ - **generation.control.skeleton**, the pose file it names when it is no built-in — required: `width`, `height`, `points`
107
108
  - **seethrough** — required: `resolution`, `steps`, `seed`, `offload`; optional: `head_box`
108
109
  - **assemble** — required: `rig_scale`, `plan` (from plain `assemble` on); optional: `extend_below_crop`, `patches`, `cuts`
109
110
  - **assemble.extend_below_crop[i]** — required: `part`, `run`, `tag`
@@ -117,7 +118,7 @@ part name, `[i]` an entry of a list):
117
118
  - **meshes.&lt;part&gt;.ribs.&lt;chain&gt;** — required: `stations`
118
119
  - **meshes.&lt;part&gt;.contour** — required: `tolerance`, `margin`, `spacing`; optional: `budget`, `stray`, `regions`
119
120
  - **meshes.&lt;part&gt;.contour.regions[i]** — required: `name`, `shape`, `bone`, `spacing`, `band`, and `cx`, `cy`, `r` for a circle or `points` for a polygon
120
- - **meshes.&lt;part&gt;.auto** — required: `source`, `sourceBounds`, `targets`, `influences`, `budget`, `minArtSamples`, `motion`; optional: `protect`, `regions`, and the three Stage B opt-ins `boundaryRuns`, `retriangulate`, `removalOrder`
121
+ - **meshes.&lt;part&gt;.auto** — required: `source`, `sourceBounds`, `targets`, `influences`, `budget`, `minArtSamples`, `motion`; optional: `protect`, `regions`, the three Stage B opt-ins `boundaryRuns`, `retriangulate`, `removalOrder`, and `stimulus`
121
122
  - **meshes.&lt;part&gt;.auto.source** — required: `tolerance`, `margin`, `spacing`; optional: `stray`
122
123
  - **meshes.&lt;part&gt;.auto.sourceBounds** and **meshes.&lt;part&gt;.auto.targets.artFit** — required: `minCoverage`, `maxOvershoot`, `maxUndercut` (each of the last two a number or null, the bound declared absent)
123
124
  - **meshes.&lt;part&gt;.auto.targets** — required: `artFit`, `maxBoundaryDeviation`; optional: `minAngle`
@@ -129,6 +130,7 @@ part name, `[i]` an entry of a list):
129
130
  - **meshes.&lt;part&gt;.auto.protect** — optional: `hull`, `vertices`, `edges`, `regionBoundaries`, `weightJump`, `influences`
130
131
  - **meshes.&lt;part&gt;.auto.regions[i]** — required: `name`, `shape`, `maxEdgeLength`, `transition`, `grade`, `minArtSamples`, and `cx`, `cy`, `r` for a circle or `points` for a polygon; optional: `bone`, `band` (both or neither; with neither the region is density only)
131
132
  - **meshes.&lt;part&gt;.auto.boundaryRuns** — required: `maxVertices`
133
+ - **meshes.&lt;part&gt;.auto.stimulus**, or each entry of it when a list — required: `chain`, `degrees`
132
134
  - **regions.&lt;part&gt;** — a bone name
133
135
  - **motion** — required: `duration`, `tracks`; optional: `blink`, `animations_from`
134
136
  - **motion.tracks[i]**, a single track — required: `bone`, `prop`, `amp`, `period`, `phase`; optional: `base`
@@ -156,7 +158,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
156
158
  | `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from`, 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`, `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite` |
157
159
  | `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 |
158
160
  | `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions`; 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`); `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) |
159
- | `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`, `loop_max_diff`, the seam figures), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `RECOMPOSITE_HOLES`, `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
161
+ | `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`, `loop_max_diff`, the seam figures), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `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`), `RECOMPOSITE_HOLES`, `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
160
162
  | `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections and `notes` |
161
163
  | `basis.json` | `propose` | spec `spine-parts-basis/1` |
162
164
  | the scene report | `compose` | spec `spine-parts-scene-report/1` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-parts",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
5
5
  "type": "module",
6
6
  "bin": {