rig-parts 1.86.0 → 1.88.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/cli.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  import { applyTakenLayers } from './src/takelayers.ts';
3
+ import { applyDepth, prepareDepth } from './src/depthworkflow.ts';
3
4
  /**
4
5
  * rig-parts — one painting and its See-through layers in, Spine-ready parts
5
6
  * and rig specs out, verified through rig-c.
@@ -67,6 +68,16 @@ const LATER: ReadonlyArray<[string, string]> = [];
67
68
  const HELP = `rig-parts ${version()} — Spine-ready parts from one painting and its See-through layers
68
69
 
69
70
  usage:
71
+ rig-parts depth-prepare --parts <assembled-dir> --config <depth-recipe.json> --out <empty-dir>
72
+ Write per-part inference PNGs on neutral gray, upscaled and padded as
73
+ declared, plus a source-hashed manifest. No model is bundled or called.
74
+
75
+ rig-parts depth-apply --parts <assembled-dir> --manifest <manifest.json> --config <depth-recipe.json> --rig-config <config.json> --out <empty-dir>
76
+ Register floating-point JSON depth or signed normals, normalize inside
77
+ alpha >=128, apply declared median/relief and group near/zScale/pivot,
78
+ extend past alpha, and write parts, relocated config and depth-report.json.
79
+ Normals require one closed convex island. See AUTHORING for the schemas.
80
+
70
81
  rig-parts layers <dir | layers.json | file.psd>
71
82
  Read a See-through decomposition — the ComfyUI wrapper form (a directory
72
83
  holding layers.json and one PNG per layer) or an upstream .psd — and print
@@ -544,7 +555,8 @@ usage:
544
555
  [--page-edges pot|free] [--pack-shape rect|polygon]
545
556
  [--requirements <file.json>] [--seam-pairs] [--keep-stills]
546
557
  [--set <name> | --all-sets] [--environment <schedule.json>]
547
- [--wind-band <spec.json>]
558
+ [--wind-band <spec.json>] [--json]
559
+ --json prints the completed check.json object only, with unchanged exit codes.
548
560
  Build, gate, render and measure a rig through rig-c's CLI (the rigc at
549
561
  node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
550
562
  a rig spec, which rigc build compiles, not a compiled skeleton.json.
@@ -766,7 +778,7 @@ usage:
766
778
  extends below a crop (ASSEMBLE_SINGLE_RUN_PLAN), and the config sets no
767
779
  seethrough.head_box (ASSEMBLE_SINGLE_RUN_HEAD_BOX).
768
780
  Writes nothing unless every check passed.
769
- --seam defaults to ${DEFAULT_SEAM_RULE}. --project says where a layer takes
781
+ --seam overrides config.assemble.seam, otherwise ${DEFAULT_SEAM_RULE}. --project says where a layer takes
770
782
  the painting's pixel: core (the reference's) erodes every layer's top-most
771
783
  opaque area by 5x5 first, so a part a few pixels wide takes none; visible
772
784
  erodes only along a rim where a later layer is in front. It defaults to
@@ -850,7 +862,7 @@ usage:
850
862
  green build ends with the pack line beside the spineboy yardstick and the
851
863
  three artifact paths — skeleton .json, .atlas and the packed page: the
852
864
  packed atlas is the result, the loose parts are the intermediate it was
853
- made from. --seam defaults to ${DEFAULT_SEAM_RULE}, --project to
865
+ made from. --seam overrides config.assemble.seam, otherwise ${DEFAULT_SEAM_RULE}; --project to
854
866
  ${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES} and
855
867
  --pack-shape to ${DEFAULT_PACK_SHAPE} (as for check; both packed builds, rig's gate
856
868
  and check's artifact, take them). --requirements is forwarded to check
@@ -1077,12 +1089,14 @@ function printLayerTable(set: LayerSet): void {
1077
1089
  * no row; `layers` takes one positional path beside its flags (issue #359).
1078
1090
  */
1079
1091
  const TAKES = {
1092
+ 'depth-prepare': { valued: ['--parts', '--config', '--out'], switches: [] },
1093
+ 'depth-apply': { valued: ['--parts', '--manifest', '--config', '--rig-config', '--out'], switches: [] },
1080
1094
  layers: { valued: ['--export', '--template', '--canvas', '--full', '--source', '--config'], switches: [] },
1081
1095
  sheet: { valued: ['--source', '--layers', '--out', '--cell', '--cols'], switches: [] },
1082
1096
  rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
1083
1097
  propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule', '--cloth', '--write-config', '--from'], switches: ['--head-box', '--no-back-hair', '--only-missing'] },
1084
1098
  compare: { valued: ['--left', '--right', '--map'], switches: [] },
1085
- check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment', '--wind-band'], switches: ['--seam-pairs', '--keep-stills', '--all-sets', '--no-extremes'] },
1099
+ check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment', '--wind-band'], switches: ['--seam-pairs', '--keep-stills', '--all-sets', '--no-extremes', '--json'] },
1086
1100
  loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
1087
1101
  assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config', '--requirements'], switches: ['--propose-plan'] },
1088
1102
  'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll', '--config', '--depth-run'], switches: ['--offload', '--no-offload', '--lama', '--nf4'] },
@@ -1325,6 +1339,8 @@ function flags(args: string[], command: Command, required: readonly string[]): M
1325
1339
 
