rig-parts 1.87.0 → 2.0.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
@@ -555,7 +555,8 @@ usage:
555
555
  [--page-edges pot|free] [--pack-shape rect|polygon]
556
556
  [--requirements <file.json>] [--seam-pairs] [--keep-stills]
557
557
  [--set <name> | --all-sets] [--environment <schedule.json>]
558
- [--wind-band <spec.json>]
558
+ [--wind-band <spec.json>] [--json]
559
+ --json prints the completed check.json object only, with unchanged exit codes.
559
560
  Build, gate, render and measure a rig through rig-c's CLI (the rigc at
560
561
  node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
561
562
  a rig spec, which rigc build compiles, not a compiled skeleton.json.
@@ -777,7 +778,7 @@ usage:
777
778
  extends below a crop (ASSEMBLE_SINGLE_RUN_PLAN), and the config sets no
778
779
  seethrough.head_box (ASSEMBLE_SINGLE_RUN_HEAD_BOX).
779
780
  Writes nothing unless every check passed.
780
- --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
781
782
  the painting's pixel: core (the reference's) erodes every layer's top-most
782
783
  opaque area by 5x5 first, so a part a few pixels wide takes none; visible
783
784
  erodes only along a rim where a later layer is in front. It defaults to
@@ -861,7 +862,7 @@ usage:
861
862
  green build ends with the pack line beside the spineboy yardstick and the
862
863
  three artifact paths — skeleton .json, .atlas and the packed page: the
863
864
  packed atlas is the result, the loose parts are the intermediate it was
864
- made from. --seam defaults to ${DEFAULT_SEAM_RULE}, --project to
865
+ made from. --seam overrides config.assemble.seam, otherwise ${DEFAULT_SEAM_RULE}; --project to
865
866
  ${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES} and
866
867
  --pack-shape to ${DEFAULT_PACK_SHAPE} (as for check; both packed builds, rig's gate
867
868
  and check's artifact, take them). --requirements is forwarded to check
@@ -1095,7 +1096,7 @@ const TAKES = {
1095
1096
  rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
1096
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'] },
1097
1098
  compare: { valued: ['--left', '--right', '--map'], switches: [] },
1098
- 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'] },
1099
1100
  loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
1100
1101
  assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config', '--requirements'], switches: ['--propose-plan'] },
1101
1102
  'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll', '--config', '--depth-run'], switches: ['--offload', '--no-offload', '--lama', '--nf4'] },
@@ -1338,6 +1339,8 @@ function flags(args: string[], command: Command, required: readonly string[]): M
1338
1339
 
1339
1340
  function cmdCheck(args: string[]): number {
1340
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');
1341
1344
  const seamPairs = args.filter((a) => a === '--seam-pairs').length;
1342
1345
  if (seamPairs > 1) return usage('--seam-pairs is given twice');
1343
1346
  const keepStills = args.filter((a) => a === '--keep-stills').length;
@@ -1347,7 +1350,7 @@ function cmdCheck(args: string[]): number {
1347
1350
  const allSets = args.filter((a) => a === '--all-sets').length;
1348
1351
  if (allSets > 1) return usage('--all-sets is given twice');
1349
1352
  const f = flags(
1350
- 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'),
1351
1354
  'check',
1352
1355
  ['--rig', '--out'],
1353
1356
  );
@@ -1365,8 +1368,9 @@ function cmdCheck(args: string[]): number {
1365
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 } : {}) },
1366
1369
  rigcRunner(bin),
1367
1370
  bin,
1368
- console.log,
1371
+ json === 1 ? () => {} : console.log,
1369
1372
  );
1373
+ if (json === 1) process.stdout.write(`${JSON.stringify(r.figures, null, 1)}\n`);
1370
1374
  return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
