@rig-c/parts 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,13 +41,13 @@ from the full run, because the twin tails leave the head crop sideways (a plan e
41
41
  <code>loop: idle 41 frame(s) at 12 fps, f0000 vs f0040 (t = 3.333333333333333s): max |d| 0 (0 required)</code><br/>
42
42
  <code>seam: setup pose at 654x1216, scale 0.9438: mean |d| 0.147 (&lt;= 1.0), 0 px over 40, 0.00 rig px (the count / s²; &lt;= 50), 0 px over 80 (reported)</code><br/>
43
43
  <code>pack: skeleton.png 1051x972, 23 region(s), 96.9% covered, padding 2, page edges free, shape polygon</code><br/>
44
- The loop is that build's indexed APNG, 40 frames at 12 fps, 704,009 bytes, one
45
- 256-entry palette at a measured error of max 70, mean 0.631 per channel over every
46
- frame (the lossless APNG beside it, the exactness record, is 4,446,263 bytes; the
47
- GIF, at the same error, 760,294). The rig's hair, skirt, sleeves, bows and earrings
44
+ The loop is that build's indexed APNG, 40 frames at 12 fps, 703,909 bytes, one
45
+ 256-entry palette at a measured error of max 71, mean 0.636 per channel over every
46
+ frame (the lossless APNG beside it, the exactness record, is 4,449,233 bytes; the
47
+ GIF, at the same error, 760,058). The rig's hair, skirt, sleeves, bows and earrings
48
48
  carry physics, which rigc's render starts from rest, so the idle is played three
49
49
  periods ahead and the loop is the fourth: the frame one period after its first differs
50
- from it by max 7 levels, 0 px over 40 (<code>--lead-in</code>). Frame 29 (2.417 s) is the
50
+ from it by max 1 level, 0 px over 40 (<code>--lead-in</code>). Frame 29 (2.417 s) is the
51
51
  closed eye: the blink holds for 0.084 s, one 12 fps frame rounded up; the painting is
52
52
  shown at half size, resampled and written by this package's PNG codec (nothing here
53
53
  encodes JPEG). <code>rig-parts sheet</code> made the contact sheet. Its
@@ -278,7 +278,7 @@ agent skill.
278
278
  | `comfy seethrough --image --out [--host]` | optional: run the ComfyUI See-through wrapper on one image and write the form `layers` reads |
279
279
  | `layers <dir \| layers.json \| file.psd>` | print every layer of a decomposition: draw order, name, tag group, box, size, opaque pixels, depth, and its translucent, background and area figures; a `WARN` line for a layer `--propose-plan` will leave out |
280
280
  | `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
281
- | `assemble --propose-plan …` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule, and naming in a note any full-run body no planned part covers |
281
+ | `assemble --propose-plan …` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule, naming in a note any full-run body no planned part covers, and naming every layer no row takes with why (empty, taken from the other run only, a plausibility rule; `nose`, never planned, with the row that lets `propose` read the head's pitch) |
282
282
  | `assemble --source --full --head --config --out [--seam] [--project]` | merge the two runs into rig-space parts, `parts.json`, the recomposite and its error map, and list the uncovered holes |
283
283
  | `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
