@rig-c/parts 1.14.0 → 1.15.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
@@ -17,7 +17,7 @@ import { tmpdir } from 'node:os';
17
17
  import { basename, dirname, extname, join } from 'node:path';
18
18
  import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
19
19
  import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage, suppliedCheckFlag } from './src/build.ts';
20
- import { ACCEPTABLE_BARS, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, NO_PARTS_SENTENCE, SEAM_MEAN_BAR, SOURCE_SENTENCE, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
20
+ import { ACCEPTABLE_BARS, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, rigcRunnerAt, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, NO_PARTS_SENTENCE, SEAM_MEAN_BAR, SOURCE_SENTENCE, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
21
21
  import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
22
22
  import { type CharacterConfig, loadConfig, loadEarlyConfig, loadPaintConfig } from './src/config.ts';
23
23
  import { PartsError, problemLine } from './src/errors.ts';
@@ -347,8 +347,8 @@ usage:
347
347
  BREATH_VISIBLE, BLINK_NO_HOLE, TIP_OVER_ROOT, STILL_REGIONS_DARK and
348
348
  RECOMPOSITE_HOLES; without an idle the loop and every line that reads idle
349
349
  frames, and no idle is rendered. The gate always runs. When motion.json
350
- holds an animation beside the idle, ANIMATIONS_DO_NOT_FOLD reads each with
351
- rigc render --folds, physics constraints left out: a mesh triangle turned
350
+ holds an animation, ANIMATIONS_DO_NOT_FOLD reads each, the idle included,
351
+ with rigc render --folds, physics constraints left out: a mesh triangle turned
352
352
  over against the setup pose fails, unless the --requirements file's
353
353
  may_fold declares that slot and animation with a why. The last line says
354
354
  how many of the bars measured — nine, ten with the fold bar — and names
@@ -825,14 +825,15 @@ function cmdRig(args: string[]): number {
825
825
  * `cli_core.ts` and rigc's own validator where it does not. No stage runs a
826
826
  * rigc source file by path: under rig-c 2.0, `cli.ts` by path is the
827
827
  * spine-core entry whether or not spine-core is there. `src/` is pure and takes
828
- * the spawn from here.
828
+ * the spawn from here. A call that fails carries which rigc ran it, path and
829
+ * version ({@link rigcRunnerAt}, issue #337), and every refusal quoting it says so.
829
830
  */
830
831
  function rigcRunner(bin: string): RigcRunner {
831
- return (args) => {
832
- const r = spawnSync(bin, [...args], { encoding: 'utf8', maxBuffer: 1 << 28 });
833
- if (r.error !== undefined) return { status: 127, out: `could not start ${bin}: ${r.error.message}` };
832
+ return rigcRunnerAt(bin, (b, args) => {
833
+ const r = spawnSync(b, [...args], { encoding: 'utf8', maxBuffer: 1 << 28 });
834
+ if (r.error !== undefined) return { status: 127, out: `could not start ${b}: ${r.error.message}` };
834
835
  return { status: r.status ?? 1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
835
- };
836
+ });
836
837
  }
837
838
 
838
839
  /** The command's valued flags as `--flag value` pairs, each one its {@link TAKES} row holds; `required` must all be given. Its bare switches are taken out by the caller before this reads the rest. A string is a usage error. */
package/docs/AUTHORING.md CHANGED
@@ -98,7 +98,7 @@ read by no CPU stage).
98
98
  | `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` 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), `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`). `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 |
99
99
  | `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip}` whose links are named `<chain>0 … <chain>n`. Parents come before children. In `rig.json` each link is **turned along its chain** — `rotation` is the direction from its origin to the next link's (the last link's to `tip`) and `length` that distance (issue #73), so a physics constraint added downstream finds a lever; under `--idle-keys ctl` the link's `<link>_ctl` carries the turn and the length and the link sits at local rotation 0 beneath it. A single bone 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 |
100
100
  | `meshes.<part>` | rig | **proposed**, then corrected. Exactly one of `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 every mode: `r` (added to every distance before weighting: `w = 1/(d + r)²`), `segments` (a chain name, a bone name with a `tip`, or `[bone, [x0,y0], [x1,y1]]`), and, **authored**, optional, `exponent` (issue #161): a number above 0 that replaces the 2 in that rule, `w = 1/(d + r)^exponent` — a larger one hands each vertex more fully to its nearest bone; absent is 2 and changes no byte, a value that is not a number above 0 is `CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`, and a declared one is echoed as `exponent` in the mesh's `mesh_report.json` row; the exponent is the distance rule's, so declared beside `rule: "heat"` it is refused (`CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`). The segment list is the one authored decision about a layer: which bones may pull it. The slot's bone is the first segment's. Optional `rule` (issue #161): `"distance"` — the rule above, and what an absent `rule` means (every byte is the same) — or `"heat"`, bone heat over the part's own silhouette, in every mode: the lattice's art (alpha above 8, 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 |
101
- | `meshes.<part>.contour` | rig | **authored**, optional (issue #84): `{tolerance, margin, spacing, maxEdge?, budget?, stray?, bridges?, regions?}`. `tolerance` — rig-c's Douglas–Peucker tolerance on the traced outline, px, 0 or more; `margin` — how far the silhouette grows before it is traced, px: 0, or 1 or more (a pixel joins when its centre lies within `margin` of an art pixel's centre; a value above 0 and below 1 adds nothing and is refused, `CONFIG_FIELD_TYPE`); `spacing` — the background interior spacing, px, above 0; `maxEdge` (issue #265) — the longest outline edge, px, above 0: every simplified edge longer than it is cut into `ceil(length / maxEdge)` equal pieces, each cut point on the 1/256 px grid, before any region or rib touches the outline (absent: nothing is cut, and no byte moves; under 2/256 px the rig stage refuses it, `CONTOUR_PARAMETER`, because two cut points could snap together); the row echoes it in `params` and counts the points it added in `contour.outlineCut`, both only when declared; `budget` — the most vertices the mesh may have (refused above it, nothing thinned); `stray` — the largest island, in art pixels, that may be left out of the mesh (absent: none is); `bridges` (issue #326) — `true`, its one value: every island the art holds is meshed (but those `stray` leaves out; the largest, or a tie for it, is never left out), and the pieces are joined into the one outline rig-c takes by bridges — the kept islands, holes filled, grow together by `margin`, so islands closer than about twice the margin are one grown silhouette, traced as one; each silhouette is traced, simplified, capped, refined and triangulated as a one-island mesh is; then, starting from the silhouette with the most art, the nearest pair of a vertex on the joined outline and one on a silhouette still apart whose quad holds (wound as the mesh, crossing no outline, holding no other vertex, one closed outline after) is joined by that quad, two triangles over no art, until none is apart. Coverage and overshoot read the silhouettes' triangles; a bridge's area is in `meshArea` and in the row's `contour.bridges` (`islands`, `units`, and per join `unit`, `span` — its two crossing edges, px — `area`, `vertices`, `pair`), and the row's `params` echoes `bridges`, both only when declared. Absent, a second island is refused as below and no byte moves; on one island it writes the mesh the undeclared entry writes. Any other value is `CONFIG_FIELD_TYPE`; beside `ribs` it is `CONTOUR_PARAMETER` (a rib runs across one outline); a join no pair makes is `CONTOUR_BRIDGE`. A bridge stretches with whatever pulls its two ends apart, so it belongs on a part whose pieces ride one bone — the only case `mesh-draft` drafts; `regions` — local deformation regions, each `{name, shape, bone, spacing, band}` with `shape` `"circle"` (`cx, cy, r`) or `"polygon"` (`points`, 3 or more): `bone` is the region's control bone (any bone `bones` declares), `spacing` the interior spacing inside the region and its band, `band` the width the bone's weight falls across. Every length and position is **rig px**, like every other in the config; the part image is the rig less `(x − 4, y − 4)` (its box less the pad), a translation, so a length is the same number in both. A region's `cx`, `cy`, `r`, `band` and `points` are multiples of 1/256 px (`CONFIG_FIELD_TYPE` otherwise); region names are unique per mesh (`CONFIG_REGION_NAME_UNIQUE`). The alpha threshold is not a field: art is alpha above 8, the lattice's and `check`'s |
101
+ | `meshes.<part>.contour` | rig | **authored**, optional (issue #84): `{tolerance, margin, spacing, maxEdge?, budget?, stray?, bridges?, regions?}`. `tolerance` — rig-c's Douglas–Peucker tolerance on the traced outline, px, 0 or more; `margin` — how far the silhouette grows before it is traced, px: 0, or 1 or more (a pixel joins when its centre lies within `margin` of an art pixel's centre; a value above 0 and below 1 adds nothing and is refused, `CONFIG_FIELD_TYPE`); `spacing` — the background interior spacing, px, above 0; `maxEdge` (issue #265) — the longest outline edge, px, above 0: every simplified edge longer than it is cut into `ceil(length / maxEdge)` equal pieces, each cut point on the 1/256 px grid, before any region or rib touches the outline (absent: nothing is cut, and no byte moves; under 2/256 px the rig stage refuses it, `CONTOUR_PARAMETER`, because two cut points could snap together); the row echoes it in `params` and counts the points it added in `contour.outlineCut`, both only when declared; `budget` — the most vertices the mesh may have (refused above it, nothing thinned); `stray` — the largest island, in art pixels, that may be left out of the mesh (absent: none is); `bridges` (issue #326) — `true`, its one value: islands are read in this order — **dust first** (those `stray` leaves out), **then faint islands** (every pixel's alpha under 64, a quarter of full opacity: left out the same way, their pixels kept in the image but not meshed, so not drawn, and named in the row's `contour.bridges.faint` with their px, brightest alpha and box), **then every visible island is meshed and bridged, whatever the bridge costs** — overdraw is reported, never a reason to drop art; the largest island, or a tie for it, is never left out, faint or not: dropping it would draft the part without its art, and a part whose main body is faint is a question about the layer, which the plan and the author own, not one for the mesh — and the pieces are joined into the one outline rig-c takes by bridges — the kept islands, holes filled, grow together by `margin`, so islands closer than about twice the margin are one grown silhouette, traced as one; each silhouette is traced, simplified, capped, refined and triangulated as a one-island mesh is; then, starting from the silhouette with the most art, the nearest pair of a vertex on the joined outline and one on a silhouette still apart whose quad holds (wound as the mesh, crossing no outline, holding no other vertex, one closed outline after) is joined by that quad, two triangles over no art, until none is apart. Coverage and overshoot read the silhouettes' triangles; a bridge's area is in `meshArea` and in the row's `contour.bridges` (`islands`, `units`, and per join `unit`, `span` — its two crossing edges, px — `area`, `vertices`, `pair`; and `faint`, only when an island was left out as faint), and the row's `params` echoes `bridges`, both only when declared; `art_coverage` counts a faint island's pixels as art not covered. Absent, a second island is refused as below and no byte moves; on one island it writes the mesh the undeclared entry writes. Any other value is `CONFIG_FIELD_TYPE`; beside `ribs` it is `CONTOUR_PARAMETER` (a rib runs across one outline); a join no pair makes is `CONTOUR_BRIDGE`. A bridge stretches with whatever pulls its two ends apart, so it belongs on a part whose pieces ride one bone — the only case `mesh-draft` drafts; `regions` — local deformation regions, each `{name, shape, bone, spacing, band}` with `shape` `"circle"` (`cx, cy, r`) or `"polygon"` (`points`, 3 or more): `bone` is the region's control bone (any bone `bones` declares), `spacing` the interior spacing inside the region and its band, `band` the width the bone's weight falls across. Every length and position is **rig px**, like every other in the config; the part image is the rig less `(x − 4, y − 4)` (its box less the pad), a translation, so a length is the same number in both. A region's `cx`, `cy`, `r`, `band` and `points` are multiples of 1/256 px (`CONFIG_FIELD_TYPE` otherwise); region names are unique per mesh (`CONFIG_REGION_NAME_UNIQUE`). The alpha threshold is not a field: art is alpha above 8, the lattice's and `check`'s |
102
102
  | `meshes.<part>.ribs` | rig | **authored**, optional (issue #188), in the `contour` and `auto` modes: `{<chain>: {stations}, …}`, each key a chain this mesh's `segments` name. Each link of the chain gets a row of vertices across the part at its origin and at `stations` points evenly between its origin and its end (`stations`, a whole number, 0 or more, required; a chain of L links carries L × (stations + 1) ribs), each rib a row of vertices across the part, kept as a row of locked edges; the rule is "Ribs along a chain" under "A contour mesh". Absent places none and changes no byte. Refused: a lattice mesh with ribs (`CONFIG_RIBS_MODE` — its vertices are its grid's corners), a key that is not one of the mesh's chains (`CONFIG_NAME_RESOLVES`), an object naming no chain or a `stations` that is not a whole number from 0 (`CONFIG_FIELD_TYPE`); at the rig stage `CONTOUR_RIB_ART`, `CONTOUR_RIB_LINK`, `CONTOUR_RIB_CROSSING`, `CONTOUR_RIB_EDGE` (the contour refusals). The mesh's `mesh_report.json` row gains `ribs` — the declaration, the ribs placed and the vertices they hold — and its contour report (the source's, in `auto`) lists each rib's vertices |
103
103
  | `meshes.<part>.influences` | rig | **authored**, optional (issue #284), in the `grid` and `contour` modes: `{maxInfluences?, floor?}`, at least one. `maxInfluences` — the most bones that pull one vertex, a whole number 1 or more, or `null`, no cap (every bone the mesh's `segments` name; `null` is the config's spelling of a bound declared absent, as in `maxOvershoot` and `weightJump`); absent is 4. `floor` — `"cut"` (the capped shares normalised, any under 0.03 dropped, the rest normalised again; what an absent `floor` means) or `"continuous"` (0.03 subtracted from every capped share, those at or below 0 dropped, the rest normalised again, so a bone fades out of a vertex instead of leaving it in one step). Absent, the mesh is weighted at 4 and the cut and every byte is the one written before the field existed; declared, it applies under either `rule`, and is echoed as `influences` in the mesh's `mesh_report.json` row. `propose` never writes it, and `mesh-draft` carries a lattice's declaration to the contour candidate it drafts. A cap that is not a whole number 1 or more or `null` (the string `"none"` included), another `floor`, or an object naming neither is `CONFIG_FIELD_TYPE`; a vertex the declared cut leaves with no bone is `RIG_INFLUENCES_KEPT`; beside `auto` it is `CONFIG_INFLUENCES_MODE` (that mode's cap and floor are `auto.influences`). What each setting trades is "The weight cut" below |
104
104
  | `meshes.<part>.stray` | rig | **authored**, optional (issue #303), in the `grid` mode: the largest island, in art pixels, that may be left out of the lattice — the contour mode's `contour.stray`, with the same meaning, over the same islands (the 4-connected islands of the art, alpha above 8): every island of `stray` art px or fewer is cleared from the art the lattice and, under `rule: "heat"`, its silhouette are built over, except an island as large as the largest, which is never left out (two equal largest pieces both stay). Unlike the contour mode an island above the figure is not refused — the lattice bridges every island it keeps, as it always has. An island that sits in a cell the lattice keeps for other art leaves the lattice unchanged: there `stray` changes only the silhouette `rule: "heat"` reads, and gives back no triangle (demo `bottomwear`'s 2 px island, in the cell kept for the hem's tip, is that case). Absent, nothing is left out and every byte is the one written before the field existed; a figure that leaves nothing out changes no byte of `rig.json` or `motion.json`. The part's image is not touched: a pixel left out stays in it, and is drawn only where a cell kept for other art happens to cover it; `art_coverage` is over all the art, the islands left out included. Declared, the mesh's `mesh_report.json` row echoes `stray` and adds `stray_cleared` (`islands`, `pixels`, `art_pixels`, after `influences`), and the rig stage's mesh line ends `stray <n> left out <i> island(s), <p> of <a> art px`. A value that is not a whole number from 0 is `CONFIG_FIELD_TYPE`; beside `contour` or `auto` it is `CONFIG_STRAY_MODE` (those modes state it in `contour.stray` and `auto.source.stray`). `propose` and `mesh-draft` never write it |
@@ -107,7 +107,7 @@ read by no CPU stage).
107
107
  | `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`). A bone property is keyed by one track (`CONFIG_BONE_PROPERTY_KEYED_ONCE`): a chain keys `rotate` on every link, the blink's `eyes` group `scaley` and its `brows` group `translatey` on every member, so a single track on any of those is a second track on it — rigc would refuse it one stage later, `animation "idle" has two tracks on eye.scaley`. The proposed breath (issues #252, #270) is `chest` `translatey` travelling 2.5 % of the torso's length peak to peak — the distance from the `neck` bone to the `hip` bone as the proposal writes them, the run the chest sits half way along, whichever rule placed them — (amplitude half of it to two decimals, `base` the amplitude, so the chest rises from the setup pose and returns); the `breath:` note prints the fraction, the length, the two bones and the pixels. Until #270 it was 2.5 % of the `topwear` part's height, and a sweater to the thighs (`scarf`, 690 px) breathed 2.9 times as deep as `demo`; on neck → hip `scarf` reads 431 px (10.78 px) and `demo` 192 px (4.80 px). Neck and hip on one point: no length to scale by, so the breath keeps the reference's amplitude 1.3 (2.6 px peak to peak) and the note says so — scale it to the figure. `chest` `scalex` and the `head` and `neck` rolls are a ratio and angles and are proposed the same on every figure. A chain link is turned along its chain (the `bones` row), so a `translatex`/`translatey` key on a bone whose parent is a turned link, or a `scale`/`shear` key on a turned link, would move or stretch along the link and not along the picture's axis: the rig stage refuses it (`RIG_KEY_FRAME_UNTURNED`); `rotate` is keyed on any bone |
108
108
  | `motion.blink` | rig | **proposed**: `{t, eyes, brows, squash, brow_drop}`; the whole blink, `t` to `t + 0.364` s (`t + 0.314` s without brows: the brows' keys are the ones that end last), must fit inside the idle (`RIG_BLINK_INSIDE_IDLE`). The `eyes` group's `scaley` squashes every part on the eye bones about the bone's origin, the eyewhite's centre. Optional: a figure with no `eyewhite-r`/`-l` part has no eye bone, so `propose` writes no `blink` and notes `no blink: no eyewhite part (looked for: eyewhite-r, eyewhite-l), …`; with eyes and no `eyebrow-r`/`-l` part it writes the blink without `brows` and `brow_drop` and notes `blink without brows: …`. `brows` and `brow_drop` are stated together or not at all (`CONFIG_BLINK_BROWS_PAIRED`), and a group that names no bone is refused (`CONFIG_BLINK_GROUP_MEMBERS`) — rigc would refuse it one stage later, `group "eyes" declares no members` — as is a group that names one bone twice (`CONFIG_BLINK_GROUP_UNIQUE`; rigc: `group "eyes" names member "eye" twice`). An `irides-*` or `eyelash-*` part on a side with no eyewhite has no eye bone to ride: `propose` places it on `head` as a region, which the blink does not move, and notes `no eyewhite part for eye_r (looked for: eyewhite-r): iris (head:irides-r) is placed on head as a region, …`. The timing is fixed, not a field (table below) |
109
109
  | `motion.blink.still` | rig | **proposed** only for a lash `propose` notes (below), then checked: `{<part>: {row, bone}}` — the rows of that region part above `row` (rig px, y down) are drawn by a second slot `<part>_still` on `bone` and do not blink; rows from `row` down keep the part's slot and blink. The part must be a region on a bone `eyes` names, `bone` one it does not (`CONFIG_STILL_OFF_THE_BLINK`), and `row` a row of the part with no art across its whole width (`RIG_STILL_ROW_CLEAR`) |
110
- | `motion.animations_from` | rig; check (named; read for folds) | optional, never proposed (issue #183): a path, relative to the config file's directory, to a file in rig-c's motion-spec shape, `{"spec": "rigc-motion/1", "animations": {"<name>": {"duration": …, "tracks": [...]}, …}}` — an expression, a gesture, anything beside the idle. On every `rig` and `build` each of its animations is written into `rig/motion.json` after `idle`, as read: nothing added, re-keyed or judged here, and each is rigc's to accept or refuse at the rig stage's gate, in rigc's words. It may name what `motion.json` already holds — the bones `bones` declares, the easings `shut` and `open`, the groups `eyes` and `brows` when the blink writes them — and nothing else from the file reaches it. The loader refuses, every problem at once: no such file or one that cannot be read (`CONFIG_FILE_PRESENT`), text that is not JSON (`CONFIG_IS_JSON`), a file that is not an object, a `spec` other than `"rigc-motion/1"`, an `animations` that is not a table of objects (`CONFIG_FIELD_TYPE`), any other key — rig-c's `easings`, `groups`, `setup`, `physics`, `mix`, `archetype` and `cut` included, which would be read and not written (`CONFIG_KEY_KNOWN`), an animation named `idle` (`CONFIG_ANIMATION_NAME_FREE`), and a key written twice in one object of the file, which JSON would fold into its last value without a word (`CONFIG_KEY_UNIQUE`). `check` names the others on one line (`beside the idle: …`) and reads each for folds — `ANIMATIONS_DO_NOT_FOLD` (§7, issue #320); every other bar reads the idle. Absent, `motion.json` holds the idle alone, byte for byte as before, and `check` writes and prints what it did |
110
+ | `motion.animations_from` | rig; check (named; read for folds) | optional, never proposed (issue #183): a path, relative to the config file's directory, to a file in rig-c's motion-spec shape, `{"spec": "rigc-motion/1", "animations": {"<name>": {"duration": …, "tracks": [...]}, …}}` — an expression, a gesture, anything beside the idle. On every `rig` and `build` each of its animations is written into `rig/motion.json` after `idle`, as read: nothing added, re-keyed or judged here, and each is rigc's to accept or refuse at the rig stage's gate, in rigc's words. It may name what `motion.json` already holds — the bones `bones` declares, the easings `shut` and `open`, the groups `eyes` and `brows` when the blink writes them — and nothing else from the file reaches it. The loader refuses, every problem at once: no such file or one that cannot be read (`CONFIG_FILE_PRESENT`), text that is not JSON (`CONFIG_IS_JSON`), a file that is not an object, a `spec` other than `"rigc-motion/1"`, an `animations` that is not a table of objects (`CONFIG_FIELD_TYPE`), any other key — rig-c's `easings`, `groups`, `setup`, `physics`, `mix`, `archetype` and `cut` included, which would be read and not written (`CONFIG_KEY_KNOWN`), an animation named `idle` (`CONFIG_ANIMATION_NAME_FREE`), and a key written twice in one object of the file, which JSON would fold into its last value without a word (`CONFIG_KEY_UNIQUE`). `check` names the others on one line (`beside the idle: …`) and reads each for folds, with the idle — `ANIMATIONS_DO_NOT_FOLD` (§7, issues #320, #333); every other bar reads the idle. Absent, `motion.json` holds the idle alone, byte for byte as before, and `check` reads that idle for folds |
111
111
  | `constraints` | rig (handed to rig-c as `rig.json`'s `constraints`); `compare` and the coverage lines (roles) | **authored**, optional; `propose` writes none. A list of constraints in **rig-c's own rig-spec shape** (`RigConstraint`: `ik`, `transform`, `path`, `physics`, `slider` — rig-c's `src/rig.ts` documents every field), in the order rigc applies them. The loader checks only what this package owns: each entry is an object with a `type` rigc names (`CONFIG_CONSTRAINT_TYPE_KNOWN`) and a non-empty `name`, unique within its kind as rigc finds constraints (`CONFIG_CONSTRAINT_NAME_UNIQUE`; an ik and a transform may share one); every bone it names — `bones`, an ik's `target`, a transform's `source`, a physics or slider constraint's `bone` — is a bone `bones` declares, a chain link as `<chain><i>` (`CONFIG_NAME_RESOLVES`); the bone a constraint follows does not sit under a bone it drives (`CONFIG_CONSTRAINT_TARGET_DETACHED`). Every other field (`mix`, `softness`, `properties`, a path's `slot`, a slider's `animation`) and every value is rigc's to accept or refuse at the rig stage's gate, in rigc's words (`RIG_RIGC_GREEN`) — an ik's `bones` as a shape too: from rig-c 2.15.0 rigc refuses an ik over more than two bones, or over a pair whose second bone is not the first's child (issue #103; this loader refused both itself until then). A note and an `x-` record may ride on an entry; the rig stage leaves them out of what it hands rigc. Under the default `--idle-keys ctl` a two-bone ik whose child link the idle keys is split by that link's control, and rigc refuses the pair: run `rig --idle-keys direct` or `build --idle-keys direct` (the rig stage adds that sentence, naming the flag on the command that ran, to rigc's line; `build` forwards it to its rig stage, issue #95). Each bone a constraint follows is declared to rigc as `invariants.detached` from every bone it drives, which rigc's gate rule `A25` checks |
112
112
  | `turn` | rig (adds the turn control) | **proposed** only with `propose --full`/`--head` (issue #251), then corrected: `{pivot, depths}`. `depths` holds, per part, the See-through layer's `depth_median` the part came from (larger = further back), read from the run the part was assembled from; a `painting:` patch has none and is not carried. A rider (issue #271) — a part from an `eyewhite`, `irides`, `eyelash`, `eyebrow`, `nose` or `mouth` layer — is drafted at the `face` part's depth, 0.001 nearer when parts.json draws it after the face and 0.001 further when before: See-through makes no depth of its own for the eye group (it cuts them from one depth map of the combined eyes) and clamps nose and mouth to the face's median − 0.001, and the two runs' own medians for these layers disagreed by up to 0.47 and swapped their order against the face. Every other layer — ears, headwear and earwear included — keeps its own median. With no `face` part, or more than one, the riders keep their own and a note says so. One note lists each part's depth and where it came from; a carrier the other run places more than 10 % of `pivot − nearest` away gets a note naming both values and the run read — the two runs are not reconciled. `pivot` is the depth that stays put; `propose` drafts the mean of the parts' depths weighted by `opaque_px`, the one depth at which the painted pixels' centroid does not slide under the dial. The rig stage adds a bone `turn` under `root` at its origin that nothing keys — its local x is the dial, in rig px — and carries each part by `(pivot − depth) / (pivot − nearest depth)` of it: the nearest part all of it, a part behind the pivot the other way, a part at the pivot not at all. Each part rides leaf twins (`turn_<part>_<bone>`, an identity child of every bone its slot and weights bind) moved by one `transform` constraint (`turn_<part>`, after the config's own); the idle's keys, the `<bone>_ctl` controls and a physics constraint stay on the bones, and the dial moves none of them. At rest: with `turn` declared, no pixel differs by more than 1 level at dial 0 (not byte-identical; the cause is float noise in the identity child bones, and the constraints add nothing); without `turn`, byte-identical. Neither field has a default (`CONFIG_FIELD_PRESENT`); the pivot must lie behind the nearest part (`CONFIG_TURN_PIVOT_BEHIND_NEAREST`). Absent, no control is added and no byte of any output moves |
113
113
  | `scene` | assemble (the recomposite, its holes and the figure's mask); check, through `parts.json`'s `scene` block (`RECOMPOSITE_HOLES`, the seam) | **authored**, optional; `propose` writes none (issue #242). `{}` declares the painting a scene: the figure and its surroundings in one image, See-through's full run holding only the figure. With it, `assemble` reads the recomposite figures, the hole list and the `uncovered:` lines inside the figure — the rig pixels where any full-run layer's alpha is above 8 (`OPAQUE_ALPHA_ABOVE`, the cut every layer's painted pixels are read by) — each line starting `inside the figure (config.scene: …, <n> of <m> rig px)`; writes that figure as `figure_rig.png` (white on black) beside the recomposite, and a `scene` block in `parts.json`; flags error pixels on the error map inside it only (outside, the painting grey); and `check` leaves out of the seam every frame pixel whose composite sample reaches the painting's outermost row or column, saying how many (`seam_border` in `check.json`, and on the seam line). It takes no field: the figure and its threshold are derived. A key in it other than a record or an annotation is `CONFIG_KEY_KNOWN`. Absent, no byte of any output moves |
@@ -1172,7 +1172,7 @@ issue #320). [`examples/reference-sample`](https://github.com/rig-c/parts/tree/m
1172
1172
  ```
1173
1173
  [rig] animations beside the idle, from …/animations.json as written: hop
1174
1174
  [check] beside the idle: 1 animation(s), hop — built and gated by rigc with the rig; ANIMATIONS_DO_NOT_FOLD reads each for folds, every other bar here reads the idle
1175
- [check] ANIMATIONS_DO_NOT_FOLD: PASS — 1 animation(s) beside the idle read by rigc render --folds at 12 fps, its 2 physics constraint(s) left out (hop): 0 folding mesh(es), 0 not declared in may_fold (<= 0)
1175
+ [check] ANIMATIONS_DO_NOT_FOLD: PASS — 2 animation(s) read by rigc render --folds at 12 fps, its 2 physics constraint(s) left out (idle, hop): 0 folding mesh(es), 0 not declared in may_fold (<= 0)
1176
1176
  [check] check: PASS; 10 of 10 bar(s) measured, 0 skipped
1177
1177
  ```
1178
1178
 
@@ -1226,8 +1226,8 @@ way there, are in their READMEs, in the repository:
1226
1226
  **Limits.** Every slot holds one attachment, the part's own image, so an expression moves,
1227
1227
  turns and scales the face's parts and never swaps one for another: there is no second mouth
1228
1228
  or eye shape to key, and no lip-sync. `check` reads an animation beside the idle for folds
1229
- alone (`ANIMATIONS_DO_NOT_FOLD`, §7): beyond rigc's gate, no seam, stretch or hole bar has read
1230
- it. On the
1229
+ alone (`ANIMATIONS_DO_NOT_FOLD`, §7, which reads the idle too): beyond rigc's gate, no seam,
1230
+ stretch or hole bar has read it. On the
1231
1231
  examples each clip was read by `check`'s own `TEXTURE_STRETCH` reading applied to the clip's
1232
1232
  `rigc render --geometry` (their READMEs give the figures); that is a measurement, not a
1233
1233
  gate, and it is where an arm raised overhead was found to tear `demo`'s elbow (5.327
@@ -1519,7 +1519,16 @@ full loader before its assemble stage writes anything.
1519
1519
  is drafted. A bridge covers no art: the part's `candidate.bridges` in
1520
1520
  `mesh_draft.json` gives each join's span and area and the share of the
1521
1521
  candidate's overdraw they draw (`overdraw`), and the five readings below
1522
- read the bridged mesh as they read any candidate. Each candidate is
1522
+ read the bridged mesh as they read any candidate. Islands are read in a
1523
+ fixed order: dust first (`stray`, above); then **faint** islands — every
1524
+ pixel's alpha under 64 — are left out of the draft and the line names
1525
+ each (`LEFT OUT as faint`, its px, its brightest alpha, its box;
1526
+ `candidate.bridges.faint`): not meshed, so not drawn, and if those pixels
1527
+ matter to you, keep the lattice for that part; then every visible island
1528
+ is bridged, whatever its bridge costs — overdraw is reported, never a
1529
+ reason to drop art. On the public examples the faint islands a draft
1530
+ reaches peak at alpha 11–17 (demo `earring`'s 8, 2 and 1 px specks) and
1531
+ every visible one at 255. Each candidate is
1523
1532
  kept only where it holds against that part's
1524
1533
  lattice: every `check` bar that reads the part passes; within 1 rig px of
1525
1534
  the lattice over the idle; at the bend set (every chain link the part's
@@ -1566,7 +1575,7 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
1566
1575
  | `propose` | `note:` lines, `LINT` lines, the `coverage` lines, `landmarks.png`, `basis.json` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter; the `coverage:` summary says how many bones that verdict covers — a `not checked` bone was read by no check (its line says why), and a `ratio` or `fallback` bone in `basis.json` is a guess | a link off the art (a bone on the background) — move it onto the layer; `LINT <link> at … runs to … N degrees off the head's down axis …` — a fringe link that does not lean with the head (§3, the fringe in the head's frame): move it along the lock it drives; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head`; a `no face part: face box derived from …` note (always the first) — `head`, `neck` and every face-height scale come from a box guessed off the hair and neck (§3): check `head` and `neck` on `landmarks_head.png` and move them; when it adds `the blink shuts the eyes over no face part`, read `BLINK_NO_HOLE` after `check` — an eye with no face under it can open a hole (the `reference-sample` with only its face removed: 587 px). Under `--keypoints`: the `lint rule set:` line first (joints, or screen and why), then the joint notes — `missing: the rule "…" placed bone X instead` is a bone still guessed; `not used — …` names why an arm's joints did not place its sleeve (for a wrist on no part, the nearest part and the `--wrist-snap` that would bind it); a `wrist snap: …` note is a wrist bound past 15 px by the flag — check that chain's tip on `landmarks.png`; a `one handwear part holds both wrists` note is two chains on one mesh — cut the part with `assemble.cuts` to separate them; `LINT chest at … is not between the neck joint …`, `LINT hip at … is not past chest …`, `LINT hip at … is nearer the neck joint …` and `LINT <chain><i> at … does not advance from <s>_shoulder toward <s>_wrist` are the joint-frame lines: move that bone between, past or along the joints they name |
1567
1576
  | `compare` | one line per pair (origin, parent, tip, length, direction — or SKIP and why), then the per-row summary, the unmapped bones of each side, `required:`, each side's roles and the `controls:` line | proposal against the config you corrected: the rows you changed and no others. Config against the `rig.json` `build` writes for it: measured on both examples, every origin within 6.19e-7 px (the rig's offsets are written to 6 places) and every chain link's tip and length within 4.99e-4 px (`length` is written to 3), printed to three places as `0.000` (a signed figure as `+0.000` or `-0.000`); `parent … is the right's ancestor at depth 2, <bone>_ctl between` for every keyed mesh bone, no `DIFFERENT` and no `NOT MAPPED` | `parent DIFFERENT: left a, right b` — a bone hangs from another parent than the other side's; `frames not related` — a rig with no stage (`skeleton.width`/`height`), two configs at different `rig_scale`: the distance rows SKIP, so declare the frame in a `--map` file if the author knows it; `FAIL STRUCTURE_REQUIRED_PRESENT` — a bone the map requires is missing through its pairs |
1568
1577
  | `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`); in `rig.json`, each chain link's (or its `_ctl`'s) `length` and `rotation` | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages); every link's `length` the distance to the next link and `rotation` its direction (Spine degrees, counter-clockwise, y up, local to the parent), every offset under it — a child bone's `x, y`, a weight's bind `x, y`, a region's `x, y` — in that turned frame, and a region on a link carrying `rotation` that turns it back upright. Nothing moved: `flattenRig` (`src/rig.ts`) turns every offset back and gives the unturned numbers | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
1569
- | `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:` (and `loop physics:` when the rig declares a physics constraint, §7), `seam:`, the six judgement lines, then — when `motion.json` holds an animation beside the idle — `ANIMATIONS_DO_NOT_FOLD` and one `folds` line per folding mesh, rigc's own (issue #320), `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the bars measured — nine, ten with the fold bar — and names the ones that said SKIP | `check: PASS; 10 of 10 bar(s) measured, 0 skipped` on `demo`, `scarf` and `reference-sample`, which carry animations beside the idle, and `9 of 9` on `reference-demo`, which does not; a judgement line SKIP only where the character lacks what it reads; on a rig spec with no `parts.json` or no `idle` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), PASS with the skipped bars named | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6); `CHECK_ANIMATIONS_DO_NOT_FOLD` — a clip turns a mesh over (§6) |
1578
+ | `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:` (and `loop physics:` when the rig declares a physics constraint, §7), `seam:`, the six judgement lines, then — when `motion.json` holds an animation, the idle included — `ANIMATIONS_DO_NOT_FOLD` and one `folds` line per folding mesh, rigc's own (issues #320, #333), `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the bars measured — nine, ten with the fold bar — and names the ones that said SKIP | `check: PASS; 10 of 10 bar(s) measured, 0 skipped` on all four examples — `demo`, `scarf` and `reference-sample`, which carry animations beside the idle, and `reference-demo`, whose idle alone the fold bar reads (issue #333); a judgement line SKIP only where the character lacks what it reads; on a rig spec with no `parts.json` or no `idle` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), PASS with the skipped bars named | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6); `CHECK_ANIMATIONS_DO_NOT_FOLD` — a clip turns a mesh over (§6) |
1570
1579
  | `check --requirements` (and `build --requirements`) | after the lines above, one line per declared requirement, `NAME: PASS`, `FAIL` or `NOT MEASURABLE — …` with its figures, the bar as declared and the worst frame; then `requirements: N declared — M measured (P PASS, F FAIL), K NOT MEASURABLE; not declared: <kinds>`; `check.json`'s `requirements` block; the renders under `requirements/` (§7, *Declared requirements*) | every line PASS and `0 NOT MEASURABLE`; a follow's `released_copy` says whether rigc's consumer-driven door was taken on the throwaway copy; a `stretch` line's `held_by` (issue #321), right after `worst`, names the bones the worst edge's two vertices hang on and their idle chains, in `CHECK_TEXTURE_STRETCH`'s words (§6) and by the same function, on a PASS as on a FAIL | `CHECK_REQUIREMENT_MET` (a figure past its bar; the line names the frame) or `CHECK_REQUIREMENT_MEASURABLE` (the quantity is undefined in the frames: a tip or an axis on a bone of length 0, an aim with no line, a follow no frame of which reaches its least drive); the last line then reads `check: FAIL — B bar(s) not met, R declared requirement(s) not PASS` |
1571
1580
  | `compose` | the `[compose]` lines: one per character (its offset, its scale, its placed bounds, the shift and how many first-level bones, regions on `root` and weights bound to `root` it moved, its bone, slot, track and constraint counts, its `rig_scale` and idle), the declared order and the slot count it expands to, rigc's gate lines; then the `[check]` lines as for `check`, and `scene.json` | both gate summaries `0 failed`, `check: PASS` with the bars that measured and the ones that said SKIP named — on a composed rig `4 of 9`: the gate, the loop, `CHAIN_LAG` and `TEXTURE_STRETCH` (there is no `parts.json`; each character's own `build` measured all nine). `scene.json`'s `order.slots` is the order the compiled skeleton draws | a `SCENE_*` refusal (§6, *compose*) — the scene file, a build, or the order; `COMPOSE_RIGC_GREEN` — rigc's own line, quoted |
1572
1581
  | `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line, or, when the end frame is not frame 0, the jump line: its max \|d\| and px over 40, then why — under `build`, `the rig holds N physics constraint(s), and rigc's render starts every one at rest at frame 0` with the remedy `build --loop --lead-in <n>`, or `the rig holds no physics constraint, so the idle itself does not close` (`CHECK_LOOP_CLOSES`); under `loop --frames`, which reads frames alone, both causes (issue #325); under `build --lead-in`, the lead-in line in its place (the frames kept and the jump at the wrap); each file's line, then `loop: idle.png N B (lossless); idle-indexed.png N B (max …, mean …); idle.gif N B (max …, mean …)` | `f0048.png equals f0000.png byte for byte, so it is dropped` | `LOOP_ENCODE` (§6) |
@@ -2156,7 +2165,7 @@ exits 0, and `mesh_draft.json` and the console name the reading and the amount.
2156
2165
  | `CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's art in the lower half of its box travels less than 1.4725 times as far as in the upper half, measured from the idle's posed geometry (rig px quoted) | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them), or its mode: on `scarf`'s short skirt the proposal's lattice read 1.207 and the same part in `contour` mode 1.738 with the same segments and idle (issue #118) — both carried the `chest` segment #262 removed, after which the lattice reads 3.487. When the detail says `the root travels with the breath (x px of y px): it is carried by \`<bone>\`` (issue #262) the chain is not the cause: a bone an idle `translate` track moves (the breath) moves the root more than the tip, and with that translate held still (the part posed again without it, in proportion to each vertex's weight on the bone and those under it) the ratio passes — the row adds `carried_by`, `breath_rig_px`, `root_without_breath_rig_px` and `tip_without_breath_rig_px`; take that bone's segment off the mesh, or bind the root to the bone the part hangs from. A part the scene file declares `rests` (issue #245) is held the other way, its tip half's travel at most its root half's (ratio ≤ 1); the detail then reads `the scene file's rests declares its tip at rest, so a ratio <= 1 is required`, and for it the fix is the chain's `amps` (shrinking toward the tip) or the mesh's tip weighted to a bone the idle does not swing |
2157
2166
  | `CHECK_STILL_REGIONS_DARK` | a slot of the face's still set (the `face` slots and every slot drawn over the face that the head bone alone carries) moves relative to the bone the face's slot rides, or a `footwear` slot moves on the screen, by more than the arithmetic's own rounding — measured from the idle's posed geometry (the slot, its largest displacement in rig px, the idle frame and the tolerance quoted) | on the face: a face mesh with a weight on a bone other than the head (`segments`), or one deformed by another bone; on the feet: a mesh weighted to a swinging bone, or a region on the wrong bone. A bang swinging over the face is not this line's failure: its depth is reported (`crossing`), with no bar |
2158
2167
  | `CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named, and — issue #314 — the bones the edge's two vertices hang on: each vertex's two largest weights from `rig.json`, `vertex <i> is weighted to "<bone>" <w>, …`, then `on the idle's chains, the chain from "<first link>" carries "<bone>", …` — each chain `CHAIN_LAG` reads named once, by its first link — with a bone on no chain said, or `none of these bones hangs on a chain of the idle`); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other. Two chains named, with opposed amplitudes: give both one sign, or drop one — `propose --from-config` measures the pair before a build (§3, *Two chains on one lattice*) |
2159
- | `CHECK_ANIMATIONS_DO_NOT_FOLD` | an animation beside the idle turns a mesh triangle over against the setup pose on some frame, read by `rigc render --folds` over the build with its physics constraints left out (issue #320); the object names the animation and the mesh, the detail quotes rigc's line — the frames, the worst frame, which triangles, their world box and setup-pose area. Or a `may_fold` entry of the `--requirements` file names a slot and animation that does not fold: an exception that excepts nothing | the clip's keys on the bones that mesh is weighted to (a bend past what the weights carry, a translate that carries one row of vertices past the next), or the mesh's `segments`; to look, `rigc inspect --candidate <out>/check/build --show art,folds,mesh --animation <name> --at <the worst frame's time> --zoom folds`. A fold the art means (a sleeve behind the body) is declared, `"may_fold": [{ "slot": …, "animation": …, "why": … }]` (§7, *Declared requirements*); a stale entry is taken out |
2168
+ | `CHECK_ANIMATIONS_DO_NOT_FOLD` | an animation, the idle included, turns a mesh triangle over against the setup pose on some frame, read by `rigc render --folds` over the build with its physics constraints left out (issues #320, #333); the object names the animation and the mesh, the detail quotes rigc's line — the frames, the worst frame, which triangles, their world box and setup-pose area. Or a `may_fold` entry of the `--requirements` file names a slot and animation that does not fold: an exception that excepts nothing | the clip's keys on the bones that mesh is weighted to (a bend past what the weights carry, a translate that carries one row of vertices past the next), or the mesh's `segments`; to look, `rigc inspect --candidate <out>/check/build --show art,folds,mesh --animation <name> --at <the worst frame's time> --zoom folds`. A fold the art means (a sleeve behind the body) is declared, `"may_fold": [{ "slot": …, "animation": …, "why": … }]` (§7, *Declared requirements*); a stale entry is taken out |
2160
2169
  | `CHECK_FOLDS_READ` | the fold render's `frames.json` names no set for an animation, a `folds.json` is missing, not `rigc-folds/1`, another animation's or another rate's, or a folding mesh has no line rigc printed for it | a rigc problem; report it, with `rigc --version` |
2161
2170
  | `CHECK_SEAM_FRAME_SIZE`, `FRAMES_SIDECAR`, `CHECK_GEOMETRY_FILE` | rigc's render is not what its `frames.json` says, or its `geometry.json` is not a whole `rigc-geometry/1` export of the frames beside it (a frame count, a mesh whose vertex count differs from its rest entry; for the face half, another viewport than `frames.json`'s, other frame indices, the head bone or a feature's bone missing from a frame, a non-finite transform or time) | a rigc problem; report it |
2162
2171
  | `LOOP_ENCODE` | the loop encoder refused a frame (translucent pixel in a GIF, a size change) | the frames; for a translucent frame write the lossless or the indexed APNG, which keep alpha |
@@ -2174,7 +2183,7 @@ exits 0, and `mesh_draft.json` and the console name the reading and the amount.
2174
2183
  | `CHECK_REQUIREMENTS_FRAMES` | a requirement render did not write every frame rigc sampled, or the as-declared, released and full renders of a follow sampled different frames | a rigc problem; report it |
2175
2184
  | `CHECK_REQUIREMENT_MET` | a declared requirement measured outside its bar; the figure, its frame and the bar are quoted | the rig or its motion (§7, *Declared requirements*) — or the bar, if the scene asks less |
2176
2185
  | `CHECK_REQUIREMENT_MEASURABLE` | a declared requirement is NOT MEASURABLE: what it asks is undefined in the frames, and the reason is quoted. Not a pass | the declaration (a bone with a length, a target off the bone's origin, a least drive the constraint reaches) or the rig |
2177
- | `REQUIREMENTS_MAY_FOLD` | a `may_fold` entry names the idle, which the fold bar does not read, or a slot with no mesh attachment — a region is one quad and cannot fold (issue #320). Refused before anything is built | a mesh slot and an animation beside the idle |
2186
+ | `REQUIREMENTS_MAY_FOLD` | a `may_fold` entry names a slot with no mesh attachment — a region is one quad and cannot fold (issue #320). Refused before anything is built. Before issue #333 an entry naming the idle was refused here too; the bar reads the idle now, and `idle` is an animation name like any other | a mesh slot |
2178
2187
  | `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 |
2179
2188
  | `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 |
2180
2189
  | `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` |
@@ -2446,11 +2455,13 @@ The selftest's planted cases (by hand):
2446
2455
  | a foot whose root moves `fround(0.05)` rig px | PASS, heat 0.779–0.785 | FAIL, 0.05 rig px |
2447
2456
  | a face mesh weighted to a bone sliding `fround(0.05)` under a rolling head | PASS, 0.851–0.855 against 0.936–0.948 | FAIL, 0.05 rig px |
2448
2457
 
2449
- **`ANIMATIONS_DO_NOT_FOLD`: the animations beside the idle (issue #320).** The one bar that
2450
- reads an animation other than the idle, written — as a key of `check.json` after
2451
- `TEXTURE_STRETCH`, and a console line after the judgement lines — only when `motion.json`
2452
- holds one (`motion.animations_from`, §3); a rig of the idle alone writes and prints what it
2453
- did, and the summary counts nine bars. It is rig-c's reading, not this package's: one
2458
+ **`ANIMATIONS_DO_NOT_FOLD`: every animation, the idle included (issues #320, #333).** The one
2459
+ bar that reads an animation other than the idle, and it reads the idle with them, written — as
2460
+ a key of `check.json` after `TEXTURE_STRETCH`, and a console line after the judgement lines —
2461
+ whenever `motion.json` holds an animation: a rig of the idle alone writes it too, and the
2462
+ summary counts ten bars. `TEXTURE_STRETCH` reads how far the idle's triangles stretch; a
2463
+ triangle that turns inside out is this bar's defect, on the idle as on any clip (issue #333;
2464
+ until then the idle was read for folds by hand, beside the build). It is rig-c's reading, not this package's: one
2454
2465
  `rigc render --folds` over every animation of the build, at 12 fps (the idle's rate, under
2455
2466
  `--requirements` too: the file's `fps` is its requirements', and a file moves only what it
2456
2467
  declares), with frames drawn at rigc's smallest
@@ -2465,7 +2476,7 @@ exemption that exempts nothing.
2465
2476
  | field | what |
2466
2477
  | --- | --- |
2467
2478
  | `status` | `PASS` or `FAIL` |
2468
- | `animations` | every animation beside the idle, in `motion.json`'s order: each one read |
2479
+ | `animations` | every animation, the idle included, in `motion.json`'s order: each one read |
2469
2480
  | `fps` | the rate the reading sampled at |
2470
2481
  | `physics_left_out` | how many physics constraints the read build left out (0 without any) |
2471
2482
  | `folding` | one row per mesh that folds in an animation: `animation`, `slot`, `attachment`, `frames` (how many show a folded triangle), `worst_frame`, `worst_triangles`, `triangles` (the mesh's), `declared` (the `may_fold` why, or null) and `line`, rigc's printed line verbatim |
@@ -2483,8 +2494,10 @@ A bar on the reading with physics would have refused a public example for a fram
2483
2494
  physics at is the scene's. To read a clip with its physics at a rate you choose:
2484
2495
  `rigc render --candidate <out>/check/build --animation <name> --fps <n> --folds`.
2485
2496
 
2486
- On the examples: `demo` (7 animations), `scarf` (7) and `reference-sample` (1) read no
2487
- folding mesh; `reference-demo` has no animation beside the idle and writes no key. What the
2497
+ On the examples: `demo` (the idle and 7 animations), `scarf` (the idle and 7),
2498
+ `reference-sample` (the idle and 1) and `reference-demo` (the idle alone) read no folding mesh,
2499
+ and neither does the trailer's idle (`rig-c/trailer`, its parts built by this tree, and its film
2500
+ rig's `idle` and `show`) [measured for issue #333, rig-c 2.47.1, physics left out, 12 fps]. What the
2488
2501
  reading adds to `check`, measured with `/usr/bin/time` on `demo`'s keyed build (median of
2489
2502
  three, user + sys, a machine under load, so the wall figures are not quoted): one run over the
2490
2503
  idle and its 7 animations 4.1 s, the idle alone 2.9 s, and the same rig with its 7 animations
@@ -2625,16 +2638,16 @@ it reads is there, and says SKIP with the reason when it is not:
2625
2638
  | seam | `parts.json` and `parts/` | no `parts.json` (`seam_*` null, `skipped.seam` the reason) |
2626
2639
  | `BREATH_VISIBLE`, `BLINK_NO_HOLE`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK` | `parts.json`'s tags and the idle | either missing, or as in the table above |
2627
2640
  | `CHAIN_LAG`, `TEXTURE_STRETCH` | the idle (and `rig.json`) | no `idle`, or as in the table above |
2628
- | `ANIMATIONS_DO_NOT_FOLD` | the animations beside the idle, through `rigc render --folds` | never SKIP: written when `motion.json` holds an animation beside the idle, absent otherwise (with or without an `idle` and a `parts.json`) |
2641
+ | `ANIMATIONS_DO_NOT_FOLD` | every animation, the idle included, through `rigc render --folds` | never SKIP: written when `motion.json` holds an animation, the idle alone included (issue #333), absent when it holds none (with or without a `parts.json`) |
2629
2642
  | `RECOMPOSITE_HOLES` | `parts.json`'s `recomposite` block | no `parts.json`, or no block |
2630
2643
  | `SETUP_POSE_VS_SOURCE` | the rig spec and the painting | no `--source` |
2631
2644
 
2632
2645
  `PASS` reads every bar that measured, and the last console line says how many did:
2633
- `check: PASS; 1 of 9 bar(s) measured, 8 skipped (loop, seam, …)` is a rig whose gate is
2634
- green and nothing else was looked at. Without an idle no idle is rendered
2646
+ `check: PASS; 1 of 9 bar(s) measured, 8 skipped (loop, seam, …)` is a rig with no animation at
2647
+ all whose gate is green and nothing else was looked at. Without an idle no idle is rendered
2635
2648
  (`idle_frames/`, `contact.png` and `motion_heat.png` are not written). Reference-demo's own
2636
- rig directory, copied away from its `parts.json`, reads `4 of 9`: the gate, the loop,
2637
- `CHAIN_LAG` and `TEXTURE_STRETCH`.
2649
+ rig directory, copied away from its `parts.json`, reads `5 of 10`: the gate, the loop,
2650
+ `CHAIN_LAG`, `TEXTURE_STRETCH` and, since issue #333, `ANIMATIONS_DO_NOT_FOLD` over its idle.
2638
2651
 
2639
2652
  ### Composing several characters (`compose`, issue #74)
2640
2653
 
@@ -2831,8 +2844,8 @@ declaration says how far the tip moves and not when. That line's first figures a
2831
2844
  `declared_rests` and `resting_chains`. Each name must be a swinging part in
2832
2845
  `parts.json` and not one `not_swinging` names; a part may both rise and rest.
2833
2846
 
2834
- **Folds the art means** (issue #320). `ANIMATIONS_DO_NOT_FOLD` holds every animation beside
2835
- the idle to no mesh triangle turned over against the setup pose (§7). When a clip folds a
2847
+ **Folds the art means** (issue #320). `ANIMATIONS_DO_NOT_FOLD` holds every animation, the
2848
+ idle included (issue #333), to no mesh triangle turned over against the setup pose (§7). When a clip folds a
2836
2849
  mesh on purpose — a sleeve that tucks behind the body as the arm crosses it — the scene says
2837
2850
  so, by slot and animation, with its reason:
2838
2851
  `"may_fold": [{ "slot": "handwear_l", "animation": "dance", "why": "the cuff tucks behind the chest on the cross step" }]`.
@@ -2840,7 +2853,8 @@ The fold's row in the line carries `declared`, the why, the console's `folds` li
2840
2853
  `— declared in may_fold: <why>`, and it is not held to the bar. Each entry is three names, no
2841
2854
  default — a `why` left blank is refused, as is an empty list, a field the entry does not read
2842
2855
  and a pair declared twice (`REQUIREMENTS_FIELD`); the slot must hold a mesh and the
2843
- animation must be one beside the idle (`REQUIREMENTS_RESOLVES`, `REQUIREMENTS_MAY_FOLD`);
2856
+ animation must be one `motion.json` declares — `idle` is one like any other
2857
+ (`REQUIREMENTS_RESOLVES`, `REQUIREMENTS_MAY_FOLD`);
2844
2858
  and an entry whose slot does not fold in its animation fails the line
2845
2859
  (`CHECK_ANIMATIONS_DO_NOT_FOLD`, `declared_not_folding`) — an exception that excepts nothing
2846
2860
  is taken out of the file, as rig-c refuses one for `invariants.deformMayFold`.
package/docs/MIGRATION.md CHANGED
@@ -148,3 +148,65 @@ nothing else.
148
148
  What to do: install with `npm i -D @rig-c/parts` (or `npm install -g @rig-c/parts`);
149
149
  command lines do not change. A dependant that names `rig-parts` keeps receiving
150
150
  every release under it (#222).
151
+
152
+ ## After 1.14.0: the idle is read for folds
153
+
154
+ | | 1.14.0 | after 1.14.0 |
155
+ | --- | --- | --- |
156
+ | `check.json`'s `ANIMATIONS_DO_NOT_FOLD` | written only when `motion.json` holds an animation beside the idle; its `animations` lists those | written whenever `motion.json` holds an animation, a rig of the idle alone included; its `animations` lists every one read, the idle first (#333) |
157
+ | the console's fold line | `N animation(s) beside the idle read by rigc render --folds …` | `N animation(s) read by rigc render --folds …`, the idle counted and named |
158
+ | the summary of a rig with an idle and no other animation | `… of 9 bar(s) measured …` | `… of 10 bar(s) measured …` |
159
+ | `may_fold` naming `"animation": "idle"` | refused, `REQUIREMENTS_MAY_FOLD` (the bar did not read the idle) | accepted like any animation name; a fold it names is listed with its why and not held, and one that does not fold fails the bar |
160
+
161
+ The reading is the one #320 made for the other animations — one `rigc render --folds` at
162
+ 12 fps over the build with its physics constraints left out — and now takes the idle in
163
+ the same run. A rig whose idle turns a mesh triangle over against the setup pose, which
164
+ `check` passed before, fails it, naming the idle and quoting rigc's line. On the four
165
+ public examples and the trailer no idle folds [measured for #333, rig-c 2.47.1], so no
166
+ verdict moved; every example's `expected/check.json` moved by the key and nothing else.
167
+ `reference-demo`, an idle alone, gains the whole key after `TEXTURE_STRETCH`:
168
+
169
+ ```diff
170
+ @@ -259,6 +259,18 @@
171
+ "severity": 1.074
172
+ }
173
+ ]
174
+ + },
175
+ + "ANIMATIONS_DO_NOT_FOLD": {
176
+ + "status": "PASS",
177
+ + "animations": [
178
+ + "idle"
179
+ + ],
180
+ + "fps": 12,
181
+ + "physics_left_out": 0,
182
+ + "folding": [],
183
+ + "folding_undeclared": 0,
184
+ + "declared_not_folding": [],
185
+ + "bar": 0
186
+ },
187
+ "RECOMPOSITE_HOLES": {
188
+ ```
189
+
190
+ and `demo`, `scarf` and `reference-sample` gain `"idle"` as the first entry of its
191
+ `animations`.
192
+
193
+ What to do: nothing for a rig whose idle does not fold. A consumer that reads
194
+ `check.json` finds the key on every rig with an animation. An idle that folds on purpose
195
+ is declared, `"may_fold": [{ "slot": …, "animation": "idle", "why": … }]`, in the
196
+ `--requirements` file.
197
+
198
+ ## After 1.14.0: faint islands under `contour.bridges`
199
+
200
+ `contour.bridges` shipped in 1.14.0 meshing every island past `stray` and bridging it.
201
+ The patch after it reads islands in a fixed order (issue #326, HQ's ruling): dust first
202
+ (`stray`), then **faint** islands — every pixel's alpha under 64 — left out and named,
203
+ then every visible island bridged whatever its bridge costs. No field was added and
204
+ no declared form changed.
205
+
206
+ | change | what moved | measured |
207
+ | --- | --- | --- |
208
+ | #326, faint islands left out | only a part that declares `contour.bridges` and holds a faint island (not the largest): its mesh, its `mesh_report.json` row (`contour.bridges.faint`, and `art_coverage`, which counts those pixels as art not covered) and, from `mesh-draft`, its written entry's note and `mesh_draft.json`'s `candidate.bridges.faint`. A part without a faint island writes the bytes 1.14.0 wrote | demo `earring` (the only such part a `mesh-draft` of the public examples drafts): its 8, 2 and 1 px specks, brightest alpha 17, 17 and 11, are left out — 33 → 26 triangles, overdraw 4.82 → 1.94; the idle rendered at scale 1.01 differs from the build of #335 (the change 1.14.0 carries) by at most 9 of 255 levels on at most 38 px where the specks were (4 frames; elsewhere 158 px differ by 1 level, as they do with the earring slot hidden), and `check`'s seam bar reads 0 px over 40 on both |
209
+
210
+ What to do: nothing for a config without `contour.bridges`. A part that declares it and
211
+ holds a faint island no longer draws that island; if those pixels matter, keep the
212
+ lattice for that part.
package/docs/STABILITY.md CHANGED
@@ -171,11 +171,11 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
171
171
  | --- | --- | --- |
172
172
  | `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), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px`, `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, 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) |
173
173
  | `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 |
174
- | `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins` — only when it declares `contour.bridges`); 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) |
175
- | `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `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 beside the idle (issue #320: `status`, `animations`, `fps`, `physics_left_out`, `folding`, `folding_undeclared`, `declared_not_folding`, `bar`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
174
+ | `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`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` "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) |
175
+ | `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `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 (issue #320: `status`, `animations`, `fps`, `physics_left_out`, `folding`, `folding_undeclared`, `declared_not_folding`, `bar`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
176
176
  | `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `turn` with `--full`/`--head`, and `notes` |
177
177
  | `basis.json` | `propose` | spec `spine-parts-basis/1` |
178
- | `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw` — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
178
+ | `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw`, and `faint` only when an island was left out as faint — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
179
179
  | the scene report | `compose` | spec `spine-parts-scene-report/1` |
180
180
  | `idle.png`, `idle-indexed.png`, `idle.gif` | `loop`, `build --loop` | APNG and GIF encodings of the rendered idle |
181
181
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/parts",
3
- "version": "1.14.0",
3
+ "version": "1.15.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/build.ts CHANGED
@@ -70,7 +70,7 @@ import {
70
70
  splitSchedule,
71
71
  untestedIntervals,
72
72
  } from './autoreplay.ts';
73
- import { besideIdleLine, FOLD_LINE, type FoldsLine, buildGateLines, causeLines, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, rigcFailed, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
73
+ import { besideIdleLine, FOLD_LINE, type FoldsLine, buildGateLines, causeLines, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
74
74
  import { type Cut, loadConfig, loadConfigAndAnimations, loadEarlyConfig } from './config.ts';
75
75
  import { PartsError, type Problem, problemLine, refuseIfAny } from './errors.ts';
76
76
  import type { MeshQualityReport, MotionSchedule } from 'rig-c/mesh';
@@ -277,6 +277,8 @@ export interface GateRun {
277
277
  * {@link causeLines} says name why.
278
278
  */
279
279
  lines: string[];
280
+ /** Which rigc ran it, when the runner said ({@link RigcCall.ran}; issue #337). */
281
+ ran?: string;
280
282
  }
281
283
 
282
284
  /**
@@ -295,7 +297,7 @@ function gateRun(label: string, rigc: RigcRunner, args: string[]): GateRun {
295
297
  const r = rigc(args);
296
298
  const lines = r.out.split('\n').filter((l) => /^ {2}FAIL {2}/.test(l) || /assertions: \d+ measured/.test(l) || /^rigc compile error/.test(l) || DECLARED_SKIP.test(l) || HERE_LINE.test(l));
297
299
  const failed = lines.some((l) => /^ {2}FAIL {2}/.test(l) || /^rigc compile error/.test(l));
298
- return { label, status: r.status, lines: r.status !== 0 && !failed ? [...lines, ...causeLines(r.out)] : lines };
300
+ return { label, status: r.status, lines: r.status !== 0 && !failed ? [...lines, ...causeLines(r.out)] : lines, ...(r.ran === undefined ? {} : { ran: r.ran }) };
299
301
  }
300
302
 
301
303
  /**
@@ -490,7 +492,7 @@ export function motionGates(
490
492
  {
491
493
  code: 'AUTO_MESH_MOTION_INPUT',
492
494
  object: c.object,
493
- detail: `the candidate with the declared stimulus (the rig as written, its motion carrying the stimulus animations) ${candidate.status !== 0 ? `is refused by rigc's gate, exit ${candidate.status}${candidate.lines.length > 0 ? `: ${candidate.lines.map((l) => l.trim()).join(' | ')}` : ''}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a candidate that is not gated is not evidence, and nothing is built in the part's place`,
495
+ detail: `the candidate with the declared stimulus (the rig as written, its motion carrying the stimulus animations) ${candidate.status !== 0 ? `is refused by rigc's gate, exit ${candidate.status}${candidate.lines.length > 0 ? `: ${candidate.lines.map((l) => l.trim()).join(' | ')}` : ''}${rigcRanClause(candidate)}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a candidate that is not gated is not evidence, and nothing is built in the part's place`,
494
496
  },
495
497
  ],
496
498
  notCompared: 'the stimulus candidate build is red',
@@ -520,7 +522,7 @@ export function motionGates(
520
522
  {
521
523
  code: 'AUTO_MESH_MOTION_INPUT',
522
524
  object: c.object,
523
- detail: `the reference (the rig with "${c.part}" as its unreduced source mesh) ${gate.status !== 0 ? `is refused by rigc's gate, exit ${gate.status}${gate.lines.length > 0 ? `: ${gate.lines.map((l) => l.trim()).join(' | ')}` : ''}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a reference that is not gated is not evidence, and nothing is built in the part's place`,
525
+ detail: `the reference (the rig with "${c.part}" as its unreduced source mesh) ${gate.status !== 0 ? `is refused by rigc's gate, exit ${gate.status}${gate.lines.length > 0 ? `: ${gate.lines.map((l) => l.trim()).join(' | ')}` : ''}${rigcRanClause(gate)}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a reference that is not gated is not evidence, and nothing is built in the part's place`,
524
526
  },
525
527
  ],
526
528
  notCompared: 'the reference build is red',
@@ -642,7 +644,7 @@ export function replaySearch(
642
644
  fatal = {
643
645
  code: 'AUTO_MESH_MOTION_INPUT',
644
646
  object: c.object,
645
- detail: `the replay to accepted step ${step} (the rig with "${c.part}" as that step's mesh) ${gate.status !== 0 ? `is refused by rigc's gate, exit ${gate.status}${gate.lines.length > 0 ? `: ${gate.lines.map((l) => l.trim()).join(' | ')}` : ''}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a candidate that is not gated is not evidence, and nothing is built in the part's place`,
647
+ detail: `the replay to accepted step ${step} (the rig with "${c.part}" as that step's mesh) ${gate.status !== 0 ? `is refused by rigc's gate, exit ${gate.status}${gate.lines.length > 0 ? `: ${gate.lines.map((l) => l.trim()).join(' | ')}` : ''}${rigcRanClause(gate)}` : `built green and wrote no ${RIGC_MODEL_DOCUMENT}`}; a candidate that is not gated is not evidence, and nothing is built in the part's place`,
646
648
  };
647
649
  return refused(`rigc exit ${gate.status}`);
648
650
  }
@@ -897,7 +899,7 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
897
899
  const problems: Problem[] = red.map((g) => ({
898
900
  code: 'RIG_RIGC_GREEN',
899
901
  object: `rigc ${g.label}`,
900
- detail: `exited ${g.status}${g.lines.length > 0 ? `: ${g.lines.map((l) => l.trim()).join(' | ')}` : ', and it printed nothing'}; ${ctlRemedies(g.lines, rig.controls, input.command ?? 'rig').map((s) => `${s}; `).join('')}exit 0 is required before anything is written, and nothing was`,
902
+ detail: `exited ${g.status}${g.lines.length > 0 ? `: ${g.lines.map((l) => l.trim()).join(' | ')}` : ', and it printed nothing'}${rigcRanClause(g)}; ${ctlRemedies(g.lines, rig.controls, input.command ?? 'rig').map((s) => `${s}; `).join('')}exit 0 is required before anything is written, and nothing was`,
901
903
  }));
902
904
  throw new PartsError(problems);
903
905
  }
@@ -1031,14 +1033,14 @@ export function judgementLine(name: string, line: JudgementLine | ReportedLine):
1031
1033
  }
1032
1034
 
1033
1035
  /**
1034
- * The fold line (issue #320) as the console prints it: the status, what was
1036
+ * The fold line (issues #320, #333) as the console prints it: the status, what was
1035
1037
  * read and at what rate, and then each folding mesh on a line of its own,
1036
1038
  * rigc's own words, with the declaration's why where `may_fold` takes it.
1037
1039
  */
1038
1040
  export function foldsLines(line: FoldsLine): string[] {
1039
1041
  const physics = line.physics_left_out === 0 ? '' : `, its ${line.physics_left_out} physics constraint(s) left out`;
1040
1042
  const head =
1041
- `${FOLD_LINE}: ${line.status} — ${line.animations.length} animation(s) beside the idle read by rigc render --folds at ${line.fps} fps${physics} (${line.animations.join(', ')}): ` +
1043
+ `${FOLD_LINE}: ${line.status} — ${line.animations.length} animation(s) read by rigc render --folds at ${line.fps} fps${physics} (${line.animations.join(', ')}): ` +
1042
1044
  `${line.folding.length} folding mesh(es), ${line.folding_undeclared} not declared in may_fold (<= ${line.bar})` +
1043
1045
  (line.declared_not_folding.length === 0 ? '' : `; may_fold declares ${line.declared_not_folding.map((d) => `slot "${d.slot}" in "${d.animation}"`).join(', ')}, which do(es) not fold`);
1044
1046
  return [head, ...line.folding.map((r) => ` folds ${r.line}${r.declared === null ? '' : ` — declared in may_fold: ${r.declared}`}`)];
@@ -1060,7 +1062,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
1060
1062
  log(`rig-parts check: ${input.rig} -> ${input.out}`);
1061
1063
  const versionLines = v.out.trim().split('\n');
1062
1064
  const entryLine = versionLines.find((l) => l.startsWith('entry:'));
1063
- log(` rigc ${versionLines[0]} at ${bin}${entryLine === undefined ? '' : `; ${entryLine}`}`);
1065
+ log(` ${rigcIdentity(bin, v)}${entryLine === undefined ? '' : `; ${entryLine}`}`);
1064
1066
  const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
1065
1067
  const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true);
1066
1068
  log(` gate spine-html (rigc ${packedBuildLabel(mode)}), verbatim:`);
package/src/check.ts CHANGED
@@ -56,9 +56,9 @@
56
56
  * uncovered holes `assemble` recorded in `parts.json`'s `recomposite` block,
57
57
  * copied into `check.json` because the seam cannot see them — it compares the
58
58
  * setup pose with the flat stack of parts, and a pixel no part holds is
59
- * missing from both. When `motion.json` holds an animation beside the idle,
60
- * one more bar, `ANIMATIONS_DO_NOT_FOLD` (issue #320): rigc's own
61
- * `render --folds` over every animation, physics constraints left out
59
+ * missing from both. When `motion.json` holds an animation, one more bar,
60
+ * `ANIMATIONS_DO_NOT_FOLD` (issues #320, #333): rigc's own `render --folds`
61
+ * over every animation, the idle included, physics constraints left out
62
62
  * ({@link FOLD_LINE}). The part-alone renders, the shut-eye still, the keyed
63
63
  * build and the fold render are scratch (`_isolated/`, `_still/`, `_loop/`,
64
64
  * `_folds/`), removed before the stage returns.
@@ -317,10 +317,58 @@ export interface RigcCall {
317
317
  status: number;
318
318
  /** stdout then stderr. */
319
319
  out: string;
320
+ /**
321
+ * Which rigc ran the call, as {@link rigcIdentity} words it (`rigc <version> at <path>`), from a runner that knows
322
+ * its binary ({@link rigcRunnerAt}): set on every call that exited non-zero and on every `--version` call, so a
323
+ * refusal quoting the call names the binary and its version (issue #337). Absent from a runner that does not know.
324
+ */
325
+ ran?: string;
320
326
  }
321
327
 
322
328
  export type RigcRunner = (args: readonly string[]) => RigcCall;
323
329
 
330
+ /** The process primitive {@link rigcRunnerAt} is given: `src/` spawns nothing, so the caller's spawn is injected. */
331
+ export type RigcSpawn = (bin: string, args: readonly string[]) => RigcCall;
332
+
333
+ /**
334
+ * A rigc binary as what it says it is: `rigc <version> at <path>`, the version being the first line `--version`
335
+ * printed — the line `check` prints before it builds (`checkStage`). A `--version` run that exited non-zero or
336
+ * printed nothing is said so, with the path, rather than read as a version.
337
+ */
338
+ export function rigcIdentity(bin: string, version: RigcCall): string {
339
+ const first = version.out.trim().split('\n')[0] ?? '';
340
+ if (version.status === 0 && first !== '') return `rigc ${first} at ${bin}`;
341
+ return `rigc (\`--version\` exited ${version.status}${first === '' ? ' and printed nothing' : `, printing ${JSON.stringify(first)}`}) at ${bin}`;
342
+ }
343
+
344
+ /** The clause a refusal quoting a rigc call ends with: which rigc ran it ({@link RigcCall.ran}), or nothing when the runner did not say. */
345
+ export function rigcRanClause(call: { ran?: string }): string {
346
+ return call.ran === undefined ? '' : `; ${call.ran} ran it`;
347
+ }
348
+
349
+ /**
350
+ * The runner every stage is given (issue #337): `bin` spawned through `spawn`, each call that exited non-zero, and
351
+ * each `--version` call, carrying {@link RigcCall.ran}. The identity is read once per runner, off its first
352
+ * `--version` call or, when a call fails before any, off one `--version` run then — so a green run spawns nothing
353
+ * extra, and a failure names the binary and version that produced it rather than leaving the reader to guess
354
+ * which rigc a path resolved to.
355
+ */
356
+ export function rigcRunnerAt(bin: string, spawn: RigcSpawn): RigcRunner {
357
+ let identity: string | null = null;
358
+ return (args) => {
359
+ const call = spawn(bin, args);
360
+ const version = args.length === 1 && args[0] === '--version';
361
+ if (version) {
362
+ const own = rigcIdentity(bin, call);
363
+ identity ??= own;
364
+ return { ...call, ran: own };
365
+ }
366
+ if (call.status === 0) return call;
367
+ identity ??= rigcIdentity(bin, spawn(bin, ['--version']));
368
+ return { ...call, ran: identity };
369
+ };
370
+ }
371
+
324
372
  /** A line in which a rigc run that stopped says why: Bun's own `error: …`, rigc's `rigc <command>: …` refusals, a `usage: …` line. */
325
373
  const CAUSE = /^(error: |rigc[\w -]*: |usage: )/;
326
374
 
@@ -396,6 +444,27 @@ export function readRigcEntry(versionOut: string): RigcEntryRecord {
396
444
  throw new Error('unreachable');
397
445
  }
398
446
 
447
+ /**
448
+ * This package's own rigc: `node_modules/.bin/rigc` under `root` and nowhere else — no directory above it and no
449
+ * PATH, which {@link findRigc} walks (issue #337). The selftest's suites run rigc through it from the package root,
450
+ * so a fixture in a temp directory, or a tree nested inside another checkout (a harness worktree under
451
+ * `.claude/worktrees/`) whose own install is missing, cannot reach another tree's rigc. A miss is refused naming
452
+ * the one path looked at.
453
+ */
454
+ export function packageRigc(root: string): string {
455
+ const bin = join(resolve(root), 'node_modules', '.bin', 'rigc');
456
+ if (!existsSync(bin)) {
457
+ refuseIfAny([
458
+ {
459
+ code: 'CHECK_RIGC_PACKAGE',
460
+ object: bin,
461
+ detail: `does not exist; this package's own rigc is required here and no other is looked for — not in a directory above ${resolve(root)}, not on PATH (\`bun install\` puts it there)`,
462
+ },
463
+ ]);
464
+ }
465
+ return bin;
466
+ }
467
+
399
468
  /**
400
469
  * Where the `rigc` binary is: `node_modules/.bin/rigc` in `from` or any
401
470
  * directory above it (a clone has it beside the package; an install has it in
@@ -1646,7 +1715,7 @@ export interface CheckFigures {
1646
1715
  TIP_OVER_ROOT: JudgementLine;
1647
1716
  STILL_REGIONS_DARK: JudgementLine;
1648
1717
  TEXTURE_STRETCH: JudgementLine;
1649
- /** Written only when motion.json holds an animation beside the idle (issue #320): {@link FoldsLine}. */
1718
+ /** Written whenever motion.json holds an animation, the idle included (issues #320, #333): {@link FoldsLine}. */
1650
1719
  [FOLD_LINE]?: FoldsLine;
1651
1720
  RECOMPOSITE_HOLES: ReportedLine;
1652
1721
  /** Written only under `--source` (issue #77): the setup pose against the painting, REPORTED. */
@@ -1672,8 +1741,9 @@ export interface RequirementsBlock {
1672
1741
  }
1673
1742
 
1674
1743
  /**
1675
- * The bar over the animations beside the idle (issue #320): no mesh triangle
1676
- * turned over against the setup pose on any frame, on rig-c's own reading —
1744
+ * The bar over every animation, the idle included (issues #320, #333): no mesh
1745
+ * triangle turned over against the setup pose on any frame, on rig-c's own
1746
+ * reading —
1677
1747
  * `rigc render --folds`, one run over every animation the build holds — and
1678
1748
  * not a reading of this package's. A fold the art means is declared in the
1679
1749
  * requirements file's `may_fold`, by slot and animation with its why, and is
@@ -1689,8 +1759,12 @@ export interface RequirementsBlock {
1689
1759
  * not tracked]. The keys and weights fold at every rate or at none that the
1690
1760
  * sampling resolves; the rate a consumer steps physics at is the scene's.
1691
1761
  *
1692
- * The idle is not read here: written only when the motion holds an animation
1693
- * beside it, so a rig of the idle alone writes and prints what it did.
1762
+ * The idle is read like any other animation (issue #333): `TEXTURE_STRETCH`
1763
+ * reads how far the idle's triangles stretch, and a triangle that turns inside
1764
+ * out is this bar's. Written whenever the motion holds an animation, so a rig
1765
+ * of the idle alone writes it too; on the public examples and the trailer no
1766
+ * idle folds, keys only [measured for issue #333, rigc 2.47.1, a scratch run,
1767
+ * not tracked].
1694
1768
  */
1695
1769
  export const FOLD_LINE = 'ANIMATIONS_DO_NOT_FOLD';
1696
1770
 
@@ -1777,7 +1851,7 @@ export function besideIdleLine(names: readonly string[]): string | null {
1777
1851
  }
1778
1852
 
1779
1853
  // ---------------------------------------------------------------------------
1780
- // the animations beside the idle, read for folds (issue #320)
1854
+ // every animation, the idle included, read for folds (issues #320, #333)
1781
1855
  // ---------------------------------------------------------------------------
1782
1856
 
1783
1857
  /**
@@ -1792,7 +1866,7 @@ export const FOLD_RENDER_MAX = 16;
1792
1866
  /** The fold render, under `--out`; removed before the stage returns. */
1793
1867
  const FOLD_SCRATCH = '_folds';
1794
1868
 
1795
- /** One mesh that folds in one animation beside the idle: rigc's figures, the declaration that takes it if any, and rigc's own line. */
1869
+ /** One mesh that folds in one animation: rigc's figures, the declaration that takes it if any, and rigc's own line. */
1796
1870
  export interface FoldRow {
1797
1871
  animation: string;
1798
1872
  slot: string;
@@ -1811,10 +1885,10 @@ export interface FoldRow {
1811
1885
  line: string;
1812
1886
  }
1813
1887
 
1814
- /** `check.json`'s `ANIMATIONS_DO_NOT_FOLD`: written only when the motion holds an animation beside the idle. */
1888
+ /** `check.json`'s `ANIMATIONS_DO_NOT_FOLD`: written whenever the motion holds an animation, the idle included. */
1815
1889
  export interface FoldsLine {
1816
1890
  status: 'PASS' | 'FAIL';
1817
- /** Every animation beside the idle, in motion.json's order: each one read. */
1891
+ /** Every animation, the idle included, in motion.json's order: each one read. */
1818
1892
  animations: string[];
1819
1893
  fps: number;
1820
1894
  /** How many physics constraints the read build left out ({@link withoutPhysics}); 0 when the rig has none. */
@@ -1957,8 +2031,9 @@ export function rigcFailed(what: string, call: RigcCall, lines: readonly string[
1957
2031
  const named = lines.filter((l) => l.includes('FAIL') || l.includes('compile error'));
1958
2032
  const quoted = named.length > 0 ? named.map((l) => l.trim()) : causeLines(call.out);
1959
2033
  const object = `\`rigc ${what}\``;
1960
- if (quoted.length === 0) return [{ code: 'CHECK_RIGC_GREEN', object, detail: `exit ${call.status}, and it printed nothing` }];
1961
- return quoted.map((l) => ({ code: 'CHECK_RIGC_GREEN', object, detail: `exit ${call.status}: ${l}` }));
2034
+ const ran = rigcRanClause(call);
2035
+ if (quoted.length === 0) return [{ code: 'CHECK_RIGC_GREEN', object, detail: `exit ${call.status}, and it printed nothing${ran}` }];
2036
+ return quoted.map((l) => ({ code: 'CHECK_RIGC_GREEN', object, detail: `exit ${call.status}: ${l}${ran}` }));
1962
2037
  }
1963
2038
 
1964
2039
  /**
@@ -2065,7 +2140,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
2065
2140
  let loopSet: FrameSet | null = null;
2066
2141
  let loopPhysics: LoopPhysics[] | null = null;
2067
2142
  // The keyed build (issues #183, #320): with a physics constraint, the same rig and motion with every one left out,
2068
- // built once the same way, which the loop and the folds of the animations beside the idle are both read from.
2143
+ // built once the same way, which the loop and the folds of every animation are both read from.
2069
2144
  const stripped = withoutPhysics(inp.rig, inp.motion);
2070
2145
  let keyed: string | null = null;
2071
2146
  const keyedBuild = (what: string): string => {
@@ -2080,7 +2155,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
2080
2155
  keyed = join(loopDir, 'build');
2081
2156
  return keyed;
2082
2157
  };
2083
- const named = isRecord(inp.motion.animations) ? Object.keys(inp.motion.animations).filter((n) => n !== 'idle') : [];
2158
+ const animationNames = isRecord(inp.motion.animations) ? Object.keys(inp.motion.animations) : [];
2159
+ const named = animationNames.filter((n) => n !== 'idle');
2084
2160
  let folds: FoldsLine | null = null;
2085
2161
  const foldProblems: Problem[] = [];
2086
2162
  try {
@@ -2133,15 +2209,15 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
2133
2209
  written.push('motion_heat.png');
2134
2210
  }
2135
2211
 
2136
- // 3a. the animations beside the idle, read for folds by rigc (issue #320) — only when there is one; at the idle's
2137
- // rate whatever --requirements says, so a requirements file moves only what it declares (CK78), from the keyed build
2138
- if (named.length > 0) {
2212
+ // 3a. every animation, the idle included, read for folds by rigc (issues #320, #333) — only when there is one; at
2213
+ // the idle's rate whatever --requirements says, so a requirements file moves only what it declares (CK78), from the keyed build
2214
+ if (animationNames.length > 0) {
2139
2215
  const fps = IDLE_FPS;
2140
2216
  try {
2141
- const fr = rigc(['render', '--candidate', keyedBuild('the animations beside the idle with their physics constraint(s) left out'), '--folds', '--fps', String(fps), '--max', String(FOLD_RENDER_MAX), '--out', foldDir]);
2217
+ const fr = rigc(['render', '--candidate', keyedBuild('the animations with their physics constraint(s) left out'), '--folds', '--fps', String(fps), '--max', String(FOLD_RENDER_MAX), '--out', foldDir]);
2142
2218
  if (fr.status !== 0) refuseIfAny(rigcFailed(`render --folds --fps ${fps}${stripped === null ? '' : ' (physics constraint(s) left out)'}`, fr, fr.out.split('\n').filter((l) => l.includes('FAIL') || l.includes('rigc:'))));
2143
- const readings = readFoldSets(foldDir, named, fps, fr.out);
2144
- folds = foldsLine(named, fps, stripped === null ? 0 : stripped.physics.length, readings, reqFile === null ? [] : reqFile.may_fold, reqFile === null ? null : reqFile.path, foldProblems);
2219
+ const readings = readFoldSets(foldDir, animationNames, fps, fr.out);
2220
+ folds = foldsLine(animationNames, fps, stripped === null ? 0 : stripped.physics.length, readings, reqFile === null ? [] : reqFile.may_fold, reqFile === null ? null : reqFile.path, foldProblems);
2145
2221
  } finally {
2146
2222
  rmSync(foldDir, { recursive: true, force: true });
2147
2223
  }
@@ -2334,7 +2410,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
2334
2410
  const l = figures[n];
2335
2411
  tally(n, l.status === 'SKIP' ? l.reason : null);
2336
2412
  }
2337
- // Issue #320: the fold bar is counted only where it is written, so a rig of the idle alone counts what it counted.
2413
+ // Issues #320, #333: the fold bar is counted where it is written — on every rig whose motion holds an animation.
2338
2414
  if (folds !== null) tally(FOLD_LINE, null);
2339
2415
  return {
2340
2416
  figures,
@@ -3511,7 +3587,7 @@ export function requireRigcVersion(rigc: RigcRunner): string {
3511
3587
  {
3512
3588
  code: 'CHECK_RIGC_VERSION',
3513
3589
  object: '`rigc --version`',
3514
- detail: `${got === null ? `exit ${v.status}, printed ${JSON.stringify(v.out.trim().split('\n')[0] ?? '')} — no version` : `is ${got.join('.')}`}; rig-c ${RIGC_GEOMETRY_VERSION} or later is required — its \`render --geometry\` writes the skinned mesh vertices TEXTURE_STRETCH is measured from (\`bun install\` puts this package's own rig-c at node_modules/.bin/rigc)`,
3590
+ detail: `${got === null ? `exit ${v.status}, printed ${JSON.stringify(v.out.trim().split('\n')[0] ?? '')} — no version` : `is ${got.join('.')}`}${rigcRanClause(v)}; rig-c ${RIGC_GEOMETRY_VERSION} or later is required — its \`render --geometry\` writes the skinned mesh vertices TEXTURE_STRETCH is measured from (\`bun install\` puts this package's own rig-c at node_modules/.bin/rigc)`,
3515
3591
  },
3516
3592
  ]);
3517
3593
  }
package/src/contour.ts CHANGED
@@ -380,8 +380,31 @@ export interface ContourBridges {
380
380
  units: number[][];
381
381
  /** One per unit but unit 0, in the order they were joined. */
382
382
  joins: ContourBridge[];
383
+ /**
384
+ * Only when one was left out (HQ's ruling on #326): each faint island — every pixel's alpha under
385
+ * {@link FAINT_ALPHA} — largest first. Its pixels stay in the image and are not meshed, so not drawn, as `stray`
386
+ * leaves dust; `artPixels` does not count them.
387
+ */
388
+ faint?: ContourFaintIsland[];
389
+ }
390
+
391
+ /** A faint island a bridged mesh left out (issue #326): its art px, its brightest pixel's alpha, and its box in part-image px `[x, y, w, h]`. */
392
+ export interface ContourFaintIsland {
393
+ art: number;
394
+ maxAlpha: number;
395
+ box: [number, number, number, number];
383
396
  }
384
397
 
398
+ /**
399
+ * Issue #326, HQ's ruling: under `bridges`, an island whose every pixel has alpha below this is faint, left out and
400
+ * named; any island with one pixel at or above it is visible and bridged, whatever the bridge costs. 64/255 is a
401
+ * quarter of full opacity — a faint pixel moves the composite by at most 63/255 of the colour beneath it — and it
402
+ * sits below the 128 that assemble and `check` read as covered (`FRONT_ALPHA`, `COVERED_ALPHA`, check's coverage),
403
+ * so nothing those readings count as drawn is left out. Read over what a bridged draft reaches on the public figures
404
+ * (the four examples and rig-c/trailer 6e2de45), the faint islands peak at 11–17 and the visible ones at 255.
405
+ */
406
+ export const FAINT_ALPHA = 64;
407
+
385
408
  export interface ContourMesh {
386
409
  /** Part-image px, y down, every value a multiple of 1/{@link GRID}; the first `hull` are the outline, in walk order. */
387
410
  vertices: Array<[number, number]>;
@@ -2010,8 +2033,11 @@ interface BridgeUnit {
2010
2033
  * The bridged mesh (issue #326, Stage B; `bridges: true`). Stage A (`docs/evidence/island-draft.md`) measured the
2011
2034
  * join this builds by hand: rig-c reads one closed outline per mesh, and a bridge is what a join costs.
2012
2035
  *
2013
- * 1. **Islands.** Every 4-connected island of the art is kept, but an island that is not the largest (nor tied
2014
- * with it) and holds `stray` art px or fewer, which is left out as the one-island rule leaves it out.
2036
+ * 1. **Islands**, in this order. Dust first: an island that is not the largest (nor tied with it) and holds `stray`
2037
+ * art px or fewer is left out as the one-island rule leaves it out. Then faint islands: one that is not the
2038
+ * largest and whose every pixel has alpha under {@link FAINT_ALPHA} is left out the same way and named in
2039
+ * `bridges.faint`. Every other island is kept, and step 3 bridges it whatever its bridge costs — overdraw is
2040
+ * reported (`joins[].area`), never a reason to drop art.
2015
2041
  * 2. **Silhouettes, in the trace.** The kept islands with their holes filled grow together by the margin
2016
2042
  * ({@link growSilhouette}, its pinch fill included), and each 4-connected piece of the grown set is one unit,
2017
2043
  * traced on its own. Islands closer than about twice the margin grow into one unit and are traced as one
@@ -2044,17 +2070,26 @@ function bridgedMesh(part: string, mask: AlphaMask, params: ContourParams, comp:
2044
2070
  for (let l = 1; l <= n; l++) largest = Math.max(largest, comp.stats[l].area);
2045
2071
  const leave: number[] = [];
2046
2072
  for (let l = 1; l <= n; l++) if (comp.stats[l].area !== largest && params.stray !== undefined && comp.stats[l].area <= params.stray) leave.push(l);
2073
+ // Issue #326 (HQ's ruling): after dust, a faint island — every pixel's alpha under FAINT_ALPHA — is left out and
2074
+ // named; every other island is bridged whatever its bridge costs. The largest island (or a tie for it) is never left out.
2075
+ const peak = new Array<number>(n + 1).fill(0);
2076
+ for (let i = 0; i < comp.labels.length; i++) if (comp.labels[i] > 0) peak[comp.labels[i]] = Math.max(peak[comp.labels[i]], mask.alpha[i]);
2077
+ const faint: number[] = [];
2078
+ for (let l = 1; l <= n; l++) if (comp.stats[l].area !== largest && !leave.includes(l) && peak[l] < FAINT_ALPHA) faint.push(l);
2047
2079
  let meshed = mask;
2048
2080
  let strayPixels = 0;
2049
- if (leave.length > 0) {
2050
- const out = new Set(leave);
2081
+ if (leave.length > 0 || faint.length > 0) {
2082
+ const out = new Set([...leave, ...faint]);
2051
2083
  const alpha = new Uint8Array(mask.alpha);
2052
2084
  for (let i = 0; i < alpha.length; i++) if (out.has(comp.labels[i])) alpha[i] = 0;
2053
2085
  meshed = { width: w, height: h, alpha };
2054
2086
  for (const l of leave) strayPixels += comp.stats[l].area;
2055
2087
  }
2056
2088
  const keptIslands = new Set<number>();
2057
- for (let l = 1; l <= n; l++) if (!leave.includes(l)) keptIslands.add(l);
2089
+ for (let l = 1; l <= n; l++) if (!leave.includes(l) && !faint.includes(l)) keptIslands.add(l);
2090
+ const left: ContourFaintIsland[] = faint
2091
+ .map((l) => ({ art: comp.stats[l].area, maxAlpha: peak[l], box: [comp.stats[l].left, comp.stats[l].top, comp.stats[l].width, comp.stats[l].height] as [number, number, number, number] }))
2092
+ .sort((a, b) => b.art - a.art || a.box[1] - b.box[1] || a.box[0] - b.box[0]);
2058
2093
 
2059
2094
  const island = artMask(meshed, threshold);
2060
2095
  const filled = fillHoles(island);
@@ -2269,6 +2304,7 @@ function bridgedMesh(part: string, mask: AlphaMask, params: ContourParams, comp:
2269
2304
  islands: [...keptIslands].map((l) => comp.stats[l].area).sort((a, b) => b - a),
2270
2305
  units: units.map((u) => u.islands),
2271
2306
  joins: joins.map((j, i) => ({ ...j, vertices: quads[i].map(at) as [number, number, number, number] })),
2307
+ ...(left.length === 0 ? {} : { faint: left }),
2272
2308
  },
2273
2309
  },
2274
2310
  };
package/src/meshdraft.ts CHANGED
@@ -70,7 +70,7 @@ import { BARS, type CheckReport, type PackShape, type PageEdges, type RigcRunner
70
70
  import { type CharacterConfig, type ContourMeshSpec, type LatticeMeshSpec, loadConfigAndAnimations, type Segment } from './config.ts';
71
71
  import { PartsError, type Problem, problemLine } from './errors.ts';
72
72
  import { IDLE_FPS } from './motion.ts';
73
- import type { ContourBridges } from './contour.ts';
73
+ import { type ContourBridges, FAINT_ALPHA } from './contour.ts';
74
74
  import { ART_ALPHA, DUST_MAX_PX, DUST_MAX_SHARE, isDust } from './mesh.ts';
75
75
  import { type PartsFile, readParts } from './parts.ts';
76
76
  import { alphaAbove, connectedComponents } from './raster/index.ts';
@@ -607,6 +607,12 @@ export interface BridgeFigure {
607
607
  area_px2: number;
608
608
  /** That area over the part's art px: the share of the candidate's overdraw the bridges draw (4 decimals). */
609
609
  overdraw: number;
610
+ /**
611
+ * Only when one was left out (HQ's ruling on #326): each faint island — every pixel's alpha under `FAINT_ALPHA`
612
+ * (`src/contour.ts`) — with its art px, its brightest pixel's alpha and its box in part-image px. Not meshed, so not
613
+ * drawn.
614
+ */
615
+ faint?: Array<{ art_px: number; max_alpha: number; box: [number, number, number, number] }>;
610
616
  }
611
617
 
612
618
  export interface PartRow {
@@ -637,7 +643,13 @@ export function bridgesOf(rigDir: string, part: string, art: number): BridgeFigu
637
643
  const b = rows.find((r) => r.part === part)?.contour?.bridges;
638
644
  if (b === undefined) return null;
639
645
  const area = b.joins.reduce((t, j) => t + j.area, 0);
640
- return { units: b.units, joins: b.joins.map((j) => ({ span: j.span, area_px2: j.area })), area_px2: area, overdraw: art === 0 ? 0 : pyRound(area / art, 4) };
646
+ return {
647
+ units: b.units,
648
+ joins: b.joins.map((j) => ({ span: j.span, area_px2: j.area })),
649
+ area_px2: area,
650
+ overdraw: art === 0 ? 0 : pyRound(area / art, 4),
651
+ ...(b.faint === undefined ? {} : { faint: b.faint.map((f) => ({ art_px: f.art, max_alpha: f.maxAlpha, box: f.box })) }),
652
+ };
641
653
  }
642
654
 
643
655
  /** The mesh's area over the art's: the triangles' area in image px over the image's art px. */
@@ -772,11 +784,20 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
772
784
  return d === null || 'keep' in d || d.dust.length === 0 ? 'no dust' : `stray ${d.stray as number} leaves out ${d.dust.length} dust island(s) of ${d.dust.join(', ')} px`;
773
785
  };
774
786
  const row = (part: string): PartRow => rows.get(part) as PartRow;
787
+ const faintText = (faint: NonNullable<BridgeFigure['faint']>): string =>
788
+ `LEFT OUT as faint, every pixel under alpha ${FAINT_ALPHA}: ${faint.map((f) => `${f.art_px} px (brightest alpha ${f.max_alpha}, at part-image px ${f.box[0]},${f.box[1]} ${f.box[2]}x${f.box[3]})`).join('; ')} — not meshed, so not drawn; if those pixels matter to you, keep the lattice for this part`;
775
789
  const bridgeText = (r: PartRow): string => {
776
790
  const j = joined.get(r.part) as { real: number[]; bone: string };
777
791
  const b = r.candidate?.bridges;
778
- const joins = b === undefined ? 'its bridges were not read' : `${b.joins.length} bridge(s) of ${b.joins.map((x) => `${x.span[0]} and ${x.span[1]} px`).join('; ')} span, ${b.area_px2} px² over no art (overdraw ${b.overdraw} of the candidate's ${r.candidate?.overdraw})`;
779
- return `its ${j.real.length} real islands (${j.real.join(', ')} px) traced as ${b === undefined ? 'its' : b.units.length} grown silhouette(s) and joined into one outline by ${joins}, every vertex bound to the one bone "${j.bone}" (issue #326)`;
792
+ const joins =
793
+ b === undefined
794
+ ? 'its bridges were not read'
795
+ : b.joins.length === 0
796
+ ? 'no bridge'
797
+ : `${b.joins.length} bridge(s) of ${b.joins.map((x) => `${x.span[0]} and ${x.span[1]} px`).join('; ')} span, ${b.area_px2} px² over no art (overdraw ${b.overdraw} of the candidate's ${r.candidate?.overdraw}; overdraw is reported, never a reason to leave art out)`;
798
+ const faint = b?.faint ?? [];
799
+ const visible = j.real.length - faint.length;
800
+ return `its ${j.real.length} real islands (${j.real.join(', ')} px)${faint.length === 0 ? '' : `: ${faintText(faint)}; the ${visible} visible`} traced as ${b === undefined ? 'its' : b.units.length} grown silhouette(s) and joined into one outline by ${joins}, every vertex bound to the one bone "${j.bone}" (issue #326)`;
780
801
  };
781
802
 
782
803
  // 3. The drafted rig must build: a refusal laid on a part keeps that part's lattice.
@@ -933,7 +954,8 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
933
954
  drafted.set(row.part, { ...cand, note: row.note });
934
955
  } else if (row.note === '') {
935
956
  const failed = row.gate.filter((x) => !x.pass);
936
- row.note = `keeps its lattice at grid ${row.lattice.grid}: ${failed.length === 0 ? 'no candidate was read' : failed.map((x) => `${x.reading} — ${x.text}`).join('; ')}`;
957
+ const faint = row.candidate?.bridges?.faint ?? [];
958
+ row.note = `keeps its lattice at grid ${row.lattice.grid}: ${failed.length === 0 ? 'no candidate was read' : failed.map((x) => `${x.reading} — ${x.text}`).join('; ')}${faint.length === 0 ? '' : `; on the bridged candidate those readings read: ${faintText(faint)}`}`;
937
959
  }
938
960
  }
939
961
  const configText = splicedConfig(text, drafted);
@@ -972,7 +994,7 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
972
994
  writeFileSync(written[0], configText);
973
995
  writeFileSync(written[1], `${JSON.stringify(report, null, 2)}\n`);
974
996
  for (const r of rowsOut) {
975
- const cand = r.candidate === null ? '' : `; candidate ${r.candidate.triangles} triangle(s), overdraw ${r.candidate.overdraw}${r.candidate.bridges === undefined ? '' : ` (${r.candidate.bridges.joins.length} bridge(s), ${r.candidate.bridges.area_px2} px² over no art: overdraw ${r.candidate.bridges.overdraw} of it)`}`;
997
+ const cand = r.candidate === null ? '' : `; candidate ${r.candidate.triangles} triangle(s), overdraw ${r.candidate.overdraw}${r.candidate.bridges === undefined ? '' : ` (${r.candidate.bridges.joins.length} bridge(s), ${r.candidate.bridges.area_px2} px² over no art: overdraw ${r.candidate.bridges.overdraw} of it${(r.candidate.bridges.faint ?? []).map((f) => `; LEFT OUT faint ${f.art_px} px, brightest alpha ${f.max_alpha} < ${FAINT_ALPHA}`).join('')})`}`;
976
998
  log(` ${r.mode === 'contour' ? 'DRAFTED' : 'LATTICE'} ${r.part}: lattice grid ${r.lattice.grid} ${r.lattice.triangles} triangle(s), overdraw ${r.lattice.overdraw}${cand}`);
977
999
  for (const g of r.gate) log(` ${g.pass ? 'pass' : 'FAIL'} ${g.reading}: ${g.text}`);
978
1000
  if (r.mode === 'grid' && r.gate.length === 0) log(` ${r.note}`);
@@ -181,8 +181,9 @@ export interface RequirementsFile {
181
181
  rests: string[];
182
182
  /**
183
183
  * Issue #320: the folds the scene means — a slot whose mesh turns a triangle
184
- * over in a named animation on purpose (a sleeve hidden behind the body as
185
- * the arm crosses it), each by slot and animation with the author's why.
184
+ * over in an animation, the idle included (issue #333), on purpose (a sleeve
185
+ * hidden behind the body as the arm crosses it), each by slot and animation
186
+ * with the author's why.
186
187
  * `ANIMATIONS_DO_NOT_FOLD` names each such fold with its why and does not
187
188
  * hold it to the bar, and fails an entry whose slot does not fold in that
188
189
  * animation, as rig-c refuses an exemption that exempts nothing. Empty when
@@ -581,11 +582,11 @@ export function resolveRequirements(file: RequirementsFile, rig: Record<string,
581
582
  }
582
583
  }
583
584
 
584
- // Issue #320: each declared fold names a slot that holds a mesh and an animation beside the idle, the ones ANIMATIONS_DO_NOT_FOLD reads.
585
+ // Issues #320, #333: each declared fold names a slot that holds a mesh and an animation motion.json declares — the idle
586
+ // included, since ANIMATIONS_DO_NOT_FOLD reads every animation.
585
587
  file.may_fold.forEach((m, i) => {
586
588
  const object = `may_fold[${i}] (slot "${m.slot}", animation "${m.animation}")`;
587
- if (m.animation === 'idle') problems.push({ code: 'REQUIREMENTS_MAY_FOLD', object, detail: 'names the idle, which ANIMATIONS_DO_NOT_FOLD does not read — it reads the animations beside the idle, so declaring it excepts nothing' });
588
- else if (!(m.animation in anims)) unresolved(object, `names animation "${m.animation}", which motion.json does not declare; it declares ${listed(animNames)}`);
589
+ if (!(m.animation in anims)) unresolved(object, `names animation "${m.animation}", which motion.json does not declare; it declares ${listed(animNames)}`);
589
590
  const slots = (Array.isArray(rig.slots) ? rig.slots : []).filter(isRecord).map((s) => s.name);
590
591
  if (!slots.includes(m.slot)) {
591
592
  unresolved(object, `names slot "${m.slot}", which rig.json does not declare`);
package/src/rig.ts CHANGED
@@ -1455,7 +1455,7 @@ export function buildRig(
1455
1455
  bones: [...bones].sort(),
1456
1456
  max_influences: maxInfl,
1457
1457
  mean_influences: pyRound(infl / cm.vertices.length, 2),
1458
- art_coverage: pyRound(rep.coveredArtPixels / (rep.artPixels + rep.strayPixels), 5),
1458
+ art_coverage: pyRound(rep.coveredArtPixels / (rep.artPixels + rep.strayPixels + (rep.bridges?.faint ?? []).reduce((t, f) => t + f.art, 0)), 5),
1459
1459
  mode: 'contour',
1460
1460
  ...(exponent === undefined ? {} : { exponent }),
1461
1461
  ...(declared === undefined ? {} : { influences: declared }),