rig-parts 1.66.0 → 1.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -287,7 +287,7 @@ rig-parts assemble --source painting.png --full layers/full --head layers/head -
287
287
  # -> a large red hole neither run holds? add an assemble.patches entry (cut from the painting) and assemble again
288
288
  rig-parts propose --parts work/rig --source painting.png --out work
289
289
  # -> proposal.json, basis.json (what each bone rests on) and render/landmarks.png; per-bone coverage lines;
290
- # correct it, copy bones/meshes/regions/limbs/motion/constraints/turn/proposed_rig_scale into config.json, each it wrote
290
+ # correct it, copy bones/meshes/regions/limbs/motion/constraints/turn/motion_seams/proposed_rig_scale into config.json, each it wrote
291
291
  # (limbs only with --keypoints, constraints only with --cloth, turn only with --full/--head;
292
292
  # a limbs block left out is refused next)
293
293
  rig-parts propose --parts work/rig --source painting.png --out work --from-config config.json
@@ -407,8 +407,8 @@ These are limits of the approach, stated so nobody reads more into a green run:
407
407
  - **rig-parts generates no images.** The painting and See-through layers are made
408
408
  by models the user runs on their GPU or a web demo. No weights ship here; the
409
409
  optional `comfy` adapter drives the user's own ComfyUI host and models.