284
284
  | `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay; print, per bone, which check read it or that none did and why (its role, its kind of segment), and write `basis.json` beside the proposal — what each bone rests on (a joint, a part's mask, a ratio, other bones) and, with `--compare`, whether the config's bone differs |
@@ -355,7 +355,7 @@ for each — a config fragment, the `check` lines it passes with, and its limit:
355
355
  and a `physics` constraint on it; `examples/reference-sample` carries one. Under its slow
356
356
  4 s idle the bust moves at most 0.066 rig px and the hips not at all: the spring answers
357
357
  faster keys on the body. Its `hop`, a named animation keying only `hip`, moves the
358
- bust up to 6.583 and the hips up to 4.432 rig px; the region's `band` caps how far a patch may
358
+ bust up to 6.702 and the hips up to 4.609 rig px; the region's `band` caps how far a patch may
359
359
  go before the texture-stretch ceiling, and `check` measures the idle alone.
360
360
 
361
361
  ### How much of a rig the model painted
package/docs/AUTHORING.md CHANGED
@@ -6,12 +6,17 @@ the files it writes. Every refusal the tool prints names a rule, an object, the
6
6
  value found and the value required — §6 maps each rule to the field or input that
7
7
  has to change.
8
8
 
9
- The worked example throughout is [`examples/reference-sample`](../examples/reference-sample): its
9
+ The worked example throughout is [`examples/reference-sample`](https://github.com/rig-c/parts/tree/main/examples/reference-sample): its
10
10
  `config.json` is a complete, loading config, `proposal.json` is what the proposer
11
11
  wrote for it, and `expected/` is what the reference implementation produced from
12
12
  the same inputs — except `check.json`, which this port's `build` regenerates since
13
13
  the reference has no judgement lines (§7). `bun run fetch-examples` puts its painting and See-through layers
14
- into `examples/reference-sample/inputs/`.
14
+ into `examples/reference-sample/inputs/` in a clone of the repository.
15
+
16
+ A path this page names that the installed package does not hold — anything under
17
+ `examples/`, `docs/evidence/`, `tools/`, `fixtures/` or `scripts/`, and the scripts
18
+ `bun run` names — is in the repository, https://github.com/rig-c/parts; the package holds the files
19
+ `package.json` lists under `files` (`npm pack --dry-run` prints them).
15
20
 
16
21
  The examples' names: `reference-sample` and `reference-demo` are the two characters
17
22
  the reference implementation ran (published as `sample` and `demo` until issue #227),
@@ -92,9 +97,11 @@ read by no CPU stage).
92
97
  | `assemble.patches` | assemble; `meshes`/`regions` keys may name them too | **authored**, optional: `[{name, box, alpha, draw}]` — an extra part cut from the **painting** itself, for figure no See-through layer holds (a hem both runs dropped). `box` is `[x0, y0, x1, y1]` in **rig** pixels, `x1`/`y1` exclusive — the space of `parts.json` and `recomposite_rig.png`, where the hole is found; `alpha` is `"silhouette"` (the painting's figure silhouette inside the box, the one `--seam silhouette` uses) or `"box"` (the whole box); `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`. A patch is always a region: its bone is `regions.<name>`, in the one place every region's bone lives, and a `meshes` entry for it is refused. See §5 for how to place one |
93
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 |
94
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 |
95
- | `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) 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 |
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 |
96
101
  | `meshes.<part>.contour` | rig | **authored**, optional (issue #84): `{tolerance, margin, spacing, maxEdge?, budget?, stray?, 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); `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 |
97
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
+ | `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
+ | `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 |
98
105
  | `regions.<part>` | rig | **proposed**: the bone a rigid part rides. Every plan part is exactly one of a mesh or a region (`CONFIG_PART_ATTACHED`) |
99
106
  | `motion.duration` | rig; check (the loop is measured at this time) | proposed as 40 frames at 12 fps, `40/12` s, written `3.3333333333333335` (issue #252; the reference proposed 4 s, and every period it drafted is scaled with it so each track keeps its cycles per idle); a whole number of 1/12 s ticks, because `check` renders at 12 fps and compares frame 0 with the frame at this time — read within 10^-9 s (`TIME_SLACK`, issue #244), since a whole number of frames is often not exact in binary: rigc's 40th frame sits at `40 × (1/12)` = `3.333333333333333`. Half a frame off is `CHECK_LOOP_LAST_FRAME_AT_DURATION` |
100
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 |
@@ -374,6 +381,75 @@ top edge instead would close the eye upward onto the upper lid and still move wh
374
381
  above it by (1 − squash) × its height above the pivot, so the cut, not the pivot, is what
375
382
  holds the crease.
376
383
 
384
+ ### The weight cut (`meshes.<part>.influences`, issue #284)
385
+
386
+ The distance rule (and bone heat) shares a vertex among the bones its mesh's segments name,
387
+ then cuts the list: the 4 largest kept, any share under 0.03 dropped. Both numbers are the
388
+ reference implementation's (`[:4]` and `>= 0.03`), carried over as port constants. Neither
389
+ is a limit of the format — spine-core reads a weighted vertex as a bone count followed by
390
+ that many bindings, with no bound on the count — and rig-c neither refuses nor warns above
391
+ four: its one assertion on a mesh's weights, `A20_MESH_WEIGHTS_COHERENT`, checks that they
392
+ are coherent, not how many there are (`docs/evidence/weight-cap.md`, *Where four comes
393
+ from*, in the repository). The cut is where a vertex changes hands in one step, and in the
394
+ lattice and contour modes a mesh may declare it:
395
+
396
+ ```json
397
+ "bottomwear": { "grid": 25, "r": 10, "segments": [["hip", [232, 425], [601, 425]], "skirt_r", "skirt_c", "skirt_l"], "influences": { "maxInfluences": 6, "floor": "continuous" } }
398
+ ```
399
+
400
+ **When to reach for it:** a part that folds where two chains meet, or at the end of a curl.
401
+ The default does not move; a fold is today found by looking at a build that passes every
402
+ bar, and this field is what there is to turn once one is seen.
403
+
404
+ **What it trades.** Every figure below is issue #284 Stage E's, verbatim from
405
+ [`docs/evidence/weight-cap.md`](https://github.com/rig-c/parts/blob/main/docs/evidence/weight-cap.md), on every lattice mesh of `demo`,
406
+ `scarf` and the trailer (`rig-c/trailer` at a99d976): the inverted triangles at the bend
407
+ sets (±15° on every chain link; on the trailer also ± the film's own bends, after the
408
+ slash), the largest idle displacement against today's rig in rig px (the part in the
409
+ survey's tables), and the cost — bindings (one per bone per vertex) in the whole
410
+ `rig.json`, `rig.json`'s length and the compiled `skeleton.json`'s, in bytes. Every
411
+ setting passed every `check` bar on all three examples, and `rigc render`'s time did not
412
+ separate them (the run's own spread was as large). "Cap none" is the survey's name for
413
+ `maxInfluences: null`.
414
+
415
+ | setting | inverted at the bends: demo / scarf / trailer ±15° / trailer film | idle vs today, rig px: demo / scarf / trailer | bindings: demo / scarf / trailer | `rig.json` bytes: demo / scarf / trailer | `skeleton.json` bytes: demo / scarf / trailer |
416
+ | --- | --- | --- | --- | --- | --- |
417
+ | cap 4, cut (today, absent) | 1 / 36 / 6 / 7 | — | 3008 / 4192 / 5014 | 471635 / 665178 / 773228 | 648296 / 886481 / 993045 |
418
+ | cap 4, continuous | 1 / 42 / 5 / 0 | 1.252 / 0.676 / 1.471 | 3008 / 4192 / 5014 | 471627 / 665230 / 773235 | 648282 / 886533 / 993052 |
419
+ | cap 5, cut | 1 / 35 / 5 / 1 | 1.853 / 1.291 / 1.875 | 3257 / 4546 / 5337 | 502053 / 707658 / 812385 | 670404 / 917493 / 1021528 |
420
+ | cap 5, continuous | 1 / 46 / 3 / 0 | 1.293 / 1.139 / 1.580 | 3257 / 4546 / 5337 | 502059 / 707681 / 812407 | 670410 / 917516 / 1021550 |
421
+ | cap 6, cut | 1 / 41 / 7 / 0 | 2.417 / 1.274 / 2.235 | 3423 / 4787 / 5562 | 522287 / 736622 / 839712 | 685243 / 938707 / 1041677 |
422
+ | cap 6, continuous | 1 / 45 / 3 / 0 | 1.329 / 1.132 / 1.750 | 3423 / 4787 / 5561 | 522272 / 736693 / 839600 | 685228 / 938778 / 1041595 |
423
+ | cap 8, cut | 1 / 43 / 20 / 3 | 3.350 / 1.274 / 3.106 | 3532 / 4969 / 5692 | 535476 / 758656 / 855632 | 694930 / 954712 / 1053604 |
424
+ | cap 8, continuous | 1 / 46 / 7 / 0 | 1.365 / 1.122 / 2.248 | 3532 / 4969 / 5692 | 535455 / 758684 / 855638 | 694909 / 954740 / 1053610 |
425
+ | cap none, cut | 1 / 43 / 22 / 3 | 3.350 / 1.291 / 3.179 | 3514 / 4987 / 5684 | 533305 / 760874 / 854648 | 693337 / 956381 / 1052864 |
426
+ | cap none, continuous | 1 / 47 / 7 / 0 | 1.365 / 1.114 / 2.197 | 3513 / 4987 / 5684 | 533169 / 760956 / 854631 | 693238 / 956463 / 1052847 |
427
+ | every bone, nothing dropped (the survey's reference; not a setting) | 0 / 36 / 0 / 0 | 3.350 / 1.336 / 3.286 | 5446 / 8969 / 9087 | 770980 / 1244841 / 1270495 | 866749 / 1314917 / 1358750 |
428
+
429
+ In the survey's words: cap 5 adds 6.4–8.4 % bindings, cap 6 10.9–14.2 %, cap 8 13.5–18.5 %,
430
+ no cap 13.4–19.0 %; `rig.json` grows 5.1–6.5 % at cap 5 and 10.5–14.4 % with no cap, the
431
+ compiled `skeleton.json` 2.9–3.5 % and 6.0–7.9 %; nothing dropped at all costs 81–114 % more
432
+ bindings. Read per part, where the folds are: on the trailer the skirt's hem corner at the
433
+ film's bends folds 5 today and none at caps 5 and 6 with either floor, and none at any cap
434
+ with the continuous floor; the long hair's curl tips fold 4 at ±15° today and none at caps
435
+ 5 and 6 with the continuous floor, and more, not fewer, above six with the cut (17 at cap
436
+ 8, 19 with no cap); demo's one fold, on the fringe, is there under every setting. The idle
437
+ moves under every setting that changes a weight: that column is the price of the change
438
+ itself, not a defect of the setting, and it is what to look at on the part before keeping
439
+ one. The survey's own pages hold the per-part tables and the pictures.
440
+
441
+ **What a declaration writes.** On `demo` and `scarf`, a config declaring each setting on
442
+ every lattice mesh writes, at every lattice vertex, the bones and weights the survey wrote
443
+ for that setting (measured when the field landed: 0 vertices differ in each of the twenty
444
+ builds), so the bindings and the largest count are the table's. `rig.json`'s length matches
445
+ the table at cap 4 and is a little shorter above it (demo at cap 5: 500517 bytes against
446
+ the survey's 502053), because the survey bound a bone new to a vertex at an offset read off
447
+ the rendered setup pose, and the rig stage writes that offset exactly, as it writes every
448
+ other. Under the continuous floor a share can round to 0, so such a mesh is rounded as a
449
+ contour region is (each share to 5 places, a 0 dropped, the remainder to the heaviest); under
450
+ the cut it is rounded as the lattice always was. In the contour mode a region's bone counts
451
+ toward the cap like any other (*The vertex's weights* in `src/localweights.ts`).
452
+
377
453
  ### A contour mesh (`meshes.<part>.contour`, issue #84)
378
454
 
379
455
  The lattice keeps every grid cell that holds an art pixel, so its boundary and its interior
@@ -984,7 +1060,7 @@ file in rig-c's motion-spec shape beside the config and name it:
984
1060
  ```
985
1061
 
986
1062
  `build` writes `surprised` into `rig/motion.json` after `idle`, gates it with the rig, and
987
- `check` names it on a line of its own (`examples/reference-sample` carries one, `animations.json` with `hop`, the
1063
+ `check` names it on a line of its own ([`examples/reference-sample`](https://github.com/rig-c/parts/tree/main/examples/reference-sample), in the repository, carries one, `animations.json` with `hop`, the
988
1064
  recipe below). With exactly this file beside `examples/reference-sample`'s config in its place,
989
1065
  `build` printed (this tree, rig-c as `package.json` pins it):
990
1066
 
@@ -997,10 +1073,10 @@ recipe below). With exactly this file beside `examples/reference-sample`'s confi
997
1073
  A bone the rig lacks is rigc's refusal at the rig stage, in rigc's words (`RIG_RIGC_GREEN`); a
998
1074
  fault in the file itself is the loader's (the row above names each one).
999
1075
 
1000
- **Worked sets.** `examples/demo/animations.json` and `examples/scarf/animations.json`
1001
- (issue #224) carry `tilt`, `look_l`, `look_r`, `smile`, `startled`, a gesture (`wave` on
1076
+ **Worked sets.** [`examples/demo/animations.json`](https://github.com/rig-c/parts/blob/main/examples/demo/animations.json) and [`examples/scarf/animations.json`](https://github.com/rig-c/parts/blob/main/examples/scarf/animations.json)
1077
+ (issue #224; in the repository, not the package) carry `tilt`, `look_l`, `look_r`, `smile`, `startled`, a gesture (`wave` on
1002
1078
  `demo`, `bow` on `scarf`) and `hop`, each a one-shot that keys only what it needs; the
1003
- examples' READMEs list what each keys and why. Two rules make a clip layer over the idle on
1079
+ examples' READMEs ([demo](https://github.com/rig-c/parts/blob/main/examples/demo/README.md), [scarf](https://github.com/rig-c/parts/blob/main/examples/scarf/README.md)) list what each keys and why. Two rules make a clip layer over the idle on
1004
1080
  a player's second track rather than replace it:
1005
1081
 
1006
1082
  - **Key the bones the idle leaves alone.** The idle turns and breathes the `*_ctl` bones
@@ -1013,10 +1089,33 @@ a player's second track rather than replace it:
1013
1089
 
1014
1090
  **Strands that trail.** A chain whose links lag the body is one `physics` constraint per
1015
1091
  link in `constraints`, on the link bone (`hairback_r0`, not `hairback_r0_ctl`), driving
1016
- `rotate`; the examples soften it down the chain. A single bone with no `length` (a bow, an
1017
- earring, a bun) cannot take a `rotate` drive (rig-c's `A23`) and takes `x`/`y` instead. The
1018
- values that read on the examples, and the ones refused on the way there, are in their
1019
- READMEs.
1092
+ `rotate`; the examples soften it down the chain: every link takes the root link's `inertia`,
1093
+ `damping`, `mass` and `mix`, and link i of n (i = 0 at the root) takes the root link's `strength` times
1094
+ 1 − 0.35 · i / (n − 1), so the last link is the softest at 0.65 of the root's. `demo`'s right
1095
+ twin tail, the three links of chain `hairback_r`, as
1096
+ [`examples/demo/config.json`](https://github.com/rig-c/parts/blob/main/examples/demo/config.json) (in the repository)
1097
+ holds it:
1098
+
1099
+ ```json
1100
+ "constraints": [
1101
+ { "type": "physics", "name": "hairback_r0_phys", "bone": "hairback_r0", "rotate": 1, "inertia": 0.8, "strength": 90.0, "damping": 0.85, "mass": 1, "mix": 1 },
1102
+ { "type": "physics", "name": "hairback_r1_phys", "bone": "hairback_r1", "rotate": 1, "inertia": 0.8, "strength": 74.25, "damping": 0.85, "mass": 1, "mix": 1 },
1103
+ { "type": "physics", "name": "hairback_r2_phys", "bone": "hairback_r2", "rotate": 1, "inertia": 0.8, "strength": 58.5, "damping": 0.85, "mass": 1, "mix": 1 },
1104
+ … ]
1105
+ ```
1106
+
1107
+ `scarf`'s scarf ends, four links each, take the same shape at inertia 0.7: `strength` 70,
1108
+ 61.8333, 53.6667 and 45.5 down `robe_r` ([`examples/scarf/config.json`](https://github.com/rig-c/parts/blob/main/examples/scarf/config.json)).
1109
+ What these values do, measured on the clips and not by any bar: in `demo`'s `hop` the last
1110
+ link of each tail sat 46.6 and 45.3 rig px from where the keys put it relative to `head` at
1111
+ the landing (0.73 s); inertia 1 tore `hair_back` in that hop (`TEXTURE_STRETCH` 3.258 against
1112
+ 1.926544), and lowering `strength` instead of raising inertia saturates (the demo README,
1113
+ *The strands*). A single bone with no `length` (a bow, an earring, a bun) cannot take a
1114
+ `rotate` drive (rig-c's `A23`) and takes `x`/`y` instead — `demo`'s bows `"x": 0.5, "y": 1`,
1115
+ inertia 0.3, strength 160. The values that read on the examples, and the ones refused on the
1116
+ way there, are in their READMEs, in the repository:
1117
+ [`examples/demo/README.md`](https://github.com/rig-c/parts/blob/main/examples/demo/README.md) and
1118
+ [`examples/scarf/README.md`](https://github.com/rig-c/parts/blob/main/examples/scarf/README.md).
1020
1119
 
1021
1120
  **Limits.** Every slot holds one attachment, the part's own image, so an expression moves,
1022
1121
  turns and scales the face's parts and never swaps one for another: there is no second mouth
@@ -1031,7 +1130,7 @@ against 1.926544).
1031
1130
 
1032
1131
  A jiggle (a bust, the hips) is a patch of one part that lags behind the body. The package
1033
1132
  proposes none and judges none; what it gives is the three pieces to build one, all in the
1034
- config. `examples/reference-sample` carries a worked one (issue #183, PR #187), and its config is the
1133
+ config. [`examples/reference-sample`](https://github.com/rig-c/parts/tree/main/examples/reference-sample) carries a worked one (issue #183, PR #187), and its config is the
1035
1134
  fragment below, cut to the bust:
1036
1135
 
1037
1136
  ```jsonc
@@ -1084,10 +1183,12 @@ idle moves the bust at most 0.066 rig px, idle frame 2). Faster keys on the pare
1084
1183
  a turn, in a named animation (the recipe above) or a consumer's own — are what the spring
1085
1184
  answers. `reference-sample`'s `animations.json` holds one, `hop` (1 s: `hip` `translate` y down 5, up 24,
1086
1185
  landing at -6 at 0.45 s, settling at 0.55 s; nothing keys the jiggle bones): rendered at 30 fps
1087
- against the same build without its constraints, the bust moves at most 6.583 rig px and the hips
1088
- (`hip_phys` inertia 0.2, strength 160, damping 0.88, mass 1.5) at most 4.432 (frame 13, the fall
1089
- into the landing), and both overshoot after it, 3.38 and 1.38 rig px (frames 21 and 22;
1090
- `docs/evidence/sample-hop.png`). No bar here judges the jiggle, and two limits follow from that:
1186
+ against the same build without its constraints, the bust moves at most 6.702 rig px and the hips
1187
+ (`hip_phys` inertia 0.2, strength 160, damping 0.88, mass 1.5) at most 4.609 (frame 14, at the
1188
+ landing), and both overshoot after it, 3.27 and 1.35 rig px (frames 21 and 22). These are rig-c
1189
+ 2.45.2's renders, whose physics no longer ends a frame on a step's tie; on rig-c 2.40.0 the same
1190
+ build read 6.583 and 4.432 (frame 13) and 3.38 and 1.38, which
1191
+ [`docs/evidence/sample-hop.png`](https://github.com/rig-c/parts/blob/main/docs/evidence/sample-hop.png) shows. No bar here judges the jiggle, and two limits follow from that:
1091
1192
 
1092
1193
  - **The region's `band` caps the displacement.** The patch moves as one piece and the band is
1093
1194
  where the mesh stretches to follow it, so physics values that carry the patch further than
@@ -1096,8 +1197,8 @@ into the landing), and both overshoot after it, 3.38 and 1.38 rig px (frames 21
1096
1197
  geometry with PR #187's values (inertia 0.5, strength 100, damping 0.85, mass 1, a bone
1097
1198
  displacement of 9.85 rig px against bands of 16 and 12) gives
1098
1199
  `CHECK_TEXTURE_STRETCH: mesh "bottomwear" triangle 13 (vertices 5 183 6), edge 6-5, idle frame 13 — the edge is 0.233 times its rest length (max(ratio, 1/ratio) 4.29); <= 1.926544 is required`
1099
- and `topwear` 2.597; with the values tracked now it reads 1.697 (`topwear`) and 1.566
1100
- (`bottomwear`). Tune the constraint against the animation that drives it, not the idle.
1200
+ and `topwear` 2.597; with the values tracked now it reads 1.719 (`topwear`) and 1.602
1201
+ (`bottomwear`) (1.697 and 1.566 on rig-c 2.40.0, before 2.45.2's render clock). Tune the constraint against the animation that drives it, not the idle.
1101
1202
 
1102
1203
  #### A part only physics moves, in the automatic mode
1103
1204
 
@@ -1182,8 +1283,26 @@ full loader before its assemble stage writes anything.
1182
1283
  6. `rig-parts assemble --propose-plan --source … --full … --head … --config config.json`
1183
1284
  → paste `plan` and `extend_below_crop` into `config.assemble`. Config: the
1184
1285
  `layers` row, with `head_box`.
1185
- Read `notes`: a layer left out by a plausibility rule is named there with its
1186
- figures and the rule (§6, *Plausibility*), and a full-run layer holding a body
1286
+ Read `notes`: every layer not planned — each tag a layer of either run holds that no
1287
+ row takes — is named there once, `<tag>: in no plan row — …`, with why, run by run: the
1288
+ run holds no such layer; the proposer takes that tag from the other run only
1289
+ (`PROPOSE_HEAD_TAGS` in `src/assemble.ts` lists the tags it takes from the head run,
1290
+ and every other tag comes from the full run); the layer is empty, its opaque count (run
1291
+ pixels of alpha above 8 after ghost clean-up) below the proposer's floor of 150
1292
+ (`PROPOSE_MIN_PX`, `src/assemble.ts`); or it crosses a plausibility rule, and the
1293
+ layer's own note, last, gives its figures and the rule (§6, *Plausibility*; a layer
1294
+ only such a note leaves out is named by it alone). On the four examples the
1295
+ `neckwear`, `tail`, `wings`, `objects` and `eyewear` layers hold 62 to 74 px in each
1296
+ run, so each is named empty (or taken from the other run only). `nose` is never
1297
+ planned — the reference implementation's rule, which this port keeps; the reference
1298
+ records no reason — and its note gives both runs' counts and what a nose part buys:
1299
+ `propose`'s `head pitch:` note reads where the nose sits between the eye midpoint and
1300
+ the mouth only off a part tagged `nose`, so the note names the row to add,
1301
+ `["nose", "head", "nose"]` after the row that takes `face` (the run whose layer holds
1302
+ more of the nose). With that one row added to a copy of each config, `demo` reads
1303
+ 0.474 and `scarf` 0.564 (the figures `FRONTAL_REFERENCE` in `src/propose.ts` quotes),
1304
+ and the recomposite moves by 2 error px on `demo` (14,570 to 14,568) and none on
1305
+ `scarf`, uncovered error px unchanged on both. A full-run layer holding a body
1187
1306
  no planned part covers — long hair that leaves the head crop sideways, or a
1188
1307
  head-run part that stops above the crop line so `extend_below_crop` continues
1189
1308
  nothing — is named with the body, the rule that skipped it and the two
@@ -1235,7 +1354,10 @@ full loader before its assemble stage writes anything.
1235
1354
  rules for `bones`, `meshes`, `regions` and `motion`.
1236
1355
  9. `rig-parts build --config config.json --source inputs/painting.png --full … --head … --out out [--loop]`
1237
1356
  — assemble, rig and check in one process. `propose` is not part of it, on purpose:
1238
- a proposal is a draft, and the config you corrected is the input.
1357
+ a proposal is a draft, and the config you corrected is the input. How long it takes
1358
+ is §8 (the two reference examples, 12.7 s and 27.5 s wall clock on one machine): a
1359
+ run your harness stopped before its last line printed no verdict, which is not a
1360
+ failed build — the build's verdict is its `check:` line or a named refusal (§5, §6).
1239
1361
  `--idle-keys direct` is forwarded to the rig stage as `rig` takes it; a config
1240
1362
  with a two-bone ik over chain links the idle keys needs it (§3, `constraints`).
1241
1363
  Every flag `check` takes is forwarded to the check stage, taken by `build` as
@@ -1326,6 +1448,14 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
1326
1448
  | `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 |
1327
1449
  | `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line (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) |
1328
1450
 
1451
+ `RECOMPOSITE_HOLES` is `REPORTED`, never judged, and its count reads large: for scale, the
1452
+ four examples, each a green build, report 259 (`reference-sample`), 291 (`scarf`), 453
1453
+ (`reference-demo`) and 1,020 (`demo`) uncovered holes, holding 1,185, 1,365, 1,564 and
1454
+ 2,939 uncovered error px, the largest hole 94, 63, 70 and 48 px (each example's
1455
+ `expected/parts.json`, its `recomposite` block, in the repository) — hundreds of slivers
1456
+ along part edges are what a green build reports; one hole of hundreds of px is the red
1457
+ the `assemble` row above describes.
1458
+
1329
1459
  `loop` writes three files from one frame set, and they are not interchangeable.
1330
1460
  `idle.png` (`loop --out x.png`) is the lossless APNG: every frame decodes to the
1331
1461
  rendered frame byte for byte, so it is the exactness record. `idle-indexed.png`
@@ -1336,8 +1466,10 @@ entry, no dithering, filter None) — and is the small file to show. `idle.gif`
1336
1466
  print their palette error, per channel over R, G and B of every frame (and alpha's
1337
1467
  max for the APNG); a figure is a measurement of the file, not a bar. On the demo
1338
1468
  (the README's animation since issue #224's follow-up: `build --loop --lead-in 3`), with
1339
- the breath of issue #270 (2.5 % of neck -> hip, 4.80 px peak to peak): 4,446,263 B lossless,
1340
- 704,009 B indexed and 760,294 B GIF, both palette files at max 70, mean 0.631; on the
1469
+ the breath of issue #270 (2.5 % of neck -> hip, 4.80 px peak to peak): 4,449,233 B lossless,
1470
+ 703,909 B indexed and 760,058 B GIF, both palette files at max 71, mean 0.636 (rig-c 2.45.2
1471
+ and later, whose render no longer ends a frame on a physics step's tie; on rig-c 2.40.0,
1472
+ 4,446,263, 704,009 and 760,294 B at max 70, mean 0.631); on the
1341
1473
  40-frame idle of issue #252 (rig-c 2.40.0, the breath 2.5 % of the topwear's height, 5.92 px):
1342
1474
  4,514,756 B lossless, 725,845 B indexed and 784,074 B GIF, at the same error; on the 4 s idle before it
1343
1475
  (rig-c 2.33.0): 5,148,992, 727,486 and 781,526 B at max 69, mean 0.630 — the two share one quantiser, so their error is the same by
@@ -1354,7 +1486,7 @@ renders the idle played n + 1 times back to back (`idle_lead_in`, the build's ot
1354
1486
  animations kept so the viewport is fitted as for the idle frames), keeps the last
1355
1487
  period, drops its end frame (the next period's first) and prints how far that end frame
1356
1488
  is from the first kept one — the jump the loop will show. The settling is the rig's, not
1357
- a constant: on the demo's 4 s idle, `--lead-in 1` and `2` each still read max |d| 117, 305 px over 40, and `3` reads max 1, 0 px over 40; on its 40-frame idle (issue #252, a shorter period and a breath twice as far), `3` reads max 7, 0 px over 40. The idle
1489
+ a constant: on the demo's 4 s idle, `--lead-in 1` and `2` each still read max |d| 117, 305 px over 40, and `3` reads max 1, 0 px over 40; on its 40-frame idle (issue #252, a shorter period and a breath twice as far), `3` reads max 1, 0 px over 40 (max 7 before rig-c 2.45.2, whose render no longer ends a frame on a physics step's tie). The idle
1358
1490
  must repeat without an invented value — every track keyed from 0 to the duration, ending
1359
1491
  where it starts — or the build refuses it (`LOOP_LEAD_IN_IDLE`). `check` and its bars
1360
1492
  read `check/idle_frames/` as before; only the loop files move. On reference-demo with `--seam silhouette`
@@ -1773,6 +1905,8 @@ See-through with another seed.
1773
1905
  | `CONFIG_BONE_UNIQUE` | a bone or chain declared twice, or `root` declared | `bones` |
1774
1906
  | `CONFIG_PART_ATTACHED` | a plan part or a cut's piece with neither or both of a mesh and a region; a patch with a mesh, or with no region | `meshes` / `regions` |
1775
1907
  | `CONFIG_RIBS_MODE` | `meshes.<part>.ribs` on a lattice (`grid`) mesh: its vertices are its grid's corners, which no row can move (issue #188) | put the part in `contour` or `auto`, or remove `ribs` |
1908
+ | `CONFIG_INFLUENCES_MODE` | `meshes.<part>.influences` on an automatic (`auto`) mesh: that mode's cap and floor are `auto.influences`, which rig-c's reduction is handed (issue #284) | declare them in `auto.influences`, or remove `influences` |
1909
+ | `CONFIG_STRAY_MODE` | `meshes.<part>.stray` on a contour (`contour`) or automatic (`auto`) mesh: those modes state the largest island they may leave out in `contour.stray` and `auto.source.stray`, which their trace reads (issue #303) | declare it there, or remove `stray` |
1776
1910
  | `CONFIG_PATCH_BOX` | an `assemble.patches` box with `x1 <= x0` or `y1 <= y0` | that patch's `box` (`x1`, `y1` are exclusive) |
1777
1911
  | `CONFIG_AMPS_MATCH_CHAIN` | a chain track's `amps` is not one per link | that track's `amps` |
1778
1912
  | `CONFIG_PERIOD_DIVIDES_DURATION` | a period that is not a whole fraction of the idle — the loop could not close | that track's `period`, or `motion.duration` |
@@ -1852,6 +1986,7 @@ See-through with another seed.
1852
1986
  | `RIG_PART_PRESENT`, `RIG_PART_ATTACHED` | a `meshes`/`regions` key that is not in `parts.json`, a part with neither, or a `painting:` part with a mesh | the config and the plan must name the same parts; a patch is a region |
1853
1987
  | `RIG_PNG_PRESENT`, `RIG_PNG_MATCHES_BOX`, `RIG_PART_HAS_ART` | a part's PNG is missing, the wrong size, or has no art pixel | re-run assemble |
1854
1988
  | `RIG_NAME_RESOLVES`, `RIG_SEGMENT_DEFINED`, `RIG_CHAIN_POINTS` | a segment names an undeclared bone, a bone with no `tip` and no next link, or a chain too short to make a link | that bone's `tip`, or write the segment out as `[bone, [x0,y0], [x1,y1]]` |
1989
+ | `RIG_INFLUENCES_KEPT` | a mesh's declared `influences` leaves a vertex with no bone: every capped share is under 0.03 (at or under it, for the continuous floor). The largest of k shares is at least 1/k, so this needs 34 or more kept bones; the default cap of 4 never reaches it (issue #284). The detail names the first vertex, how many more, and its largest share | declare `maxInfluences` 33 or less, or name fewer bones in `segments` |
1855
1990
  | `RIG_LATTICE_ONE_LOOP` | the lattice over a part does not close into one outline even after the repair passes | that mesh's `grid` |
1856
1991
  | `RIG_CONTROL_NAME_FREE` | a keyed, mesh-weighted bone needs `<bone>_ctl` and that name is taken | rename the declared bone |
1857
1992
  | `RIG_TURN_NAME_FREE` | with `turn`: a twin (`turn_<part>_<bone>`), the control `turn` or a transform constraint `turn_<part>` the turn writes is a name the rig already holds | rename the declared bone or constraint |
package/docs/STABILITY.md CHANGED
@@ -124,7 +124,8 @@ part name, `[i]` an entry of a list):
124
124
  - **assemble.cuts[i].draw**, when an object — required: `before`
125
125
  - **bones[i]**, a single bone — required: `name`, `parent`, `at`; optional: `tip`
126
126
  - **bones[i]**, a chain — required: `chain`, `parent`, `points`, `tip`
127
- - **meshes.&lt;part&gt;** — exactly one of `grid`, `contour`, `auto`; required in every mode: `r`, `segments`; optional in every mode: `exponent`, `rule`; optional with `contour` or `auto`: `ribs`
127
+ - **meshes.&lt;part&gt;** — exactly one of `grid`, `contour`, `auto`; required in every mode: `r`, `segments`; optional in every mode: `exponent`, `rule`; optional with `contour` or `auto`: `ribs`; optional with `grid` or `contour`: `influences` (issue #284; beside `auto` it is refused, CONFIG_INFLUENCES_MODE); optional with `grid`: `stray` (issue #303; beside `contour` or `auto` it is refused, CONFIG_STRAY_MODE)
128
+ - **meshes.&lt;part&gt;.influences** — optional, at least one: `maxInfluences` (a whole number, 1 or more, or null for no cap), `floor` ("cut" or "continuous")
128
129
  - **meshes.&lt;part&gt;.ribs.&lt;chain&gt;** — required: `stations`
129
130
  - **meshes.&lt;part&gt;.contour** — required: `tolerance`, `margin`, `spacing`; optional: `maxEdge`, `budget`, `stray`, `regions`
130
131
  - **meshes.&lt;part&gt;.contour.regions[i]** — required: `name`, `shape`, `bone`, `spacing`, `band`, and `cx`, `cy`, `r` for a circle or `points` for a polygon
@@ -170,7 +171,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
170
171
  | --- | --- | --- |
171
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) |
172
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 |
173
- | `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`); 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`); `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) |
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`); 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) |
174
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`), `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 |
175
176
  | `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `turn` with `--full`/`--head`, and `notes` |
176
177
  | `basis.json` | `propose` | spec `spine-parts-basis/1` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/parts",
3
- "version": "1.10.0",
3
+ "version": "1.12.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": {
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "ag-psd": "^31.0.2",
63
- "rig-c": "^2.40.0"
63
+ "rig-c": "^2.47.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@esotericsoftware/spine-core": "4.3.13",
package/src/assemble.ts CHANGED
@@ -2654,6 +2654,14 @@ function fmt0(v: number): string {
2654
2654
  * no role. One both runs hold is proposed once, from the full run, and a note
2655
2655
  * says how to take the head run's too. No public example holds one, so no
2656
2656
  * proposal of theirs moves.
2657
+ *
2658
+ * ⚖️ This port's too (issue #308): every tag a layer of either run holds that
2659
+ * no plan row or extend entry takes is named once, with why, run by run
2660
+ * (`unplannedNotes`), after the uncovered bodies and before the plausibility
2661
+ * drops. The plan is not changed. [observed] On the four public examples the
2662
+ * plan, the extend entries and every earlier note are byte for byte as
2663
+ * before; each gains one such note per tag left out (demo 6, scarf 9,
2664
+ * reference-demo 7, reference-sample 9).
2657
2665
  */
2658
2666
  export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx = PROPOSE_MIN_PX): PlanProposal {
2659
2667
  const frame = checkGeometry(g, { full, head });
@@ -2778,8 +2786,83 @@ export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx =
2778
2786
  why,
2779
2787
  );
2780
2788
  const bodyNotes = left.map((u) => uncoveredNote(u, plan, minBody));
2781
- // The reference's notes, then the uncovered bodies, then the plausibility drops — a drop note is last, as it was.
2782
- return { plan, extend_below_crop: extend, notes: [...notes, ...bodyNotes, ...dropNotes] };
2789
+ const unplanned = unplannedNotes(runs, plan, extend, noted, figures, minPx, (run, tag) => (find(run, tag)?.opaqueRunPx ?? 0) * (run === 'head' ? headArea : fullArea), new Set(left.map((u) => u.tag)));
2790
+ // The reference's notes, then the uncovered bodies, then the layers in no row (issue #308), then the plausibility drops — a drop note is last, as it was.
2791
+ return { plan, extend_below_crop: extend, notes: [...notes, ...bodyNotes, ...unplanned, ...dropNotes] };
2792
+ }
2793
+
2794
+ /** What every `--propose-plan` note on a layer in no row says its pixel figures are. */
2795
+ export const UNPLANNED_PX = 'px: run pixels of alpha above 8 after ghost clean-up';
2796
+
2797
+ /**
2798
+ * One note per tag that a layer of either run holds and no plan row or
2799
+ * extend entry takes (issue #308), in the full run's draw order and then the
2800
+ * head run's, saying for each run why: it holds no such layer; the proposer
2801
+ * takes the tag from the other run only (`PROPOSE_HEAD_TAGS`); its layer is
2802
+ * empty, its opaque count below the floor (`PROPOSE_MIN_PX`); it crosses a
2803
+ * plausibility rule (that layer's own note, which follows, names its figures);
2804
+ * or, for a full-run `headwear` or `earwear`, the head run's was not below 0.8
2805
+ * of it in rig px. A tag every holding run of which a plausibility note
2806
+ * already names gets no second note. `nose`, which the proposer never plans
2807
+ * (the reference's rule), gets a note of its own: the counts, and the row that
2808
+ * lets `propose` read the head's pitch, the only reading in this package made
2809
+ * off a part tagged `nose` (`headFrameNotes`, `src/propose.ts`). The plan is
2810
+ * not changed.
2811
+ */
2812
+ function unplannedNotes(
2813
+ runs: Record<Run, RunLayer[]>,
2814
+ plan: readonly PlanEntry[],
2815
+ extend: readonly Extend[],
2816
+ noted: ReadonlySet<string>,
2817
+ figures: Record<Run, LayerFigures[]>,
2818
+ minPx: number,
2819
+ rigPx: (run: Run, tag: string) => number,
2820
+ bodies: ReadonlySet<string>,
2821
+ ): string[] {
2822
+ const inRow = new Set<string>([...plan.map((p) => p[2]), ...extend.map((e) => e.tag)]);
2823
+ const find = (run: Run, tag: string): RunLayer | undefined => runs[run].find((l) => l.tag === tag);
2824
+ const floor = minPx === PROPOSE_MIN_PX ? `the proposer's floor of ${minPx} (PROPOSE_MIN_PX, src/assemble.ts)` : `the floor of ${minPx} this proposal was given`;
2825
+ const out: string[] = [];
2826
+ const seen = new Set<string>();
2827
+ for (const { tag } of [...runs.full, ...runs.head]) {
2828
+ if (seen.has(tag) || inRow.has(tag)) continue;
2829
+ seen.add(tag);
2830
+ const held = (['full', 'head'] as const).filter((r) => find(r, tag) !== undefined);
2831
+ const body = bodies.has(tag) ? "; the full run's layer holds a body no planned part covers, named in its note above" : '';
2832
+ if (tag === 'nose') {
2833
+ const counts = (['full', 'head'] as const).map((r) => `${r} run ${find(r, tag)?.opaqueRunPx ?? 'no layer'}${find(r, tag) === undefined ? '' : ' px'}`).join(', ');
2834
+ const best = (['head', 'full'] as const).reduce((a, b) => ((find(b, tag)?.opaqueRunPx ?? 0) > (find(a, tag)?.opaqueRunPx ?? 0) ? b : a));
2835
+ const most = find(best, tag)?.opaqueRunPx ?? 0;
2836
+ const after = plan.some((p) => p[2] === 'face') ? ` after the row that takes "face"` : '';
2837
+ const how =
2838
+ most === 0
2839
+ ? 'neither run\'s layer holds a pixel, so there is no nose part to add'
2840
+ : `propose reads the head's pitch (where the nose sits between the eye midpoint and the mouth) only off a part tagged nose, and without one prints "not read: no part tagged nose"; to have it read, add ["nose", "${best}", "nose"] to assemble.plan${after} (the ${best} run's layer holds the more of it) and run assemble and propose again`;
2841
+ out.push(`nose: in no plan row — the proposer never plans nose (the reference implementation's rule, which this port keeps); ${counts} (${UNPLANNED_PX}); ${how}${body}`);
2842
+ continue;
2843
+ }
2844
+ if (held.every((r) => noted.has(`${r}:${tag}`))) continue;
2845
+ const reason = (run: Run): string => {
2846
+ const l = find(run, tag);
2847
+ if (l === undefined) return `${run} run: no layer`;
2848
+ const px = `${run} run: ${l.opaqueRunPx} px`;
2849
+ const headTag = PROPOSE_HEAD_TAGS.includes(tag);
2850
+ const fallback = tag === 'headwear' || tag === 'earwear';
2851
+ if (run === 'full' && headTag && !fallback) return `${px}, but the proposer takes ${tag} from the head run only (PROPOSE_HEAD_TAGS, src/assemble.ts)`;
2852
+ if (run === 'head' && !headTag && readAuthorTag(tag) === null) return `${px}, but the proposer takes ${tag} from the full run only (it is not one of PROPOSE_HEAD_TAGS, src/assemble.ts)`;
2853
+ if (l.opaqueRunPx < minPx) return `${px}, empty: below ${floor}`;
2854
+ if (noted.has(`${run}:${tag}`)) {
2855
+ const f = figures[run].find((x) => x.name === tag) as LayerFigures;
2856
+ return `${px}, crosses ${implausibleRules(f).join(', ')} (named with its figures in the note below)`;
2857
+ }
2858
+ if (run === 'full' && fallback) {
2859
+ return `${px}, but the proposer takes ${tag} from the full run only when the head run's is below 0.8 of it in rig px (head run ${fmt0(rigPx('head', tag))}, full run ${fmt0(rigPx('full', tag))})`;
2860
+ }
2861
+ throw new Error(`unplannedNotes: no rule left ${run}:${tag} out of the plan, yet no row takes it`);
2862
+ };
2863
+ out.push(`${tag}: in no plan row — ${(['full', 'head'] as const).map(reason).join('; ')} (${UNPLANNED_PX})${body}`);
2864
+ }
2865
+ return out;
2783
2866
  }
2784
2867
 
2785
2868
  // ---------------------------------------------------------------------------
package/src/build.ts CHANGED
@@ -834,7 +834,11 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
834
834
  const stray = c.strayIslands === 0 ? '' : ` left out ${c.strayIslands} island(s), ${c.strayPixels} px`;
835
835
  const regions = m.regions.map((rg) => ` region ${rg.name}->${rg.bone} reaches ${rg.reached} (${rg.whole} whole)`).join('');
836
836
  log(`${head}contour tol=${m.params.tolerance} margin=${m.params.margin} spacing=${m.params.spacing}${m.params.maxEdge === undefined ? '' : ` maxEdge=${m.params.maxEdge} (outline +${c.outlineCut ?? 0})`} ${infl} overshoot ${c.overshoot} min angle ${c.smallestAngle.value}${stray}${regions}`);
837
- } else log(`${head}grid=${m.grid} ${infl} loop passes ${rig.loopPasses[m.part]}`);
837
+ } else {
838
+ // issue #303: only beside a declared stray, so a lattice line without one reads as it always did.
839
+ const stray = m.stray_cleared === undefined ? '' : ` stray ${m.stray} left out ${m.stray_cleared.islands} island(s), ${m.stray_cleared.pixels} of ${m.stray_cleared.art_pixels} art px`;
840
+ log(`${head}grid=${m.grid} ${infl} loop passes ${rig.loopPasses[m.part]}${stray}`);
841
+ }
838
842
  }
839
843
  const regions = rig.rig.slots.length - rig.meshReport.length;
840
844
  const tracks = rig.motion.animations.idle.tracks;