1371
1375
  } catch (err) {
1372
1376
  return printRefusal(err);
@@ -1726,8 +1730,8 @@ function cmdAssemble(args: string[]): number {
1726
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');
1727
1731
  if (!propose && flags.has('--write-config')) return usage('--write-config belongs to assemble --propose-plan');
1728
1732
  if (!propose && !flags.has('--out')) return usage('assemble needs --out <dir>');
1729
- const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
1730
- 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`);
1731
1735
  const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
1732
1736
  if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
1733
1737
  const [source, full, config] = ['--source', '--full', '--config'].map((f) => flags.get(f) as string);
@@ -1755,7 +1759,7 @@ function cmdAssemble(args: string[]): number {
1755
1759
  }
1756
1760
  const out = flags.get('--out') as string;
1757
1761
  assembleStage(
1758
- { 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 }) },
1759
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) },
1760
1764
  console.log,
1761
1765
  );
@@ -1946,8 +1950,8 @@ function cmdBuild(args: string[]): number {
1946
1950
  // --head is optional (issue #359), as for assemble.
1947
1951
  for (const f of ['--config', '--source', '--full', '--out']) if (!flags.has(f)) return usage(`build needs ${f}`);
1948
1952
  if (on.has('--all-sets') && flags.has('--set')) return usage('--set and --all-sets are two modes; give one');
1949
- const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
1950
- 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`);
1951
1955
  const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
1952
1956
  if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
1953
1957
  const edges = pageEdgesOf(flags.get('--page-edges'));
@@ -1971,7 +1975,7 @@ function cmdBuild(args: string[]): number {
1971
1975
  const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-build-'));
1972
1976
  try {
1973
1977
  const r = build(
1974
- { 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 } : {}) },
1975
1979
  { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate'), stages: {
1976
1980
  assemble: (input, outputs) => runBuildProcess({ stage: 'assemble', bin, input, outputs }, join(scratch, 'workers')),
1977
1981
  rig: (input, _rigc, gateScratch) => runBuildProcess({ stage: 'rig', bin, input, scratch: gateScratch }, join(scratch, 'workers')),
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 |
@@ -172,7 +173,7 @@ read by no CPU stage).
172
173
  | `motion.blink.still` | rig | **proposed** only for a lash `propose` notes (below), then checked: `{<part>: {row, bone}}` — the rows of that region part above `row` (rig px, y down) are drawn by a second slot `<part>_still` on `bone` and do not blink; rows from `row` down keep the part's slot and blink. The part must be a region on a bone `eyes` names, `bone` one it does not (`CONFIG_STILL_OFF_THE_BLINK`), and `row` a row of the part with no art across its whole width (`RIG_STILL_ROW_CLEAR`) |
173
174
  | `motion.animations_from` | rig; check (named; read for folds) | optional, never proposed (issue #183): a path, relative to the config file's directory, to a file in rig-c's motion-spec shape, `{"spec": "rigc-motion/1", "animations": {"<name>": {"duration": …, "tracks": [...]}, …}}` — an expression, a gesture, anything beside the idle. On every `rig` and `build` each of its animations is written into `rig/motion.json` after `idle`, as read: nothing added, re-keyed or judged here (except `derive` keys on `turn`/`tilt`, issue #436, and `{"t": …, "set": "<name>" \| null}` keys on slot `"attachment"` tracks, issue #473, which expand to `"v": "<set>_<slot>"` or `"v": null` when the set hides the slot, or `"v": "<slot>"` for `null`, refused with `CONFIG_SET_COVERS_SLOT` when the set is undeclared or does not cover the track's slot), and each is rigc's to accept or refuse at the rig stage's gate, in rigc's words. It may name what `motion.json` already holds — the bones `bones` declares, the easings `shut` and `open`, the groups `eyes` and `brows` when the blink writes them — and nothing else from the file reaches it. The loader refuses, every problem at once: no such file or one that cannot be read (`CONFIG_FILE_PRESENT`), text that is not JSON (`CONFIG_IS_JSON`), a file that is not an object, a `spec` other than `"rigc-motion/1"`, an `animations` that is not a table of objects (`CONFIG_FIELD_TYPE`), any other key — rig-c's `easings`, `groups`, `setup`, `physics`, `mix`, `archetype` and `cut` included, which would be read and not written (`CONFIG_KEY_KNOWN`), an animation named `idle` (`CONFIG_ANIMATION_NAME_FREE`), and a key written twice in one object of the file, which JSON would fold into its last value without a word (`CONFIG_KEY_UNIQUE`). `check` names the others on one line (`beside the idle: …`) and reads each for folds, with the idle — `ANIMATIONS_DO_NOT_FOLD` (§7, issues #320, #333); every other bar reads the idle. Absent, `motion.json` holds the idle alone, byte for byte as before, and `check` reads that idle for folds |
174
175
  | `constraints` | rig (handed to rig-c as `rig.json`'s `constraints`); `compare` and the coverage lines (roles) | **authored**, optional; `propose` writes none, but with `--cloth` (issue #364), where it writes the recipe's physics and swing transforms (*Recipe: cloth and hair that answer the world*, below). A list of constraints in **rig-c's own rig-spec shape** (`RigConstraint`: `ik`, `transform`, `path`, `physics`, `slider` — rig-c's `src/rig.ts` documents every field), in the order rigc applies them. The loader checks only what this package owns: each entry is an object with a `type` rigc names (`CONFIG_CONSTRAINT_TYPE_KNOWN`) and a non-empty `name`, unique within its kind as rigc finds constraints (`CONFIG_CONSTRAINT_NAME_UNIQUE`; an ik and a transform may share one); every bone it names — `bones`, an ik's `target`, a transform's `source`, a physics or slider constraint's `bone` — is a bone `bones` declares, a chain link as `<chain><i>` (`CONFIG_NAME_RESOLVES`); the bone a constraint follows does not sit under a bone it drives (`CONFIG_CONSTRAINT_TARGET_DETACHED`). Every other field (`mix`, `softness`, `properties`, a path's `slot`, a slider's `animation`) and every value is rigc's to accept or refuse at the rig stage's gate, in rigc's words (`RIG_RIGC_GREEN`) — an ik's `bones` as a shape too: from rig-c 2.15.0 rigc refuses an ik over more than two bones, or over a pair whose second bone is not the first's child (issue #103; this loader refused both itself until then). A note and an `x-` record may ride on an entry; the rig stage leaves them out of what it hands rigc. Under the default `--idle-keys ctl` a two-bone ik whose child link the idle keys is split by that link's control, and rigc refuses the pair: run `rig --idle-keys direct` or `build --idle-keys direct` (the rig stage adds that sentence, naming the flag on the command that ran, to rigc's line; `build` forwards it to its rig stage, issue #95). Each bone a constraint follows is declared to rigc as `invariants.detached` from every bone it drives, which rigc's gate rule `A25` checks |
175
- | `stops` | rig (handed to rig-c as `rig.json`'s `stops`, and moves slots, mesh weights and child bones on stopped links onto `<link>_d`); `propose --from-config` (`LINT stop on <chain> …`) | **authored**, optional (issues #404, #447): `[{chain, links?, against, at, inward, outward, lower?: {inward, outward}}]` — rig-c's leg stops (`RigStop`, rig-c 2.56.0, `@rig-c/core/src/stop.ts`). `chain` is a chain `bones` declares (`CONFIG_NAME_RESOLVES`, `RIG_NAME_RESOLVES`); `links`, optional, is a non-empty list of links of that chain (`CONFIG_NAME_RESOLVES`, `CONFIG_FIELD_TYPE`), or absent for every link of `chain` in chain order; `against` is a bone `bones` declares, a chain link as `<chain><i>` (`CONFIG_NAME_RESOLVES`, `RIG_NAME_RESOLVES`); `at` is `[x, y]` in `against`'s local space, as rig-c's stop takes it (`CONFIG_FIELD_TYPE`): since each chain link (`leg_r0`, `leg_r1`) is turned to point along its segment (`+x` from origin to next link or tip, `+y` 90° counter-clockwise in Spine world), `at: [d, n]` sits `d` px from `against`'s origin toward its end and `n` px perpendicular to it; `inward`, `outward` and `lower.inward`, `lower.outward` are finite numbers (`CONFIG_FIELD_TYPE`). Every other stop rule — non-negative bounds, each stopped link written by a constraint, `against` not under the stopped chain, the aim not degenerate, `\|rest\| + outward <= 180°` (`RIG_STOP_BOUNDS`) — is rigc's to accept or refuse at the rig stage's gate, in rigc's words (`RIG_RIGC_GREEN`). **Rising-limb / prone geometry (issue #447):** rig-c's `expandStops` aims `<chain>_rest` from `links[0]`'s setup origin at `<chain>_touch` (`at` in `against`'s local frame) and clamps `<link>_d` inside `[rest − inward, rest + outward]`; Spine wraps rotation at `±180°`, so `\|rest\| + outward <= 180°` is required (`RIG_STOP_BOUNDS`). Whenever `<chain>_touch` lies **above** `links[0]`'s root in rig pixels (`ty < y0`, i.e. higher Spine `y` — e.g. stopping a hanging cloth chain against a rising shin `leg_<s1>` in a prone or kneeling pose where the knee sits below `links[0]` and the ankle above it, so positive `at[0]` walks upward along the shin past `links[0]`'s root), `rest` lands in the upper half-plane (`\|rest\| > 90°`) and `\|rest\| + outward` exceeds `180°`. Stop against the descending thigh (`leg_<s0>`), set `links` to start at a link whose root sits above `<chain>_touch`, or omit the stop when the rising shin sits outside the hanging chain's sweep; `propose --from-config` prints `LINT stop on <chain> …` with the exact coordinates and angles when `<chain>_touch` lies above `links[0]`'s root or `\|rest\| + outward > 180°`. For every stopped link `<link>`, the rig stage moves any slot on `<link>`, any mesh weight on `<link>`, and any bone whose parent is `<link>` (a branched cloth tree's branches hanging from `<trunk_last>`, before `--idle-keys ctl` adds `<bone>_ctl` and before `turn` adds `turn_<part>_<bone>`) onto `<link>_d`, the drawn link bone rig-c's `expandStops` creates at `<link>`'s setup frame to carry the clamped rotation. Absent, `rig.json` carries no `stops` and no byte of any output moves |
176
+ | `stops` | rig (handed to rig-c as `rig.json`'s `stops`, and moves slots, mesh weights and child bones on stopped links onto `<link>_d`); `propose --from-config` (`LINT stop on <chain> …`) | **authored**, optional (issues #404, #447): `[{chain, links?, against, at, inward, outward, lower?: {inward, outward}}]` — rig-c's leg stops (`RigStop`, rig-c 2.56.0, `@rig-c/core/stop`). `chain` is a chain `bones` declares (`CONFIG_NAME_RESOLVES`, `RIG_NAME_RESOLVES`); `links`, optional, is a non-empty list of links of that chain (`CONFIG_NAME_RESOLVES`, `CONFIG_FIELD_TYPE`), or absent for every link of `chain` in chain order; `against` is a bone `bones` declares, a chain link as `<chain><i>` (`CONFIG_NAME_RESOLVES`, `RIG_NAME_RESOLVES`); `at` is `[x, y]` in `against`'s local space, as rig-c's stop takes it (`CONFIG_FIELD_TYPE`): since each chain link (`leg_r0`, `leg_r1`) is turned to point along its segment (`+x` from origin to next link or tip, `+y` 90° counter-clockwise in Spine world), `at: [d, n]` sits `d` px from `against`'s origin toward its end and `n` px perpendicular to it; `inward`, `outward` and `lower.inward`, `lower.outward` are finite numbers (`CONFIG_FIELD_TYPE`). Every other stop rule — non-negative bounds, each stopped link written by a constraint, `against` not under the stopped chain, the aim not degenerate, `\|rest\| + outward <= 180°` (`RIG_STOP_BOUNDS`) — is rigc's to accept or refuse at the rig stage's gate, in rigc's words (`RIG_RIGC_GREEN`). **Rising-limb / prone geometry (issue #447):** rig-c's `expandStops` aims `<chain>_rest` from `links[0]`'s setup origin at `<chain>_touch` (`at` in `against`'s local frame) and clamps `<link>_d` inside `[rest − inward, rest + outward]`; Spine wraps rotation at `±180°`, so `\|rest\| + outward <= 180°` is required (`RIG_STOP_BOUNDS`). Whenever `<chain>_touch` lies **above** `links[0]`'s root in rig pixels (`ty < y0`, i.e. higher Spine `y` — e.g. stopping a hanging cloth chain against a rising shin `leg_<s1>` in a prone or kneeling pose where the knee sits below `links[0]` and the ankle above it, so positive `at[0]` walks upward along the shin past `links[0]`'s root), `rest` lands in the upper half-plane (`\|rest\| > 90°`) and `\|rest\| + outward` exceeds `180°`. Stop against the descending thigh (`leg_<s0>`), set `links` to start at a link whose root sits above `<chain>_touch`, or omit the stop when the rising shin sits outside the hanging chain's sweep; `propose --from-config` prints `LINT stop on <chain> …` with the exact coordinates and angles when `<chain>_touch` lies above `links[0]`'s root or `\|rest\| + outward > 180°`. For every stopped link `<link>`, the rig stage moves any slot on `<link>`, any mesh weight on `<link>`, and any bone whose parent is `<link>` (a branched cloth tree's branches hanging from `<trunk_last>`, before `--idle-keys ctl` adds `<bone>_ctl` and before `turn` adds `turn_<part>_<bone>`) onto `<link>_d`, the drawn link bone rig-c's `expandStops` creates at `<link>`'s setup frame to carry the clamped rotation. Absent, `rig.json` carries no `stops` and no byte of any output moves |
176
177
  | `turn` | rig (adds the turn control or 2.5D head-turn producer) | **proposed** only with `propose --full`/`--head` (issue #251), then corrected, or **authored** for 2.5D depth-sampled head turns (issue #763): `{pivot?, depths?, deform?, band?, radius?, surface?: {radius?}, head?: {pivot?, radius?}, roles?, counter?, grid?, unitDepths?, body?}`. When `roles` is declared (`{<part>: "surface" \| "unit" \| "rear"}`, issue #763; `CONFIG_TURN_ROLE_UNKNOWN`, `CONFIG_TURN_SURFACE_REQUIRED`, `RIG_TURN_SURFACE_MESH_BINDINGS`, `CONFIG_TURN_UNIT_MESH_BONES`), the rig stage runs the 2.5D depth-sampled head-turn producer: it edge-extends each turn-bound part's depth sheet across its transparent border and semi-transparent fringe from the nearest fully opaque (`alpha === 255`) texel before `PAD = 4` clamping (`padTurnDepthSheet`, `DEPTH_SILHOUETTE_SIZE_MISMATCH`), keeps authored `"surface"` meshes and their skinning (#795), sampling each vertex at its weighted setup position on its own extended sheet and emitting one inverse-setup-bone offset per influence (`D = -(pivot + z - zRef) * sin(angle)`, on x for yaw and y for pitch, with no narrowing or 2D spin); shared frames moved by another turn role are refused (`RIG_TURN_SURFACE_MESH_BINDINGS`), as are alternate drawings without their own depth sampling frame (`RIG_TURN_SURFACE_MESH_ATTACHMENT`). It promotes `"surface"` regions to `generator: {kind: "grid", cols, rows, depth}` (`grid`: `[cols, rows]` or `{<part>: [cols, rows]}`, default `[13, 16]`, bounded at `<= 2,400` triangles per slot and `<= 4,000` total, `RIG_TURN_GRID_BUDGET`, `RIG_TURN_SURFACE_DEPTH_REQUIRED`, `RIG_TURN_SHARED_DEPTH_SCALE`), emits `faceshift` and `rear_shift` under `head` (`CONFIG_TURN_HEAD_BONE`, `RIG_TURN_HEAD_BONE`), `<part>_surface` and `<part>_unit` under `faceshift`, and `yaw_dial` and `pitch_dial` under `root` (`CONFIG_TURN_BONE_FREE`, `RIG_TURN_NAME_FREE`), re-parents `"unit"` region bones and `"rear"` bones and mesh weights to `faceshift` and `rear_shift` (while `"unit"` mesh parts receive their own `<part>_unit` carrier under `faceshift`, rebind direct `"head"` weights to `<part>_unit`, reparent bound chain roots to `<part>_unit` while keeping chain skinning weights intact, and key `<part>_unit` in `turn_units`, #790), samples each `"unit"` bone's depth from the `"surface"` depth sheet at its rest position (or reads `unitDepths.<part>`, `RIG_TURN_UNIT_DEPTH_REQUIRED`), enforces ear occlusion (`ear_r`/`ear_l` drawn before the `face` surface in `assemble.plan`, `CONFIG_TURN_EAR_DRAW_ORDER`, and behind the cheek surface, `RIG_TURN_EAR_DEPTH_BEHIND_CHEEK`), and writes `turn` and `tilt` `derive` tracks (`pivot` default `55` rig px, `counter` default `0.1` in `[0, 1]`, `CONFIG_TURN_COUNTER_RANGE`). `depths` holds, per part, the See-through layer's `depth_median` the part came from (larger = further back), read from the run the part was assembled from; a `painting:` patch has none and is not carried. A rider (issue #271) — a part from an `eyewhite`, `irides`, `eyelash`, `eyebrow`, `nose` or `mouth` layer — is drafted at the `face` part's depth, 0.001 nearer when parts.json draws it after the face and 0.001 further when before: See-through makes no depth of its own for the eye group (it cuts them from one depth map of the combined eyes) and clamps nose and mouth to the face's median − 0.001, and the two runs' own medians for these layers disagreed by up to 0.47 and swapped their order against the face. Every other layer — ears, headwear and earwear included — keeps its own median, unless it rides another part's bone. With no `face` part, or more than one, the riders keep their own and a note says so. A rider by bone (issue #535) — a part every *ownable* bone of which (a chain link, or a bone no other bone hangs from; `head`, `chest` and any bone with a child are shared and never count on their own) is *hosted* by another carried part, the host of a bone being the carried part that binds it (its region's bone; a mesh's `segments`, `contour` and `auto` regions' bones) with the most `opaque_px`, the earlier in parts.json on a tie, and which hosts no bone itself — is drafted at its host's depth, 0.001 nearer when drawn after the host and 0.001 further when before, and a note names the host, the bones and its own median: a pin, clip or ornament on the back hair's links travels with the back hair, not with its own layer's depth. `propose` reads the proposal's own `bones`, `meshes` and `regions`; the face riders are read by their tag first and never by their bones. A part bound to two hosts' bones rides neither and a note says so. In the rig stage, an authored depth on a rider by bone whose fraction differs from its host's by more than 0.1 of the dial (`TURN_RIDER_APART`, a share of `pivot − nearest`) is refused (`CONFIG_TURN_RIDER_DEPTH`); the host is read off `parts.json`'s `opaque_px`, so the config loader cannot see it. One note lists each part's depth and where it came from; a carrier the other run places more than 10 % of `pivot − nearest` away gets a note naming both values and the run read — the two runs are not reconciled. `pivot` is the depth that stays put; `propose` drafts the mean of the parts' depths weighted by `opaque_px`, the one depth at which the painted pixels' centroid does not slide under the dial. The rig stage adds a bone `turn` under `root` at its origin that nothing keys — its local x is the dial, in rig px — and carries each part by `(pivot − depth) / (pivot − nearest depth)` of it: the nearest part all of it, a part behind the pivot the other way, a part at the pivot not at all. Each part rides leaf twins (`turn_<part>_<bone>`, an identity child of every bone its slot and weights bind) moved by one `transform` constraint (`turn_<part>`, after the config's own); the idle's keys, the `<bone>_ctl` controls and a physics constraint stay on the bones, and the dial moves none of them. Optional `deform` (`Partial<FaceDeformFractions>`, issue #436) overrides the Set B ×2.5 face surface deformation table (`FACE_DEFORM_FRACTIONS`, checked against `FACE_DEFORM_RANGES` by `CONFIG_TURN_DEFORM_RANGE`); when `applyFaceSurfaceDeform` runs on a rig whose `motion` carries `tilt` or `turn` slider `derive` keys, it requires `config.turn` (`CONFIG_TURN_REQUIRED`, issue #478; `propose --full`/`--head` drafts it), promotes `face` on `head` (or `turn_face_head` when `turn` is declared) to the uneven 5×5 grid mesh, keys `face`, `hair_front` and `hair_back` deform timelines and ear/topknot tracks from `evalPitchFaceSurface` and `evalYawFaceSurface`, and samples the same deform field across each `features` member's rest span (`featRx = (24 / 106) · W`, `featRy = (12 / 128) · H`) with its depth fraction `z / zRef` as the field's angle input and chord foreshortening (`scalex` narrowing on the leading side and widening on the trailing side, `scaley` on pitch) so every feature's bounding box stays inside the deformed face plate across the rated band (issue #468). On yaw (`issue #479`), `evalYawFaceSurface` evaluates a **cylinder projection** (`cylinderYawDx`: `Δx(x, θ, R) = R · (sin(clamp(arcsin(clamp(x / R, −1, 1)) − θ, −π/2, π/2)) − clamp(x / R, −1, 1))`) of radius `R` (`turnSurfaceRadius`: `turn.surface.radius ?? turn.radius ?? turn.head.radius`, positive rig px checked by `CONFIG_FIELD_TYPE`, defaulting to half the face plate's opaque width `W / 2`) tapered by a cubic Hermite outline weight (`wOutline = 1` for `\|ux\| <= 0.7`, reaching `0` at `\|ux\| = 1.0`) so features slide across a fixed outline (`iris` shift within `±2%` of `cylinderYawDx`), the trailing cheek strip widens (`>= 1.54×` at `±15°`), and the leading jaw edge holds fixed (`0 px` shift) unless explicit horizontal `turn.deform` overrides (`turn_mid_x`, `turn_lead_compress_x`, `turn_trail_shift_x`) request the flat yaw table. On yaw (`issue #478`), `applyFaceSurfaceDeform` also derives a **head-only pivot** (`headTurnPivot`: `turn.head.pivot` when authored, otherwise the `opaque_px`-weighted mean of `depths[part]` across the head parts `DEFAULT_HEAD_TURN_PARTS` — `face`, `hair_front`, `hair_back`, `ear_r`, `ear_l`, `hairpin` — from `parts.json`, falling back to `turn.pivot`) so a forward torso or arm in `turn.depths` does not pull the head's rotation axis in front of `face`: `hair_back` (`depth_median > headPivot`, negative `headTurnFraction`) and `hairpin` behind `headPivot` translate opposite to the face's turn (`dx = s · turn_face_center_x · W · (headPivot − depth) / (headPivot − nearestHeadDepth)`), emerging on the trailing side to compensate for the leading-cheek foreshortening and keeping the combined head silhouette width near-constant across `±15°` without any same-sign `turn_hair_back_cheek_x` term. Optional `band` (`number` or `{yaw?, pitch?}`, positive degrees, issue #472) declares the rig's rated face turn band (default `FACE_RATED_BAND`: `yaw <= 15°`, `pitch <= 12°`); declaring a `turn.band` or a `motion.animations_from` `turn`/`tilt` `derive.degrees` above `±15°` yaw or `±12°` pitch without an `assemble.alternates` entry that replaces `face` is refused (`CONFIG_FACE_BAND_EXCEEDED`), and on a single-plate rig `applyFaceSurfaceDeform` adds self-targeting transform constraints `yaw_clamp` (`[-15, 15]`) and `pitch_clamp` (`[-12, 12]`) ahead of the slider constraints so driving `yaw` or `pitch` beyond the rated band clamps at the boundary. At rest: with `turn` declared, no pixel differs by more than 1 level at dial 0 (not byte-identical; the cause is float noise in the identity child bones, and the constraints add nothing); without `turn`, byte-identical. Neither `pivot` nor `depths` has a default (`CONFIG_FIELD_PRESENT`); `pivot` and `head.pivot` must lie behind the nearest part (`CONFIG_TURN_PIVOT_BEHIND_NEAREST`). Absent, no control is added and no byte of any output moves |
177
178
  | `scene` | assemble (the recomposite, its holes and the figure's mask); check, through `parts.json`'s `scene` block (`RECOMPOSITE_HOLES`, the seam) | **authored**, optional; `propose` writes none (issue #242). `{}` declares the painting a scene: the figure and its surroundings in one image, See-through's full run holding only the figure. With it, `assemble` reads the recomposite figures, the hole list and the `uncovered:` lines inside the figure — the rig pixels where any full-run layer's alpha is above 8 (`OPAQUE_ALPHA_ABOVE`, the cut every layer's painted pixels are read by) — each line starting `inside the figure (config.scene: …, <n> of <m> rig px)`; writes that figure as `figure_rig.png` (white on black) beside the recomposite, and a `scene` block in `parts.json`; flags error pixels on the error map inside it only (outside, the painting grey); and `check` leaves out of the seam every frame pixel whose composite sample reaches the painting's outermost row or column, saying how many (`seam_border` in `check.json`, and on the seam line). It takes no field: the figure and its threshold are derived. A key in it other than a record or an annotation is `CONFIG_KEY_KNOWN`. Absent, no byte of any output moves |
178
179
  | `cloth` | `propose --cloth` only; no other stage reads it, so it moves no byte of any build | **authored**, optional (issue #364): one recipe per tag, `{"bottomwear": {…}, "back hair": {…}, "front hair": {…}}`, each `{chains, links, wind, gravity, split?, length_ratio?, strength?, damping?, alternate_damping?, translate?, swing?, part?, influence?}`. `influence` (issue #518) — `{across, max, shape, radius?}`: the parts this recipe meshes are drafted in the influence mode (`meshes.<part>.influence` above) at their lattice's cell, with `across`, `max` and `shape` as declared (each required — the reading behind the radius did not vary them) and `radius` as declared or, absent, drafted as the farthest vertex's distance plus 3 cells (the `influence.radius` row); without it the parts keep their lattice. `chains` — trees across the piece (for `back hair`, per side of the eye axis), a whole number, 1 or more; `links` — links from root to tip on every path, 1 or more; `split` — `{at, into}`: the first `at` links are the trunk, which forks into `into` (2 or more) branches; `at` below `links` (`CONFIG_CLOTH_SPLIT_BEFORE_TIP`); `length_ratio` — a path's last link over its first, above 0, default 0.5; `strength` — `{trunk, branch, edge}`, each above 0 and below the spring's stability bound (`CONFIG_CLOTH_SPRING_STABLE`), default 100 / 100 / 50; `damping` — in [0, 1], absent: rig-c's default; `alternate_damping` — the odd trees' damping, in [0, 1], beside `damping` and different from it (`CONFIG_CLOTH_ALTERNATE_DIFFERS`); `wind`, `gravity` — `[root, tip]`, each 0 or more, tip at or above root (`CONFIG_CLOTH_RESPONSE_GROWS_TO_TIP`); `translate` — the x and y mix of a dangling end's flutter, above 0 and at most 1; `swing` — `bottomwear` only, `{scales}`, one positive scale per tree (`CONFIG_CLOTH_SWING_SCALE_PER_TREE`); `part` — `bottomwear` only (`CONFIG_CLOTH_PART_ON_BOTTOMWEAR`), a non-empty part or cut-piece name: the `topwear` part that holds a skirt See-through returned inside it (a dress), or its skirt piece cut by `assemble.cuts`, which takes the skirt's trees (issue #402, *A skirt inside topwear* below; the part is checked by `propose`, `PROPOSE_CLOTH_PART`). The two defaults are the recipe's own, measured on its synthetic fixture (the recipe below). The recipe below says what each field drafts. The config written: `propose --cloth <config.json> --write-config <out.json>` (issue #537) writes this config's own text back with the sections the recipe drafts spliced in — `meshes.<part>` and `regions.<part>`, and `bones`, `constraints` and `motion.tracks` entry by entry by name (issue #552) — and every other byte as read; `--from <previous config>` (issue #571) takes an entry equal to the previous recipe's draft as the recipe's (*The config written*, under the recipe below) |
@@ -5084,7 +5085,7 @@ documents do not ship). Each is an input this package built; none is an author's
5084
5085
  | <a id="REQUIREMENTS_MAY_FOLD"></a>`REQUIREMENTS_MAY_FOLD` | a `may_fold` entry names a slot with no mesh attachment — a region is one quad and cannot fold (issue #320). Refused before anything is built. Before issue #333 an entry naming the idle was refused here too; the bar reads the idle now, and `idle` is an animation name like any other | a mesh slot |
5085
5086
  | <a id="REQUIREMENTS_NOT_PLANTED"></a>`REQUIREMENTS_NOT_PLANTED` | a `not_planted` name is a part `check` does not read as a planted foot — it does not come from a See-through `footwear` layer — or there is no `parts.json` to choose the feet by (issue #231). Refused before anything is built | a `footwear` part's name |
5086
5087
  | <a id="REQUIREMENTS_TIP_EXCEPTION"></a>`REQUIREMENTS_TIP_EXCEPTION` | a `not_swinging`, `rises` or `rests` name is a part `TIP_OVER_ROOT` does not read — not from a See-through `handwear` or `bottomwear` layer — or there is no `parts.json` (refused before anything is built); or a `rises` part is not carried by a chain that rises: the chain's **last link**, at the setup pose, runs other than up — from its origin along its `length` (the extent the rig stage turns a link to), or, a last link with no length, from the link before's origin to its own — the detail giving the link read and the direction measured (refused after the idle is rendered, before `check.json` is written) (issues #231, #322; until #322 it read the chain's first link's origin to its last's, which refused a folded arm — a link that hangs, then a forearm raised) | a swinging part's name; for `rises`, a part whose chain does rise |
5087
- | <a id="CHECK_ENVIRONMENT"></a>`CHECK_ENVIRONMENT` | `check --environment` (or `build --environment`, forwarded): the schedule is refused, the detail rigc's own sentence verbatim. Read once by rig-c's reader (`readEnvironmentSchedule`, `@rig-c/core/src/environment.ts`) before anything is built — nothing at the path, a file that is not JSON or not an object, a `spec` other than `rigc-environment/1`, a key besides `spec`, `steps` or a step's `t`, `wind`, `gravity`, no step, a `t` not finite, below 0 or not after the step before, a `wind` or `gravity` not a pair of finite numbers (JSON's `1e999` reads as `Infinity` and is refused), a step stating neither — and `--out` is not written (`build` refuses under `[assemble]`, before it assembles). After the build, what `rigc render --environment` refuses of a well-formed schedule on this rig, quoted from its `--environment <path>:` line: no physics constraint, a half no constraint answers, two steps on one frame at 12 fps, a step after the last frame of the animation rendered. Also refused before anything is built: a `motion.json` with no animation, where no render `check` makes could pose the schedule | the schedule — rigc's sentence names the entry, the field, the value found and the value required; for a rig with no physics constraint, drop the flag: no world moves it |
5088
+ | <a id="CHECK_ENVIRONMENT"></a>`CHECK_ENVIRONMENT` | `check --environment` (or `build --environment`, forwarded): the schedule is refused, the detail rigc's own sentence verbatim. Read once by rig-c's reader (`readEnvironmentSchedule`, `@rig-c/core/environment`) before anything is built — nothing at the path, a file that is not JSON or not an object, a `spec` other than `rigc-environment/1`, a key besides `spec`, `steps` or a step's `t`, `wind`, `gravity`, no step, a `t` not finite, below 0 or not after the step before, a `wind` or `gravity` not a pair of finite numbers (JSON's `1e999` reads as `Infinity` and is refused), a step stating neither — and `--out` is not written (`build` refuses under `[assemble]`, before it assembles). After the build, what `rigc render --environment` refuses of a well-formed schedule on this rig, quoted from its `--environment <path>:` line: no physics constraint, a half no constraint answers, two steps on one frame at 12 fps, a step after the last frame of the animation rendered. Also refused before anything is built: a `motion.json` with no animation, where no render `check` makes could pose the schedule | the schedule — rigc's sentence names the entry, the field, the value found and the value required; for a rig with no physics constraint, drop the flag: no world moves it |
5088
5089
  | <a id="CHECK_WIND_BAND"></a>`CHECK_WIND_BAND` | `check --wind-band` (or `build --wind-band`, forwarded, issue #590): the sweep file is refused before anything is built, or cannot be measured against the rig. Refused before the build (`build` refuses under `[assemble]` before it assembles, and `--out` is not written): nothing at the path, a file that is not a JSON object, an unknown key (besides `note`, `*_note` and `x-…`), a missing or malformed field (`spec` must be `"rig-parts-wind-band/1"`; `min`, `max`, `step` finite numbers with `min < max`, `step > 0`, and `(max - min) / step` within `1e-6` of an integer at most `64`; `fps` a positive integer; `max_gap` and `max_ghost` non-negative integers; `pairs` a non-empty array of unique `"<a>:<b>"` strings with `a !== b`). Refused against the rig before `rigc build`: a slot in `pairs` not in `rig.json`'s `slots`, `motion.json` with no animation, a rig with no physics constraint, or a rig where no physics constraint has a non-zero `wind`. Refused after the first sweep render (before `check.json` is written, with `--out` cleaned): a pair in `pairs` whose drawings share no overlap band at the setup pose (`status: "skip"` or `banded === 0` in `seam.json`) | the `--wind-band` file or the rig — every field in `"rig-parts-wind-band/1"` is required (nothing falls back to `SEAM_OVER_MOTION`), each `"<a>:<b>"` pair must name two distinct slots whose art overlaps at rest, and the rig must declare at least one physics constraint that answers `wind` |
5089
5090
  | <a id="REQUIREMENTS_SEAM"></a>`REQUIREMENTS_SEAM` | a `seam` cannot be read against the rig: there is no `parts.json` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), or the two parts' art does not meet at the setup pose (no 4-adjacent pair). Refused before anything is built | two parts whose art touches, on a rig with its `parts.json` |
5090
5091
 
@@ -5157,7 +5158,7 @@ Absent declaration retains existing output bytes except optional new evidence on
5157
5158
 
5158
5159
  **Body-root seam identity** (issue #686). A limb must have exactly one segment0 part. Its declared bone must have one named immediate parent; the root slot must bind that bone (or its declared turn child). Non-limb parts with `topwear` or `bottomwear` source provenance, a slot on that parent (directly or through its verified generated turn child) and a named default-skin drawing are candidates. The body-side child must match `applyTurn`'s exact generated name, unique identity bone and parent, control and per-part transform binding; a similarly named bone alone is not proof (issue #708). The original carrier and pair slot names do not change under turn; stock setup pose `render --seam` selects all matching drawings with `banded > 0` (its existing alpha-overlap band, without dilation), so one selects one pair and two select both pairs. When eligible candidates all return valid setup banded 0, that limb has no selected root pair: `unmeasuredRoots` retains its limb, bone, parent, segment0 part, candidates and named reason, reported as SKIP, not measured PASS. Part spelling, nearest geometry and shared mesh influences select nothing. On `wind` the pairs are `topwear|upperarm_r`, `topwear|upperarm_l`, `bottomwear|thigh_r` and `bottomwear|thigh_l`; “trunk” is not a part name. Existing pairs keep their order; a reversed explicit declaration deduplicates with the root pair and retains both declarations. An ambiguous/missing named binding, no eligible candidate, or unreadable/undrawn setup data is `CHECK_MOTION_SEAM_ROOT`, naming the part and candidates. Valid zero-band metadata is carried separately as optional `roots.unmeasured`, never as a fabricated measured pair. The all-zero root block remains present and its bar is skipped; measured FAIL takes precedence over SKIP, while measured PASS plus any unmeasured root stays SKIP. No body provenance leaves legacy pair selection unchanged and supplies no body-root coverage.
5159
5160
 
5160
- Renderer metadata and exact raster reads use the promised `@rig-c/core/render-shared` entry (rig-c >=2.69.0 <3.0.0). It registers no Spine runtime; where that runtime resolves from rig-c's own package root, the existing `@rig-c/core/render` entry registers the official Spine side explicitly. An install without Spine keeps rig-c's own candidate chooser and gate; candidate or loader failures never select a guessed fallback.
5161
+ Renderer metadata and exact raster reads use the promised `@rig-c/core/render-shared` entry (from rig-c 2.69.0). It registers no Spine runtime; where that runtime resolves from rig-c's own package root, the existing `@rig-c/core/render` entry registers the official Spine side explicitly. An install without Spine keeps rig-c's own candidate chooser and gate; candidate or loader failures never select a guessed fallback.
5161
5162
 
5162
5163
  The body-root bar counts stock partitions `gapOpen` plus `ghostA` or `ghostB` owned by the pair's UNDER-owned drawing in resolved draw order on each active frame, comparing their summed cells against the unchanged strict floor `px > round(max(box width, box height) / cell)` on the original raw kind boxes (their union when both kinds contribute), never the narrowed partition boxes; `gapCovered` and OVER-owned ghost cells do not count (issue #711). The metric mode is `gap-open+under-ghost`; rows retain the exact partition counts, UNDER owner and counted total in `cells`, including excluded and under-floor readings. Missing partitions, inconsistent sums or unavailable ownership are named `FRAMES_SIDECAR` refusals, never inferred zero. An active rendered environment fails `CHECK_SEAM_OVER_MOTION` above that floor, setting overall PASS false and exits 1. Existing internal-joint and explicit-only pairs remain warnings: their PASS, exit status and bar count are unchanged; reference worlds remain informational. The root bar is counted once, and absence is not a zero or a PASS. No whole-box alpha exemption or consumer compositor is used. This gate samples the supplied animations at 12 fps, not another motion or player scene.
5163
5164
 
@@ -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
@@ -6682,6 +6740,26 @@ The trial's role relief/width ratios (face 0.1264803, front hair 0.1018772,
6682
6740
  back hair 0.0857722, ornament 0.0375868, garment 0.0990473) came from one
6683
6741
  head-turn demo's p5..p95 depth and part widths. They are measured starting
6684
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
+
6685
6763
  The owner's preference for per-part volume is subjective provenance, not an
6686
6764
  automated quality conclusion. Model licence posture remains the owner's choice.
6687
6765
 
package/docs/STABILITY.md CHANGED
@@ -62,7 +62,7 @@ defaults are the help's too, and none is guessed from the art.
62
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) |
63
63
  | `compare` | `--left`, `--right`; optional `--map` |
64
64
  | `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
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) |
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) |
66
66
  | `loop` | `--frames`, `--out`; optional `--palette` |
67
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) |
68
68
  | `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
@@ -116,6 +116,18 @@ rules that make the contract:
116
116
  - **`turn`** (issue #251) adds a control bone `turn` under `root` that nothing
117
117
  keys, and carries each part in `depths` by `(pivot − depth) / (pivot − nearest
118
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.
119
131
  - **`scene`** (issue #242) declares the painting a scene — the figure and what
120
132
  is round it — and takes no field: `{}`. With it `assemble` reads the
121
133
  recomposite inside the figure (the union of the full run's layers at alpha
@@ -135,7 +147,7 @@ part name, `[i]` an entry of a list):
135
147
  - **generation.control.skeleton**, the pose file it names when it is no built-in — required: `width`, `height`, `points`
136
148
  - **seethrough** — required: `resolution`, `steps`, `seed`, `offload`; optional: `head_box`, `arm_split`
137
149
  - **seethrough.arm_split** — required: `order`, and one of `x` or `line` (issue #439)
138
- - **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`
139
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
140
152
  - **assemble.extend_below_crop[i]** — required: `part`, `run`, `tag`
141
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
@@ -206,6 +218,11 @@ part name, `[i]` an entry of a list):
206
218
  - **waist** — required: `top`, `bottom`, `upper`, `lower`, `bone`, `constraint` (#650; absent keeps the ordinary bindings and graph)
207
219
  <!-- /stable:config -->
208
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
+
209
226
  `TY14` holds the set of field names in backticks above to the set of names the
210
227
  loader's object checks spell (every `c.object(…)` in `src/config.ts`, with the lists
211
228
  they name), in both directions, and refuses an object check whose key list it cannot
@@ -252,7 +269,7 @@ keeps complete canonical raw pair data (boxes/cut metadata included), omitting
252
269
  `picture` unless its image exists. Semantic signatures preserve original source
253
270
  attachment UVs/topology/weights, own image content and world vertices/colour,
254
271
  not atlas pages/UVs or global piece ordinals. Unknown/no-art support stays conservative.
255
- Renderer metadata and exact raster reads use the promised `@rig-c/core/render-shared` entry (rig-c >=2.69.0 <3.0.0). It registers no Spine runtime; where that runtime resolves from rig-c's own package root, the existing `@rig-c/core/render` entry registers the official Spine side explicitly. An install without Spine keeps rig-c's own candidate chooser and gate; candidate or loader failures never select a guessed fallback.
272
+ Renderer metadata and exact raster reads use the promised `@rig-c/core/render-shared` entry (from rig-c 2.69.0). It registers no Spine runtime; where that runtime resolves from rig-c's own package root, the existing `@rig-c/core/render` entry registers the official Spine side explicitly. An install without Spine keeps rig-c's own candidate chooser and gate; candidate or loader failures never select a guessed fallback.
256
273
 
257
274
  An unavailable, nonfinite, unrepresentable or oversized union, or nonempty official pinned.outside, refuses `CHECK_SEAM_DOMAIN`. Geometry margins outside the authored canvas are included in U, not treated as opaque-clip evidence. State-by-state geometry reads retain bounds/identities rather than all decoded pages or poses. `--keep-stills` keeps
258
275
  canonical frames in `setup_still/` and fitted diagnostics in `setup_still_auto/`.
@@ -311,20 +328,28 @@ in the package.
311
328
 
312
329
  ## rig-c
313
330
 
314
- `dependencies` takes `@rig-c/core` `2.82.0` exactly. Bun 1.4 or later
331
+ `dependencies` takes `@rig-c/core` `3.0.0` exactly. Bun 1.4 or later
315
332
  is the declared floor (`engines.bun` `>=1.4.0`; older 1.4 releases are not tested in
316
333
  CI, which runs `latest`, and compatibility with an older 1.4 release is handled when
317
334
  reported as an issue). What this package relies on in rig-c, every import
318
335
  read off `src/` and `cli.ts`:
319
336
 
320
- - the named entries `@rig-c/core/mesh` (the outline functions, `measureAuthoredMeshFit`,
321
- `reduceMesh`, `skinningEnvelopeBone`, `writeMeshQualityReport` and their types)
322
- and `@rig-c/core/meshcompare` (`compareMeshesInMotion`);
323
- - the deep paths its `exports` map carries: `@rig-c/core/src/rig.ts` (`parseRigSpec`,
324
- `RIG_SPEC_VERSION`, the constraint keys), `@rig-c/core/src/errors.ts`,
325
- `@rig-c/core/src/png.ts`, `@rig-c/core/src/transform.ts` (the y flip, through
326
- `src/coords.ts` only), `@rig-c/core/tools/plate.ts` (the PNG codec) and
327
- `@rig-c/core/tools/font5x7.ts`;
337
+ - the named entries its `exports` map carries (from `@rig-c/core` 3.0.0 `./*.ts` deep
338
+ file paths are gone, and `TY39` refuses any in the tree):
339
+ `@rig-c/core/mesh` (the outline functions, `measureAuthoredMeshFit`, `reduceMesh`,
340
+ `skinningEnvelopeBone`, `writeMeshQualityReport` and their types),
341
+ `@rig-c/core/meshcompare` (`compareMeshesInMotion`, `comparePreparedMotion`,
342
+ `prepareMotionReference`), `@rig-c/core/compile` (`f32`), `@rig-c/core/core`
343
+ (`RUNTIME_PI`), `@rig-c/core/deformgen` (`evaluateDeformTransform`),
344
+ `@rig-c/core/environment` (`ENVIRONMENT_SPEC`, `EnvironmentError`,
345
+ `environmentResponse`, `readEnvironmentSchedule`), `@rig-c/core/errors`
346
+ (`CompileError`), `@rig-c/core/font5x7` (`GLYPH_H`, `drawText`),
347
+ `@rig-c/core/passthrough` (`DRAWN_ALPHA`), `@rig-c/core/plate` (the PNG codec),
348
+ `@rig-c/core/png` (`assertPng`), `@rig-c/core/render` (`GeometryFile`,
349
+ `GeometryFrame`), `@rig-c/core/render-shared` (`Viewport`), `@rig-c/core/rig`
350
+ (`parseRigSpec`, `RIG_SPEC_VERSION`, the constraint keys and types),
351
+ `@rig-c/core/stop` (`expandStops`) and `@rig-c/core/transform` (the y flip and
352
+ frame transforms, through `src/coords.ts` only);
328
353
  - the `rigc` command: `build` and `render`, and `--version`, whose `entry:` line
329
354
  says which validator gated a build (`check.json`'s `rigc_entry`).
330
355
 
@@ -507,10 +532,17 @@ The separate ordering scalar is unchanged. No-image outputs retain legacy bytes.
507
532
  ### Declared setup-painting regions
508
533
 
509
534
  The optional `setup_painting` key in `spine-parts-requirements/1` adds a
510
- `SETUP_MATCHES_PAINTING` observation only when declared. It requires authored
535
+ `SETUP_MATCHES_PAINTING` measurement when declared. It requires authored
511
536
  `rgb_tolerance` and named native-stage boxes. Each optional `max_pixels` is
512
- that whole region's limit; omission is SKIP, never PASS. Existing files without
513
- the key retain their output bytes and renderer calls. Other requirement fields
537
+ that whole region's limit; omission is SKIP, never PASS. With `--source` but
538
+ no declaration, the console, `check.json` and the report's skipped-bar list now
539
+ say SKIP with the reason and the `--requirements` declaration remedy; exit
540
+ codes stay unchanged. No metric, region or limit is invented. Without source
541
+ and without the key, the line stays absent. Declared output bytes and renderer
542
+ calls remain unchanged. `check --json` prints the completed `check.json` bytes
543
+ to stdout and keeps the ordinary check's exit code; early input refusals and
544
+ usage errors keep their existing named text and exits. `build --json` is refused
545
+ with a `check --json` remedy; build keeps its multi-stage text report. Other requirement fields
514
546
  remain independent. The [authoring contract](AUTHORING.md#setup-against-the-painting)
515
547
  defines white compositing, the figure domain, strict RGB comparison, top
516
548
  contributor attribution and named measurement refusals. The check observes the
@@ -540,3 +572,9 @@ encoding, convexity sign and refusal remedies are in
540
572
  calls these stages; no-config builds retain their existing behavior and bytes.
541
573
  Group numbers and unit depths are explicit; no provider or absolute scale is
542
574
  inferred. A new depth stage never replaces Core's rig gate.
575
+ `relief` is an explicit rig-pixel value, not an implicit width multiplier.
576
+ Part-width and head-referenced calibrations are authored recipe choices;
577
+ changing a calibration requires preparing its recipe again. Compare the same
578
+ provider maps and preserve their provenance. Each report row names the part,
579
+ declared relief, group `zScale` and clipping over original alpha>=128 support;
580
+ 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.87.0",
3
+ "version": "2.0.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": {
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "ag-psd": "^31.0.2",
70
- "@rig-c/core": "2.84.0"
70
+ "@rig-c/core": "3.0.0"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@esotericsoftware/spine-core": "4.3.13",
package/src/apng.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Pure: bytes in, bytes out, no clock, no file system. The only thing it leans
6
6
  * on is `node:zlib` for DEFLATE, which is also all rig-c's own PNG codec
7
- * (`rig-c/tools/plate.ts`) leans on; the chunk framing and CRC are that
7
+ * (`@rig-c/core/plate`) leans on; the chunk framing and CRC are that
8
8
  * codec's `pngChunk`, so there is one PNG chunk writer in the dependency tree,
9
9
  * not two.
10
10
  *
@@ -34,7 +34,7 @@
34
34
  * bitstream encoder, and none ships here.
35
35
  */
36
36
  import { deflateSync } from 'node:zlib';
37
- import { PNG_SIGNATURE, pngChunk } from '@rig-c/core/tools/plate.ts';
37
+ import { PNG_SIGNATURE, pngChunk } from '@rig-c/core/plate';
38
38
  import { histogram, mapToPalette, medianCut, type PaletteError, paletteError, type Rgba } from './palette.ts';
39
39
  import type { Raster } from './raster/types.ts';
40
40
 
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
package/src/build.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_SEAM_RULE } from './seamrule.ts';
1
2
  import { SETUP_PAINTING_LINE, setupPaintingLines } from './setuppainting.ts';
2
3
  import { writeMaterialSources } from './cutmaterialio.ts';
3
4
  import { CUT_MATERIAL_LINE, CUT_MATERIAL_SOURCES, materialLines } from './cutmaterial.ts';
@@ -164,7 +165,7 @@ export interface AssembleStageInput {
164
165
  /** The head run, or null for one run that holds the face (issue #359). */
165
166
  head: string | null;
166
167
  config: string;
167
- seam: SeamRule;
168
+ seam?: SeamRule;
168
169
  project: ProjectRule;
169
170
  /** `--alt` entries, merged with `config.assemble.alternates` by set name (issue #454). */
170
171
  alts?: readonly AlternateCliInput[];
@@ -327,13 +328,14 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
327
328
  // The early door: assemble runs before propose has drafted bones, meshes,
328
329
  // regions and motion, so it requires only what it reads (issue #20).
329
330
  const cfg = loadEarlyConfig(input.config, 'assemble');
331
+ const seam = input.seam ?? cfg.assemble.seam ?? DEFAULT_SEAM_RULE;
330
332
  const src = readSource(input.source);
331
333
  const runs = applyTakenLayers(readRuns(input.full, input.head), src, cfg, input.config);
332
334
  for (const note of runs.notes) log(`note: ${note}`);
333
335
  const fields = stageFields(cfg, runs.single ? { w: src.width, h: src.height } : null);
334
336
  const under = readUnders(input.config, fields.cuts);
335
337
  const caps = fields.limbs === undefined ? undefined : readCaps(input.config, fields.limbs);
336
- const result = assemble({ source: src, full: runs.full, head: runs.head, depth: cfg.depth, depthBase: dirname(resolve(input.config)), ...fields, under, ...(caps === undefined ? {} : { caps }), seamRule: input.seam, projectRule: input.project });
338
+ const result = assemble({ source: src, full: runs.full, head: runs.head, depth: cfg.depth, depthBase: dirname(resolve(input.config)), ...fields, under, ...(caps === undefined ? {} : { caps }), seamRule: seam, projectRule: input.project });
337
339
 
338
340
  // Issue #454: alternate painting sets, assembled before any file is written.
339
341
  const configDir = dirname(resolve(input.config));
@@ -673,7 +675,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
673
675
  ...(fields.yieldRefused === undefined ? {} : { yieldRefused: fields.yieldRefused }),
674
676
  ...(fields.turnMovingPair === undefined ? {} : { turnMovingPair: fields.turnMovingPair }),
675
677
  ...(fields.turn === undefined ? {} : { turn: fields.turn, regions: fields.regions, meshes: fields.meshes, motion: fields.motion }),
676
- seamRule: input.seam,
678
+ seamRule: seam,
677
679
  projectRule: input.project,
678
680
  ...(altExclude.length === 0 ? {} : { exclude: altExclude }),
679
681
  ...(altIslands.length === 0 ? {} : { limbIslands: altIslands }),
@@ -2218,7 +2220,7 @@ export interface BuildInput {
2218
2220
  /** The head run, or null for one run that holds the face (issue #359). */
2219
2221
  head: string | null;
2220
2222
  out: string;
2221
- seam: SeamRule;
2223
+ seam?: SeamRule;
2222
2224
  project: ProjectRule;
2223
2225
  loop: boolean;
2224
2226
  /** `--page-edges` for both packed builds, the rig stage's gate and the check's artifact. */
@@ -2253,7 +2255,7 @@ export interface BuildInput {
2253
2255
  windBand?: string;
2254
2256
  }
2255
2257
 
2256
- /** What `build` does with one flag `check` takes: hands it to its check stage, takes it as its own, or refuses it because build supplies the value itself. */
2258
+ /** What `build` does with one flag `check` takes: hands it to its check stage, takes it as its own, or refuses it because build supplies the value or report itself. */
2257
2259
  export type CheckFlagInBuild =
2258
2260
  | { as: 'forwarded' }
2259
2261
  | { as: 'own'; why: string }
@@ -2280,6 +2282,7 @@ export const CHECK_FLAGS_IN_BUILD: Readonly<Record<string, CheckFlagInBuild>> =
2280
2282
  '--no-extremes': { as: 'forwarded' },
2281
2283
  '--environment': { as: 'forwarded' },
2282
2284
  '--wind-band': { as: 'forwarded' },
2285
+ '--json': { as: 'supplied', why: 'build supplies its multi-stage text report; run check --rig <out>/rig --parts <out> --out <dir> --json for the completed check object' },
2283
2286
  };
2284
2287
 
2285
2288
  /** The requirements file build's check reads (issue #556): `read` is where it is opened, `recorded` what `check.json` names it by. */
@@ -2315,7 +2318,7 @@ export function buildRequirements(flag: string | undefined, declared: string | u
2315
2318
  ]);
2316
2319
  }
2317
2320
 
2318
- /** The `build` refusal of a `check` flag build supplies itself ({@link CHECK_FLAGS_IN_BUILD}), or null for any other flag. */
2321
+ /** The `build` refusal of a `check` flag whose value or report build supplies itself ({@link CHECK_FLAGS_IN_BUILD}), or null for any other flag. */
2319
2322
  export function suppliedCheckFlag(flag: string): string | null {
2320
2323
  const row = Object.hasOwn(CHECK_FLAGS_IN_BUILD, flag) ? CHECK_FLAGS_IN_BUILD[flag] : undefined;
2321
2324
  return row?.as === 'supplied' ? `build does not take "${flag}"; ${row.why}` : null;
@@ -2408,7 +2411,6 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
2408
2411
  rmSync(join(out, FIGURE_MASK_FILE), { force: true });
2409
2412
  // The idle-keys value is named only when it is not the default, so a build without the flag, or with the default, prints the line it printed before issue #95.
2410
2413
  const idleKeys = input.idleKeys ?? DEFAULT_IDLE_KEYS;
2411
- log(`rig-parts build: ${input.config} -> ${out}, seam rule ${input.seam}, projection rule ${input.project}, page edges ${input.pageEdges}, pack shape ${input.packShape ?? DEFAULT_PACK_SHAPE}${idleKeys === DEFAULT_IDLE_KEYS ? '' : `, idle keys ${idleKeys}`}${input.loop ? `, with the idle loop${input.leadIn === undefined ? '' : ` after a lead-in of ${input.leadIn} period(s)`}` : ''}`);
2412
2414
 
2413
2415
  let req: BuildRequirements | null = null;
2414
2416
  let configRecord: InputFileRecord | null = null;
@@ -2420,6 +2422,8 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
2420
2422
  // Issue #556: the flag, or the config's declaration when there is none; both naming other bytes is refused here.
2421
2423
  // Issue #607: the bytes this read parsed are the ones check.json's config record hashes, under the path as typed.
2422
2424
  const loaded = loadConfigWithBytes(input.config);
2425
+ const seam = input.seam ?? loaded.config.assemble.seam ?? DEFAULT_SEAM_RULE;
2426
+ log(`rig-parts build: ${input.config} -> ${out}, seam rule ${seam}, projection rule ${input.project}, page edges ${input.pageEdges}, pack shape ${input.packShape ?? DEFAULT_PACK_SHAPE}${idleKeys === DEFAULT_IDLE_KEYS ? '' : `, idle keys ${idleKeys}`}${input.loop ? `, with the idle loop${input.leadIn === undefined ? '' : ` after a lead-in of ${input.leadIn} period(s)`}` : ''}`);
2423
2427
  configRecord = inputFileRecord(loaded.bytes, input.config);
2424
2428
  req = buildRequirements(input.requirements, loaded.config.requirements, input.config);
2425
2429
  // The same for the requirements file's own shape; its names resolve against the rig, in the check, before it builds.
@@ -2429,7 +2433,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
2429
2433
  // And the steady-wind band spec (issue #590), as check reads it.
2430
2434
  if (input.windBand !== undefined) readWindBand(input.windBand);
2431
2435
  stages.assemble(
2432
- { source: input.source, full: input.full, head: input.head, config: input.config, seam: input.seam, project: input.project, ...(req === null ? {} : { requirements: req.read }), ...(input.alts === undefined ? {} : { alts: input.alts }) },
2436
+ { source: input.source, full: input.full, head: input.head, config: input.config, seam, project: input.project, ...(req === null ? {} : { requirements: req.read }), ...(input.alts === undefined ? {} : { alts: input.alts }) },
2433
2437
  { partsJson: join(out, 'parts.json'), partsDir: join(out, 'parts'), recomposite: join(out, 'recomposite_rig.png'), errorMap: join(out, ERROR_MAP_FILE) },
2434
2438
  prefixed('assemble'),
2435
2439
  );
package/src/check.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SETUP_PAINTING_LINE, setupPaintingBounds, setupPaintingLine, type SetupPainting, type SetupPaintingLine } from './setuppainting.ts';
1
+ import { SETUP_PAINTING_LINE, UNDECLARED_SETUP_PAINTING_REASON, setupPaintingBounds, setupPaintingLine, type SetupPainting, type SetupPaintingLine, type SetupPaintingObservation } from './setuppainting.ts';
2
2
  import { readMaterialSources } from './cutmaterialio.ts';
3
3
  import { CUT_MATERIAL_LINE, checkJointMaterial, type MaterialLine } from './cutmaterial.ts';
4
4
  import { ART_JOINT_LINE, artJointLine, type ArtJointLine } from './artjoints.ts';
@@ -170,9 +170,9 @@ import {
170
170
  type SeamPair,
171
171
  stretchRequirementLine,
172
172
  } from './requirements.ts';
173
- import { RIG_BONE_INHERIT, type RigSpec as RigcRigSpec } from '@rig-c/core/src/rig.ts';
174
- import { expandStops } from '@rig-c/core/src/stop.ts';
175
- import { ENVIRONMENT_SPEC, EnvironmentError, environmentResponse, readEnvironmentSchedule } from '@rig-c/core/src/environment.ts';
173
+ import { RIG_BONE_INHERIT, type RigSpec as RigcRigSpec } from '@rig-c/core/rig';
174
+ import { expandStops } from '@rig-c/core/stop';
175
+ import { ENVIRONMENT_SPEC, EnvironmentError, environmentResponse, readEnvironmentSchedule } from '@rig-c/core/environment';
176
176
  import { ART_ALPHA } from './mesh.ts';
177
177
  import { connectedComponents } from './raster/components.ts';
178
178
  import { alphaComposite } from './raster/composite.ts';
@@ -329,8 +329,8 @@ export const DEFAULT_PACK_MODE: PackMode = { pageEdges: DEFAULT_PAGE_EDGES, pack
329
329
  /**
330
330
  * The profile every rigc build this package runs is gated under. rig-c 2.50.0
331
331
  * renamed `spine-html` to `rig-play` and runs the same rules under it; the old
332
- * name is accepted only until a later minor release and prints a deprecation
333
- * line on stderr (selftest `CK20` holds every stage's build to the new name).
332
+ * name was removed in rig-c 3.0.0 (selftest `CK20` holds every stage's build to
333
+ * the new name and verifies the old one is refused).
334
334
  */
335
335
  export const GATE_PROFILE = 'rig-play';
336
336
 
@@ -2730,8 +2730,8 @@ export interface CheckFigures {
2730
2730
  requirements?: RequirementsBlock;
2731
2731
  /** Issue #880: only when requirements declares fixed_skeleton. */
2732
2732
  [ART_JOINT_LINE]?: ArtJointLine;
2733
- /** Only when setup_painting is explicitly declared. */
2734
- [SETUP_PAINTING_LINE]?: SetupPaintingLine;
2733
+ /** Under --source: the declared observation, or an explicit undeclared SKIP. */
2734
+ [SETUP_PAINTING_LINE]?: SetupPaintingObservation;
2735
2735
  [CUT_MATERIAL_LINE]?: MaterialLine;
2736
2736
  PASS: boolean;
2737
2737
  }
@@ -5309,7 +5309,7 @@ function environmentRefused(given: string, sentence: string): PartsError {
5309
5309
 
5310
5310
  /**
5311
5311
  * `--environment <file>` read once, before anything is built (issue #538): the file's bytes hashed and handed to
5312
- * rig-c's own reader, `readEnvironmentSchedule` (`rig-c/src/environment.ts`), which refuses every malformed schedule
5312
+ * rig-c's own reader, `readEnvironmentSchedule` (`@rig-c/core/environment`), which refuses every malformed schedule
5313
5313
  * — a `spec` other than {@link ENVIRONMENT_SPEC}, a key it does not read, no step, a `t` that is not finite, at or
5314
5314
  * above 0 and after the step before, a `wind` or `gravity` that is not a pair of finite numbers, a step that changes
5315
5315
  * nothing — by entry, field, the value found and the value required. Nothing here reads the schedule's meaning: its
@@ -6476,7 +6476,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
6476
6476
  const setHints = new Map<string, string>();
6477
6477
  const fixedSkeleton = reqFile?.fixed_skeleton;
6478
6478
  let artJoints: ArtJointLine | null = null;
6479
- let setupPainting: SetupPaintingLine | null = null;
6479
+ let setupPainting: SetupPaintingObservation | null = inp.source !== null && reqFile?.setup_painting === undefined
6480
+ ? { status: 'SKIP', reason: UNDECLARED_SETUP_PAINTING_REASON } : null;
6480
6481
  if (inp.parts !== null || inp.source !== null || fixedSkeleton !== undefined) {
6481
6482
  try {
6482
6483
  mkdirSync(stillDir, { recursive: true });
package/src/config.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { SEAM_RULES, type SeamRule } from './seamrule.ts';
1
2
  import type { TakeLayers } from './takelayers.ts';
2
3
  import { paintingExtensionProblems, type PaintingExtension } from './paintingextension.ts';
3
4
  import { baseAnchorProblems, type BaseAnchor } from './baseanchor.ts';
@@ -40,7 +41,7 @@ import type { DepthConfig } from './depthsheets.ts';
40
41
  *
41
42
  * Coordinates in `bones`, `meshes` and `motion` are RIG pixels, y down, origin
42
43
  * top-left — the parts' own space. The y flip to Spine happens once, in
43
- * `rig-c/src/transform.ts` (see `src/coords.ts`), when the rig is built.
44
+ * `@rig-c/core/transform` (see `src/coords.ts`), when the rig is built.
44
45
  *
45
46
  * 🔗 **`constraints` is rig-c's, verbatim (issue #92).** An optional list
46
47
  * of constraint objects in rig-c's own rig-spec shape (`RigConstraint`:
@@ -90,7 +91,7 @@ import type { DepthConfig } from './depthsheets.ts';
90
91
  */
91
92
  import { existsSync, readFileSync, statSync } from 'node:fs';
92
93
  import { dirname, resolve } from 'node:path';
93
- import { RIG_BONE_INHERIT, RIG_SKIN_CONSTRAINT_KEYS, type RigBoneInherit, type RigConstraint, type RigSkinConstraintKey, type RigStop } from '@rig-c/core/src/rig.ts';
94
+ import { RIG_BONE_INHERIT, RIG_SKIN_CONSTRAINT_KEYS, type RigBoneInherit, type RigConstraint, type RigSkinConstraintKey, type RigStop } from '@rig-c/core/rig';
94
95
  import { pyRound } from './round.ts';
95
96
  import { motionSeamProblems, type MotionSeamDeclaration } from './motionseams.ts';
96
97
  import { REQUIREMENTS_SPEC } from './requirements.ts';
@@ -300,6 +301,8 @@ export interface AlternateSetConfig {
300
301
  }
301
302
 
302
303
  export interface Assemble {
304
+ /** Optional per-character seam rule; an explicit CLI flag wins. */
305
+ seam?: SeamRule;
303
306
  /** Verified named replacements from another See-through run (#866). */
304
307
  take?: TakeLayers[];
305
308
  /** Rig pixels per source pixel. */
@@ -1622,7 +1625,7 @@ export interface EarlyConfig {
1622
1625
  depth?: DepthConfig;
1623
1626
  key: string;
1624
1627
  seethrough: SeeThrough;
1625
- assemble: { rig_scale: number; take?: TakeLayers[]; head_face_underfill?: boolean; yield_refused?: boolean; keep_under_occluders?: string[]; extend_painting?: PaintingExtension[] };
1628
+ assemble: { rig_scale: number; seam?: SeamRule; take?: TakeLayers[]; head_face_underfill?: boolean; yield_refused?: boolean; keep_under_occluders?: string[]; extend_painting?: PaintingExtension[] };
1626
1629
  }
1627
1630
 
1628
1631
  /** What plain `assemble` reads: the early config plus the plan `--propose-plan` printed. */
@@ -1691,8 +1694,9 @@ function parseEarlyConfigFrom(raw: Json, door: EarlyDoor, base: string | null):
1691
1694
  if (door === 'layers') {
1692
1695
  if ('seethrough' in t) checkSeeThrough(c, t.seethrough);
1693
1696
  if ('assemble' in t) {
1694
- const a = c.object('config.assemble', t.assemble, sectionRequired(door, 'assemble'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take']);
1697
+ const a = c.object('config.assemble', t.assemble, sectionRequired(door, 'assemble'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam']);
1695
1698
  if (a !== null) {
1699
+ checkSeam(c, a);
1696
1700
  if ('rig_scale' in a) c.number('config.assemble.rig_scale', a.rig_scale, 'positive');
1697
1701
  if ('head_face_underfill' in a && typeof a.head_face_underfill !== 'boolean') {
1698
1702
  c.fail('CONFIG_FIELD_TYPE', 'config.assemble.head_face_underfill', `is ${show(a.head_face_underfill)}; true or false is required`);
@@ -2457,10 +2461,17 @@ function checkTakeLayers(c: Check, value: Json): void {
2457
2461
  });
2458
2462
  }
2459
2463
 
2464
+ function checkSeam(c: Check, a: Record<string, Json>): void {
2465
+ if ('seam' in a && !(SEAM_RULES as readonly unknown[]).includes(a.seam)) {
2466
+ c.fail('CONFIG_FIELD_TYPE', 'config.assemble.seam', `is ${show(a.seam)}; one of ${SEAM_RULES.join(', ')} is required`);
2467
+ }
2468
+ }
2469
+
2460
2470
  function checkAssemble(c: Check, v: Json, door: 'assemble' | 'full'): { parts: string[]; patches: string[]; cuts: string[] } {
2461
2471
  const required = sectionRequired(door, 'assemble');
2462
- const a = c.object('config.assemble', v, required.filter((k) => k !== 'plan'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take']);
2472
+ const a = c.object('config.assemble', v, required.filter((k) => k !== 'plan'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam']);
2463
2473
  if (a === null) return { parts: [], patches: [], cuts: [] };
2474
+ checkSeam(c, a);
2464
2475
  if (required.includes('plan') && !('plan' in a)) {
2465
2476
  c.fail('CONFIG_FIELD_PRESENT', 'config.assemble.plan', 'is absent and required; `assemble --propose-plan` prints one from the two runs — paste its plan and extend_below_crop into config.assemble');
2466
2477
  }
package/src/coords.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * `flattenRig` goes back through `toWorld`. No rotation matrix is written out
23
23
  * by hand in this package.
24
24
  */
25
- import { cropToSpineY } from '@rig-c/core/src/transform.ts';
25
+ import { cropToSpineY } from '@rig-c/core/transform';
26
26
 
27
27
  /** A painting point on the accepted integer rig canvas, in setup-world coordinates. */
28
28
  export function paintingToSetupPoint(point: readonly number[], sourceSize: readonly number[], rigSize: readonly number[]): [number, number] {
@@ -34,5 +34,5 @@ export function setupToPaintingPoint(point: readonly number[], sourceSize: reado
34
34
  return [(point[0] + rigSize[0] / 2) / (rigSize[0] / sourceSize[0]), cropToSpineY(point[1], rigSize[1]) / (rigSize[1] / sourceSize[1])];
35
35
  }
36
36
 
37
- export { computeExactFrameTransforms, computeWorldTransforms, cropToSpineY, normaliseDegrees, toBoneLocal, toBoneLocalVector, toWorld } from '@rig-c/core/src/transform.ts';
38
- export type { BoneTransform } from '@rig-c/core/src/transform.ts';
37
+ export { computeExactFrameTransforms, computeWorldTransforms, cropToSpineY, normaliseDegrees, toBoneLocal, toBoneLocalVector, toWorld } from '@rig-c/core/transform';
38
+ export type { BoneTransform } from '@rig-c/core/transform';
@@ -962,7 +962,7 @@ export function partWorldBox(p: PartRecord, rigH: number, stage: StageBox): Worl
962
962
  /**
963
963
  * How many floating-point roundings stand between the rig and one coordinate
964
964
  * of a displacement {@link stillReading} returns, on its longest path. Every
965
- * number on it is a double: rigc's core poser (`rig-c/src/core/vertices.ts`,
965
+ * number on it is a double: rigc's core poser (`@rig-c/core/core`,
966
966
  * rig-c 2.15.0) reads a vertex's local coordinates and weights through
967
967
  * `Math.fround` and does the arithmetic in doubles, and `geometry.json` carries
968
968
  * those doubles. The float32 inputs are the same numbers in the rest vertex and
package/src/propose.ts CHANGED
@@ -131,7 +131,7 @@ import { explicitWeightRows, regionBone, type RegionSpec } from './config.ts';
131
131
  */
132
132
  import { existsSync } from 'node:fs';
133
133
  import { join } from 'node:path';
134
- import { drawText, GLYPH_H } from '@rig-c/core/tools/font5x7.ts';
134
+ import { drawText, GLYPH_H } from '@rig-c/core/font5x7';
135
135
  import { type BoneEntry, type CharacterConfig, type ConfigConstraint, type ConfigStop, contourRegionsOf, deriveBustRegions, type Limb, type MeshSpec, type PartBinding, parseConfig, type Point, type Segment, type SkeletonSections, turnRidersByBone } from './config.ts';
136
136
  export { contourRegionsOf, deriveBustRegions };
137
137
  import { computeExactFrameTransforms, cropToSpineY, normaliseDegrees, toWorld } from './coords.ts';
package/src/raster/png.ts CHANGED
@@ -12,14 +12,14 @@
12
12
  * refuses by name rather than misreading. So adding `pngjs` would have been a
13
13
  * second codec for the same bytes, and two codecs are two answers.
14
14
  *
15
- * 🔌 **The deep path is the interface.** rig-c's `exports` map carries
16
- * `./*.ts`, so `rig-c/tools/plate.ts` is how a consumer reaches the codec
17
- * and it is what the installed package resolves. If a rigc release narrows the
18
- * map to omit these paths, this import is what breaks, by name, and
19
- * `bun run smoke` is where that is seen first.
15
+ * 🔌 **The named entries are the interface.** `@rig-c/core/plate` and
16
+ * `@rig-c/core/png` are how a consumer reaches the codec and what the
17
+ * installed package resolves. If a rigc release moves or drops these entries,
18
+ * this import is what breaks, by name, and `bun run smoke` is where that is
19
+ * seen first.
20
20
  */
21
- import { assertPng } from '@rig-c/core/src/png.ts';
22
- import { decodePng, encodePng } from '@rig-c/core/tools/plate.ts';
21
+ import { assertPng } from '@rig-c/core/png';
22
+ import { decodePng, encodePng } from '@rig-c/core/plate';
23
23
  import { readFileSync, writeFileSync } from 'node:fs';
24
24
  import { type FloatImage, newFloatImage, newRaster, type Raster, RasterError } from './types.ts';
25
25
 
@@ -11,7 +11,7 @@
11
11
  * feathered blend weight).
12
12
  *
13
13
  * Nothing here knows about Spine's y-up world. The only conversion between the
14
- * two lives in `rig-c/src/transform.ts`, re-exported by `src/coords.ts`.
14
+ * two lives in `@rig-c/core/transform`, re-exported by `src/coords.ts`.
15
15
  */
16
16
  export interface Raster {
17
17
  readonly width: number;
package/src/remesh.ts CHANGED
@@ -75,8 +75,8 @@ import { applyMeshEdit, type DeltaApplied, type DeltaPlant, deltaSourceProblems,
75
75
  import { DEFAULT_IDLE_KEYS, type IdleKeys, type MeshAttachment, meshBoundBones, rigJsonText, type RigSpec } from './rig.ts';
76
76
  import { readRequirements } from './requirements.ts';
77
77
  import { pyRound } from './round.ts';
78
- import { type RigSpec as RigcRigSpec } from '@rig-c/core/src/rig.ts';
79
- import { expandStops } from '@rig-c/core/src/stop.ts';
78
+ import { type RigSpec as RigcRigSpec } from '@rig-c/core/rig';
79
+ import { expandStops } from '@rig-c/core/stop';
80
80
 
81
81
  /** The provenance file's spec. */
82
82
  export const REMESH_SPEC = 'rig-parts-remesh/1';
@@ -50,8 +50,8 @@ import { fixedSkeletonProblems, type FixedSkeleton } from './artjoints.ts';
50
50
  */
51
51
  import { existsSync, readFileSync, statSync } from 'node:fs';
52
52
  import { resolve } from 'node:path';
53
- import { CompileError } from '@rig-c/core/src/errors.ts';
54
- import { splitRigSkin, type RigSkin } from '@rig-c/core/src/rig.ts';
53
+ import { CompileError } from '@rig-c/core/errors';
54
+ import { splitRigSkin, type RigSkin } from '@rig-c/core/rig';
55
55
  import { cropToSpineY } from './coords.ts';
56
56
  import type { Problem } from './errors.ts';
57
57
  import { refuseIfAny } from './errors.ts';
package/src/rig.ts CHANGED
@@ -138,12 +138,12 @@ import { explicitAttachment, type ExplicitMeshReport } from './explicitmesh.ts';
138
138
  * order the objects are built in, and every number is rounded by `pyRound`).
139
139
  */
140
140
  import { type AutoSpec, type BoneEntry, type InfluenceMeshSpec, type WeightRule, type CharacterConfig, type ConfigConstraint, type ConfigStop, CONSTRAINT_FOLLOWS, constraintForRig, contourRegionsOf, type ContourSpec, DEFAULT_BUST_BONE_L, DEFAULT_BUST_BONE_R, type Point, type RibsSpec, ROOT_BONE, stopForRig, turnRidersByBone, weightsABone } from './config.ts';
141
- import { RUNTIME_PI } from '@rig-c/core/src/core/world.ts';
141
+ import { RUNTIME_PI } from '@rig-c/core/core';
142
142
  import { type BoneTransform, computeExactFrameTransforms, computeWorldTransforms, cropToSpineY, normaliseDegrees, paintingToSetupPoint, setupToPaintingPoint, toBoneLocal, toWorld } from './coords.ts';
143
143
  import { type Problem, refuseIfAny, PartsError } from './errors.ts';
144
144
  import { recordedFocusProblems } from './setfocus.ts';
145
145
  import { eyeLineDegrees } from './headframe.ts';
146
- import type { RigBoneInherit, RigStop } from '@rig-c/core/src/rig.ts';
146
+ import type { RigBoneInherit, RigStop } from '@rig-c/core/rig';
147
147
  import { f32 } from '@rig-c/core/compile';
148
148
  import type { MeshCounts, MeshQualityReport, MeshReductionInput, MotionAmplitude, ReducedMesh, Termination } from '@rig-c/core/mesh';
149
149
  import { bindWeightedVertices, buildGridMesh, buildSegmentsLattice, type EntrySegment, influenceShares, segmentDistance as rigcSegmentDistance, type SegmentsLattice, type SegmentsLatticeReport } from '@rig-c/core/mesh';
@@ -309,6 +309,14 @@ function lineGap(a: number, b: number): number {
309
309
  /** The bones the face's line runs through: rigc's A51 draws its line through their origins, and the face bones are turned to that same line. */
310
310
  export const EYE_BONES: readonly [string, string] = ['eye_r', 'eye_l'];
311
311
 
312
+ /**
313
+ * Issue #903: midpoint of absolute setup eye-line rolls measured on the public
314
+ * controls (maximum 4.864514437760526 degrees across the five parts examples,
315
+ * dancer and the head-turn demo) and three-quarter (32.106255955117845 degrees).
316
+ * This separates those inputs; it is not an anatomical or visual-quality bar.
317
+ */
318
+ const HEAD_TURN_ROLL_LIMIT = (4.864514437760526 + 32.106255955117845) / 2;
319
+
312
320
  /**
313
321
  * The eye line (issue #239). It exists when parts.json holds an eye on each
314
322
  * side — the first part tagged `eyewhite-r` and the first tagged `eyewhite-l`
@@ -3656,6 +3664,15 @@ export function buildWaistPairSource(
3656
3664
  boneLandmarks.set(bName, [bEntry.x, bEntry.y]);
3657
3665
  }
3658
3666
  const turnWarnings: Problem[] = [];
3667
+ if ((cfg.turn?.depths !== undefined || cfg.turn?.roles !== undefined) && line.degrees !== null && Math.abs(line.degrees) > HEAD_TURN_ROLL_LIMIT) {
3668
+ const faces = parts.parts.filter(p => tagOf(p.from) === 'face' && p.cut_from === undefined).map(p => p.name);
3669
+ const names = faces.length === 0 ? line.parts : faces;
3670
+ turnWarnings.push({
3671
+ code: 'RIG_TURN_HEAD_ROLL',
3672
+ object: `${names.length === 1 ? 'part' : 'parts'} ${names.map(n => `"${n}"`).join(', ')}`,
3673
+ detail: `setup eye-line roll ${pyRound(line.degrees, 6)} degrees (Spine, counter-clockwise), absolute ${pyRound(Math.abs(line.degrees), 6)} exceeds limit ${pyRound(HEAD_TURN_ROLL_LIMIT, 6)} degrees; read through authored ${EYE_BONES.join(' and ')} origins on ${line.parts.map(n => `"${n}"`).join(' and ')} (an eye-origin proxy, not painting detection); the declared head turn uses upright yaw/pitch axes on an already rolled head; inspect the neck at both turn extremes; warning only, no pose or output changed`,
3674
+ });
3675
+ }
3659
3676
  if (cfg.turn?.roles !== undefined || cfg.turn?.body !== undefined) {
3660
3677
  buildDepth25DTurn(rig, motion, {
3661
3678
  warnings: turnWarnings,
package/src/scene.ts CHANGED
@@ -93,7 +93,7 @@
93
93
  */
94
94
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
95
95
  import { dirname, join, resolve } from 'node:path';
96
- import type { RigStop } from '@rig-c/core/src/rig.ts';
96
+ import type { RigStop } from '@rig-c/core/rig';
97
97
  import { checkStage, gateThroughRigc, type Log } from './build.ts';
98
98
  import { ACCEPTABLE_BARS, barsNotMet, type CheckReport, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, type PackMode, requirementsNotPass, type RigcRunner } from './check.ts';
99
99
  import { CONSTRAINT_BONE_FIELDS, ROOT_BONE } from './config.ts';
@@ -0,0 +1,11 @@
1
+ /** Shared seam choices for the config reader and assembly. */
2
+ export type SeamRule = 'near-white' | 'silhouette';
3
+ export const SEAM_RULES: readonly SeamRule[] = ['near-white', 'silhouette'];
4
+
5
+ /**
6
+ * The seam rule a run uses when none is named. Set by measurement, not taste:
7
+ * the silhouette rule ships as the default only if it lowers the error pixel
8
+ * count on every oracle character without raising mean |d| on any
9
+ * (ORACLE_assemble.md, *faithful vs silhouette*).
10
+ */
11
+ export const DEFAULT_SEAM_RULE: SeamRule = 'near-white';
@@ -74,6 +74,10 @@ export interface SetupPaintingLine {
74
74
  reason?: string;
75
75
  }
76
76
 
77
+ /** An undeclared observation has no measured pixels, inferred regions or limit. */
78
+ export type SetupPaintingObservation = SetupPaintingLine | { status: 'SKIP'; reason: string };
79
+ export const UNDECLARED_SETUP_PAINTING_REASON = 'setup_painting is undeclared; setup-to-painting was not measured. To measure it, give --requirements <file> declaring setup_painting.rgb_tolerance and setup_painting.regions with name, box and authored max_pixels limits';
80
+
77
81
  /** Pixel metric only. Caller supplies the actual renderer's textured setup and
78
82
  * its per-pixel drawn-piece owners on precisely the source grid. RGB under
79
83
  * zero alpha is ignored: both observations are source-over white first.
@@ -119,7 +123,8 @@ export function setupPaintingLine(
119
123
  ...(noLimit ? { reason: 'a region has no declared max_pixels; it is measured as SKIP, never PASS' } : {}) };
120
124
  }
121
125
 
122
- export function setupPaintingLines(line: SetupPaintingLine): string[] {
126
+ export function setupPaintingLines(line: SetupPaintingObservation): string[] {
127
+ if (!('regions' in line)) return [`${SETUP_PAINTING_LINE}: SKIP — ${line.reason}`];
123
128
  return [`${SETUP_PAINTING_LINE}: ${line.status}${line.reason === undefined ? '' : ` — ${line.reason}`}; ${line.registration}; ${line.compositing}`,
124
129
  ...line.regions.flatMap(r => [`${SETUP_PAINTING_LINE}: region "${r.name}" ${r.status} — box [${r.box.join(', ')}], pixelcount ${r.pixel_count}, limit ${r.limit ?? 'undeclared (SKIP)'}, RGB tolerance ${line.rgb_tolerance}`,
125
130
  ...r.slots.map(s => `${SETUP_PAINTING_LINE}: slot "${s.slot}" region "${r.name}" — box [${s.box.join(', ')}], pixelcount ${s.pixel_count}; region total ${r.pixel_count}, limit ${r.limit ?? 'undeclared (SKIP)'}`)])];
package/src/sheet.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { existsSync } from 'node:fs';
20
20
  import { basename, dirname, join } from 'node:path';
21
- import { drawText, GLYPH_H } from '@rig-c/core/tools/font5x7.ts';
21
+ import { drawText, GLYPH_H } from '@rig-c/core/font5x7';
22
22
  import { type Problem, refuseIfAny } from './errors.ts';
23
23
  import { type LayerSet, OPAQUE_ALPHA_ABOVE, readLayers } from './layers.ts';
24
24
  import { readParts } from './parts.ts';
package/src/structure.ts CHANGED
@@ -71,8 +71,8 @@ import { explicitWeightRows, regionBone } from './config.ts';
71
71
  * Pure but for reading the three files it is handed.
72
72
  */
73
73
  import { existsSync, readFileSync } from 'node:fs';
74
- import { CompileError } from '@rig-c/core/src/errors.ts';
75
- import { parseRigSpec, RIG_SPEC_VERSION, type RigBone as RigcBone, type RigSpec as RigcSpec, splitRigSkin } from '@rig-c/core/src/rig.ts';
74
+ import { CompileError } from '@rig-c/core/errors';
75
+ import { parseRigSpec, RIG_SPEC_VERSION, type RigBone as RigcBone, type RigSpec as RigcSpec, splitRigSkin } from '@rig-c/core/rig';
76
76
  import { type ConfigConstraint, CONSTRAINT_FOLLOWS, contourRegionsOf, parseConfig, parseProposalSections, type Point, ROOT_BONE, type SkeletonSections, weightsABone } from './config.ts';
77
77
  import { computeExactFrameTransforms, cropToSpineY, normaliseDegrees, toWorld } from './coords.ts';
78
78
  import { PartsError, type Problem, refuseIfAny } from './errors.ts';
package/src/turn.ts CHANGED
@@ -38,7 +38,7 @@ import { sampleTurnTriangleField, turnTriangleCoordinates } from './turnfield.ts
38
38
  import { PartsError } from './errors.ts';
39
39
  import { f32 } from '@rig-c/core/compile';
40
40
  import { warnUncoveredBodyTurn } from './turncoverage.ts';
41
- import { evaluateDeformTransform } from '@rig-c/core/src/deformgen.ts';
41
+ import { evaluateDeformTransform } from '@rig-c/core/deformgen';
42
42
 
43
43
  /**
44
44
  * The layers that ride on the face (issue #271): See-through makes no depth
@@ -1,7 +1,7 @@
1
1
  /** Report missing body-turn declarations from shown setup alpha and emitted fields. */
2
2
  import { f32 } from '@rig-c/core/compile';
3
- import { DRAWN_ALPHA } from '@rig-c/core/src/passthrough.ts';
4
- import { evaluateDeformTransform, type DeformTransform } from '@rig-c/core/src/deformgen.ts';
3
+ import { DRAWN_ALPHA } from '@rig-c/core/passthrough';
4
+ import { evaluateDeformTransform, type DeformTransform } from '@rig-c/core/deformgen';
5
5
  import { computeWorldTransforms, paintingToSetupPoint, toWorld } from './coords.ts';
6
6
  import { extendDepthSilhouette, sampleDepthFloat } from './depthsheets.ts';
7
7
  import { edgeLength } from './length.ts';
@@ -1,5 +1,5 @@
1
1
  /** Source-alpha overlap selects turn fields, never skin bindings. */
2
- import { DRAWN_ALPHA } from '@rig-c/core/src/passthrough.ts';
2
+ import { DRAWN_ALPHA } from '@rig-c/core/passthrough';
3
3
  import { computeWorldTransforms, setupToPaintingPoint, toWorld } from './coords.ts';
4
4
  import type { Depth25DTurnOptions } from './turn.ts';
5
5
  import type { Problem } from './errors.ts';