rig-parts 1.75.0 → 1.76.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/docs/AUTHORING.md +108 -27
- package/package.json +1 -1
- package/src/assemble.ts +134 -15
- package/src/build.ts +5 -0
- package/src/parts.ts +68 -5
- package/src/provenance.ts +106 -0
package/docs/AUTHORING.md
CHANGED
|
@@ -116,10 +116,10 @@ read by no CPU stage).
|
|
|
116
116
|
| `assemble.rig_scale` | assemble — the rig is `trunc(w × rig_scale)` x `trunc(h × rig_scale)`, and every rig-px value after it (`bones`, `meshes`, `motion`, `patches`, `cuts`) is in that rig's pixels; `check --source` (`CHECK_SOURCE_SIZE`) and `compose` (`SCENE_RIG_SCALE_AGREES`) read it back off `parts.json` | authored; rig pixels per source pixel. 0.5 in both examples (a 1664x2432 painting makes an 832x1216 rig). **Keep the figure about 1,100 to 1,200 rig px tall** — the `H` of `propose`'s `mesh spacing:` note, the height of the parts' boxes together — so for another painting, about 1,150 over the figure's height in painting px: that is the size the proposer's spacing divisor was calibrated at (H 1106–1185, the `mesh spacing` note says so) and the size of the two examples `check`'s bars are derived from, and the proposer and `check` also hold fixed rig-px values that do not scale with it (the fringe's 10 px step, the trunk segments' 18, 20, 30 and 100 px, the 15 px wrist tolerance, the 10-row pendant floor, the seam bar's 50 rig px over 40). What else it trades, measured on `demo` with the README loop from a fresh config at 0.25, 0.5 and 1 (issue #345, rig-c 2.47.1): the same plan and the same ten chains; every bar but the one the uncorrected `demo` always fails (`bottomwear` `TIP_OVER_ROOT`, the README loop's `RL01`) passing at all three; the atlas page 418x606, 832x1205 and 1825x2047; the figure's mesh spacing 16, 32 and 64, so 830, 868 and 932 triangles; the breath 2.5 % of a torso that scales with it; and the proposed bones, scaled to 0.5, off the 0.5 proposal's by up to 38 px at 0.25 and 22.5 px at 1 (the hairpin's origin; the skirt roots 20 and 10.5) — the fixed-px values above. `propose` records it on the proposal as `proposed_rig_scale` (issue #354), and changing `assemble.rig_scale` after the rig sections are written is refused (`CONFIG_PROPOSED_RIG_SCALE`): without that refusal `demo`'s tracked config built at 0.25 gated green and `check` failed only `TIP_OVER_ROOT` on both sleeves (`propose --from-config` named 54 `LINT` lines, links off the art) — re-run `assemble` and `propose` at the new scale. Absent: `CONFIG_FIELD_PRESENT`; 0, negative or not a number: `CONFIG_FIELD_TYPE`; a rig under 1x1: `ASSEMBLE_RIG_SIZE` |
|
|
117
117
|
| `assemble.plan` | assemble; also the list every `meshes`/`regions` key must come from | **proposed** by `assemble --propose-plan`. `[part name, "full" \| "head", See-through tag]`, in draw order back to front. Names are free; roles come from the tag. A layer the author made and added to a run takes `author:<label>` in the tag's place, and has no role (§5, *A layer the author made*). When `generation.control.skeleton` is present and a full-run `handwear-r` or `handwear-l` layer merges sleeve, bare hand and overgarment drape into one layer — a skin-coloured hand cluster (`>= 40` run px, `GHOST_MIN_AREA`, measured `516..4,787` run px across the 9-standee corpus) lies within `0.35` forearm lengths of `<s>_wrist` and non-skin pixels reach more than `1.0x` the forearm length (`<s>_elbow -> <s>_wrist`) below `<s>_wrist` (issue #490; measured `-0.15x..0.45x` on the 6 clean standees vs `1.25x..2.02x` on the 3 merged standees, placing `1.0x` inside the `0.45x..1.25x` gap) — the layer is split into `drape-<s>`, `handwear-<s>` and `hand-<s>` (back to front) when the Euclidean RGB distance between the sleeve median colour above `<s>_wrist` and the drape median colour below `<s>_wrist` is at least `80` (`ARM_DRAPE_COLOR_DISTANCE`, measured `103.6..236.0` on the two distinct-colour merged standees vs `20.8..28.2` on the same-colour merged standee, placing `80` between `28.2` and `103.6`), or refused with `SEETHROUGH_ARM_RUN_MERGED` when the distance is under `80` |
|
|
118
118
|
| `assemble.extend_below_crop` | assemble | **proposed** with the plan: `{part, run, tag}` — a head-run part that reaches the bottom of the head crop is continued from a full-run layer, whole connected components |
|
|
119
|
-
| `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. `draw` sets assembled part order; an exclusively bound limb rider's final rig slot joins that limb's block (see the limbs subsection).
|
|
120
|
-
| `assemble.cuts` | assemble; `meshes`/`regions` keys may name the new parts too, and must (each is a part like any other) | **authored**, optional (issue #170): `[{from, into, polygon, draw, overlap, under, under_fill}]`, `under` and `under_fill` optional — a plan part's layer
|
|
119
|
+
| `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. `draw` sets assembled part order; an exclusively bound limb rider's final rig slot joins that limb's block (see the limbs subsection). Use [Source before scissors](#source-before-scissors) before a patch; §5 describes structural placement |
|
|
120
|
+
| `assemble.cuts` | assemble; `meshes`/`regions` keys may name the new parts too, and must (each is a part like any other) | **authored**, optional (issue #170): `[{from, into, polygon, draw, overlap, under, under_fill}]`, `under` and `under_fill` optional — a structural separation under [Source before scissors](#source-before-scissors), cut from a plan part's layer by the author's polygon: the pixels of plan part `from` whose **centre** lies inside `polygon` (even-odd; a centre exactly on an edge is outside) leave `from` and make the new part `into`, colour and alpha, so every pixel of the layer ends in exactly one of the two. `polygon` is `[[x, y], …]`, at least three points, non-negative integer **rig** pixels — the space of `parts.json` and of the patches' boxes. The file that shows the layer in those coordinates is `assemble`'s own `rig/parts/<from>.png`, placed at its `parts.json` `x`, `y` (the PNG's pixel `(i, j)` is rig pixel `(x + i, y + j)`), or `render/recomposite_rig.png`, the whole rig 1:1; the contact sheets (`sheet`) are scaled and carry no coordinates. `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`, as for a patch (to keep the setup pose, draw the piece immediately in front of `from` — `{"before": "<the plan part after from>"}`, or `"front"` when `from` is the last plan part; `{"before": "<from>"}` draws it behind `from`, which `under` refuses as `ASSEMBLE_UNDER_DRAW` because the fill would hide it). `from` must be a plan part (`CONFIG_NAME_RESOLVES`; a patch or another cut's piece is refused; a part a limb takes may be cut where no segment of the limb holds it, issue #564, §5 *Limbs*), `into` a new name (`CONFIG_PART_UNIQUE`), the polygon three or more integer points (`CONFIG_FIELD_TYPE`). The piece's `parts.json` record carries `from`'s `<run>:<tag>` and its own counts, each split from the layer's by where the pixel lies; it is a mesh or a region under its own name, and the coverage rule holds it like a plan part. What the config cannot know the stage refuses: a polygon point past the rig (`ASSEMBLE_CUT_INSIDE`), two cuts of one part sharing a pixel (`ASSEMBLE_CUT_OVERLAP`), a cut that takes none of the layer's opaque pixels or leaves `from` none (`ASSEMBLE_CUT_PIXELS`), a cut of a part a limb takes that takes a pixel a segment of the limb holds (`ASSEMBLE_CUT_CROSSES_LIMB`). `overlap` is required, a non-negative integer of rig pixels with no default (`CONFIG_FIELD_TYPE`): the **band** — the polygon's pixels within `overlap` px of its edge by Chebyshev distance (the polygon's mask less its erosion by a `2 * overlap + 1` square) — stays in `from` as well as going to the piece, so the piece's resampled edge has art under it; the counts stay a strict partition and the band is the base's extra copy, printed on the `cut:` line. At `0` the two abut and `check`'s seam bar judges the edge between them (`CHECK_SEAM_WITHIN_BAR`, §5). A band only covers the edge at rest: a piece that swings well clear of where it was painted shows the hole it leaves, and a thin piece's band shows a ghost of the piece. `under` (issue #193), optional, is the author's image of what lies under the piece: a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE` unless a non-empty string), in rig pixels over the polygon's **bounding box** — the least and greatest polygon x and y, so the image is `max x − min x` by `max y − min y` and its pixel `(i, j)` is rig pixel `(min x + i, min y + j)`. Where the cut removed pixels from `from` — the piece's pixels (alpha above 0) less the band — `from` takes the image's pixel, colour and alpha wherever the image's alpha is at least the alpha the cut removed there (**filled**); elsewhere `from` stays empty there (**left empty**): a transparent pixel is the author's statement that nothing lies under it (the background, or a part drawn behind), and a less opaque one is not half-filled. The image is read nowhere else, so its pixels over the band, over the box outside the polygon or where the layer had no art never change `from`. The `cut:` line then ends `; under <n> px filled, <m> px left empty` (`n + m` is every removed pixel), so an unintended hole is a number in the log, not a guess; the filled pixels count in `from`'s `opaque_px` and `occluded_px` (the piece is drawn over them), never as visible or projected. The stage refuses: a file that is not there (`ASSEMBLE_UNDER_PRESENT`) or not a PNG (`ASSEMBLE_UNDER_DECODES`), an image not exactly the box's size (`ASSEMBLE_UNDER_SIZE`, both sizes given), an image that fills none of the removed pixels — all transparent there, or another file (`ASSEMBLE_UNDER_EMPTY`; to leave the whole hole, drop `under`), and a piece drawn behind `from` (`ASSEMBLE_UNDER_DRAW`: the fill would hide it; draw it `"front"` or `{"before": "<the plan part after from>"}`). Producing the image is the author's, by whatever means; absent, `from` keeps only its band. `under_fill` (issue #360), optional, is the one value `"colour"` (`CONFIG_FIELD_TYPE` on any other): the same pixels are filled and left empty, and a filled pixel takes the image's **colour** at the alpha the cut removed there instead of the image's alpha, so `from`'s alpha is the uncut layer's wherever the image fills and its silhouette does not grow — an opaque image over a soft edge leaves the edge soft. It applies to the part's hidden-art layer as well as to a file; a cut with neither is refused (`ASSEMBLE_UNDER_FILL_SOURCE`). The `cut:` line's filled count then says how many took the colour only: `; under 190 px filled (190 colour only, at the alpha the cut removed), 0 px left empty`. Absent, a filled pixel takes the image's colour and alpha, as above |
|
|
121
121
|
| `assemble.alternates` | assemble, rig, check (`--set`, `--all-sets`) | **authored**, optional (issues #454, #465, #472, #473, #476): `[{name, source?, full?, head?, head_box?, parts?, hides?, cuts?, skeleton?, anchor?, base_anchor?, drawn?, tips?, front?}]` — alternate painting sets assembled into `<out>/rig/parts/<set>_<part>.png` (`parts.json` `alternates`), attached on each covered slot in `rig.json` as `<set>_<part>` beside the base attachment, and indexed in `<out>/rig/sets.json` (`rig-parts-sets/1`: `sets.<set>.slots`, `.hides`, `.drawn` when present, and `.threshold: {yaw: 15, pitch: 12}` when the set replaces `face` on a rig with `turn` or `turn`/`tilt` slider animations). `source`, `full` and `head` may instead be given on the CLI with `--alt <name>=<source>:<full>[:<head>]`. Windows absolute drive paths such as `C:\paint\painting.png` are accepted as written; each segment's leading drive colon belongs to that path, while other colons still separate `source`, `full` and `head`. `parts` lists the subset of parts the set replaces (default: every part not in `hides`); a **subset set** (issue #473, e.g. mouth visemes `a`, `i`, `u` with `"parts": ["mouth"]`) cuts only its covered parts from the alternate runs (refusing with `ASSEMBLE_ALT_INPUT_PRESENT` if a covered part's layer is missing or empty), writes only `{slot: "<set>_<slot>"}` in `sets.json` so `@rig-c/play`'s `setSet(name)` switches those slots while leaving every other slot on its base attachment (and `setSet(null)` restores the base pose), may be keyed on a slot attachment track in `motion.animations_from` as `{"t": …, "set": "<name>" \| null}` (`CONFIG_SET_COVERS_SLOT` if `"set"` is undeclared or does not cover the track's slot), and under `check --set <name>` counts `ANIMATIONS_DO_NOT_FOLD` only on its own covered slots; `hides` lists slots the set hides (`null` in `sets.json`). A **gesture subset set** (issue #476) covers a limb chain declared in `config.limbs.<limb>` (all of its segment `part`s; with `drawn` alone, cut at the base's joints posed to the set's own `drawn` angles by `poseBoneEntriesForDrawn`, each segment keeping the base's length; with `skeleton` (issue #539), cut at the skeleton's own joint positions by `placeBoneEntriesForSkeleton` — each covered bone the body-18 table names (`sleeve_*`, `arm_*0`/`arm_*1`, `leg_*0`/`leg_*1`) at its skeleton origin with the skeleton segment's own length and direction (`drawn` overriding the direction of a bone it names), and a covered bone the table does not name (a hand link `arm_r2`, a single `foot_r`) placed from its parent's new place — its base offset from the parent, stretched along the parent by the parent's length change and turned by its turn, so a link or a foot at the parent's end lands on the skeleton's end — with its base length and the drawn or base direction, so a foreshortened shin or forearm is cut where it is painted; naming the unsplit `from` layer, covering only some segments of the limb, or declaring polygon `cuts` on a limb set is refused with `CONFIG_LIMB_SET_JOINTS`) and carries `drawn` (or `skeleton`) for the covered bones only (giving `drawn` for an uncovered bone is refused with `CONFIG_SET_COVERS_BONE` / `RIG_SET_COVERS_BONE`; uncovered bones keep the base's `drawn`). A subset set's limb part takes no pixel that a base part drawn after it, which the set neither covers nor hides, covers at setup (issue #587): the set's attachment is drawn in the base slot's place, so that base part is drawn over it and what the set's painting shows there is that base part (shorts over a leg set's thigh top, which See-through synthesised in the set's leg layer); `assemble` clears those pixels — the base part's PNG at alpha above 8, at its `parts.json` x, y, on the shared canvas — from the set's part once its limbs are cut, before any count — its `merged_px` and its cap's record too (issue #603): a cap pixel it clears leaves the count it was in (`layer_px`, `image_px`, `upper_px`, `lower_px`, `lower_kept_px`) for the cap's `excluded_px`, written only when above 0, so every count is the written PNG's — and prints `LINT set "<set>" <part>: <n> px under <base part> excluded` per part and base part that took a pixel (n counts alpha above 8); a set that carries that base part itself (`"parts": ["thigh_l", "shin_l", "bottomwear"]`) excludes nothing and writes what it wrote before. A subset set that names one limb side of a layer more limbs take (issue #588: `"parts": ["thigh_r", "shin_r"]` where `leg_r` and `leg_l` both take the one `legs` layer) would put every pixel of that layer on its one limb, so each of its parts would hold a piece of both legs; when the set's layer holds more than one island (8-connected pixels with alpha above 8) of at least 1 % of its largest (issue #608: the ghost clean-up's own share; a smaller island is a speck, which goes with the island nearest it — the one holding the pixel centre nearest one of the speck's, none when two are equally near — and is never an island of its own), each segment part keeps the island its joints fall on — its bone's origin and the next segment's bone's origin, at the set's own bones, a joint on the island of a pixel whose square holds the point, and a joint on no island on the one island whose nearest pixel centre lies within the radius of the cap cut at that joint (`cap.radius`: an ankle just past the legwear's edge, on the shoe) — and drops the rest, once its limbs are cut and before the exclusion above, and `assemble` prints `LINT set "<set>" <part>: <layer part> holds <n> islands (8-connected, alpha above 8)[, and <s> speck(s) under 1 % of the largest (<f> px), each with the island nearest it], kept the one under <bone> [x, y] and <bone> [x, y][ (<e> px off it, within its cap's radius <r>)] (<k> px), dropped <m> (<d> px)` per part (k and d count the part's pixels above alpha 8, m the islands it lost one from, and `dropped none` when it lost none); a joint on no island with none or two within that radius, or with no cap cut at it (a limb's first joint), and a segment whose joints fall on two, are refused with `ASSEMBLE_ALT_LIMB_ISLAND` rather than guessed. A layer of one island — specks aside — may hold both legs over each other (a folded shin across the standing leg; issue #609): each of the set's segment parts then keeps the pixels nearer its own limb's segment line — the polyline of only its segments taking that source, their bone origins and the last segment's supplied tip or following segment's joint (issue #638) — than the other limb's, both in the set's pose, and gives the rest away, once its limbs are cut and before the exclusion above; a pixel whose two distances differ by 0.5 px or less (the overlap band: half a pixel, the reach by which a pixel centre is read as on a line) goes to the limb whose part there is drawn later in the base's draw order, and the `LINT set` line above counts what each part kept, gave and held in the band. The other limb stands where the set's `skeleton` places it — its keypoints place every limb bone of the body-18 table, the other leg's as well as the set's own (its `drawn` and `drawnLength` stay the covered bones') — and never at the base's place: a set that does not place it (a `drawn` set, which may not name a bone it does not cover, or a set with neither) is refused with `ASSEMBLE_ALT_LIMB_POSE` when its layer has art where the base draws that limb, and when it has none nothing says the layer holds it and the layer is cut as before; two limbs whose segment lines run closer than the limb's half-width over most of where their art can meet are refused with `ASSEMBLE_ALT_LIMB_OVERLAP`. A set that carries every limb of the layer is cut as before. For a subset set, where a limb's own source layer holds art (`alpha > 8`) under another layer of that run drawn in front (`alpha > 8`, by the projection's plan/depth order), the limb keeps its own layer pixels through projection, limb cutting, island/nearest-line assignment, base-footprint exclusion, fringe clearing and seam correction (issue #637). Those pixels are occluded, never counted as painting projection; no base footprint or painting colour replaces them. After source-art island/nearest-line assignment, a source-transparent cap pixel is assigned before protection, in both doors: its centre is compared against only the segments taking the recipient's source, at the set's supplied bones (ankle-to-toe for a separate foot source), with the existing 0.5 px overlap band and base draw order. A missing last endpoint is refused with `ASSEMBLE_ALT_LIMB_POSE`; an actual following joint may supply that endpoint, never a guessed point. An unposed other limb with base art beneath these cap pixels is likewise refused; without such overlap the existing compatible path retains the cap without inventing the other limb's pose. A pixel cleared by this assignment is never restored. `CAP_FROM_UPPER` records a copy made before assignment and alone proves no ownership: only a retained copy is protected beneath the upper segment, which is drawn over it. The source-art island selection itself is unchanged. Other retained cap pixels (from the layer or a supplied cap image) are protected under an actual front layer even where the lower source is transparent. Protection follows each assigned piece through exclusion, fringe clearing and seam correction; it never restores a pixel ownership removed. Cap pixels removed during ownership leave their original counts for `excluded_px`, just as excluded pixels do, so the cap ledger describes the written PNG. Target assignment reconciles this ledger even when there are no source-transparent cap candidates and when a direct caller does not request cover protection. Unprotected source pixels still obey the base-footprint exclusion. Missing limb/cap art is never extended or guessed. Base builds and uncovered, unexcluded sets keep their existing bytes. `skeleton` (a pose file path relative to the config's directory, `{"width": W, "height": H, "points": {…}}` with all 18 body-18 keypoints on the canvas, validated by the same rules as `generation.control.skeleton`: `CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON`, `CONFIG_FIELD_TYPE`, `CONFIG_KEY_KNOWN`, `CONFIG_KEY_UNIQUE`, `CONFIG_POSE_CANVAS`, `CONFIG_POSE_KEYPOINTS`, `CONFIG_POSE_IN_CANVAS`) or `drawn` (`{<bone>: <worldDegrees>}`, each bone resolved against `bones`, `CONFIG_NAME_RESOLVES`) declares the posed angles the alternate was painted at (issue #465); when `skeleton` is given, `assemble` derives `drawn` from its 18 keypoints (filtered to covered bones on a subset set, with `drawn` overriding any bone named in both), records both `skeleton` (scaled by `assemble.rig_scale` to rig px) and `drawn` in `parts.json` `alternates` — and, when the skeleton placed the set's limb bones (issue #539), `drawnLength` (`{<bone>: <ratio>}`, each covered table bone's skeleton segment length over its base length, 6 places) and `registration` (`{offset: [dx, dy], scale}`: the similarity without rotation, about the canvas origin, that maps the skeleton's torso — the mean of its shoulders `r_shoulder`/`l_shoulder` to the mean of its hips `r_hip`/`l_hip`, read where the base declares the bone rooted there, `sleeve_*0`/`arm_*0` and `leg_*0` — onto those base bones' origins, 6 places), both also written to `sets.json`; the set's painting must be registered, its torso within 1 rig px and 1 % in scale of the base's, or the set is refused by name before any joint is cut (`ASSEMBLE_ALT_SKELETON_REGISTERED`, with the offset and scale the painting would need) — and the `rig` stage writes `<set>_*` region and mesh attachments in the bone-local frame of `poseRigBonesForDrawn(bones, drawn)` (weighting limb segments directly from `skeleton`'s joint endpoints when `skeleton` is present) and emits `drawn` in `sets.json` so `rig-pose` and `check --set <name>` / `--all-sets` pose the bones at `drawn` without rotating the already-posed art a second time. `front` (issue #664) lists covered limb segment parts the set draws on their **front slot** instead of the base slot: each must be in the set's `parts` and not its `hides`, and listed in some `config.limbs.<limb>.front.parts` (`CONFIG_SET_FRONT`); the set then maps `<part>_front` (never `<part>`) to `<set>_<part>` in `parts.json` and `sets.json` and hides `<part>` (and `<part>_still`), its #587 exclusion counts only base parts drawn after the front slot's target, and keying that part's set on a slot track is refused (`CONFIG_SET_FRONT_KEYED`) — *Front slots (issue #664)* below `layers` optionally maps covered, nonhidden output part names to layer names in their existing source run (issue #666); for example `"layers": {"upperarm_r": "author:raised", "forearm_r": "author:raised"}`. Limb cuts, caps, islands and base exclusions read the selected layer; the base plan stays unchanged. |
|
|
122
|
-
| `assemble.head_face_underfill` | assemble, `assemble --propose-plan` | **authored**, optional (issue #492): `boolean` (`CONFIG_FIELD_TYPE` on any other). When both runs exist and `full:face` has at least `200` opaque run px (`PROPOSE_MIN_PX`), the stage maps `full:face` into the head canvas via `fullLayerToHead` and measures how much of its hole-filled silhouette (`alpha > 8`) is covered by `head:face` (`alpha > 8`). A head-run `face` covering less than `75 %` (`HEAD_FACE_COVERAGE_FLOOR`, measured `0.0 %..48.6 %` on hollow/omitted head runs vs `84.8 %..99.6 %` on clean head runs across the 4 public examples and 9-standee corpus) is refused with `SEETHROUGH_HEAD_RUN_HOLLOW` unless `head_face_underfill: true` is declared. When `head_face_underfill: true` is declared and `head:face` exists (`opaqueRunPx >= 200`), `fullLayerToHead(full:face)` is composited under `head:face` on the head canvas (`alpha = max(headAlpha, fullAlpha)`, RGB blended by `headAlpha / 255`) before `head:face` is placed in rig space (`underfill=<N>px(full:face)` on the `face` part log line and a `face: head_face_underfill composited …` note in `assemble --propose-plan`). The fill restores transparent pixels only and leaves the cut's own edge where it is — a hollow whose edge is not under hair stays visible
|
|
122
|
+
| `assemble.head_face_underfill` | assemble, `assemble --propose-plan` | **authored**, optional (issue #492): `boolean` (`CONFIG_FIELD_TYPE` on any other). When both runs exist and `full:face` has at least `200` opaque run px (`PROPOSE_MIN_PX`), the stage maps `full:face` into the head canvas via `fullLayerToHead` and measures how much of its hole-filled silhouette (`alpha > 8`) is covered by `head:face` (`alpha > 8`). A head-run `face` covering less than `75 %` (`HEAD_FACE_COVERAGE_FLOOR`, measured `0.0 %..48.6 %` on hollow/omitted head runs vs `84.8 %..99.6 %` on clean head runs across the 4 public examples and 9-standee corpus) is refused with `SEETHROUGH_HEAD_RUN_HOLLOW` unless `head_face_underfill: true` is declared. When `head_face_underfill: true` is declared and `head:face` exists (`opaqueRunPx >= 200`), `fullLayerToHead(full:face)` is composited under `head:face` on the head canvas (`alpha = max(headAlpha, fullAlpha)`, RGB blended by `headAlpha / 255`) before `head:face` is placed in rig space (`underfill=<N>px(full:face)` on the `face` part log line and a `face: head_face_underfill composited …` note in `assemble --propose-plan`). The fill restores transparent pixels only and leaves the cut's own edge where it is — a hollow whose edge is not under hair stays visible; follow [Source before scissors](#source-before-scissors) for its source remedy. When `head:face` has fewer than `200` opaque pixels, `SEETHROUGH_HEAD_RUN_HOLLOW` still fires even under `head_face_underfill: true` because the head run omitted the face layer and cannot supply a jawline |
|
|
123
123
|
| `assemble.yield_refused` | assemble | **authored**, optional boolean, default `false` (issue #819; other types: `CONFIG_FIELD_TYPE`). With `true`, only an original front projection-core pixel refused at max-channel drift > `DRIFT_LIMIT` (90) can yield: its original source pixel must still be opaque, and the actual top-most opaque holder below it (alpha >= `CORE_ALPHA`, 250), after earlier yields, must have colour within 90 of the painting. A deeper matching part cannot bypass an intervening opaque mismatch. This setting also applies to alternate sets. Its alpha becomes 0; RGB is not repainted. Merge/ring replacements are not original refused pixels. Front is relative to the matching part behind, so original refusals in another run are eligible even when a different part is the global top holder. The reading proceeds back to front against the current behind pixels; alpha changes do not repaint or create a new matching colour. `yieldReadings` records skipped merge/ring/nonopaque/no-match counts and original/final global holders; a yielded pixel also taken by projection closing is omitted from the written `source_px_taken` count. Existing cuts/patches and thresholds are unchanged. The `drift:` line reports `yielded N px` per affected part, even below the drift-body size bar; with the setting off it names `assemble.yield_refused: true` as the conditional remedy. Rebuild and inspect the newly exposed layer in motion before considering any cut; a mismatching or absent behind holder cannot be repaired by this setting. |
|
|
124
124
|
| `limbs` | assemble (the cut and the caps), rig (through `regions`), check (`LIMB_ONE_BONE`) | **proposed** by `propose --keypoints` (issue #349), then checked: `{<limb>: {segments: [{part, from, bone}, {part, from, bone, joint, range?, cap?, edge?, cap_shape?, cut_band?, hybrid?}, …], split?, layout?, front?}}`, the upper segment first. The rule is *Limbs: cut at their joints* below the table. `part` is a new part (`CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` against the plan, patches, cuts and every other limb part); `from` a plan part or a cut's piece (`CONFIG_NAME_RESOLVES`) — the layer the segment's pixels come from, which is cut into the limb parts and is not itself made: a `meshes` or `regions` entry for it is `CONFIG_NAME_RESOLVES`, and the segments of one limb that take one part are consecutive (`CONFIG_LIMB_FROM_RUN`); `bone` a declared bone, each segment's the child of the one before (`CONFIG_LIMB_BONES_CHAIN`); the upper segment names no `joint`, `range`, `cap`, `edge`, `cap_shape` or `cut_band` (`CONFIG_KEY_KNOWN`), every other names its `joint` (`CONFIG_FIELD_PRESENT`) — the joint is its bone's origin; `range` is `[min, max]` degrees from rest with `min <= 0 <= max` (`CONFIG_LIMB_RANGE`), which `assemble` writes into the lower part's `parts.json` record and `check` poses the joint at — its two ends and its middle — reading the seam there (`SEAM_OVER_RANGE`, issue #360; rig-c needs no rotation limit); `cap` a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE`); `edge` `"feather"` (issue #360), the one value it takes, and `cap_shape` `"disc"` (issue #360) or `"ball"` (issue #579) (`CONFIG_FIELD_TYPE` on any other) — the cut's geometry at that joint, *Limbs: cut at their joints* below; absent, the cut is hard and the cap the layer's half-disc of half the limb's width; `cut_band` a whole number of px, 1 or more (`CONFIG_FIELD_TYPE`), on a segment that takes the segment above's part — between two layers nothing is cut, so a band there is `CONFIG_KEY_KNOWN` — places the cut within that many px of the joint line (issue #360, *Limbs: cut at their joints*); absent, the cut is the joint line. Each limb part is attached like any part — `regions.<part>`, which must be the segment's `bone` (`CONFIG_LIMB_BONE`), or a mesh, which `check` holds to that bone alone (`LIMB_ONE_BONE`). With `limbs`, `assemble` reads `bones` too (`CONFIG_FIELD_PRESENT` without it). Absent, nothing is cut at a joint and no byte of any output moves. Optional `front` (issue #664), `{parts: [<own segment part>, …], after: "<produced part>"}`, gives each named segment a second slot `<part>_front`, empty at setup, placed after `after`'s slot family, for a gesture set to draw on (`CONFIG_FRONT_SCOPE`, `CONFIG_FRONT_NAME`, `CONFIG_FRONT_LAYOUT`; *Front slots (issue #664)* below) |
|
|
125
125
|
| `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip, link_inherit?}` whose links are named `<chain>0 … <chain>n`. Parents come before children. In `rig.json` each link is **turned along its chain** — `rotation` is the direction from its origin to the next link's (the last link's to `tip`) and `length` that distance (issue #73), so a physics constraint added downstream finds a lever; under `--idle-keys ctl` the link's `<link>_ctl` carries the turn and the length and the link sits at local rotation 0 beneath it. A single bone with a `tip` is turned the same way, towards its tip, with that distance as its length (issue #239); a tip on the bone's own origin is refused (`RIG_TIP_DISTINCT`). The five face bones `eye_r`, `eye_l`, `brow_r`, `brow_l` and `mouth` (by name, when one has no tip of its own) are turned to the eye line — the line through the origins of `eye_r` and `eye_l` as `bones` writes them, read image-left to image-right, drawn when parts.json holds both an `eyewhite-r` and an `eyewhite-l` part — so the blink's `scaley` squashes across the eye line on a tilted head; move an eye bone and the line moves with it. With one eyewhite or none, or an eye bone missing, the rig stage's `face axis:` line says so and they keep the page's axes. A bone under a face bone keeps the face's axis. With the line, `rig.json` also declares `invariants.axes` — one entry, `"face"`, through `eye_r` and `eye_l`, holding the turned face bones (and their controls) — so rig-c's gate holds the build to the line; the line is the one rigc draws through the same two bones, so the tolerance is only the float floor — the largest gap the emitted (float32) bones have to it, rounded up to 6 places plus one 6-place unit — and the `face axis:` line prints it. With the line the rig stage also adds `head_up` (issue #250): a slotless bone under `head`, at head's origin, turned to the eye line + 90 degrees and as long as the eye midpoint lies from head's origin along that axis — the head's frame, which a rule that needs it reads; `head` keeps its own frame, so no key on it or under it changes meaning, and a config bone of that name is refused (`RIG_HEAD_UP_NAME_FREE`). A face bone with its own `tip` is held too (issue #258), in a second entry, `"face_tips"`, when its tip lies along the line within whole-pixel rounding — a tip written in whole pixels sits at most sqrt(2)/2 px off the point of the line it stands for, so its direction may sit up to asin((sqrt(2)/2) / length) degrees off — with its tolerance measured the same way; a tip further off is the bone's own axis, and the `face axis:` line names it with its gap and that bound. A failure is rig-c's `A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` line, which names the bone and the fix (rig-c's `docs/AUTHORING.md` §3.7 and §5.2); with no line no entry is written and A51 reports SKIP. Every region on a turned bone is turned back, so the setup pose does not move. Any other single bone keeps world rotation 0; one whose parent is a link is turned back upright. A single bone may declare `inherit` (issue #348): what it takes from its parent's transform, one of rig-c's own mode names exactly as rig-c's table spells them — `normal`, `onlyTranslation`, `noRotationOrReflection`, `noScale`, `noScaleOrReflection` (rig-c's `RigBone.inherit`) — written to that bone in `rig.json` as declared; any other spelling, `"NoScale"` included (the runtime would fold its first letter; nothing here folds or maps), is refused (`CONFIG_BONE_INHERIT`), and a chain takes no `inherit` (`CONFIG_KEY_KNOWN`). A chain may instead declare `link_inherit` (issue #369), an object mapping its own expanded link names to those same modes, for example `"link_inherit": {"leg_r1": "noScale"}`. Only the named link changes; omitted links inherit normally. A key outside this chain is `CONFIG_NAME_RESOLVES`, with this chain's valid link names in the detail; a non-object or non-string mode is `CONFIG_FIELD_TYPE`, and another mode spelling is `CONFIG_BONE_INHERIT`. The mode is forwarded to the expanded link, or to its control under `--idle-keys ctl`, by the same rule as a single bone. This is an authored opt-in: `propose` does not add it. Its setup rotation is the turn from where rig-c's own poser puts the bone with no rotation of its own under that mode, so it still stands upright (or along its tip) at the setup pose. Where the idle gives the bone a `<bone>_ctl`, the mode goes on the control: the control is the bone the parent's rotation reaches, and the bone under it inherits normally, so the idle's keys moved onto the control still turn it. A foot that does not take its shin's rotation (`noRotationOrReflection` or `onlyTranslation`) keeps its shoe level while the leg is posed (`FEET_STAY_LEVEL`, §7); which mode is the author's. `noScale` keeps a shin or shoe's shape under a scaled parent but still takes the parent's rotation: a shoe that must also stay level needs a separate rotation constraint, for example a transform constraint from an unrotated bone. This does not change the proposed feet's existing inheritance or `FEET_STAY_LEVEL` default. An automatic mesh weighted through a bone with a mode other than `normal` gets no derived amplitude (its row names the bone) and no residual envelope (rig-c's `SKINNING_RANGE_UNSUPPORTED`). Absent, `rig.json` carries no `inherit` and no byte moves |
|
|
@@ -547,8 +547,8 @@ examples the four lashes reach 17-26 % (4-6 px, the lash line itself) and the pa
|
|
|
547
547
|
1.00 and 1.04 apart, so neither is noted. When such a lash has a row between its top and
|
|
548
548
|
the eyewhite's top with no art across its whole width, the lowest such row is proposed as
|
|
549
549
|
`motion.blink.still` on `head`, and the rows above it stop blinking; when it has none, the
|
|
550
|
-
note says so and nothing is cut —
|
|
551
|
-
|
|
550
|
+
note says so and nothing is cut — choose another source layer/run using
|
|
551
|
+
[Source before scissors](#source-before-scissors), or leave the crease to blink. The cut has to be a clear row: two pieces cut through
|
|
552
552
|
art are each resampled against their own transparent edge, and on the reference-demo example a cut
|
|
553
553
|
through its lash line changed 36 px of the setup-pose render (up to 7 levels, at render
|
|
554
554
|
scale 0.942) and up to 21 levels in the idle frames where the head rolls; a clear-row cut
|
|
@@ -3331,8 +3331,9 @@ proposing a cut for a defect.
|
|
|
3331
3331
|
### The correction loop (issue #359)
|
|
3332
3332
|
|
|
3333
3333
|
A parse can be wrong: a patch painted into the wrong layer, two layers that should
|
|
3334
|
-
be one, a split along a painted edge.
|
|
3335
|
-
|
|
3334
|
+
be one, a split along a painted edge. Start with
|
|
3335
|
+
[Source before scissors](#source-before-scissors). The wrapper export below is
|
|
3336
|
+
an inspection and structural-authoring interface:
|
|
3336
3337
|
|
|
3337
3338
|
1. **Export.** `rig-parts layers --export work art.psd` writes `work/layers.json`
|
|
3338
3339
|
and one PNG per layer (`<index>_<name>.png`), drawn layers in draw order, then
|
|
@@ -3349,12 +3350,12 @@ on the wrapper form instead, which any image library edits:
|
|
|
3349
3350
|
and `seam=` counts on the part drawn over it when it is not: projection and the
|
|
3350
3351
|
seam override repaint that part to the painting's colour, so the error map
|
|
3351
3352
|
stays clean while the part carries the wrong art.
|
|
3352
|
-
3. **
|
|
3353
|
-
|
|
3353
|
+
3. **Choose the source remedy** from the ladder above. For a structural piece
|
|
3354
|
+
that needs an author layer, keep its PNG and `layers.json` box together (`LAYERS_PNG_MATCHES_BBOX`
|
|
3354
3355
|
otherwise).
|
|
3355
3356
|
4. **Re-run** from step 2 on `work`.
|
|
3356
3357
|
|
|
3357
|
-
**A
|
|
3358
|
+
**A historical reader control**, not a repair prescription (the selftest's, `fixtures/artist.ts`): a 128-px file whose
|
|
3358
3359
|
right eyewhite was painted half into `back hair` — the 8 x 10 patch at
|
|
3359
3360
|
`[52, 22, 60, 32]`, near-white, behind the face. Assembled from the export:
|
|
3360
3361
|
|
|
@@ -3364,7 +3365,7 @@ recomposite vs source: mean |d|=0.40, within 8: 99.5%, error px > 40: 80, uncove
|
|
|
3364
3365
|
|
|
3365
3366
|
80 covered error pixels, blue on the error map, in one 8 x 10 block; the face's
|
|
3366
3367
|
part line says `drift=60`, the block's pixels inside the face's projection core
|
|
3367
|
-
whose colour is more than 90 off the painting's. The
|
|
3368
|
+
whose colour is more than 90 off the painting's. The historical pixel-transfer control:
|
|
3368
3369
|
in `work/00_back_hair.png` clear the block (it is `back hair`'s pixels
|
|
3369
3370
|
`(20..27, 18..27)`, box `[32, 4, 96, 48]`); widen `work/03_eyewhite-r.png` from
|
|
3370
3371
|
8 x 10 to 16 x 10, painting the block into its right half; and set that layer's
|
|
@@ -3423,8 +3424,8 @@ existing hardlinks are refused before artifact output; the ordinary mode names
|
|
|
3423
3424
|
| --- | --- | --- | --- |
|
|
3424
3425
|
| `layers` | the table, then one `author-made: layer "author:<label>" — …` line per layer the author made (group `author` in the table; §5, *A layer the author made*), then the `WARN` lines | every tag the plan will need has opaque pixels; one `face` in the head run; `0 WARN line(s)` | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`); a `WARN PLAN_LAYER_…` line (§6, *Plausibility*) |
|
|
3425
3426
|
| `sheet` of both runs | the tile list (and the sheet, if you can see) | eyes, irises, lashes and brows as left/right pairs in the head run | a head box that cut off an ornament: move `head_box`, re-run the head crop |
|
|
3426
|
-
| `assemble` | one line per part — its counts (`op=` opaque, `vis=` visible, `src=` taken / candidates, `unproj=` visible but not projected, `drift=<n> (<p>% of core)`, `merged=`, `seam=`; *parts.json*, below). `drift=` is `refused_drift_px` in rig px and its share of the part's candidates, `projected_core_px`, which it is a subset of (issue #314); no bar reads either. On the four public examples the share runs to 94.5 % (`scarf` `hair_back`, 479 of 507) and the count to 26,472 (`scarf` `topwear`, 22.3 %), and the seam override repaints refused top-most pixels afterwards, so a large count says where See-through's colour was not taken, not that the part is unlike the painting: the `drift:` line below is the one that names a body of them another part holds. Then the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…)[, "<holder>" drawn in front of "<part>"][; the painting is within 90 of "<behind>" (<px> px at alpha >= 250, drawn behind "<part>")]; informational: no gate reads this. Swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do): swap, rebuild and read this line again; if it moves, removing it needs different input layers. At rest, the seam pass repaints the holder's pixels that pass its rule from the painting` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order or that a part drawn behind it matches within 90 of the painting (issues #255, #351, #758) — that layer is not what the painting shows there; no gate reads this line, swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do) — swap, rebuild and read this line again, and if it moves, removing it needs different input layers — and at rest the seam pass repaints the holder's pixels that pass its rule from the painting (none on the five public paintings), then, per subset alternate set, one `LINT set "<set>" <part>: <layer part> holds <n> islands (8-connected, alpha above 8)[, and <s> speck(s) under 1 % of the largest (<f> px), each with the island nearest it], kept the one under <joints> (<k> px), dropped <m> (<d> px)` line per limb part cut from a layer of more than one island when the set names one limb side of it (issue #588; issue #608: a joint taken to the nearest island reads `<bone> [x, y] (<e> px off it, within its cap's radius <r>)`, and a part that lost no pixel `dropped none`), then one `LINT set "<set>" <part>: <layer part> holds <limb> and <other limb> in one island (8-connected, alpha above 8), split by the nearest segment line at the set's bones: kept <k> px (<share>), gave <m> px (<share>) to <other limb>; <b> px in the overlap band (nearer one line than the other by 0.5 px or less) went to the limb drawn in front, <bk> kept and <bg> given` line per limb part cut from a layer of one island that a limb the set does not carry also takes (issue #609; k, m and b count the part's pixels above alpha 8, each share of the part's pixels before the split, 3 places) and one `LINT set "<set>" <part>: <n> px under <base part> excluded` line per subset alternate set's limb part and base part drawn over it that the set does not replace which took a pixel (issue #587, `assemble.alternates` above; none on the public examples, which declare no alternate set); and look at `render/recomposite_error_rig.png`. The opt-in `assemble.yield_refused` adds `yielded N px` to each affected part's drift line (including a yielded refusal smaller than the body bar); with it off the line names `assemble.yield_refused: true` as the conditional config remedy before any cut. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then — with `config.limbs` (issue #349) — one `limb cut: "<part>" opaque <n> = "<segment part>" <px> + …` line per part a limb takes and one `limb cap:` line per joint, its cap's pixels from the cap image, from the layer and missing, and its box (under `cap_shape: "ball"`, issue #579, then the lower half's pixels written, left as painted and missing), followed on a short cap by one `LINT "<part>" cap at [x, y] … short_px left <n>, right <m>: no layer gives them …` line (issue #557) — from the upper segment where it gave a pixel (under `cap_shape: "disc"` always), under `edge: "feather"` the pixels faded, under `cut_band` the line the cut was placed on and the stroke pixels it and the joint line cross (issue #360) (§3, *Limbs: cut at their joints*), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `reference-sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `reference-demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (reference-demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where.
|
|
3427
|
-
| `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 <bone> under <parent>: amplitude <a> below the link above's <b> in the keyed chain …` — under `--cloth`, a keyed sibling the recipe left the only keyed child of its parent, which `CHAIN_LAG` would refuse (issue #572, §3 *A keyed sibling left behind*): raise the field it names to `<b>` or more in size; `LINT hairback_… on mesh '<part>' (side <r\|l>): <N> tree(s) for <M> visible hair mass(es) (mass 0 columns x=<minX>..<maxX>, y=<minY>..<maxY>; …)` — back-hair cloth trees whose count does not match the side's visible hair masses (`max(1, floor(w / (4 * grid)) + 1)` per mass) or whose link or tip leaves its mass's column range (issues #445, #461): omit `chains` on `cloth."back hair"` or draft trees per visible hair mass inside its columns; `LINT <a_limb> and <b_limb> on <part> (<from>) cross at [x, y] with no per-limb split: …` — two limbs sharing one layer cross in an X without a per-limb split (issue #446): declare `config.limbs.<covering>.split` (`{ "line": [[x, y], …] }` or `{ "edge": "painting" }`, §3 *Which limb*); `LINT foot_<s> at [x, y], the <s>_ankle joint and leg_<s>1's tip, lies on none of leg_<s>'s art within 1 px along its joint line (the nearest of it on that line is the pixel [x, y], D px from the joint): …` — an ankle keypoint off its leg's art on the line `assemble` sizes the ankle's cap by (issue #517): `propose` drafts every leg without its ankle cut, the shoe layer stays one part, and nothing is moved onto the art; move that ankle keypoint onto the leg's or the shoe's art and re-run `propose` (`(the line crosses none of it)` when no art lies on that line at all); `LINT stop on <chain> (<firstLink> at [x0, y0]) against <against> (at [ax, ay]) places <chain>_touch at [tx, ty], above <firstLink>'s root (rest R deg, \|rest\| + outward B deg): …` — a stop's contact point `<chain>_touch` lies above `links[0]`'s root in rig px or pushes `\|rest\| + outward` past `180°` (issue #447, §2 `stops`): stop against the descending limb bone below `firstLink`, set `links` to start at a link below `<chain>_touch`, or omit the stop on a rising limb; `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 —
|
|
3427
|
+
| `assemble` | one line per part — its counts (`op=` opaque, `vis=` visible, `src=` taken / candidates, `unproj=` visible but not projected, `drift=<n> (<p>% of core)`, `merged=`, `seam=`; *parts.json*, below). `drift=` is `refused_drift_px` in rig px and its share of the part's candidates, `projected_core_px`, which it is a subset of (issue #314); no bar reads either. On the four public examples the share runs to 94.5 % (`scarf` `hair_back`, 479 of 507) and the count to 26,472 (`scarf` `topwear`, 22.3 %), and the seam override repaints refused top-most pixels afterwards, so a large count says where See-through's colour was not taken, not that the part is unlike the painting: the `drift:` line below is the one that names a body of them another part holds. Then the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…)[, "<holder>" drawn in front of "<part>"][; the painting is within 90 of "<behind>" (<px> px at alpha >= 250, drawn behind "<part>")]; informational: no gate reads this. Swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do): swap, rebuild and read this line again; if it moves, removing it needs different input layers. At rest, the seam pass repaints the holder's pixels that pass its rule from the painting` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order or that a part drawn behind it matches within 90 of the painting (issues #255, #351, #758) — that layer is not what the painting shows there; no gate reads this line, swapping the plan order of the parts named here can clear it (when only the front one differs from the painting) or move it to the other part (when both do) — swap, rebuild and read this line again, and if it moves, removing it needs different input layers — and at rest the seam pass repaints the holder's pixels that pass its rule from the painting (none on the five public paintings), then, per subset alternate set, one `LINT set "<set>" <part>: <layer part> holds <n> islands (8-connected, alpha above 8)[, and <s> speck(s) under 1 % of the largest (<f> px), each with the island nearest it], kept the one under <joints> (<k> px), dropped <m> (<d> px)` line per limb part cut from a layer of more than one island when the set names one limb side of it (issue #588; issue #608: a joint taken to the nearest island reads `<bone> [x, y] (<e> px off it, within its cap's radius <r>)`, and a part that lost no pixel `dropped none`), then one `LINT set "<set>" <part>: <layer part> holds <limb> and <other limb> in one island (8-connected, alpha above 8), split by the nearest segment line at the set's bones: kept <k> px (<share>), gave <m> px (<share>) to <other limb>; <b> px in the overlap band (nearer one line than the other by 0.5 px or less) went to the limb drawn in front, <bk> kept and <bg> given` line per limb part cut from a layer of one island that a limb the set does not carry also takes (issue #609; k, m and b count the part's pixels above alpha 8, each share of the part's pixels before the split, 3 places) and one `LINT set "<set>" <part>: <n> px under <base part> excluded` line per subset alternate set's limb part and base part drawn over it that the set does not replace which took a pixel (issue #587, `assemble.alternates` above; none on the public examples, which declare no alternate set); and look at `render/recomposite_error_rig.png`. The opt-in `assemble.yield_refused` adds `yielded N px` to each affected part's drift line (including a yielded refusal smaller than the body bar); with it off the line names `assemble.yield_refused: true` as the conditional config remedy before any cut. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then — with `config.limbs` (issue #349) — one `limb cut: "<part>" opaque <n> = "<segment part>" <px> + …` line per part a limb takes and one `limb cap:` line per joint, its cap's pixels from the cap image, from the layer and missing, and its box (under `cap_shape: "ball"`, issue #579, then the lower half's pixels written, left as painted and missing), followed on a short cap by one `LINT "<part>" cap at [x, y] … short_px left <n>, right <m>: no layer gives them …` line (issue #557) — from the upper segment where it gave a pixel (under `cap_shape: "disc"` always), under `edge: "feather"` the pixels faded, under `cut_band` the line the cut was placed on and the stroke pixels it and the joint line cross (issue #360) (§3, *Limbs: cut at their joints*), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `reference-sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `reference-demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (reference-demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. Use [Source before scissors](#source-before-scissors) for the source ladder; a missing independently moving structural piece can use a painting patch (below) |
|
|
3428
|
+
| `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 <bone> under <parent>: amplitude <a> below the link above's <b> in the keyed chain …` — under `--cloth`, a keyed sibling the recipe left the only keyed child of its parent, which `CHAIN_LAG` would refuse (issue #572, §3 *A keyed sibling left behind*): raise the field it names to `<b>` or more in size; `LINT hairback_… on mesh '<part>' (side <r\|l>): <N> tree(s) for <M> visible hair mass(es) (mass 0 columns x=<minX>..<maxX>, y=<minY>..<maxY>; …)` — back-hair cloth trees whose count does not match the side's visible hair masses (`max(1, floor(w / (4 * grid)) + 1)` per mass) or whose link or tip leaves its mass's column range (issues #445, #461): omit `chains` on `cloth."back hair"` or draft trees per visible hair mass inside its columns; `LINT <a_limb> and <b_limb> on <part> (<from>) cross at [x, y] with no per-limb split: …` — two limbs sharing one layer cross in an X without a per-limb split (issue #446): declare `config.limbs.<covering>.split` (`{ "line": [[x, y], …] }` or `{ "edge": "painting" }`, §3 *Which limb*); `LINT foot_<s> at [x, y], the <s>_ankle joint and leg_<s>1's tip, lies on none of leg_<s>'s art within 1 px along its joint line (the nearest of it on that line is the pixel [x, y], D px from the joint): …` — an ankle keypoint off its leg's art on the line `assemble` sizes the ankle's cap by (issue #517): `propose` drafts every leg without its ankle cut, the shoe layer stays one part, and nothing is moved onto the art; move that ankle keypoint onto the leg's or the shoe's art and re-run `propose` (`(the line crosses none of it)` when no art lies on that line at all); `LINT stop on <chain> (<firstLink> at [x0, y0]) against <against> (at [ax, ay]) places <chain>_touch at [tx, ty], above <firstLink>'s root (rest R deg, \|rest\| + outward B deg): …` — a stop's contact point `<chain>_touch` lies above `links[0]`'s root in rig px or pushes `\|rest\| + outward` past `180°` (issue #447, §2 `stops`): stop against the descending limb bone below `firstLink`, set `links` to start at a link below `<chain>_touch`, or omit the stop on a rising limb; `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 — use another layer/run first; when no separate layer exists, a structural `assemble.cuts` entry separates the independently moving arms ([Source before scissors](#source-before-scissors)); `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 |
|
|
3428
3429
|
| `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 |
|
|
3429
3430
|
| `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` |
|
|
3430
3431
|
| `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), then — where a `footwear` part rides a bone that hangs from another — `FEET_STAY_LEVEL` and one `foot` line per such part (issue #348), then — when `parts.json` names a `handwear` part and there is an idle — `STRESS_OFF_CHAIN` (issue #347), then — where `parts.json` holds a limb segment's part — `LIMB_ONE_BONE` and one `limb` line per such part (issue #349), then — where a limb record carries a `range` — `SEAM_OVER_RANGE` and one `joint` line per joint and angle (issue #360), then `SEAM_OVER_MOTION` — a SKIP naming why on a rig that declares no neighbour pair, else the pairs read in every animation; existing internal-joint and explicit-seam pairs retain their `WARN seam` policy (issue #493), while proven body-root tears print `FAIL seam` and fail a counted bar (issue #686), then `CLOTH_SETTLES` — a SKIP naming why on a rig with no physics constraint, else the chains read and one `WARN chain` line per chain whose tip comes to rest before the idle's loop length (issue #501, a warning and never a bar), `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, one more each with the feet-level bar, the stress bar, the limb bar, the range bar and, where proven body-root pairs exist, the body-root seam bar — and names the ones that said SKIP, then — where `SEAM_OVER_MOTION` read a pair — how many warnings it printed, `; N warning(s) (SEAM_OVER_MOTION, not a bar)` — with `CLOTH_SETTLES` named beside it, and N their sum, where the rig declares a physics constraint — which no bar counts | `check: PASS; 11 of 11 bar(s) measured, 0 skipped` on `demo` and `scarf`, whose sleeves each ride one chain; `check: PASS; 10 of 11 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN)` on `reference-demo` and `reference-sample`, whose one `sleeves` mesh rides both sleeve chains (issue #347) — no example writes `FEET_STAY_LEVEL`, every shoe riding `root`; 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); `CHECK_STRESS_OFF_CHAIN` — a sleeve weighted off its chain (§6); `CHECK_LIMB_ONE_BONE` — a limb part on a second bone (§6); `CHECK_SEAM_OVER_RANGE` — a joint that opens when bent (§6); `CHECK_SEAM_OVER_MOTION` — a proven body-root seam opens over the existing floor (§6) |
|
|
@@ -3654,9 +3655,88 @@ run on a `direct` build (it has no rig spec to read) refuses A15 once per keyed
|
|
|
3654
3655
|
bone. No stage runs `validate`: the gate is `rigc build`, which reads the rig spec
|
|
3655
3656
|
and its declaration, and is green on both (`RG19`).
|
|
3656
3657
|
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3658
|
+
### Source before scissors
|
|
3659
|
+
|
|
3660
|
+
**Scissors are for structure, never for repair.** `cuts`, `patches` and
|
|
3661
|
+
`author:<label>` layers separate something that must move independently and
|
|
3662
|
+
that See-through has no layer for: a joint, an accessory, or a rear piece that
|
|
3663
|
+
changes draw order. A tear, ghost, smudge, stray outline or seam is a source
|
|
3664
|
+
problem. Follow this repair ladder, recording what each step measured:
|
|
3665
|
+
|
|
3666
|
+
1. Choose another See-through layer or run (`full` versus `head`, another tag,
|
|
3667
|
+
or `alternates`).
|
|
3668
|
+
2. Re-run See-through with a tighter resolution or crop.
|
|
3669
|
+
3. Correct the declared depth or role in config.
|
|
3670
|
+
|
|
3671
|
+
A cut is appropriate for an independently moving accessory still inside its
|
|
3672
|
+
parent's layer, two independently moving limbs in one layer, or a rear piece
|
|
3673
|
+
that needs a separate draw position. A patch or author layer can supply such a
|
|
3674
|
+
structural piece when no run provides it. These are authoring decisions, not
|
|
3675
|
+
ways to hide a defect under another part. A seam finding alone does not justify
|
|
3676
|
+
moving a polygon. Existing edits remain legal: accounting is a report, with no
|
|
3677
|
+
new cut/patch arithmetic, visual threshold or acceptance gate.
|
|
3678
|
+
|
|
3679
|
+
Assembly reports **two independent axes**. Unchanged colour does not mean
|
|
3680
|
+
unedited structure.
|
|
3681
|
+
|
|
3682
|
+
**Value axis.** `provenance:` names each output part; `provenance total:` sums
|
|
3683
|
+
its figure, with separate lines/totals for alternate sets. Each part record's
|
|
3684
|
+
`provenance` contains `unchanged_layer_px`, `hand_px`, `automatic_px`,
|
|
3685
|
+
`edits: [{edit, px}]` and `automatic: [{stage, px}]`. The exclusive partition is
|
|
3686
|
+
`unchanged_layer_px + hand_px + automatic_px = opaque_px`; edit subtotals equal
|
|
3687
|
+
`hand_px`, automatic subtotals equal `automatic_px`. Only final alpha above 8
|
|
3688
|
+
counts, after `seamOverride`. Copies in separate output images count separately;
|
|
3689
|
+
the figure total is not a union silhouette.
|
|
3690
|
+
|
|
3691
|
+
Compare final RGBA first with the immutable original See-through layer after
|
|
3692
|
+
normal `placeLayer`/`layerToRig` mapping, before cleanup, projection or other
|
|
3693
|
+
preparation. Equality is unchanged, even after intervening edits. Otherwise
|
|
3694
|
+
**the last stage that changed a value wins**. A same-value assignment cannot
|
|
3695
|
+
supersede an earlier value-changing operation. Cut, limb-split and hybrid
|
|
3696
|
+
buffer copies at the same coordinates inherit their input's value lineage;
|
|
3697
|
+
creating a new transparent output buffer is not a colour change. A cut piece
|
|
3698
|
+
keeps its real source layer as baseline. An `author:<label>` layer and painting
|
|
3699
|
+
patch have no See-through baseline: they start as `<run>:author:<label>` or
|
|
3700
|
+
`patches[i]`, retaining that label unless a later operation actually changes
|
|
3701
|
+
RGBA. Actual cut under-fill changes use `cuts[i]`; restoring raw-layer values
|
|
3702
|
+
still ends in the unchanged class by the first comparison.
|
|
3703
|
+
|
|
3704
|
+
Automatic value changes are named `sourceProjection`, `extend`, `growRim`,
|
|
3705
|
+
`limbCap`, `limbFeather`, `fringeRestore`, `seamOverride`, `armSplit`,
|
|
3706
|
+
`headFaceUnderfill` or `ghostCleanup`. `limbSplit` and `limbHybrid` remain
|
|
3707
|
+
recognized stage names, but their pure copies do not claim a value change.
|
|
3708
|
+
Run preparation compares actual normalized samples, not layer-wrapper identity:
|
|
3709
|
+
the head under-fill loop assigns the full composite, but equal samples keep
|
|
3710
|
+
their earlier lineage. Arm-generated tags use their original hand-layer
|
|
3711
|
+
contributors (the input copy order for a combined arm split), never the
|
|
3712
|
+
prepared raster as baseline. Cleanup names retained mapped samples whose RGBA
|
|
3713
|
+
it changes. `fringeRestore` records a cleared bridge restored to its previous
|
|
3714
|
+
value; final raw equality still takes precedence. `unattributed` explicitly
|
|
3715
|
+
reports a changed sample with missing lineage, never silently calling it
|
|
3716
|
+
unchanged or inferring a stage. It is an accounting defect to investigate,
|
|
3717
|
+
not a hand-edit acceptance threshold.
|
|
3718
|
+
|
|
3719
|
+
**Ownership axis.** `ownership:` lines and the optional `cut_ownership` array
|
|
3720
|
+
in `parts.json` name every `cuts[i]`, with `from`, `into`,
|
|
3721
|
+
`moved_in_opaque_px`, `retained_band_opaque_px`, `cleared_opaque_px`,
|
|
3722
|
+
`under_filled_px`, `under_filled_opaque_px` and `net_cleared_opaque_px`.
|
|
3723
|
+
These reuse the original parent partition counts before a separately selected
|
|
3724
|
+
cut source, limb processing, fringe or seam passes. Moved-in opaque pixels are
|
|
3725
|
+
those taken from the parent; gross cleared opaque pixels are moved-in minus
|
|
3726
|
+
the retained band, **before refill**. Total under-fill counts every filled
|
|
3727
|
+
pixel (including faint alpha), regardless of colour equality; opaque under-fill
|
|
3728
|
+
counts its alpha above 8. Net cleared opaque pixels equal gross cleared minus
|
|
3729
|
+
opaque under-fill, and can be negative if filling faint art increases opacity.
|
|
3730
|
+
Indices refer to that assembly's effective cut list, including a set's custom
|
|
3731
|
+
list. A cut can therefore report moved pixels while every final value remains
|
|
3732
|
+
unchanged. Patches and author layers retain their own edit names on the value
|
|
3733
|
+
axis; they have no parent subtraction to record in this cut-move census.
|
|
3734
|
+
Neither a value count nor an edit boundary proves a visual defect's cause.
|
|
3735
|
+
|
|
3736
|
+
**Patching an independently moving piece no layer holds.** Follow
|
|
3737
|
+
[Source before scissors](#source-before-scissors) for any uncovered hole first.
|
|
3738
|
+
When the missing piece is structural and must move separately, an
|
|
3739
|
+
`assemble.patches` entry describes it:
|
|
3660
3740
|
the box round the hole in rig pixels, `alpha: "silhouette"` so the page round the
|
|
3661
3741
|
figure is not taken, and a `regions.<name>` bone. Draw it `"back"` (or
|
|
3662
3742
|
`{"before": …}` the part it belongs under) and let the box **reach under its
|
|
@@ -3669,8 +3749,8 @@ and read `uncovered error px` again: a patch covering `n` of the uncovered error
|
|
|
3669
3749
|
pixels lowers it by exactly `n` (the selftest's `AS17` plants that and counts it).
|
|
3670
3750
|
|
|
3671
3751
|
**Cutting a piece out of a layer.** When one See-through layer holds two things that
|
|
3672
|
-
must move apart — a scarf end painted into the sweater's `topwear`, both arms in one
|
|
3673
|
-
`handwear` — add
|
|
3752
|
+
must move apart and no separate source layer supplies them — a scarf end painted into the sweater's `topwear`, both arms in one
|
|
3753
|
+
`handwear` — use [Source before scissors](#source-before-scissors), then add a structural `assemble.cuts` entry (§3): open `rig/parts/<part>.png` from an
|
|
3674
3754
|
earlier `assemble`, add its `parts.json` `x`, `y` to a pixel's column and row to get
|
|
3675
3755
|
rig pixels, and draw the polygon round the piece. The stage prints, between the part
|
|
3676
3756
|
lines and the `fringe pushed back` lines, one line per cut part in the `pixels:`
|
|
@@ -4041,8 +4121,8 @@ says why each requirement exists and what breaks without it.
|
|
|
4041
4121
|
| <a id="ASSEMBLE_SINGLE_RUN_PLAN"></a>`ASSEMBLE_SINGLE_RUN_PLAN` | with no `--head`, a plan row takes `"head"`, or an `extend_below_crop` entry continues a part below a crop there is not | `"full"` in the row; remove the entry |
|
|
4042
4122
|
| <a id="ASSEMBLE_SINGLE_RUN_HEAD_BOX"></a>`ASSEMBLE_SINGLE_RUN_HEAD_BOX` | with no `--head`, the config sets `seethrough.head_box`, which places a head run there is not | remove `head_box`, or give the head run with `--head` |
|
|
4043
4123
|
| <a id="ASSEMBLE_ARM_SPLIT_CUTS_ARM"></a>`ASSEMBLE_ARM_SPLIT_CUTS_ARM` | `seethrough.arm_split` cuts a horizontal opaque run of the arm layer into more than two segments (issue #439; the detail gives the row, the run and the split x) | `seethrough.arm_split`: move `x` or `line` so each horizontal opaque run of the arm layer is split at most once |
|
|
4044
|
-
| <a id="SEETHROUGH_ARM_RUN_MERGED"></a>`SEETHROUGH_ARM_RUN_MERGED` | a full-run `handwear-r` or `handwear-l` layer merges sleeve, bare hand (`>= 40` skin run px around `<s>_wrist`, `GHOST_MIN_AREA`) and overgarment into one arm layer with non-skin pixels reaching more than `1.0x` the forearm length (`<s>_elbow -> <s>_wrist`) below `<s>_wrist` (measured `-0.15x..0.45x` on the 6 clean standees vs `1.25x..2.02x` on the 3 merged standees), and the sleeve above `<s>_wrist` and the drape below `<s>_wrist` have Euclidean RGB distance `< 80` (`ARM_DRAPE_COLOR_DISTANCE`, measured `20.8..28.2` when same-coloured vs `103.6..236.0` when distinct) so the stage cannot split them by colour (issue #490; the detail gives the hand px, the non-skin span, the forearm length and both RGB medians) |
|
|
4045
|
-
| <a id="SEETHROUGH_HEAD_RUN_HOLLOW"></a>`SEETHROUGH_HEAD_RUN_HOLLOW` | `full:face` has at least `200` opaque run px (`PROPOSE_MIN_PX`) and `head:face` covers less than `75 %` (`HEAD_FACE_COVERAGE_FLOOR`, measured `0.0 %..48.6 %` on hollow/omitted head runs vs `84.8 %..99.6 %` on clean head runs across the 4 public examples and 9-standee corpus) of `fullLayerToHead(full:face)`'s hole-filled silhouette (`alpha > 8`) in the head canvas while `config.assemble.head_face_underfill: true` is not declared, or `head:face` has fewer than `200` opaque pixels so there is no head-run `face` layer to under-fill (issue #492; the detail gives the full-run mapped count, the head-run overlap and upper/lower half shares) | re-seed `config.seethrough.seed`, or set `config.assemble.head_face_underfill: true` when `head:face` exists so the stage composites `fullLayerToHead(full:face)` under `head:face` on the head canvas (the fill restores transparent pixels only and leaves the cut's own edge where it is — a hollow whose edge is not under hair stays visible
|
|
4124
|
+
| <a id="SEETHROUGH_ARM_RUN_MERGED"></a>`SEETHROUGH_ARM_RUN_MERGED` | a full-run `handwear-r` or `handwear-l` layer merges sleeve, bare hand (`>= 40` skin run px around `<s>_wrist`, `GHOST_MIN_AREA`) and overgarment into one arm layer with non-skin pixels reaching more than `1.0x` the forearm length (`<s>_elbow -> <s>_wrist`) below `<s>_wrist` (measured `-0.15x..0.45x` on the 6 clean standees vs `1.25x..2.02x` on the 3 merged standees), and the sleeve above `<s>_wrist` and the drape below `<s>_wrist` have Euclidean RGB distance `< 80` (`ARM_DRAPE_COLOR_DISTANCE`, measured `20.8..28.2` when same-coloured vs `103.6..236.0` when distinct) so the stage cannot split them by colour (issue #490; the detail gives the hand px, the non-skin span, the forearm length and both RGB medians) | follow [Source before scissors](#source-before-scissors): select another layer/run, then a tighter run, then correct depth/role; if no separate source exists and sleeve and overgarment must move independently, a structural `author:` layer or `assemble.cuts` entry separates them |
|
|
4125
|
+
| <a id="SEETHROUGH_HEAD_RUN_HOLLOW"></a>`SEETHROUGH_HEAD_RUN_HOLLOW` | `full:face` has at least `200` opaque run px (`PROPOSE_MIN_PX`) and `head:face` covers less than `75 %` (`HEAD_FACE_COVERAGE_FLOOR`, measured `0.0 %..48.6 %` on hollow/omitted head runs vs `84.8 %..99.6 %` on clean head runs across the 4 public examples and 9-standee corpus) of `fullLayerToHead(full:face)`'s hole-filled silhouette (`alpha > 8`) in the head canvas while `config.assemble.head_face_underfill: true` is not declared, or `head:face` has fewer than `200` opaque pixels so there is no head-run `face` layer to under-fill (issue #492; the detail gives the full-run mapped count, the head-run overlap and upper/lower half shares) | re-seed `config.seethrough.seed`, or set `config.assemble.head_face_underfill: true` when `head:face` exists so the stage composites `fullLayerToHead(full:face)` under `head:face` on the head canvas (the fill restores transparent pixels only and leaves the cut's own edge where it is — a hollow whose edge is not under hair stays visible; follow [Source before scissors](#source-before-scissors) for its source remedy) |
|
|
4046
4126
|
| <a id="ASSEMBLE_PLAN_TAG_IN_RUN"></a><a id="ASSEMBLE_EXTEND_TAG_IN_RUN"></a>`ASSEMBLE_PLAN_TAG_IN_RUN`, `ASSEMBLE_EXTEND_TAG_IN_RUN` | an entry takes a tag its run does not hold (the detail lists what it does hold) | that entry's run or tag |
|
|
4047
4127
|
| <a id="ASSEMBLE_PART_OPAQUE"></a>`ASSEMBLE_PART_OPAQUE` | a part ended with no opaque pixel | drop the entry, or take the tag from the other run |
|
|
4048
4128
|
| <a id="ASSEMBLE_PATCH_BOX_INSIDE"></a>`ASSEMBLE_PATCH_BOX_INSIDE` | a patch's box reaches past the rig (the painting times `rig_scale`; the detail gives the size) | that patch's `box` — it is in rig pixels, not painting pixels |
|
|
@@ -4095,7 +4175,7 @@ says why each requirement exists and what breaks without it.
|
|
|
4095
4175
|
| <a id="KEYPOINTS_JOINT_KNOWN"></a><a id="KEYPOINTS_JOINT_STATE"></a><a id="KEYPOINTS_POSITION_STATED"></a><a id="KEYPOINTS_POSITION_INSIDE"></a>`KEYPOINTS_JOINT_KNOWN`, `KEYPOINTS_JOINT_STATE`, `KEYPOINTS_POSITION_STATED`, `KEYPOINTS_POSITION_INSIDE` | a joint name outside body-18, a state outside observed / occluded / missing, an observed joint with no `at` or a missing one with one, or a position outside the `width`x`height` image | that joint |
|
|
4096
4176
|
| <a id="KEYPOINTS_PERSON_UNIQUE"></a><a id="KEYPOINTS_PERSON_CHOSEN"></a>`KEYPOINTS_PERSON_UNIQUE`, `KEYPOINTS_PERSON_CHOSEN` | two people share an id; or the file holds more than one person and no `--person` names one, or `--person` names none of them (the message lists the ids) | `--person <id>` |
|
|
4097
4177
|
| <a id="KEYPOINTS_IMAGE_SIZE"></a>`KEYPOINTS_IMAGE_SIZE` | the file's `width`x`height` is not the `--source` painting's | a file measured on this painting |
|
|
4098
|
-
| <a id="PARTS_FILE_PRESENT"></a><a id="PARTS_IS_JSON"></a><a id="PARTS_KEY_KNOWN"></a><a id="PARTS_FIELD_PRESENT"></a><a id="PARTS_FIELD_TYPE"></a><a id="PARTS_NAME_UNIQUE"></a><a id="PARTS_FROM_KNOWN"></a><a id="PARTS_CUT_FROM_KNOWN"></a><a id="PARTS_BOX_INSIDE_RIG"></a><a id="PARTS_COUNTS_ADD_UP"></a><a id="PARTS_HOLES_LARGEST_FIRST"></a><a id="PARTS_HOLE_PART_KNOWN"></a><a id="PARTS_SINGLE_RUN_PAD"></a>`PARTS_*` (`PARTS_FILE_PRESENT`, `PARTS_IS_JSON`, `PARTS_KEY_KNOWN`, `PARTS_FIELD_PRESENT`, `PARTS_FIELD_TYPE`, `PARTS_NAME_UNIQUE`, `PARTS_FROM_KNOWN`, `PARTS_CUT_FROM_KNOWN`, `PARTS_BOX_INSIDE_RIG`, `PARTS_COUNTS_ADD_UP`, `PARTS_HOLES_LARGEST_FIRST`, `PARTS_HOLE_PART_KNOWN`, `PARTS_SINGLE_RUN_PAD`) | the `parts.json` read is not assemble's contract (`PARTS_FILE_PRESENT`: no `parts.json` under `--parts`, which names the directory holding `parts.json` and `parts/` — `work/rig` from `assemble --out work`, or `out` from `build --out out`; `PARTS_COUNTS_ADD_UP`: `visible_px + occluded_px` is not `opaque_px`, `visible_not_projected_px` is above `visible_px`, or the `recomposite` block's figures disagree — more uncovered than error pixels, a hole larger than its box, the wrong number of holes listed, listed areas that do not sum to `uncovered_error_px`; the last two codes are the block's order and its border names; `PARTS_SINGLE_RUN_PAD`: the `single_run_pad` block's margins are not the centring, its file is not smaller than its square, or `rig_size` is not the file at the scale, issue #366 — §5) | pass the directory holding `parts.json` and `parts/` (`work/rig` or `out`), or re-run assemble; do not edit `parts.json
|
|
4178
|
+
| <a id="PARTS_FILE_PRESENT"></a><a id="PARTS_IS_JSON"></a><a id="PARTS_KEY_KNOWN"></a><a id="PARTS_FIELD_PRESENT"></a><a id="PARTS_FIELD_TYPE"></a><a id="PARTS_NAME_UNIQUE"></a><a id="PARTS_FROM_KNOWN"></a><a id="PARTS_CUT_FROM_KNOWN"></a><a id="PARTS_BOX_INSIDE_RIG"></a><a id="PARTS_COUNTS_ADD_UP"></a><a id="PARTS_HOLES_LARGEST_FIRST"></a><a id="PARTS_HOLE_PART_KNOWN"></a><a id="PARTS_SINGLE_RUN_PAD"></a>`PARTS_*` (`PARTS_FILE_PRESENT`, `PARTS_IS_JSON`, `PARTS_KEY_KNOWN`, `PARTS_FIELD_PRESENT`, `PARTS_FIELD_TYPE`, `PARTS_NAME_UNIQUE`, `PARTS_FROM_KNOWN`, `PARTS_CUT_FROM_KNOWN`, `PARTS_BOX_INSIDE_RIG`, `PARTS_COUNTS_ADD_UP`, `PARTS_HOLES_LARGEST_FIRST`, `PARTS_HOLE_PART_KNOWN`, `PARTS_SINGLE_RUN_PAD`) | the `parts.json` read is not assemble's contract (`PARTS_FILE_PRESENT`: no `parts.json` under `--parts`, which names the directory holding `parts.json` and `parts/` — `work/rig` from `assemble --out work`, or `out` from `build --out out`; `PARTS_COUNTS_ADD_UP`: `visible_px + occluded_px` is not `opaque_px`, `visible_not_projected_px` is above `visible_px`, or the `recomposite` block's figures disagree — more uncovered than error pixels, a hole larger than its box, the wrong number of holes listed, listed areas that do not sum to `uncovered_error_px`; the last two codes are the block's order and its border names; `PARTS_SINGLE_RUN_PAD`: the `single_run_pad` block's margins are not the centring, its file is not smaller than its square, or `rig_size` is not the file at the scale, issue #366 — §5) | pass the directory holding `parts.json` and `parts/` (`work/rig` or `out`), or re-run assemble; do not edit `parts.json`; optional value/ownership report fields follow [Source before scissors](#source-before-scissors) |
|
|
4099
4179
|
|
|
4100
4180
|
### compare
|
|
4101
4181
|
|
|
@@ -4390,7 +4470,7 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
4390
4470
|
| <a id="CHECK_RIGC_GREEN"></a>`CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
4391
4471
|
| <a id="CHECK_LOOP_LAST_FRAME_AT_DURATION"></a>`CHECK_LOOP_LAST_FRAME_AT_DURATION` | the idle's last frame does not sit at `duration` | `motion.duration` — a whole number of 1/12 s |
|
|
4392
4472
|
| <a id="CHECK_LOOP_CLOSES"></a>`CHECK_LOOP_CLOSES` | frame 0 and the frame at `duration` differ (max and first pixel quoted); with a physics constraint, the frames of the rig with every physics constraint left out, and the object says so (§7, *loop*) | a track whose last key is not its first |
|
|
4393
|
-
| <a id="CHECK_SEAM_WITHIN_BAR"></a>`CHECK_SEAM_WITHIN_BAR` | the setup pose (or, under `--set <name>` / `--all-sets`, an alternate set's setup pose posed at its `drawn` bone angles with its `<set>_*` attachments and `hides` applied, issue #465) does not reproduce the flat stack of parts (`recomposite_rig.png`, or `recomposite_rig_<set>.png`) | read the `seam pair` lines and `check/seam/` (`check/seam_<set>/`) pictures (§7; magenta = a counted pixel, cyan = the box round them): the pair, its boxes and the pixels counted. Usually a region or mesh placed off its part; compare with `recomposite_rig.png` (`recomposite_rig_<set>.png`). A pair of one layer cut in two with `differing_copies_px`
|
|
4473
|
+
| <a id="CHECK_SEAM_WITHIN_BAR"></a>`CHECK_SEAM_WITHIN_BAR` | the setup pose (or, under `--set <name>` / `--all-sets`, an alternate set's setup pose posed at its `drawn` bone angles with its `<set>_*` attachments and `hides` applied, issue #465) does not reproduce the flat stack of parts (`recomposite_rig.png`, or `recomposite_rig_<set>.png`) | read the `seam pair` lines and `check/seam/` (`check/seam_<set>/`) pictures (§7; magenta = a counted pixel, cyan = the box round them): the pair, its boxes and the pixels counted. Usually a region or mesh placed off its part; compare with `recomposite_rig.png` (`recomposite_rig_<set>.png`). A pair of one layer cut in two with `differing_copies_px` identifies differing copies near the cut; follow [Source before scissors](#source-before-scissors) before revisiting a structural separation. Widening `overlap` is not a source remedy |
|
|
4394
4474
|
| <a id="CHECK_BREATH_VISIBLE"></a>`CHECK_BREATH_VISIBLE` | the breath alone barely moves the torso (`topwear`, rendered alone, the idle with and without its breath tracks — issue #411; the whole idle's figure is printed beside it, and motion that is not the breath's is not counted), or the idle has no breath track at all — or the feet (`footwear`), rendered alone, move at all | the chest's breath tracks (`motion.tracks` on `chest`), or the torso mesh's `segments`; for the feet, the bone their region rides (`regions.<part>`, `root` in both examples) |
|
|
4395
4475
|
| <a id="CHECK_BLINK_NO_HOLE"></a>`CHECK_BLINK_NO_HOLE` | with the blink held shut, the eyewhite box shows the page where the open eye had art | the layer under the eye: the `face` part has no art there. Take the face from the other run, or add a part under the eye; `motion.blink.squash` only hides the hole less |
|
|
4396
4476
|
| <a id="CHECK_CHAIN_LAG"></a>`CHECK_CHAIN_LAG` | a rotate track leads (or does not lag) the keyed bone above it, or a chain link swings less than the link above — or, in a chain that carries a part the scene file declares `rests`, more than the link above (issue #245; the object names the part and the file). A lag refusal (issue #580) prints the step, then the two phases it read — as the sine reader returns them, a negative amplitude read as amplitude > 0 at phase + 0.5 — then their difference, folded into (−¼, ¼] cycle because a sine's sign is half a cycle (so flipping an amplitude's sign does not change the step), then the child phases that pass for this parent: `phase step -0.230 cycle(s); >= 0.001 is required — … Phases read: "chest" 0.000, "sleeve_l0" 0.270 …; the step is their difference, 0.270, folded into (-1/4, 1/4] cycle … here a "sleeve_l0" phase in [0.001, 0.250] or [0.501, 0.750]` | for a lag, that chain track's `phase`/`lag`: a child phase from the parent's + 0.001 to + 0.25, modulo 0.5 — the windows the line prints. A larger phase helps only inside them: a child 0.25 to 0.5 above its parent still reads as a lead, and a negated `amps` moves nothing. For a swing, its `amps` (non-decreasing toward the tip; non-increasing on a chain declared resting) |
|
|
@@ -4401,10 +4481,10 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
4401
4481
|
| <a id="CHECK_FEET_STAY_LEVEL"></a>`CHECK_FEET_STAY_LEVEL` | a `footwear` part riding a bone that hangs from another turns more than ±10 degrees from its setup pose on a frame where a bone above that bone is turned (issue #348) — the bar is HQ's line (rig-c/parts#347 ruling 4), not a figure measured here; the object names the part, its bone and the bone it hangs from, the detail the animation, frame and time, the turned bone and its turn, the drawing's turn, and every bone the turn came through with its `inherit` | config.bones' `inherit` on the foot's bone (§3, `bones`): a mode that does not take the parent's rotation (`noRotationOrReflection`, `onlyTranslation`) keeps the drawing at its setup angle; which mode is the author's. Or the clip's keys on the foot's own bone, which turn the shoe whatever its mode |
|
|
4402
4482
|
| <a id="CHECK_STRESS_OFF_CHAIN"></a>`CHECK_STRESS_OFF_CHAIN` | a mesh from a See-through `handwear` layer that rides one chain of the idle does not turn rigidly with it: with the chain's first link turned 90°, −90° or 180° on a throwaway copy (physics left out, posed by rigc, issue #347), its worst edge reads above 1.0000 as max(ratio, 1/ratio) against the setup pose. The object names the mesh, the link and the turn; the detail quotes the edge and names each bone the mesh is weighted to off the chain, with how many vertices carry it and the largest weight. Or the turned render shows no mesh on the slot: not read is not a pass | the mesh's `segments`: drop the segment on the bone the line names (a 16 px `chest` stub at the shoulder, as `propose` drafted until issue #347), so the sleeve rides its chain alone. Measured (rigc 2.47.1): `demo`'s sleeves read 2.46 to 254.7 with the stub and 1.0000 without it, and the setup-pose seam does not move (mean 0.147, 0 px over 40, both ways) |
|
|
4403
4483
|
| <a id="CHECK_LIMB_ONE_BONE"></a>`CHECK_LIMB_ONE_BONE` | a part a `config.limbs` segment made is bound in `rig.json` to a bone besides its segment's — a mesh weighted to a second bone, or a slot on another bone — or to none (issue #349); the object names the part, its limb and its bone, the detail every other bone | `regions.<part>` the segment's bone, or a mesh whose `segments` name that bone alone: a bend is a rotation at the joint |
|
|
4404
|
-
| <a id="CHECK_SEAM_OVER_RANGE"></a>`CHECK_SEAM_OVER_RANGE` | a limb joint with a declared `range` (issue #360), bent to one of its range's ends or its middle, fails the rest-pose seam's bar against the same pose with the joint's own art drawn under the limb — the bend uncovers the joint; the object names the joint, its limb and segment, the part, the bone and the angle. Under `cap_shape: "disc"` (issue #360, unit B) or `"ball"` (issue #598), also any pixel of the disc over 40 at a pose, under the seam's bar or not. Also a joint the line cannot read (its bone has no parent in `rig.json`, no slot shows its limb, or its cap's disc holds no opaque art), and, refused, two renders of the bent copy that rigc did not put on one grid | a cap that covers the turn: the layer's art within the cap's radius on the upper side of the joint line, or a `cap` image painting what the layer lacks (`limb cap:` names the missing pixels); or a narrower `range` |
|
|
4405
|
-
| <a id="SEAM_OVER_MOTION"></a>`SEAM_OVER_MOTION` | **Existing internal-joint and explicit-seam pairs remain warnings** (issue #493; a seam that opens in motion may be intended). **Proven body-root pairs are a bar** (issue #686): an above-floor opening adds `CHECK_SEAM_OVER_MOTION`, sets `PASS` false and exits 1. Every declared neighbour pair — each limb's two segments at a joint, the upper and the one below (`parts.json`'s limb records, `config.limbs`), and under `--requirements` each `seam`'s `part` and `neighbour`, one pair however many declare it — read in every animation `motion.json` holds, the idle included, by rig-c's seam reading: one `rigc render --seam <a>:<b>,… --fps 12` over the packed build as played (physics on), whose `seam.json` (`rigc-seam/1`, rig-c's AUTHORING §8.8) names per pair and frame a **gap** (background inside the joint) or a **ghost** (one piece's overlap band swung clear of its partner), each frame's cells (`px`, one texel of the coarser drawing — a rig px on a build at scale 1), its cell size and its world box. Printed for existing warning pairs and animations with a frame over the floor: `WARN seam "<a>\|<b>" opens on N frame(s) [<frames>] max <px> px in "<animation>" — over the one-cell floor`, then how many more frames rigc named that were under it. **The floor**: a frame's gap or ghost is reported when it holds more cells than its box is long (its longer side, in cells) — thicker than one cell. A cell is drawn when the renderer paints its centre at half opacity, so an outline that moves by less than a cell flips the cells along it: a line one cell deep, joined or scattered, holds at most as many cells as its box is long, and a hole of any area more. The rule types no number; measured on `examples/wind`'s idle, rigc names 43 openings over its six limb pairs (the two elbows, 1 to 5 cells, boxes up to 17 cells long), every one one cell thick, and on rig-c's `gallery/walk` 13 joint cracks of 1 to 3 cells, all one cell thick. `check.json`'s `SEAM_OVER_MOTION` holds `status` (`WARN`, `CLEAR`, `SKIP`, or `FAIL` for a new root tear), `fps`, `floor`, `pairs`, `animations`, `warnings` and one row per pair and animation (`opens` and `px` over the floor, `max_px`, `under_floor`, rigc's `status`, `drawn` and `banded`); for existing warning pairs it moves no exit status, no `check: PASS` and no bar count — the summary ends `; N warning(s) (SEAM_OVER_MOTION, not a bar)`. **SKIP**, named: no pair declared (no limb segment, no `seam`; then `check.json` has no key), no `parts.json`, no animation, every pair naming a slot `rig.json` lacks (each printed `not read`), or rigc reading no band on any frame of any pair (rigc's own `skip`: the two drawings share no cell at rest) — nothing measured, never a pass | if the opening is not intended: a cap that covers the turn (`config.limbs`, *Limbs: cut at their joints*; `limb cap:` names the pixels the layer lacks), or a motion that bends that joint less; if it is intended, nothing |
|
|
4484
|
+
| <a id="CHECK_SEAM_OVER_RANGE"></a>`CHECK_SEAM_OVER_RANGE` | a limb joint with a declared `range` (issue #360), bent to one of its range's ends or its middle, fails the rest-pose seam's bar against the same pose with the joint's own art drawn under the limb — the bend uncovers the joint; the object names the joint, its limb and segment, the part, the bone and the angle. Under `cap_shape: "disc"` (issue #360, unit B) or `"ball"` (issue #598), also any pixel of the disc over 40 at a pose, under the seam's bar or not. Also a joint the line cannot read (its bone has no parent in `rig.json`, no slot shows its limb, or its cap's disc holds no opaque art), and, refused, two renders of the bent copy that rigc did not put on one grid | [Source before scissors](#source-before-scissors) first; a cap that covers the turn: the layer's art within the cap's radius on the upper side of the joint line, or a `cap` image painting what the layer lacks (`limb cap:` names the missing pixels); or a narrower `range` |
|
|
4485
|
+
| <a id="SEAM_OVER_MOTION"></a>`SEAM_OVER_MOTION` | **Existing internal-joint and explicit-seam pairs remain warnings** (issue #493; a seam that opens in motion may be intended). **Proven body-root pairs are a bar** (issue #686): an above-floor opening adds `CHECK_SEAM_OVER_MOTION`, sets `PASS` false and exits 1. Every declared neighbour pair — each limb's two segments at a joint, the upper and the one below (`parts.json`'s limb records, `config.limbs`), and under `--requirements` each `seam`'s `part` and `neighbour`, one pair however many declare it — read in every animation `motion.json` holds, the idle included, by rig-c's seam reading: one `rigc render --seam <a>:<b>,… --fps 12` over the packed build as played (physics on), whose `seam.json` (`rigc-seam/1`, rig-c's AUTHORING §8.8) names per pair and frame a **gap** (background inside the joint) or a **ghost** (one piece's overlap band swung clear of its partner), each frame's cells (`px`, one texel of the coarser drawing — a rig px on a build at scale 1), its cell size and its world box. Printed for existing warning pairs and animations with a frame over the floor: `WARN seam "<a>\|<b>" opens on N frame(s) [<frames>] max <px> px in "<animation>" — over the one-cell floor`, then how many more frames rigc named that were under it. **The floor**: a frame's gap or ghost is reported when it holds more cells than its box is long (its longer side, in cells) — thicker than one cell. A cell is drawn when the renderer paints its centre at half opacity, so an outline that moves by less than a cell flips the cells along it: a line one cell deep, joined or scattered, holds at most as many cells as its box is long, and a hole of any area more. The rule types no number; measured on `examples/wind`'s idle, rigc names 43 openings over its six limb pairs (the two elbows, 1 to 5 cells, boxes up to 17 cells long), every one one cell thick, and on rig-c's `gallery/walk` 13 joint cracks of 1 to 3 cells, all one cell thick. `check.json`'s `SEAM_OVER_MOTION` holds `status` (`WARN`, `CLEAR`, `SKIP`, or `FAIL` for a new root tear), `fps`, `floor`, `pairs`, `animations`, `warnings` and one row per pair and animation (`opens` and `px` over the floor, `max_px`, `under_floor`, rigc's `status`, `drawn` and `banded`); for existing warning pairs it moves no exit status, no `check: PASS` and no bar count — the summary ends `; N warning(s) (SEAM_OVER_MOTION, not a bar)`. **SKIP**, named: no pair declared (no limb segment, no `seam`; then `check.json` has no key), no `parts.json`, no animation, every pair naming a slot `rig.json` lacks (each printed `not read`), or rigc reading no band on any frame of any pair (rigc's own `skip`: the two drawings share no cell at rest) — nothing measured, never a pass | [Source before scissors](#source-before-scissors) first; if the opening is not intended: a cap that covers the turn (`config.limbs`, *Limbs: cut at their joints*; `limb cap:` names the pixels the layer lacks), or a motion that bends that joint less; if it is intended, nothing |
|
|
4406
4486
|
| <a id="CHECK_MOTION_SEAM_ROOT"></a>`CHECK_MOTION_SEAM_ROOT` | a body-root declaration has missing or ambiguous segment0, bone, parent, slot or default-skin attachment; the refusal names the part and candidates | supply the declared one-to-one hierarchy/slot/provenance binding; no nearest or mesh-influence fallback |
|
|
4407
|
-
| <a id="CHECK_SEAM_OVER_MOTION"></a>`CHECK_SEAM_OVER_MOTION` | a proven body-root pair has an active animation gapOpen plus UNDER-owned ghost above the unchanged one-cell floor; the refusal names pair, animation, frame, cells, cell size and world box | repair the declared rest overlap or authored motion; the existing internal-joint warning policy and physics reference worlds are unchanged |
|
|
4487
|
+
| <a id="CHECK_SEAM_OVER_MOTION"></a>`CHECK_SEAM_OVER_MOTION` | a proven body-root pair has an active animation gapOpen plus UNDER-owned ghost above the unchanged one-cell floor; the refusal names pair, animation, frame, cells, cell size and world box | [Source before scissors](#source-before-scissors) first; repair the declared rest overlap or authored motion; the existing internal-joint warning policy and physics reference worlds are unchanged |
|
|
4408
4488
|
| <a id="CLOTH_SETTLES"></a>`CLOTH_SETTLES` | **a warning, never a refusal** (issue #501; the owner's ruling: cloth at rest while the character stands may be intended). Every physics chain, read by its tip on the idle `check` renders — `rigc render --animation idle --fps 12 --geometry`, physics on, rigc's default environment. A chain is a **physics leaf** — a bone a physics constraint names (`rig.json`'s `constraints` or `motion.json`'s `physics`) with no such bone under it — and the run of physics bones above it; its tip is the leaf's end (origin plus `length` along its world x axis), or its origin when the leaf has no length; its speed between frames is the tip's distance over their time, from the idle's `geometry.json`. A chain **settles** at the first frame from which every speed to the idle's last frame is under the floor, and each one that does — before the loop's length is out — prints `WARN chain "<root>" .. "<tip>" (N physics link(s)) settles at <t> s of the <d> s idle — its tip's end under 0.01694 rig px/s from there to the last frame (fastest on the idle <v> rig px/s)`. **The floor**, 0.01694 rig px/s, is derived, not chosen: half the slowest tip an idle key drives on the public examples (the repository's floor rule, half the weaker figure) — measured with this port's build of each (rig-c 2.60.0), over the 31 chains a keyed bone at or above the leaf drives (`demo` 12, `reference-sample` 1, `scarf` 11, `wind` 7) the slowest frame-to-frame tip speed is `scarf`'s `skirt_l2`, 0.03388 rig px/s; the ten no key drives — `wind`'s skirt, eight trees and the swing, hung from a `hip` its idle never keys, and `reference-sample`'s `hip_jiggle` — fall under it from 1.5 to 2.75 s on a 3.333 s idle (`hip_jiggle` at 0 s: it never moves) and end at 0.0025 rig px/s or less. `check.json`'s `CLOTH_SETTLES` holds `status` (`WARN`, `CLEAR`), `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings` and one row per chain (`tip`, `root`, `links`, `point` — `end` or `origin` — `settles_at`, null when it does not, `max_rig_px_per_s`, `min_rig_px_per_s`); it moves no exit status, no `check: PASS` and no bar count — the summary's warning clause names it, `; N warning(s) (CLOTH_SETTLES, not a bar)` (with `SEAM_OVER_MOTION` before it, and N their sum, where that read a pair). It reads the idle alone, one loop from rigc's physics reset at frame 0: a chain that settles later than the idle is long is not seen. **SKIP**, named: no physics constraint (then `check.json` has no key), or no idle — nothing measured, never a pass | if the chain should move while the character stands, one of the consumer's two levers: an environment at render (`rigc render --environment`, wind and gravity in time — rig-c's AUTHORING §8.4), or a consumer's animation that moves the bone the chain hangs from. The drafted idle keys no hip, by measurement: a hip turn at the idle's period fails a bar on all five public examples (issue #502, §3 *What moves them*). A `cloth` recipe's skirt hangs from `hip`, which the drafted idle never keys (§3, *Recipe: cloth and hair that answer the world*, *What moves them*, which has a worked schedule for `wind`). If it is intended, nothing |
|
|
4409
4489
|
| <a id="CHECK_FOLDS_READ"></a>`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` |
|
|
4410
4490
|
| <a id="CHECK_SEAM_FRAME_SIZE"></a><a id="FRAMES_SIDECAR"></a><a id="CHECK_GEOMETRY_FILE"></a>`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 |
|
|
@@ -4713,8 +4793,9 @@ assemble*, at the end of this section):
|
|
|
4713
4793
|
"scarf"` along the cut line, 16 of them reading a band pixel where `topwear`
|
|
4714
4794
|
holds the layer's colour and `scarf` the painting's, the other 8 where the cut
|
|
4715
4795
|
line runs along the scarf's red edge [observed, issue #202]. A wider band moves
|
|
4716
|
-
neither
|
|
4717
|
-
|
|
4796
|
+
neither. The historical polygon-edge experiment measured a structural cut
|
|
4797
|
+
through art of one colour on both sides; it is not a seam-repair prescription
|
|
4798
|
+
([Source before scissors](#source-before-scissors)). The two copies in `rig/parts/<from>.png` and `rig/parts/<into>.png`
|
|
4718
4799
|
show pixel for pixel. The composite meets the frame pixel centre to pixel
|
|
4719
4800
|
centre: rig pixel index u lands on frame index s·u + t + 0.5·(s − 1), `s` the
|
|
4720
4801
|
scale the `seam:` line prints (issue #209; the reference implementation's map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.76.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/assemble.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AUTOMATIC_WRITERS as AW, compactValueBaseline, pixelProvenance, rgbaEqual, type ValueBaseline } from './provenance.ts';
|
|
1
2
|
import { bindDepthRun, copyDepthMaterial, registerDepthRun, transportPartDepth, type DepthConfig, type RegisteredDepth } from './depthsheets.ts';
|
|
2
3
|
/**
|
|
3
4
|
* The assemble stage: two See-through runs over one painting — a FULL-body run
|
|
@@ -2257,7 +2258,7 @@ export function pixelOwners(rigLayers: readonly Raster[], rank: readonly number[
|
|
|
2257
2258
|
* masks mark own-layer art under a subset set's actual cover (alpha above 8);
|
|
2258
2259
|
* those pixels are occluded, outside every projection core, and never recoloured.
|
|
2259
2260
|
*/
|
|
2260
|
-
export function projectSource(rigLayers: Raster[], srcr: Raster, frame: Frame, run: Run, rule: ProjectRule = DEFAULT_PROJECT_RULE, rank: readonly number[] | null = null, keep: ReadonlyArray<Mask | null> | null = null): ProjectionStats[] {
|
|
2261
|
+
export function projectSource(rigLayers: Raster[], srcr: Raster, frame: Frame, run: Run, rule: ProjectRule = DEFAULT_PROJECT_RULE, rank: readonly number[] | null = null, keep: ReadonlyArray<Mask | null> | null = null, changed?: (layer: number, rigPixel: number) => void): ProjectionStats[] {
|
|
2261
2262
|
const { W, H } = frame;
|
|
2262
2263
|
const owner = pixelOwners(rigLayers, rank);
|
|
2263
2264
|
const inFront = (o: number, i: number): boolean => frontOf(o, i, rank);
|
|
@@ -2320,10 +2321,14 @@ export function projectSource(rigLayers: Raster[], srcr: Raster, frame: Frame, r
|
|
|
2320
2321
|
if (core.data[p] === 0) continue;
|
|
2321
2322
|
const w = blurred.data[p];
|
|
2322
2323
|
const rest = f32(1 - w);
|
|
2324
|
+
let different = false;
|
|
2323
2325
|
for (let c = 0; c < 3; c++) {
|
|
2324
2326
|
const v = f32(f32(w * srcr.data[p * 4 + c]) + f32(rest * r.data[p * 4 + c]));
|
|
2325
|
-
|
|
2327
|
+
const next = Math.trunc(v < 0 ? 0 : v > 255 ? 255 : v);
|
|
2328
|
+
different ||= next !== r.data[p * 4 + c];
|
|
2329
|
+
r.data[p * 4 + c] = next;
|
|
2326
2330
|
}
|
|
2331
|
+
if (different) changed?.(i, p);
|
|
2327
2332
|
}
|
|
2328
2333
|
return { core: coreN, taken, refused, clear, takenMask, coreMask: core, refusedMask };
|
|
2329
2334
|
});
|
|
@@ -2437,7 +2442,7 @@ export function growRim(part: Raster, sel: Mask, srcr: Raster, front: Mask | nul
|
|
|
2437
2442
|
* overwriting an earlier one — the layer whose run says whether the pixel is
|
|
2438
2443
|
* visible — and `trace.ring` set when the last write was the ring's.
|
|
2439
2444
|
*/
|
|
2440
|
-
export function mergeBelowCrop(part: Raster, extras: Raster[], front: Mask, srcr: Raster, headBottom: number, trace?: MergeTrace): number {
|
|
2445
|
+
export function mergeBelowCrop(part: Raster, extras: Raster[], front: Mask, srcr: Raster, headBottom: number, trace?: MergeTrace, changed?: (rigPixel: number, stage: 'extend' | 'growRim') => void): number {
|
|
2441
2446
|
let merged = 0;
|
|
2442
2447
|
extras.forEach((extra, e) => {
|
|
2443
2448
|
const sel = belowCrop(part, extra, headBottom);
|
|
@@ -2445,6 +2450,7 @@ export function mergeBelowCrop(part: Raster, extras: Raster[], front: Mask, srcr
|
|
|
2445
2450
|
for (let p = 0; p < sel.data.length; p++) {
|
|
2446
2451
|
if (sel.data[p] === 0) continue;
|
|
2447
2452
|
copied++;
|
|
2453
|
+
if (!rgbaEqual(part.data, p * 4, extra.data, p * 4)) changed?.(p, 'extend');
|
|
2448
2454
|
part.data.set(extra.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
2449
2455
|
if (trace !== undefined) {
|
|
2450
2456
|
trace.from[p] = e;
|
|
@@ -2458,6 +2464,7 @@ export function mergeBelowCrop(part: Raster, extras: Raster[], front: Mask, srcr
|
|
|
2458
2464
|
trace.lastRing = ring;
|
|
2459
2465
|
for (let p = 0; p < ring.data.length; p++) {
|
|
2460
2466
|
if (ring.data[p] === 1) {
|
|
2467
|
+
changed?.(p, 'growRim');
|
|
2461
2468
|
trace.from[p] = e;
|
|
2462
2469
|
trace.ring.data[p] = 1;
|
|
2463
2470
|
}
|
|
@@ -2805,6 +2812,7 @@ export function pushBackFringe(
|
|
|
2805
2812
|
feathered: ReadonlyArray<Mask | null> | null = null,
|
|
2806
2813
|
keep: ReadonlyArray<Mask | null> | null = null,
|
|
2807
2814
|
turnMovingPair: ((a: string, b: string) => boolean) | null = null,
|
|
2815
|
+
restored?: (part: number, rigPixel: number) => void,
|
|
2808
2816
|
): FringePushed[][] {
|
|
2809
2817
|
const { width: W } = srcr;
|
|
2810
2818
|
const { holder, cand } = seamCandidates(parts, srcr, rule, silhouette);
|
|
@@ -2840,7 +2848,10 @@ export function pushBackFringe(
|
|
|
2840
2848
|
}));
|
|
2841
2849
|
const counts: Array<Map<number, number>> = parts.map(() => new Map());
|
|
2842
2850
|
cleared.forEach((list, i) => {
|
|
2843
|
-
for (const c of list)
|
|
2851
|
+
for (const c of list) {
|
|
2852
|
+
if (c.on) counts[i].set(c.h, (counts[i].get(c.h) ?? 0) + 1);
|
|
2853
|
+
else restored?.(i, (parts[i].record.y + Math.floor(c.q / parts[i].record.w)) * W + parts[i].record.x + c.q % parts[i].record.w);
|
|
2854
|
+
}
|
|
2844
2855
|
});
|
|
2845
2856
|
return counts.map((m) =>
|
|
2846
2857
|
[...m.entries()]
|
|
@@ -2878,6 +2889,7 @@ export function seamOverride(
|
|
|
2878
2889
|
silhouette: Mask | null,
|
|
2879
2890
|
keep: ReadonlyArray<Mask | null> | null = null,
|
|
2880
2891
|
turnMovingPair: ((a: string, b: string) => boolean) | null = null,
|
|
2892
|
+
written?: (part: number, rigPixel: number) => void,
|
|
2881
2893
|
): void {
|
|
2882
2894
|
const { width: W } = srcr;
|
|
2883
2895
|
const { top, cand } = seamCandidates(parts, srcr, rule, silhouette);
|
|
@@ -2913,9 +2925,11 @@ export function seamOverride(
|
|
|
2913
2925
|
) {
|
|
2914
2926
|
continue;
|
|
2915
2927
|
}
|
|
2928
|
+
const different = image.data[s] !== srcr.data[d * 4] || image.data[s + 1] !== srcr.data[d * 4 + 1] || image.data[s + 2] !== srcr.data[d * 4 + 2];
|
|
2916
2929
|
image.data[s] = srcr.data[d * 4];
|
|
2917
2930
|
image.data[s + 1] = srcr.data[d * 4 + 1];
|
|
2918
2931
|
image.data[s + 2] = srcr.data[d * 4 + 2];
|
|
2932
|
+
if (different) written?.(i, d);
|
|
2919
2933
|
n++;
|
|
2920
2934
|
}
|
|
2921
2935
|
}
|
|
@@ -3415,6 +3429,8 @@ export interface CutCount {
|
|
|
3415
3429
|
band: number;
|
|
3416
3430
|
/** With `under` only (issue #193): the base pixels filled from the under image (`fillUnder`); absent without it. */
|
|
3417
3431
|
under?: number;
|
|
3432
|
+
/** Filled pixels with final alpha above the opaque threshold (report-only). */
|
|
3433
|
+
underOpaque?: number;
|
|
3418
3434
|
/** With `under` only: the removed pixels the image left empty (its alpha below the alpha removed); `under + underEmpty` is the removed count. */
|
|
3419
3435
|
underEmpty?: number;
|
|
3420
3436
|
/** With `under_fill: "colour"` only (issue #360): the `under` pixels took the image's colour at the alpha the cut removed; absent without it. */
|
|
@@ -3518,6 +3534,8 @@ export function cutBand(polygonMask: Mask, overlap: number): Mask {
|
|
|
3518
3534
|
|
|
3519
3535
|
/** One layer's part as the stage builds it, before it is placed: its key, pixels, projection figures, merge trace and, when the layer is cut, the rig pixels it owns. */
|
|
3520
3536
|
interface Built {
|
|
3537
|
+
/** Report-only last writer at each rig coordinate; never read by raster arithmetic. */
|
|
3538
|
+
writers?: Int32Array;
|
|
3521
3539
|
key: string;
|
|
3522
3540
|
r: Raster;
|
|
3523
3541
|
st: ProjectionStats;
|
|
@@ -4031,7 +4049,8 @@ function cutLimbs(
|
|
|
4031
4049
|
problems.push({ code: 'ASSEMBLE_COUNTS_ADD_UP', object: `part "${src}" (config.limbs)`, detail: `opaque px: ${before} before the limbs, ${sum} over their parts; equal is required (a bug in the assembler, not in the inputs)` });
|
|
4032
4050
|
}
|
|
4033
4051
|
for (const { q, i } of shares) {
|
|
4034
|
-
|
|
4052
|
+
const writers = b.writers?.slice();
|
|
4053
|
+
pieces[i] = { ...b, writers, r: rasters[i], region: regions[i], merged: mergedIn(b, regions[i]) };
|
|
4035
4054
|
if (count(regions[i], regions[i]) === 0 && opaqueOf(rasters[i]) === 0) {
|
|
4036
4055
|
problems.push({
|
|
4037
4056
|
code: 'ASSEMBLE_LIMB_PIXELS',
|
|
@@ -4125,6 +4144,10 @@ function cutLimbs(
|
|
|
4125
4144
|
return;
|
|
4126
4145
|
}
|
|
4127
4146
|
const lower = rasters[at];
|
|
4147
|
+
const capWrite = (p: number, data: Uint8ClampedArray, offset: number): void => {
|
|
4148
|
+
const ids = (pieces[at] as Built).writers;
|
|
4149
|
+
if (ids !== undefined && !rgbaEqual(lower.data, p * 4, data, offset)) ids[p] = AW.limbCap;
|
|
4150
|
+
};
|
|
4128
4151
|
const lowerSrc = builtOf(sg.from) as Built;
|
|
4129
4152
|
// Issue #360 (cut_band): the cap's pixels are the disc's on the upper side of the cut, wherever it was placed.
|
|
4130
4153
|
const cut = (cutLines.get(l) as Array<JointLine | null>)[k] as JointLine;
|
|
@@ -4147,6 +4170,7 @@ function cutLimbs(
|
|
|
4147
4170
|
const from: CapFrom = { box, codes: new Uint8Array(Math.max(0, box[2]) * Math.max(0, box[3])) };
|
|
4148
4171
|
const mark = (x: number, y: number, code: number): void => {
|
|
4149
4172
|
from.codes[(y - box[1]) * box[2] + (x - box[0])] = code;
|
|
4173
|
+
|
|
4150
4174
|
};
|
|
4151
4175
|
for (let y = Math.max(0, box[1]); y < Math.min(H, box[1] + box[3]); y++) {
|
|
4152
4176
|
for (let x = Math.max(0, box[0]); x < Math.min(W, box[0] + box[2]); x++) {
|
|
@@ -4161,6 +4185,7 @@ function cutLimbs(
|
|
|
4161
4185
|
lowerKept++;
|
|
4162
4186
|
mark(x, y, CAP_FROM_LOWER_KEPT);
|
|
4163
4187
|
} else if (img !== null && img.data[u + 3] > 0) {
|
|
4188
|
+
capWrite(p, img.data, u);
|
|
4164
4189
|
lower.data.set(img.data.subarray(u, u + 4), p * 4);
|
|
4165
4190
|
lowerPx++;
|
|
4166
4191
|
mark(x, y, CAP_FROM_LOWER);
|
|
@@ -4173,10 +4198,12 @@ function cutLimbs(
|
|
|
4173
4198
|
continue;
|
|
4174
4199
|
}
|
|
4175
4200
|
if (img !== null && img.data[u + 3] > 0) {
|
|
4176
|
-
|
|
4201
|
+
capWrite(p, img.data, u);
|
|
4202
|
+
lower.data.set(img.data.subarray(u, u + 4), p * 4);
|
|
4177
4203
|
imagePx++;
|
|
4178
4204
|
mark(x, y, CAP_FROM_IMAGE);
|
|
4179
4205
|
} else if (ownLower[p] === li && lowerSrc.r.data[p * 4 + 3] > 0) {
|
|
4206
|
+
capWrite(p, lowerSrc.r.data, p * 4);
|
|
4180
4207
|
lower.data.set(lowerSrc.r.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
4181
4208
|
layerPx++;
|
|
4182
4209
|
mark(x, y, CAP_FROM_LAYER);
|
|
@@ -4184,6 +4211,7 @@ function cutLimbs(
|
|
|
4184
4211
|
// Issue #360: the disc is filled from the art — the upper segment's, where the lower's layer has none.
|
|
4185
4212
|
// Issue #557: under either shape — the half-disc too takes the neighbouring segment's pixels, so a cap is
|
|
4186
4213
|
// short only where no layer of the limb gives a pixel.
|
|
4214
|
+
capWrite(p, upper.data, p * 4);
|
|
4187
4215
|
lower.data.set(upper.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
4188
4216
|
upperPx++;
|
|
4189
4217
|
mark(x, y, CAP_FROM_UPPER);
|
|
@@ -4230,6 +4258,7 @@ function cutLimbs(
|
|
|
4230
4258
|
// piece's region and its remaining ramp is marked feathered so pushBackFringe does not clear the fade.
|
|
4231
4259
|
if (na <= OPAQUE_ALPHA_ABOVE) upperRegion.data[p] = 0;
|
|
4232
4260
|
else featheredMask.data[p] = 1;
|
|
4261
|
+
if (upPiece.writers !== undefined) upPiece.writers[p] = AW.limbFeather;
|
|
4233
4262
|
featheredPx++;
|
|
4234
4263
|
}
|
|
4235
4264
|
}
|
|
@@ -4334,7 +4363,8 @@ function cutLimbs(
|
|
|
4334
4363
|
base.hybrid = { part: sg.hybrid.part, ...geometry };
|
|
4335
4364
|
capFrom[loAt] = null;
|
|
4336
4365
|
parts.push({ limb: l, segment: k, part: sg.hybrid.part, of: sg.from, record: { name: l, segment: k, of: base.of, bone: sg.bone, joint: sg.joint, band: geometry } });
|
|
4337
|
-
|
|
4366
|
+
const writers = source.writers?.slice();
|
|
4367
|
+
pieces.push({ ...source, writers, r, region, merged: mergedIn(source, region) });
|
|
4338
4368
|
capFrom.push(null);
|
|
4339
4369
|
// Existing piece objects refer to the same raster and mask.
|
|
4340
4370
|
for (const i of [upAt, loAt]) (pieces[i] as Built).merged = mergedIn(pieces[i] as Built, regions[i]);
|
|
@@ -4621,6 +4651,19 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4621
4651
|
const coverFootprints = new Map<string, Mask>();
|
|
4622
4652
|
const limbSources = new Set(Object.values(input.limbs ?? {}).flatMap((l) => l.segments.map((sg) => sg.from)));
|
|
4623
4653
|
const limbPlans = new Set([...limbSources, ...cuts.filter((c) => limbSources.has(c.into)).map((c) => c.from)]);
|
|
4654
|
+
const editNames: string[] = [];
|
|
4655
|
+
const editId = (name: string): number => {
|
|
4656
|
+
const old = editNames.indexOf(name);
|
|
4657
|
+
if (old >= 0) return old + 1;
|
|
4658
|
+
editNames.push(name);
|
|
4659
|
+
return editNames.length;
|
|
4660
|
+
};
|
|
4661
|
+
const cutIds = cuts.map((_, i) => editId(`cuts[${i}]`));
|
|
4662
|
+
const patchIds = patches.map((_, i) => editId(`patches[${i}]`));
|
|
4663
|
+
const layerWriters = new Map<string, Int32Array>();
|
|
4664
|
+
const valueBaselines = new Map<string, ValueBaseline | null>();
|
|
4665
|
+
// Extend-only rasters are still projected/merged, but are never emitted as own sources.
|
|
4666
|
+
const readKeys = new Set(sourcePlan.map(([, run, tag]) => `${run}:${tag}`));
|
|
4624
4667
|
const rig = new Map<string, Raster>();
|
|
4625
4668
|
const stats = new Map<string, ProjectionStats>();
|
|
4626
4669
|
const ghost: Record<string, number> = {};
|
|
@@ -4645,7 +4688,59 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4645
4688
|
coverFootprints.set(`${run}:${l.tag}`, cover);
|
|
4646
4689
|
return mask;
|
|
4647
4690
|
});
|
|
4648
|
-
|
|
4691
|
+
// Normalize the unedited input to the same rig grid. Cleanup effects on
|
|
4692
|
+
// retained samples are named; split/underfill outputs are prepared writes.
|
|
4693
|
+
const writes = layers.map((l, i): Int32Array | null => {
|
|
4694
|
+
const key = `${run}:${l.tag}`;
|
|
4695
|
+
if (!readKeys.has(key)) return null;
|
|
4696
|
+
const ids = new Int32Array(W * H);
|
|
4697
|
+
const originalSet = run === 'full' ? fullPadded : depthHeadInput;
|
|
4698
|
+
const original = originalSet.layers.find(q => q.name === l.tag);
|
|
4699
|
+
const prepared = set.layers.find(q => q.name === l.tag);
|
|
4700
|
+
let preparedWriter = 0;
|
|
4701
|
+
if (run === 'head' && prepared?.pixels !== original?.pixels) preparedWriter = AW.headFaceUnderfill;
|
|
4702
|
+
if (run === 'full') {
|
|
4703
|
+
const arm = fullArmSplit.layers.find(q => q.name === l.tag);
|
|
4704
|
+
if (prepared?.pixels !== arm?.pixels || arm?.pixels !== original?.pixels) preparedWriter = AW.armSplit;
|
|
4705
|
+
}
|
|
4706
|
+
const author = l.tag.startsWith('author:') ? editId(key) : 0;
|
|
4707
|
+
// Split-generated tags retain their actual original run contributors,
|
|
4708
|
+
// never the prepared/split raster as their baseline. arm_split copies
|
|
4709
|
+
// hand layers in input order; the merged-arm split inherits that source.
|
|
4710
|
+
const splitTag = /^(handwear|drape|hand)-[rl]$/.test(l.tag);
|
|
4711
|
+
const parentTag = l.tag.replace(/^(drape|hand)-/, 'handwear-');
|
|
4712
|
+
const sourceLayers = run === 'full' && splitTag && preparedWriter === AW.armSplit
|
|
4713
|
+
? input.armSplit === undefined ? fullPadded.layers.filter(q => q.name === parentTag) : fullPadded.layers.filter(q => q.name === 'handwear-r' || q.name === 'handwear-l')
|
|
4714
|
+
: original === undefined ? [] : [original];
|
|
4715
|
+
let raw: Raster | null = null;
|
|
4716
|
+
if (author === 0 && sourceLayers.length > 0) {
|
|
4717
|
+
// A sole layer needs no composite copy: layerToRig premultiplies,
|
|
4718
|
+
// so discarded RGB at alpha 0 has exactly the same float32 input.
|
|
4719
|
+
const canvas = sourceLayers.length === 1 ? placeLayer(sourceLayers[0], input.resolution) : newRaster(input.resolution, input.resolution);
|
|
4720
|
+
if (sourceLayers.length > 1) for (const originalLayer of sourceLayers) {
|
|
4721
|
+
const sourceCanvas = placeLayer(originalLayer, input.resolution);
|
|
4722
|
+
for (let p = 0; p < input.resolution * input.resolution; p++) if (sourceCanvas.data[p * 4 + 3] > 0) canvas.data.set(sourceCanvas.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
4723
|
+
}
|
|
4724
|
+
raw = layerToRig(canvas, frame, run);
|
|
4725
|
+
}
|
|
4726
|
+
// Reuse only a proved identical placed input, never wrapper identity.
|
|
4727
|
+
const samePlacedSource = raw !== null && sourceLayers.length === 1 && prepared !== undefined &&
|
|
4728
|
+
sourceLayers[0].pixels === prepared.pixels && sourceLayers[0].left === prepared.left && sourceLayers[0].top === prepared.top;
|
|
4729
|
+
const preparedRig = samePlacedSource ? raw : prepared === undefined ? null : layerToRig(placeLayer(prepared, input.resolution), frame, run);
|
|
4730
|
+
for (let p = 0; p < W * H; p++) {
|
|
4731
|
+
if (rigLayers[i].data[p * 4 + 3] === 0) continue;
|
|
4732
|
+
ids[p] = author;
|
|
4733
|
+
if (preparedWriter !== 0 && (raw === null || preparedRig !== null && !rgbaEqual(raw.data, p * 4, preparedRig.data, p * 4))) ids[p] = preparedWriter;
|
|
4734
|
+
if (preparedRig !== null && !rgbaEqual(preparedRig.data, p * 4, rigLayers[i].data, p * 4)) ids[p] = AW.ghostCleanup;
|
|
4735
|
+
}
|
|
4736
|
+
// Keep only samples that could equal a final opaque pixel; full raw and
|
|
4737
|
+
// prepared normalization temporaries end with this layer's callback.
|
|
4738
|
+
valueBaselines.set(key, raw === null ? null : compactValueBaseline(raw));
|
|
4739
|
+
return ids;
|
|
4740
|
+
});
|
|
4741
|
+
const st = projectSource(rigLayers, srcr, frame, run, projectRule, rank, keep, (i, p) => { if (writes[i] !== null) (writes[i] as Int32Array)[p] = AW.sourceProjection; });
|
|
4742
|
+
writes.forEach((ids, i) => { if (ids !== null) layerWriters.set(`${run}:${layers[i].tag}`, ids); });
|
|
4743
|
+
writes.length = 0;
|
|
4649
4744
|
layers.forEach((l, i) => {
|
|
4650
4745
|
rig.set(`${run}:${l.tag}`, rigLayers[i]);
|
|
4651
4746
|
stats.set(`${run}:${l.tag}`, st[i]);
|
|
@@ -4775,6 +4870,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4775
4870
|
const extendKeys = extend.filter((e) => e.part === name).map((e) => `${e.run}:${e.tag}`);
|
|
4776
4871
|
const extras = extendKeys.map((k) => rig.get(k) as Raster);
|
|
4777
4872
|
let merged = 0;
|
|
4873
|
+
const writers = (layerWriters.get(key) as Int32Array).slice();
|
|
4778
4874
|
const trace: MergeTrace = { from: new Int32Array(W * H).fill(-1), ring: newMask(W, H) };
|
|
4779
4875
|
if (extras.length > 0) {
|
|
4780
4876
|
const front = newMask(W, H);
|
|
@@ -4782,7 +4878,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4782
4878
|
const f = rig.get(`${run2}:${tag2}`) as Raster;
|
|
4783
4879
|
for (let p = 0; p < W * H; p++) if (f.data[p * 4 + 3] >= FRONT_ALPHA) front.data[p] = 1;
|
|
4784
4880
|
}
|
|
4785
|
-
merged = mergeBelowCrop(r, extras, front, srcr, frame.headBottom, trace);
|
|
4881
|
+
merged = mergeBelowCrop(r, extras, front, srcr, frame.headBottom, trace, (p, stage) => { writers[p] = AW[stage]; });
|
|
4786
4882
|
}
|
|
4787
4883
|
const { x1, opaque } = measureBox(r);
|
|
4788
4884
|
if (opaque === 0) {
|
|
@@ -4793,7 +4889,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4793
4889
|
});
|
|
4794
4890
|
return null;
|
|
4795
4891
|
}
|
|
4796
|
-
return { key, r, st, extendKeys, trace, merged, region: null };
|
|
4892
|
+
return { key, r, st, extendKeys, trace, merged, region: null, writers };
|
|
4797
4893
|
};
|
|
4798
4894
|
plan.forEach(([name, run, tag], pi) => { built[pi] = buildSource(name, run, tag, pi); });
|
|
4799
4895
|
// Snapshot independent piece pixels before any parent's subtraction or under fill.
|
|
@@ -4802,6 +4898,9 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4802
4898
|
const row = plan[pi], tag = input.cutLayers?.get(c.into);
|
|
4803
4899
|
return tag === undefined ? null : buildSource(c.from, row[1], tag, pi);
|
|
4804
4900
|
});
|
|
4901
|
+
// Every Built owns its writer slice. No source-map lookup follows this
|
|
4902
|
+
// last cut-layer build; drop originals before cut/limb working sets grow.
|
|
4903
|
+
layerWriters.clear();
|
|
4805
4904
|
const silhouette = seamRule === 'silhouette' || patches.some((q) => q.alpha === 'silhouette') ? figureSilhouette(srcr) : null;
|
|
4806
4905
|
const cut = patches.map((q, i) => {
|
|
4807
4906
|
const c = cutPatch(q, srcr, silhouette);
|
|
@@ -4863,7 +4962,9 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4863
4962
|
if (band.data[p] === 1) nb++;
|
|
4864
4963
|
}
|
|
4865
4964
|
}
|
|
4866
|
-
|
|
4965
|
+
const writers = (b.writers as Int32Array).slice();
|
|
4966
|
+
// A retained base band is not written: it keeps its earlier writer.
|
|
4967
|
+
pieces[i] = { ...b, r, writers, region: m, merged: mergedIn(b, m) };
|
|
4867
4968
|
bands.push(nb);
|
|
4868
4969
|
return n;
|
|
4869
4970
|
});
|
|
@@ -4912,6 +5013,10 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4912
5013
|
// Filled where the image is at least as opaque as the layer was; left empty elsewhere (`fillUnder`).
|
|
4913
5014
|
// With under_fill "colour" (issue #360) a filled pixel keeps the alpha the cut removed and takes the image's colour only.
|
|
4914
5015
|
const f = fillUnder(b.r, (pieces[i] as Built).r, cutBand(cutMasks[i], cuts[i].overlap), u, underBox(cuts[i].polygon), cuts[i].under_fill);
|
|
5016
|
+
const band = cutBand(cutMasks[i], cuts[i].overlap);
|
|
5017
|
+
let opaqueFilled = 0;
|
|
5018
|
+
// The under operation wrote only removed pixels it actually filled.
|
|
5019
|
+
for (let p = 0; p < W * H; p++) if (cutMasks[i].data[p] === 1 && band.data[p] === 0 && (pieces[i] as Built).r.data[p * 4 + 3] > 0 && b.r.data[p * 4 + 3] > 0) { (b.writers as Int32Array)[p] = cutIds[i]; if (b.r.data[p * 4 + 3] > OPAQUE_ALPHA_ABOVE) opaqueFilled++; }
|
|
4915
5020
|
// A hidden-art layer (issue #359) is the whole layer's covered art, read by every cut of its part: a cut it holds nothing
|
|
4916
5021
|
// under fills 0, which the cut line counts, and is not refused as a file made for this one cut would be.
|
|
4917
5022
|
if (f.filled === 0 && input.cuts[i].under !== undefined) {
|
|
@@ -4921,13 +5026,14 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4921
5026
|
detail: `${cuts[i].under} fills 0 of the ${f.empty} pixel(s) the cut removed from "${name}": at every one its alpha is below the alpha removed (an all-transparent image, or another file); an image that fills at least one is required — to leave the whole hole, drop under`,
|
|
4922
5027
|
});
|
|
4923
5028
|
}
|
|
4924
|
-
return f;
|
|
5029
|
+
return { ...f, opaqueFilled };
|
|
4925
5030
|
});
|
|
4926
5031
|
mine.forEach((i, k) => {
|
|
4927
5032
|
const c: CutCount = { from: name, into: cuts[i].into, before, kept, taken: taken[k], band: bands[k] };
|
|
4928
5033
|
const f = filled[k];
|
|
4929
5034
|
if (f !== undefined) {
|
|
4930
5035
|
c.under = f.filled;
|
|
5036
|
+
c.underOpaque = f.opaqueFilled;
|
|
4931
5037
|
c.underEmpty = f.empty;
|
|
4932
5038
|
if (cuts[i].under_fill === 'colour') c.underFill = 'colour';
|
|
4933
5039
|
}
|
|
@@ -4941,7 +5047,8 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4941
5047
|
const m = cutMasks[i], r = newRaster(W, H);
|
|
4942
5048
|
for (let p = 0; p < W * H; p++) if (m.data[p] === 1) r.data.set(source.r.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
4943
5049
|
if (opaqueOf(r) === 0) empty.push({ code: 'ASSEMBLE_CUT_PIXELS', object: `config.assemble.cuts[${i}] (part "${cuts[i].into}")`, detail: `takes 0 opaque pixels from selected ${source.key}; draw its polygon over that layer's art` });
|
|
4944
|
-
|
|
5050
|
+
const writers = (source.writers as Int32Array).slice();
|
|
5051
|
+
pieces[i] = { ...source, r, writers, region: m, merged: mergedIn(source, m) };
|
|
4945
5052
|
});
|
|
4946
5053
|
refuseIfAny(empty);
|
|
4947
5054
|
// In cut order, as the config lists them.
|
|
@@ -5064,7 +5171,9 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5064
5171
|
});
|
|
5065
5172
|
const featheredInOrder: Array<Mask | null> = order.map((d) => ('patch' in d ? null : (pieceAt(d).b.feathered ?? null)));
|
|
5066
5173
|
const keepInOrder = order.map((d) => ('limb' in d ? limbKeep[d.limb] : null));
|
|
5067
|
-
const
|
|
5174
|
+
const patchWriters = patches.map((_, i) => new Int32Array(W * H).fill(patchIds[i]));
|
|
5175
|
+
const writersInOrder = order.map(d => 'patch' in d ? patchWriters[d.patch] : pieceAt(d).b.writers as Int32Array);
|
|
5176
|
+
const pushedBy = pushBackFringe(stack, srcr, seamRule, seamRule === 'silhouette' ? silhouette : null, featheredInOrder, keepInOrder, input.turnMovingPair ?? null, (i, p) => { writersInOrder[i][p] = AW.fringeRestore; });
|
|
5068
5177
|
// Issue #255: a part's drift refusals in a body, and who holds the body in the draw order (the push-back clears
|
|
5069
5178
|
// only fringe below CORE_ALPHA; these are the holders before the opt-in yield, and the seam override only recolours).
|
|
5070
5179
|
const holderAt = new Int32Array(W * H).fill(-1);
|
|
@@ -5242,7 +5351,14 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5242
5351
|
for (const p of placed) if (p.record.depth !== undefined && artFiles.has(p.record.depth.image)) refuseIfAny([{ code: 'DEPTH_SHEET_UNAMBIGUOUS', object: p.record.depth.image, detail: 'collides with a part artwork filename; choose noncolliding part names before sheet emission' }]);
|
|
5243
5352
|
|
|
5244
5353
|
|
|
5245
|
-
seamOverride(placed, srcr, seamRule, seamRule === 'silhouette' ? silhouette : null, keepInOrder, input.turnMovingPair ?? null);
|
|
5354
|
+
seamOverride(placed, srcr, seamRule, seamRule === 'silhouette' ? silhouette : null, keepInOrder, input.turnMovingPair ?? null, (i, p) => { writersInOrder[i][p] = AW.seamOverride; });
|
|
5355
|
+
placed.forEach((p, i) => { p.record.provenance = pixelProvenance(p.image, p.record.x, p.record.y, W, writersInOrder[i], editNames, 0, valueBaselines.get(p.record.from) ?? null); });
|
|
5356
|
+
// Provenance is now numbers/labels only. No later raster/figure operation
|
|
5357
|
+
// reads report buffers; release all aliases before recomposite/error work.
|
|
5358
|
+
valueBaselines.clear();
|
|
5359
|
+
writersInOrder.length = 0;
|
|
5360
|
+
patchWriters.length = 0;
|
|
5361
|
+
for (const group of [built, pieces, cutSources, limbPieces]) for (const b of group) if (b !== null) b.writers = undefined;
|
|
5246
5362
|
const can = recomposite(placed, W, H);
|
|
5247
5363
|
// Issue #242: on a scene painting the figure is read off the full run, and every recomposite figure inside it.
|
|
5248
5364
|
const inside = input.scene === true ? sceneFigure([...rig.entries()].filter(([k]) => k.startsWith('full:')).map(([, r]) => r), W, H) : null;
|
|
@@ -5272,6 +5388,9 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5272
5388
|
scale_rig_per_source: frame.S,
|
|
5273
5389
|
...(input.motion_seams === undefined ? {} : { motion_seams: input.motion_seams.map(({ part, neighbour }) => ({ part, neighbour })) }),
|
|
5274
5390
|
parts: placed.map((p) => p.record),
|
|
5391
|
+
...(cutCounts.length === 0 ? {} : { cut_ownership: cutCounts.map((c, i) => ({ edit: `cuts[${i}]`, from: c.from, into: c.into, moved_in_opaque_px: c.taken,
|
|
5392
|
+
retained_band_opaque_px: c.band, cleared_opaque_px: c.taken - c.band, under_filled_px: c.under ?? 0,
|
|
5393
|
+
under_filled_opaque_px: c.underOpaque ?? 0, net_cleared_opaque_px: c.taken - c.band - (c.underOpaque ?? 0) })) }),
|
|
5275
5394
|
ghost_px: ghost,
|
|
5276
5395
|
recomposite: recompositeRecord(figures),
|
|
5277
5396
|
...(sceneRecord === null ? {} : { scene: sceneRecord }),
|
package/src/build.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownershipLines, provenanceLines } from './provenance.ts';
|
|
1
2
|
import { readSourceDepth } from './depthsheets.ts';
|
|
2
3
|
import { orderedBaseAnchor } from './baseanchor.ts';
|
|
3
4
|
import { alternateLayerSources, alternateLayerIslands, alternateLayerOthers } from './alternatelayers.ts';
|
|
@@ -705,6 +706,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
705
706
|
...(altFront.size === 0 ? {} : { front: [...altFront] }),
|
|
706
707
|
slots,
|
|
707
708
|
records: keptRecords,
|
|
709
|
+
...(altRes.parts.cut_ownership === undefined ? {} : { cut_ownership: altRes.parts.cut_ownership }),
|
|
708
710
|
ghost_px: altRes.parts.ghost_px,
|
|
709
711
|
recomposite: altRecomposite,
|
|
710
712
|
...(altRes.parts.scene === undefined ? {} : { scene: altRes.parts.scene }),
|
|
@@ -782,6 +784,9 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
782
784
|
`unproj=${String(p.visible_not_projected_px).padStart(5)} drift=${String(p.refused_drift_px).padStart(5)} ${driftShare(p.refused_drift_px, p.projected_core_px)} merged=${p.merged_px} seam=${p.seam_override_px}${underfillSuffix}`,
|
|
783
785
|
);
|
|
784
786
|
}
|
|
787
|
+
for (const line of provenanceLines(result.parts.parts)) log(line);
|
|
788
|
+
for (const line of ownershipLines(result.parts.cut_ownership ?? [])) log(line);
|
|
789
|
+
for (const alt of altBuilt) for (const line of [...provenanceLines(alt.record.records), ...ownershipLines(alt.record.cut_ownership ?? [])]) log(` set "${alt.record.name}" ${line.trimStart()}`);
|
|
785
790
|
// Issue #256: one line per part from an author-made layer, so it never reads as See-through's. None without one.
|
|
786
791
|
for (const l of authorPartLines(result.parts.parts)) log(l);
|
|
787
792
|
// One line per plan part an assemble.cuts entry cut, before the fringe lines: the cut is made first. None without cuts.
|
package/src/parts.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AUTOMATIC_WRITERS, provenanceAddsUp, type PixelProvenance, type CutOwnership } from './provenance.ts';
|
|
1
2
|
import { baseAnchorProblems, orderedBaseAnchor, type BaseAnchor } from './baseanchor.ts';
|
|
2
3
|
import { partDepthProblems, type PartDepthReference } from './depthsheets.ts';
|
|
3
4
|
/**
|
|
@@ -96,6 +97,8 @@ import { edgeLength } from './length.ts';
|
|
|
96
97
|
import { recordedFocusProblems } from './setfocus.ts';
|
|
97
98
|
|
|
98
99
|
export interface PartRecord {
|
|
100
|
+
/** Report-only final last-writer census (#817); absent on older records. */
|
|
101
|
+
provenance?: PixelProvenance;
|
|
99
102
|
/** Optional #773 input reference; production geometry does not consume it yet. */
|
|
100
103
|
depth?: PartDepthReference;
|
|
101
104
|
name: string;
|
|
@@ -326,6 +329,7 @@ export interface AlternateSetMap {
|
|
|
326
329
|
export const FRONT_SUFFIX = '_front';
|
|
327
330
|
|
|
328
331
|
export interface AlternatePartsRecord {
|
|
332
|
+
cut_ownership?: CutOwnership[];
|
|
329
333
|
name: string;
|
|
330
334
|
base_anchor?: BaseAnchor;
|
|
331
335
|
/** Original authored painting point and its source size, only when supplied. */
|
|
@@ -358,6 +362,8 @@ export interface AlternatePartsRecord {
|
|
|
358
362
|
}
|
|
359
363
|
|
|
360
364
|
export interface PartsFile {
|
|
365
|
+
/** Structural moves, independent of per-pixel value equality. */
|
|
366
|
+
cut_ownership?: CutOwnership[];
|
|
361
367
|
rig_size: [number, number];
|
|
362
368
|
scale_rig_per_source: number;
|
|
363
369
|
parts: PartRecord[];
|
|
@@ -406,8 +412,8 @@ export interface SceneRecord {
|
|
|
406
412
|
figure_mask: string;
|
|
407
413
|
}
|
|
408
414
|
|
|
409
|
-
const TOP_KEYS = ['rig_size', 'scale_rig_per_source', 'parts', 'ghost_px', 'recomposite', 'scene', 'single_run_pad', 'alternates', 'motion_seams'] as const;
|
|
410
|
-
const REQUIRED_TOP_KEYS = TOP_KEYS.filter((k) => k !== 'recomposite' && k !== 'scene' && k !== 'single_run_pad' && k !== 'alternates' && k !== 'motion_seams');
|
|
415
|
+
const TOP_KEYS = ['rig_size', 'scale_rig_per_source', 'parts', 'ghost_px', 'recomposite', 'scene', 'single_run_pad', 'alternates', 'motion_seams', 'cut_ownership'] as const;
|
|
416
|
+
const REQUIRED_TOP_KEYS = TOP_KEYS.filter((k) => k !== 'recomposite' && k !== 'scene' && k !== 'single_run_pad' && k !== 'alternates' && k !== 'motion_seams' && k !== 'cut_ownership');
|
|
411
417
|
export const SINGLE_RUN_PAD_KEYS = ['file_size', 'side', 'left', 'top'] as const;
|
|
412
418
|
export const SCENE_KEYS = ['figure_alpha_above', 'figure_px', 'figure_mask'] as const;
|
|
413
419
|
export const RECOMPOSITE_KEYS = ['mean_abs', 'within_limit', 'within_share', 'error_limit', 'error_px', 'covered_alpha', 'uncovered_error_px', 'hole_count', 'holes_listed', 'holes'] as const;
|
|
@@ -423,6 +429,7 @@ export const PART_KEYS = [
|
|
|
423
429
|
'w',
|
|
424
430
|
'h',
|
|
425
431
|
'opaque_px',
|
|
432
|
+
'provenance',
|
|
426
433
|
'visible_px',
|
|
427
434
|
'occluded_px',
|
|
428
435
|
'projected_core_px',
|
|
@@ -441,10 +448,10 @@ const FRINGE_KEY = 'fringe_pushed_back';
|
|
|
441
448
|
const CUT_FROM_KEY = 'cut_from';
|
|
442
449
|
/** The one part field that is a record of its own: a limb segment's provenance (issue #349), written only on one. */
|
|
443
450
|
const LIMB_KEY = 'limb';
|
|
444
|
-
const COUNT_KEYS = PART_KEYS.slice(2).filter((k) => k !== FRINGE_KEY && k !== CUT_FROM_KEY && k !== LIMB_KEY && k !== 'depth') as ReadonlyArray<(typeof PART_KEYS)[number]>;
|
|
451
|
+
const COUNT_KEYS = PART_KEYS.slice(2).filter((k) => k !== FRINGE_KEY && k !== CUT_FROM_KEY && k !== LIMB_KEY && k !== 'depth' && k !== 'provenance') as ReadonlyArray<(typeof PART_KEYS)[number]>;
|
|
445
452
|
/** The port's own counts: all three or none (none = a record the reference wrote). */
|
|
446
453
|
export const VISIBILITY_KEYS = ['visible_px', 'occluded_px', 'visible_not_projected_px'] as const;
|
|
447
|
-
const REQUIRED_KEYS = PART_KEYS.filter((k) => !(VISIBILITY_KEYS as readonly string[]).includes(k) && k !== FRINGE_KEY && k !== CUT_FROM_KEY && k !== LIMB_KEY && k !== 'depth');
|
|
454
|
+
const REQUIRED_KEYS = PART_KEYS.filter((k) => !(VISIBILITY_KEYS as readonly string[]).includes(k) && k !== FRINGE_KEY && k !== CUT_FROM_KEY && k !== LIMB_KEY && k !== 'depth' && k !== 'provenance');
|
|
448
455
|
|
|
449
456
|
/** Where a part came from: a See-through run and a v3 tag or an author-made layer's `author:<label>` (issue #256), or the painting itself for a patch. */
|
|
450
457
|
export type From = { run: 'full' | 'head'; tag: string } | { run: 'painting'; patch: string };
|
|
@@ -467,9 +474,34 @@ export function tagOf(from: string): string | null {
|
|
|
467
474
|
return f === null || f.run === 'painting' ? null : f.tag;
|
|
468
475
|
}
|
|
469
476
|
|
|
477
|
+
const OWNERSHIP_KEYS = ['edit', 'from', 'into', 'moved_in_opaque_px', 'retained_band_opaque_px', 'cleared_opaque_px', 'under_filled_px', 'under_filled_opaque_px', 'net_cleared_opaque_px'] as const;
|
|
478
|
+
function orderedOwnership(rows: readonly CutOwnership[]): Array<Record<string, unknown>> {
|
|
479
|
+
return rows.map(r => Object.fromEntries(OWNERSHIP_KEYS.map(k => [k, r[k]])));
|
|
480
|
+
}
|
|
481
|
+
function checkOwnership(raw: unknown, object: string, fail: (code: string, object: string, detail: string) => void): void {
|
|
482
|
+
if (!Array.isArray(raw)) { fail('PARTS_FIELD_TYPE', object, `is ${show(raw)}; an array of indexed cut ownership records is required`); return; }
|
|
483
|
+
const seen = new Set<string>();
|
|
484
|
+
raw.forEach((row, i) => {
|
|
485
|
+
const r = row as CutOwnership | null;
|
|
486
|
+
const shape = r !== null && typeof r === 'object' && !Array.isArray(r) && Object.keys(r).sort().join(',') === [...OWNERSHIP_KEYS].sort().join(',') &&
|
|
487
|
+
typeof r.edit === 'string' && /^cuts\[\d+\]$/.test(r.edit) && !seen.has(r.edit) && typeof r.from === 'string' && r.from !== '' && typeof r.into === 'string' && r.into !== '' &&
|
|
488
|
+
OWNERSHIP_KEYS.slice(3).every(k => Number.isSafeInteger(r[k]) && (k === 'net_cleared_opaque_px' || (r[k] as number) >= 0));
|
|
489
|
+
if (!shape) { fail('PARTS_FIELD_TYPE', `${object}[${i}]`, `is ${show(row)}; a distinct cuts[i] with from/into names, nonnegative move/clear/fill counts and signed net_cleared_opaque_px is required`); return; }
|
|
490
|
+
const q = r as CutOwnership; seen.add(q.edit);
|
|
491
|
+
if (q.moved_in_opaque_px - q.retained_band_opaque_px !== q.cleared_opaque_px || q.cleared_opaque_px - q.under_filled_opaque_px !== q.net_cleared_opaque_px || q.under_filled_opaque_px > q.under_filled_px) {
|
|
492
|
+
fail('PARTS_COUNTS_ADD_UP', `${object}[${i}]`, `moved ${q.moved_in_opaque_px} - retained ${q.retained_band_opaque_px} = ${q.moved_in_opaque_px - q.retained_band_opaque_px} (gross cleared ${q.cleared_opaque_px} required); cleared - opaque fill ${q.under_filled_opaque_px} = ${q.cleared_opaque_px - q.under_filled_opaque_px} (net ${q.net_cleared_opaque_px} required); opaque fill must be at most total fill ${q.under_filled_px}`);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function orderedProvenance(p: PixelProvenance): PixelProvenance {
|
|
498
|
+
return { unchanged_layer_px: p.unchanged_layer_px, hand_px: p.hand_px, automatic_px: p.automatic_px,
|
|
499
|
+
edits: p.edits.map(e => ({ edit: e.edit, px: e.px })), automatic: p.automatic.map(e => ({ stage: e.stage, px: e.px })) };
|
|
500
|
+
}
|
|
501
|
+
|
|
470
502
|
function serializePartList(parts: readonly PartRecord[]): Array<Record<string, unknown>> {
|
|
471
503
|
return parts.map((p) =>
|
|
472
|
-
Object.fromEntries(PART_KEYS.filter((k) => p[k] !== undefined).map((k) => [k, k === FRINGE_KEY ? (p.fringe_pushed_back ?? []).map((q) => ({ part: q.part, px: q.px })) : k === LIMB_KEY ? orderedLimb(p.limb as LimbRecord) : p[k]])),
|
|
504
|
+
Object.fromEntries(PART_KEYS.filter((k) => p[k] !== undefined).map((k) => [k, k === FRINGE_KEY ? (p.fringe_pushed_back ?? []).map((q) => ({ part: q.part, px: q.px })) : k === LIMB_KEY ? orderedLimb(p.limb as LimbRecord) : k === 'provenance' ? orderedProvenance(p.provenance as PixelProvenance) : p[k]])),
|
|
473
505
|
);
|
|
474
506
|
}
|
|
475
507
|
|
|
@@ -485,6 +517,7 @@ export function serializeParts(file: PartsFile): string {
|
|
|
485
517
|
rig_size: file.rig_size,
|
|
486
518
|
scale_rig_per_source: file.scale_rig_per_source,
|
|
487
519
|
parts: serializePartList(file.parts),
|
|
520
|
+
...(file.cut_ownership === undefined ? {} : { cut_ownership: orderedOwnership(file.cut_ownership) }),
|
|
488
521
|
...(file.motion_seams === undefined ? {} : { motion_seams: file.motion_seams.map(({ part, neighbour }) => ({ part, neighbour })) }),
|
|
489
522
|
ghost_px: Object.fromEntries(Object.keys(file.ghost_px).sort().map((k) => [k, file.ghost_px[k]])),
|
|
490
523
|
...(file.recomposite === undefined ? {} : { recomposite: serializeRecomposite(file.recomposite) }),
|
|
@@ -501,6 +534,7 @@ export function serializeParts(file: PartsFile): string {
|
|
|
501
534
|
...(a.front === undefined ? {} : { front: a.front }),
|
|
502
535
|
slots: a.slots,
|
|
503
536
|
records: serializePartList(a.records),
|
|
537
|
+
...(a.cut_ownership === undefined ? {} : { cut_ownership: orderedOwnership(a.cut_ownership) }),
|
|
504
538
|
ghost_px: Object.fromEntries(Object.keys(a.ghost_px).sort().map((k) => [k, a.ghost_px[k]])),
|
|
505
539
|
recomposite: serializeRecomposite(a.recomposite),
|
|
506
540
|
...(a.scene === undefined ? {} : { scene: Object.fromEntries(SCENE_KEYS.map((k) => [k, (a.scene as SceneRecord)[k]])) }),
|
|
@@ -541,6 +575,30 @@ function show(v: unknown): string {
|
|
|
541
575
|
return v === undefined ? 'absent' : JSON.stringify(v);
|
|
542
576
|
}
|
|
543
577
|
|
|
578
|
+
/** The same optional measurement contract for base and alternate records. */
|
|
579
|
+
function checkProvenance(raw: unknown, opaque: unknown, object: string, fail: (code: string, object: string, detail: string) => void): void {
|
|
580
|
+
const v = raw as PixelProvenance | null;
|
|
581
|
+
const count = (n: unknown): boolean => Number.isSafeInteger(n) && (n as number) >= 0;
|
|
582
|
+
const edit = (name: string): boolean => /^(cuts\[\d+\]|patches\[\d+\])$/.test(name) ||
|
|
583
|
+
(/^(full|head):author:/.test(name) && readFrom(name) !== null);
|
|
584
|
+
const shape = v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
585
|
+
Object.keys(v).sort().join(',') === 'automatic,automatic_px,edits,hand_px,unchanged_layer_px' &&
|
|
586
|
+
[v.unchanged_layer_px, v.hand_px, v.automatic_px].every(count) && Array.isArray(v.edits) && Array.isArray(v.automatic) &&
|
|
587
|
+
v.edits.every(e => e !== null && typeof e === 'object' && Object.keys(e).sort().join(',') === 'edit,px' && typeof e.edit === 'string' && edit(e.edit) && count(e.px) && e.px > 0) &&
|
|
588
|
+
v.automatic.every(e => e !== null && typeof e === 'object' && Object.keys(e).sort().join(',') === 'px,stage' && typeof e.stage === 'string' && Object.hasOwn(AUTOMATIC_WRITERS, e.stage) && count(e.px) && e.px > 0) &&
|
|
589
|
+
new Set(v.edits.map(e => e.edit)).size === v.edits.length && new Set(v.automatic.map(e => e.stage)).size === v.automatic.length;
|
|
590
|
+
if (!shape) {
|
|
591
|
+
fail('PARTS_FIELD_TYPE', object, `is ${show(raw)}; unchanged_layer_px, hand_px and automatic_px as nonnegative safe integers, edits as distinct cuts[i]/patches[i]/<run>:author:<label> entries, and automatic as distinct known stage entries, each with a positive integer px, are required`);
|
|
592
|
+
} else if (!count(opaque)) {
|
|
593
|
+
fail('PARTS_FIELD_TYPE', object, `opaque_px is ${show(opaque)}; a nonnegative safe integer opaque_px is required beside provenance`);
|
|
594
|
+
} else if (!provenanceAddsUp(v as PixelProvenance, opaque as number)) {
|
|
595
|
+
const p = v as PixelProvenance;
|
|
596
|
+
const total = p.unchanged_layer_px + p.hand_px + p.automatic_px;
|
|
597
|
+
const hand = p.edits.reduce((n, e) => n + e.px, 0), automatic = p.automatic.reduce((n, e) => n + e.px, 0);
|
|
598
|
+
fail('PARTS_COUNTS_ADD_UP', object, `unchanged_layer_px ${p.unchanged_layer_px} + hand_px ${p.hand_px} + automatic_px ${p.automatic_px} = ${total}; opaque_px ${opaque} is required; edit subtotal ${hand} (hand_px ${p.hand_px} required), stage subtotal ${automatic} (automatic_px ${p.automatic_px} required)`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
544
602
|
function checkParts(raw: unknown, path: string): PartsFile {
|
|
545
603
|
const problems: Problem[] = [];
|
|
546
604
|
const fail = (code: string, object: string, detail: string): void => {
|
|
@@ -601,6 +659,7 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
601
659
|
if (k in r && !(Number.isInteger(r[k]) && (r[k] as number) >= 0)) fail('PARTS_FIELD_TYPE', `${label} field "${k}"`, `is ${show(r[k])}; a non-negative integer is required`);
|
|
602
660
|
}
|
|
603
661
|
const count = (k: string): number | null => (Number.isInteger(r[k]) && (r[k] as number) >= 0 ? (r[k] as number) : null);
|
|
662
|
+
if ('provenance' in r) checkProvenance(r.provenance, r.opaque_px, label + ' provenance', fail);
|
|
604
663
|
const [op, vis, occ, vnp] = ['opaque_px', 'visible_px', 'occluded_px', 'visible_not_projected_px'].map(count);
|
|
605
664
|
if (held.length === VISIBILITY_KEYS.length && op !== null && vis !== null && occ !== null && vnp !== null) {
|
|
606
665
|
if (vis + occ !== op) fail('PARTS_COUNTS_ADD_UP', `${label}`, `visible_px ${vis} + occluded_px ${occ} = ${vis + occ}; opaque_px ${op} is required`);
|
|
@@ -692,6 +751,7 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
692
751
|
if (!('recomposite' in o)) fail('PARTS_FIELD_PRESENT', `${path} field "recomposite"`, 'is absent; a scene block says how the recomposite block beside it was measured, so it is required with one');
|
|
693
752
|
}
|
|
694
753
|
}
|
|
754
|
+
if ('cut_ownership' in o) checkOwnership(o.cut_ownership, `${path} cut_ownership`, fail);
|
|
695
755
|
if ('single_run_pad' in o) checkSingleRunPad(o.single_run_pad, path, sizeOk ? (size as [number, number]) : null, typeof scale === 'number' ? scale : null, fail);
|
|
696
756
|
if ('recomposite' in o && sizeOk) {
|
|
697
757
|
const names = Array.isArray(o.parts) ? new Set(o.parts.map((p) => (typeof p === 'object' && p !== null ? (p as Record<string, unknown>).name : undefined)).filter((n): n is string => typeof n === 'string')) : new Set<string>();
|
|
@@ -706,6 +766,7 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
706
766
|
if (typeof a !== 'object' || a === null || Array.isArray(a)) return;
|
|
707
767
|
const set = a as Record<string, unknown>;
|
|
708
768
|
const at = `${path} alternates[${i}]${typeof set.name === 'string' ? ` (set "${set.name}")` : ''}`;
|
|
769
|
+
if ('cut_ownership' in set) checkOwnership(set.cut_ownership, `${at} cut_ownership`, fail);
|
|
709
770
|
if ('base_anchor' in set) {
|
|
710
771
|
const names = new Set(Array.isArray(set.records) ? set.records.flatMap(v => typeof v === 'object' && v !== null && typeof v.name === 'string' ? [v.name] : []) : []);
|
|
711
772
|
const patches = new Set(Array.isArray(set.records) ? set.records.flatMap(v => typeof v === 'object' && v !== null && typeof v.name === 'string' && typeof v.from === 'string' && v.from.startsWith('painting:') ? [v.name] : []) : []);
|
|
@@ -749,6 +810,8 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
749
810
|
set.records.forEach((p, j) => {
|
|
750
811
|
if (typeof p !== 'object' || p === null || Array.isArray(p)) return;
|
|
751
812
|
const r = p as Record<string, unknown>;
|
|
813
|
+
const part817 = typeof r.name === 'string' ? `part "${r.name}"` : `records[${j}]`;
|
|
814
|
+
if ('provenance' in r) checkProvenance(r.provenance, r.opaque_px, `${at} ${part817} provenance`, fail);
|
|
752
815
|
const l = r[LIMB_KEY];
|
|
753
816
|
if (typeof l !== 'object' || l === null || Array.isArray(l) || !('cap' in l)) return;
|
|
754
817
|
const part = typeof r.name === 'string' ? `part "${r.name}"` : `records[${j}]`;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Report-only last-writer accounting over final output pixels (#817). */
|
|
2
|
+
import { OPAQUE_ALPHA_ABOVE } from './layers.ts';
|
|
3
|
+
import type { PartRecord } from './parts.ts';
|
|
4
|
+
import { crop, type Raster } from './raster/index.ts';
|
|
5
|
+
|
|
6
|
+
/** Negative IDs distinguish automatic writes from positive, config-indexed hand edits. */
|
|
7
|
+
export const AUTOMATIC_WRITERS = {
|
|
8
|
+
sourceProjection: -1, extend: -2, growRim: -3, limbSplit: -4,
|
|
9
|
+
limbCap: -5, limbHybrid: -6, limbFeather: -7, fringeRestore: -8,
|
|
10
|
+
seamOverride: -9, armSplit: -10, headFaceUnderfill: -11, ghostCleanup: -12, unattributed: -13,
|
|
11
|
+
} as const;
|
|
12
|
+
export type AutomaticWriter = keyof typeof AUTOMATIC_WRITERS;
|
|
13
|
+
export interface PixelProvenance {
|
|
14
|
+
unchanged_layer_px: number;
|
|
15
|
+
hand_px: number;
|
|
16
|
+
automatic_px: number;
|
|
17
|
+
edits: Array<{ edit: string; px: number }>;
|
|
18
|
+
automatic: Array<{ stage: AutomaticWriter; px: number }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** RGBA equality on the same rig coordinate, including alpha. */
|
|
22
|
+
export function rgbaEqual(a: Uint8ClampedArray, ai: number, b: Uint8ClampedArray, bi: number): boolean {
|
|
23
|
+
return a[ai] === b[bi] && a[ai + 1] === b[bi + 1] && a[ai + 2] === b[bi + 2] && a[ai + 3] === b[bi + 3];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** An immutable crop of normalized raw samples that can equal an opaque output.
|
|
27
|
+
* Outside this box raw alpha is <= 8, so it cannot equal final alpha > 8.
|
|
28
|
+
* The crop copies RGBA; it must never alias a raster later projected in place. */
|
|
29
|
+
export interface ValueBaseline { readonly image: Raster; readonly x: number; readonly y: number }
|
|
30
|
+
export function compactValueBaseline(raw: Raster): ValueBaseline {
|
|
31
|
+
let x0 = raw.width, y0 = raw.height, x1 = -1, y1 = -1;
|
|
32
|
+
for (let y = 0; y < raw.height; y++) for (let x = 0; x < raw.width; x++) {
|
|
33
|
+
if (raw.data[(y * raw.width + x) * 4 + 3] <= OPAQUE_ALPHA_ABOVE) continue;
|
|
34
|
+
x0 = Math.min(x0, x); y0 = Math.min(y0, y); x1 = Math.max(x1, x); y1 = Math.max(y1, y);
|
|
35
|
+
}
|
|
36
|
+
return x1 < 0 ? { image: crop(raw, 0, 0, 0, 0), x: 0, y: 0 }
|
|
37
|
+
: { image: crop(raw, x0, y0, x1 - x0 + 1, y1 - y0 + 1), x: x0, y: y0 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Value equality with the raw normalized See-through baseline wins first.
|
|
41
|
+
* Otherwise the last stage that CHANGED RGBA wins; buffer copies inherit IDs.
|
|
42
|
+
* A null baseline is explicit for authored layers/painting patches. */
|
|
43
|
+
export function pixelProvenance(image: Raster, x: number, y: number, width: number, writers: Int32Array | null, edits: readonly string[], patchId = 0, baseline: Raster | ValueBaseline | null = null): PixelProvenance {
|
|
44
|
+
const out: PixelProvenance = { unchanged_layer_px: 0, hand_px: 0, automatic_px: 0, edits: [], automatic: [] };
|
|
45
|
+
const counts = new Map<number, number>();
|
|
46
|
+
const reference = baseline === null ? null : 'image' in baseline ? baseline.image : baseline;
|
|
47
|
+
const bx = baseline !== null && 'image' in baseline ? baseline.x : 0;
|
|
48
|
+
const by = baseline !== null && 'image' in baseline ? baseline.y : 0;
|
|
49
|
+
for (let j = 0; j < image.height; j++) for (let i = 0; i < image.width; i++) {
|
|
50
|
+
if (image.data[(j * image.width + i) * 4 + 3] <= OPAQUE_ALPHA_ABOVE) continue;
|
|
51
|
+
const q = (y + j) * width + x + i;
|
|
52
|
+
const u = x + i - bx, v = y + j - by;
|
|
53
|
+
const same = reference !== null && u >= 0 && v >= 0 && u < reference.width && v < reference.height &&
|
|
54
|
+
rgbaEqual(image.data, (j * image.width + i) * 4, reference.data, (v * reference.width + u) * 4);
|
|
55
|
+
const previous = writers?.[q] ?? patchId;
|
|
56
|
+
// Unknown lineage is reported explicitly, never invented as unchanged.
|
|
57
|
+
const id = same ? 0 : previous === 0 ? AUTOMATIC_WRITERS.unattributed : previous;
|
|
58
|
+
if (id > 0) out.hand_px++;
|
|
59
|
+
else if (id < 0) out.automatic_px++;
|
|
60
|
+
else out.unchanged_layer_px++;
|
|
61
|
+
if (id !== 0) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
out.edits = edits.flatMap((edit, i) => counts.has(i + 1) ? [{ edit, px: counts.get(i + 1) as number }] : []);
|
|
64
|
+
out.automatic = (Object.keys(AUTOMATIC_WRITERS) as AutomaticWriter[]).flatMap(stage => counts.has(AUTOMATIC_WRITERS[stage]) ? [{ stage, px: counts.get(AUTOMATIC_WRITERS[stage]) as number }] : []);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The reader and selftests share this arithmetic check; no hand-edit gate. */
|
|
69
|
+
export function provenanceAddsUp(p: PixelProvenance, opaque: number): boolean {
|
|
70
|
+
return p.unchanged_layer_px + p.hand_px + p.automatic_px === opaque &&
|
|
71
|
+
p.edits.reduce((n, e) => n + e.px, 0) === p.hand_px && p.automatic.reduce((n, e) => n + e.px, 0) === p.automatic_px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function provenanceLines(parts: readonly PartRecord[]): string[] {
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
let opaque = 0, unchanged = 0, automatic = 0, hand = 0;
|
|
77
|
+
const edits = new Map<string, number>(), stages = new Map<string, number>();
|
|
78
|
+
const detail = (e: ReadonlyMap<string, number>): string => [...e].map(([name, px]) => `${name} ${px}`).join(', ') || 'none';
|
|
79
|
+
for (const p of parts) {
|
|
80
|
+
const v = p.provenance;
|
|
81
|
+
if (v === undefined) continue;
|
|
82
|
+
opaque += p.opaque_px; unchanged += v.unchanged_layer_px; automatic += v.automatic_px; hand += v.hand_px;
|
|
83
|
+
for (const e of v.edits) edits.set(e.edit, (edits.get(e.edit) ?? 0) + e.px);
|
|
84
|
+
for (const e of v.automatic) stages.set(e.stage, (stages.get(e.stage) ?? 0) + e.px);
|
|
85
|
+
lines.push(` provenance: part "${p.name}" (${p.from}) opaque ${p.opaque_px} = unchanged layer ${v.unchanged_layer_px} + hand ${v.hand_px} + automatic ${v.automatic_px}; edits ${detail(new Map(v.edits.map(e => [e.edit, e.px])))}; stages ${detail(new Map(v.automatic.map(e => [e.stage, e.px])))}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push(` provenance total: opaque ${opaque} = unchanged layer ${unchanged} + hand ${hand} + automatic ${automatic}; edits ${detail(edits)}; stages ${detail(stages)}; output pixels, including copies in separate parts; value equality first; last value-changing stage wins`);
|
|
88
|
+
return lines;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Structural cut census, independent of the value partition. Counts are from
|
|
92
|
+
* the original parent partition before limb/fringe/seam processing. */
|
|
93
|
+
export interface CutOwnership {
|
|
94
|
+
edit: string;
|
|
95
|
+
from: string;
|
|
96
|
+
into: string;
|
|
97
|
+
moved_in_opaque_px: number;
|
|
98
|
+
retained_band_opaque_px: number;
|
|
99
|
+
cleared_opaque_px: number;
|
|
100
|
+
under_filled_px: number;
|
|
101
|
+
under_filled_opaque_px: number;
|
|
102
|
+
net_cleared_opaque_px: number;
|
|
103
|
+
}
|
|
104
|
+
export function ownershipLines(rows: readonly CutOwnership[]): string[] {
|
|
105
|
+
return rows.map(r => ` ownership: ${r.edit} "${r.from}" -> "${r.into}" moved in ${r.moved_in_opaque_px} opaque px; retained band ${r.retained_band_opaque_px}; gross cleared ${r.cleared_opaque_px} before refill; under filled ${r.under_filled_px} px (${r.under_filled_opaque_px} opaque); net cleared ${r.net_cleared_opaque_px}; independent of RGBA equality`);
|
|
106
|
+
}
|