rig-parts 2.5.0 → 2.7.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 +79 -4
- package/docs/STABILITY.md +7 -1
- package/package.json +1 -1
- package/src/assemble.ts +53 -2
- package/src/build.ts +7 -1
- package/src/check.ts +176 -20
- package/src/config.ts +9 -4
- package/src/paintingedges.ts +70 -4
- package/src/projectrule.ts +15 -0
package/docs/AUTHORING.md
CHANGED
|
@@ -149,6 +149,7 @@ read by no CPU stage).
|
|
|
149
149
|
| `seethrough.arm_split` | assemble, `assemble --propose-plan` — separates a single full-run `handwear-r` or `handwear-l` layer that holds both overlapping arms into both arm tags | **authored**, optional (issue #439): `{x?: number, line?: [[x, y], …], order: ["handwear-r", "handwear-l"] \| ["handwear-l", "handwear-r"]}` in `seethrough.resolution` full-run pixels. Exactly one of `x` (image column, `0 <= x <= resolution`) or `line` (a polyline of two or more `[x, y]` points in `[0, resolution]`, interpolated in x along y and clamped above/below its endpoints) is required (`CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE`), and `order` is `[back_tag, front_tag]`, the draw order of the two separated arm layers (`CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE`). Pixels at `< splitX(y)` go to `handwear-r` (image left, the subject's right arm) and pixels at `>= splitX(y)` go to `handwear-l` (image right, the subject's left arm). A split line that cuts a horizontal opaque run of the arm layer into more than two segments is refused (`ASSEMBLE_ARM_SPLIT_CUTS_ARM`). Without the declaration, when one arm layer's 8-connected component reaches both wrists under `propose --keypoints`, `propose` emits an `overlapping-arms` `LINT` naming the part, both wrists and `seethrough.arm_split`; absent, no byte of any output moves |
|
|
150
150
|
| `assemble.seam` | assemble, build | **authored**, optional `"near-white"` or `"silhouette"` (issue #905). An explicit `--seam` overrides the config; otherwise this field overrides the existing `near-white` default. An invalid value is `CONFIG_FIELD_TYPE` at `config.assemble.seam`, including when a CLI override is given. Absent, existing build bytes are unchanged. |
|
|
151
151
|
| `assemble.project` | assemble, build | **authored**, optional `"core"` or `"visible"` (issue #953): the projection rule for this character. An explicit `--project` overrides the config; otherwise this field overrides the existing `core` default. An invalid value is `CONFIG_FIELD_TYPE` at `config.assemble.project`, including when a CLI override is given. Absent, existing build bytes are unchanged. |
|
|
152
|
+
| `assemble.ground` | assemble, build | **authored**, optional `"plain"` or `"busy"` (issue #996): what lies behind the figure in the painting. `plain` (the default) is a ground of one colour; `busy` is an opaque ground of many colours (a wall, a banner). Read by `--project visible` only: a busy, opaque ground needs `assemble.ground: busy`; without it, visible takes backdrop pixels. An invalid value is `CONFIG_FIELD_TYPE` at `config.assemble.ground`. Absent or `plain`, existing build bytes are unchanged. `assemble` warns (`ASSEMBLE_GROUND_PLAIN`, §5), under project: visible, where paintingEdges reads the ground, when a plain ground's border ring is not one colour. |
|
|
152
153
|
| `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` |
|
|
153
154
|
| `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` |
|
|
154
155
|
| `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 |
|
|
@@ -3274,7 +3275,7 @@ step-up method below.
|
|
|
3274
3275
|
| `motion.duration` and `track.period` | `check`'s 12 fps grid (`1/12` s per frame) | Choose `N` frames (e.g. `N = 40`, `duration = 40 / 12 = 3.3333333333333335` s, or `N = 48`, `duration = 4.0` s) and set every body track's `period = duration`. `CHECK_LOOP_LAST_FRAME_AT_DURATION` requires `duration` to land on a `1/12` s tick, and `CONFIG_PERIOD_DIVIDES_DURATION` requires `duration / period` to be an integer. |
|
|
3275
3276
|
| Torso breath (`chest` `translatey`, `scalex`) | Figure height `H` (`parts.json` union height, kept near `1,100..1,200` rig px via `assemble.rig_scale`), torso span `L_torso = hypot(hip.x - neck.x, hip.y - neck.y)` from `bones`, and `BREATH_VISIBLE`'s `torso_heat_mean` vs `torso_floor` (pictured in `motion_heat.png`) | Floor: peak-to-peak vertical travel at `1.25 %` of `L_torso` (`amp = round2(0.00625 * L_torso)`, `base = amp`, `phase = 0.0`, i.e. `amp = 1.2` rig px on `demo`), paired with `chest` `scalex` `amp = 0.008`, `base = 1.008`, `phase = 0.0`. `check` scales the reference floor (`2.13` at `render_scale = 0.4211`) by `render_scale / 0.4211` and isolates the breath by posing only `chest`/`hip` `translate` and `scale` tracks. |
|
|
3276
3277
|
| Upper-body sway (`chest` → `neck` → `head` `rotate`) | Shoulder lever arm `R_shoulder = \|sleeve_s0.x - chest.x\|` in `bones`, and `CHAIN_LAG` / `TIP_OVER_ROOT` on `handwear` | Floor: `amp(chest) = 0.7°`, `amp(neck) = 0.85°`, `amp(head) = 1.0°` with lag `0.05` (`phase: 0.25, 0.30, 0.35`). **Set `chest` `rotate` `phase` to `0.25` (quadrature, 90° from `chest` `translatey`'s `phase: 0.0`), never near `0.0`:** at `phase ≈ 0`, vertical breath `y(t)` and shoulder lift `Δx · θ(t)` reinforce on one shoulder and cancel on the other, inflating `root_rig_px` on the reinforced sleeve and dropping `tip_rig_px / root_rig_px` below `1.4725`. At `phase = 0.25`, vertical breath and rotational sway are orthogonal on both shoulders alike. |
|
|
3277
|
-
| Sleeve physics (`sleeve_<s>0..2` under `chest`) | `CHECK_TIP_OVER_ROOT` (`ratio >= 1.4725` on each `handwear` part) and `CHECK_ANIMATIONS_DO_NOT_FOLD` | Set `"rotate": 1, "mix": 1, "inertia": 1.0, "damping": 0.75, "mass": 1` and taper `strength` down the chain: `[45, 30, 20]` for a 3-link sleeve (`1 : 0.67 : 0.44`). Stiff follow-through springs (`strength: 200`, `inertia: 0.2`) barely bend under a `3.333` s idle and move the sleeve as a rigid unit (`ratio ≈ 1.19..1.22`, failing `TIP_OVER_ROOT`); tapering `45 → 30 → 20` lets cumulative link rotation amplify cuff travel past `2.0x` the shoulder travel while keeping `0` folded triangles in `wave` and `hop`. |
|
|
3278
|
+
| Sleeve physics (`sleeve_<s>0..2` under `chest`) | `CHECK_TIP_OVER_ROOT` (`ratio >= 1.4725` on each `handwear` part) and `CHECK_ANIMATIONS_DO_NOT_FOLD` | Set `"rotate": 1, "mix": 1, "inertia": 1.0, "damping": 0.75, "mass": 1` and taper `strength` down the chain: `[45, 30, 20]` for a 3-link sleeve (`1 : 0.67 : 0.44`). Stiff follow-through springs (`strength: 200`, `inertia: 0.2`) barely bend under a `3.333` s idle and move the sleeve as a rigid unit (`ratio ≈ 1.19..1.22`, failing `TIP_OVER_ROOT`); tapering `45 → 30 → 20` lets cumulative link rotation amplify cuff travel past `2.0x` the shoulder travel while keeping `0` folded triangles in `wave` and `hop`. Those ratios are the world reading `TIP_OVER_ROOT` took until issue #987; it now reads the halves against `chest`, the bone the sleeves hang from, and this recipe on `demo` (the working build reproduced below) reads `9.383` and `9.952` there (`handwear_l`, `handwear_r`; `3.626` and `2.905` in the world, which the row keeps as `world_ratio`) — it passes the reading the line holds (rig-parts at `c17292c` with #987's change, rigc 3.2.1). |
|
|
3278
3279
|
| Hair physics (`hairback_*`, `bang_*` under `head`) | `CLOTH_SETTLES` (`>= 0.01694` rig px/s to the last frame), `CHECK_TEXTURE_STRETCH` (`<= 1.926544`), and `CHECK_ANIMATIONS_DO_NOT_FOLD` | Taper `strength` down each `n`-link chain by `strength_0 * (1 - 0.35 * i / (n - 1))`: on long back-hair chains, `strength_0 = 90` (`[90, 74.25, 58.5]` for 3 links), `inertia = 0.8`, `damping = 0.85`; on shorter fringe chains (`bang_*`), `strength = [130, 105]`, `inertia = 0.2`, `damping = 0.9`. |
|
|
3279
3280
|
| Skirt / `bottomwear` physics (`skirt_*` under `hip`) | `CHECK_TIP_OVER_ROOT` on `bottomwear` (`ratio >= 1.4725`), `CHECK_ANIMATIONS_DO_NOT_FOLD` on `hop`, and `CLOTH_SETTLES` | Keep high restoration `strength` (`[200, 165, 130]` for 3 links, `inertia = 0.5`, `damping = 0.82`) so `bottomwear` does not fold during `hop`. With `hip` unkeyed, floor `"wind": [0.4, 0.6, 0.8]` passes `TIP_OVER_ROOT` (`ratio 1.83`); with `hip` keyed in `rotate` (`amp <= 0.4°`, `phase: 0.20`), set `"wind": 0` on the upper-half link `skirt_*0` and put `"wind"` on the lower links (`[0, 15, 40]`) so `tip / root` stays above `1.4725` and `CLOTH_SETTLES` clears on the default world. |
|
|
3280
3281
|
|
|
@@ -3331,13 +3332,15 @@ at `../examples/demo/animations.json` and `generation.control.skeleton` at
|
|
|
3331
3332
|
loop: idle 41 frame(s) at 12 fps, f0000 vs f0040 (t = 3.333333333333333s) with the rig's 29 physics constraint(s) left out (named on the `loop physics:` line; rendered with them, the ends need not meet — `build --loop --lead-in <n>`): max |d| 0 (0 required)
|
|
3332
3333
|
BREATH_VISIBLE: PASS — torso_parts topwear; torso_heat_mean 48.993; torso_floor 3.809; feet_parts shoes; feet_heat_max 0; feet_ceiling 0
|
|
3333
3334
|
CHAIN_LAG: PASS — chains hip_ctl > chest_ctl > neck > head_ctl: period 3.333s, phase 0.200 0.250 0.300 0.350, amp 0.400 2.500 3.000 3.600; lags hip_ctl > chest_ctl +0.050 | chest_ctl > neck +0.050 | neck > head_ctl +0.050; min_step 0.05; step_floor 0.001; other_period none; unread none; first_violation null
|
|
3334
|
-
TIP_OVER_ROOT: PASS — instrument geometry; parts part handwear_l, root_rig_px
|
|
3335
|
+
TIP_OVER_ROOT: PASS — instrument geometry, carrier frame; parts part handwear_l, hang_from chest, root_rig_px 1.3, tip_rig_px 12.202, ratio 9.383, world_root_rig_px 5.786, world_tip_rig_px 20.983, world_ratio 3.626 | part handwear_r, hang_from chest, root_rig_px 1.053, tip_rig_px 10.48, ratio 9.952, world_root_rig_px 6.213, world_tip_rig_px 18.047, world_ratio 2.905 | part bottomwear, hang_from hip, root_rig_px 0.231, tip_rig_px 3.813, ratio 16.512, world_root_rig_px 2.742, world_tip_rig_px 4.239, world_ratio 1.546; ratio_floor 1.4725
|
|
3335
3336
|
TEXTURE_STRETCH: PASS — meshes 9; triangles 1816; frames 41; severity 1.259; severity_ceiling 1.926544
|
|
3336
3337
|
ANIMATIONS_DO_NOT_FOLD: PASS — 8 animation(s) read by rigc render --folds at 12 fps, with its 29 physics constraint(s) on and off (idle, tilt, look_l, look_r, smile, startled, wave, hop): 0 folding mesh(es), 0 not declared in may_fold (<= 0)
|
|
3337
3338
|
CLOTH_SETTLES: CLEAR — 12 physics chain(s) read by their tip on the idle as rendered (41 frame(s) at 12 fps over 3.333 s, physics on): 0 of them settle before the loop's length — 0 warning(s), never a bar
|
|
3338
3339
|
check: PASS; 11 of 11 bar(s) measured, 0 skipped; 0 warning(s) (CLOTH_SETTLES, not a bar)
|
|
3339
3340
|
```
|
|
3340
3341
|
|
|
3342
|
+
The `TIP_OVER_ROOT` line above was read again for issue #987, from this build at rig-parts `c17292c` with #987's change and rigc 3.2.1; the other lines are as first printed. Its world figures for the sleeves (`3.626`, `2.905`) differ from the first print's (`3.399`, `2.491`); `bottomwear`'s are the same.
|
|
3343
|
+
|
|
3341
3344
|
## 4. The command order
|
|
3342
3345
|
|
|
3343
3346
|
Two steps are external: the See-through runs (by any route; the optional `comfy seethrough` adapter is only a client for a ComfyUI box). Everything else is this tool.
|
|
@@ -3988,6 +3991,7 @@ and a usage error with `--head-box`, `--cloth` or `--from-config`.
|
|
|
3988
3991
|
| `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*) |
|
|
3989
3992
|
| `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 |
|
|
3990
3993
|
| `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. After the part lines, `provenance:` per part and `provenance total:` report the value partition, followed by `ownership:` per cut for structural moves, independent of RGBA equality; alternate sets get their own `set "<set>"`-prefixed lines ([the two axes](#source-before-scissors)). The author-made/cut/limb/fringe lines described below precede 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 the `ASSEMBLE_PINHOLES: PASS — 0 pinholes …` or `ASSEMBLE_PINHOLES: SKIP — not measured, not a pass: …` line ([Pinholes at rest](#pinholes-at-rest-issue-944)), 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 informational `drift: "<part>" source re-take changed N own-layer px` lines under soft front cover, naming `assemble.keep_under_occluders` and its kept count when enabled ([source ladder](#source-before-scissors)), 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) |
|
|
3994
|
+
| `assemble` (the ground) | a `WARN ASSEMBLE_GROUND_PLAIN` line | no such line: the ground is plain and its border ring one colour (the five public examples read 0.0006 or less), or it is declared `busy` | `N % of the border ring is not one colour` — more than 2 % of the ring (`SILHOUETTE_BORDER` px wide) differs from its median by more than `SILHOUETTE_LIMIT`, under a ground absent or declared `plain`, under project: visible, where paintingEdges reads the ground. Two causes: the ground is not plain (declare `assemble.ground: busy`), or the figure crosses the border (leave the ground as it is). A warning, never a refusal; the ground stays as declared |
|
|
3991
3995
|
| `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 |
|
|
3992
3996
|
| `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 |
|
|
3993
3997
|
| `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` |
|
|
@@ -4220,6 +4224,25 @@ and 13,872 to 1,110 (`wind`), and the holes from 1,020 to 62, 453 to 40, 259 to
|
|
|
4220
4224
|
291 to 34 and 1,118 to 62; `check` passes the same bars as before on all five. A
|
|
4221
4225
|
painting patch is read placed on the rig canvas, and is never written. `core` is unchanged.
|
|
4222
4226
|
|
|
4227
|
+
**A busy ground (`assemble.ground`, issue #996).** The step above reads the painting
|
|
4228
|
+
as a figure on a ground of one colour: a painted pixel no part covers is the figure's
|
|
4229
|
+
when it differs from the border ring's colour, and the colour behind a part's edge is
|
|
4230
|
+
one colour across the window. On an opaque ground of many colours — a wall, a banner —
|
|
4231
|
+
both readings take the ground: the ground beside a thin part (a strand of hair, a
|
|
4232
|
+
finger) becomes the part's, opaque, in the ground's own colour. A busy, opaque ground
|
|
4233
|
+
needs `assemble.ground: busy`; without it, visible takes backdrop pixels. Under `busy`
|
|
4234
|
+
a part's alpha rises only where See-through's own layer reaches (its alpha above 0
|
|
4235
|
+
before the step), a painted pixel no part covers is a gap only where two parts' layers
|
|
4236
|
+
meet, and a pixel on the ground's side of the edge is read only where it is a mix of
|
|
4237
|
+
the two colours (within 20 of the line between them). Measured on a busy ground planted
|
|
4238
|
+
under two public examples (`fixtures/busy_ground.ts`): part pixels outside the `core`
|
|
4239
|
+
build's footprint went from 8,065 to 0 (`wind`) and 6,119 to 0 (`demo`); 50 and 31
|
|
4240
|
+
pixels inside See-through's soft rim still show the ground's colour where `core` showed
|
|
4241
|
+
none. The price of the bound is the painted outline beyond See-through's reach, which a
|
|
4242
|
+
plain ground keeps, so `busy` is a declaration, not a default: on the five public
|
|
4243
|
+
examples it would raise the recomposite's error pixels. The painting cannot tell the two
|
|
4244
|
+
grounds apart near the figure, so the ground is the author's to declare.
|
|
4245
|
+
|
|
4223
4246
|
**`rig --idle-keys ctl|direct`** (and `build --idle-keys`, which forwards the
|
|
4224
4247
|
value to its rig stage; without the flag, or with `ctl`, a build writes and prints
|
|
4225
4248
|
what it did before `build` took the flag). rig-c's `A15_IDLE_NO_MESH_BONE_KEYS`
|
|
@@ -4777,6 +4800,7 @@ row in this page's `rule` and `line` tables; the selftest holds that (`TY16`).
|
|
|
4777
4800
|
| <a id="LAYERS_EXPORT_DIR_EMPTY"></a>`LAYERS_EXPORT_DIR_EMPTY` | `layers --export <dir>` was given a file, or a directory that is not empty (issue #359): a file left there could be read in place of the one written | a new directory, or empty it |
|
|
4778
4801
|
| <a id="LAYERS_EXPORT_ROUND_TRIP"></a>`LAYERS_EXPORT_ROUND_TRIP` | the export, read back, differs from the input's reading (the detail names the first difference) — an exporter bug, not an input problem | report it with the input named |
|
|
4779
4802
|
| <a id="ASSEMBLE_SOURCE_PRESENT"></a><a id="ASSEMBLE_SOURCE_DECODES"></a>`ASSEMBLE_SOURCE_PRESENT`, `ASSEMBLE_SOURCE_DECODES` | the painting is missing or not a PNG | `--source` |
|
|
4803
|
+
| <a id="ASSEMBLE_GROUND_PLAIN"></a>`ASSEMBLE_GROUND_PLAIN` | WARN only, never a refusal (issue #996): under project: visible, where paintingEdges reads the ground, with `assemble.ground` absent or `plain`, more than 2 % of the painting's border ring is not one colour (the ring is `SILHOUETTE_BORDER` px wide; a pixel counts when it differs from the ring's median by more than `SILHOUETTE_LIMIT`). Two causes: the ground is not plain, or the figure crosses the border | if it is the ground, declare `assemble.ground: busy`; if the figure crosses the border, leave the ground as it is (the WARN never changes output) |
|
|
4780
4804
|
| <a id="SHEET_SOURCE_PRESENT"></a><a id="SHEET_PNG_PRESENT"></a>`SHEET_SOURCE_PRESENT`, `SHEET_PNG_PRESENT` | `sheet`'s painting, or a part PNG a `parts.json` lists, is missing | `--source`, `--layers` |
|
|
4781
4805
|
|
|
4782
4806
|
### Plausibility (`WARN` in `layers`, a note in `assemble --propose-plan`)
|
|
@@ -5344,7 +5368,7 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
5344
5368
|
| <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) |
|
|
5345
5369
|
| <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 |
|
|
5346
5370
|
| <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) |
|
|
5347
|
-
| <a id="CHECK_TIP_OVER_ROOT"></a>`CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's art in the lower half of its box travels less than 1.4725 times as far as in the upper half, measured from the idle's posed geometry (rig px quoted) | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them), or its mode: on `scarf`'s short skirt the proposal's lattice read 1.207 and the same part in `contour` mode 1.738 with the same segments and idle (issue #118) — both carried the `chest` segment #262 removed, after which the lattice reads 3.487. When the detail says `the root travels with the breath (x px of y px): it is carried by \`<bone>\`` (issue #262) the chain is not the cause: a bone an idle `translate` track moves (the breath) moves the root more than the tip, and with that translate held still (the part posed again without it, in proportion to each vertex's weight on the bone and those under it) the ratio passes — the row adds `carried_by`, `breath_rig_px`, `root_without_breath_rig_px` and `tip_without_breath_rig_px`; take that bone's segment off the mesh, or bind the root to the bone the part hangs from. A part the scene file declares `rests` (issue #245) is held the other way, its tip half's travel at most its root half's (ratio ≤ 1); the detail then reads `the scene file's rests declares its tip at rest, so a ratio <= 1 is required`, and for it the fix is the chain's `amps` (shrinking toward the tip) or the mesh's tip weighted to a bone the idle does not swing |
|
|
5371
|
+
| <a id="CHECK_TIP_OVER_ROOT"></a>`CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's art in the lower half of its box travels less than 1.4725 times as far as in the upper half, measured from the idle's posed geometry against the bone the part hangs from (issue #987; rig px quoted, the world reading beside it) | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them), or its mode: on `scarf`'s short skirt the proposal's lattice read 1.207 and the same part in `contour` mode 1.738 with the same segments and idle (issue #118) — both carried the `chest` segment #262 removed, after which the lattice reads 3.487. When the detail says `the root travels with the breath (x px of y px): it is carried by \`<bone>\`` (issue #262) the chain is not the cause: a bone an idle `translate` track moves (the breath) moves the root more than the tip, and with that translate held still (the part posed again without it, in proportion to each vertex's weight on the bone and those under it) the ratio passes — the row adds `carried_by`, `breath_rig_px`, `root_without_breath_rig_px` and `tip_without_breath_rig_px`; take that bone's segment off the mesh, or bind the root to the bone the part hangs from, which the detail names. Since issue #987 only a breath keyed under that bone is read: one at or above it is in the frame the halves are read in and carries nothing. When the bone named is a link of the chain the part hangs on, its segment stays, and the detail says to key the translate on the bone the part hangs from or above it. `nothing in it moves against \`<bone>\`` is a part that rides the bone it hangs from whole — a rigid sleeve, whose world ratio is 1. Since issue #995 a lone swing counts as a link too — a rotate-keyed bone with no child bone, or whose only child is its control twin (a one-link chain's control) — so a skirt bound to the one bone that swings it is read against the bone above, where its swing shows. A rotate-keyed body segment other bones hang under (a pelvis carrying the legs) is not one: a part bound whole to it reads `nothing in it moves against` that bone and fails, its world ratio passing only through the lever arm. Hang swinging cloth on its own bone under the body segment; a fitted garment the rig holds rigid on purpose (shorts on the pelvis) is declared in the scene file's `not_swinging` (§7), and the line then reads it SKIP with the reason. A part the scene file declares `rests` (issue #245) is held the other way, its tip half's travel at most its root half's (ratio ≤ 1); the detail then reads `the scene file's rests declares its tip at rest, so a ratio <= 1 is required`, and for it the fix is the chain's `amps` (shrinking toward the tip) or the mesh's tip weighted to a bone the idle does not swing |
|
|
5348
5372
|
| <a id="CHECK_STILL_REGIONS_DARK"></a>`CHECK_STILL_REGIONS_DARK` | a slot of the face's still set (the `face` slots and every slot drawn over the face that the head bone alone carries) moves relative to the bone the face's slot rides, or a `footwear` slot moves on the screen, by more than the arithmetic's own rounding — measured from the idle's posed geometry (the slot, its largest displacement in rig px, the idle frame and the tolerance quoted) | on the face: a face mesh with a weight on a bone other than the head (`segments`), or one deformed by another bone; on the feet: a mesh weighted to a swinging bone, or a region on the wrong bone. A bang swinging over the face is not this line's failure: its depth is reported (`crossing`), with no bar |
|
|
5349
5373
|
| <a id="CHECK_TEXTURE_STRETCH"></a>`CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named, and — issue #314 — the bones the edge's two vertices hang on: each vertex's two largest weights from `rig.json`, `vertex <i> is weighted to "<bone>" <w>, …`, then `on the idle's chains, the chain from "<first link>" carries "<bone>", …` — each chain `CHAIN_LAG` reads named once, by its first link — with a bone on no chain said, or `none of these bones hangs on a chain of the idle`); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other. Two chains named, with opposed amplitudes: give both one sign, or drop one — `propose --from-config` measures the pair before a build (§3, *Two chains on one lattice*). An edge whose two ends disagree on whether a bone's small share survives the 0.03 cut (one vertex keeps the bone at a few hundredths, the next drops it — read the two vertices' weights the line quotes): declare `influences: {floor: "continuous"}` on that mesh first, `rule: "heat"` second; the weight `exponent` moves the figure without removing the cut (issue #115, `reference-demo` `hair_back` as contour at tolerance 1, rig-c 2.47.1: 2.535 as drafted; continuous floor 1.422, heat 1.816, both PASS; exponent 1.5 / 3 / 4 / 6 read 2.564 / 1.902 / 2.119 / 4.816, the one pass on the same cut at another edge) |
|
|
5350
5374
|
| <a id="CHECK_ANIMATIONS_DO_NOT_FOLD"></a>`CHECK_ANIMATIONS_DO_NOT_FOLD` | an animation, the idle included, turns a mesh triangle over against the setup pose on some frame, read by `rigc render --folds` over the build with its physics on — and with its physics constraints left out beside it when the rig declares any, so each row reports `physics_on` and `physics_off` side by side (issues #320, #333, #363); the object names the animation and the mesh, the detail quotes rigc's line — the frames, the worst frame, which triangles, their world box and setup-pose area — and when the fold appears only with physics on, names the physics constraint(s) on the mesh's bones and the worst frame. After rigc's line it names what the worst frame's folded triangles hang on (issue #567), in `TEXTURE_STRETCH`'s words: each vertex's dominant bone is its largest weight in `rig.json`, the triangles whose vertices share one set of dominant bones are one group (the set in `rig.json`'s bone order), and each group lists its first 6 triangles (`triangles 63, 78, 79 dominated by "knee_r", "knee_l"`, past 6 `… and N more`) and spells out its first triangle's vertices — the largest two weights of each, and the idle chains those bones hang on; 3 groups are written and the rest counted (`; … and N more group(s), M triangle(s)`). An attachment the skin does not hold, or a triangle its table does not reach, is said, not guessed. Or a `may_fold` entry of the `--requirements` file names a slot and animation that does not fold: an exception that excepts nothing | the physics constraint(s) the line names (lower `inertia`, raise `strength`, or lower `damping`), the clip's keys on the bones the line names (a bend past what the weights carry, two bones turned toward each other until the rows between them pass, a translate that carries one row of vertices past the next), or the mesh's `segments`; to look, `rigc inspect --candidate <out>/check/build --show art,folds,mesh --animation <name> --at <the worst frame's time> --zoom folds`. A fold the art means (a sleeve behind the body) is declared, `"may_fold": [{ "slot": …, "animation": …, "why": … }]` (§7, *Declared requirements*); a stale entry is taken out |
|
|
@@ -5755,7 +5779,7 @@ the two examples [observed]:
|
|
|
5755
5779
|
| <a id="BLINK_NO_HOLE"></a>`BLINK_NO_HOLE` | the setup pose with every `scaley` track on the eyewhite slots' bones held at its closed value, against the setup pose, at full size: pixels in the eyewhite box that show the page where the open eye had art | 0 px | 0 | 0 | no `eyewhite` part, or no `scaley` track on its bones goes below its first key |
|
|
5756
5780
|
| ″ (reported) | the same box: each closed-eye pixel's max-channel distance to the nearest colour the open eye's box holds — max, and pixels over 40 | none | 15; 0 | 11; 0 | as above |
|
|
5757
5781
|
| <a id="CHAIN_LAG"></a>`CHAIN_LAG` | `motion.json`'s rotate tracks read as sines (DFT of the keys: period, amplitude, phase) and arranged by the bone tree — a keyed bone's parent is its nearest keyed ancestor | every lag ≥ 0.001 cycle; amplitude non-decreasing down each unbranched chain — non-increasing down a chain that carries a part declared `rests` (issue #245) | lags 0.040 (neck to head) to 0.120; 12 chains | lags 0.040 to 0.100; 9 chains | no rotate track under another of the same period |
|
|
5758
|
-
| <a id="TIP_OVER_ROOT"></a>`TIP_OVER_ROOT` | each `handwear`/`bottomwear` part: how far the centroid of its art travels in the lower half of its box against the upper half — the art's AREA centroid inside each fixed half (cut at h/2), every art pixel (alpha above 8) carried through its attachment's posed triangles from the idle's `geometry.json` and clipped to the half exactly, rig px; `instrument: "geometry"` in the line (issue #118) | ratio ≥ 1.4725; a part declared `rests`, ratio ≤ 1 (issue #245) | `bottomwear` 3.222, `sleeves` 4.
|
|
5782
|
+
| <a id="TIP_OVER_ROOT"></a>`TIP_OVER_ROOT` | each `handwear`/`bottomwear` part: how far the centroid of its art travels in the lower half of its box against the upper half — the art's AREA centroid inside each fixed half (cut at h/2), every art pixel (alpha above 8) carried through its attachment's posed triangles from the idle's `geometry.json` and clipped to the half exactly, rig px; `instrument: "geometry"` in the line (issue #118). Since issue #987 every posed vertex is first carried into the setup place of the bone the part hangs from (`hang_from`, below), so the halves travel against it; the row keeps the world reading as `world_root_rig_px`, `world_tip_rig_px`, `world_ratio`, and the line says `instrument: "geometry, carrier frame"`. A half that travels 0 at the line's precision is 0 | ratio ≥ 1.4725; a part declared `rests`, ratio ≤ 1 (issue #245) | `bottomwear` 3.222, `sleeves` 9.891 against `chest` (world 4.762) | `bottomwear` 5.393, `sleeves` 27.482 against `chest` (world 11.830) | no such part |
|
|
5759
5783
|
| <a id="STILL_REGIONS_DARK"></a>`STILL_REGIONS_DARK` | from the idle's posed geometry (`geometry.json`), `instrument: "geometry"` in each half (issue #123). **Face:** the still set — the `face` slots, and every slot drawn after the first of them whose art overlaps a face part's at the setup pose, that the head bone alone carries (a region on it, or a mesh whose every weight is it) and that the idle does not key by slot (`deform`, `sequence`, a slot track: `driven_slots`). The head bone is the bone the `face` parts' slot rides in `rig.json`. Each still slot's art (alpha above 8), carried through its posed attachment and back through the head bone's setup-to-frame map on every idle frame, against its setup position: `max_rig_px` (and where, `at`, when it is a motion), `rms_rig_px`, per slot in `still_slots`. **Feet:** the `footwear` slots the same way in screen space — they ride the root, which the idle does not key, so the screen is their own frame. **Reported, no bar:** `crossing` — per slot drawn after the still set's first slot and not in it, how deep its art reaches into the region (`face` on top at the setup pose, clear of where `eyewhite`, `irides`, `eyelash`, `eyebrow` and `mouth` go in the head's frame; for the feet, `footwear` on top), `depth_rig_px`, and the most of it covered on one frame, `area_rig_px2` | each half's `max_rig_px` ≤ its `tolerance_rig_px`: nothing moves, up to the arithmetic's rounding, √2 × 2 × 35 × M² × ulp(C) (below) | face 4.202e-13 ≤ 2.286e-11; `hair_front` crossing 2.816 rig px deep, 89.395 rig px² (frame 47); feet 0 ≤ 2.814e-12 | face 4.418e-13 ≤ 2.286e-11 (`face`, `ear_l`, `ear_r`); `hair_front` 2.194 deep, 50.605 rig px² (frame 23); feet 0 ≤ 2.814e-12 | no `face` and no `footwear` part (one of the two absent leaves that half unmeasured; so do face parts on two slot bones, or idle frames with no `geometry.json`) |
|
|
5760
5784
|
| <a id="TEXTURE_STRETCH"></a>`TEXTURE_STRETCH` | every mesh triangle's three edges in every idle frame, read from the `geometry.json` the idle render writes (`rigc render --geometry`, rig-c 1.4.0 or later): skinned length over rest length, the rest being the setup pose's bones with no deform. The figure is the rig's worst max(ratio, 1/ratio); each mesh's largest and smallest ratio, with triangle, vertices, edge and frame, is the detail | max(ratio, 1/ratio) ≤ 1.926544 | 1.388: `hair_back` triangle 449, edge 42-43 at 0.720 in frame 26 (largest stretch 1.291, frame 2) | 1.227: `sleeves` triangle 169, edge 126-51 at 0.815 in frame 28 (largest stretch 1.196, frame 5) | the rig draws no mesh, or the render wrote no `geometry.json` |
|
|
5761
5785
|
| <a id="ANIMATIONS_DO_NOT_FOLD"></a>`ANIMATIONS_DO_NOT_FOLD` | every animation, the idle included, through `rigc render --folds` at 12 fps over the build with its physics stepping and, when the rig declares physics constraints, over the keyed build with them left out beside it (issue #363): the meshes with a triangle turned over against the setup pose on some frame, less those the requirements file's `may_fold` declares (below) | 0 undeclared folding meshes, and no `may_fold` entry that matches no fold | 0, over `idle` | 0, over `idle` and `hop`, 2 physics constraints on and off | never SKIP: written whenever `motion.json` holds an animation |
|
|
@@ -5795,6 +5819,57 @@ on the geometric figure (`scarf`'s lattice `bottomwear` 1.337, 1.316, 1.285, 1.2
|
|
|
5795
5819
|
1200, 2400, 4800, 8000), with one gap not explained: `reference-sample`'s `bottomwear` tip reads
|
|
5796
5820
|
2.342 rig px there against 2.596 from geometry, its root agreeing to 0.4 %.
|
|
5797
5821
|
|
|
5822
|
+
**`TIP_OVER_ROOT` reads the halves against the bone the part hangs from (issue #987).**
|
|
5823
|
+
Read in the world, a sleeve whose chain trails its body's sway scores the trail as the
|
|
5824
|
+
tip moving less: the body carries the root, the chain's lag carries the tip back, and the
|
|
5825
|
+
two cancel at the tip. A heavier physics sleeve read lower there, and a pendulum that
|
|
5826
|
+
turned about its middle read higher, so tuning toward the floor made the sleeve swing
|
|
5827
|
+
more or turn the wrong way. Each idle frame's posed vertices are now mapped by
|
|
5828
|
+
setup(B) · frame(B)⁻¹, B the bone the part hangs from: from the deepest common
|
|
5829
|
+
ancestor-or-self of the bones its attachments are bound to, walk up while the bone is a
|
|
5830
|
+
link — a bone of a chain `CHAIN_LAG` reads, or a bone a physics constraint drives — and
|
|
5831
|
+
B is the first that is not (the rig stage keys `<link>_ctl` and binds the mesh to its
|
|
5832
|
+
child the physics drives, so the walk passes both; no bone is read by its name). When
|
|
5833
|
+
that common bone is a link's control twin — at the link's origin, unturned, unscaled and
|
|
5834
|
+
keyed by no idle track — the walk starts from the link, so a keyed chain whose bound
|
|
5835
|
+
child no physics drives is lifted too; the twin step is taken only there, so the walk
|
|
5836
|
+
stops at `chest`, which is `chest_ctl`'s twin. The row names the bone as `hang_from`.
|
|
5837
|
+
The floor stays 1.4725, the bar set from the pixel instrument above, not chased. Every
|
|
5838
|
+
public example passes both
|
|
5839
|
+
readings; the bottomwear parts hang from `hip`, which no example keys, so they read the
|
|
5840
|
+
same:
|
|
5841
|
+
|
|
5842
|
+
| part | world (until #987) | against `hang_from` |
|
|
5843
|
+
| --- | --- | --- |
|
|
5844
|
+
| `demo` `handwear_l`, `handwear_r` | 4.761, 4.835 | 10.249, 11.911 (`chest`) |
|
|
5845
|
+
| `demo` `bottomwear` | 2.426 | 2.426 (`hip`) |
|
|
5846
|
+
| `reference-demo` `bottomwear`, `sleeves` | 3.222, 4.762 | 3.222 (`hip`), 9.891 (`chest`) |
|
|
5847
|
+
| `reference-sample` `bottomwear`, `sleeves` | 5.393, 11.830 | 5.393 (`hip`), 27.482 (`chest`) |
|
|
5848
|
+
| `scarf` `handwear_r`, `handwear_l` | 2.632, 2.460 | 9.235, 7.732 (`chest`) |
|
|
5849
|
+
| `scarf` `bottomwear` | 3.436 | 3.436 (`hip`) |
|
|
5850
|
+
| `wind` `bottomwear` | 1.816 | 1.816 (`hip`) |
|
|
5851
|
+
|
|
5852
|
+
The selftest plants the four cases the reading is for on one two-link sleeve under a
|
|
5853
|
+
body the idle carries 4 rig px: ridden rigid it reads 0 / 0 against the bone it rides
|
|
5854
|
+
and fails (world 1); turned about its middle it reads about 1 and fails (world above
|
|
5855
|
+
the floor); trailing the carry, on keyed links with physics children or on a
|
|
5856
|
+
physics-only chain, it passes (world below 1). The skirt whose waistband rides `chest`
|
|
5857
|
+
(issue #262) hangs from a bone above `chest`, so the breath still carries its root and
|
|
5858
|
+
it still fails, naming `chest`.
|
|
5859
|
+
|
|
5860
|
+
A lone swing is a link too (issue #995): a rotate-keyed bone with no child bone, or whose
|
|
5861
|
+
only child is its control twin, is the swing itself — a skirt bound to the one bone that
|
|
5862
|
+
swings it from the top of its box, or a one-link chain's control — so the walk passes it
|
|
5863
|
+
and the part is read against the bone above (in the build fixtures that is `root`, which
|
|
5864
|
+
the idle does not move, so the carrier reading equals the world one: 2.336 for both). A
|
|
5865
|
+
rotate-keyed body segment other bones hang under is not one: a garment bound whole to a
|
|
5866
|
+
pelvis that carries the legs reads 0 / 0 against it and fails (world 3.501, through the
|
|
5867
|
+
lever arm alone). `TIP_OVER_ROOT` reads every `handwear` and `bottomwear` part, declared or
|
|
5868
|
+
not, so a fitted garment held rigid on purpose is declared `not_swinging` and reads SKIP
|
|
5869
|
+
with the reason; the leg-set build fixtures declare their shorts so. A track that keys a
|
|
5870
|
+
`motion.groups` group keys each member for the walk too, as `CHAIN_LAG` reads it, so a
|
|
5871
|
+
group-keyed link is not taken for its control's unkeyed twin.
|
|
5872
|
+
|
|
5798
5873
|
**`STILL_REGIONS_DARK` is measured from geometry too, and its scope changed (issue #123).**
|
|
5799
5874
|
|
|
5800
5875
|
**What it read until #123.** The face half compared two pixel figures on the idle's
|
package/docs/STABILITY.md
CHANGED
|
@@ -147,7 +147,7 @@ part name, `[i]` an entry of a list):
|
|
|
147
147
|
- **generation.control.skeleton**, the pose file it names when it is no built-in — required: `width`, `height`, `points`
|
|
148
148
|
- **seethrough** — required: `resolution`, `steps`, `seed`, `offload`; optional: `head_box`, `arm_split`
|
|
149
149
|
- **seethrough.arm_split** — required: `order`, and one of `x` or `line` (issue #439)
|
|
150
|
-
- **assemble** — required: `rig_scale`, `plan` (from plain `assemble` on); optional: `seam`, `project`, `extend_below_crop`, `patches`, `cuts`, `alternates`, `head_face_underfill`, `yield_refused`, `keep_under_occluders`, `extend_painting`, `take`, `editor_receipts`
|
|
150
|
+
- **assemble** — required: `rig_scale`, `plan` (from plain `assemble` on); optional: `seam`, `project`, `ground`, `extend_below_crop`, `patches`, `cuts`, `alternates`, `head_face_underfill`, `yield_refused`, `keep_under_occluders`, `extend_painting`, `take`, `editor_receipts`
|
|
151
151
|
- **assemble.alternates[i]** — required: `name`; optional: `source`, `full`, `head`, `parts`, `hides`, `head_box`, `cuts`, `skeleton`, `anchor`, `base_anchor`, `drawn`, `tips`, `front`, `layers`, `focus` (issues #454, #465, #543, #652, #664, #666, #719, #780). `focus` is an authored finite `[x, y]` in painting pixels, within its canvas (edges included), refused as CONFIG_SET_FOCUS before assemble output when invalid; only when present, `parts.json` records it with focus_source_size and `sets.json` emits it in setup-world coordinates through the actual rig resize and `src/coords.ts`. Absent focus emits no field or inferred value; changing/removing it requires reassembly
|
|
152
152
|
- **assemble.extend_below_crop[i]** — required: `part`, `run`, `tag`
|
|
153
153
|
- **assemble.extend_painting[i]** — required: `part`, `band`, `max_distance`; optional: `min_held_alpha`, `order` (issues #848, #854, #855; explicit final layer-derived part, half-open rig band and positive four-neighbour path bound; base painting only). The optional integer floor 1–255 admits exact original alpha-over coverage strictly below it; absent or 1 blocks every nonzero holder. Only selected target samples below the floor may take painting RGBA; target samples at/above the floor and every non-target holder remain exact. By default overlapping reachable claims refuse. With `order` on every entry (unique nonnegative safe integers), lower values claim first and their gains block later candidates and paths; array order never breaks ties or supplies missing precedence
|
|
@@ -232,6 +232,12 @@ an absent field preserves existing output bytes. The bytes `visible` writes
|
|
|
232
232
|
are not promised across versions (issue #953 resolves its edges from the
|
|
233
233
|
painting).
|
|
234
234
|
|
|
235
|
+
`assemble.ground` accepts `plain` or `busy` (issue #996). Invalid values refuse
|
|
236
|
+
as `CONFIG_FIELD_TYPE` at `config.assemble.ground`; an absent field and `plain`
|
|
237
|
+
preserve existing output bytes. It is read by `visible` only, whose bytes are
|
|
238
|
+
not promised. The `ASSEMBLE_GROUND_PLAIN` warning prints under `visible` only, is a
|
|
239
|
+
report line and never changes output.
|
|
240
|
+
|
|
235
241
|
`TY14` holds the set of field names in backticks above to the set of names the
|
|
236
242
|
loader's object checks spell (every `c.object(…)` in `src/config.ts`, with the lists
|
|
237
243
|
they name), in both directions, and refuses an object check whose key list it cannot
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.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,5 +1,5 @@
|
|
|
1
1
|
import { type SeamRule } from './seamrule.ts';
|
|
2
|
-
import { DEFAULT_PROJECT_RULE, type ProjectRule } from './projectrule.ts';
|
|
2
|
+
import { DEFAULT_GROUND, DEFAULT_PROJECT_RULE, type Ground, type ProjectRule } from './projectrule.ts';
|
|
3
3
|
import { paintingEdges, type PaintingEdgeSkips } from './paintingedges.ts';
|
|
4
4
|
import { edgeLength } from './length.ts';
|
|
5
5
|
import { materialSource, type MaterialSource, materialRows, polygonMaterialEdges, type MaterialRow, type MaterialEdge, type PlacedArt } from './cutmaterial.ts';
|
|
@@ -193,7 +193,7 @@ export const SILHOUETTE_LIMIT = 40;
|
|
|
193
193
|
|
|
194
194
|
export { DEFAULT_SEAM_RULE, SEAM_RULES, type SeamRule } from './seamrule.ts';
|
|
195
195
|
|
|
196
|
-
export { DEFAULT_PROJECT_RULE, PROJECT_RULES, type ProjectRule } from './projectrule.ts';
|
|
196
|
+
export { DEFAULT_GROUND, DEFAULT_PROJECT_RULE, GROUNDS, type Ground, PROJECT_RULES, type ProjectRule } from './projectrule.ts';
|
|
197
197
|
|
|
198
198
|
// ---------------------------------------------------------------------------
|
|
199
199
|
// inputs
|
|
@@ -331,6 +331,8 @@ export interface AssembleInput {
|
|
|
331
331
|
under?: ReadonlyArray<Raster | null>;
|
|
332
332
|
seamRule: SeamRule;
|
|
333
333
|
projectRule: ProjectRule;
|
|
334
|
+
/** `config.assemble.ground` (issue #996), read by `paintingEdges` under `visible` only; absent, `plain`. */
|
|
335
|
+
ground?: Ground;
|
|
334
336
|
/** `config.scene` (issue #242): the painting is a scene, so the recomposite is read inside the figure ({@link sceneFigure}). Absent or false: nothing changes. */
|
|
335
337
|
scene?: boolean;
|
|
336
338
|
/** `config.limbs` (issue #349): the parts cut at their joints ({@link LimbCount}). Absent or empty: nothing changes. */
|
|
@@ -2619,6 +2621,47 @@ export function paintedFigure(srcr: Raster): Uint8Array {
|
|
|
2619
2621
|
return painted;
|
|
2620
2622
|
}
|
|
2621
2623
|
|
|
2624
|
+
/**
|
|
2625
|
+
* Issue #996: the share of the border ring that `paintedFigure`'s own one-colour
|
|
2626
|
+
* test reads as figure — the ring's pixels (the outer `SILHOUETTE_BORDER` rig
|
|
2627
|
+
* pixels) whose `paintedFigure` value is 1, over the ring's pixel count. A plain
|
|
2628
|
+
* ground reads near 0; a ground of many colours, or a figure crossing the
|
|
2629
|
+
* border, reads more.
|
|
2630
|
+
*/
|
|
2631
|
+
export function groundRingShare(srcr: Raster): number {
|
|
2632
|
+
const { width: W, height: H } = srcr;
|
|
2633
|
+
const painted = paintedFigure(srcr);
|
|
2634
|
+
let ring = 0;
|
|
2635
|
+
let figure = 0;
|
|
2636
|
+
for (let y = 0; y < H; y++) {
|
|
2637
|
+
for (let x = 0; x < W; x++) {
|
|
2638
|
+
if (y >= SILHOUETTE_BORDER && y < H - SILHOUETTE_BORDER && x >= SILHOUETTE_BORDER && x < W - SILHOUETTE_BORDER) continue;
|
|
2639
|
+
ring++;
|
|
2640
|
+
figure += painted[y * W + x];
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return ring === 0 ? 0 : figure / ring;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/**
|
|
2647
|
+
* Share of the border ring that paintedFigure's own one-colour test reads as figure. Plain grounds measured at most 0.001 and the busy fixtures 0.464; 0.02 sits near the geometric middle of that gap, an order of magnitude from each side. A figure crossing the border counts too, so the WARN names both causes and leaves the ground as declared.
|
|
2648
|
+
*/
|
|
2649
|
+
export const GROUND_RING_SHARE_LIMIT = 0.02;
|
|
2650
|
+
|
|
2651
|
+
/**
|
|
2652
|
+
* Issue #996: the WARN for a ground declared or defaulted `plain` whose border ring
|
|
2653
|
+
* is not one colour (more than {@link GROUND_RING_SHARE_LIMIT} of it reads as
|
|
2654
|
+
* figure). Report-level only: it never changes the ground and never refuses. Null
|
|
2655
|
+
* when the ground is declared `busy` or the share is at or under the limit, and under
|
|
2656
|
+
* `project: core`: only the visible rule's paintingEdges reads the ground, so under
|
|
2657
|
+
* core the declaration changes nothing and there is nothing to warn about.
|
|
2658
|
+
*/
|
|
2659
|
+
export function groundWarning(share: number, ground: Ground | undefined, rule: ProjectRule): Problem | null {
|
|
2660
|
+
if (rule !== 'visible' || (ground ?? DEFAULT_GROUND) !== 'plain' || share <= GROUND_RING_SHARE_LIMIT) return null;
|
|
2661
|
+
const pct = (100 * share).toFixed(1);
|
|
2662
|
+
return { code: 'ASSEMBLE_GROUND_PLAIN', object: 'config.assemble.ground', detail: `${pct} % of the border ring is not one colour: either the ground is not plain or the figure crosses the border; if it is the ground, declare assemble.ground: busy` };
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2622
2665
|
export interface PlacedPart {
|
|
2623
2666
|
depthImage?: Raster;
|
|
2624
2667
|
record: PartRecord;
|
|
@@ -4586,6 +4629,8 @@ export interface AssembleResult {
|
|
|
4586
4629
|
projectRule: ProjectRule;
|
|
4587
4630
|
/** Issue #953: what `paintingEdges` resolved, under `projectRule: 'visible'` only. */
|
|
4588
4631
|
paintingEdges?: PaintingEdgeReading;
|
|
4632
|
+
/** Issue #996: {@link groundRingShare} of the painting in the rig, read by the ground WARN. */
|
|
4633
|
+
groundShare: number;
|
|
4589
4634
|
depthReadings?: Array<{ from: string; reading: import('./depthsheets.ts').DepthReading }>;
|
|
4590
4635
|
/** Visible registered samples clamped within <1 source pixel of a crop. */
|
|
4591
4636
|
depthClamps?: Array<{ part: string; samples: number }>;
|
|
@@ -4801,6 +4846,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
4801
4846
|
checkPlanAgainstRuns(sourcePlan, extend, full, head);
|
|
4802
4847
|
const { W, H } = frame;
|
|
4803
4848
|
const srcr = sourceInRig(source, W, H);
|
|
4849
|
+
const groundShare = groundRingShare(srcr);
|
|
4804
4850
|
// Issue #359: a part's hidden-art layer is the under of a cut that names none — and, with one named, a refusal, since
|
|
4805
4851
|
// reading either would be a guess. Its uses are counted for `hiddenLines`.
|
|
4806
4852
|
const hiddenArt = hiddenArtByPart(plan, input.cuts, { full, head }, frame, input.resolution);
|
|
@@ -5608,6 +5654,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5608
5654
|
};
|
|
5609
5655
|
const e = paintingEdges(stack.map(onRig), srcr, {
|
|
5610
5656
|
painted: paintedFigure(srcr),
|
|
5657
|
+
...(input.ground === undefined ? {} : { ground: input.ground }),
|
|
5611
5658
|
editable: order.map((d) => !('patch' in d)),
|
|
5612
5659
|
keep: keepInOrder,
|
|
5613
5660
|
feathered: featheredInOrder,
|
|
@@ -5821,6 +5868,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5821
5868
|
hidden: [...hiddenUses].map(([layer, readBy]) => ({ layer, readBy })),
|
|
5822
5869
|
seamRule,
|
|
5823
5870
|
projectRule,
|
|
5871
|
+
groundShare,
|
|
5824
5872
|
...(edgeReading === null ? {} : { paintingEdges: edgeReading }),
|
|
5825
5873
|
...(headFacePrep.underfillPx > 0 ? { headFaceUnderfillPx: headFacePrep.underfillPx } : {}),
|
|
5826
5874
|
excluded,
|
|
@@ -6705,6 +6753,8 @@ export function stageFields(
|
|
|
6705
6753
|
regions?: Record<string, RegionSpec>;
|
|
6706
6754
|
meshes?: Record<string, MeshSpec>;
|
|
6707
6755
|
motion?: CharacterConfig['motion'];
|
|
6756
|
+
/** `assemble.ground` (issue #996); absent, `plain`. */
|
|
6757
|
+
ground?: Ground;
|
|
6708
6758
|
} {
|
|
6709
6759
|
const problems: Problem[] = [];
|
|
6710
6760
|
if (cfg.seethrough === undefined) {
|
|
@@ -6726,6 +6776,7 @@ export function stageFields(
|
|
|
6726
6776
|
...(cfg.assemble.head_face_underfill === undefined ? {} : { headFaceUnderfill: cfg.assemble.head_face_underfill }),
|
|
6727
6777
|
...(cfg.assemble.yield_refused === undefined ? {} : { yieldRefused: cfg.assemble.yield_refused }),
|
|
6728
6778
|
...(cfg.assemble.keep_under_occluders === undefined ? {} : { keepUnderOccluders: cfg.assemble.keep_under_occluders }),
|
|
6779
|
+
...(cfg.assemble.ground === undefined ? {} : { ground: cfg.assemble.ground }),
|
|
6729
6780
|
...(cfg.assemble.extend_painting === undefined ? {} : { paintingExtension: cfg.assemble.extend_painting }),
|
|
6730
6781
|
...(turnMovingPair === null ? {} : { turnMovingPair }),
|
|
6731
6782
|
...(cfg.turn === undefined ? {} : { turn: cfg.turn, regions: cfg.regions, meshes: cfg.meshes, motion: cfg.motion }),
|
package/src/build.ts
CHANGED
|
@@ -58,7 +58,7 @@ import { createHash } from 'node:crypto';
|
|
|
58
58
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
59
59
|
import { dirname, join, resolve } from 'node:path';
|
|
60
60
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
61
|
-
import { assemble, type AssembleResult, cutLines, type ExcludedCount, excludedLines, setExclusions, islandLines, setIslandSources, type IslandCount, hiddenLines, SINGLE_RUN_FACE_TAGS, singleRunHeadBox, softOcclusionLine, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, measurePinholes, measureRecomposite, pinholeProblems, pinholesLine, type ProjectRule, recomposite, recompositeRecord, scenePhrase, type SeamRule, sourceInRig, stageFields, uncoveredLine } from './assemble.ts';
|
|
61
|
+
import { assemble, type AssembleResult, cutLines, type ExcludedCount, excludedLines, setExclusions, islandLines, setIslandSources, type IslandCount, hiddenLines, SINGLE_RUN_FACE_TAGS, singleRunHeadBox, softOcclusionLine, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, groundWarning, holeKindsLine, holeLines, measurePinholes, measureRecomposite, pinholeProblems, pinholesLine, type ProjectRule, recomposite, recompositeRecord, scenePhrase, type SeamRule, sourceInRig, stageFields, uncoveredLine } from './assemble.ts';
|
|
62
62
|
import { allocationClause, type Reducer, stageBClause, unboundedClause } from './automesh.ts';
|
|
63
63
|
import { residualClause } from './autoenvelope.ts';
|
|
64
64
|
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, prepareComparison, type ReplayCandidate, runComparison, runPreparedComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
@@ -689,6 +689,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
689
689
|
...(fields.armSplit !== undefined && altPlan.some((e) => e[1] === 'full' && (e[2] === 'handwear-r' || e[2] === 'handwear-l')) ? { armSplit: fields.armSplit } : {}),
|
|
690
690
|
...(fields.headFaceUnderfill === undefined ? {} : { headFaceUnderfill: fields.headFaceUnderfill }),
|
|
691
691
|
...(fields.yieldRefused === undefined ? {} : { yieldRefused: fields.yieldRefused }),
|
|
692
|
+
...(fields.ground === undefined ? {} : { ground: fields.ground }),
|
|
692
693
|
...(fields.turnMovingPair === undefined ? {} : { turnMovingPair: fields.turnMovingPair }),
|
|
693
694
|
...(fields.turn === undefined ? {} : { turn: fields.turn, regions: fields.regions, meshes: fields.meshes, motion: fields.motion }),
|
|
694
695
|
seamRule: seam,
|
|
@@ -809,10 +810,15 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
809
810
|
if (figurePath !== null) writePng(figurePath, result.figure as Raster);
|
|
810
811
|
const [W, H] = result.parts.rig_size;
|
|
811
812
|
log(`rig-parts assemble: ${result.images.length} part(s) on a ${W}x${H} rig (${result.parts.scale_rig_per_source} rig px per source px), seam rule ${result.seamRule}, projection rule ${result.projectRule}`);
|
|
813
|
+
// Issue #996: a ground declared or defaulted plain whose border ring is not one colour, under project: visible only.
|
|
814
|
+
const groundWarn = groundWarning(result.groundShare, fields.ground, result.projectRule);
|
|
815
|
+
if (groundWarn !== null) log(` WARN ${groundWarn.code}: ${groundWarn.object} — ${groundWarn.detail}`);
|
|
812
816
|
// Issue #953: what the painting's edges resolved, under the visible rule only.
|
|
813
817
|
if (result.paintingEdges !== undefined) {
|
|
814
818
|
const e = result.paintingEdges;
|
|
815
819
|
log(`painting edges: ${e.resolved} edge px resolved from the painting (${e.unseparated} against the two reference colours where the window's far sides overlap), ${e.changed} part px changed; left as they were: ${e.skipped.cover} under a part the two-layer model does not carry, ${e.skipped.contrast} where the painting draws no edge (contrast below ${EDGE_CONTRAST}), ${e.skipped.locked} kept, feathered, a patch or a turn's moving pair, ${e.skipped.noBehind} with no behind pixel in the window; ${e.skipped.pieces} part px not written so a part's art keeps its pieces`);
|
|
820
|
+
// Issue #996: under a declared busy ground, what the two ground rules left.
|
|
821
|
+
if (fields.ground === 'busy') log(` busy ground (assemble.ground): ${e.skipped.mix} behind-side px left that are not a mix of the two reference colours, ${e.skipped.support} part px not raised outside the part's own support`);
|
|
816
822
|
}
|
|
817
823
|
// Issue #359: one run, said, so a reader of the lines knows no head run was placed.
|
|
818
824
|
if (runs.single) log(` one run: ${input.full} holds the face (${SINGLE_RUN_FACE_TAGS.join(', ')}), so no head run is read; every part comes from the full run`);
|
package/src/check.ts
CHANGED
|
@@ -7889,9 +7889,117 @@ const NO_TIP_EXCEPTIONS: TipExceptions = { path: '', not_swinging: [], rises: []
|
|
|
7889
7889
|
* the idle's posed geometry, not a render of the part. Until #118 the line
|
|
7890
7890
|
* read rendered pixels — figures in frame px under `root_px`/`tip_px` — so an
|
|
7891
7891
|
* old and a new check.json are told apart by this key and by the units in
|
|
7892
|
-
* the field names.
|
|
7892
|
+
* the field names. Issue #987: the halves are read in the frame of the bone
|
|
7893
|
+
* the part hangs from ({@link hangBone}), the world reading beside them
|
|
7894
|
+
* (`world_*`), so the key names the frame too.
|
|
7893
7895
|
*/
|
|
7894
|
-
export const TIP_INSTRUMENT = 'geometry';
|
|
7896
|
+
export const TIP_INSTRUMENT = 'geometry, carrier frame';
|
|
7897
|
+
|
|
7898
|
+
/**
|
|
7899
|
+
* The bone a part hangs from (issue #987): the deepest common ancestor-or-self of the bones its attachments are
|
|
7900
|
+
* bound to ({@link boundBones}), walked up while it is a link — a bone of a chain `CHAIN_LAG` reads, a bone a
|
|
7901
|
+
* physics constraint drives, or a lone swing (a rotate-keyed bone with no child bone but its control twin) — to the first bone that is not, so a chain-hung part is read against the body that
|
|
7902
|
+
* carries the chain and not against its own first link (no bone is read by its name). When the common bone is a
|
|
7903
|
+
* link's control twin (at its origin, unturned, unscaled, keyed by no idle track), the walk starts from the link; only
|
|
7904
|
+
* there, so a body bone that is its own `_ctl`'s twin (`chest`) is where the walk stops. Null when the part is bound to
|
|
7905
|
+
* nothing or the walk leaves the tree, and the part is then read in the world.
|
|
7906
|
+
*/
|
|
7907
|
+
export function hangBone(rig: Record<string, unknown>, motion: Record<string, unknown>, part: string): string | null {
|
|
7908
|
+
const bones = (Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord).filter((b) => typeof b.name === 'string');
|
|
7909
|
+
const parent = new Map(bones.map((b) => [String(b.name), typeof b.parent === 'string' ? b.parent : null]));
|
|
7910
|
+
const upFrom = (b: string): string[] => {
|
|
7911
|
+
const path: string[] = [];
|
|
7912
|
+
for (let x: string | null = b; x !== null && !path.includes(x); x = parent.get(x) ?? null) path.push(x);
|
|
7913
|
+
return path;
|
|
7914
|
+
};
|
|
7915
|
+
const bound = [...boundBones(rig, part)];
|
|
7916
|
+
if (bound.length === 0) return null;
|
|
7917
|
+
const paths = bound.map(upFrom);
|
|
7918
|
+
const common = paths[0].find((b) => paths.every((p) => p.includes(b)));
|
|
7919
|
+
if (common === undefined) return null;
|
|
7920
|
+
// A link: a bone of a chain CHAIN_LAG reads, or a bone a physics constraint drives. The rig stage keys a chain's
|
|
7921
|
+
// `<link>_ctl` and binds the mesh to its child the physics drives, and a physics-only chain has no keys at all, so
|
|
7922
|
+
// both are walked up through until the body bone that carries them.
|
|
7923
|
+
const links = new Set(chainFigures(rig, motion).chains.filter((c) => c.length >= 2).flatMap((c) => c.map((l) => l.bone)));
|
|
7924
|
+
for (const c of Array.isArray(rig.constraints) ? rig.constraints : []) if (isRecord(c) && c.type === 'physics' && typeof c.bone === 'string') links.add(c.bone);
|
|
7925
|
+
// Every bone an idle track keys, a group track's members included (as chainFigures reads them); `rotated` keeps
|
|
7926
|
+
// the ones a rotate track keys.
|
|
7927
|
+
const idle = isRecord(motion.animations) && isRecord(motion.animations.idle) ? motion.animations.idle : {};
|
|
7928
|
+
const groups = isRecord(motion.groups) ? motion.groups : {};
|
|
7929
|
+
const keyedBones = new Set<string>();
|
|
7930
|
+
const rotated = new Set<string>();
|
|
7931
|
+
for (const t of (Array.isArray(idle.tracks) ? idle.tracks : []).filter(isRecord)) {
|
|
7932
|
+
const targets = typeof t.bone === 'string' ? [t.bone] : typeof t.group === 'string' && Array.isArray(groups[t.group]) ? (groups[t.group] as unknown[]).filter((m): m is string => typeof m === 'string') : [];
|
|
7933
|
+
for (const b of targets) {
|
|
7934
|
+
keyedBones.add(b);
|
|
7935
|
+
if (t.property === 'rotate') rotated.add(b);
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7938
|
+
const setup = new Map(bones.map((b) => [String(b.name), b]));
|
|
7939
|
+
const children = new Map<string, string[]>();
|
|
7940
|
+
for (const [b, pa] of parent) if (pa !== null) children.set(pa, [...(children.get(pa) ?? []), b]);
|
|
7941
|
+
const kids = (b: string): string[] => children.get(b) ?? [];
|
|
7942
|
+
// At its parent's origin, unturned and unscaled, keyed by no idle track: the parent carries it whole.
|
|
7943
|
+
const atOrigin = (b: string): boolean => {
|
|
7944
|
+
const r = setup.get(b);
|
|
7945
|
+
if (r === undefined || keyedBones.has(b)) return false;
|
|
7946
|
+
const zero = (k: string, at: number): boolean => r[k] === undefined || r[k] === at;
|
|
7947
|
+
return zero('x', 0) && zero('y', 0) && zero('rotation', 0) && zero('scaleX', 1) && zero('scaleY', 1) && zero('shearX', 0) && zero('shearY', 0);
|
|
7948
|
+
};
|
|
7949
|
+
// A lone swing (#995): a rotate-keyed bone with no child bones, or whose only child is its control twin with none.
|
|
7950
|
+
// That is a bone that is the swing itself, and a one-link chain's control; a rotate-keyed body segment other bones
|
|
7951
|
+
// hang under (a pelvis carrying legs) is not one, so a part bound whole to it stays read against it.
|
|
7952
|
+
for (const b of rotated) {
|
|
7953
|
+
const k = kids(b);
|
|
7954
|
+
if (k.length === 0 || (k.length === 1 && atOrigin(k[0]) && kids(k[0]).length === 0)) links.add(b);
|
|
7955
|
+
}
|
|
7956
|
+
// The rig stage's control twin: a bone at its link's origin, unturned and unscaled, that nothing keys — the link carries it whole.
|
|
7957
|
+
const twinOfLink = (b: string): boolean => {
|
|
7958
|
+
const pa = parent.get(b) ?? null;
|
|
7959
|
+
return pa !== null && links.has(pa) && atOrigin(b);
|
|
7960
|
+
};
|
|
7961
|
+
const seen = new Set<string>();
|
|
7962
|
+
let at: string | null = twinOfLink(common) ? (parent.get(common) ?? null) : common;
|
|
7963
|
+
while (at !== null && links.has(at) && !seen.has(at)) {
|
|
7964
|
+
seen.add(at);
|
|
7965
|
+
at = parent.get(at) ?? null;
|
|
7966
|
+
}
|
|
7967
|
+
return at;
|
|
7968
|
+
}
|
|
7969
|
+
|
|
7970
|
+
/**
|
|
7971
|
+
* Each idle frame's posed vertices mapped into the setup place of `bone` (issue #987): world point p on frame i
|
|
7972
|
+
* goes to setup(bone) · frame_i(bone)⁻¹ · p, so what the bone's own motion carries is taken out and what moves
|
|
7973
|
+
* against it is left. Identity when `bone` is null; null when the geometry does not record the bone on every frame.
|
|
7974
|
+
*/
|
|
7975
|
+
export function carrierFrame(geo: IdleGeometry | null, bone: string | null): ((i: number, posed: readonly number[]) => number[]) | null {
|
|
7976
|
+
if (bone === null) return (_i, posed) => posed.slice();
|
|
7977
|
+
if (geo === null) return null;
|
|
7978
|
+
const world = (bs: unknown): BoneWorld | null => {
|
|
7979
|
+
const b = Array.isArray(bs) ? bs.filter(isRecord).find((x) => x.name === bone) : undefined;
|
|
7980
|
+
if (b === undefined) return null;
|
|
7981
|
+
const f = [b.a, b.b, b.c, b.d, b.worldX, b.worldY];
|
|
7982
|
+
if (!f.every((v) => typeof v === 'number' && Number.isFinite(v))) return null;
|
|
7983
|
+
return { a: b.a as number, b: b.b as number, c: b.c as number, d: b.d as number, worldX: b.worldX as number, worldY: b.worldY as number };
|
|
7984
|
+
};
|
|
7985
|
+
const s = world(geo.setupBones);
|
|
7986
|
+
const fs = geo.frameBones.map(world);
|
|
7987
|
+
if (s === null || fs.some((f) => f === null || f.a * f.d - f.b * f.c === 0)) return null;
|
|
7988
|
+
return (i, posed) => {
|
|
7989
|
+
const m = fs[i] as BoneWorld;
|
|
7990
|
+
const det = m.a * m.d - m.b * m.c;
|
|
7991
|
+
const out = posed.slice();
|
|
7992
|
+
for (let j = 0; j + 1 < out.length; j += 2) {
|
|
7993
|
+
const dx = posed[j] - m.worldX;
|
|
7994
|
+
const dy = posed[j + 1] - m.worldY;
|
|
7995
|
+
const lx = (m.d * dx - m.b * dy) / det;
|
|
7996
|
+
const ly = (-m.c * dx + m.a * dy) / det;
|
|
7997
|
+
out[j] = s.a * lx + s.b * ly + s.worldX;
|
|
7998
|
+
out[j + 1] = s.c * lx + s.d * ly + s.worldY;
|
|
7999
|
+
}
|
|
8000
|
+
return out;
|
|
8001
|
+
};
|
|
8002
|
+
}
|
|
7895
8003
|
|
|
7896
8004
|
/**
|
|
7897
8005
|
* (4) Each swinging part's tip travels further than its root: how far the art
|
|
@@ -7915,11 +8023,19 @@ export const TIP_INSTRUMENT = 'geometry';
|
|
|
7915
8023
|
* relation: tip / root at most {@link TIP_REST_RATIO_CEILING}. Its row says
|
|
7916
8024
|
* so first (`declared: "rests"`), the list is the line's first figure after
|
|
7917
8025
|
* the other two, and the ceiling follows the floor.
|
|
8026
|
+
*
|
|
8027
|
+
* Issue #987: the halves are read against the bone the part hangs from
|
|
8028
|
+
* ({@link hangBone}, through {@link carrierFrame}), so a chain that trails its
|
|
8029
|
+
* body's carry is not scored as a tip that moves less; the row names that bone
|
|
8030
|
+
* (`hang_from`) and keeps the world reading (`world_*`). A half that travels 0
|
|
8031
|
+
* at the line's precision is 0, so a part that rides its carrier whole reads
|
|
8032
|
+
* 0 / 0 and fails, not a ratio of round-off.
|
|
7918
8033
|
*/
|
|
7919
8034
|
function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems: Problem[], excepted: TipExceptions = NO_TIP_EXCEPTIONS): JudgementLine {
|
|
7920
8035
|
const swing = partsTagged(inp.parts, SWING_TAGS);
|
|
7921
8036
|
if (swing.length === 0) return skip(`no part comes from a See-through ${tagWords(SWING_TAGS)} layer, so there is no hem or sleeve to swing`);
|
|
7922
8037
|
const poses = attachmentPoses(geometryPath, frames);
|
|
8038
|
+
const idleGeo = readGeometry(geometryPath, frames);
|
|
7923
8039
|
// Issue #231: a part declared `rises` is read up only when the chain carrying it does rise, measured at the setup pose.
|
|
7924
8040
|
// Issue #322: at its last link, the end the tip half is the art of; the row says what was read.
|
|
7925
8041
|
const risesRead = new Map<string, string>();
|
|
@@ -7964,44 +8080,65 @@ function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems
|
|
|
7964
8080
|
const [rootHalf, tipHalf] = rises ? ['lower', 'upper'] : ['upper', 'lower'];
|
|
7965
8081
|
const art = placedArt(p.x, p.y, readPng(join(inp.partsDir, `${p.name}.png`)));
|
|
7966
8082
|
const shown = poses.map((f) => f.get(p.name) ?? null);
|
|
7967
|
-
const
|
|
8083
|
+
const w = halfTravels(art, p, inp.stage, shown, rises ? 'up' : 'down');
|
|
8084
|
+
// Issue #987: the halves are read in the frame of the bone the part hangs from; the world reading rides beside them.
|
|
8085
|
+
const hangFound = hangBone(inp.rig, inp.motion, p.name);
|
|
8086
|
+
const frameOf = carrierFrame(idleGeo, hangFound);
|
|
8087
|
+
const hang = frameOf === null ? null : hangFound;
|
|
8088
|
+
const toCarrier = frameOf ?? ((_i: number, posed: readonly number[]) => posed.slice());
|
|
8089
|
+
const held = shown.map((f, i) => (f === null ? null : { rest: f.rest, triangles: f.triangles, posed: toCarrier(i, f.posed) }));
|
|
8090
|
+
const c = hang === null ? w : halfTravels(art, p, inp.stage, held, rises ? 'up' : 'down');
|
|
7968
8091
|
// A part not declared writes the row it wrote before issue #231; a declared one says which half is its tip's.
|
|
7969
8092
|
const said = { ...(rests ? { declared: 'rests' } : {}), ...(rises ? { tip_half: tipHalf, tip_read: risesRead.get(p.name) as string } : {}) };
|
|
7970
|
-
if (
|
|
7971
|
-
|
|
8093
|
+
if (w.root === null || w.tip === null || c.root === null || c.tip === null) {
|
|
8094
|
+
const half = w.root === null || c.root === null ? rootHalf : tipHalf;
|
|
8095
|
+
rows.push({ part: p.name, ...said, unmeasured: shown[0] === null ? 'its slot shows no attachment on idle frame 0' : `on idle frame 0 the ${half} half of its box holds no art its attachment draws` });
|
|
7972
8096
|
continue;
|
|
7973
8097
|
}
|
|
7974
8098
|
measured++;
|
|
8099
|
+
// Issue #987: a half that does not move against its carrier, read at the line's precision, is 0 and not the
|
|
8100
|
+
// arithmetic's residue — a rigid part reads 0 / 0, not a ratio of two round-off figures.
|
|
8101
|
+
const t = { root: round3(c.root) === 0 ? 0 : c.root, tip: round3(c.tip) === 0 ? 0 : c.tip };
|
|
7975
8102
|
const ratio = t.root > 0 ? round3(t.tip / t.root) : null;
|
|
8103
|
+
const worldRatio = w.root > 0 ? round3(w.tip / w.root) : null;
|
|
7976
8104
|
// Issue #245: a resting tip is held to the opposite relation — no further than its root, at the line's precision.
|
|
7977
|
-
const ok = rests ? (ratio === null ?
|
|
7978
|
-
rows.push({ part: p.name, ...said, root_rig_px: round3(t.root), tip_rig_px: round3(t.tip), ratio });
|
|
8105
|
+
const ok = rests ? (ratio === null ? t.tip === 0 : ratio <= TIP_REST_RATIO_CEILING) : ratio === null ? t.tip > 0 : ratio >= TIP_RATIO_FLOOR;
|
|
8106
|
+
rows.push({ part: p.name, ...said, hang_from: hang, root_rig_px: round3(t.root), tip_rig_px: round3(t.tip), ratio, world_root_rig_px: round3(w.root), world_tip_rig_px: round3(w.tip), world_ratio: worldRatio });
|
|
8107
|
+
const against = hang === null ? 'in the world' : `against \`${hang}\`, the bone it hangs from`;
|
|
8108
|
+
const inWorld = hang === null ? '' : `; in the world ${round3(w.tip)} and ${round3(w.root)}, ratio ${worldRatio ?? 'undefined'}`;
|
|
8109
|
+
const travels = `the art in its ${tipHalf} half travels ${round3(t.tip)} rig px and in its ${rootHalf} half ${round3(t.root)} rig px ${against}`;
|
|
7979
8110
|
if (!ok && rests) {
|
|
7980
8111
|
failed = true;
|
|
7981
8112
|
problems.push({
|
|
7982
8113
|
code: 'CHECK_TIP_OVER_ROOT',
|
|
7983
8114
|
object: `part "${p.name}", box ${p.x},${p.y} ${p.w}x${p.h} rig px, posed by the idle's geometry, declared rests${rises ? ' and rises' : ''}`,
|
|
7984
|
-
detail:
|
|
8115
|
+
detail: `${travels} (ratio ${ratio ?? 'undefined: the root does not move'}${inWorld}); the scene file's rests declares its tip at rest, so a ratio <= ${TIP_REST_RATIO_CEILING} is required — the tip swings: the chain's amplitudes grow toward the tip, or the tip of the mesh is weighted to a bone the idle swings`,
|
|
7985
8116
|
});
|
|
7986
8117
|
} else if (!ok) {
|
|
7987
8118
|
failed = true;
|
|
7988
8119
|
// Issue #262: a root the breath carries is named with its carrier, not blamed on the chain.
|
|
7989
8120
|
// Issue #351: when the breath carries the root and the chain is also below the bar, or when the mesh is weighted
|
|
7990
8121
|
// off its chain without a breath carrier, name that too instead of only blaming the chain's amplitudes.
|
|
7991
|
-
|
|
8122
|
+
// Issue #987: only a breath under the hang bone moves the root against it; one at or above it is in the frame.
|
|
8123
|
+
const carry = breathCarrier(inp.rig, inp.motion, p.name, geometryPath, frames, { art, box: p, stage: inp.stage, shown, toward: rises ? 'up' : 'down', root: t.root, tip: t.tip, hang, toCarrier });
|
|
7992
8124
|
if (carry !== null) rows[rows.length - 1] = { ...rows[rows.length - 1], carried_by: carry.bone, breath_rig_px: round3(carry.breath), root_without_breath_rig_px: round3(carry.root), tip_without_breath_rig_px: round3(carry.tip) };
|
|
7993
8125
|
const offChain = carry === null && round3(t.tip) > 0 ? offChainClause(inp.rig, inp.motion, p.name) : null;
|
|
7994
|
-
const misplaced = rises || carry !== null || offChain !== null ? null : chainRootsBelowArtClause(inp.rig, inp.motion, p, inp.stage,
|
|
8126
|
+
const misplaced = rises || carry !== null || offChain !== null ? null : chainRootsBelowArtClause(inp.rig, inp.motion, p, inp.stage, idleGeo?.setupBones);
|
|
8127
|
+
const hangName = hang === null ? 'the bone the part hangs from' : `\`${hang}\`, the bone the part hangs from`;
|
|
8128
|
+
const held = carry === null ? '' : `the root travels with the breath (${round3(t.root)} px of ${round3(carry.breath)} px): it is carried by \`${carry.bone}\`, which moves it more than the tip; with \`${carry.keyed}\`'s translate held still the root travels ${round3(carry.root)} rig px and the tip ${round3(carry.tip)}`;
|
|
8129
|
+
const heldRatio = carry === null ? '' : `ratio ${carry.root > 0 ? round3(carry.tip / carry.root) : 'undefined: the root moves only with it'}`;
|
|
7995
8130
|
const why =
|
|
7996
8131
|
carry === null
|
|
7997
|
-
? `the chain's amplitudes do not grow toward the tip, or the mesh is not weighted to the chain${offChain === null ? '' : `; ${offChain}`}`
|
|
7998
|
-
: carry.
|
|
7999
|
-
? `the
|
|
8000
|
-
:
|
|
8132
|
+
? `${t.root === 0 && t.tip === 0 && hang !== null ? `nothing in it moves against \`${hang}\`: it rides that bone whole; ` : ''}the chain's amplitudes do not grow toward the tip, or the mesh is not weighted to the chain${offChain === null ? '' : `; ${offChain}`}`
|
|
8133
|
+
: carry.link
|
|
8134
|
+
? `${held}, which ${carry.passes ? `the bar passes (${heldRatio})` : `is still below the bar (${heldRatio}, < ${TIP_RATIO_FLOOR})`} — \`${carry.bone}\` is a link of the chain the part hangs on, so its segment stays: key \`${carry.keyed}\`'s translate on ${hangName} or a bone above it, where this reading takes it out, not under it${carry.passes ? '' : ", and grow the chain's amplitudes toward the tip"}`
|
|
8135
|
+
: carry.passes
|
|
8136
|
+
? `${held}, which the bar passes (${heldRatio}) — bind the root to ${hangName}, not to \`${carry.bone}\``
|
|
8137
|
+
: `${held}, which is still below the bar (${heldRatio}, < ${TIP_RATIO_FLOOR}) — drop the segment on \`${carry.bone}\` from \`meshes.${p.name}.segments\` (or bind the root to ${hangName}, not to \`${carry.bone}\`) and grow the chain's amplitudes toward the tip`;
|
|
8001
8138
|
problems.push({
|
|
8002
8139
|
code: 'CHECK_TIP_OVER_ROOT',
|
|
8003
8140
|
object: `part "${p.name}", box ${p.x},${p.y} ${p.w}x${p.h} rig px, posed by the idle's geometry${rises ? ', declared rises' : ''}`,
|
|
8004
|
-
detail:
|
|
8141
|
+
detail: `${travels} (ratio ${ratio ?? (hang === null ? 'undefined: nothing moves' : `undefined: nothing moves against \`${hang}\``)}${inWorld}); a ratio >= ${TIP_RATIO_FLOOR} is required — ${misplaced ?? why}`,
|
|
8005
8142
|
});
|
|
8006
8143
|
}
|
|
8007
8144
|
}
|
|
@@ -8109,7 +8246,13 @@ const BREATH_PROPS: readonly string[] = ['translate', 'translatex', 'translatey'
|
|
|
8109
8246
|
* them. The root travels with the breath when the breath adds more to the root's travel than to the tip's and the tip
|
|
8110
8247
|
* moves above 0 at the line's precision (round3); `passes` records whether, held still, the halves pass the bar
|
|
8111
8248
|
* (tip >= {@link TIP_RATIO_FLOOR} x root). The carrier named is the bone the part is bound to ({@link boundBones}) at
|
|
8112
|
-
* or under `K` that no other such bone is above
|
|
8249
|
+
* or under `K` that no other such bone is above. Null otherwise.
|
|
8250
|
+
*
|
|
8251
|
+
* Issue #987: the halves are read in the frame of the bone the part hangs from (`m.hang`), so only a breath keyed
|
|
8252
|
+
* strictly under that bone is read, and the held pose goes through the same frame (`m.toCarrier`). Before #987 a
|
|
8253
|
+
* sleeve whose chain rides `chest` was named carried by the chain's own first link, and the advice told the author to
|
|
8254
|
+
* drop that link's segment; the breath on `chest` is now in the frame and carries nothing. `link` says the carrier
|
|
8255
|
+
* named is a link of a chain `CHAIN_LAG` reads, whose segment the advice must not tell the author to drop.
|
|
8113
8256
|
*/
|
|
8114
8257
|
function breathCarrier(
|
|
8115
8258
|
rig: Record<string, unknown>,
|
|
@@ -8117,8 +8260,18 @@ function breathCarrier(
|
|
|
8117
8260
|
part: string,
|
|
8118
8261
|
geometryPath: string,
|
|
8119
8262
|
frames: number,
|
|
8120
|
-
m: {
|
|
8121
|
-
|
|
8263
|
+
m: {
|
|
8264
|
+
art: ArtMask;
|
|
8265
|
+
box: { x: number; y: number; w: number; h: number };
|
|
8266
|
+
stage: StageBox;
|
|
8267
|
+
shown: ReadonlyArray<PosedAttachment | null>;
|
|
8268
|
+
toward: 'down' | 'up';
|
|
8269
|
+
root: number;
|
|
8270
|
+
tip: number;
|
|
8271
|
+
hang: string | null;
|
|
8272
|
+
toCarrier: (i: number, posed: readonly number[]) => number[];
|
|
8273
|
+
},
|
|
8274
|
+
): { bone: string; keyed: string; breath: number; root: number; tip: number; passes: boolean; link: boolean } | null {
|
|
8122
8275
|
const anims = isRecord(motion.animations) ? motion.animations : {};
|
|
8123
8276
|
const idle = isRecord(anims.idle) ? anims.idle : {};
|
|
8124
8277
|
const keyed = [...new Set((Array.isArray(idle.tracks) ? idle.tracks : []).filter(isRecord).filter((t) => typeof t.bone === 'string' && typeof t.property === 'string' && BREATH_PROPS.includes(t.property)).map((t) => String(t.bone)))];
|
|
@@ -8154,7 +8307,10 @@ function breathCarrier(
|
|
|
8154
8307
|
return { a: b.a as number, b: b.b as number, c: b.c as number, d: b.d as number, worldX: b.worldX as number, worldY: b.worldY as number };
|
|
8155
8308
|
};
|
|
8156
8309
|
const bound = [...boundBones(rig, part)];
|
|
8310
|
+
const links = new Set(chainFigures(rig, motion).chains.filter((c) => c.length >= 2).flatMap((c) => c.map((l) => l.bone)));
|
|
8157
8311
|
for (const k of keyed) {
|
|
8312
|
+
// Issue #987: a breath at or above the hang bone is in the frame the halves are read in, so it carries nothing.
|
|
8313
|
+
if (m.hang !== null && (k === m.hang || !atOrUnder(k, m.hang))) continue;
|
|
8158
8314
|
const under = bound.filter((b) => atOrUnder(b, k));
|
|
8159
8315
|
const carrier = under.find((b) => !under.some((o) => o !== b && atOrUnder(b, o)));
|
|
8160
8316
|
if (carrier === undefined) continue;
|
|
@@ -8174,11 +8330,11 @@ function breathCarrier(
|
|
|
8174
8330
|
const breath = Math.max(...delta.map(([x, y]) => edgeLength(x, y)));
|
|
8175
8331
|
if (!(breath > 0)) continue;
|
|
8176
8332
|
const share = weights.map((v) => v.filter((w) => atOrUnder(String(w.bone), k)).reduce((s, w) => s + (w.weight as number), 0));
|
|
8177
|
-
const held = m.shown.map((f, i) => (f === null ? null : { rest: f.rest, triangles: f.triangles, posed: f.posed.map((v, j) => v - share[j >> 1] * delta[i][j & 1]) }));
|
|
8333
|
+
const held = m.shown.map((f, i) => (f === null ? null : { rest: f.rest, triangles: f.triangles, posed: m.toCarrier(i, f.posed.map((v, j) => v - share[j >> 1] * delta[i][j & 1])) }));
|
|
8178
8334
|
const t = halfTravels(m.art, m.box, m.stage, held, m.toward);
|
|
8179
8335
|
if (t.root === null || t.tip === null) continue;
|
|
8180
8336
|
if (!(m.root - t.root > Math.max(0, m.tip - t.tip)) || round3(t.tip) === 0) continue;
|
|
8181
|
-
return { bone: carrier, keyed: k, breath, root: t.root, tip: t.tip, passes: t.tip >= TIP_RATIO_FLOOR * t.root };
|
|
8337
|
+
return { bone: carrier, keyed: k, breath, root: t.root, tip: t.tip, passes: t.tip >= TIP_RATIO_FLOOR * t.root, link: links.has(carrier) };
|
|
8182
8338
|
}
|
|
8183
8339
|
return null;
|
|
8184
8340
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SEAM_RULES, type SeamRule } from './seamrule.ts';
|
|
2
|
-
import { PROJECT_RULES, type ProjectRule } from './projectrule.ts';
|
|
2
|
+
import { GROUNDS, type Ground, PROJECT_RULES, type ProjectRule } from './projectrule.ts';
|
|
3
3
|
import type { TakeLayers } from './takelayers.ts';
|
|
4
4
|
import { paintingExtensionProblems, type PaintingExtension } from './paintingextension.ts';
|
|
5
5
|
import { baseAnchorProblems, type BaseAnchor } from './baseanchor.ts';
|
|
@@ -307,6 +307,8 @@ export interface Assemble {
|
|
|
307
307
|
seam?: SeamRule;
|
|
308
308
|
/** Optional per-character projection rule (issue #953); an explicit CLI flag wins. */
|
|
309
309
|
project?: ProjectRule;
|
|
310
|
+
/** Optional per-character painting ground (issue #996); absent, `plain`. Read by `visible` only. */
|
|
311
|
+
ground?: Ground;
|
|
310
312
|
/** Verified named replacements from another See-through run (#866). */
|
|
311
313
|
take?: TakeLayers[];
|
|
312
314
|
/** Issue #946: receipts for images edited in an image editor; absent, no file is read and no byte moves. */
|
|
@@ -1629,7 +1631,7 @@ export interface EarlyConfig {
|
|
|
1629
1631
|
depth?: DepthConfig;
|
|
1630
1632
|
key: string;
|
|
1631
1633
|
seethrough: SeeThrough;
|
|
1632
|
-
assemble: { rig_scale: number; seam?: SeamRule; project?: ProjectRule; take?: TakeLayers[]; editor_receipts?: EditorReceipts; head_face_underfill?: boolean; yield_refused?: boolean; keep_under_occluders?: string[]; extend_painting?: PaintingExtension[] };
|
|
1634
|
+
assemble: { rig_scale: number; seam?: SeamRule; project?: ProjectRule; ground?: Ground; take?: TakeLayers[]; editor_receipts?: EditorReceipts; head_face_underfill?: boolean; yield_refused?: boolean; keep_under_occluders?: string[]; extend_painting?: PaintingExtension[] };
|
|
1633
1635
|
}
|
|
1634
1636
|
|
|
1635
1637
|
/** What plain `assemble` reads: the early config plus the plan `--propose-plan` printed. */
|
|
@@ -1705,7 +1707,7 @@ function parseEarlyConfigFrom(raw: Json, door: EarlyDoor, base: string | null):
|
|
|
1705
1707
|
if (door === 'layers') {
|
|
1706
1708
|
if ('seethrough' in t) checkSeeThrough(c, t.seethrough);
|
|
1707
1709
|
if ('assemble' in t) {
|
|
1708
|
-
const a = c.object('config.assemble', t.assemble, sectionRequired(door, 'assemble'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam', 'project', 'editor_receipts']);
|
|
1710
|
+
const a = c.object('config.assemble', t.assemble, sectionRequired(door, 'assemble'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam', 'project', 'ground', 'editor_receipts']);
|
|
1709
1711
|
if (a !== null) {
|
|
1710
1712
|
checkAssembleRules(c, a);
|
|
1711
1713
|
if ('rig_scale' in a) c.number('config.assemble.rig_scale', a.rig_scale, 'positive');
|
|
@@ -2513,11 +2515,14 @@ function checkAssembleRules(c: Check, a: Record<string, Json>): void {
|
|
|
2513
2515
|
if ('project' in a && !(PROJECT_RULES as readonly unknown[]).includes(a.project)) {
|
|
2514
2516
|
c.fail('CONFIG_FIELD_TYPE', 'config.assemble.project', `is ${show(a.project)}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
2515
2517
|
}
|
|
2518
|
+
if ('ground' in a && !(GROUNDS as readonly unknown[]).includes(a.ground)) {
|
|
2519
|
+
c.fail('CONFIG_FIELD_TYPE', 'config.assemble.ground', `is ${show(a.ground)}; one of ${GROUNDS.join(', ')} is required (busy for an opaque ground of many colours, issue #996)`);
|
|
2520
|
+
}
|
|
2516
2521
|
}
|
|
2517
2522
|
|
|
2518
2523
|
function checkAssemble(c: Check, v: Json, door: 'assemble' | 'full'): { parts: string[]; patches: string[]; cuts: string[] } {
|
|
2519
2524
|
const required = sectionRequired(door, 'assemble');
|
|
2520
|
-
const a = c.object('config.assemble', v, required.filter((k) => k !== 'plan'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam', 'project', 'editor_receipts']);
|
|
2525
|
+
const a = c.object('config.assemble', v, required.filter((k) => k !== 'plan'), ['plan', 'extend_below_crop', 'patches', 'cuts', 'alternates', 'head_face_underfill', 'yield_refused', 'keep_under_occluders', 'extend_painting', 'take', 'seam', 'project', 'ground', 'editor_receipts']);
|
|
2521
2526
|
if (a === null) return { parts: [], patches: [], cuts: [] };
|
|
2522
2527
|
checkAssembleRules(c, a);
|
|
2523
2528
|
if (required.includes('plan') && !('plan' in a)) {
|
package/src/paintingedges.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { OPAQUE_ALPHA_ABOVE } from './layers.ts';
|
|
2
2
|
import { connectedComponents } from './raster/components.ts';
|
|
3
|
+
import { type Ground } from './projectrule.ts';
|
|
3
4
|
import { type Mask, newMask, type Raster, RasterError } from './raster/types.ts';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -100,6 +101,31 @@ import { type Mask, newMask, type Raster, RasterError } from './raster/types.ts'
|
|
|
100
101
|
* and pixel). Where that leaves out BEHIND's fill under FRONT, FRONT takes the
|
|
101
102
|
* painting's pixel at full alpha instead, under the same rule.
|
|
102
103
|
*
|
|
104
|
+
* ⚖️ A busy ground (`ground: 'busy'`, issue #996). The two premises above —
|
|
105
|
+
* `painted` separates figure from ground, and BEHIND is one colour `Cb` across
|
|
106
|
+
* the window — hold only where the ground is one colour. On an opaque ground of
|
|
107
|
+
* many colours `paintedFigure` marks the ground as figure, so the gap rule hands
|
|
108
|
+
* ground pixels to the nearest part, and a ground pixel that is not `Cb`
|
|
109
|
+
* projects along `Cb -> Cf` as FRONT. Under a busy ground three rules bound what
|
|
110
|
+
* a part may take:
|
|
111
|
+
*
|
|
112
|
+
* - a part's alpha rises at a pixel only where its own alpha before the step is
|
|
113
|
+
* above 0 (its support, the reach of See-through's layer through the
|
|
114
|
+
* resampling into the rig) — FRONT's coverage, BEHIND's fill and the
|
|
115
|
+
* painting's pixel FRONT takes where BEHIND's fill is held back alike;
|
|
116
|
+
* a write that would raise one is not made (counted as `support`);
|
|
117
|
+
* - the gap rule takes a pixel only where the supports of at least two parts
|
|
118
|
+
* meet (a gap between two soft rims, which is what it is for), and gives it
|
|
119
|
+
* to the nearest of those parts;
|
|
120
|
+
* - a pixel on BEHIND's side of See-through's line is read only where it is a
|
|
121
|
+
* mix of `Cb` and `Cf`: the painting differs from the nearest point of the
|
|
122
|
+
* segment `Cb..Cf` by at most half of {@link EDGE_CONTRAST} (max channel, the
|
|
123
|
+
* tolerance the composite is held to above); otherwise it is left as it was
|
|
124
|
+
* (counted as `mix`).
|
|
125
|
+
*
|
|
126
|
+
* Under a plain ground (the default) none of the three applies, and the step is
|
|
127
|
+
* the one described above, byte for byte.
|
|
128
|
+
*
|
|
103
129
|
* Every decision is read off the stack as it stood before the step, then
|
|
104
130
|
* applied, so the result does not depend on the order pixels are visited.
|
|
105
131
|
* Deterministic: integer rig arithmetic, medians by sort, no float reduction
|
|
@@ -119,6 +145,10 @@ export interface PaintingEdgeSkips {
|
|
|
119
145
|
noBehind: number;
|
|
120
146
|
/** Part writes left out so a part's art keeps its 4-connected pieces (counted per part and pixel, not per edge pixel). */
|
|
121
147
|
pieces: number;
|
|
148
|
+
/** Busy ground only (issue #996): BEHIND-side pixels that are not a mix of the two reference colours. */
|
|
149
|
+
mix: number;
|
|
150
|
+
/** Busy ground only (issue #996): part writes left out because they would raise a part's alpha outside its own support (per part and pixel). */
|
|
151
|
+
support: number;
|
|
122
152
|
}
|
|
123
153
|
|
|
124
154
|
export interface PaintingEdgeResult {
|
|
@@ -148,6 +178,8 @@ export interface PaintingEdgeOptions {
|
|
|
148
178
|
movingPair?: ((a: number, b: number) => boolean) | null;
|
|
149
179
|
/** Called once per part and pixel whose RGBA changed. */
|
|
150
180
|
written?: (part: number, pixel: number) => void;
|
|
181
|
+
/** Issue #996: the painting's ground; absent, `plain` (see the invariant above {@link EDGE_RADIUS}). */
|
|
182
|
+
ground?: Ground;
|
|
151
183
|
}
|
|
152
184
|
|
|
153
185
|
/** Chebyshev distance to the nearest set pixel, capped at `cap` (two-pass, exact for the 8-neighbour metric). */
|
|
@@ -219,6 +251,9 @@ export function paintingEdges(parts: Raster[], srcr: Raster, options: PaintingEd
|
|
|
219
251
|
});
|
|
220
252
|
const R = EDGE_RADIUS;
|
|
221
253
|
const cap = 2 * R + 2;
|
|
254
|
+
const busy = options.ground === 'busy';
|
|
255
|
+
/** A part's support before the step: where its own alpha is above 0. */
|
|
256
|
+
const supports = (i: number, p: number): boolean => parts[i].data[p * 4 + 3] > 0;
|
|
222
257
|
const owner = new Int16Array(W * H).fill(-1);
|
|
223
258
|
for (let p = 0; p < W * H; p++) {
|
|
224
259
|
for (let i = n - 1; i >= 0; i--) {
|
|
@@ -239,9 +274,15 @@ export function paintingEdges(parts: Raster[], srcr: Raster, options: PaintingEd
|
|
|
239
274
|
const fill: Array<[number, number]> = [];
|
|
240
275
|
for (let p = 0; p < W * H; p++) {
|
|
241
276
|
if (owner[p] !== -1 || options.painted[p] !== 1) continue;
|
|
277
|
+
// Busy ground: only where two parts' supports meet, and only to a part whose support holds the pixel.
|
|
278
|
+
if (busy) {
|
|
279
|
+
let holders = 0;
|
|
280
|
+
for (let k = 0; k < n && holders < 2; k++) if (supports(k, p)) holders++;
|
|
281
|
+
if (holders < 2) continue;
|
|
282
|
+
}
|
|
242
283
|
let best = -1;
|
|
243
284
|
let bd = cap;
|
|
244
|
-
for (let k = n - 1; k >= 0; k--) if (near[k][p] < bd) { best = k; bd = near[k][p]; }
|
|
285
|
+
for (let k = n - 1; k >= 0; k--) if (near[k][p] < bd && (!busy || supports(k, p))) { best = k; bd = near[k][p]; }
|
|
245
286
|
if (best >= 0) fill.push([p, best]);
|
|
246
287
|
}
|
|
247
288
|
for (const [p, k] of fill) owner[p] = k;
|
|
@@ -261,7 +302,7 @@ export function paintingEdges(parts: Raster[], srcr: Raster, options: PaintingEd
|
|
|
261
302
|
dist.push(any ? chebyshev(set, W, H, cap) : new Uint8Array(W * H).fill(cap));
|
|
262
303
|
}
|
|
263
304
|
const distOf = (label: number): Uint8Array => dist[label < 0 ? n : label];
|
|
264
|
-
const skipped: PaintingEdgeSkips = { cover: 0, contrast: 0, locked: 0, noBehind: 0, pieces: 0 };
|
|
305
|
+
const skipped: PaintingEdgeSkips = { cover: 0, contrast: 0, locked: 0, noBehind: 0, pieces: 0, mix: 0, support: 0 };
|
|
265
306
|
const decisions: Decision[] = [];
|
|
266
307
|
let resolved = 0;
|
|
267
308
|
let unseparated = 0;
|
|
@@ -363,6 +404,17 @@ export function paintingEdges(parts: Raster[], srcr: Raster, options: PaintingEd
|
|
|
363
404
|
const sideHi = sideF.length === 0 ? -Infinity : Math.min(...sideF.map(along));
|
|
364
405
|
const separate = sideHi > sideLo;
|
|
365
406
|
if (!separate) unseparated++;
|
|
407
|
+
// Busy ground: a pixel on BEHIND's side is read only where it is a mix of Cb and Cf (its distance, max channel, from
|
|
408
|
+
// the nearest point of the segment Cb..Cf within half the contrast floor); a ground of many colours is not Cb.
|
|
409
|
+
if (busy && a !== front) {
|
|
410
|
+
const ax2 = axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2];
|
|
411
|
+
const t = Math.min(1, Math.max(0, along(p) / ax2));
|
|
412
|
+
const off = Math.max(...[0, 1, 2].map((c) => Math.abs(srcr.data[p * 4 + c] - (cb[c] + t * axis[c]))));
|
|
413
|
+
if (2 * off > EDGE_CONTRAST) {
|
|
414
|
+
skipped.mix++;
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
366
418
|
const lo = separate ? sideLo : 0;
|
|
367
419
|
const hi = separate ? sideHi : along(ref);
|
|
368
420
|
resolved++;
|
|
@@ -428,6 +480,10 @@ export function paintingEdges(parts: Raster[], srcr: Raster, options: PaintingEd
|
|
|
428
480
|
const d = parts[i].data;
|
|
429
481
|
const s = p * 4;
|
|
430
482
|
if (d[s] === r && d[s + 1] === g && d[s + 2] === bl && d[s + 3] === al) return;
|
|
483
|
+
if (busy && al > d[s + 3] && !supports(i, p)) {
|
|
484
|
+
skipped.support++;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
431
487
|
planned[i].set(p, [r, g, bl, al]);
|
|
432
488
|
};
|
|
433
489
|
/** Per part, the pixels it fills as BEHIND, with the FRONT part over them. */
|
|
@@ -444,7 +500,14 @@ export function paintingEdges(parts: Raster[], srcr: Raster, options: PaintingEd
|
|
|
444
500
|
if (alpha > 0 && alpha < 255) while (alpha < 255 && 2 * miss(alpha) > EDGE_CONTRAST) alpha++;
|
|
445
501
|
if (alpha >= 255 - OPAQUE_ALPHA_ABOVE) alpha = 255;
|
|
446
502
|
const unmix = (c: number): number => unmixAt(c, alpha);
|
|
447
|
-
|
|
503
|
+
// Busy ground: a BEHIND raise outside BEHIND's own support is refused in `plan`, so FRONT is not lowered there either —
|
|
504
|
+
// FRONT keeps the coverage it had, and a seam whose two supports do not overlap is never left transparent.
|
|
505
|
+
// The refused BEHIND raise below is the one counted in `skipped.support`.
|
|
506
|
+
const behindRefused = busy && q.behind >= 0 && alpha < 255 && parts[q.behind].data[s + 3] < 255 && !supports(q.behind, q.p);
|
|
507
|
+
const keepFront = behindRefused && alpha < parts[q.front].data[s + 3];
|
|
508
|
+
if (keepFront) {
|
|
509
|
+
// FRONT keeps its coverage: nothing is planned for it here.
|
|
510
|
+
} else if (alpha === 0) plan(q.front, q.p, 0, 0, 0, 0);
|
|
448
511
|
else if (alpha === 255) plan(q.front, q.p, P[0], P[1], P[2], 255);
|
|
449
512
|
else plan(q.front, q.p, unmix(0), unmix(1), unmix(2), alpha);
|
|
450
513
|
if (q.behind >= 0 && alpha < 255) {
|
|
@@ -526,7 +589,10 @@ export function paintingEdges(parts: Raster[], srcr: Raster, options: PaintingEd
|
|
|
526
589
|
// A fill BEHIND cannot take: FRONT takes the painting's pixel whole, so the composite there is still the painting
|
|
527
590
|
// (FRONT is later in draw order, so its own pieces are held after this).
|
|
528
591
|
const front = fills[i].get(p);
|
|
529
|
-
if (front !== undefined)
|
|
592
|
+
if (front !== undefined) {
|
|
593
|
+
if (busy && !supports(front, p)) skipped.support++;
|
|
594
|
+
else planned[front].set(p, [srcr.data[p * 4], srcr.data[p * 4 + 1], srcr.data[p * 4 + 2], 255]);
|
|
595
|
+
}
|
|
530
596
|
}
|
|
531
597
|
}
|
|
532
598
|
});
|
package/src/projectrule.ts
CHANGED
|
@@ -20,3 +20,18 @@ export const PROJECT_RULES: readonly ProjectRule[] = ['core', 'visible'];
|
|
|
20
20
|
* it is opt-in.
|
|
21
21
|
*/
|
|
22
22
|
export const DEFAULT_PROJECT_RULE: ProjectRule = 'core';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Issue #996: what lies behind the figure in the painting. `plain` is a ground
|
|
26
|
+
* of one colour, the assumption `paintedFigure` (src/assemble.ts) makes when it
|
|
27
|
+
* reads the border ring's median as the ground. `busy` is an opaque ground of
|
|
28
|
+
* many colours (a wall, a banner), where that test marks the ground as figure
|
|
29
|
+
* and the painting alone cannot tell a part's painted outline from the ground
|
|
30
|
+
* next to it. Under `visible`, `paintingEdges` then bounds what a part may take
|
|
31
|
+
* (see its invariant). The ground is declared by the author, never inferred.
|
|
32
|
+
*/
|
|
33
|
+
export type Ground = 'plain' | 'busy';
|
|
34
|
+
export const GROUNDS: readonly Ground[] = ['plain', 'busy'];
|
|
35
|
+
|
|
36
|
+
/** The ground when none is declared: `plain`, the code path every build ran before issue #996. */
|
|
37
|
+
export const DEFAULT_GROUND: Ground = 'plain';
|