rig-parts 1.64.0 → 1.65.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
@@ -523,6 +523,7 @@ usage:
523
523
  [--page-edges pot|free] [--pack-shape rect|polygon]
524
524
  [--requirements <file.json>] [--seam-pairs] [--keep-stills]
525
525
  [--set <name> | --all-sets] [--environment <schedule.json>]
526
+ [--wind-band <spec.json>]
526
527
  Build, gate, render and measure a rig through rig-c's CLI (the rigc at
527
528
  node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
528
529
  a rig spec, which rigc build compiles, not a compiled skeleton.json.
@@ -671,6 +672,14 @@ usage:
671
672
  response or no band is SKIP; no pair or animation gives no worlds.
672
673
  Nonphysics output is unchanged. In rig-play environment(null) returns
673
674
  to the default world, not still air (AUTHORING, Physics seam environments).
675
+ --wind-band <spec.json> sweeps steady wind [x, 0], gravity [0, 1] at t = 0
676
+ over [min, max] in step increments through rigc render --seam (spec
677
+ "rig-parts-wind-band/1"; every field required: spec, min, max, step, fps,
678
+ max_gap, max_ghost, pairs; refused by CHECK_WIND_BAND before anything is
679
+ written). Prints WIND_BAND: REPORTED with the steady-wind band and one
680
+ WIND_BAND: line per pair that breaks inside [min, max], and records
681
+ WIND_BAND in check.json. Steady wind only; it does not measure gusts or
682
+ transient overshoot (AUTHORING §6, Steady-wind seam band).
674
683
 
675
684
  rig-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
676
685
  Encode a frame set rigc render wrote (its --out directory, or the set
@@ -788,7 +797,7 @@ usage:
788
797
  [--pack-shape rect|polygon] [--loop] [--requirements <file.json>]
789
798
  [--idle-keys ctl|direct] [--lead-in <periods>] [--seam-pairs] [--keep-stills]
790
799
  [--alt <name>=<source>:<full>[:<head>]] [--set <name> | --all-sets]
791
- [--environment <schedule.json>]
800
+ [--environment <schedule.json>] [--wind-band <spec.json>]
792
801
  assemble, then rig, then check, in one process, each stage's own lines
793
802
  printed under [assemble], [rig] and [check]; the first stage that refuses
794
803
  stops the build with its own FAIL lines. The config must already carry
@@ -819,6 +828,9 @@ usage:
819
828
  --environment is forwarded to check (as for check); the schedule is read
820
829
  by rig-c's reader before assemble runs, so a malformed one is refused,
821
830
  CHECK_ENVIRONMENT, under [assemble] before anything is assembled.
831
+ --wind-band is forwarded to check (as for check); the spec is read before
832
+ assemble runs, so a malformed one is refused, CHECK_WIND_BAND, under
833
+ [assemble] before anything is assembled.
822
834
  --seam-pairs and --keep-stills are forwarded to check (as for check:
823
835
  the seam split by pair on a pass too, under check/seam/; the setup-pose
824
836
  still under check/setup_still/). check's --rig and --parts are refused:
@@ -1014,13 +1026,13 @@ const TAKES = {
1014
1026
  rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
1015
1027
  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'] },
1016
1028
  compare: { valued: ['--left', '--right', '--map'], switches: [] },
1017
- check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
1029
+ check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment', '--wind-band'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
1018
1030
  loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
1019
1031
  assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config'], switches: ['--propose-plan'] },
1020
1032
  'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll'], switches: ['--offload', '--lama', '--nf4'] },
1021
1033
  'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
1022
1034
  inputs: { valued: ['--source', '--config', '--out'], switches: [] },
1023
- build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in', '--alt', '--set', '--environment'], switches: ['--loop', '--seam-pairs', '--keep-stills', '--all-sets'] },
1035
+ 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'] },
1024
1036
  compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