1326
1340
  function cmdCheck(args: string[]): number {
1327
1341
  // The switches check takes (issues #202, #209, #454): they carry no value, so they are taken out before the valued flags are read.
1342
+ const json = args.filter((a) => a === '--json').length;
1343
+ if (json > 1) return usage('--json is given twice');
1328
1344
  const seamPairs = args.filter((a) => a === '--seam-pairs').length;
1329
1345
  if (seamPairs > 1) return usage('--seam-pairs is given twice');
1330
1346
  const keepStills = args.filter((a) => a === '--keep-stills').length;
@@ -1334,7 +1350,7 @@ function cmdCheck(args: string[]): number {
1334
1350
  const allSets = args.filter((a) => a === '--all-sets').length;
1335
1351
  if (allSets > 1) return usage('--all-sets is given twice');
1336
1352
  const f = flags(
1337
- args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills' && a !== '--all-sets' && a !== '--no-extremes'),
1353
+ args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills' && a !== '--all-sets' && a !== '--no-extremes' && a !== '--json'),
1338
1354
  'check',
1339
1355
  ['--rig', '--out'],
1340
1356
  );
@@ -1352,8 +1368,9 @@ function cmdCheck(args: string[]): number {
1352
1368
  { rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1, noExtremes: noExtremes === 1, ...(f.has('--set') ? { set: f.get('--set') as string } : {}), allSets: allSets === 1, ...(f.has('--environment') ? { environment: f.get('--environment') as string } : {}), ...(f.has('--wind-band') ? { windBand: f.get('--wind-band') as string } : {}) },
1353
1369
  rigcRunner(bin),
1354
1370
  bin,
1355
- console.log,
1371
+ json === 1 ? () => {} : console.log,
1356
1372
  );
1373
+ if (json === 1) process.stdout.write(`${JSON.stringify(r.figures, null, 1)}\n`);
1357
1374
  return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
1358
1375
  } catch (err) {
1359
1376
  return printRefusal(err);
@@ -1713,8 +1730,8 @@ function cmdAssemble(args: string[]): number {
1713
1730
  if (propose && (flags.has('--out') || flags.has('--seam') || flags.has('--project') || alts.length > 0)) return usage('--propose-plan prints to the console; it takes none of --out, --seam, --project, --alt');
1714
1731
  if (!propose && flags.has('--write-config')) return usage('--write-config belongs to assemble --propose-plan');
1715
1732
  if (!propose && !flags.has('--out')) return usage('assemble needs --out <dir>');
1716
- const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
1717
- if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
1733
+ const seam = flags.get('--seam');
1734
+ if (seam !== undefined && !(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
1718
1735
  const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
1719
1736
  if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
1720
1737
  const [source, full, config] = ['--source', '--full', '--config'].map((f) => flags.get(f) as string);
@@ -1742,7 +1759,7 @@ function cmdAssemble(args: string[]): number {
1742
1759
  }
1743
1760
  const out = flags.get('--out') as string;
1744
1761
  assembleStage(
1745
- { source, full, head, config, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), seam: seam as SeamRule, project: project as ProjectRule, ...(alts.length === 0 ? {} : { alts }) },
1762
+ { source, full, head, config, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), ...(seam === undefined ? {} : { seam: seam as SeamRule }), project: project as ProjectRule, ...(alts.length === 0 ? {} : { alts }) },
1746
1763
  { partsJson: join(out, 'rig', 'parts.json'), partsDir: join(out, 'rig', 'parts'), recomposite: join(out, 'render', 'recomposite_rig.png'), errorMap: join(out, 'render', ERROR_MAP_FILE) },
1747
1764
  console.log,
1748
1765
  );
@@ -1933,8 +1950,8 @@ function cmdBuild(args: string[]): number {
1933
1950
  // --head is optional (issue #359), as for assemble.
1934
1951
  for (const f of ['--config', '--source', '--full', '--out']) if (!flags.has(f)) return usage(`build needs ${f}`);
1935
1952
  if (on.has('--all-sets') && flags.has('--set')) return usage('--set and --all-sets are two modes; give one');
1936
- const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
1937
- if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
1953
+ const seam = flags.get('--seam');
1954
+ if (seam !== undefined && !(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
1938
1955
  const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
1939
1956
  if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
1940
1957
  const edges = pageEdgesOf(flags.get('--page-edges'));
@@ -1958,7 +1975,7 @@ function cmdBuild(args: string[]): number {
1958
1975
  const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-build-'));
1959
1976
  try {
1960
1977
  const r = build(
1961
- { config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges, packShape: shape, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), idleKeys: keys, ...(leadText === undefined ? {} : { leadIn: Number(leadText) }), seamPairs: on.has('--seam-pairs'), keepStills: on.has('--keep-stills'), noExtremes: on.has('--no-extremes'), ...(alts.length === 0 ? {} : { alts }), ...(flags.has('--set') ? { set: flags.get('--set') as string } : {}), allSets: on.has('--all-sets'), ...(flags.has('--environment') ? { environment: flags.get('--environment') as string } : {}), ...(flags.has('--wind-band') ? { windBand: flags.get('--wind-band') as string } : {}) },
1978
+ { config, source, full, head, out, ...(seam === undefined ? {} : { seam: seam as SeamRule }), project: project as ProjectRule, loop, pageEdges: edges, packShape: shape, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), idleKeys: keys, ...(leadText === undefined ? {} : { leadIn: Number(leadText) }), seamPairs: on.has('--seam-pairs'), keepStills: on.has('--keep-stills'), noExtremes: on.has('--no-extremes'), ...(alts.length === 0 ? {} : { alts }), ...(flags.has('--set') ? { set: flags.get('--set') as string } : {}), allSets: on.has('--all-sets'), ...(flags.has('--environment') ? { environment: flags.get('--environment') as string } : {}), ...(flags.has('--wind-band') ? { windBand: flags.get('--wind-band') as string } : {}) },
1962
1979
  { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate'), stages: {
1963
1980
  assemble: (input, outputs) => runBuildProcess({ stage: 'assemble', bin, input, outputs }, join(scratch, 'workers')),
1964
1981
  rig: (input, _rigc, gateScratch) => runBuildProcess({ stage: 'rig', bin, input, scratch: gateScratch }, join(scratch, 'workers')),
@@ -2181,6 +2198,27 @@ function cmdPainting(args: string[]): number {
2181
2198
  }
2182
2199
  }
2183
2200
 
2201
+ function cmdDepthPrepare(args: string[]): number {
2202
+ const f = flags(args, 'depth-prepare', ['--parts', '--config', '--out']);
2203
+ if (typeof f === 'string') return usage(f);
2204
+ try {
2205
+ const result = prepareDepth({ parts: f.get('--parts')!, config: f.get('--config')!, out: f.get('--out')! });
2206
+ console.log(`depth-prepare: ${result.parts.length} part(s); manifest.json binds crop, padding and source/input hashes`);
2207
+ return EXIT_OK;
2208
+ } catch (error) { return printRefusal(error); }
2209
+ }
2210
+
2211
+ function cmdDepthApply(args: string[]): number {
2212
+ const f = flags(args, 'depth-apply', ['--parts', '--manifest', '--config', '--rig-config', '--out']);
2213
+ if (typeof f === 'string') return usage(f);
2214
+ try {
2215
+ const result = applyDepth({ parts: f.get('--parts')!, manifest: f.get('--manifest')!, config: f.get('--config')!, rigConfig: f.get('--rig-config')!, out: f.get('--out')! });
2216
+ for (const row of result.parts as Array<Record<string, unknown>>) console.log(`depth-apply: ${row.part} levels=${row.levels} clip=${row.clipFraction} reliefRigPixels=${row.reliefRigPixels} sign=${row.sign}`);
2217
+ console.log('depth-apply: wrote registered parts, config.json and depth-report.json; run rig for the core gate');
2218
+ return EXIT_OK;
2219
+ } catch (error) { return printRefusal(error); }
2220
+ }
2221
+
2184
2222
  function main(argv: string[]): number | Promise<number> {
2185
2223
  const [command, ...rest] = argv;
2186
2224
  if (command === undefined || command === '--help' || command === '-h' || command === 'help') {
@@ -2192,6 +2230,8 @@ function main(argv: string[]): number | Promise<number> {
2192
2230
  return EXIT_OK;
2193
2231
  }
2194
2232
  if (command === 'layers') return cmdLayers(rest);
2233
+ if (command === 'depth-prepare') return cmdDepthPrepare(rest);
2234
+ if (command === 'depth-apply') return cmdDepthApply(rest);
2195
2235
  if (command === 'sheet') return cmdSheet(rest);
2196
2236
  if (command === 'rig') return cmdRig(rest);
2197
2237
  if (command === 'propose') return cmdPropose(rest);
package/docs/AUTHORING.md CHANGED
@@ -147,6 +147,7 @@ read by no CPU stage).
147
147
  | `seethrough.steps`, `.seed`, `.offload` | `comfy seethrough --config` | run inputs when `--config <config.json>` is given; explicit CLI flags override them, including `--no-offload` for false |
148
148
  | `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. With one run (no `--head`, issue #359: an artist's own file, §4 *For artists*) it is left out — there is no crop to place — and a config that sets it is refused (`ASSEMBLE_SINGLE_RUN_HEAD_BOX`) |
149
149
  | `seethrough.arm_split` | assemble, `assemble --propose-plan` — separates a single full-run `handwear-r` or `handwear-l` layer that holds both overlapping arms into both arm tags | **authored**, optional (issue #439): `{x?: number, line?: [[x, y], …], order: ["handwear-r", "handwear-l"] \| ["handwear-l", "handwear-r"]}` in `seethrough.resolution` full-run pixels. Exactly one of `x` (image column, `0 <= x <= resolution`) or `line` (a polyline of two or more `[x, y]` points in `[0, resolution]`, interpolated in x along y and clamped above/below its endpoints) is required (`CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE`), and `order` is `[back_tag, front_tag]`, the draw order of the two separated arm layers (`CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE`). Pixels at `< splitX(y)` go to `handwear-r` (image left, the subject's right arm) and pixels at `>= splitX(y)` go to `handwear-l` (image right, the subject's left arm). A split line that cuts a horizontal opaque run of the arm layer into more than two segments is refused (`ASSEMBLE_ARM_SPLIT_CUTS_ARM`). Without the declaration, when one arm layer's 8-connected component reaches both wrists under `propose --keypoints`, `propose` emits an `overlapping-arms` `LINT` naming the part, both wrists and `seethrough.arm_split`; absent, no byte of any output moves |
150
+ | `assemble.seam` | assemble, build | **authored**, optional `"near-white"` or `"silhouette"` (issue #905). An explicit `--seam` overrides the config; otherwise this field overrides the existing `near-white` default. An invalid value is `CONFIG_FIELD_TYPE` at `config.assemble.seam`, including when a CLI override is given. Absent, existing build bytes are unchanged. |
150
151
  | `assemble.rig_scale` | assemble — the rig is `trunc(w × rig_scale)` x `trunc(h × rig_scale)`, and every rig-px value after it (`bones`, `meshes`, `motion`, `patches`, `cuts`) is in that rig's pixels; `check --source` (`CHECK_SOURCE_SIZE`) and `compose` (`SCENE_RIG_SCALE_AGREES`) read it back off `parts.json` | authored; rig pixels per source pixel. 0.5 in both examples (a 1664x2432 painting makes an 832x1216 rig). **Keep the figure about 1,100 to 1,200 rig px tall** — the `H` of `propose`'s `mesh spacing:` note, the height of the parts' boxes together — so for another painting, about 1,150 over the figure's height in painting px: that is the size the proposer's spacing divisor was calibrated at (H 1106–1185, the `mesh spacing` note says so) and the size of the two examples `check`'s bars are derived from, and the proposer and `check` also hold fixed rig-px values that do not scale with it (the fringe's 10 px step, the trunk segments' 18, 20, 30 and 100 px, the 15 px wrist tolerance, the 10-row pendant floor, the seam bar's 50 rig px over 40). What else it trades, measured on `demo` with the README loop from a fresh config at 0.25, 0.5 and 1 (issue #345, rig-c 2.47.1): the same plan and the same ten chains; every bar but the one the uncorrected `demo` always fails (`bottomwear` `TIP_OVER_ROOT`, the README loop's `RL01`) passing at all three; the atlas page 418x606, 832x1205 and 1825x2047; the figure's mesh spacing 16, 32 and 64, so 830, 868 and 932 triangles; the breath 2.5 % of a torso that scales with it; and the proposed bones, scaled to 0.5, off the 0.5 proposal's by up to 38 px at 0.25 and 22.5 px at 1 (the hairpin's origin; the skirt roots 20 and 10.5) — the fixed-px values above. `propose` records it on the proposal as `proposed_rig_scale` (issue #354), and changing `assemble.rig_scale` after the rig sections are written is refused (`CONFIG_PROPOSED_RIG_SCALE`): without that refusal `demo`'s tracked config built at 0.25 gated green and `check` failed only `TIP_OVER_ROOT` on both sleeves (`propose --from-config` named 54 `LINT` lines, links off the art) — re-run `assemble` and `propose` at the new scale. Absent: `CONFIG_FIELD_PRESENT`; 0, negative or not a number: `CONFIG_FIELD_TYPE`; a rig under 1x1: `ASSEMBLE_RIG_SIZE` |
151
152
  | `assemble.plan` | assemble; also the list every `meshes`/`regions` key must come from | **proposed** by `assemble --propose-plan`. `[part name, "full" \| "head", See-through tag]`, in draw order back to front. Names are free; roles come from the tag. A layer the author made and added to a run takes `author:<label>` in the tag's place, and has no role (§5, *A layer the author made*). When `generation.control.skeleton` is present and a full-run `handwear-r` or `handwear-l` layer merges sleeve, bare hand and overgarment drape into one layer — a skin-coloured hand cluster (`>= 40` run px, `GHOST_MIN_AREA`, measured `516..4,787` run px across the 9-standee corpus) lies within `0.35` forearm lengths of `<s>_wrist` and non-skin pixels reach more than `1.0x` the forearm length (`<s>_elbow -> <s>_wrist`) below `<s>_wrist` (issue #490; measured `-0.15x..0.45x` on the 6 clean standees vs `1.25x..2.02x` on the 3 merged standees, placing `1.0x` inside the `0.45x..1.25x` gap) — the layer is split into `drape-<s>`, `handwear-<s>` and `hand-<s>` (back to front) when the Euclidean RGB distance between the sleeve median colour above `<s>_wrist` and the drape median colour below `<s>_wrist` is at least `80` (`ARM_DRAPE_COLOR_DISTANCE`, measured `103.6..236.0` on the two distinct-colour merged standees vs `20.8..28.2` on the same-colour merged standee, placing `80` between `28.2` and `103.6`), or refused with `SEETHROUGH_ARM_RUN_MERGED` when the distance is under `80` |
152
153
  | `assemble.extend_below_crop` | assemble | **proposed** with the plan: `{part, run, tag}` — a head-run part that reaches the bottom of the head crop is continued from a full-run layer, whole connected components |
@@ -5233,6 +5234,7 @@ and stay absent).
5233
5234
  | <a id="RIG_TURN_SURFACE_MESH_BINDINGS"></a>`RIG_TURN_SURFACE_MESH_BINDINGS` | an authored surface mesh has unresolved/nonclosing weights or shares a frame moved by a unit/rear turn carrier; declared stop draw aliases resolve to their links for setup sampling | provide weighted bindings closing at 1 in the authored frame; `<link>_d` resolves only for links declared in `config.stops`; use independent chains for parts with different roles |
5234
5235
  | <a id="RIG_TURN_SURFACE_MESH_ATTACHMENT"></a>`RIG_TURN_SURFACE_MESH_ATTACHMENT` | a surface mesh has an alternate drawing without its own registered depth sampling frame | use the base attachment; explicit islands retain their own slot, attachment and bindings |
5235
5236
  | <a id="RIG_TURN_BODY_UNCOVERED"></a>`RIG_TURN_BODY_UNCOVERED` | **Warning:** a shown part without a body role overlaps a moved `turn.body` surface at setup alpha >= 128 and has different emitted endpoint displacement | declare the intended body role and inspect both endpoints; no role or depth is inferred |
5237
+ | <a id="RIG_TURN_HEAD_ROLL"></a>`RIG_TURN_HEAD_ROLL` | **Warning:** a declared head turn has an absolute authored-eye-origin roll above the empirical limit displayed as 18.485385 degrees; names the face/eye parts, both eye bones, signed roll and limit | inspect the neck at both extremes; this proxy does not detect the painting independently, correct tilt or change emitted bytes; see [Head roll before a declared turn](#head-roll-before-a-declared-turn) |
5236
5238
  | <a id="RIG_TURN_SURFACE_DEPTH_REQUIRED"></a>`RIG_TURN_SURFACE_DEPTH_REQUIRED` | a part with role `"surface"` in `config.turn.roles` or `config.turn.body.roles` carries no depth sheet in `parts.json` | run `assemble` with `config.depth` or provide `<part>_depth.png` in `parts.json` |
5237
5239
  | <a id="RIG_TURN_GRID_BUDGET"></a>`RIG_TURN_GRID_BUDGET` | a `"surface"` grid exceeds 2,400 triangles on one slot or 4,000 triangles across all `"surface"` slots | lower `config.turn.grid` / `config.turn.body.grid` `[cols, rows]` within the per-slot and total budgets |
5238
5240
  | <a id="RIG_TURN_HEAD_BONE"></a>`RIG_TURN_HEAD_BONE` | `buildRig` found no `"head"` (or body turn root) bone when emitting 2.5D turn carriers | declare `"head"` (or the body turn root bone) in `config.bones` |
@@ -5663,7 +5665,7 @@ Under `--source <painting.png>`, a second reported line, also never a FAIL
5663
5665
  | line | reports | bar | `reference-demo` | `reference-sample` | SKIP when |
5664
5666
  | --- | --- | --- | --- | --- | --- |
5665
5667
  | <a id="SETUP_POSE_VS_SOURCE"></a>`SETUP_POSE_VS_SOURCE` | the setup pose against the painting, by `assemble`'s own `measureRecomposite` over the stage's pixels — `mean_abs` and `within_share` (mean channel, within 8), `error_px` (max channel over 40), `uncovered_error_px` (of those, where the pose has alpha 128 or less), `hole_count` and the largest hole's box — with `painting_px`, `stage_px`, `render_px` and `render_scale` beside them. The painting is resampled onto the stage as assemble resamples it (lanczos3, alpha dropped). rigc renders onto an opaque grey, so the pose over white and its alpha are read off a second still of the same pose with every slot tinted black; both are carried onto the stage with the seam's map turned round (bilinear) | none — status `REPORTED`; assemble applies no bar to the figures it measures, so none is derived here | 3.146 / 93.43 % / 12,399 / 1,732, 518 holes, the largest 61 px at 351,157 6x20 (assemble's recomposite: 2.391 / 95.81 % / 11,050 / 1,564, 453) | 1.095 / 97.30 % / 5,348 / 1,239, 313 holes, the largest 88 px at 479,416 6x29 (assemble's: 0.835 / 97.95 % / 4,512 / 1,185, 259) | not written without `--source` |
5666
- | <a id="SETUP_MATCHES_PAINTING"></a>`SETUP_MATCHES_PAINTING` | declared region count, native box, limit and actual top contributor rows | PASS/FAIL against authored limits; no limit is SKIP; undeclared regions are not judged |
5668
+ | <a id="SETUP_MATCHES_PAINTING"></a>`SETUP_MATCHES_PAINTING` | declared region count, native box, limit and actual top contributor rows | PASS/FAIL against authored limits; no limit is SKIP; with `--source`, no declaration is SKIP with its reason and declaration remedy; undeclared regions are not judged |
5667
5669
 
5668
5670
  The figures are assemble's by construction, not a second definition beside them;
5669
5671
  what differs is what they are taken of — the pose rigc draws, not the flat stack —
@@ -6229,7 +6231,22 @@ maximum absolute RGB-channel difference is **strictly greater** than that
6229
6231
  value. The region passes when its total count is at most its authored
6230
6232
  nonnegative integer `max_pixels`. Without that limit the region reports
6231
6233
  **SKIP**, including a zero-count region. An undeclared region has no verdict.
6232
- Without `setup_painting`, existing output bytes and rendering remain unchanged.
6234
+ With `--source` but no `setup_painting` declaration, `SETUP_MATCHES_PAINTING`
6235
+ is explicitly **SKIP** in the console, `check.json`, the report's skipped-bar
6236
+ list and `check --json`. Its reason says the comparison was not measured and
6237
+ asks for `--requirements <file>` declaring `setup_painting.rgb_tolerance` and
6238
+ named `regions` with `box` and authored `max_pixels` limits. The SKIP changes
6239
+ no exit code and invents no metric, region or limit. This applies both without
6240
+ a requirements file and with one that omits the key. Without `--source` and
6241
+ without the declaration, the line stays absent. Declared observations retain
6242
+ their output bytes and renderer calls.
6243
+
6244
+ `check --json` prints only the completed `check.json` object to stdout, with
6245
+ the same bytes as the file and the same exit code as ordinary `check` (0 or 1).
6246
+ Early input refusals and usage errors retain their named text and exits (1 or 2);
6247
+ they have no completed check object. `build --json` is refused by name: run
6248
+ `check --rig <out>/rig --parts <out> --out <dir> --json` after the build to read
6249
+ the completed check object.
6233
6250
 
6234
6251
  The observation is the actual renderer's setup pose, without animation or
6235
6252
  physics advancement, from the already gated setup candidate. Its viewport is
@@ -6566,6 +6583,47 @@ art. An authored `parts.json` depth reference remains an active standalone
6566
6583
  guards still apply. With binding inactive there is no added reference, file or
6567
6584
  legacy-byte change. #773 adds no image generation, geometry or motion model.
6568
6585
 
6586
+ ### Head roll before a declared turn
6587
+
6588
+ `rig` and `build` report `WARN RIG_TURN_HEAD_ROLL` when a declared head
6589
+ turn (`turn.depths` or `turn.roles`, including the depth-sampled producer)
6590
+ has an absolute setup eye-line roll above 18.485385 degrees (six-place display).
6591
+ The line names the face part (or the eye parts without a face), the signed
6592
+ roll, its absolute value, the limit, and both eye bones and parts. It reads
6593
+ the existing `eyeLineDegrees` on the authored `eye_r` and `eye_l` origins,
6594
+ image-left to image-right in Spine degrees, counter-clockwise; it does not
6595
+ detect the painting's tilt independently of those authored origins, estimate
6596
+ tilt from a bounding box, or invent a reading without both eye parts
6597
+ and bones or when their origins share a column.
6598
+
6599
+ The limit is the midpoint between the maximum absolute roll of the measured
6600
+ public controls, 4.864514 degrees, and the tilted `three-quarter`
6601
+ input from issue #900, 32.106256 degrees. The comparison uses the unrounded
6602
+ midpoint. The controls are the five
6603
+ parts examples plus the published dancer config and head-turn demo's compiled
6604
+ setup eye origins. The named quiet controls read 3.239700 degrees
6605
+ (`reference-demo`), -1.941486 degrees (`dancer`), and
6606
+ 0.520856 degrees (head-turn demo). This is an empirical separation
6607
+ of these inputs, not an anatomical limit or a visual-quality verdict.
6608
+
6609
+ The parts configs were read at
6610
+ [`38e5f574`](https://github.com/rig-c/parts/tree/38e5f574a88b2e6753a982b0ee0952dbf273a12e/examples),
6611
+ the [dancer config](https://github.com/rig-c/examples/blob/564285414fccf7b47df32501c0b3c2968db77691/dancer/config.json)
6612
+ at `56428541`, and the
6613
+ [head-turn model](https://github.com/rig-c/rig-c.github.io/blob/989cebbd6733a58142923cec96fe2333da08194a/demos/head-turn/bundle/skeleton.model.json)
6614
+ at `989cebbd`. The [issue #900](https://github.com/rig-c/parts/issues/900)
6615
+ bootstrap used public `three-quarter` inputs at examples commit
6616
+ `ba99e403d4058fc1cfe40699bdcde8aaf00a90c0`; its authored crop-pixel eye origins
6617
+ are `[236, 136]` and `[287, 104]`. The compiled demo is a setup metric control,
6618
+ not a rebuild of its unavailable parts config.
6619
+
6620
+ An already rolled head is turned about upright yaw/pitch axes; inspect its
6621
+ neck at both extremes before accepting that motion. This warning does not
6622
+ refuse the build, correct the tilt, add a roll axis, or change any pose or
6623
+ emitted file. With no head turn, including a body-only turn, it adds no warning.
6624
+ With an unreadable eye line the existing face-axis report retains its reason;
6625
+ silence is not a measured zero. The warning lives only in the stage report.
6626
+
6569
6627
  ### Missing body-turn coverage
6570
6628
 
6571
6629
  `RIG_TURN_BODY_UNCOVERED` warns when a shown part without a
@@ -6586,6 +6644,151 @@ and never assign a role, depth, binding or draw order. A part already bound to
6586
6644
  its actual intended role, then inspect setup, both turn endpoints and their diff;
6587
6645
  an explicit role alone does not establish that two drawings remain coincident.
6588
6646
 
6647
+ ### Per-part provider maps (#897)
6648
+
6649
+ `depth-prepare` and `depth-apply` are explicit stages after `assemble`, before
6650
+ `rig`. They carry no model, weights, provider service or downloads. Prepare
6651
+ writes one inference PNG per selected assembled part, composited onto gray 128,
6652
+ with integer LANCZOS upscaling and padding. A `rig-parts-depth-inputs/1` manifest
6653
+ binds the original parts.json hash, part PNG hash, rig crop `[x,y,w,h]`, native
6654
+ and upscaled sizes, padding, recipe hash and inference PNG hash. Map registration
6655
+ is declared, never inferred from a filename or guessed from art.
6656
+
6657
+ ```sh
6658
+ rig-parts depth-prepare --parts assembled --config depth.json --out inference
6659
+ # A provider reads inference/*.png and writes the declared floating-point maps.
6660
+ rig-parts depth-apply --parts assembled --manifest inference/manifest.json --config depth.json --rig-config config.json --out mapped
6661
+ rig-parts rig --parts mapped --config mapped/config.json --out compiled
6662
+ ```
6663
+
6664
+ The separate recipe is not a new field on character config:
6665
+
6666
+ ```json
6667
+ {
6668
+ "spec": "rig-parts-depth/1",
6669
+ "prepare": {"longSide": 1024, "paddingFraction": 0.08, "padding": 4},
6670
+ "groups": {"head": {"near": "white", "zScale": 30, "pivot": 15}},
6671
+ "parts": {
6672
+ "face": {"map": "face.json", "source": "depth", "frame": "part", "group": "head", "median": 21, "relief": 13}
6673
+ },
6674
+ "clear": []
6675
+ }
6676
+ ```
6677
+
6678
+ All displayed numbers are authored example choices, not defaults. `longSide`
6679
+ and additive `padding` are integers; upscale is `max(1,ceil(longSide/max(w,h)))`
6680
+ and pad is `floor(max(upscaledSize)*paddingFraction)+padding`. Each part declares
6681
+ `median` and nonnegative `relief` in rig pixels. Each group (`head` or `body`)
6682
+ declares `near`, positive `zScale` and `pivot`; optional `unitDepths` explicitly
6683
+ replaces that group's rigid depths. `clear` explicitly lists part references to
6684
+ remove, including any small units intended to sample the surface. Unselected
6685
+ parts retain their artwork and depth references. The output config copies the
6686
+ original, sets the declared group pivots/unit depths, and relocates animation,
6687
+ requirements and control-skeleton file paths. Built-in skeleton names remain
6688
+ names. A selected part whose provenance records `depth.kind: "authored-unit"`
6689
+ is refused before output (`DEPTH_AUTHORED_UNIT_CONFLICT`); apply does not invent
6690
+ a transition to a transported scalar reference. Unselected authored units keep
6691
+ their original representation. Roles and draw order stay the author's.
6692
+
6693
+ Maps are UTF-8 JSON `{"spec":"rig-parts-map/1","width":2,"height":2,
6694
+ "channels":1,"data":[0.125,0.25,0.5,0.875]}`: row-major finite numeric values,
6695
+ with channels interleaved for signed XYZ normals (`channels:3`). JSON is open,
6696
+ lossless for binary64 numbers, and requires no numeric-array dependency; an
6697
+ 8-bit preview cannot preserve the raw map's normalization. Paths resolve beside
6698
+ the recipe. `frame:"part"` requires native part dimensions; `"upscaled"` requires
6699
+ the prepared art's enlarged dimensions without padding; `"input"` requires the
6700
+ whole prepared image and crops its declared padding before reduction. Scalar
6701
+ maps use float32 BOX reduction. Normal sources require signed image-axis XYZ
6702
+ (x right, y down), not an RGB normal preview.
6703
+
6704
+ Apply computes `u=(raw-median(raw))/(p95-p5)` inside original alpha >=128,
6705
+ using linear percentiles, then `Z=median+relief*u`. A flat map requires explicit
6706
+ relief 0. Encode `Z/zScale` for near white, `1-Z/zScale` for near black, clip to
6707
+ [0,1], and round to the nearest 8-bit level with even ties. Extend every
6708
+ non-support pixel with the Euclidean nearest support value, with row-major ties.
6709
+ The resulting opaque gray sheet carries no background cliff. The report names
6710
+ levels, clip fraction (Z <=0 or >=zScale on support), declared relief in rig
6711
+ pixels, group pivot, sign, raw median/p5/p95 and source/input/map/sheet hashes.
6712
+ It reports measurements, not a depth-quality pass mark. `rig` still runs Core's
6713
+ gate; apply writes parts and configuration, never a skeleton or a gate verdict.
6714
+ Outputs must be absent or empty; all declared inputs validate before emitting.
6715
+
6716
+ `source:"normals"` is optional and limited to one closed convex part. It refuses
6717
+ multiple four-connected alpha islands both before and after stride2 sampling.
6718
+ At half resolution it integrates adjacent gradients `-nx/max(abs(nz),0.15*norm)`
6719
+ and `-ny/max(abs(nz),0.15*norm)` with step2 trapezoids and matrix-free LSQR
6720
+ (maximum 3000 iterations, atol/btol 1e-6, condition limit 1e8). Cross erosion
6721
+ finds an interior core (max(3,floor(0.12*sqrt(area))) steps) and a three-step rim.
6722
+ The sign makes mean core depth greater than mean rim depth; no reference is
6723
+ read. An empty core or unresolved sign refuses. Before float32 BOX, unsupported
6724
+ integration-grid samples receive the Euclidean nearest supported float height,
6725
+ with row-major ties; supported integrated heights are unchanged. This fill uses
6726
+ the stride2 integration mask itself, not the native mask or encoded gray levels,
6727
+ so unsupported zeros cannot enter a supported native pixel through BOX. BOX
6728
+ then brings the field back to the native grid before the same normalization.
6729
+ Normalization still reads all native alpha>=128 support; a boundary repair can
6730
+ change its percentiles and therefore interior levels too. Iterations and
6731
+ residual are reported. One connected silhouette does not certify convexity or
6732
+ exclude internal occlusion edges; choose scalar depth for strands and collars.
6733
+
6734
+ The [provider trial](https://github.com/rig-c/parts/issues/897) measured Depth
6735
+ Anything V2 Large and Small, DA3MONO-LARGE, DepthPro, Lotus depth/normal G,
6736
+ Marigold depth, and bundled MiDaS/Zoe/LeReS/Metric3D routes. DA3 and Small rows
6737
+ were added after the initial report. The stage2 reproduction uses V2 Large
6738
+ with the Lotus-normal face, not DA3. No model is required by these commands.
6739
+ The trial's role relief/width ratios (face 0.1264803, front hair 0.1018772,
6740
+ back hair 0.0857722, ornament 0.0375868, garment 0.0990473) came from one
6741
+ head-turn demo's p5..p95 depth and part widths. They are measured starting
6742
+ points for declared proportions, not anatomical laws or default calibration.
6743
+ For a head-referenced calibration, declare
6744
+ `relief = referenceRoleRelief * targetFaceWidth / referenceFaceWidth`, using
6745
+ the assembled target face width and the same declared reference head measure
6746
+ for every role. This keeps head relief on one scale when a hair silhouette
6747
+ is much wider than the face. In contrast, `roleRatio * targetPartWidth`
6748
+ increases relief with that silhouette while the group's `zScale` stays fixed.
6749
+ An ornament narrower than its reference can gain relief under the head rule;
6750
+ inspect it as well as the widest part. The command derives neither rule:
6751
+ write the resulting rig-pixel `relief` explicitly in each recipe entry.
6752
+
6753
+ Compare calibrations on identical prepared inputs and numeric maps, with the
6754
+ same medians, group pivot and `zScale`; only the declared relief changes.
6755
+ Read each named report row's `relief`, `zScale` and `clipFraction` before
6756
+ judging its render. The clipping denominator is every original part pixel
6757
+ with alpha >=128, including boundary support, never the padded input area.
6758
+ A low clipped fraction does not establish shape quality. Inspect setup,
6759
+ both yaw endpoints and their difference at native scale, then enlarge the
6760
+ same pixels with nearest-neighbour sampling. Neither clipping nor agreement
6761
+ with a reference map substitutes for that inspection.
6762
+
6763
+ The owner's preference for per-part volume is subjective provenance, not an
6764
+ automated quality conclusion. Model licence posture remains the owner's choice.
6765
+
6766
+ `depth.full.layers` and `depth.head.layers` still accept authored source-grid
6767
+ sheets. They bind the See-through layer before assembly transports/scales/crops
6768
+ it; they do not bind an already assembled part's post-merge image. Use them
6769
+ when a sheet genuinely occupies that source grid. The commands above bind the
6770
+ final assembled grid and preserve the existing `parts.json` reference consumed
6771
+ by `rig`, so their output requires no alternate compiler or dependency pin.
6772
+
6773
+ | rule | refused input | remedy |
6774
+ | --- | --- | --- |
6775
+ | <a id="DEPTH_RECIPE"></a>`DEPTH_RECIPE` | unknown, absent or wrongly typed recipe field, group, source or frame | use the documented explicit recipe schema; no guessed field |
6776
+ | <a id="DEPTH_RECIPE_NUMBER"></a>`DEPTH_RECIPE_NUMBER` | missing/nonfinite median, relief, near-scale pivot or prepare number; invalid range/integer | declare every numeric field in its documented units and range |
6777
+ | <a id="DEPTH_INPUT_PRESENT"></a>`DEPTH_INPUT_PRESENT` | declared JSON input missing or not a file | restore the named file |
6778
+ | <a id="DEPTH_INPUT_JSON"></a>`DEPTH_INPUT_JSON` | malformed numeric or configuration JSON | write valid finite JSON |
6779
+ | <a id="DEPTH_PART_RESOLVES"></a>`DEPTH_PART_RESOLVES` | selected or cleared name absent from parts.json | use the assembled part name |
6780
+ | <a id="DEPTH_OUTPUT_EMPTY"></a>`DEPTH_OUTPUT_EMPTY` | nonempty output or output containing an input | choose a separate absent or empty directory |
6781
+ | <a id="DEPTH_INPUT_BINDING"></a>`DEPTH_INPUT_BINDING` | changed recipe, art, part record, crop, padding or inference PNG | prepare again for these exact inputs |
6782
+ | <a id="DEPTH_MAP_FORMAT"></a>`DEPTH_MAP_FORMAT` | wrong numeric schema, channels, shape, count, unknown field or nonfinite element | provide the declared float JSON map, not a preview |
6783
+ | <a id="DEPTH_MAP_SIZE"></a>`DEPTH_MAP_SIZE` | map dimensions differ from its explicit frame | use the registered part, enlarged art or padded input grid |
6784
+ | <a id="DEPTH_MAP_SUPPORT"></a>`DEPTH_MAP_SUPPORT` | no alpha>=128 support | provide a supported part |
6785
+ | <a id="DEPTH_MAP_SPREAD"></a>`DEPTH_MAP_SPREAD` | p5=p95 with nonzero relief | supply a nonflat map or declare relief 0 |
6786
+ | <a id="DEPTH_NORMAL_ISLANDS"></a>`DEPTH_NORMAL_ISLANDS` | zero/multiple connected components at native or integration resolution | scalar source, or a larger registered normal input that preserves one island |
6787
+ | <a id="DEPTH_NORMAL_VECTOR"></a>`DEPTH_NORMAL_VECTOR` | a sampled support normal is zero | provide nonzero signed XYZ normals |
6788
+ | <a id="DEPTH_NORMAL_CONVEXITY"></a>`DEPTH_NORMAL_CONVEXITY` | core/rim or its convexity sign is unresolved | larger closed convex mask or scalar source |
6789
+ | <a id="DEPTH_GROUP_BINDING"></a>`DEPTH_GROUP_BINDING` | part outside declared turn roles, mixed retained near/zScale, or filename collision | register exact roles and one shared scale/polarity, replace/clear conflicting sheets, use distinct names |
6790
+ | <a id="DEPTH_AUTHORED_UNIT_CONFLICT"></a>`DEPTH_AUTHORED_UNIT_CONFLICT` | a selected part already records `provenance.depth.kind: "authored-unit"`; adding scalar depth would make its two representations conflict | retain the authored unit or supply an explicitly reassembled scalar-compatible input; apply never replaces the provenance automatically |
6791
+
6589
6792
  ### Unit depth precedence
6590
6793
 
6591
6794
  For `"unit"` roles, a transported part's sheet is recorded and validated but is not
package/docs/STABILITY.md CHANGED
@@ -49,6 +49,8 @@ defaults are the help's too, and none is guessed from the art.
49
49
  <!-- stable:commands -->
50
50
  | command | flags |
51
51
  | --- | --- |
52
+ | `depth-prepare` | `--parts`, `--config`, `--out` (issue #897; separate numeric-depth recipe) |
53
+ | `depth-apply` | `--parts`, `--manifest`, `--config`, `--rig-config`, `--out` (issue #897; original character config is read-only) |
52
54
  | `layers` | one positional path: a wrapper directory, a `layers.json` or a `.psd`; with it, optional `--export` (a directory to write the wrapper form into, issue #359); or `--template` with `--canvas` and no path; or a positional head run with `--full`, `--source`, `--config` together for `PLAN_HEAD_GARMENT_OVERLAP` (issue #826) |
53
55
  | `sheet` | `--source`, `--layers` (repeatable), `--out`; optional `--cell`, `--cols` |
54
56
  | `inputs` | `--source`, `--config`, `--out` |
@@ -60,7 +62,7 @@ defaults are the help's too, and none is guessed from the art.
60
62
  | `propose` | `--parts`, `--source`, `--out`, `--full`, `--head`, `--write-config` together: the [neck draft](AUTHORING.md#a-neck-that-moves-on-its-own) for an existing config with `turn.body` and one neck plan row; reassembles under `<out>/rig`, preserves the sampled turn and drafts a rigid neck unit (issue #826) |
61
63
  | `compare` | `--left`, `--right`; optional `--map` |
62
64
  | `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
63
- | `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--no-extremes` (issue #829), `--set` or `--all-sets` (issue #454), `--environment` (issue #538, additive), `--wind-band` (issue #590, additive) |
65
+ | `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--no-extremes` (issue #829), `--set` or `--all-sets` (issue #454), `--environment` (issue #538, additive), `--wind-band` (issue #590, additive), `--json` (issue #904) |
64
66
  | `loop` | `--frames`, `--out`; optional `--palette` |
65
67
  | `build` | `--config`, `--source`, `--full`, `--out`; optional `--head` (as for `assemble`), `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--lead-in`, `--requirements`, `--idle-keys`, `--seam-pairs`, `--keep-stills`, `--no-extremes` (issue #829), `--alt` (as for `assemble`, issue #454), `--set` or `--all-sets` (as for `check`, issue #454), `--environment` (as for `check`, issue #538, additive), `--wind-band` (as for `check`, issue #590, additive) |
66
68
  | `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
@@ -114,6 +116,18 @@ rules that make the contract:
114
116
  - **`turn`** (issue #251) adds a control bone `turn` under `root` that nothing
115
117
  keys, and carries each part in `depths` by `(pivot − depth) / (pivot − nearest
116
118
  depth)` of its x. Without the field no byte of any output moves.
119
+ A declared head turn (`depths` or `roles`) reports `RIG_TURN_HEAD_ROLL`
120
+ when the absolute setup eye-line roll exceeds the limit displayed as
121
+ 18.485385 degrees
122
+ (issue #903). This empirical limit is the midpoint of the measured public
123
+ control maximum, 4.864514 degrees, and `three-quarter`,
124
+ 32.106256 degrees (the comparison uses the unrounded midpoint). The warning
125
+ names the face/eye parts, the eye
126
+ bones, signed roll and limit. It is report-only: no refusal, field, pose or
127
+ emitted-byte change. No head turn, a body-only turn, or an unreadable eye
128
+ line adds no warning; unreadable retains the existing face-axis reason.
129
+ See [Head roll before a declared turn](AUTHORING.md#head-roll-before-a-declared-turn)
130
+ for the measurement scope and its limits.
117
131
  - **`scene`** (issue #242) declares the painting a scene — the figure and what
118
132
  is round it — and takes no field: `{}`. With it `assemble` reads the
119
133
  recomposite inside the figure (the union of the full run's layers at alpha
@@ -133,7 +147,7 @@ part name, `[i]` an entry of a list):
133
147
  - **generation.control.skeleton**, the pose file it names when it is no built-in — required: `width`, `height`, `points`
134
148
  - **seethrough** — required: `resolution`, `steps`, `seed`, `offload`; optional: `head_box`, `arm_split`
135
149
  - **seethrough.arm_split** — required: `order`, and one of `x` or `line` (issue #439)
136
- - **assemble** — required: `rig_scale`, `plan` (from plain `assemble` on); optional: `extend_below_crop`, `patches`, `cuts`, `alternates`, `head_face_underfill`, `yield_refused`, `keep_under_occluders`, `extend_painting`, `take`
150
+ - **assemble** — required: `rig_scale`, `plan` (from plain `assemble` on); optional: `seam`, `extend_below_crop`, `patches`, `cuts`, `alternates`, `head_face_underfill`, `yield_refused`, `keep_under_occluders`, `extend_painting`, `take`
137
151
  - **assemble.alternates[i]** — required: `name`; optional: `source`, `full`, `head`, `parts`, `hides`, `head_box`, `cuts`, `skeleton`, `anchor`, `base_anchor`, `drawn`, `tips`, `front`, `layers`, `focus` (issues #454, #465, #543, #652, #664, #666, #719, #780). `focus` is an authored finite `[x, y]` in painting pixels, within its canvas (edges included), refused as CONFIG_SET_FOCUS before assemble output when invalid; only when present, `parts.json` records it with focus_source_size and `sets.json` emits it in setup-world coordinates through the actual rig resize and `src/coords.ts`. Absent focus emits no field or inferred value; changing/removing it requires reassembly
138
152
  - **assemble.extend_below_crop[i]** — required: `part`, `run`, `tag`
139
153
  - **assemble.extend_painting[i]** — required: `part`, `band`, `max_distance`; optional: `min_held_alpha`, `order` (issues #848, #854, #855; explicit final layer-derived part, half-open rig band and positive four-neighbour path bound; base painting only). The optional integer floor 1–255 admits exact original alpha-over coverage strictly below it; absent or 1 blocks every nonzero holder. Only selected target samples below the floor may take painting RGBA; target samples at/above the floor and every non-target holder remain exact. By default overlapping reachable claims refuse. With `order` on every entry (unique nonnegative safe integers), lower values claim first and their gains block later candidates and paths; array order never breaks ties or supplies missing precedence
@@ -204,6 +218,11 @@ part name, `[i]` an entry of a list):
204
218
  - **waist** — required: `top`, `bottom`, `upper`, `lower`, `bone`, `constraint` (#650; absent keeps the ordinary bindings and graph)
205
219
  <!-- /stable:config -->
206
220
 
221
+ `assemble.seam` accepts `near-white` or `silhouette` (issue #905). CLI
222
+ `--seam` overrides config, then the existing `near-white` default applies.
223
+ Invalid values refuse as `CONFIG_FIELD_TYPE` at `config.assemble.seam`;
224
+ an absent field preserves existing output bytes.
225
+
207
226
  `TY14` holds the set of field names in backticks above to the set of names the
208
227
  loader's object checks spell (every `c.object(…)` in `src/config.ts`, with the lists
209
228
  they name), in both directions, and refuses an object check whose key list it cannot
@@ -505,10 +524,17 @@ The separate ordering scalar is unchanged. No-image outputs retain legacy bytes.
505
524
  ### Declared setup-painting regions
506
525
 
507
526
  The optional `setup_painting` key in `spine-parts-requirements/1` adds a
508
- `SETUP_MATCHES_PAINTING` observation only when declared. It requires authored
527
+ `SETUP_MATCHES_PAINTING` measurement when declared. It requires authored
509
528
  `rgb_tolerance` and named native-stage boxes. Each optional `max_pixels` is
510
- that whole region's limit; omission is SKIP, never PASS. Existing files without
511
- the key retain their output bytes and renderer calls. Other requirement fields
529
+ that whole region's limit; omission is SKIP, never PASS. With `--source` but
530
+ no declaration, the console, `check.json` and the report's skipped-bar list now
531
+ say SKIP with the reason and the `--requirements` declaration remedy; exit
532
+ codes stay unchanged. No metric, region or limit is invented. Without source
533
+ and without the key, the line stays absent. Declared output bytes and renderer
534
+ calls remain unchanged. `check --json` prints the completed `check.json` bytes
535
+ to stdout and keeps the ordinary check's exit code; early input refusals and
536
+ usage errors keep their existing named text and exits. `build --json` is refused
537
+ with a `check --json` remedy; build keeps its multi-stage text report. Other requirement fields
512
538
  remain independent. The [authoring contract](AUTHORING.md#setup-against-the-painting)
513
539
  defines white compositing, the figure domain, strict RGB comparison, top
514
540
  contributor attribution and named measurement refusals. The check observes the
@@ -525,3 +551,22 @@ only for that explicit block; `check --requirements` refuses unavailable or
525
551
  stale evidence rather than reconstructing original art from emitted siblings.
526
552
  Without the block the API reading is SKIP; CLI stdout and emitted bytes remain unchanged.
527
553
  See [Source before scissors](AUTHORING.md#source-before-scissors).
554
+
555
+ ## Provider-map depth stages (#897)
556
+
557
+ The opt-in `depth-prepare` and `depth-apply` commands accept the separate
558
+ `rig-parts-depth/1` recipe and finite floating-point `rig-parts-map/1` JSON.
559
+ Prepared provenance is `rig-parts-depth-inputs/1`; application writes
560
+ `rig-parts-depth-report/1`, standard part depth references and a relocated
561
+ character config. Their schemas, frames, alpha threshold, normalization,
562
+ encoding, convexity sign and refusal remedies are in
563
+ [AUTHORING](AUTHORING.md#per-part-provider-maps-897). No existing build path
564
+ calls these stages; no-config builds retain their existing behavior and bytes.
565
+ Group numbers and unit depths are explicit; no provider or absolute scale is
566
+ inferred. A new depth stage never replaces Core's rig gate.
567
+ `relief` is an explicit rig-pixel value, not an implicit width multiplier.
568
+ Part-width and head-referenced calibrations are authored recipe choices;
569
+ changing a calibration requires preparing its recipe again. Compare the same
570
+ provider maps and preserve their provenance. Each report row names the part,
571
+ declared relief, group `zScale` and clipping over original alpha>=128 support;
572
+ it remains a measurement rather than a shape-quality gate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-parts",
3
- "version": "1.86.0",
3
+ "version": "1.88.0",
4
4
  "description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/assemble.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type SeamRule } from './seamrule.ts';
1
2
  import { edgeLength } from './length.ts';
2
3
  import { materialSource, type MaterialSource, materialRows, polygonMaterialEdges, type MaterialRow, type MaterialEdge, type PlacedArt } from './cutmaterial.ts';
3
4
  import { unionTakenSources } from './takesource.ts';
@@ -188,16 +189,7 @@ export const SILHOUETTE_BORDER = 8;
188
189
  /** `figureSilhouette`: a pixel is figure where it differs from that median by more than this, max channel. */
189
190
  export const SILHOUETTE_LIMIT = 40;
190
191
 
191
- export type SeamRule = 'near-white' | 'silhouette';
192
- export const SEAM_RULES: readonly SeamRule[] = ['near-white', 'silhouette'];
193
-
194
- /**
195
- * The seam rule a run uses when none is named. Set by measurement, not taste:
196
- * the silhouette rule ships as the default only if it lowers the error pixel
197
- * count on every oracle character without raising mean |d| on any
198
- * (ORACLE_assemble.md, *faithful vs silhouette*).
199
- */
200
- export const DEFAULT_SEAM_RULE: SeamRule = 'near-white';
192
+ export { DEFAULT_SEAM_RULE, SEAM_RULES, type SeamRule } from './seamrule.ts';
201
193
 
202
194
  /**
203
195
  * Where a layer may take the painting's pixel (`projectSource`). `core` is the