410
- - **One depth value per layer.** See-through gives each layer a single depth, so a
411
- layer that is in front of another in one place and behind it in another is drawn
410
+ - **Legacy turns use one ordering depth per layer.** See-through also supplies per-pixel
411
+ sheets, now preserved as inputs for the subsequent generator consumer (#773/#763). A layer that is in front of another in one place and behind it in another is drawn
412
412
  right in the still (the painting's own pixels are projected onto it) and wrong once
413
413
  it moves.
414
414
  - **No expression or lip-sync, and no jiggle.** The mouth is one layer; there is no
@@ -572,3 +572,22 @@ the interface it holds.
572
572
  ## Licence
573
573
 
574
574
  MIT — see [LICENSE](LICENSE). Third-party terms: [NOTICE.md](NOTICE.md).
575
+
576
+
577
+ ### Per-layer depth input
578
+
579
+ `config.depth` explicitly enables scalar input. Without it, `depth_filename`
580
+ is a legacy hint: no depth-file I/O, validation, fetch, reference or output is
581
+ activated, even when files or in-memory sheets exist. Active See-through sheets
582
+ are read as opaque grayscale source data. Declare positive finite `depth.zScale`;
583
+ eight bits do not supply world units. See-through far-high encoding is explicitly `near: "black"`. User
584
+ per-layer/run-canvas fallbacks declare image, near and zScale themselves; a
585
+ missing declared file is an error, not a fallback. The ordering median remains
586
+ separate, and its agreement reading cannot certify an image's polarity.
587
+
588
+ Assembly emits registered part sheets and references; rig emits PAD4 sheets and
589
+ `depth_sheets.json`. Existing weighted meshes do not use them yet: production
590
+ Core grid/contour generators and turns belong to #763. No-image builds keep the
591
+ legacy output bytes. `comfy seethrough --config <file> --depth-run <full|head>`
592
+ uses the same opt-in; its default captures art only. Standalone authored part
593
+ references remain active declarations. See [the input contract and refusals](docs/AUTHORING.md#per-layer-depth-sheets-773).
package/cli.ts CHANGED
@@ -779,6 +779,7 @@ usage:
779
779
  rig-parts comfy seethrough --image <png> --out <dir> [--host <url>]
780
780
  [--resolution 1024] [--steps 30] [--seed 42] [--offload] [--lama] [--nf4]
781
781
  [--prefix spine_parts] [--wait 1800] [--timeout 3600] [--poll 3]
782
+ [--config <config.json> --depth-run <full|head>]
782
783
  Optional. Run See-through on a ComfyUI box with the jtydhr88/ComfyUI-See-through
783
784
  wrapper installed, and write the wrapper form \`layers\` reads into <out>
784
785
  (absent or empty): layers.json, parts/<tag>.png, meta.json, previews/.
@@ -788,6 +789,9 @@ usage:
788
789
  writes <out> only after the layer reader accepts what came back.
789
790
  --lama turns the wrapper's LaMa inpainting on and --nf4 its nf4
790
791
  quantisation; both are off unless given, as --offload is.
792
+ Depth hints are inactive by default. --config reads its optional depth
793
+ block; with that block --depth-run explicitly selects full or head.
794
+ Active declared sheets are fetched and validated, never inferred.
791
795
 
792
796
  rig-parts comfy paint --config <config.json> --out <dir> [--host <url>]
793
797
  [--seeds 1] [--seed0 <generation.seed>] [--wait 1800] [--timeout 3600] [--poll 3]
@@ -1055,7 +1059,7 @@ const TAKES = {
1055
1059
  check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment', '--wind-band'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
1056
1060
  loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
1057
1061
  assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config'], switches: ['--propose-plan'] },
1058
- 'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll'], switches: ['--offload', '--lama', '--nf4'] },
1062
+ 'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll', '--config', '--depth-run'], switches: ['--offload', '--lama', '--nf4'] },
1059
1063
  'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
1060
1064
  inputs: { valued: ['--source', '--config', '--out'], switches: [] },
1061
1065
  build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in', '--alt', '--set', '--environment', '--wind-band'], switches: ['--loop', '--seam-pairs', '--keep-stills', '--all-sets'] },
@@ -1761,6 +1765,10 @@ async function cmdComfy(args: string[]): Promise<number> {
1761
1765
  // config comfy paint cannot paint from is refused with no box named.
1762
1766
  const paint = sub === 'paint' ? loadPaintConfig(configPath as string) : null;
1763
1767
  const cfg = paint === null ? null : paint.config;
1768
+ const depthConfig = sub === 'seethrough' && configPath !== undefined ? loadEarlyConfig(configPath, 'layers').depth : undefined;
1769
+ const depthRun = v.get('--depth-run');
1770
+ if (sub === 'seethrough' && depthConfig !== undefined && depthRun !== 'full' && depthRun !== 'head') return usage('active config.depth requires --depth-run <full|head>; the source run is not inferred');
1771
+ if (sub === 'seethrough' && depthConfig === undefined && depthRun !== undefined) return usage('--depth-run requires --config with an explicit depth block');
1764
1772
  const host = resolveHost(v.get('--host'), process.env.COMFY_HOST);
1765
1773
  const client = new ComfyClient(host, { poll: poll as number, request: 30 });
1766
1774
  if (sub === 'seethrough') {
@@ -1784,6 +1792,7 @@ async function cmdComfy(args: string[]): Promise<number> {
1784
1792
  quant: on.has('--nf4') ? ('nf4' as const) : ('none' as const),
1785
1793
  wait: wait as number,
1786
1794
  timeout: timeout as number,
1795
+ ...(depthConfig === undefined ? {} : { depth: { config: depthConfig, run: depthRun as 'full' | 'head', base: dirname(resolve(configPath as string)) } }),
1787
1796
  };
1788
1797
  console.log(`rig-parts comfy seethrough: ${image} -> ${out}`);
1789
1798
  console.log(` resolution ${run.resolution}, steps ${run.steps}, seed ${run.seed}, offload ${run.offload}, lama ${run.lama}, quant ${run.quant}; wait <= ${run.wait} s, timeout ${run.timeout} s`);
package/docs/AUTHORING.md CHANGED
@@ -136,10 +136,11 @@ read by no CPU stage).
136
136
  | `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 |
137
137
  | `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 |
138
138
  | `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/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 |
139
- | `turn` | rig (adds the turn control) | **proposed** only with `propose --full`/`--head` (issue #251), then corrected: `{pivot, depths, deform?, band?, radius?, surface?: {radius?}, head?: {pivot?, radius?}}`. `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 |
139
+ | `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?}`. When `roles` is declared (`{<part>: "surface" \| "unit" \| "rear"}`, issue #763; `CONFIG_TURN_ROLE_UNKNOWN`, `CONFIG_TURN_SURFACE_REQUIRED`), 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`), 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` under `faceshift`, and `yaw_dial` and `pitch_dial` under `root` (`CONFIG_TURN_BONE_FREE`, `RIG_TURN_NAME_FREE`), re-parents `"unit"` and `"rear"` bones and mesh weights to `faceshift` and `rear_shift`, 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 `"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 |
140
140
  | `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 |
141
141
  | `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) |
142
142
  | `proposed_rig_scale` | assemble, rig, build, `propose --from-config`/`--compare` | **proposed** by `propose` (issue #354): `parts.json`'s `scale_rig_per_source`, the `assemble.rig_scale` the rig sections (`bones`, `meshes`, `limbs`, `motion`) were drafted at. Held equal to `assemble.rig_scale` when present (`CONFIG_PROPOSED_RIG_SCALE`; 0, negative or not a number: `CONFIG_FIELD_TYPE`). Absent on a hand-authored config, where no byte of any output moves |
143
+ | `motion_seams` | propose, assemble, check | Optional `[{part, neighbour}]`, using output part names. Each unordered pair is declared once; self-pairs, unknown names, wrong shapes and unknown fields refuse (`CONFIG_MOTION_SEAM`, `PARTS_MOTION_SEAM`, `CHECK_MOTION_SEAM_DECLARATION`). Empty adds no pair. Assemble copies the declaration into `parts.json`; check merges it with existing pairs and never regenerates a removed declaration. It uses the existing non-root WARN floor and changes no bar or exit status. See *Explicit motion seam observations* below. |
143
144
  | `requirements` | build (read in place of `--requirements` when the flag is not given), and so its check | **authored**, optional, never proposed (issue #556): a path, relative to the config file's directory, to the `spine-parts-requirements/1` file the build's check reads (§7, *Declared requirements*) — so a rig that needs one (a `not_swinging` part, a `may_fold`) rebuilds from its config alone. `build` reads it before assemble, as it reads the flag's file, and `check.json`'s `requirements.file` records it with `path` as declared. Given beside `--requirements`, the two must name the same file once resolved, or a file of the same bytes, and the declaration is read and recorded (a build given both writes what the config alone writes); otherwise `BUILD_REQUIREMENTS_CONFLICT`, naming both. Not a non-empty string: `CONFIG_FIELD_TYPE`; no such file, where the config is read from a path: `CONFIG_FILE_PRESENT`. `check`, `compose`, `mesh-draft`, `remesh` and `retune` do not read it: their `--requirements` is the only door. Absent, nothing is read and no byte of any output moves |
144
145
 
145
146
  A skeleton alternate may declare `anchor: {neck: "head"}` (issue #543):
@@ -1280,7 +1281,7 @@ silhouette holds besides the art, each pixel counted once: `filledHolePixels` (t
1280
1281
  holes), `grownPixels` (added by the margin), `pinchFilledPixels` (added by the pinch fill) and
1281
1282
  `grownHolePixels` (transparent pixels the grown silhouette encloses — a notch whose mouth grew
1282
1283
  shut))
1283
- and, per region, the vertices its bone reaches (`g > 0`) and holds alone (`g = 1`).
1284
+ and, per region (`regions: [{ name, bone, reached, whole }, ...]`, `src/rig.ts`), the vertices its bone reaches (`reached`, where the region owns a nonzero contribution, `g > 0`) and holds alone (`whole`, where `g >= 1`) — static setup-weight vertex counts, not a rendered frame-to-frame or `> 0.5 px` counterfactual-motion count.
1284
1285
  Across contour, automatic and influence reports, `regions[].bone` and `reach.nearest`
1285
1286
  name the authored template bones used to calculate the regions and reach. Stops and turn
1286
1287
  can rebind the emitted attachment after those calculations; these diagnostic names are
@@ -2108,8 +2109,8 @@ it runs in under a second; the build it saves runs the render and `check` as wel
2108
2109
  roughly as area / spacing², including the transition band: for a circle that area
2109
2110
  extends to `r + band`, not just `r`. The outline, background grid and sampled region
2110
2111
  rings also contribute, so multiplying the *whole* triangle count by that ratio
2111
- incorrectly scales the fixed part. Read the part's `mesh_report.json` row first, and use
2112
- `mesh-stats` for the candidate rather than waiting for a rendered build.
2112
+ incorrectly scales the fixed part. Read the part's `mesh_report.json` row first (`regions[].{name, bone, reached, whole}` and `contour` in `rig/mesh_report.json` or `out/rig/mesh_report.json` — see *A contour mesh*, *What it reports*, above, which `mesh-stats` does not print), and use
2113
+ `mesh-stats` for the candidate's `{part, mode, vertices, triangles}` dry count rather than waiting for a rendered build.
2113
2114
 
2114
2115
  A bounded calibration on the public [family office config](https://github.com/rig-c/examples/blob/d5a1768ce4a2d1e3f5726d1739cd0da4a63b0e13/family/roa/config.json)
2115
2116
  uses both `topwear` bust regions at spacing 14. Its pinned
@@ -2252,15 +2253,15 @@ the wind — is the consumer's, and the recipe drafts no key and no stimulus.
2252
2253
  ```
2253
2254
 
2254
2255
  (The selftest's fixture recipe, `fixtures/cloth.ts`: values chosen to exercise every
2255
- rule, not advice. It leaves `length_ratio` and `strength` out, so the recipe's own
2256
+ rule, not advice — in particular, `"back hair": { "chains": 2, … }` exercises explicit multi-tree splitting and is not a recommendation to declare two trees on a one-mass side. It leaves `length_ratio` and `strength` out, so the recipe's own
2256
2257
  defaults are what is built and measured below.)
2257
2258
 
2258
2259
  What each rule drafts (`src/cloth.ts` holds them):
2259
2260
 
2260
2261
  | rule | drafts | from |
2261
2262
  | --- | --- | --- |
2262
- | where | `bottomwear`: from `hip`, the root row 20 px below hip's y (the keyed chains' row), the tip row 4 px above the cloth's own bottom under each column (4 px above the part box's bottom when every column reaches the bottom band, or 4 px above where the cloth's alpha ends under that column on a floor-spread skirt whose middle hem ends above bare thighs; a column with no cloth below the root row gets no tree drafted there and emits a note saying so, issue #438). `back hair` that reaches below the shoulders: from `head`, per side of the eye axis (twin tails are a side each), the eye line to 4 px above its bottom. `front hair`: each strand that hangs below the chin (the lock rule's), from `head`, 0.22 face heights above the eye line to 5 px below the strand; the fringe's three columns keep their two links and their places | the keyed chains' own rows, which these replace |
2263
- | trees | tree k of `chains` owns the cell k/N .. (k+1)/N of the art's width in every row, and its links sit at the cell's centre in the 24-row band there, so a piece that widens fans its trees. On `back hair`, `chains` is optional (issues #390, #445, #461): when omitted, `propose --cloth` derives the tree count on each side from the side's **visible hair masses** — connected components of the `back hair` part's alpha below the eye-line root row (after skipping any undivided crown rows that bridge continuously across the eye axis), merged when they touch within one mesh grid cell — placing `max(1, floor(w / (4 * grid)) + 1)` trees inside each mass across its root-row opaque width `w` (one tree per `4 * grid` of horizontal reach, where a tree's segment weight `1 / (d + r)^2` with `r ≈ 0.42 * grid` drops to `MIN_WEIGHT = 0.03` at `2 * grid` on each side of the trunk), rooted at that mass's own top row and with every link inside that mass's column range; `propose`'s `LINT` names the masses it found and their columns when a side's tree count or link leaves them. An explicit `chains` that exceeds what the narrower side's opaque width at the eye-line root row holds (`max(1, min(floor(w_r / grid), floor(w_l / grid)))`) or places a root off the 15 px dilated art is refused (`PROPOSE_CLOTH_BACK_HAIR_CHAINS`). With `split`, the trunk's tip is the fork, and branch b of `into` follows the centre of its share of the cell. Chains are named `<prefix><k>_` (trunk) and `<prefix><k><a, b, …>_` (branches): `skirt`, `hairback_r`/`hairback_l`, `lockt`. With `split.hold_tip: true` (issues #403, #392), sibling branches keep `strength.branch` across the fork, the mesh's `r` is raised to `grid`, and the rig stage blends each fork's sibling branch weight shares across four passes of 1-ring mesh-edge averaging (self weight 0.5, floor 0.005, cap 8 influences) — every share alike, trunk, branch and fixed bones, none held in place — holding the triangles where two branches' cells meet; when the recipe also declares `influence`, the drafted influence mesh carries `hold_tip: true` and `max` at least 8, and the rig stage smooths it by the same passes capped at that `max` (issue #615, the `meshes.<part>.influence` row) | `chains`, `links`, `split` |
2263
+ | where | `bottomwear`: from `hip`, the root row 20 px below hip's y (the keyed chains' row), the tip row 4 px above the cloth's own bottom under each column (4 px above the part box's bottom when every column reaches the bottom band, or 4 px above where the cloth's alpha ends under that column on a floor-spread skirt whose middle hem ends above bare thighs; a column with no cloth below the root row gets no tree drafted there and emits a note saying so, issue #438). `back hair` that reaches below the shoulders: from `head`, per side of the eye axis (twin tails are a side each), the eye line to 4 px above its bottom. `front hair`: each qualifying strand that hangs below the chin (the lock rule's: each connected component below `chin + 4` of height at least `0.3` face heights, `src/propose.ts`), from `head`, 0.22 face heights above the eye line to 5 px below the strand — the recipe's `chains`, `links` and `split` are applied separately to each qualifying hanging strand and do not replace the fringe's fixed three two-link columns (`bang_r`, `bang_c`, `bang_l` keep their two links and their places and take physics on both links) | the keyed chains' own rows, which these replace |
2264
+ | trees | tree k of `chains` (`N`) owns the cell k/N .. (k+1)/N of the art's width in every row, and its links sit at the cell's centre in the 24-row band there, so a piece that widens fans its trees. **Count unit of `chains`:** on `back hair`, an explicit `chains: N` is passed to **each emitted side of the eye axis** (`N` trees on `r` and `N` on `l` when both sides emit, not a combined total across both sides; `src/cloth.ts`, `src/propose.ts`); on `front hair`, `chains: N` is applied **separately to each qualifying hanging strand** and does not replace the fringe's fixed two-link columns (`src/propose.ts`). On `back hair`, `chains` is optional (issues #390, #445, #461): when omitted, `propose --cloth` derives the tree count on each side from the side's **visible hair masses** and root-row reach — it is **not** a universal default of 1 or 2 trees for every painting (`RECIPE_BACK_HAIR_CHAINS = 2` in `src/cloth.ts` is only the fallback when `draftTrees` is called with neither `masses` nor `chains`) — connected components of the `back hair` part's alpha below the eye-line root row (after skipping any undivided crown rows that bridge continuously across the eye axis), merged when they touch within one mesh grid cell — placing `max(1, floor(w / (4 * grid)) + 1)` trees inside each mass across its root-row opaque width `w` (one tree per `4 * grid` of horizontal reach, where a tree's segment weight `1 / (d + r)^2` with `r ≈ 0.42 * grid` drops to `MIN_WEIGHT = 0.03` at `2 * grid` on each side of the trunk), rooted at that mass's own top row and with every link inside that mass's column range; `propose`'s `LINT` names the masses it found and their columns when a side's tree count or link leaves them. An explicit `chains` that exceeds what the narrower side's opaque width at the eye-line root row holds (`max(1, min(floor(w_r / grid), floor(w_l / grid)))`) or places a root off the 15 px dilated art is refused (`PROPOSE_CLOTH_BACK_HAIR_CHAINS`). With `split`, the trunk's tip is the fork, and branch b of `into` follows the centre of its share of the cell. Chains are named `<prefix><k>_` (trunk) and `<prefix><k><a, b, …>_` (branches): `skirt`, `hairback_r`/`hairback_l`, `lockt`. With `split.hold_tip: true` (issues #403, #392), sibling branches keep `strength.branch` across the fork, the mesh's `r` is raised to `grid`, and the rig stage blends each fork's sibling branch weight shares across four passes of 1-ring mesh-edge averaging (self weight 0.5, floor 0.005, cap 8 influences) — every share alike, trunk, branch and fixed bones, none held in place — holding the triangles where two branches' cells meet; when the recipe also declares `influence`, the drafted influence mesh carries `hold_tip: true` and `max` at least 8, and the rig stage smooths it by the same passes capped at that `max` (issue #615, the `meshes.<part>.influence` row) | `chains`, `links`, `split` |
2264
2265
  | lengths | link j's length is the first's times q^j, q = `length_ratio`^(1/(links − 1)), the whole spanning root row to tip row: the links shorten toward the tip | `length_ratio` (default 0.5, below) |
2265
2266
  | stiffness | a trunk link takes `strength.trunk`, a branch link `strength.branch`, and the edge — the outermost branch on each outer side of the piece (both sides of a skirt or a strand; the outer side of each back-hair side; or, under `split.hold_tip: true` with `strength` left out, kept at `strength.branch` so a fork's sibling branches do not pull apart at the default 2:1 stiffness), or without `split` the outermost tree — `strength.edge`; a fringe column is a trunk, its outer two the edge. **Strength does not fall along a chain**: a tip reads soft because each link turns on top of every link above it, so the motion accumulates down the chain at one strength. Only the edge is declared softer | `strength` (default trunk 100, branch 100, edge 50, below) |
2266
2267
  | response | link j of an n-link path takes `wind` and `gravity` at j/(n − 1) of the way from the root value to the tip value; a single-link path takes the tip value. Each is a scalar **multiplier** of the environment the consumer sets (spine-core's skeleton `windX, windY`, `gravityX, gravityY`; rig-c 2.52.0's live track `setEnvironment`, rig-play 1.10.0's `environment()`): no direction is drafted | `wind`, `gravity` |
@@ -2891,7 +2892,7 @@ full loader before its assemble stage writes anything.
2891
2892
  no later stage can see — then
2892
2893
  `rig-parts propose --parts work/rig --source inputs/painting.png --out work --write-config config.json` →
2893
2894
  `work/proposal.json` and `work/render/landmarks.png` (+ `_head`). The flag saves
2894
- each of `bones`, `meshes`, `regions`, `limbs`, `motion`, `constraints`, `turn` and `proposed_rig_scale` it
2895
+ each of `bones`, `meshes`, `regions`, `limbs`, `motion`, `constraints`, `turn`, `motion_seams` and `proposed_rig_scale` it
2895
2896
  wrote into the config: `limbs` is written only with `--keypoints`, `constraints`
2896
2897
  only with `--cloth` and `turn` only with `--full`/`--head` (`PROPOSAL_CONFIG_SECTIONS` in `src/propose.ts` is the list,
2897
2898
  and the selftest's `TY18` holds this sentence to it). A `limbs` block left out
@@ -3048,7 +3049,7 @@ full loader before its assemble stage writes anything.
3048
3049
  The command-order selftest runs these stages on each fetched example from a
3049
3050
  config holding only `key`, `seethrough` and `assemble.rig_scale` (`RL01`). For
3050
3051
  the proposal step, copy `bones`, `meshes`, `regions`, `limbs`, `motion`,
3051
- `constraints`, `turn` and `proposed_rig_scale` into the config, each section the
3052
+ `constraints`, `turn`, `motion_seams` and `proposed_rig_scale` into the config, each section the
3052
3053
  proposal wrote. `RL01` exercises that manual route and does not execute
3053
3054
  `--write-config`. The save forms above write the fields owned by their mode;
3054
3055
  a saved draft still needs validation.
@@ -3071,14 +3072,14 @@ mesh-stats: total 1816 triangles over 9 mesh(es)
3071
3072
  ```
3072
3073
 
3073
3074
  (`demo` as tracked, nine lattices.) The package holds no triangle budget: a budget is the
3074
- consumer's, and the command prints no line against one. One line per mesh, in parts.json's
3075
+ consumer's, and the command prints no line against one. `mesh-stats` runs `buildRig` (`src/meshstats.ts`) and projects its `meshReport` via `meshStatsRows` to `{part, mode, vertices, triangles}` and the total (`meshStatsLines`) — one line per mesh, in parts.json's
3075
3076
  order, the row order of `mesh_report.json`; the mode is `lattice` for a `grid` entry,
3076
- `contour`, `auto` or `influence`. An `auto` part's count is its reduction's result: when rigc's
3077
+ `contour`, `auto`, `influence` or `explicit`. It writes no `mesh_report.json` or skeleton data and runs no `rigc` or `check`. For per-region vertex counts on a contour mesh (`regions: [{ name, bone, reached, whole }, ...]`, where `reached` counts vertices with a nonzero region contribution, `g > 0`, and `whole` counts vertices where `g >= 1`; `src/rig.ts`), read the built rig's `mesh_report.json` (`rig/mesh_report.json` from `rig`, or `out/rig/mesh_report.json` from `build --out out` — see §3, *A contour mesh*, *What it reports*, and *Recipe: a soft region driven by physics*): those fields are written by `rig` / `build` and are not printed by `mesh-stats`. Neither `mesh-stats`'s dry totals nor `mesh_report.json`'s static `regions[].{reached, whole}` setup-weight counts are a rendered `> 0.5 px` counterfactual-motion count or a claim that the config passed the compiler, coverage, motion or visual `check` gates (§3, *Recipe: a soft region driven by physics*). An `auto` part's count is its reduction's result: when rigc's
3077
3078
  motion gate refuses that result, the rig stage replays an earlier accepted step, which needs
3078
3079
  rigc, and the line says so. A problem the rig stage would refuse — a part with no PNG, an
3079
3080
  outline that does not close, a name `bones` does not declare — is the rig stage's own refusal,
3080
- by the same name, exit 1; it takes no `--requirements`, because the rig stage's meshes read
3081
- none, and no `--idle-keys`, because where the idle's keys go moves no vertex.
3081
+ by the same name, exit 1; it takes `--config` and `--parts` alone (no `--candidate`, `--regions`, `--report`, `--requirements`, because the rig stage's meshes read
3082
+ none, and no `--idle-keys`, because where the idle's keys go moves no vertex).
3082
3083
 
3083
3084
  ### For artists: your own layered file (issue #359)
3084
3085
 
@@ -3217,7 +3218,7 @@ valued destination path with two destination rules — **in place on an existing
3217
3218
  | --- | --- | --- |
3218
3219
  | `propose --head-box` | existing regular config | `seethrough.head_box` |
3219
3220
  | `assemble --propose-plan` | existing regular config; must name `--config` itself | `assemble.plan`, `assemble.extend_below_crop` |
3220
- | ordinary `propose --parts` without `--cloth` | existing regular config | each present `PROPOSAL_CONFIG_SECTIONS` value: `bones`, `meshes`, `regions`, `limbs`, `motion`, `constraints`, `turn`, `proposed_rig_scale` |
3221
+ | ordinary `propose --parts` without `--cloth` | existing regular config | each present `PROPOSAL_CONFIG_SECTIONS` value: `bones`, `meshes`, `regions`, `limbs`, `motion`, `constraints`, `turn`, `motion_seams`, `proposed_rig_scale` |
3221
3222
  | ordinary `propose --parts … --only-missing` (issues #671, #679) | existing regular config | only resolvable `meshes.<part>` or `regions.<part>` for an unattached part: attached covers existing `meshes` and `regions`, source parts consumed by the config's own `limbs.<name>.segments[].from`, and `assemble.patches` (always regions), with or without `--keypoints`. Entries are checked against the config's bones and chains, including mesh and `auto` bone/region references; an unresolved entry is never written. Each skipped part gets a note naming its attachment or the unresolved reference and why. An accepted split part from `full:drape-<s>` / `full:hand-<s>` also gains its missing `assemble.plan` row (drape before the full run's `handwear-<s>` row, hand after it); added row by row and entry by entry, every existing byte as read |
3222
3223
  | `propose --cloth <input.json>` | new file | the recipe's edits spliced into the cloth config; §3, *The config written*, gives entry ownership, hand-edit conflicts and `--from <previous.json>` |
3223
3224
 
@@ -3779,6 +3780,9 @@ See-through with another seed.
3779
3780
  | <a id="CONFIG_TAG_KNOWN"></a><a id="CONFIG_PART_NAME"></a><a id="CONFIG_PART_UNIQUE"></a>`CONFIG_TAG_KNOWN`, `CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` | a plan or extend entry names neither a v3 tag nor `author:<label>` (an `author:` tag with a malformed label says what the label must be), a part name that is not a file name, or a part or layer twice — a patch named like a plan part or another patch, and a cut's `into` named like a plan part, a patch or another cut's, included | `assemble.plan`, `assemble.patches`, `assemble.cuts` |
3780
3781
  | <a id="CONFIG_SET_FOCUS"></a>`CONFIG_SET_FOCUS` | optional `assemble.alternates[i].focus` (issue #719) must be exactly two finite painting-pixel coordinates `[x, y]`, x right/y down, inside that alternate painting (edges included); the loader checks the tuple and `assemble` checks the actual canvas before writing. Only with focus, `parts.json` carries the original point and `focus_source_size`; `sets.json` carries the point in setup-world coordinates through the accepted rig-canvas resize and `src/coords.ts`. Absent means no focus field or inferred hand position | author a point inside the painting or omit `focus`; after changing or removing it, re-run `assemble` and `rig`. Scene placement belongs to the consumer |
3781
3782
  | <a id="CONFIG_SET_LAYERS"></a>`CONFIG_SET_LAYERS` | `assemble.alternates[i].layers` names an unknown, uncovered or hidden part, or a painting patch with no run-layer source | name a covered layer-backed output part (a limb segment name, not its consumed source); use `author:<label>` for author art |
3783
+ | <a id="CONFIG_MOTION_SEAM"></a>`CONFIG_MOTION_SEAM` | motion_seams has a malformed entry, an unknown output part, a self-pair, duplicate/reversed pair or unknown entry field | declare each unordered pair once with two resolved output part names |
3784
+ | <a id="PARTS_MOTION_SEAM"></a>`PARTS_MOTION_SEAM` | parts.json motion_seams violates the same declaration shape/name/uniqueness contract | reassemble with the declared output part names |
3785
+ | <a id="CHECK_MOTION_SEAM_DECLARATION"></a>`CHECK_MOTION_SEAM_DECLARATION` | a directly supplied parts record has an invalid explicit motion seam declaration | supply a valid parts.json declaration; check never infers a replacement |
3782
3786
  | <a id="CONFIG_NAME_RESOLVES"></a>`CONFIG_NAME_RESOLVES` | a parent, segment, region, track, blink member, blink still part or bone, extend part, patch or cut `draw.before`, or cut `from` names nothing declared above it (a cut's `from` must be a plan part); or a constraint's `bones`, `target`, `source` or `bone` names a bone `bones` does not declare (the detail names the constraint and lists the bones that exist); in `meshes.<part>.auto`, a bone or region name nothing declares; or `bones[i].link_inherit` names a link outside its own chain (the detail lists that chain's expanded links) | the named reference, or the declaration it needs |
3783
3787
  | <a id="CONFIG_BONE_UNIQUE"></a>`CONFIG_BONE_UNIQUE` | a bone or chain declared twice, or `root` declared | `bones` |
3784
3788
  | <a id="CONFIG_BONE_INHERIT"></a>`CONFIG_BONE_INHERIT` | a single bone's `inherit`, or a chain's `link_inherit.<link>`, is a string that is not one of rig-c's five mode names as its table spells them (issue #348); the line lists all five. `"NoScale"` is refused too: the runtime would fold the first letter, and this loader folds and maps nothing | `bones[i].inherit` or `bones[i].link_inherit.<link>` — `normal`, `onlyTranslation`, `noRotationOrReflection`, `noScale` or `noScaleOrReflection` (§3, `bones`) |
@@ -4174,6 +4178,8 @@ documents do not ship). Each is an input this package built; none is an author's
4174
4178
  | <a id="REDUCE_THRESHOLD_RANGE"></a>`REDUCE_THRESHOLD_RANGE` | the art threshold is not a whole number in 1..255 | an input this package built, not an author's: report it with the line |
4175
4179
  | <a id="COMPARE_INPUT_MISSING"></a>`COMPARE_INPUT_MISSING` | a field of the input this package handed rig-c's `compareMeshesInMotion` is missing or malformed, two builds share an id, or one attachment is listed twice | an input this package built, not an author's: report it with the line |
4176
4180
  | <a id="COMPARE_INPUTS_DIFFER"></a>`COMPARE_INPUTS_DIFFER` | a candidate build differs from the reference outside what the comparison may vary (the compared mesh's vertices, UVs, triangles, hull, edges and weights); the path and both values are quoted | an input this package built, not an author's: report it with the line |
4181
+ | <a id="COMPARE_PREPARED_HANDLE"></a>`COMPARE_PREPARED_HANDLE` | the comparison received a handle not made by rig-c's `prepareMotionReference` | a programmatic caller must use the genuine opaque handle returned by `prepareMotionReference`; in the rig stage this is an input this package built, so report it with the line |
4182
+ | <a id="COMPARE_PREPARED_MISMATCH"></a>`COMPARE_PREPARED_MISMATCH` | the reference, schedule, attachment art or comparison options differ from the exact input bound to the prepared handle; the changed field is named | a programmatic caller must prepare a new handle from the exact changed reference-side input before comparing; in the rig stage this is an input this package built, so report it with the line |
4177
4183
  | <a id="COMPARE_REFERENCE_FAILS"></a>`COMPARE_REFERENCE_FAILS` | the reference — the unreduced source — fails its own art fit at the setup pose (coverage, overshoot or undercut against `sourceBounds`); a deviation from a reference that does not carry its own art is not evidence | the source, which is the author's: `meshes.<part>.auto.sourceBounds`, or the source's `spacing` or outline (`auto.source`) |
4178
4184
  | <a id="COMPARE_UV_CARRIER_NOT_UNIQUE"></a>`COMPARE_UV_CARRIER_NOT_UNIQUE` | a sample lies in two UV triangles that do not meet there at a shared vertex or edge: the mesh's UVs overlap or fold, so no one triangle carries it | an input this package built, not an author's: report it with the line |
4179
4185
  | <a id="COMPARE_WARMUP_UNSUPPORTED"></a>`COMPARE_WARMUP_UNSUPPORTED` | a physics warm-up other than 0 was asked of the comparison; physics resets at time 0 and steps from there | an input this package built, not an author's: report it with the line |
@@ -4287,6 +4293,14 @@ flag) and 8 `CLOTH_SETTLES` warnings for 9 (selftest `CK515`).
4287
4293
  **Steady-wind seam band** (`--wind-band`, issue #590). `check --wind-band <spec.json>` (or `build --wind-band <spec.json>`, forwarded) sweeps steady horizontal wind across a caller-declared grid and reports the contiguous `[x_lo, x_hi]` band in which each declared seam pair holds `gap <= max_gap` and `ghost <= max_ghost` across every animation in `motion.json`. Every field in `{ "spec": "rig-parts-wind-band/1", "min", "max", "step", "fps", "max_gap", "max_ghost", "pairs" }` is required — nothing falls back to `SEAM_OVER_MOTION`'s pairs, one-cell floor, or fps. At each swept step `x = min + k * step`, `check` runs `rigc render --seam <pairs> --fps <fps> --max 16 --environment` with a single-step steady schedule `{ "spec": "rigc-environment/1", "steps": [{ "t": 0, "wind": [x, 0], "gravity": [0, 1] }] }` (`seam.json` cells are in each pair's coarser drawing texels, independent of `--max`). **What it does not measure:** the sweep is steady wind (`wind: [x, 0], gravity: [0, 1]` at `t = 0`), not a time-varying gust (`x ± a` at 0.2 Hz); transient overshoot from a changing wind is not read by a steady sweep, and the band is documented and reported as the steady-wind band (`mode: "steady"`, `gravity: [0, 1]`). It is a reported line (`status: "REPORTED"`), never a bar: it prints one `WIND_BAND:` summary line plus one `WIND_BAND:` line per pair that breaks inside `[min, max]`, and writes `WIND_BAND` in `check.json` before `PASS`.
4288
4294
 
4289
4295
 
4296
+ **Explicit motion seam observations** (issue #750). `motion_seams` is optional in a proposal, config and `parts.json`. The pure `draftMotionSeams(config, parts)` generator reads source provenance and the final declared carrier hierarchy: a non-limb `topwear` mesh on exactly two single-bone carriers which are a named parent and child, and non-limb `handwear-r`/`handwear-l` whose carriers are strict descendants with the same nearest trunk carrier, either that parent or that child. A sleeve may branch from the parent as a sibling of the child; it binds neither trunk carrier itself, and carriers with different nearest trunks do not qualify. This establishes declared structure, not positive compiled vertex weights or setup alpha overlap; the stock seam reader observes the band. Other structures add nothing. No part spelling, motion keys, distance heuristic or ancestor in common supplies identity. Proposal copy/write-config and turn preserve this field. For an older authored config, retain its original and use the same generator on its frozen structure/source parts, adding only this declaration in a derived config with a provenance receipt; regenerating its other rig or motion sections changes the experiment. Check reads the declaration, never calls this generator, and never restores a removed pair. A duplicate with an existing limb/requirement/root pair retains each source; an independently proven body-root pair keeps its existing FAIL policy.
4297
+
4298
+ Every played non-root WARN, including existing limb pairs, additionally records the actual above-floor `worst` hit (`kind`, `index`, `px`, `cell`, `box`; maximum count, earliest frame on a tie, gap before ghost on a remaining tie) and `pose_at_worst` (`index`, actual raw `time`, `joints`). Each joint records `bone`, `parent`, `world_x_axis_degrees`, `parent_relative_setup_axis_delta_degrees_mod360`, `static_range`, `comparison` and, when needed, `reason`. Carrier paths come from the drawings actually shown at that frame and their positive weights or rigid binding, followed through the declared hierarchy. The SAME seam render supplies `--geometry`; animation, viewport, frame index and raw hit time are bound before its actual `setup.bones` and posed matrices are read. Animation frame 0 can already be posed and is never substituted for setup. Missing/mismatched geometry or matrices is a named refusal, never a zero angle or an estimate from source keys or frame/fps.
4299
+
4300
+ Parent-relative axes use the full inverse parent matrix; parent-only rotation cancels. Angles are CCW in world y-up, and the setup-relative delta is explicitly modulo 360 in [-180,180). A static range is copied unchanged from its limb record, or null when absent. `comparison: "axis-modulo-360"` describes normal, unsheared, nondegenerate axes and a range within that branch; it does not certify unwrapped winding or impose an angle bar. Special inheritance, shear/reflection, singular or degenerate axes, absent ranges and ambiguous winding report `comparison: "unavailable"` with a reason. These fields are observation evidence only. Existing `opens`, `px`, `max_px`, under-floor counts, root partitions and FAIL ownership keep their definitions.
4301
+
4302
+ Absent declaration retains existing output bytes except optional new evidence on existing WARN rows. With a declared no-chest control, compare standalone checks at identical paths: exclude only the newly selected seam line's selection, rows, derived status and reason when the baseline already has that container. The historical absent-container bar required seven keys, CLEAR and zero warnings in every animation; its measured raw exit 1 remains red because the original idle keys the chest even though the added greeting keys only the head, and optional `worlds` is valid source data. [HQ's post-verdict restatement](https://github.com/rig-c/parts/issues/750#issuecomment-6398631279) instead requires the complete source-derived `MotionSeamLine` schema, including optional reference worlds, exact metadata and generated selection, and independent raw-row audits. All added greeting rows must be clean; any WARN must have an actual keyed pair-joint witness from the original motion, compiled tracks and carrier ancestry. STABILITY's *Explicit motion seam observations* section defines this audit and lossless removal of the new container: every outside byte stays identical. Keep the historical red beside the separate re-evaluation. No input, threshold, angle gate, exit behavior or animation selector is changed. Whether an open armhole should eventually fail is an open severity decision outside this WARN contract.
4303
+
4290
4304
  **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.
4291
4305
 
4292
4306
  Renderer metadata and exact raster reads use the promised `rig-c/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/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.
@@ -4331,6 +4345,37 @@ and stay absent).
4331
4345
  | <a id="GUIDE_RULE_KNOWN"></a>`GUIDE_RULE_KNOWN` | `rig-parts guide <RULE>`: no row of this guide names the rule. The line, on stderr, lists the three nearest names the guide does hold; nothing is printed in their place | one of the names listed, spelled as the line printed it |
4332
4346
  | <a id="GUIDE_SECTION_KNOWN"></a>`GUIDE_SECTION_KNOWN` | `rig-parts guide §<n>`, `§<n>.<m>` or `§<n>.<m>.<k>`: there is no such section, or no such subsection under the one it names; the line, on stderr, names the deepest section that exists and lists its sections | a number the detail lists |
4333
4347
 
4348
+ ### Depth sheet refusals (#773)
4349
+
4350
+ | rule | means | change |
4351
+ | --- | --- | --- |
4352
+ | <a id="DEPTH_SHEET_PRESENT"></a>`DEPTH_SHEET_PRESENT` | an active source or authored part-reference file is missing | restore it; legacy hints are inactive without config.depth, but active declarations never fall back |
4353
+ | <a id="DEPTH_SHEET_DECODES"></a>`DEPTH_SHEET_DECODES` | declared bytes are not a readable PNG | supply the actual grayscale PNG |
4354
+ | <a id="DEPTH_SHEET_SIZE"></a>`DEPTH_SHEET_SIZE` | sheet dimensions differ from the declared layer/canvas | supply a sheet on that exact source grid |
4355
+ | <a id="DEPTH_SHEET_GREYSCALE"></a>`DEPTH_SHEET_GREYSCALE` | some R/G/B values differ | use scalar data, not a tinted preview; RGB-encoded gray is valid |
4356
+ | <a id="DEPTH_SHEET_OPAQUE"></a>`DEPTH_SHEET_OPAQUE` | some alpha is not255 | provide an opaque declared sheet; alpha is not an unknown-depth fallback |
4357
+ | <a id="DEPTH_SHEET_FILENAME"></a>`DEPTH_SHEET_FILENAME` | writer depth filename is not a plain file for a drawn layer | correct the manifest filename; hidden-art sheets are not drawn-layer inputs |
4358
+ | <a id="DEPTH_SHEET_UNAMBIGUOUS"></a>`DEPTH_SHEET_UNAMBIGUOUS` | duplicate locations differ, or a sheet collides with art | retain one byte-identical source and noncolliding output names |
4359
+ | <a id="DEPTH_ZSCALE_REQUIRED"></a>`DEPTH_ZSCALE_REQUIRED` | units are absent, nonfinite or not positive | author positive finite world units |
4360
+ | <a id="DEPTH_NEAR_REQUIRED"></a>`DEPTH_NEAR_REQUIRED` | user convention is absent or invalid | declare black or white; no detector chooses one |
4361
+ | <a id="DEPTH_SOURCE_AMBIGUOUS"></a>`DEPTH_SOURCE_AMBIGUOUS` | writer/user or layer/canvas declarations overlap | select one authoritative route explicitly |
4362
+ | <a id="DEPTH_SOURCE_RESOLVES"></a>`DEPTH_SOURCE_RESOLVES` | user key names no source layer | use the exact source tag in the selected run |
4363
+ | <a id="DEPTH_SOURCE_BASE"></a>`DEPTH_SOURCE_BASE` | a direct assembler caller supplied user paths without config base | pass the actual config-directory base |
4364
+ | <a id="DEPTH_PART_SUPPORT"></a>`DEPTH_PART_SUPPORT` | exact copied/generated material lacks source depth or compatible units | provide a declared scalar source/registration; do not infer or fill depth |
4365
+ | <a id="PARTS_DEPTH_REFERENCE"></a>`PARTS_DEPTH_REFERENCE` | persisted fields/provenance violate the reference shape | restore image, near, positive zScale and exact source records |
4366
+ | <a id="DEPTH_SILHOUETTE_SIZE_MISMATCH"></a>`DEPTH_SILHOUETTE_SIZE_MISMATCH` | `extendDepthSilhouette` or `padTurnDepthSheet` received a source depth sheet and artwork raster of different dimensions | supply the part's matching unpadded depth sheet and artwork raster |
4367
+ | <a id="CONFIG_TURN_COUNTER_RANGE"></a>`CONFIG_TURN_COUNTER_RANGE` | `config.turn.counter` is outside `[0, 1]` | supply a non-negative rear counter-translation fraction in `[0, 1]` |
4368
+ | <a id="CONFIG_TURN_ROLE_UNKNOWN"></a>`CONFIG_TURN_ROLE_UNKNOWN` | `config.turn.roles.<part>` is not `"surface"`, `"unit"` or `"rear"` | declare `"surface"`, `"unit"` or `"rear"` |
4369
+ | <a id="CONFIG_TURN_SURFACE_REQUIRED"></a>`CONFIG_TURN_SURFACE_REQUIRED` | `config.turn.roles` declares no `"surface"` part | declare at least one part (such as `face`) as `"surface"` |
4370
+ | <a id="CONFIG_TURN_EAR_DRAW_ORDER"></a>`CONFIG_TURN_EAR_DRAW_ORDER` | an ear part in `config.turn.roles` is drawn after a `"surface"` part in `assemble.plan` | place `ear_r` and `ear_l` before `face` in `assemble.plan` so the turning cheek occludes the far ear |
4371
+ | <a id="CONFIG_TURN_HEAD_BONE"></a>`CONFIG_TURN_HEAD_BONE` | `config.turn.roles` is declared without a single bone `"head"` in `config.bones` | declare `"head"` as a single bone in `config.bones` |
4372
+ | <a id="RIG_TURN_SURFACE_DEPTH_REQUIRED"></a>`RIG_TURN_SURFACE_DEPTH_REQUIRED` | a part with role `"surface"` in `config.turn.roles` carries no depth sheet in `parts.json` | run `assemble` with `config.depth` or provide `<part>_depth.png` in `parts.json` |
4373
+ | <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` `[cols, rows]` within the per-slot and total budgets |
4374
+ | <a id="RIG_TURN_HEAD_BONE"></a>`RIG_TURN_HEAD_BONE` | `buildRig` found no `"head"` bone when emitting 2.5D turn carriers | declare `"head"` in `config.bones` |
4375
+ | <a id="RIG_TURN_SHARED_DEPTH_SCALE"></a>`RIG_TURN_SHARED_DEPTH_SCALE` | `"surface"` parts bound to one head turn declare different `near` polarities or `zScale` values | use one shared `near` and `zScale` across all `"surface"` parts in the turn |
4376
+ | <a id="RIG_TURN_UNIT_DEPTH_REQUIRED"></a>`RIG_TURN_UNIT_DEPTH_REQUIRED` | a `"unit"` bone has neither a `config.turn.unitDepths` entry nor a readable `"surface"` depth sheet | provide a `"surface"` depth sheet or an explicit `config.turn.unitDepths.<part>` value |
4377
+ | <a id="RIG_TURN_EAR_DEPTH_BEHIND_CHEEK"></a>`RIG_TURN_EAR_DEPTH_BEHIND_CHEEK` | an ear bone in `config.turn.roles` has a calibrated depth at or in front of the cheek surface depth | place the ear behind the cheek surface (for example via `config.turn.unitDepths`) |
4378
+
4334
4379
  ## 7. The bars `check` enforces, and what only an eye answers today
4335
4380
 
4336
4381
  `check.json`'s `PASS` is true exactly when all of these hold (the reference
@@ -5307,3 +5352,72 @@ missing a path, given twice, or combined with `--shard`):
5307
5352
  enter `holes`; check names or indented detail lines containing `HOLE` or `SKIP` (such as
5308
5353
  `RG82_BLINK_NO_HOLE_…` or `SO21_ZERO_SETUP_BANDS_REMAIN_NAMED_SKIPS_…`) are counted as `PASS` or
5309
5354
  `FAIL` controls and never as `skips` or `holes` (`RT16`, `RT17`).
5355
+
5356
+ The WARN pose reader also requires the published stock coordinate header and
5357
+ the required source bone/parent roster. It binds the complete geometry frame
5358
+ cohort to the actual PNG frame set through `readFrameSet` and `boneTrackOf`,
5359
+ including unselected frames, before reading the worst pose. Identical static
5360
+ ranges on a bone deduplicate; conflicting ranges are explicitly ambiguous,
5361
+ never described as no declared range.
5362
+
5363
+ ## Per-layer depth sheets (#773)
5364
+
5365
+ `depth_filename` is a legacy source hint. Only an explicit `config.depth` block
5366
+ activates sheet binding. Without it, hints are retained internally with their
5367
+ original source directory but trigger no depth-file I/O, validation/refusal,
5368
+ fetch or output, whether files or in-memory sheets exist. Original manifests
5369
+ are not rewritten. The ordering median and art reading remain unchanged. An
5370
+ active drawn-layer sheet must match its own box, have R=G=B and alpha255 everywhere. The pinned See-through
5371
+ writer stores far as high (ComfyUI-See-through commit
5372
+ `98d754bf04f668647919ab750eccb0e0640faa81`, nodes.py261–281); its later ordering
5373
+ adjustments at875–884 do not rewrite the sheet. Accordingly `near` is explicitly
5374
+ `black`, not inferred from its median. `depth_median` still controls draw order.
5375
+ Per-layer same/opposite/indeterminate readings use a half-u8-level bound and are
5376
+ not gates or inversion certificates; constants and symmetric ramps are ambiguous.
5377
+
5378
+ The explicit `depth` block declares `zScale` for writer sheets, positive finite
5379
+ world units, never derived from eight bits. User fallback sheets are declared
5380
+ under `depth.full` or `depth.head`, either `layers` keyed by exact source layer
5381
+ name, or `atlas` (one sheet on that run's manifest canvas). Each user sheet states
5382
+ `image`, `near` (`black`/`white`) and its positive finite `zScale`. Relative paths
5383
+ resolve beside config. A packed atlas is not a run canvas; no packed-region crop
5384
+ is inferred. Writer declarations are authoritative; overlapping user declarations
5385
+ refuse, and active missing declared files never trigger fallback. An empty
5386
+ `depth: {}` activates the hints but cannot supply their missing units. Without
5387
+ the block, existing hint-only public inputs remain art-only inputs.
5388
+
5389
+ The reader/export APIs accept an explicit `DepthBinding` context
5390
+ `{config: config.depth, run: "full" | "head", base: configDirectory}`. With no
5391
+ context, raw export writes the legacy art-only manifest/files even if its input
5392
+ has already-bound in-memory sheets. `layers --export` retains that default.
5393
+ `comfy seethrough --config <file> --depth-run <full|head>` uses the early layers
5394
+ config loader before contacting the box. The config depth block and named run
5395
+ select binding; the run is never guessed from the uploaded image. Without the
5396
+ block no depth hints are checked or fetched, including fresh wrapper outputs.
5397
+ With it, declared downloads and the staging reader must pass before emission.
5398
+ Relative fallback paths use the actual config directory. `--depth-run` without
5399
+ a depth block is a usage error; a block without a named run is also refused.
5400
+
5401
+ Assembly uses the actual run registration, source-copy/composite coordinates,
5402
+ merge ownership and final crop. Scalar bilinear sampling at pixel centres is
5403
+ quantized half up at each intermediate copy/composite grid, then again at
5404
+ final part registration. PAD4 repeats the resulting u8 edge values. There is no
5405
+ universal half-level total-error bound across those operators. No art-color
5406
+ premultiplication or local min/max rescaling is applied. The rig sheet receives the drawing's PAD4 with
5407
+ explicit rectangular edge clamping. Original source hashes, rectangles, medians
5408
+ and declared units accompany the optional `parts.json` part `depth` reference.
5409
+ A synthesized merge rim or replacement under-art without scalar provenance is a
5410
+ named support refusal. It is not assigned a background or host depth.
5411
+
5412
+ `rig` writes padded sheets in `images/` and a `depth_sheets.json` companion
5413
+ (`rig-parts-depth-sheets/1`). Each companion row names the output `part`, its
5414
+ actual padded drawing `size` [width,height], and its `depth` reference. A blink
5415
+ still split has separate lower/upper scalar slices at the same row as the art;
5416
+ both keep original source provenance. Its references describe inputs for Core's
5417
+ `generator.depth = {image, near, zScale}`; sources are provenance, not Core
5418
+ fields. Existing authored weighted meshes are unchanged and do not consume the
5419
+ reference yet (#763 owns generators/turns). Measurement sheets are not packed
5420
+ art. An authored `parts.json` depth reference remains an active standalone
5421
+ `rig` declaration even without config opt-in: its file/size/color/opacity
5422
+ guards still apply. With binding inactive there is no added reference, file or
5423
+ legacy-byte change. #773 adds no image generation, geometry or motion model.
package/docs/MIGRATION.md CHANGED
@@ -301,7 +301,7 @@ either (the examples' `expected/` and every existing control are unchanged).
301
301
 
302
302
  What to do: nothing. To draft a skirt or hair that the consumer's wind and gravity move,
303
303
  add a `cloth` block and propose with `--cloth`; copy each of `bones`, `meshes`, `regions`,
304
- `limbs`, `motion`, `constraints`, `turn` and `proposed_rig_scale` the proposal wrote into the config, as before
304
+ `limbs`, `motion`, `constraints`, `turn`, `motion_seams` and `proposed_rig_scale` the proposal wrote into the config, as before
305
305
  (`constraints` is the one `--cloth` adds).
306
306
 
307
307
  ## After 1.20.0: the seam over a joint's range
@@ -443,3 +443,32 @@ bands* for the source, name, width and overlap refusals. Existing configs need
443
443
  no change. The parts reader gains `limb.hybrid` and `limb.band` records only
444
444
  for this mode, and the recorded lower range is measured once. The normal
445
445
  one-bone rule and existing range thresholds remain unchanged.
446
+
447
+ ## Explicit motion seam declarations and posed WARN evidence (issue #750)
448
+
449
+ New proposals can include optional `motion_seams: [{part, neighbour}]` alongside
450
+ the other proposed config sections. Carry it through assemble into parts.json.
451
+ Each pair names two existing output parts, once regardless of direction; the
452
+ strict readers name malformed, self, duplicate and unresolved declarations.
453
+ Check observes declared pairs in every supplied animation with the existing
454
+ non-root WARN floor; bars and exit status are unchanged. Existing body-root
455
+ FAIL semantics remain in force for independently selected root pairs.
456
+
457
+ Older authored configs acquire no declaration automatically. Keep the originals
458
+ and use exported `draftMotionSeams(config, parts)` on the frozen authored
459
+ structure and source-provenance parts, preserving every other config value in
460
+ a derived input and recording the added declaration. Check does not run that
461
+ generator; removing a declaration silences its independently declared pair.
462
+
463
+ Existing played limb WARNs as well as newly declared WARNs gain same-render
464
+ worst-frame pose evidence. Read relative angles as setup-axis deltas modulo
465
+ 360, alongside unchanged static ranges; unsupported comparisons carry an
466
+ unavailable reason. The geometry setup, actual raw frame/time and parent
467
+ matrices replace no source motion keys and introduce no angle threshold.
468
+
469
+ The WARN pose reader also requires the published stock coordinate header and
470
+ the required source bone/parent roster. It binds the complete geometry frame
471
+ cohort to the actual PNG frame set through `readFrameSet` and `boneTrackOf`,
472
+ including unselected frames, before reading the worst pose. Identical static
473
+ ranges on a bone deduplicate; conflicting ranges are explicitly ambiguous,
474
+ never described as no declared range.