1025
1037
  'mesh-draft': { valued: ['--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: ['--more-triangles'] },
1026
1038
  'mesh-stats': { valued: ['--config', '--parts'], switches: [] },
@@ -1271,7 +1283,7 @@ function cmdCheck(args: string[]): number {
1271
1283
  try {
1272
1284
  const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
1273
1285
  const r = checkStage(
1274
- { rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1, ...(f.has('--set') ? { set: f.get('--set') as string } : {}), allSets: allSets === 1, ...(f.has('--environment') ? { environment: f.get('--environment') as string } : {}) },
1286
+ { rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 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 } : {}) },
1275
1287
  rigcRunner(bin),
1276
1288
  bin,
1277
1289
  console.log,
@@ -1857,7 +1869,7 @@ function cmdBuild(args: string[]): number {
1857
1869
  const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-build-'));
1858
1870
  try {
1859
1871
  const r = build(
1860
- { 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'), ...(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 } : {}) },
1872
+ { 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'), ...(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 } : {}) },
1861
1873
  { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate'), stages: {
1862
1874
  assemble: (input, outputs) => runBuildProcess({ stage: 'assemble', bin, input, outputs }, join(scratch, 'workers')),
1863
1875
  rig: (input, _rigc, gateScratch) => runBuildProcess({ stage: 'rig', bin, input, scratch: gateScratch }, join(scratch, 'workers')),
package/docs/AUTHORING.md CHANGED
@@ -120,7 +120,7 @@ read by no CPU stage).
120
120
  | `assemble.cuts` | assemble; `meshes`/`regions` keys may name the new parts too, and must (each is a part like any other) | **authored**, optional (issue #170): `[{from, into, polygon, draw, overlap, under, under_fill}]`, `under` and `under_fill` optional — a plan part's layer cut by a polygon the author draws: the pixels of plan part `from` whose **centre** lies inside `polygon` (even-odd; a centre exactly on an edge is outside) leave `from` and make the new part `into`, colour and alpha, so every pixel of the layer ends in exactly one of the two. `polygon` is `[[x, y], …]`, at least three points, non-negative integer **rig** pixels — the space of `parts.json` and of the patches' boxes. The file that shows the layer in those coordinates is `assemble`'s own `rig/parts/<from>.png`, placed at its `parts.json` `x`, `y` (the PNG's pixel `(i, j)` is rig pixel `(x + i, y + j)`), or `render/recomposite_rig.png`, the whole rig 1:1; the contact sheets (`sheet`) are scaled and carry no coordinates. `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`, as for a patch (to keep the setup pose, draw the piece immediately in front of `from` — `{"before": "<the plan part after from>"}`, or `"front"` when `from` is the last plan part; `{"before": "<from>"}` draws it behind `from`, which `under` refuses as `ASSEMBLE_UNDER_DRAW` because the fill would hide it). `from` must be a plan part (`CONFIG_NAME_RESOLVES`; a patch or another cut's piece is refused; a part a limb takes may be cut where no segment of the limb holds it, issue #564, §5 *Limbs*), `into` a new name (`CONFIG_PART_UNIQUE`), the polygon three or more integer points (`CONFIG_FIELD_TYPE`). The piece's `parts.json` record carries `from`'s `<run>:<tag>` and its own counts, each split from the layer's by where the pixel lies; it is a mesh or a region under its own name, and the coverage rule holds it like a plan part. What the config cannot know the stage refuses: a polygon point past the rig (`ASSEMBLE_CUT_INSIDE`), two cuts of one part sharing a pixel (`ASSEMBLE_CUT_OVERLAP`), a cut that takes none of the layer's opaque pixels or leaves `from` none (`ASSEMBLE_CUT_PIXELS`), a cut of a part a limb takes that takes a pixel a segment of the limb holds (`ASSEMBLE_CUT_CROSSES_LIMB`). `overlap` is required, a non-negative integer of rig pixels with no default (`CONFIG_FIELD_TYPE`): the **band** — the polygon's pixels within `overlap` px of its edge by Chebyshev distance (the polygon's mask less its erosion by a `2 * overlap + 1` square) — stays in `from` as well as going to the piece, so the piece's resampled edge has art under it; the counts stay a strict partition and the band is the base's extra copy, printed on the `cut:` line. At `0` the two abut and `check`'s seam bar judges the edge between them (`CHECK_SEAM_WITHIN_BAR`, §5). A band only covers the edge at rest: a piece that swings well clear of where it was painted shows the hole it leaves, and a thin piece's band shows a ghost of the piece. `under` (issue #193), optional, is the author's image of what lies under the piece: a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE` unless a non-empty string), in rig pixels over the polygon's **bounding box** — the least and greatest polygon x and y, so the image is `max x − min x` by `max y − min y` and its pixel `(i, j)` is rig pixel `(min x + i, min y + j)`. Where the cut removed pixels from `from` — the piece's pixels (alpha above 0) less the band — `from` takes the image's pixel, colour and alpha wherever the image's alpha is at least the alpha the cut removed there (**filled**); elsewhere `from` stays empty there (**left empty**): a transparent pixel is the author's statement that nothing lies under it (the background, or a part drawn behind), and a less opaque one is not half-filled. The image is read nowhere else, so its pixels over the band, over the box outside the polygon or where the layer had no art never change `from`. The `cut:` line then ends `; under <n> px filled, <m> px left empty` (`n + m` is every removed pixel), so an unintended hole is a number in the log, not a guess; the filled pixels count in `from`'s `opaque_px` and `occluded_px` (the piece is drawn over them), never as visible or projected. The stage refuses: a file that is not there (`ASSEMBLE_UNDER_PRESENT`) or not a PNG (`ASSEMBLE_UNDER_DECODES`), an image not exactly the box's size (`ASSEMBLE_UNDER_SIZE`, both sizes given), an image that fills none of the removed pixels — all transparent there, or another file (`ASSEMBLE_UNDER_EMPTY`; to leave the whole hole, drop `under`), and a piece drawn behind `from` (`ASSEMBLE_UNDER_DRAW`: the fill would hide it; draw it `"front"` or `{"before": "<the plan part after from>"}`). Producing the image is the author's, by whatever means; absent, `from` keeps only its band. `under_fill` (issue #360), optional, is the one value `"colour"` (`CONFIG_FIELD_TYPE` on any other): the same pixels are filled and left empty, and a filled pixel takes the image's **colour** at the alpha the cut removed there instead of the image's alpha, so `from`'s alpha is the uncut layer's wherever the image fills and its silhouette does not grow — an opaque image over a soft edge leaves the edge soft. It applies to the part's hidden-art layer as well as to a file; a cut with neither is refused (`ASSEMBLE_UNDER_FILL_SOURCE`). The `cut:` line's filled count then says how many took the colour only: `; under 190 px filled (190 colour only, at the alpha the cut removed), 0 px left empty`. Absent, a filled pixel takes the image's colour and alpha, as above |
121
121
  | `assemble.alternates` | assemble, rig, check (`--set`, `--all-sets`) | **authored**, optional (issues #454, #465, #472, #473, #476): `[{name, source?, full?, head?, head_box?, parts?, hides?, cuts?, skeleton?, anchor?, drawn?, tips?, front?}]` — alternate painting sets assembled into `<out>/rig/parts/<set>_<part>.png` (`parts.json` `alternates`), attached on each covered slot in `rig.json` as `<set>_<part>` beside the base attachment, and indexed in `<out>/rig/sets.json` (`rig-parts-sets/1`: `sets.<set>.slots`, `.hides`, `.drawn` when present, and `.threshold: {yaw: 15, pitch: 12}` when the set replaces `face` on a rig with `turn` or `turn`/`tilt` slider animations). `source`, `full` and `head` may instead be given on the CLI with `--alt <name>=<source>:<full>[:<head>]`. Windows absolute drive paths such as `C:\paint\painting.png` are accepted as written; each segment's leading drive colon belongs to that path, while other colons still separate `source`, `full` and `head`. `parts` lists the subset of parts the set replaces (default: every part not in `hides`); a **subset set** (issue #473, e.g. mouth visemes `a`, `i`, `u` with `"parts": ["mouth"]`) cuts only its covered parts from the alternate runs (refusing with `ASSEMBLE_ALT_INPUT_PRESENT` if a covered part's layer is missing or empty), writes only `{slot: "<set>_<slot>"}` in `sets.json` so `@rig-c/play`'s `setSet(name)` switches those slots while leaving every other slot on its base attachment (and `setSet(null)` restores the base pose), may be keyed on a slot attachment track in `motion.animations_from` as `{"t": …, "set": "<name>" \| null}` (`CONFIG_SET_COVERS_SLOT` if `"set"` is undeclared or does not cover the track's slot), and under `check --set <name>` counts `ANIMATIONS_DO_NOT_FOLD` only on its own covered slots; `hides` lists slots the set hides (`null` in `sets.json`). A **gesture subset set** (issue #476) covers a limb chain declared in `config.limbs.<limb>` (all of its segment `part`s; with `drawn` alone, cut at the base's joints posed to the set's own `drawn` angles by `poseBoneEntriesForDrawn`, each segment keeping the base's length; with `skeleton` (issue #539), cut at the skeleton's own joint positions by `placeBoneEntriesForSkeleton` — each covered bone the body-18 table names (`sleeve_*`, `arm_*0`/`arm_*1`, `leg_*0`/`leg_*1`) at its skeleton origin with the skeleton segment's own length and direction (`drawn` overriding the direction of a bone it names), and a covered bone the table does not name (a hand link `arm_r2`, a single `foot_r`) placed from its parent's new place — its base offset from the parent, stretched along the parent by the parent's length change and turned by its turn, so a link or a foot at the parent's end lands on the skeleton's end — with its base length and the drawn or base direction, so a foreshortened shin or forearm is cut where it is painted; naming the unsplit `from` layer, covering only some segments of the limb, or declaring polygon `cuts` on a limb set is refused with `CONFIG_LIMB_SET_JOINTS`) and carries `drawn` (or `skeleton`) for the covered bones only (giving `drawn` for an uncovered bone is refused with `CONFIG_SET_COVERS_BONE` / `RIG_SET_COVERS_BONE`; uncovered bones keep the base's `drawn`). A subset set's limb part takes no pixel that a base part drawn after it, which the set neither covers nor hides, covers at setup (issue #587): the set's attachment is drawn in the base slot's place, so that base part is drawn over it and what the set's painting shows there is that base part (shorts over a leg set's thigh top, which See-through synthesised in the set's leg layer); `assemble` clears those pixels — the base part's PNG at alpha above 8, at its `parts.json` x, y, on the shared canvas — from the set's part once its limbs are cut, before any count — its `merged_px` and its cap's record too (issue #603): a cap pixel it clears leaves the count it was in (`layer_px`, `image_px`, `upper_px`, `lower_px`, `lower_kept_px`) for the cap's `excluded_px`, written only when above 0, so every count is the written PNG's — and prints `LINT set "<set>" <part>: <n> px under <base part> excluded` per part and base part that took a pixel (n counts alpha above 8); a set that carries that base part itself (`"parts": ["thigh_l", "shin_l", "bottomwear"]`) excludes nothing and writes what it wrote before. A subset set that names one limb side of a layer more limbs take (issue #588: `"parts": ["thigh_r", "shin_r"]` where `leg_r` and `leg_l` both take the one `legs` layer) would put every pixel of that layer on its one limb, so each of its parts would hold a piece of both legs; when the set's layer holds more than one island (8-connected pixels with alpha above 8) of at least 1 % of its largest (issue #608: the ghost clean-up's own share; a smaller island is a speck, which goes with the island nearest it — the one holding the pixel centre nearest one of the speck's, none when two are equally near — and is never an island of its own), each segment part keeps the island its joints fall on — its bone's origin and the next segment's bone's origin, at the set's own bones, a joint on the island of a pixel whose square holds the point, and a joint on no island on the one island whose nearest pixel centre lies within the radius of the cap cut at that joint (`cap.radius`: an ankle just past the legwear's edge, on the shoe) — and drops the rest, once its limbs are cut and before the exclusion above, and `assemble` prints `LINT set "<set>" <part>: <layer part> holds <n> islands (8-connected, alpha above 8)[, and <s> speck(s) under 1 % of the largest (<f> px), each with the island nearest it], kept the one under <bone> [x, y] and <bone> [x, y][ (<e> px off it, within its cap's radius <r>)] (<k> px), dropped <m> (<d> px)` per part (k and d count the part's pixels above alpha 8, m the islands it lost one from, and `dropped none` when it lost none); a joint on no island with none or two within that radius, or with no cap cut at it (a limb's first joint), and a segment whose joints fall on two, are refused with `ASSEMBLE_ALT_LIMB_ISLAND` rather than guessed. A layer of one island — specks aside — may hold both legs over each other (a folded shin across the standing leg; issue #609): each of the set's segment parts then keeps the pixels nearer its own limb's segment line — the polyline of only its segments taking that source, their bone origins and the last segment's supplied tip or following segment's joint (issue #638) — than the other limb's, both in the set's pose, and gives the rest away, once its limbs are cut and before the exclusion above; a pixel whose two distances differ by 0.5 px or less (the overlap band: half a pixel, the reach by which a pixel centre is read as on a line) goes to the limb whose part there is drawn later in the base's draw order, and the `LINT set` line above counts what each part kept, gave and held in the band. The other limb stands where the set's `skeleton` places it — its keypoints place every limb bone of the body-18 table, the other leg's as well as the set's own (its `drawn` and `drawnLength` stay the covered bones') — and never at the base's place: a set that does not place it (a `drawn` set, which may not name a bone it does not cover, or a set with neither) is refused with `ASSEMBLE_ALT_LIMB_POSE` when its layer has art where the base draws that limb, and when it has none nothing says the layer holds it and the layer is cut as before; two limbs whose segment lines run closer than the limb's half-width over most of where their art can meet are refused with `ASSEMBLE_ALT_LIMB_OVERLAP`. A set that carries every limb of the layer is cut as before. For a subset set, where a limb's own source layer holds art (`alpha > 8`) under another layer of that run drawn in front (`alpha > 8`, by the projection's plan/depth order), the limb keeps its own layer pixels through projection, limb cutting, island/nearest-line assignment, base-footprint exclusion, fringe clearing and seam correction (issue #637). Those pixels are occluded, never counted as painting projection; no base footprint or painting colour replaces them. After source-art island/nearest-line assignment, a source-transparent cap pixel is assigned before protection, in both doors: its centre is compared against only the segments taking the recipient's source, at the set's supplied bones (ankle-to-toe for a separate foot source), with the existing 0.5 px overlap band and base draw order. A missing last endpoint is refused with `ASSEMBLE_ALT_LIMB_POSE`; an actual following joint may supply that endpoint, never a guessed point. An unposed other limb with base art beneath these cap pixels is likewise refused; without such overlap the existing compatible path retains the cap without inventing the other limb's pose. A pixel cleared by this assignment is never restored. `CAP_FROM_UPPER` records a copy made before assignment and alone proves no ownership: only a retained copy is protected beneath the upper segment, which is drawn over it. The source-art island selection itself is unchanged. Other retained cap pixels (from the layer or a supplied cap image) are protected under an actual front layer even where the lower source is transparent. Protection follows each assigned piece through exclusion, fringe clearing and seam correction; it never restores a pixel ownership removed. Cap pixels removed during ownership leave their original counts for `excluded_px`, just as excluded pixels do, so the cap ledger describes the written PNG. Target assignment reconciles this ledger even when there are no source-transparent cap candidates and when a direct caller does not request cover protection. Unprotected source pixels still obey the base-footprint exclusion. Missing limb/cap art is never extended or guessed. Base builds and uncovered, unexcluded sets keep their existing bytes. `skeleton` (a pose file path relative to the config's directory, `{"width": W, "height": H, "points": {…}}` with all 18 body-18 keypoints on the canvas, validated by the same rules as `generation.control.skeleton`: `CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON`, `CONFIG_FIELD_TYPE`, `CONFIG_KEY_KNOWN`, `CONFIG_KEY_UNIQUE`, `CONFIG_POSE_CANVAS`, `CONFIG_POSE_KEYPOINTS`, `CONFIG_POSE_IN_CANVAS`) or `drawn` (`{<bone>: <worldDegrees>}`, each bone resolved against `bones`, `CONFIG_NAME_RESOLVES`) declares the posed angles the alternate was painted at (issue #465); when `skeleton` is given, `assemble` derives `drawn` from its 18 keypoints (filtered to covered bones on a subset set, with `drawn` overriding any bone named in both), records both `skeleton` (scaled by `assemble.rig_scale` to rig px) and `drawn` in `parts.json` `alternates` — and, when the skeleton placed the set's limb bones (issue #539), `drawnLength` (`{<bone>: <ratio>}`, each covered table bone's skeleton segment length over its base length, 6 places) and `registration` (`{offset: [dx, dy], scale}`: the similarity without rotation, about the canvas origin, that maps the skeleton's torso — the mean of its shoulders `r_shoulder`/`l_shoulder` to the mean of its hips `r_hip`/`l_hip`, read where the base declares the bone rooted there, `sleeve_*0`/`arm_*0` and `leg_*0` — onto those base bones' origins, 6 places), both also written to `sets.json`; the set's painting must be registered, its torso within 1 rig px and 1 % in scale of the base's, or the set is refused by name before any joint is cut (`ASSEMBLE_ALT_SKELETON_REGISTERED`, with the offset and scale the painting would need) — and the `rig` stage writes `<set>_*` region and mesh attachments in the bone-local frame of `poseRigBonesForDrawn(bones, drawn)` (weighting limb segments directly from `skeleton`'s joint endpoints when `skeleton` is present) and emits `drawn` in `sets.json` so `rig-pose` and `check --set <name>` / `--all-sets` pose the bones at `drawn` without rotating the already-posed art a second time. `front` (issue #664) lists covered limb segment parts the set draws on their **front slot** instead of the base slot: each must be in the set's `parts` and not its `hides`, and listed in some `config.limbs.<limb>.front.parts` (`CONFIG_SET_FRONT`); the set then maps `<part>_front` (never `<part>`) to `<set>_<part>` in `parts.json` and `sets.json` and hides `<part>` (and `<part>_still`), its #587 exclusion counts only base parts drawn after the front slot's target, and keying that part's set on a slot track is refused (`CONFIG_SET_FRONT_KEYED`) — *Front slots (issue #664)* below `layers` optionally maps covered, nonhidden output part names to layer names in their existing source run (issue #666); for example `"layers": {"upperarm_r": "author:raised", "forearm_r": "author:raised"}`. Limb cuts, caps, islands and base exclusions read the selected layer; the base plan stays unchanged. |
122
122
  | `assemble.head_face_underfill` | assemble, `assemble --propose-plan` | **authored**, optional (issue #492): `boolean` (`CONFIG_FIELD_TYPE` on any other). When both runs exist and `full:face` has at least `200` opaque run px (`PROPOSE_MIN_PX`), the stage maps `full:face` into the head canvas via `fullLayerToHead` and measures how much of its hole-filled silhouette (`alpha > 8`) is covered by `head:face` (`alpha > 8`). A head-run `face` covering less than `75 %` (`HEAD_FACE_COVERAGE_FLOOR`, measured `0.0 %..48.6 %` on hollow/omitted head runs vs `84.8 %..99.6 %` on clean head runs across the 4 public examples and 9-standee corpus) is refused with `SEETHROUGH_HEAD_RUN_HOLLOW` unless `head_face_underfill: true` is declared. When `head_face_underfill: true` is declared and `head:face` exists (`opaqueRunPx >= 200`), `fullLayerToHead(full:face)` is composited under `head:face` on the head canvas (`alpha = max(headAlpha, fullAlpha)`, RGB blended by `headAlpha / 255`) before `head:face` is placed in rig space (`underfill=<N>px(full:face)` on the `face` part log line and a `face: head_face_underfill composited …` note in `assemble --propose-plan`). The fill restores transparent pixels only and leaves the cut's own edge where it is — a hollow whose edge is not under hair stays visible, and that is the author's to fix at the cut. When `head:face` has fewer than `200` opaque pixels, `SEETHROUGH_HEAD_RUN_HOLLOW` still fires even under `head_face_underfill: true` because the head run omitted the face layer and cannot supply a jawline |
123
- | `limbs` | assemble (the cut and the caps), rig (through `regions`), check (`LIMB_ONE_BONE`) | **proposed** by `propose --keypoints` (issue #349), then checked: `{<limb>: {segments: [{part, from, bone}, {part, from, bone, joint, range?, cap?, edge?, cap_shape?, cut_band?}, …], split?, layout?, front?}}`, the upper segment first. The rule is *Limbs: cut at their joints* below the table. `part` is a new part (`CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` against the plan, patches, cuts and every other limb part); `from` a plan part or a cut's piece (`CONFIG_NAME_RESOLVES`) — the layer the segment's pixels come from, which is cut into the limb parts and is not itself made: a `meshes` or `regions` entry for it is `CONFIG_NAME_RESOLVES`, and the segments of one limb that take one part are consecutive (`CONFIG_LIMB_FROM_RUN`); `bone` a declared bone, each segment's the child of the one before (`CONFIG_LIMB_BONES_CHAIN`); the upper segment names no `joint`, `range`, `cap`, `edge`, `cap_shape` or `cut_band` (`CONFIG_KEY_KNOWN`), every other names its `joint` (`CONFIG_FIELD_PRESENT`) — the joint is its bone's origin; `range` is `[min, max]` degrees from rest with `min <= 0 <= max` (`CONFIG_LIMB_RANGE`), which `assemble` writes into the lower part's `parts.json` record and `check` poses the joint at — its two ends and its middle — reading the seam there (`SEAM_OVER_RANGE`, issue #360; rig-c needs no rotation limit); `cap` a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE`); `edge` `"feather"` (issue #360), the one value it takes, and `cap_shape` `"disc"` (issue #360) or `"ball"` (issue #579) (`CONFIG_FIELD_TYPE` on any other) — the cut's geometry at that joint, *Limbs: cut at their joints* below; absent, the cut is hard and the cap the layer's half-disc of half the limb's width; `cut_band` a whole number of px, 1 or more (`CONFIG_FIELD_TYPE`), on a segment that takes the segment above's part — between two layers nothing is cut, so a band there is `CONFIG_KEY_KNOWN` — places the cut within that many px of the joint line (issue #360, *Limbs: cut at their joints*); absent, the cut is the joint line. Each limb part is attached like any part — `regions.<part>`, which must be the segment's `bone` (`CONFIG_LIMB_BONE`), or a mesh, which `check` holds to that bone alone (`LIMB_ONE_BONE`). With `limbs`, `assemble` reads `bones` too (`CONFIG_FIELD_PRESENT` without it). Absent, nothing is cut at a joint and no byte of any output moves. Optional `front` (issue #664), `{parts: [<own segment part>, …], after: "<produced part>"}`, gives each named segment a second slot `<part>_front`, empty at setup, placed after `after`'s slot family, for a gesture set to draw on (`CONFIG_FRONT_SCOPE`, `CONFIG_FRONT_NAME`, `CONFIG_FRONT_LAYOUT`; *Front slots (issue #664)* below) |
123
+ | `limbs` | assemble (the cut and the caps), rig (through `regions`), check (`LIMB_ONE_BONE`) | **proposed** by `propose --keypoints` (issue #349), then checked: `{<limb>: {segments: [{part, from, bone}, {part, from, bone, joint, range?, cap?, edge?, cap_shape?, cut_band?, hybrid?}, …], split?, layout?, front?}}`, the upper segment first. The rule is *Limbs: cut at their joints* below the table. `part` is a new part (`CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` against the plan, patches, cuts and every other limb part); `from` a plan part or a cut's piece (`CONFIG_NAME_RESOLVES`) — the layer the segment's pixels come from, which is cut into the limb parts and is not itself made: a `meshes` or `regions` entry for it is `CONFIG_NAME_RESOLVES`, and the segments of one limb that take one part are consecutive (`CONFIG_LIMB_FROM_RUN`); `bone` a declared bone, each segment's the child of the one before (`CONFIG_LIMB_BONES_CHAIN`); the upper segment names no `joint`, `range`, `cap`, `edge`, `cap_shape` or `cut_band` (`CONFIG_KEY_KNOWN`), every other names its `joint` (`CONFIG_FIELD_PRESENT`) — the joint is its bone's origin; `range` is `[min, max]` degrees from rest with `min <= 0 <= max` (`CONFIG_LIMB_RANGE`), which `assemble` writes into the lower part's `parts.json` record and `check` poses the joint at — its two ends and its middle — reading the seam there (`SEAM_OVER_RANGE`, issue #360; rig-c needs no rotation limit); `cap` a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE`); `edge` `"feather"` (issue #360), the one value it takes, and `cap_shape` `"disc"` (issue #360) or `"ball"` (issue #579) (`CONFIG_FIELD_TYPE` on any other) — the cut's geometry at that joint, *Limbs: cut at their joints* below; absent, the cut is hard and the cap the layer's half-disc of half the limb's width; `cut_band` a whole number of px, 1 or more (`CONFIG_FIELD_TYPE`), on a segment that takes the segment above's part — between two layers nothing is cut, so a band there is `CONFIG_KEY_KNOWN` — places the cut within that many px of the joint line (issue #360, *Limbs: cut at their joints*); absent, the cut is the joint line. Each limb part is attached like any part — `regions.<part>`, which must be the segment's `bone` (`CONFIG_LIMB_BONE`), or a mesh, which `check` holds to that bone alone (`LIMB_ONE_BONE`). With `limbs`, `assemble` reads `bones` too (`CONFIG_FIELD_PRESENT` without it). Absent, nothing is cut at a joint and no byte of any output moves. Optional `front` (issue #664), `{parts: [<own segment part>, …], after: "<produced part>"}`, gives each named segment a second slot `<part>_front`, empty at setup, placed after `after`'s slot family, for a gesture set to draw on (`CONFIG_FRONT_SCOPE`, `CONFIG_FRONT_NAME`, `CONFIG_FRONT_LAYOUT`; *Front slots (issue #664)* below) |
124
124
  | `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip, link_inherit?}` whose links are named `<chain>0 … <chain>n`. Parents come before children. In `rig.json` each link is **turned along its chain** — `rotation` is the direction from its origin to the next link's (the last link's to `tip`) and `length` that distance (issue #73), so a physics constraint added downstream finds a lever; under `--idle-keys ctl` the link's `<link>_ctl` carries the turn and the length and the link sits at local rotation 0 beneath it. A single bone with a `tip` is turned the same way, towards its tip, with that distance as its length (issue #239); a tip on the bone's own origin is refused (`RIG_TIP_DISTINCT`). The five face bones `eye_r`, `eye_l`, `brow_r`, `brow_l` and `mouth` (by name, when one has no tip of its own) are turned to the eye line — the line through the origins of `eye_r` and `eye_l` as `bones` writes them, read image-left to image-right, drawn when parts.json holds both an `eyewhite-r` and an `eyewhite-l` part — so the blink's `scaley` squashes across the eye line on a tilted head; move an eye bone and the line moves with it. With one eyewhite or none, or an eye bone missing, the rig stage's `face axis:` line says so and they keep the page's axes. A bone under a face bone keeps the face's axis. With the line, `rig.json` also declares `invariants.axes` — one entry, `"face"`, through `eye_r` and `eye_l`, holding the turned face bones (and their controls) — so rig-c's gate holds the build to the line; the line is the one rigc draws through the same two bones, so the tolerance is only the float floor — the largest gap the emitted (float32) bones have to it, rounded up to 6 places plus one 6-place unit — and the `face axis:` line prints it. With the line the rig stage also adds `head_up` (issue #250): a slotless bone under `head`, at head's origin, turned to the eye line + 90 degrees and as long as the eye midpoint lies from head's origin along that axis — the head's frame, which a rule that needs it reads; `head` keeps its own frame, so no key on it or under it changes meaning, and a config bone of that name is refused (`RIG_HEAD_UP_NAME_FREE`). A face bone with its own `tip` is held too (issue #258), in a second entry, `"face_tips"`, when its tip lies along the line within whole-pixel rounding — a tip written in whole pixels sits at most sqrt(2)/2 px off the point of the line it stands for, so its direction may sit up to asin((sqrt(2)/2) / length) degrees off — with its tolerance measured the same way; a tip further off is the bone's own axis, and the `face axis:` line names it with its gap and that bound. A failure is rig-c's `A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` line, which names the bone and the fix (rig-c's `docs/AUTHORING.md` §3.7 and §5.2); with no line no entry is written and A51 reports SKIP. Every region on a turned bone is turned back, so the setup pose does not move. Any other single bone keeps world rotation 0; one whose parent is a link is turned back upright. A single bone may declare `inherit` (issue #348): what it takes from its parent's transform, one of rig-c's own mode names exactly as rig-c's table spells them — `normal`, `onlyTranslation`, `noRotationOrReflection`, `noScale`, `noScaleOrReflection` (rig-c's `RigBone.inherit`) — written to that bone in `rig.json` as declared; any other spelling, `"NoScale"` included (the runtime would fold its first letter; nothing here folds or maps), is refused (`CONFIG_BONE_INHERIT`), and a chain takes no `inherit` (`CONFIG_KEY_KNOWN`). A chain may instead declare `link_inherit` (issue #369), an object mapping its own expanded link names to those same modes, for example `"link_inherit": {"leg_r1": "noScale"}`. Only the named link changes; omitted links inherit normally. A key outside this chain is `CONFIG_NAME_RESOLVES`, with this chain's valid link names in the detail; a non-object or non-string mode is `CONFIG_FIELD_TYPE`, and another mode spelling is `CONFIG_BONE_INHERIT`. The mode is forwarded to the expanded link, or to its control under `--idle-keys ctl`, by the same rule as a single bone. This is an authored opt-in: `propose` does not add it. Its setup rotation is the turn from where rig-c's own poser puts the bone with no rotation of its own under that mode, so it still stands upright (or along its tip) at the setup pose. Where the idle gives the bone a `<bone>_ctl`, the mode goes on the control: the control is the bone the parent's rotation reaches, and the bone under it inherits normally, so the idle's keys moved onto the control still turn it. A foot that does not take its shin's rotation (`noRotationOrReflection` or `onlyTranslation`) keeps its shoe level while the leg is posed (`FEET_STAY_LEVEL`, §7); which mode is the author's. `noScale` keeps a shin or shoe's shape under a scaled parent but still takes the parent's rotation: a shoe that must also stay level needs a separate rotation constraint, for example a transform constraint from an unrotated bone. This does not change the proposed feet's existing inheritance or `FEET_STAY_LEVEL` default. An automatic mesh weighted through a bone with a mode other than `normal` gets no derived amplitude (its row names the bone) and no residual envelope (rig-c's `SKINNING_RANGE_UNSUPPORTED`). Absent, `rig.json` carries no `inherit` and no byte moves |
125
125
  | `meshes.<part>` | rig | **proposed**, then corrected. Generated modes below; `explicit` transfers an accepted mesh (see “An accepted mesh”). Exactly one mode is allowed. `grid` (the lattice: cell size, px — what `propose` writes: by default one spacing for the figure, round(H / 37) rig px with H the height of every part's box together, a part whose tenth of its short side is already coarser keeping that, and under `propose --grid-rule part` the reference's tenth of the part's short side clamped to 5..36, issue #265), `contour` (the outline mode, authored; the next row and "A contour mesh" below) or `auto` (the automatic mode, authored; "An automatic mesh" below) — two, or none, is `CONFIG_MESH_MODE`. In distance-generated modes: `r` (added to every distance before weighting: `w = 1/(d + r)²`), `segments` (a chain name, a bone name with a `tip`, or `[bone, [x0,y0], [x1,y1]]`), and, **authored**, optional, `exponent` (issue #161): a number above 0 that replaces the 2 in that rule, `w = 1/(d + r)^exponent` — a larger one hands each vertex more fully to its nearest bone; absent is 2 and changes no byte, a value that is not a number above 0 is `CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`, and a declared one is echoed as `exponent` in the mesh's `mesh_report.json` row; the exponent is the distance rule's, so declared beside `rule: "heat"` it is refused (`CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`). The segment list is the one authored decision about a layer: which bones may pull it. The slot's bone is the first segment's. Optional `rule` (issue #161): `"distance"` — the rule above, and what an absent `rule` means (every byte is the same) — or `"heat"`, bone heat over the part's own silhouette, in every mode: the lattice's art (alpha above 8, less any island a declared `stray` left out) with holes filled, or for `contour` and `auto` the silhouette their outline is traced from (that mode's art — alpha above 8 for `contour`, alpha 1 and above for `auto` — less any island a declared `stray` left out, holes filled, grown by its `margin`); each bound bone's sources are the silhouette pixels whose centre lies within 0.75 px of its segment (a pixel two bones reach goes to the nearer, a tie to the first listed), held at 1 for that bone and 0 for the others; the field is solved by row-major Gauss–Seidel over-relaxation to a sweep change under 1e-7 within 20000 sweeps; a vertex reads the nearest silhouette pixel and takes the same cap and floor (and `regions`) as the distance rule; `r` is not read. The mesh's `mesh_report.json` row gains `heat` (the constants, each bone's sources and sweeps, the residual). Any other value is `CONFIG_FIELD_TYPE`; a bound bone with no source pixel is `RIG_HEAT_BONE_SOURCE`, an island no bone has a source in `RIG_HEAT_ISLAND_SOURCE`, a solve past its sweep bound `RIG_HEAT_CONVERGED` — none is weighted 0. The rule is chosen per mesh, so `RIG_HEAT_BONE_SOURCE` names three remedies: move the segment onto the art, take the bone out of `segments`, or keep this part on the distance rule — the one to take when the bone anchors the part from outside its art (a torso bone under a neck), since the first moves a bone another part placed and the second drops the anchor (issue #303). On a lattice, `RIG_HEAT_ISLAND_SOURCE` on an island that is dust — at most 4 px and at most 0.1 % of the part's art px, `mesh-draft`'s line — names `meshes.<part>.stray` and the figure that leaves it out, with the value declared; an island above that line keeps the segment remedy, a real piece being a real authoring question. In every mode the mesh's `mesh_report.json` row's `bones` names the bones the emitted model binds for the attachment — the sorted set its weights in the written `rig.json` name, so a `turn` twin (`turn_<part>_<bone>`) or a stopped link's `<link>_d` where those rebind it, never the segment bone it stands for — the same list in every mode, `remesh --delta`'s included (issue #536) Optional `gather: false` keeps an exclusively bound non-segment limb rider at its authored draw position (issue #680); its binding and mesh geometry are unchanged. Remove the key for normal gathering; a non-rider is refused as `RIG_GATHER_RIDER`. |
126
126
  | `meshes.<part>.influence` | rig | **authored**, optional (issue #378), a mode beside `grid`, `contour` and `auto`: `{cell, radius, across, max, shape, hold_tip?}`, every field but `hold_tip` required, none defaulted. rig-c's `segments` lattice over the part at `cell` px, each vertex pulled by every link of the mesh's `segments` within `radius` px of it — a pull that falls to exactly 0 at `radius` (`shape` `"linear"` or `"smooth"`), across the `segments` entries or only within the nearest one (`across`), the strongest `max` kept. The lattice and the shares are computed by rig-c's own promised functions (`buildSegmentsLattice`, `segmentDistance`, `influenceShares`), so the rule is rig-c's (its AUTHORING §3.4.7), and the mesh is written with explicit weights like every other mode. `radius` is a length on this part: the rig stage measures the farthest lattice vertex's distance to its nearest link and refuses a radius at or under it (`RIG_INFLUENCE_REACH`), naming the distance. `propose --cloth` drafts it when a recipe declares `influence` and no `radius` (issue #518): the farthest vertex's distance plus 3 of the part's lattice cells, printed on a `LINT` line with both figures. Measured on the cloth fixture and the public `wind` example (#513: 8 covered parts, `render --folds` with the physics on under wind and a hip sway of 8° and 30°): a radius just above the floor folds **more** than the lattice (one skirt went from 0 to 55 folded frames), and a larger one is **not** safer (one fringe folded again at about 3–4× its farthest distance); +2 to +6 cells read 0 on every part, +2 at the edge. That reading counted folds only; read against `check` as well (the rig as built, under the wind schedule +1 → −1 → calm, `build --environment`, the three covered `wind` parts at the same number of cells), the window that holds is **+2 to +3 cells**, and +4 is the ceiling: `wind` `bottomwear`'s `CHECK_TIP_OVER_ROOT` ratio is 1.898 on the lattice, 1.801 at +2 cells, 1.590 at +3 and **1.471 at +4**, under the floor 1.4725, so the build fails there, and 1.351 at +6 (#513, rig-parts 1.52.0, rigc 2.60.0; the lattice, +3 and +4 read again on rig-parts 1.54.0, rigc 2.60.0, the same figures, #599). The drafted +3 sits in that window, one cell under the ceiling (no radius between +3 and +4 was read), and the window is a hypothesis until a third subject — each edge rests on one part — so state `radius` in the recipe to keep your own. `r`, `exponent`, `rule`, `influences`, `stray` and `ribs` belong to the distance rule or another mode and are refused beside it (`CONFIG_INFLUENCE_MODE`). `hold_tip` (issue #615) — `true` only, or absent: the pulled shares are smoothed by the lattice's held-fork passes (the cloth recipe's `split.hold_tip` sentence below: four passes of 1-ring mesh-edge averaging, self weight 0.5, floor 0.005) capped at the declared `max`, so no vertex binds more than `max` bones; absent, the shares are rig-c's as pulled and every byte is the one written before the field existed (`false`, a second spelling of absent, is `CONFIG_FIELD_TYPE`). The mesh's `mesh_report.json` row echoes it in `influence`. `propose --cloth` writes it, with `max` raised to at least 8 (the lattice's smoothing cap), on the parts a recipe with `influence` and `split.hold_tip: true` meshes; the rig stage reads it off the mesh and never the recipe. Measured on `wind` `bottomwear` and the cloth fixture's skirt under a 60° hip sway (#615, `render --folds`, physics on): `wind` `bottomwear` folds 41 frames as drafted before the field (max 6) and **0** with `hold_tip` and max 8, against its lattice's 5; with `hold_tip` at max 6 it reads 23, and at max 8 without `hold_tip` 3; the fixture skirt (recipe given `split.hold_tip`) reads 20 before and 0 with it, against its lattice's 0, and 0 at max 8 without it. So the raised `max` carries most of the gap and the smoothing the rest on `wind` (rig-parts 1.54.0, rigc 2.60.0; a split skirt without `hold_tip` is not smoothed and reads its 44 as before). For cloth and hair that should bend as one surface across its chains |
@@ -652,7 +652,9 @@ the forearm with the hand, the thigh, the shin and the foot are each a part on o
652
652
  weight 1, and a bend is a rotation at the joint, never a deformed mesh. A limb mesh blended
653
653
  along a chain and bent hard is what every "grotesque" verdict on a posed rig came from (the
654
654
  exam's walk, rig-c/pose's demo); a part on one bone cannot do that, and `check`'s
655
- `LIMB_ONE_BONE` refuses a limb part bound to a second bone by name. Everything below this
655
+ `LIMB_ONE_BONE` refuses a limb part bound to a second bone by name. The explicit
656
+ hybrid band below is the bounded exception: only the generated joint band blends
657
+ two adjacent carriers; its outer pieces stay rigid regions. Everything below this
656
658
  section about fitting a mesh to motion — the weight cut, the contour and automatic modes, ribs,
657
659
  the physics recipe — is for the parts that have no joints: the skirt, the clothing, the hair,
658
660
  the torso.
@@ -946,6 +948,56 @@ The first character built this way (the exam-01 test rig, issue #349) read, per
946
948
  missing cap pixels at the elbows and knees and 468 and 562 at the ankles, where the shoe starts
947
949
  below the ankle line.
948
950
 
951
+ ### Hybrid joint bands (`limbs.*.segments[i].hybrid`, issue #399)
952
+
953
+ On the segment below a joint, `"hybrid": {"part": "joint_band"}` replaces
954
+ that joint's cut cap with a mesh across the two adjacent bones. Both segments
955
+ must take the same `from` part (`CONFIG_HYBRID_SOURCE`). Keep their existing
956
+ `regions` entries, on their own bones; do not add a `meshes` or `regions` entry
957
+ for the generated band. Its `part` must be unique among all produced names.
958
+ `cap`, `cap_shape`, `edge` and `cut_band` are incompatible at that joint
959
+ (`CONFIG_HYBRID_CUT`). Other joints retain their own declared mode.
960
+
961
+ The full axial width is twice `halfWidthAbout(limbRun(...))`: the source's
962
+ alpha-above-8 run across the joint line, measured from the joint to the nearer
963
+ run edge on each side. This chooses a scale from the pixels, not a material
964
+ property. `"hybrid": {"part": "joint_band", "width": 12}` explicitly overrides
965
+ it with a finite positive full width in rig pixels. No art crossing, a
966
+ nonpositive derived width, a band reaching either bone span's end, or bands
967
+ overlapping on source art are named refusals (`ASSEMBLE_LIMB_JOINT_ON_ART`,
968
+ `ASSEMBLE_HYBRID_WIDTH`, `ASSEMBLE_HYBRID_OVERLAP`). Move the joint onto the
969
+ art or supply a width that fits; the tool does not clamp it.
970
+
971
+ The band copies the source pixels within half that width on either side of
972
+ the joint line. A one-pixel sampling overlap at each boundary remains bound
973
+ entirely to the matching rigid carrier. Its one-pixel lattice blends only the
974
+ upper and lower bones, linearly along the axis, clamped at the band boundaries.
975
+ No hidden-art cap or generated colour fills the band. `parts.json` records
976
+ `limb.hybrid` on the lower rigid piece, including the generated part name, and
977
+ `limb.band` on the mesh. Both carry the source-measured joint centre, unit axis,
978
+ full width, existing range-disc radius and upper carrier. The band repeats the
979
+ lower carrier through `limb.bone`; only the rigid lower piece carries `range`.
980
+ The strict reader requires the pair to agree. Re-run assemble after changing
981
+ the declaration (`RIG_HYBRID_SOURCE`); non-region neighbours are
982
+ `RIG_HYBRID_RIGID`.
983
+
984
+ A subset alternate covering the limb automatically covers its generated bands
985
+ as well. A band may also be listed explicitly in `parts`; it cannot be hidden
986
+ independently or given a separate `layers` override. The band image, its paired
987
+ records and the alternate's source axis switch with the rigid pieces. The
988
+ reader applies the same band-field and pair checks to alternate records.
989
+ Declaring hybrid on an old cut manifest is refused until assemble produces the
990
+ matching band and lower joint record.
991
+
992
+ `LIMB_ONE_BONE` keeps its historical rule for ordinary segments. A recorded
993
+ band instead requires both adjacent carriers and no third carrier. The range
994
+ check still compares against the same rest-art disc and uses the same
995
+ thresholds. That reading includes a moving silhouette uncovering rest art;
996
+ it does not distinguish that from a disconnected interface. Inspect the
997
+ endpoint pictures and `rigc render --seam` at both band interfaces as well as
998
+ `--folds`. A green build alone proves neither the shape nor a zero-pixel seam.
999
+ With `hybrid` absent, the existing cut path and output fields are unchanged.
1000
+
949
1001
  ### Front slots (issue #664)
950
1002
 
951
1003
  A gesture set (`assemble.alternates`, issue #476) replaces a limb's attachments
@@ -3563,6 +3615,14 @@ row in this page's `rule` and `line` tables; the selftest holds that (`TY16`).
3563
3615
 
3564
3616
  | rule | means | change |
3565
3617
  | --- | --- | --- |
3618
+ | <a id="CONFIG_HYBRID_SOURCE"></a>`CONFIG_HYBRID_SOURCE` | the two hybrid neighbours name different source parts | use a single continuous source layer for the joint band |
3619
+ | <a id="CONFIG_HYBRID_CUT"></a>`CONFIG_HYBRID_CUT` | a hybrid joint also declares a cut-cap option | remove cap, cap_shape, edge and cut_band at that joint |
3620
+ | <a id="ASSEMBLE_HYBRID_SOURCE"></a>`ASSEMBLE_HYBRID_SOURCE` | effective alternate layer selection gives a band different upper and lower source layers | select the same continuous layer for both rigid neighbours |
3621
+ | <a id="ASSEMBLE_HYBRID_WIDTH"></a>`ASSEMBLE_HYBRID_WIDTH` | the source-derived or explicit band is nonpositive, lacks a source crossing or reaches beyond an adjacent bone span | put the joint inside the art and declare a finite positive full width that fits both spans |
3622
+ | <a id="ASSEMBLE_HYBRID_OVERLAP"></a>`ASSEMBLE_HYBRID_OVERLAP` | two joint bands overlap on source pixels | reduce their widths so their sampling supports are disjoint |
3623
+ | <a id="PARTS_HYBRID_PAIR"></a>`PARTS_HYBRID_PAIR` | a rigid joint references a missing or inconsistent generated band | re-run assemble; keep source, joint, carriers and measured geometry together |
3624
+ | <a id="RIG_HYBRID_SOURCE"></a>`RIG_HYBRID_SOURCE` | an assembled band no longer matches its config declaration | re-run assemble with the current config |
3625
+ | <a id="RIG_HYBRID_RIGID"></a>`RIG_HYBRID_RIGID` | a band neighbour is not a rigid region | attach the upper and lower pieces as regions on their own carriers |
3566
3626
  | <a id="ASSEMBLE_ALT_TIP_NAME"></a>`ASSEMBLE_ALT_TIP_NAME` | an explicit endpoint names no supplied bone | declare the single bone or expanded link |
3567
3627
  | <a id="ASSEMBLE_ALT_TIP_POINT"></a>`ASSEMBLE_ALT_TIP_POINT` | an explicit endpoint is not finite after normalization onto the accepted rig canvas | correct the source-crop pair and scale |
3568
3628
  | <a id="PARTS_ALT_TIPS"></a>`PARTS_ALT_TIPS` | alternate endpoints are not a non-empty bone-name map of finite rig-crop pairs | re-run assemble with valid source-crop tips |
@@ -4118,6 +4178,7 @@ documents do not ship). Each is an input this package built; none is an author's
4118
4178
  | <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 |
4119
4179
  | <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 |
4120
4180
  | <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/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 |
4181
+ | <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` |
4121
4182
  | <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` |
4122
4183
 
4123
4184
  **The world the readings are taken in** (`--environment`, issue #538). A physics chain
@@ -4154,6 +4215,8 @@ flag) and 8 `CLOTH_SETTLES` warnings for 9 (selftest `CK515`).
4154
4215
 
4155
4216
  **Physics seam environments** (issue #591). Default: wind [1, 0], gravity [0, 1]. Still: wind [0, 0], gravity [0, 1]. In rig-play, environment(null) returns to Default; still air requires an explicit wind [0, 0]. For a physics rig with readable declared pairs and animations, SEAM_OVER_MOTION adds optional `worlds.default` and `worlds.still` readings. They are informational raw gap/ghost cell counts (including cells below the warning floor), never new bars or warnings; neither changes PASS, exit status or the bar count. Each world records its vectors, status and rows; each row names its pair, animation, drawn/banded frames and gap/ghost maximum and worst frame, cell size and world box. Per-frame hits are not embedded in check.json. A rendered world's `dir` is relative to check.json (`seam_motion/default` or `seam_motion/still`): it retains the complete rigc render, including frames.json, frame images and each animation's seam.json with all raw hits. The directories are published with check.json only after the readings are validated; scratch is removed, and an unrendered SKIP has no directory reference. The original reading and any custom `--environment` schedule keep their existing meaning; the two reference worlds are independent of that schedule. A rig with no wind response reports Still as SKIP with its reason; no band is SKIP, never a measured zero. With no readable declared pair or animation no worlds block is written. The same declared pair list is used for both reference worlds: neighbouring limb segments, explicit seam requirements and the proven body-root declarations described below. Thigh/hem pairs are not inferred. Adding reference worlds to a nonphysics reading stays byte-identical, with no extra key, line or render; issue #686 separately adds body-root declarations and their active-reading bar. Reference renders start physics at frame 0 at 12 fps; they do not certify a composed player scene or another sampling rate. Malformed sidecars or renderer failures remain named refusals.
4156
4217
 
4218
+ **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`.
4219
+
4157
4220
 
4158
4221
  **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.
4159
4222
 
@@ -4632,6 +4695,13 @@ rigs or edits, and with `RECOMPOSITE_HOLES` for where a hole is, not digit for d
4632
4695
  with the recomposite. On the examples the line reads the same with and without
4633
4696
  `parts.json` (the still's grid does not move).
4634
4697
 
4698
+ Under `--wind-band <spec.json>`, a third reported line, also never a FAIL
4699
+ (issue #590):
4700
+
4701
+ | line | reports | bar | `reference-demo` | `reference-sample` | SKIP when |
4702
+ | --- | --- | --- | --- | --- | --- |
4703
+ | <a id="WIND_BAND"></a>`WIND_BAND` | the steady-wind band (`mode: "steady"`, `wind: [x, 0]`, `gravity: [0, 1]` at `t = 0`) over `steps = [min, min + step, …, max]` at `fps`, against the caller's `max_gap` and `max_ghost` cell ceilings for each `"<a>:<b>"` pair in `pairs` across every animation in `motion.json` — `file` (`path`, `sha256`), `steps`, `animations`, overall `band` (`[lo, hi]` or `null`, the intersection of the per-pair bands containing the nearest passing step to `0`), `pairs_holding_all_steps`, and per-pair `band`, `breaks` (`low`, `high`) and `readings` (`x`, `gap`, `ghost`, `holds`). Does not measure time-varying gusts (`x ± a`) or transient overshoot (§6, *Steady-wind seam band*) | none — status `REPORTED`; the ceiling values `max_gap` and `max_ghost` come from the `--wind-band` file and the line reports where they hold | not written without `--wind-band` | not written without `--wind-band` | not written without `--wind-band` |
4704
+
4635
4705
  What each figure is, and is not:
4636
4706
 
4637
4707
  - **The seam is the answer to "at rest, no gap, white rim or doubled line between
@@ -4739,6 +4809,7 @@ it reads is there, and says SKIP with the reason when it is not:
4739
4809
  | `SEAM_OVER_MOTION` | `parts.json` limb records, proven body-root bindings in `rig.json`, `--requirements` seams, and every animation through `rigc render --seam` | body-root gapOpen plus UNDER-owned ghost gates using resolved per-frame draw order; existing pairs stay warnings. SKIP names absent parts, pairs, animations or bands; unavailable is never measured zero |
4740
4810
  | `RECOMPOSITE_HOLES` | `parts.json`'s `recomposite` block | no `parts.json`, or no block |
4741
4811
  | `SETUP_POSE_VS_SOURCE` | the rig spec and the painting | no `--source` |
4812
+ | `WIND_BAND` | the rig spec and the `--wind-band` file | no `--wind-band` |
4742
4813
 
4743
4814
  `PASS` reads every bar that measured, and the last console line says how many did:
4744
4815
  `check: PASS; 1 of 9 bar(s) measured, 8 skipped (loop, seam, …)` is a rig with no animation at
package/docs/MIGRATION.md CHANGED
@@ -432,3 +432,14 @@ that should keep moving wants an environment at render (`rigc render --environme
432
432
  AUTHORING §3, *What moves them*, has a worked schedule for `wind`) or a consumer's
433
433
  animation that moves the bone it hangs from. The drafted idle keys no hip: issue #502
434
434
  measured a hip sway at the idle's period on every public example, and each failed a bar.
435
+
436
+ ## Opt-in hybrid joint bands (issue #399)
437
+
438
+ A lower limb segment may declare `hybrid: {part, width?}` to replace its cap
439
+ with one generated two-bone band. Its neighbours remain rigid regions. The
440
+ full axial width is source-derived unless explicitly supplied; no generated
441
+ band attachment entry is added to the config. See AUTHORING's *Hybrid joint
442
+ bands* for the source, name, width and overlap refusals. Existing configs need
443
+ no change. The parts reader gains `limb.hybrid` and `limb.band` records only
444
+ for this mode, and the recorded lower range is measured once. The normal
445
+ one-bone rule and existing range thresholds remain unchanged.
package/docs/STABILITY.md CHANGED
@@ -59,9 +59,9 @@ defaults are the help's too, and none is guessed from the art.
59
59
  | `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`), `--full`, `--head` (the turn control's depths; not with `--from-config`), `--grid-rule` (`figure` or `part`, the drafted lattice's spacing; not with `--from-config`), `--cloth` (a config whose `cloth` block is the recipe, issue #364; not with `--from-config`), `--write-config` (existing config for an ordinary proposal, issue #356; a new file with `--cloth`, issue #537; not with `--from-config` — additive: without it nothing `propose` writes changes), `--from` (the config the recipe is changed from, only with `--write-config`, issue #571 — additive: without it nothing `--write-config` writes changes), `--only-missing` (only with `--parts` and `--write-config`, not with `--cloth` or `--from-config`, issue #671 — additive: without it nothing `--write-config` writes changes) |
60
60
  | `compare` | `--left`, `--right`; optional `--map` |
61
61
  | `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
62
- | `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--set` or `--all-sets` (issue #454), `--environment` (issue #538, additive) |
62
+ | `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--set` or `--all-sets` (issue #454), `--environment` (issue #538, additive), `--wind-band` (issue #590, additive) |
63
63
  | `loop` | `--frames`, `--out`; optional `--palette` |
64
- | `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`, `--alt` (as for `assemble`, issue #454), `--set` or `--all-sets` (as for `check`, issue #454), `--environment` (as for `check`, issue #538, additive) |
64
+ | `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`, `--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) |
65
65
  | `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
66
66
  | `mesh-draft` | `--config`, `--parts`, `--out`; optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon), `--more-triangles` |
67
67
  | `mesh-stats` | `--config`, `--parts` (issue #566, additive: prints, writes nothing) |
@@ -137,7 +137,8 @@ part name, `[i]` an entry of a list):
137
137
  - **limbs.&lt;limb&gt;.front** — required: `parts` (own segment parts, issue #664), `after` (a produced part)
138
138
  - **limbs.&lt;limb&gt;.layout** — exactly one of `before`, `after`; optional: `parts` (own consecutive segment-part references, issue #647)
139
139
  - **limbs.&lt;limb&gt;.layout.before**, **limbs.&lt;limb&gt;.layout.after** — exactly one typed target: `limb` or `part`
140
- - **limbs.&lt;limb&gt;.segments[i]** — required: `part`, `from`, `bone`; required below a joint (i > 0): `joint`; optional below a joint: `range`, `cap`, `edge`, `cap_shape`, `cut_band`
140
+ - **limbs.&lt;limb&gt;.segments[i]** — required: `part`, `from`, `bone`; required below a joint (i > 0): `joint`; optional below a joint: `range`, `cap`, `edge`, `cap_shape`, `cut_band`, `hybrid`
141
+ - **limbs.&lt;limb&gt;.segments[i].hybrid** — required: `part`; optional: `width` (issue #399; lower segments only, replaces cut-cap options, one source and two adjacent rigid carriers)
141
142
  - **bones[i]**, a single bone — required: `name`, `parent`, `at`; optional: `tip`, `inherit`
142
143
  - **bones[i]**, a chain — required: `chain`, `parent`, `points`, `tip`; optional: `link_inherit`
143
144
  - **stops[i]** — required: `chain`, `against`, `at`, `inward`, `outward`; optional: `links`, `lower` (issue #404)
@@ -202,7 +203,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
202
203
  | `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, `short_px` (`left`, `right`) after it only when `missing_px` is above 0 (issue #557, additive), `excluded_px` after them only on a subset set's limb part whose cap the set exclusion took a pixel of (issue #603, additive: each pixel taken out of the count it was in, so the cap's counts are the written PNG's), then `shape` and `upper_px` under the segment's `cap_shape` — `upper_px` without the shape only when the upper segment gave a pixel (issue #557, additive) — and after them `lower_px`, `lower_kept_px` and `lower_missing_px` under `cap_shape: "ball"` (issue #579, additive), and `edge`, `feather_radius` and `feathered_px` under its `edge`, and `cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and `stroke_luma_max` (when the band has a split) under its `cut_band`, issue #360), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px` — on a subset set's limb part counted after the set exclusion, issue #603 — `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242); `single_run_pad` (`file_size`, `side`, `left`, `top`) last, only for one run whose file is not the `seethrough.resolution` square (issue #366) |
203
204
  | `rig/rig.json`, `rig/motion.json`, `rig/images/` | `rig`, `build`, `compose` | rig-c's rig spec and motion spec, `spec` `rigc-rig/1` and `rigc-motion/1`: the format is rig-c's, and this package writes only what rig-c's parser accepts; a bone carries `inherit` only where config.bones declares one, rig-c's mode name as written (issue #348) |
204
205
  | `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones` (the sorted names the attachment's weights bind in the written `rig.json`, in every mode — since 1.49.1 (issue #536) a rig with `turn` or `stops` names the twins and `<link>_d` bones there, where the row named the segment bones they replace: a fix of a wrong name, not a format change; a rig with neither writes the bytes it wrote), `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); or `mode` "explicit" with `image_sha256` and `region_sha256` (the verified guards; counts and coverage are measured from the supplied mesh, no legacy generator report is adopted); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins`, and `faint` only when an island was left out as faint — only when it declares `contour.bridges`); or `mode` "influence" with `influence` (as declared), `reach` (`farthest`, `at`, `nearest`, `capped`, `widest`) and `lattice` (rig-c's `SegmentsLatticeReport`: `cols`, `rows`, `artCells`, `keptCells`, `passes`, `islands`) (issue #378); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
205
- | `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40`, and `step_px` (`left`, `right`) only on a joint whose cap record carries `short_px` (issue #557, additive) — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493 retains existing pair warnings; issue #686 gates GAP on proven body-root pairs only (gap-only; mixed ghost remains INFO until rig-c/core#1555 supplies owners): `status` — `WARN`, `CLEAR`, `SKIP` or `FAIL` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, optional `root` (`limb`, `bone`, `parent`) on a proven body-root pair, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`, and only on new-root rows stock `gapOpen` plus UNDER-owned `ghostA`/`ghostB` cells selected by resolved per-frame draw order gate their summed count against the unchanged strict one-cell floor on the original raw kind boxes (their union when both kinds contribute), never narrowed partition boxes, while `gapCovered` and OVER-owned ghost cells are excluded (issue #711); `cells` retains partition counts, UNDER owner and counted total, `worst` retains frame/count/cell/box, and `roots` retains status, mode `gap-open+under-ghost`, ghost_owner `resolved-draw-order`, pairs, failed, reason and optional unmeasured bindings; eligible candidates whose valid setup bands are all zero retain a named unmeasured root and SKIP bar, including when no pair is selected; measured root FAIL wins over SKIP, while measured PASS plus an unmeasured root remains SKIP; every eligible body drawing with stock setup `banded > 0` is selected, including both when a parent is shared; issue #708 resolves a body slot on its exact generated turn child to the same named carrier only after unique identity-bone/control/per-part-transform proof, without changing pair identity, setup selection or the direct-carrier path; setup-only observation preserves the authored motion and default drawing, root-free inputs add no setup compile/render, and no animation remains SKIP rather than a measured zero; existing internal pair warnings and their exits are unchanged; issue #591 adds optional `worlds` on physics rigs with readable declared pairs and animations: `default` and `still`, each `wind`, `gravity`, `status` (`REPORTED` or `SKIP`), `reason` on SKIP, `dir` only when rendered (relative to check.json: `seam_motion/default` or `seam_motion/still`, the complete retained render with frames.json and each animation's raw seam.json), and `rows` — pair, animation, status, drawn, banded, `gap` and `ghost`, each `max_px` and `worst` only; worst carries `index`, `px`, `cell`, `box`. Per-frame hits stay in the raw sidecars. These raw reference readings add no bar or warning and leave the custom environment reading unchanged), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, `config` under `build` only (issue #607, additive: `path` as `--config` was typed, `sha256` of the bytes the build read; `check` and `compose` read no config and write none), a `requirements` block under `--requirements` (or `build`'s `config.requirements`) — `file` first from issue #556, additive: `path` as typed or declared, `sha256`, `keys` — and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes. Missing stock partitions or resolved owner are named refusals, never zero. No whole-box exemption or consumer compositor remains; internal warnings and reference raw readings are unchanged. |
206
+ | `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40`, and `step_px` (`left`, `right`) only on a joint whose cap record carries `short_px` (issue #557, additive) — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493 retains existing pair warnings; issue #686 gates GAP on proven body-root pairs only (gap-only; mixed ghost remains INFO until rig-c/core#1555 supplies owners): `status` — `WARN`, `CLEAR`, `SKIP` or `FAIL` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, optional `root` (`limb`, `bone`, `parent`) on a proven body-root pair, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`, and only on new-root rows stock `gapOpen` plus UNDER-owned `ghostA`/`ghostB` cells selected by resolved per-frame draw order gate their summed count against the unchanged strict one-cell floor on the original raw kind boxes (their union when both kinds contribute), never narrowed partition boxes, while `gapCovered` and OVER-owned ghost cells are excluded (issue #711); `cells` retains partition counts, UNDER owner and counted total, `worst` retains frame/count/cell/box, and `roots` retains status, mode `gap-open+under-ghost`, ghost_owner `resolved-draw-order`, pairs, failed, reason and optional unmeasured bindings; eligible candidates whose valid setup bands are all zero retain a named unmeasured root and SKIP bar, including when no pair is selected; measured root FAIL wins over SKIP, while measured PASS plus an unmeasured root remains SKIP; every eligible body drawing with stock setup `banded > 0` is selected, including both when a parent is shared; issue #708 resolves a body slot on its exact generated turn child to the same named carrier only after unique identity-bone/control/per-part-transform proof, without changing pair identity, setup selection or the direct-carrier path; setup-only observation preserves the authored motion and default drawing, root-free inputs add no setup compile/render, and no animation remains SKIP rather than a measured zero; existing internal pair warnings and their exits are unchanged; issue #591 adds optional `worlds` on physics rigs with readable declared pairs and animations: `default` and `still`, each `wind`, `gravity`, `status` (`REPORTED` or `SKIP`), `reason` on SKIP, `dir` only when rendered (relative to check.json: `seam_motion/default` or `seam_motion/still`, the complete retained render with frames.json and each animation's raw seam.json), and `rows` — pair, animation, status, drawn, banded, `gap` and `ghost`, each `max_px` and `worst` only; worst carries `index`, `px`, `cell`, `box`. Per-frame hits stay in the raw sidecars. These raw reference readings add no bar or warning and leave the custom environment reading unchanged), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `WIND_BAND` after it under `--wind-band` only (issue #590, additive: `status` `"REPORTED"`, `spec` `"rig-parts-wind-band/1"`, `path` as given, `sha256`, `steady_wind_only` `true`, `gravity` `[0, 1]`, `min`, `max`, `step`, `steps`, `fps`, `animations`, `max_gap`, `max_ghost`, `holds_from`, `holds_to`, `pairs_holding_all`, and `pairs` — each `pair`, `a`, `b`, `holds_all`, `holds_from`, `holds_to`, `first_break_below`, `first_break_above`, `worst_gap`, `worst_ghost`, `steps`), `skipped`, `config` under `build` only (issue #607, additive: `path` as `--config` was typed, `sha256` of the bytes the build read; `check` and `compose` read no config and write none), a `requirements` block under `--requirements` (or `build`'s `config.requirements`) — `file` first from issue #556, additive: `path` as typed or declared, `sha256`, `keys` — and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes. Missing stock partitions or resolved owner are named refusals, never zero. No whole-box exemption or consumer compositor remains; internal warnings and reference raw readings are unchanged. |
206
207
  | `layers.json`, `<index>_<name>.png` | `layers --export` | the wrapper form `layers` reads (issue #359): `width`, `height`, then `layers` — each `name`, `filename`, `left`, `top`, `right`, `bottom`, `depth_median` (the input's own; `null` for a PSD, the manifest's order then being the draw order) — drawn layers in draw order, then the hidden-art layers; a PNG's name is its index and its layer's name, and the manifest, not the file name, says which layer it is |
207
208
  | the template `.psd` | `layers --template` | one empty layer per name `templateLayerNames` (`src/layers.ts`) holds, bottom first; the same canvas writes the same bytes |
208
209
  | `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `constraints` with `--cloth` (issue #364: a physics constraint per tree link named `<bone>_physics`, a dangling end's `<bone>_translate`, and a skirt swing's transforms `<swing>_<tree>`), `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes` |
@@ -274,7 +275,7 @@ no parent rows. Hashes and names are deterministic.
274
275
  Files authors write for this package, each with a spec id the reader holds:
275
276
  `spine-parts-requirements/1` (`check --requirements`), `spine-parts-scene/1`
276
277
  (`compose --scene`), `spine-parts-keypoints/1` (`propose --keypoints`),
277
- `spine-parts-bonemap/1` (`compare --map`); and one another tool writes, read as that tool defines it: rig-c/review's `rig-review-mesh-edit/1` (`remesh --delta`). The spec ids keep the package's former
278
+ `spine-parts-bonemap/1` (`compare --map`), `rig-parts-wind-band/1` (`check --wind-band`, `build --wind-band`, issue #590, additive); and one another tool writes, read as that tool defines it: rig-c/review's `rig-review-mesh-edit/1` (`remesh --delta`). The spec ids of files that existed before the rename keep the package's former
278
279
  name on purpose: they name a file format, not the tool, and a file written before
279
280
  the rename still reads. They do not change at 1.0.
280
281
 
@@ -369,3 +370,14 @@ fetched, the `chain` suite builds each of them and holds `parts.json`
369
370
  (`CH02`), `rig.json`, `motion.json` and `mesh_report.json` (`CH03`),
370
371
  `check.json` (`CH04`) and the gate file (`CH05`) to its tracked `expected/`,
371
372
  field by field, with the tolerances `CH07` plants against.
373
+
374
+ ### Opt-in hybrid provenance (issue #399)
375
+
376
+ A lower rigid limb piece may carry `limb.hybrid` instead of `limb.cap`:
377
+ `{part, upper, width, radius, at, axis}`. Its generated mesh carries
378
+ `limb.band`: `{upper, width, radius, at, axis}`. Their source, joint, lower
379
+ carrier and measured geometry must agree; the lower rigid piece alone carries
380
+ `range`. The default limb records and byte contracts are unchanged. The
381
+ existing `LIMB_ONE_BONE` line checks ordinary segments as before; the band
382
+ requires exactly its two adjacent carriers. `SEAM_OVER_RANGE` uses the stored
383
+ radius and existing rest-art comparison, without changing its bar.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rig-parts",
3
- "version": "1.64.0",
3
+ "version": "1.65.0",
4
4
  "description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/assemble.ts CHANGED
@@ -4041,6 +4041,11 @@ function cutLimbs(
4041
4041
  const at = parts.findIndex((q) => q.limb === l && q.segment === k);
4042
4042
  const upAt = parts.findIndex((q) => q.limb === l && q.segment === k - 1);
4043
4043
  const upper = rasters[upAt];
4044
+ if (sg.hybrid !== undefined) {
4045
+ // Geometry only: the band replaces the cap, and never reads hidden fill.
4046
+ parts[at].record.cap = { at: [...line.at], width: w3, radius, box, layer_px: 0, missing_px: 0 };
4047
+ return;
4048
+ }
4044
4049
  // Issue #359: the lower segment's part's hidden-art layer is its cap when the segment names no cap image; with one named, a refusal.
4045
4050
  const hidden = hiddenOf(sg.from);
4046
4051
  if (hidden !== null && sg.cap !== undefined) {
@@ -4204,7 +4209,76 @@ function cutLimbs(
4204
4209
  parts[at].record.cap = cap;
4205
4210
  capFrom[at] = from;
4206
4211
  const c = counts.find((q) => q.of === sg.from);
4207
- if (c !== undefined) c.joints.push({ limb: l, joint: String(sg.joint), part: sg.part, upper: segs[k - 1].part, cap, from: sg.from, upperFrom: upFrom, half: pyRound(half, 3), shortInHalf });
4212
+ if (c !== undefined && sg.hybrid === undefined) c.joints.push({ limb: l, joint: String(sg.joint), part: sg.part, upper: segs[k - 1].part, cap, from: sg.from, upperFrom: upFrom, half: pyRound(half, 3), shortInHalf });
4213
+ });
4214
+ }
4215
+ refuseIfAny(problems);
4216
+ // Issue #399: transfer the continuous source band after the cut control has
4217
+ // measured its joint. Remove its caps: only immutable source pixels enter the band.
4218
+ for (const l of names) {
4219
+ const segs = limbs[l].segments;
4220
+ segs.forEach((sg, k) => {
4221
+ if (sg.hybrid === undefined || k === 0) return;
4222
+ const object = `config.limbs.${l}.segments[${k}].hybrid`;
4223
+ if (sg.from !== segs[k - 1].from) {
4224
+ problems.push({ code: 'ASSEMBLE_HYBRID_SOURCE', object, detail: `takes "${sg.from}" below and "${segs[k - 1].from}" above; one continuous source layer is required — select the same layer for both rigid neighbours` });
4225
+ return;
4226
+ }
4227
+ const line = (lines.get(l) as Array<JointLine | null>)[k] as JointLine;
4228
+ const source = builtOf(sg.from) as Built;
4229
+ const li = names.indexOf(l), own = owners.get(sg.from) as Int32Array;
4230
+ const art = (x: number, y: number): boolean => x >= 0 && y >= 0 && x < W && y < H && own[y * W + x] === li && source.r.data[(y * W + x) * 4 + 3] > OPAQUE_ALPHA_ABOVE;
4231
+ const run = limbRun(line, art, [0, 0, W - 1, H - 1]);
4232
+ const width = sg.hybrid.width ?? (run === null ? 0 : 2 * halfWidthAbout(run));
4233
+ const upper = places.get(segs[k - 1].bone) as BonePlace;
4234
+ const lower = places.get(sg.bone) as BonePlace;
4235
+ const reach = (p: readonly number[]): number => Math.abs((p[0] - line.at[0]) * line.axis[0] + (p[1] - line.at[1]) * line.axis[1]);
4236
+ const end = lower.end;
4237
+ const prev = k > 1 ? segs[k - 1].hybrid?.width : undefined;
4238
+ if (run === null || !(width > 0) || end === null || width / 2 >= Math.min(reach(upper.at), reach(end)) || (prev !== undefined && (prev + width) / 2 >= reach(upper.at))) {
4239
+ problems.push({ code: 'ASSEMBLE_HYBRID_WIDTH', object, detail: `width ${width} rig px at [${line.at.join(', ')}]; a positive art crossing and a band strictly inside both bone spans, without adjacent-band overlap, are required` });
4240
+ return;
4241
+ }
4242
+ const upAt = parts.findIndex((q) => q.limb === l && q.segment === k - 1 && q.record.band === undefined);
4243
+ const loAt = parts.findIndex((q) => q.limb === l && q.segment === k && q.record.band === undefined);
4244
+ const earlier = parts.filter((q) => q.limb === l && q.record.band !== undefined);
4245
+ const overlap = earlier.find((q) => {
4246
+ const h = q.record.band as NonNullable<LimbRecord['band']>;
4247
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
4248
+ if (!art(x, y)) continue;
4249
+ const d = Math.abs((x + 0.5 - h.at[0]) * h.axis[0] + (y + 0.5 - h.at[1]) * h.axis[1]);
4250
+ if (d <= h.width / 2 + 1 && Math.abs(jointOffsets(line, x, y).along) <= width / 2 + 1) return true;
4251
+ }
4252
+ return false;
4253
+ });
4254
+ if (overlap !== undefined) {
4255
+ problems.push({ code: 'ASSEMBLE_HYBRID_OVERLAP', object, detail: `overlaps band "${overlap.part}" on source art; disjoint joint bands are required — reduce their widths` });
4256
+ return;
4257
+ }
4258
+ const r = newRaster(W, H), region = newMask(W, H);
4259
+ for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
4260
+ const p = y * W + x;
4261
+ if (own[p] !== li) continue;
4262
+ const along = jointOffsets(line, x, y).along;
4263
+ // One source pixel of overlap at each boundary keeps bilinear samples
4264
+ // opaque. The overlap is wholly bound to the matching rigid carrier.
4265
+ if (Math.abs(along) <= width / 2 + 1) {
4266
+ r.data.set(source.r.data.subarray(p * 4, p * 4 + 4), p * 4);
4267
+ region.data[p] = source.region === null ? 1 : source.region.data[p];
4268
+ }
4269
+ if (along > -width / 2) { rasters[upAt].data.fill(0, p * 4, p * 4 + 4); regions[upAt].data[p] = 0; }
4270
+ if (along < width / 2) { rasters[loAt].data.fill(0, p * 4, p * 4 + 4); regions[loAt].data[p] = 0; }
4271
+ }
4272
+ const base = parts[loAt].record;
4273
+ const geometry = { upper: segs[k - 1].bone, width, radius: (base.cap as CapRecord).radius, at: [...line.at] as [number, number], axis: [...line.axis] as [number, number] };
4274
+ delete base.cap;
4275
+ base.hybrid = { part: sg.hybrid.part, ...geometry };
4276
+ capFrom[loAt] = null;
4277
+ parts.push({ limb: l, segment: k, part: sg.hybrid.part, of: sg.from, record: { name: l, segment: k, of: base.of, bone: sg.bone, joint: sg.joint, band: geometry } });
4278
+ pieces.push({ ...source, r, region, merged: mergedIn(source, region) });
4279
+ capFrom.push(null);
4280
+ // Existing piece objects refer to the same raster and mask.
4281
+ for (const i of [upAt, loAt]) (pieces[i] as Built).merged = mergedIn(pieces[i] as Built, regions[i]);
4208
4282
  });
4209
4283
  }
4210
4284
  refuseIfAny(problems);