rig-parts 2.1.0 → 2.2.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 +180 -2
- package/package.json +1 -1
- package/src/build.ts +2 -1
- package/src/check.ts +368 -2
- package/src/raster/morph.ts +40 -18
package/docs/AUTHORING.md
CHANGED
|
@@ -157,7 +157,7 @@ read by no CPU stage).
|
|
|
157
157
|
| `assemble.alternates` | assemble, rig, check (`--set`, `--all-sets`) | **authored**, optional (issues #454, #465, #472, #473, #476): `[{name, source?, full?, head?, head_box?, parts?, hides?, cuts?, skeleton?, anchor?, base_anchor?, drawn?, tips?, front?}]` — alternate painting sets assembled into `<out>/rig/parts/<set>_<part>.png` (`parts.json` `alternates`), attached on each covered slot in `rig.json` as `<set>_<part>` beside the base attachment, and indexed in `<out>/rig/sets.json` (`rig-parts-sets/1`: `sets.<set>.slots`, `.hides`, `.drawn` when present, and `.threshold: {yaw: 15, pitch: 12}` when the set replaces `face` on a rig with `turn` or `turn`/`tilt` slider animations). `source`, `full` and `head` may instead be given on the CLI with `--alt <name>=<source>:<full>[:<head>]`. Windows absolute drive paths such as `C:\paint\painting.png` are accepted as written; each segment's leading drive colon belongs to that path, while other colons still separate `source`, `full` and `head`. `parts` lists the subset of parts the set replaces (default: every part not in `hides`); a **subset set** (issue #473, e.g. mouth visemes `a`, `i`, `u` with `"parts": ["mouth"]`) cuts only its covered parts from the alternate runs (refusing with `ASSEMBLE_ALT_INPUT_PRESENT` if a covered part's layer is missing or empty), writes only `{slot: "<set>_<slot>"}` in `sets.json` so `@rig-c/play`'s `setSet(name)` switches those slots while leaving every other slot on its base attachment (and `setSet(null)` restores the base pose), may be keyed on a slot attachment track in `motion.animations_from` as `{"t": …, "set": "<name>" \| null}` (`CONFIG_SET_COVERS_SLOT` if `"set"` is undeclared or does not cover the track's slot), and under `check --set <name>` counts `ANIMATIONS_DO_NOT_FOLD` only on its own covered slots; `hides` lists slots the set hides (`null` in `sets.json`). A **gesture subset set** (issue #476) covers a limb chain declared in `config.limbs.<limb>` (all of its segment `part`s; with `drawn` alone, cut at the base's joints posed to the set's own `drawn` angles by `poseBoneEntriesForDrawn`, each segment keeping the base's length; with `skeleton` (issue #539), cut at the skeleton's own joint positions by `placeBoneEntriesForSkeleton` — each covered bone the body-18 table names (`sleeve_*`, `arm_*0`/`arm_*1`, `leg_*0`/`leg_*1`) at its skeleton origin with the skeleton segment's own length and direction (`drawn` overriding the direction of a bone it names), and a covered bone the table does not name (a hand link `arm_r2`, a single `foot_r`) placed from its parent's new place — its base offset from the parent, stretched along the parent by the parent's length change and turned by its turn, so a link or a foot at the parent's end lands on the skeleton's end — with its base length and the drawn or base direction, so a foreshortened shin or forearm is cut where it is painted; naming the unsplit `from` layer, covering only some segments of the limb, or declaring polygon `cuts` on a limb set is refused with `CONFIG_LIMB_SET_JOINTS`) and carries `drawn` (or `skeleton`) for the covered bones only (giving `drawn` for an uncovered bone is refused with `CONFIG_SET_COVERS_BONE` / `RIG_SET_COVERS_BONE`; uncovered bones keep the base's `drawn`). A subset set's limb part takes no pixel that a base part drawn after it, which the set neither covers nor hides, covers at setup (issue #587): the set's attachment is drawn in the base slot's place, so that base part is drawn over it and what the set's painting shows there is that base part (shorts over a leg set's thigh top, which See-through synthesised in the set's leg layer); `assemble` clears those pixels — the base part's PNG at alpha above 8, at its `parts.json` x, y, on the shared canvas — from the set's part once its limbs are cut, before any count — its `merged_px` and its cap's record too (issue #603): a cap pixel it clears leaves the count it was in (`layer_px`, `image_px`, `upper_px`, `lower_px`, `lower_kept_px`) for the cap's `excluded_px`, written only when above 0, so every count is the written PNG's — and prints `LINT set "<set>" <part>: <n> px under <base part> excluded` per part and base part that took a pixel (n counts alpha above 8); a set that carries that base part itself (`"parts": ["thigh_l", "shin_l", "bottomwear"]`) excludes nothing and writes what it wrote before. A subset set that names one limb side of a layer more limbs take (issue #588: `"parts": ["thigh_r", "shin_r"]` where `leg_r` and `leg_l` both take the one `legs` layer) would put every pixel of that layer on its one limb, so each of its parts would hold a piece of both legs; when the set's layer holds more than one island (8-connected pixels with alpha above 8) of at least 1 % of its largest (issue #608: the ghost clean-up's own share; a smaller island is a speck, which goes with the island nearest it — the one holding the pixel centre nearest one of the speck's, none when two are equally near — and is never an island of its own), each segment part keeps the island its joints fall on — its bone's origin and the next segment's bone's origin, at the set's own bones, a joint on the island of a pixel whose square holds the point, and a joint on no island on the one island whose nearest pixel centre lies within the radius of the cap cut at that joint (`cap.radius`: an ankle just past the legwear's edge, on the shoe) — and drops the rest, once its limbs are cut and before the exclusion above, and `assemble` prints `LINT set "<set>" <part>: <layer part> holds <n> islands (8-connected, alpha above 8)[, and <s> speck(s) under 1 % of the largest (<f> px), each with the island nearest it], kept the one under <bone> [x, y] and <bone> [x, y][ (<e> px off it, within its cap's radius <r>)] (<k> px), dropped <m> (<d> px)` per part (k and d count the part's pixels above alpha 8, m the islands it lost one from, and `dropped none` when it lost none); a joint on no island with none or two within that radius, or with no cap cut at it (a limb's first joint), and a segment whose joints fall on two, are refused with `ASSEMBLE_ALT_LIMB_ISLAND` rather than guessed. A layer of one island — specks aside — may hold both legs over each other (a folded shin across the standing leg; issue #609): each of the set's segment parts then keeps the pixels nearer its own limb's segment line — the polyline of only its segments taking that source, their bone origins and the last segment's supplied tip or following segment's joint (issue #638) — than the other limb's, both in the set's pose, and gives the rest away, once its limbs are cut and before the exclusion above; a pixel whose two distances differ by 0.5 px or less (the overlap band: half a pixel, the reach by which a pixel centre is read as on a line) goes to the limb whose part there is drawn later in the base's draw order, and the `LINT set` line above counts what each part kept, gave and held in the band. The other limb stands where the set's `skeleton` places it — its keypoints place every limb bone of the body-18 table, the other leg's as well as the set's own (its `drawn` and `drawnLength` stay the covered bones') — and never at the base's place: a set that does not place it (a `drawn` set, which may not name a bone it does not cover, or a set with neither) is refused with `ASSEMBLE_ALT_LIMB_POSE` when its layer has art where the base draws that limb, and when it has none nothing says the layer holds it and the layer is cut as before; two limbs whose segment lines run closer than the limb's half-width over most of where their art can meet are refused with `ASSEMBLE_ALT_LIMB_OVERLAP`. A set that carries every limb of the layer is cut as before. For a subset set, where a limb's own source layer holds art (`alpha > 8`) under another layer of that run drawn in front (`alpha > 8`, by the projection's plan/depth order), the limb keeps its own layer pixels through projection, limb cutting, island/nearest-line assignment, base-footprint exclusion, fringe clearing and seam correction (issue #637). Those pixels are occluded, never counted as painting projection; no base footprint or painting colour replaces them. After source-art island/nearest-line assignment, a source-transparent cap pixel is assigned before protection, in both doors: its centre is compared against only the segments taking the recipient's source, at the set's supplied bones (ankle-to-toe for a separate foot source), with the existing 0.5 px overlap band and base draw order. A missing last endpoint is refused with `ASSEMBLE_ALT_LIMB_POSE`; an actual following joint may supply that endpoint, never a guessed point. An unposed other limb with base art beneath these cap pixels is likewise refused; without such overlap the existing compatible path retains the cap without inventing the other limb's pose. A pixel cleared by this assignment is never restored. `CAP_FROM_UPPER` records a copy made before assignment and alone proves no ownership: only a retained copy is protected beneath the upper segment, which is drawn over it. The source-art island selection itself is unchanged. Other retained cap pixels (from the layer or a supplied cap image) are protected under an actual front layer even where the lower source is transparent. Protection follows each assigned piece through exclusion, fringe clearing and seam correction; it never restores a pixel ownership removed. Cap pixels removed during ownership leave their original counts for `excluded_px`, just as excluded pixels do, so the cap ledger describes the written PNG. Target assignment reconciles this ledger even when there are no source-transparent cap candidates and when a direct caller does not request cover protection. Unprotected source pixels still obey the base-footprint exclusion. Missing limb/cap art is never extended or guessed. Base builds and uncovered, unexcluded sets keep their existing bytes. `skeleton` (a pose file path relative to the config's directory, `{"width": W, "height": H, "points": {…}}` with all 18 body-18 keypoints on the canvas, validated by the same rules as `generation.control.skeleton`: `CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON`, `CONFIG_FIELD_TYPE`, `CONFIG_KEY_KNOWN`, `CONFIG_KEY_UNIQUE`, `CONFIG_POSE_CANVAS`, `CONFIG_POSE_KEYPOINTS`, `CONFIG_POSE_IN_CANVAS`) or `drawn` (`{<bone>: <worldDegrees>}`, each bone resolved against `bones`, `CONFIG_NAME_RESOLVES`) declares the posed angles the alternate was painted at (issue #465); when `skeleton` is given, `assemble` derives `drawn` from its 18 keypoints (filtered to covered bones on a subset set, with `drawn` overriding any bone named in both), records both `skeleton` (scaled by `assemble.rig_scale` to rig px) and `drawn` in `parts.json` `alternates` — and, when the skeleton placed the set's limb bones (issue #539), `drawnLength` (`{<bone>: <ratio>}`, each covered table bone's skeleton segment length over its base length, 6 places) and `registration` (`{offset: [dx, dy], scale}`: the similarity without rotation, about the canvas origin, that maps the skeleton's torso — the mean of its shoulders `r_shoulder`/`l_shoulder` to the mean of its hips `r_hip`/`l_hip`, read where the base declares the bone rooted there, `sleeve_*0`/`arm_*0` and `leg_*0` — onto those base bones' origins, 6 places), both also written to `sets.json`; the set's painting must be registered, its torso within 1 rig px and 1 % in scale of the base's, or the set is refused by name before any joint is cut (`ASSEMBLE_ALT_SKELETON_REGISTERED`, with the offset and scale the painting would need) — and the `rig` stage writes `<set>_*` region and mesh attachments in the bone-local frame of `poseRigBonesForDrawn(bones, drawn)` (weighting limb segments directly from `skeleton`'s joint endpoints when `skeleton` is present) and emits `drawn` in `sets.json` so `rig-pose` and `check --set <name>` / `--all-sets` pose the bones at `drawn` without rotating the already-posed art a second time. `front` (issue #664) lists covered limb segment parts the set draws on their **front slot** instead of the base slot: each must be in the set's `parts` and not its `hides`, and listed in some `config.limbs.<limb>.front.parts` (`CONFIG_SET_FRONT`); the set then maps `<part>_front` (never `<part>`) to `<set>_<part>` in `parts.json` and `sets.json` and hides `<part>` (and `<part>_still`), its #587 exclusion counts only base parts drawn after the front slot's target, and keying that part's set on a slot track is refused (`CONFIG_SET_FRONT_KEYED`) — *Front slots (issue #664)* below `layers` optionally maps covered, nonhidden output part names to layer names in their existing source run (issue #666); for example `"layers": {"upperarm_r": "author:raised", "forearm_r": "author:raised"}`. Limb cuts, caps, islands and base exclusions read the selected layer; the base plan stays unchanged. |
|
|
158
158
|
| `assemble.head_face_underfill` | assemble, `assemble --propose-plan` | **authored**, optional (issue #492): `boolean` (`CONFIG_FIELD_TYPE` on any other). During head-face preparation, when both runs exist and `full:face` has at least `150` opaque run px (`PROPOSE_MIN_PX`), the stage maps `full:face` into the head canvas via `fullLayerToHead` and measures how much of its hole-filled silhouette (`alpha > 8`) is covered by `head:face` (`alpha > 8`). An explicit plan selecting `head:face` is refused when that layer covers less than `75 %` (`HEAD_FACE_COVERAGE_FLOOR`, measured `0.0 %..48.6 %` on hollow/omitted head runs vs `84.8 %..99.6 %` on clean head runs across the 4 public examples and 9-standee corpus), with `SEETHROUGH_HEAD_RUN_HOLLOW`, unless `head_face_underfill: true` is declared. When `head_face_underfill: true` is declared and `head:face` exists (`opaqueRunPx >= 150`), `fullLayerToHead(full:face)` is composited under `head:face` on the head canvas (`alpha = max(headAlpha, fullAlpha)`, RGB blended by `headAlpha / 255`) before `head:face` is placed in rig space (`underfill=<N>px(full:face)` on the `face` part log line and a `face: head_face_underfill composited …` note in `assemble --propose-plan`). The fill restores transparent pixels only and leaves the cut's own edge where it is — a hollow whose edge is not under hair stays visible; follow [Source before scissors](#source-before-scissors) for its source remedy. When `head:face` has fewer than `150` opaque pixels, `SEETHROUGH_HEAD_RUN_HOLLOW` still fires even under `head_face_underfill: true` because the head run omitted the face layer and cannot supply a jawline. The proposer instead selects `full:face` after this refusal when that full-run candidate passes the pixel floor and plausibility guards (issue #863); if it does not, the refusal remains. An assemble plan selecting `full:face` without `head:face` does not prepare the unused head face |
|
|
159
159
|
| `assemble.yield_refused` | assemble | **authored**, optional boolean, default `false` (issue #819; other types: `CONFIG_FIELD_TYPE`). With `true`, an original front projection-core pixel refused at max-channel drift > `DRIFT_LIMIT` (90) can yield when its original source pixel is still opaque. Non-core rim pixels (0 < alpha < `CORE_ALPHA`, 250) on the same 8-connected part body can then yield too (issue #832). Connectivity reads the part support before clearing: the opaque border outside the eroded projection core connects the body to its rim but is not itself newly cleared. A separate rim island without a yielded core seed is unchanged. At every cleared pixel, the actual top-most opaque holder below it (alpha >= 250), after earlier yields, must have colour within 90 of the painting. A deeper matching part cannot bypass an intervening opaque mismatch. This setting also applies to alternate sets. A yielded pixel's alpha becomes 0 and its RGB is not repainted. Accepted own-core pixels in the same connected support within the original yielded core's Gaussian footprint (radius 4 rig pixels), with a matching opaque behind holder, finish to painting RGB while retaining alpha and ownership (issue #841); original refusals, protected samples and replacements are excluded. Away from that footprint projection is unchanged. Merge/ring replacements, samples explicitly kept by `keep_under_occluders`, and pixels outside a derived piece cannot connect a rim or be newly yielded. Front is relative to the matching part behind, so original refusals in another run are eligible even when a different part is the global top holder. The reading proceeds back to front against the current behind pixels; alpha changes do not repaint or create a new matching colour. `yieldReadings` optionally records `finishedProjection` (accepted samples whose RGB changed), and records `yieldedRim` as the rim subset of total `yielded`, skipped core merge/ring/nonopaque/no-match counts and original/final global holders; a yielded pixel also taken by projection closing is omitted from the written `source_px_taken` count. Existing cuts/patches and thresholds are unchanged. The `drift:` line reports `yielded N px` per affected part, even below the drift-body size bar; with the setting off it names `assemble.yield_refused: true` as the conditional remedy. Rebuild and inspect the newly exposed layer in motion before considering any cut; a mismatching or absent behind holder cannot be repaired by this setting. |
|
|
160
|
-
| `assemble.keep_under_occluders` | assemble | **authored**, optional array of unique source part names from `assemble.plan`, default `[]` (issue #825). For each named part, keep its own See-through RGBA where the front-most covering part in the final draw stack has alpha greater than 0 and below `CORE_ALPHA` (250), including a cover in the other run. For an explicitly named part, original alpha 1–8 fringe removed by ghost cleanup also counts as source cover, within its final piece region: those pixels still contributed to the painting. Only original opaque own-layer samples are restored after source projection/closing and protected from later seam re-take; merged/rim, cap, under-fill and authored replacement pixels are excluded. This keeps their existing provenance and makes no holes; fully opaque cover and absent cover keep the old path. Cuts and limb pieces inherit the source part's protection. Unknown names: `CONFIG_NAME_RESOLVES`; duplicates: `CONFIG_PART_UNIQUE`; invalid shape: `CONFIG_FIELD_TYPE`. Inspect the part's own amodal layer first: this handle preserves its existing content and cannot complete a missing pixel. Example: `"keep_under_occluders": ["shin_r"]`. |
|
|
160
|
+
| `assemble.keep_under_occluders` | assemble | **authored**, optional array of unique source part names from `assemble.plan`, default `[]` (issue #825). For each named part, keep its own See-through RGBA where the front-most covering part in the final draw stack has alpha greater than 0 and below `CORE_ALPHA` (250), including a cover in the other run. For an explicitly named part, original alpha 1–8 fringe removed by ghost cleanup also counts as source cover, within its final piece region: those pixels still contributed to the painting. Only original opaque own-layer samples are restored after source projection/closing and protected from later seam re-take; merged/rim, cap, under-fill and authored replacement pixels are excluded. This keeps their existing provenance and makes no holes; fully opaque cover and absent cover keep the old path. Cuts and limb pieces inherit the source part's protection. Unknown names: `CONFIG_NAME_RESOLVES`; duplicates: `CONFIG_PART_UNIQUE`; invalid shape: `CONFIG_FIELD_TYPE`. Inspect the part's own amodal layer first: this handle preserves its existing content and cannot complete a missing pixel — in particular, it does not close a motion opening inside the figure (`figure_openings` / [Openings inside the figure on undeclared layer pairs](#openings-inside-the-figure-on-undeclared-layer-pairs-issue-933)) where the lower layer has no opaque core under an opaque upper layer (measured with every `assemble.plan` part named in `keep_under_occluders`: `0` of `1` pair-animation warnings across `demo`'s eight animations closed, with `hair_back\|handwear_r` on `hop` frame `5` staying at `40` cells over `28` allowed in `38x70` world units at `2.517` world units/cell, while `scarf` reads `CLEAR` (`0` warnings) both with and without `keep_under_occluders`; use `assemble.extend_painting` when the lower part lacks opaque source material under an opaque upper cover). Example: `"keep_under_occluders": ["shin_r"]`. |
|
|
161
161
|
| `limbs` | assemble (the cut and the caps), rig (through `regions`), check (`LIMB_ONE_BONE`) | **proposed** by `propose --keypoints` (issue #349), then checked: `{<limb>: {segments: [{part, from, bone}, {part, from, bone, joint, range?, cap?, edge?, cap_shape?, cut_band?, hybrid?}, …], split?, layout?, front?}}`, the upper segment first. The rule is *Limbs: cut at their joints* below the table. `part` is a new part (`CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` against the plan, patches, cuts and every other limb part); `from` a plan part or a cut's piece (`CONFIG_NAME_RESOLVES`) — the layer the segment's pixels come from, which is cut into the limb parts and is not itself made: a `meshes` or `regions` entry for it is `CONFIG_NAME_RESOLVES`, and the segments of one limb that take one part are consecutive (`CONFIG_LIMB_FROM_RUN`); `bone` a declared bone, each segment's the child of the one before (`CONFIG_LIMB_BONES_CHAIN`); the upper segment names no `joint`, `range`, `cap`, `edge`, `cap_shape` or `cut_band` (`CONFIG_KEY_KNOWN`), every other names its `joint` (`CONFIG_FIELD_PRESENT`) — the joint is its bone's origin; `range` is `[min, max]` degrees from rest with `min <= 0 <= max` (`CONFIG_LIMB_RANGE`), which `assemble` writes into the lower part's `parts.json` record and `check` poses the joint at — its two ends and its middle — reading the seam there (`SEAM_OVER_RANGE`, issue #360; rig-c needs no rotation limit); `cap` a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE`); `edge` `"feather"` (issue #360), the one value it takes, and `cap_shape` `"disc"` (issue #360) or `"ball"` (issue #579) (`CONFIG_FIELD_TYPE` on any other) — the cut's geometry at that joint, *Limbs: cut at their joints* below; absent, the cut is hard and the cap the layer's half-disc of half the limb's width; `cut_band` a whole number of px, 1 or more (`CONFIG_FIELD_TYPE`), on a segment that takes the segment above's part — between two layers nothing is cut, so a band there is `CONFIG_KEY_KNOWN` — places the cut within that many px of the joint line (issue #360, *Limbs: cut at their joints*); absent, the cut is the joint line. Each limb part is attached like any part — `regions.<part>`, which must be the segment's `bone` (`CONFIG_LIMB_BONE`), or a mesh, which `check` holds to that bone alone (`LIMB_ONE_BONE`). With `limbs`, `assemble` reads `bones` too (`CONFIG_FIELD_PRESENT` without it). Absent, nothing is cut at a joint and no byte of any output moves. Optional `front` (issue #664), `{parts: [<own segment part>, …], after: "<produced part>"}`, gives each named segment a second slot `<part>_front`, empty at setup, placed after `after`'s slot family, for a gesture set to draw on (`CONFIG_FRONT_SCOPE`, `CONFIG_FRONT_NAME`, `CONFIG_FRONT_LAYOUT`; *Front slots (issue #664)* below) |
|
|
162
162
|
| `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip, link_inherit?}` whose links are named `<chain>0 … <chain>n`. Parents come before children. In `rig.json` each link is **turned along its chain** — `rotation` is the direction from its origin to the next link's (the last link's to `tip`) and `length` that distance (issue #73), so a physics constraint added downstream finds a lever; under `--idle-keys ctl` the link's `<link>_ctl` carries the turn and the length and the link sits at local rotation 0 beneath it. A single bone with a `tip` is turned the same way, towards its tip, with that distance as its length (issue #239); a tip on the bone's own origin is refused (`RIG_TIP_DISTINCT`). The five face bones `eye_r`, `eye_l`, `brow_r`, `brow_l` and `mouth` (by name, when one has no tip of its own) are turned to the eye line — the line through the origins of `eye_r` and `eye_l` as `bones` writes them, read image-left to image-right, drawn when parts.json holds both an `eyewhite-r` and an `eyewhite-l` part — so the blink's `scaley` squashes across the eye line on a tilted head; move an eye bone and the line moves with it. With one eyewhite or none, or an eye bone missing, the rig stage's `face axis:` line says so and they keep the page's axes. A bone under a face bone keeps the face's axis. With the line, `rig.json` also declares `invariants.axes` — one entry, `"face"`, through `eye_r` and `eye_l`, holding the turned face bones (and their controls) — so rig-c's gate holds the build to the line; the line is the one rigc draws through the same two bones, so the tolerance is only the float floor — the largest gap the emitted (float32) bones have to it, rounded up to 6 places plus one 6-place unit — and the `face axis:` line prints it. With the line the rig stage also adds `head_up` (issue #250): a slotless bone under `head`, at head's origin, turned to the eye line + 90 degrees and as long as the eye midpoint lies from head's origin along that axis — the head's frame, which a rule that needs it reads; `head` keeps its own frame, so no key on it or under it changes meaning, and a config bone of that name is refused (`RIG_HEAD_UP_NAME_FREE`). A face bone with its own `tip` is held too (issue #258), in a second entry, `"face_tips"`, when its tip lies along the line within whole-pixel rounding — a tip written in whole pixels sits at most sqrt(2)/2 px off the point of the line it stands for, so its direction may sit up to asin((sqrt(2)/2) / length) degrees off — with its tolerance measured the same way; a tip further off is the bone's own axis, and the `face axis:` line names it with its gap and that bound. A failure is rig-c's `A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` line, which names the bone and the fix (rig-c's `docs/AUTHORING.md` §3.7 and §5.2); with no line no entry is written and A51 reports SKIP. Every region on a turned bone is turned back, so the setup pose does not move. Any other single bone keeps world rotation 0; one whose parent is a link is turned back upright. A single bone may declare `inherit` (issue #348): what it takes from its parent's transform, one of rig-c's own mode names exactly as rig-c's table spells them — `normal`, `onlyTranslation`, `noRotationOrReflection`, `noScale`, `noScaleOrReflection` (rig-c's `RigBone.inherit`) — written to that bone in `rig.json` as declared; any other spelling, `"NoScale"` included (the runtime would fold its first letter; nothing here folds or maps), is refused (`CONFIG_BONE_INHERIT`), and a chain takes no `inherit` (`CONFIG_KEY_KNOWN`). A chain may instead declare `link_inherit` (issue #369), an object mapping its own expanded link names to those same modes, for example `"link_inherit": {"leg_r1": "noScale"}`. Only the named link changes; omitted links inherit normally. A key outside this chain is `CONFIG_NAME_RESOLVES`, with this chain's valid link names in the detail; a non-object or non-string mode is `CONFIG_FIELD_TYPE`, and another mode spelling is `CONFIG_BONE_INHERIT`. The mode is forwarded to the expanded link, or to its control under `--idle-keys ctl`, by the same rule as a single bone. This is an authored opt-in: `propose` does not add it. Its setup rotation is the turn from where rig-c's own poser puts the bone with no rotation of its own under that mode, so it still stands upright (or along its tip) at the setup pose. Where the idle gives the bone a `<bone>_ctl`, the mode goes on the control: the control is the bone the parent's rotation reaches, and the bone under it inherits normally, so the idle's keys moved onto the control still turn it. A foot that does not take its shin's rotation (`noRotationOrReflection` or `onlyTranslation`) keeps its shoe level while the leg is posed (`FEET_STAY_LEVEL`, §7); which mode is the author's. `noScale` keeps a shin or shoe's shape under a scaled parent but still takes the parent's rotation: a shoe that must also stay level needs a separate rotation constraint, for example a transform constraint from an unrotated bone. This does not change the proposed feet's existing inheritance or `FEET_STAY_LEVEL` default. An automatic mesh weighted through a bone with a mode other than `normal` gets no derived amplitude (its row names the bone) and no residual envelope (rig-c's `SKINNING_RANGE_UNSUPPORTED`). Absent, `rig.json` carries no `inherit` and no byte moves |
|
|
163
163
|
| `meshes.<part>` | rig | **proposed**, then corrected. Generated modes below; `explicit` transfers an accepted mesh (see “An accepted mesh”). Exactly one mode is allowed. `grid` (the lattice: cell size, px — what `propose` writes: by default one spacing for the figure, round(H / 37) rig px with H the height of every part's box together, a part whose tenth of its short side is already coarser keeping that, and under `propose --grid-rule part` the reference's tenth of the part's short side clamped to 5..36, issue #265), `contour` (the outline mode, authored; the next row and "A contour mesh" below) or `auto` (the automatic mode, authored; "An automatic mesh" below) — two, or none, is `CONFIG_MESH_MODE`. In distance-generated modes: `r` (added to every distance before weighting: `w = 1/(d + r)²`), `segments` (a chain name, a bone name with a `tip`, or `[bone, [x0,y0], [x1,y1]]`), and, **authored**, optional, `exponent` (issue #161): a number above 0 that replaces the 2 in that rule, `w = 1/(d + r)^exponent` — a larger one hands each vertex more fully to its nearest bone; absent is 2 and changes no byte, a value that is not a number above 0 is `CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`, and a declared one is echoed as `exponent` in the mesh's `mesh_report.json` row; the exponent is the distance rule's, so declared beside `rule: "heat"` it is refused (`CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`). The segment list is the one authored decision about a layer: which bones may pull it. The slot's bone is the first segment's. Optional `rule` (issue #161): `"distance"` — the rule above, and what an absent `rule` means (every byte is the same) — or `"heat"`, bone heat over the part's own silhouette, in every mode: the lattice's art (alpha above 8, less any island a declared `stray` left out) with holes filled, or for `contour` and `auto` the silhouette their outline is traced from (that mode's art — alpha above 8 for `contour`, alpha 1 and above for `auto` — less any island a declared `stray` left out, holes filled, grown by its `margin`); each bound bone's sources are the silhouette pixels whose centre lies within 0.75 px of its segment (a pixel two bones reach goes to the nearer, a tie to the first listed), held at 1 for that bone and 0 for the others; the field is solved by row-major Gauss–Seidel over-relaxation to a sweep change under 1e-7 within 20000 sweeps; a vertex reads the nearest silhouette pixel and takes the same cap and floor (and `regions`) as the distance rule; `r` is not read. The mesh's `mesh_report.json` row gains `heat` (the constants, each bone's sources and sweeps, the residual). Any other value is `CONFIG_FIELD_TYPE`; a bound bone with no source pixel is `RIG_HEAT_BONE_SOURCE`, an island no bone has a source in `RIG_HEAT_ISLAND_SOURCE`, a solve past its sweep bound `RIG_HEAT_CONVERGED` — none is weighted 0. The rule is chosen per mesh, so `RIG_HEAT_BONE_SOURCE` names three remedies: move the segment onto the art, take the bone out of `segments`, or keep this part on the distance rule — the one to take when the bone anchors the part from outside its art (a torso bone under a neck), since the first moves a bone another part placed and the second drops the anchor (issue #303). On a lattice, `RIG_HEAT_ISLAND_SOURCE` on an island that is dust — at most 4 px and at most 0.1 % of the part's art px, `mesh-draft`'s line — names `meshes.<part>.stray` and the figure that leaves it out, with the value declared; an island above that line keeps the segment remedy, a real piece being a real authoring question. In every mode the mesh's `mesh_report.json` row's `bones` names the bones the emitted model binds for the attachment — the sorted set its weights in the written `rig.json` name, so a `turn` twin (`turn_<part>_<bone>`) or a stopped link's `<link>_d` where those rebind it, never the segment bone it stands for — the same list in every mode, `remesh --delta`'s included (issue #536) Optional `gather: false` keeps an exclusively bound non-segment limb rider at its authored draw position (issue #680); its binding and mesh geometry are unchanged. Remove the key for normal gathering; a non-rider is refused as `RIG_GATHER_RIDER`. |
|
|
@@ -3077,6 +3077,168 @@ max and absolute diff at native scale, then
|
|
|
3077
3077
|
inspect neck boundaries at native and 4x nearest-neighbour scale for tears or
|
|
3078
3078
|
ghosts. A green build alone does not certify the source art.
|
|
3079
3079
|
|
|
3080
|
+
### Recipe: a Form 1 idle (living illustration, issue #932)
|
|
3081
|
+
|
|
3082
|
+
A Form 1 living illustration (an anime character CG with no joint-cut `limbs` block)
|
|
3083
|
+
drives **the body with keyframes** and **every cloth and hair chain with `physics`
|
|
3084
|
+
constraints**. Plain `propose` (without `--cloth`) drafts keyframed harmonic `chain`
|
|
3085
|
+
tracks on every hair, fringe, sleeve, skirt and strap chain and writes no
|
|
3086
|
+
`constraints`, while `propose --cloth` replaces only the `cloth`-declared tags
|
|
3087
|
+
(`bottomwear`, `back hair`, `front hair`) and leaves `handwear` sleeves and
|
|
3088
|
+
`topwear` straps on keyframed chains. To author a Form 1 idle where cloth and hair
|
|
3089
|
+
all move by physics instead of keyframed sine chains:
|
|
3090
|
+
|
|
3091
|
+
1. **Key only the body carrier in `motion.tracks`.** Keep the torso breath on `chest`
|
|
3092
|
+
(`translatey` and `scalex`), the upper-body sway chain `chest` → `neck` → `head`
|
|
3093
|
+
on `rotate`, and `motion.blink`. Remove every `{ "chain": "…" }` track on hair
|
|
3094
|
+
(`hairback_*`, `bang_*`, `lock*`), sleeves (`sleeve_*`), skirts (`skirt_*`), and
|
|
3095
|
+
straps (`robe_*`). Whether `hip` is also keyed at the root of the sway chain
|
|
3096
|
+
(`hip` → `chest` → `neck` → `head`) depends on what sits below the chest:
|
|
3097
|
+
- Under keyframed `propose` (§3, *A standing cloth is the consumer's to drive*,
|
|
3098
|
+
issue #502), `skirt_*` chains carry keyframed `rotate` tracks at disparate
|
|
3099
|
+
phases (`0.08..0.38`), so keying `hip` in `rotate` puts `hip_ctl` above four
|
|
3100
|
+
keyframed children (`chest_ctl`, `skirt_l0`, `skirt_c0`, `skirt_r0`) whose
|
|
3101
|
+
phases disagree and fails `CHAIN_LAG`. Once `skirt_*` moves to `physics`
|
|
3102
|
+
constraints, `hip`'s only keyframed child is `chest`: a 4-link body chain
|
|
3103
|
+
`hip` → `chest` → `neck` → `head` (`phase: 0.20, 0.25, 0.30, 0.35`, with
|
|
3104
|
+
`amp(hip) <= amp(chest)`) passes `CHAIN_LAG` and drives the skirt from a moving
|
|
3105
|
+
parent (clearing `CLOTH_SETTLES` on the default world with `0` warnings).
|
|
3106
|
+
- Two geometric limits then decide whether `hip` is left still or keyed, and by
|
|
3107
|
+
how much:
|
|
3108
|
+
1. **Standing feet (`shoes` and `legwear`):** On a full-body standing rig where
|
|
3109
|
+
`shoes` sit on `root` (`BREATH_VISIBLE` enforces `feet_heat_max: 0`) and
|
|
3110
|
+
`legwear` rides `hip` as a rigid region (as on `examples/demo`), large `hip`
|
|
3111
|
+
sway slides the shins across the stationary shoes unless the legs use the
|
|
3112
|
+
planted-feet two-bone IK recipe (*Recipe: a two-bone IK chain with a planted
|
|
3113
|
+
target (issue #570)*) or the crop ends above the ankles. Without leg IK on
|
|
3114
|
+
`demo`, either leave `hip` unkeyed or keep `hip` `rotate` `amp <= 0.4°` so
|
|
3115
|
+
ankle shear stays sub-pixel.
|
|
3116
|
+
2. **Waist and upper-skirt binding (`CHECK_TIP_OVER_ROOT` on `bottomwear`):**
|
|
3117
|
+
`TIP_OVER_ROOT` splits `bottomwear`'s box (`y = 400..647` on `demo`, midpoint
|
|
3118
|
+
`y = 523.5`) into its upper half (`root_rig_px`) and lower half
|
|
3119
|
+
(`tip_rig_px`) and requires `tip / root >= 1.4725`. When
|
|
3120
|
+
`meshes.bottomwear.segments` binds the waist to `["hip", [x0, y0], [x1, y0]]`
|
|
3121
|
+
and the root link `skirt_*0` (`y = 432..499` on `demo`) also lies inside the
|
|
3122
|
+
upper half, moving `hip` while applying `"wind"` to `skirt_*0` inflates
|
|
3123
|
+
`root_rig_px` past `tip_rig_px / 1.4725`. To key `hip` when `bottomwear` is
|
|
3124
|
+
bound to it, set `"wind": 0` on the upper-half link `skirt_*0` and concentrate
|
|
3125
|
+
`"wind"` on the lower links (`skirt_*1..2`) so the hem travels at least
|
|
3126
|
+
`1.4725x` further than the waist.
|
|
3127
|
+
2. **Drive every cloth and hair link in `constraints`.** Give each link `<chain>0 ..
|
|
3128
|
+
<chain>(n-1)` a `"type": "physics"` entry with `"rotate": 1` and `"mix": 1` on the
|
|
3129
|
+
link bone itself (`sleeve_r0`, never `sleeve_r0_ctl`). A single ornament bone with
|
|
3130
|
+
no `length` (a bow, hairpin or earring) cannot take `"rotate"` (rig-c's `A23`) and
|
|
3131
|
+
takes `"x"` and `"y"` translation springs instead (e.g. `"x": 0.5, "y": 1,
|
|
3132
|
+
"inertia": 0.3, "strength": 160, "damping": 0.88, "mass": 1, "mix": 1`).
|
|
3133
|
+
|
|
3134
|
+
**How a physics rig passes `check`'s loop reading, `CHAIN_LAG`, and `STRESS_OFF_CHAIN`:**
|
|
3135
|
+
|
|
3136
|
+
- **`CHECK_LOOP_CLOSES` (`loop:` and `loop physics:`):** `check` strips every
|
|
3137
|
+
`physics` constraint (`withoutPhysics` in `src/check.ts`) when rendering the loop
|
|
3138
|
+
comparison frames (`f0000` vs the final frame at `motion.duration` at 12 fps), and
|
|
3139
|
+
lists the stripped constraints on `loop physics: <N> physics constraint(s) left out
|
|
3140
|
+
of the loop's frames, reported, not judged`. Because `physics` is left out of that
|
|
3141
|
+
comparison, the loop bar judges only the keyframed body: when `motion.duration` is
|
|
3142
|
+
a whole number of `1/12` s ticks (`CHECK_LOOP_LAST_FRAME_AT_DURATION`, within
|
|
3143
|
+
`1e-9` s) and every body track's `period` divides `motion.duration`
|
|
3144
|
+
(`CONFIG_PERIOD_DIVIDES_DURATION`), the loop reads `max |d| 0 (0 required)`
|
|
3145
|
+
regardless of how many physics springs are active. When baking a rendered loop file
|
|
3146
|
+
with physics **on** (`rig-parts build --loop` or `rig-parts loop`), frame 0 starts
|
|
3147
|
+
from `rigc render`'s physics reset while the end of cycle 1 carries the springs'
|
|
3148
|
+
motion; pass `--lead-in <n>` (for example `build --loop --lead-in 3`) so `<n>`
|
|
3149
|
+
warm-up cycles run before the final cycle is cut.
|
|
3150
|
+
- **`CHAIN_LAG`:** Reads only parent-child pairs of bones that both carry keyframed
|
|
3151
|
+
`rotate` tracks of the same period. Unkeyed physics chains are not read by
|
|
3152
|
+
`CHAIN_LAG`; only the keyframed body rotation chain (`chest_ctl > neck > head_ctl`,
|
|
3153
|
+
or `hip_ctl > chest_ctl > neck > head_ctl` when `hip` is keyed) is checked,
|
|
3154
|
+
requiring a positive lag step in `0.001 .. 0.25` of a cycle and non-decreasing
|
|
3155
|
+
amplitudes (`amp(hip) <= amp(chest) <= amp(neck) <= amp(head)`).
|
|
3156
|
+
- **`STRESS_OFF_CHAIN`:** For each `handwear` mesh, walks up the bone hierarchy from
|
|
3157
|
+
its bound sleeve links to find the nearest ancestor on a 2+-link keyframed rotation
|
|
3158
|
+
chain. Because the torso and head form a keyframed rotation chain above the sleeves,
|
|
3159
|
+
`STRESS_OFF_CHAIN` turns that chain's root (`chest_ctl` or `hip_ctl`) by `+90°`,
|
|
3160
|
+
`-90°`, and `180°`; the sleeve links hang wholly under `chest`, so each sleeve
|
|
3161
|
+
rotates rigidly (`ratio 1`, `reading 1 <= bar 1`, `PASS`) without needing keyframed
|
|
3162
|
+
tracks on `sleeve_*`.
|
|
3163
|
+
|
|
3164
|
+
**Starting floor (not the target) — getting a first green build from rig measurements:**
|
|
3165
|
+
|
|
3166
|
+
The formulas and values in the table below are **starting points that pass the bars
|
|
3167
|
+
(the floor), not the target**. A build at the floor clears every check with
|
|
3168
|
+
sub-degree sway (`0.7°..1.0°`) and `2.4` rig px of breath (`0.21 %` of figure height
|
|
3169
|
+
`H`), which is barely perceptible at full stage scale. Use this table only to reach a
|
|
3170
|
+
first green build; size the finished motion from the rig's own ceiling using the
|
|
3171
|
+
step-up method below.
|
|
3172
|
+
|
|
3173
|
+
| quantity | what to measure on the rig | floor starting value (passes the bars; step up from here) |
|
|
3174
|
+
| --- | --- | --- |
|
|
3175
|
+
| `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. |
|
|
3176
|
+
| 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. |
|
|
3177
|
+
| 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. |
|
|
3178
|
+
| 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`. |
|
|
3179
|
+
| 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`. |
|
|
3180
|
+
| 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. |
|
|
3181
|
+
|
|
3182
|
+
**Sizing the motion from the rig's own ceiling (step up until a limit refuses, then back off):**
|
|
3183
|
+
|
|
3184
|
+
You do not need an external reference rig to decide how large the breath, sway, and
|
|
3185
|
+
cloth motion should be. Once the floor build is green, **raise each motion family in
|
|
3186
|
+
steps (`1.3x..1.5x` per step) until the rig's own limits refuse a step, then back off
|
|
3187
|
+
one or two steps below that ceiling**:
|
|
3188
|
+
|
|
3189
|
+
1. **Gate ceiling (`check`'s upper-bound bars):** Step up the amplitude or physics
|
|
3190
|
+
drive until one of `check`'s upper limits fires — `CHECK_ANIMATIONS_DO_NOT_FOLD`
|
|
3191
|
+
(triangles flipping on `idle`, `wave`, or `hop`), `CHECK_TEXTURE_STRETCH` (an edge
|
|
3192
|
+
stretching or compressing past `1.926544`), `CHECK_TIP_OVER_ROOT` (`ratio < 1.4725`
|
|
3193
|
+
when vertical breath or `hip` sway carries the upper half of a garment faster than
|
|
3194
|
+
its tip swings), or `CHECK_STILL_REGIONS_DARK` (face drift relative to `head`).
|
|
3195
|
+
2. **Visual ceiling (`motion_heat.png` and `extremes/`):** Inspect `motion_heat.png`
|
|
3196
|
+
and the extreme-pose renders (`extremes/torso_y_min.png`, `torso_y_max.png`,
|
|
3197
|
+
`head_rot_min.png`, `head_rot_max.png`) at each passing step. Where a rigid region
|
|
3198
|
+
or mesh boundary shears across a neighbour (collar against neck, waist against
|
|
3199
|
+
torso, shin against shoe), the eye stops the sweep before the mesh bars trip.
|
|
3200
|
+
3. **Back off to the working size:** Pick the step at roughly `40 %..60 %` of the
|
|
3201
|
+
gate ceiling (one or two steps below the first refusal), where `extremes/` shows no
|
|
3202
|
+
seam shear and `TEXTURE_STRETCH` stays near `1.10..1.30`.
|
|
3203
|
+
|
|
3204
|
+
Stepping each motion family on `examples/demo` (`rig 832x1216`, figure height
|
|
3205
|
+
`H = 1138` rig px, `L_torso = 192` rig px, `R_shoulder = 95..96` rig px;
|
|
3206
|
+
`rig-c 3.1.0`, `@esotericsoftware/spine-core 4.3.13`) measures the floor, the
|
|
3207
|
+
backed-off working size, the gate ceiling (last passing step), and the first failing
|
|
3208
|
+
step as fractions of `H`:
|
|
3209
|
+
|
|
3210
|
+
| motion family on `examples/demo` (`H = 1138` rig px) | floor (first green build) | backed-off working size | gate ceiling (last green step) | first failing step and the limit / frame that stopped it |
|
|
3211
|
+
| --- | --- | --- | --- | --- |
|
|
3212
|
+
| **Torso breath** (`chest` `translatey` `amp = base`, p2p `2 * amp`, with `scalex` scaled in proportion; sway at `2.0° / 2.5° / 3.0°`) | `amp = 1.2` (`2.4` rig px p2p = **`0.21 %` of `H`**; `torso_heat_mean 18.045` vs `floor 3.809`) | `amp = 2.4..3.6` (`4.8..7.2` rig px p2p = **`0.42..0.63 %` of `H`**; `torso_heat_mean 40.933..50.409`) | `amp = 6.0` (`12.0` rig px p2p = **`1.05 %` of `H`**; `torso_heat_mean 64.77`, `handwear_r` `ratio 1.656`, `TEXTURE_STRETCH 1.192` on `topwear` tri 208, idle frame 10) | `amp = 8.0` (`16.0` rig px p2p = **`1.41 %` of `H`**) — stopped by **`CHECK_TIP_OVER_ROOT` on `handwear_r`** (`root 8.11`, `tip 11.854` rig px, `ratio 1.462 < 1.4725`, `carried_by sleeve_r0, breath_rig_px 8`): vertical breath carries the upper sleeve faster than cuff swing grows. |
|
|
3213
|
+
| **Shoulder & head sway** (`chest` / `neck` / `head` `rotate` at `phase 0.25 / 0.30 / 0.35`, `breath amp = 2.4`; shoulder travel = `handwear` `root_rig_px`) | `0.7° / 0.85° / 1.0°` — shoulder `1.856..2.079` rig px (**`0.16..0.18 %` of `H`**), cuff `4.21..4.994` rig px (**`0.37..0.44 %` of `H`**) | `2.5° / 3.0° / 3.6°` — shoulder `6.001..6.479` rig px (**`0.53..0.57 %` of `H`**), cuff `14.577..18.144` rig px (**`1.28..1.59 %` of `H`**), `TEXTURE_STRETCH 1.099` (`hair_back` tri 185, idle frame 16); collar seam visible in `extremes/` above `3.5°` (`0.72..0.77 %` of `H`) | `8.0° / 9.6° / 11.2°` — shoulder `18.648..19.031` rig px (**`1.64..1.67 %` of `H`**), cuff `46.699..58.15` rig px (**`4.10..5.11 %` of `H`**), `TEXTURE_STRETCH 1.643` (`hair_back` tri 225, idle frame 37, `min_ratio 0.609`) | `10.0° / 12.0° / 14.0°` — shoulder `23.322..23.734` rig px (**`2.05..2.09 %` of `H`**), cuff `58.462..72.018` rig px (**`5.14..6.33 %` of `H`**) — stopped by **`CHECK_TEXTURE_STRETCH` on `hair_back` triangle 225 (edge 29–28, idle frame 37)** (`ratio 0.439`, `severity 2.279 > 1.926544`); at `15.0°`, **`CHECK_ANIMATIONS_DO_NOT_FOLD`** also fails on `idle` mesh `hair_back` frame 36 (`1/226` tris). |
|
|
3214
|
+
| **Sleeve cuff tip** (`handwear` `tip_rig_px` via `sleeve_*_phys` `inertia` at `2.5° / 3.0° / 3.6°` sway, `strength: [45, 30, 20]`) | Stiff `strength: [200, 165, 130], inertia: 0.2` at `0.7°` sway fails `TIP_OVER_ROOT` (`tip 2.307..2.366` rig px = **`0.20..0.21 %` of `H`**, `ratio 1.19..1.216`) | `inertia = [1.0, 1.0, 1.0]` — cuff `14.577..18.144` rig px (**`1.28..1.59 %` of `H`**, `ratio 2.25..3.023`, `0` folds across all 8 clips) | `inertia = [1.0, 1.0, 1.0]` (cuff **`1.28..1.59 %` of `H`** at `2.5°` sway; reaches **`4.10..5.11 %` of `H`** at the `8.0°` sway ceiling with `0` folds) | `inertia = [1.2, 1.4, 1.6]` — stopped by **`CHECK_ANIMATIONS_DO_NOT_FOLD` on `wave` frame 14**, mesh `handwear_l` (`2/154` triangles on 5 frames; at `[1.5, 1.8, 2.0]`, cuff reaches `36.16..41.449` rig px = **`3.18..3.64 %` of `H`** and folds on `wave` frame 13 and `hop` frame 8). |
|
|
3215
|
+
| **Skirt hem tip (`hip` unkeyed)** (`bottomwear` `tip_rig_px` via `skirt_*_phys` `wind = [0.4, 0.6, 0.8] * w`, `strength: [200, 165, 130]`) | `w = 1` (`wind: [0.4, 0.6, 0.8]`) — hem `0.271` rig px (**`0.024 %` of `H`**, `root 0.148`, `ratio 1.83`) | `w = 15..22` (`wind: [6, 9, 12]..[8.8, 13.2, 17.6]`) — hem `3.942..5.64` rig px (**`0.35..0.50 %` of `H`**, `root 2.205..3.261`, `ratio 1.788..1.73`, `TEXTURE_STRETCH 1.126..1.195` on `bottomwear` tri 216, idle frame 6) | `w = 60` (`wind: [24, 36, 48]`) — hem `14.301` rig px (**`1.26 %` of `H`**, `root 9.364` = `0.82 %` of `H`, `ratio 1.527`, `TEXTURE_STRETCH 1.715` on `bottomwear` tri 216, idle frame 6) | `w = 80` (`wind: [32, 48, 64]`) — hem `18.476` rig px (**`1.62 %` of `H`**, `root 12.613`) — stopped by **`CHECK_TIP_OVER_ROOT` on `bottomwear`** (`ratio 1.465 < 1.4725`), **`CHECK_TEXTURE_STRETCH` on `bottomwear` tri 216 (edge 159–144, idle frame 6)** (`ratio 0.474`, `severity 2.108`), and **`CHECK_ANIMATIONS_DO_NOT_FOLD` on `hop` frame 8** (`1/292` tris). |
|
|
3216
|
+
| **Lower-body sway (`hip` keyed)** (`hip` `rotate` at `phase: 0.20` leading `chest 0.25 → neck 0.30 → head 0.35`, `skirt_*_phys` `wind` on links `1..2`) | `hip` `amp = 0.4°` with `wind: [0, 15, 40]` — waist `2.742` rig px (**`0.24 %` of `H`**), hem `4.239` rig px (**`0.37 %` of `H`**, `ratio 1.546`), `0` `CLOTH_SETTLES` warnings | `hip` `amp = 0.4°`, `wind: [0, 15, 40]` (waist **`0.24 %` of `H`**, hem **`0.37 %` of `H`**, `TEXTURE_STRETCH 1.259` on `bottomwear` tri 180, idle frame 5) | `hip` `amp = 0.4°`, `wind: [0, 15, 40]` | `hip` `amp = 0.6°` (`wind: [0, 18, 48]`) — stopped by **`CHECK_TIP_OVER_ROOT` on `bottomwear`** (`root 4.128` = `0.36 %` of `H`, `tip 5.263` = `0.46 %` of `H`, `ratio 1.275 < 1.4725`; also fails at `amp = 0.4°` if link `0` carries `wind: [4, 6, 8]`, `ratio 0.786`). |
|
|
3217
|
+
|
|
3218
|
+
**Reproducing the backed-off working build on `examples/demo`:**
|
|
3219
|
+
|
|
3220
|
+
Write `work/form1_config.json` from `examples/demo/config.json` by replacing
|
|
3221
|
+
`motion.tracks` with the six body tracks (`chest` `translatey` `amp: 2.4, phase: 0,
|
|
3222
|
+
base: 2.4`; `chest` `scalex` `amp: 0.008, phase: 0, base: 1.008`; `hip` `rotate`
|
|
3223
|
+
`amp: 0.4, phase: 0.20`; `chest` `rotate` `amp: 2.5, phase: 0.25`; `neck` `rotate`
|
|
3224
|
+
`amp: 3.0, phase: 0.30`; `head` `rotate` `amp: 3.6, phase: 0.35`, all at
|
|
3225
|
+
`period: 3.3333333333333335`), setting `sleeve_*_phys` to `inertia: 1.0, damping: 0.75,
|
|
3226
|
+
strength: [45, 30, 20]`, and setting `skirt_*_phys` to `inertia: 0.5, damping: 0.82,
|
|
3227
|
+
strength: [200, 165, 130], wind: [0, 15, 40]`, and pointing `motion.animations_from`
|
|
3228
|
+
at `../examples/demo/animations.json` and `generation.control.skeleton` at
|
|
3229
|
+
`../examples/demo/pose.json`, because both resolve against the config's own folder. Running `rig` and `check` produces:
|
|
3230
|
+
|
|
3231
|
+
```
|
|
3232
|
+
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)
|
|
3233
|
+
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
|
|
3234
|
+
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
|
|
3235
|
+
TIP_OVER_ROOT: PASS — instrument geometry; parts part handwear_l, root_rig_px 5.905, tip_rig_px 20.07, ratio 3.399 | part handwear_r, root_rig_px 6.404, tip_rig_px 15.951, ratio 2.491 | part bottomwear, root_rig_px 2.742, tip_rig_px 4.239, ratio 1.546; ratio_floor 1.4725
|
|
3236
|
+
TEXTURE_STRETCH: PASS — meshes 9; triangles 1816; frames 41; severity 1.259; severity_ceiling 1.926544
|
|
3237
|
+
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)
|
|
3238
|
+
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
|
|
3239
|
+
check: PASS; 11 of 11 bar(s) measured, 0 skipped; 0 warning(s) (CLOTH_SETTLES, not a bar)
|
|
3240
|
+
```
|
|
3241
|
+
|
|
3080
3242
|
## 4. The command order
|
|
3081
3243
|
|
|
3082
3244
|
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.
|
|
@@ -3726,7 +3888,7 @@ and a usage error with `--head-box`, `--cloth` or `--from-config`.
|
|
|
3726
3888
|
| `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 |
|
|
3727
3889
|
| `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 |
|
|
3728
3890
|
| `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` |
|
|
3729
|
-
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:` (and `loop physics:` when the rig declares a physics constraint, §7), `seam:`, the six judgement lines, then — when `motion.json` holds an animation, the idle included — `ANIMATIONS_DO_NOT_FOLD` and one `folds` line per folding mesh, rigc's own (issues #320, #333), then — where a `footwear` part rides a bone that hangs from another — `FEET_STAY_LEVEL` and one `foot` line per such part (issue #348), then — when `parts.json` names a `handwear` part and there is an idle — `STRESS_OFF_CHAIN` (issue #347), then — where `parts.json` holds a limb segment's part — `LIMB_ONE_BONE` and one `limb` line per such part (issue #349), then — where a limb record carries a `range` — `SEAM_OVER_RANGE` and one `joint` line per joint and angle (issue #360), then `SEAM_OVER_MOTION` — a SKIP naming why on a rig that declares no neighbour pair, else the pairs read in every animation; existing internal-joint and explicit-seam pairs retain their `WARN seam` policy (issue #493), while proven body-root tears print `FAIL seam` and fail a counted bar (issue #686), then `CLOTH_SETTLES` — a SKIP naming why on a rig with no physics constraint, else the chains read and one `WARN chain` line per chain whose tip comes to rest before the idle's loop length (issue #501, a warning and never a bar), `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the bars measured — nine, ten with the fold bar, one more each with the feet-level bar, the stress bar, the limb bar, the range bar and, where proven body-root pairs exist, the body-root seam bar — and names the ones that said SKIP, then — where `SEAM_OVER_MOTION` read a pair — how many warnings it printed, `; N warning(s) (SEAM_OVER_MOTION, not a bar)` — with `CLOTH_SETTLES` named beside it, and N their sum, where the rig declares a physics constraint — which no bar counts | `check: PASS; 11 of 11 bar(s) measured, 0 skipped` on `demo` and `scarf`, whose sleeves each ride one chain; `check: PASS; 10 of 11 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN)` on `reference-demo` and `reference-sample`, whose one `sleeves` mesh rides both sleeve chains (issue #347) — no example writes `FEET_STAY_LEVEL`, every shoe riding `root`; a judgement line SKIP only where the character lacks what it reads; on a rig spec with no `parts.json` or no `idle` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), PASS with the skipped bars named | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6); `CHECK_ANIMATIONS_DO_NOT_FOLD` — a clip turns a mesh over (§6); `CHECK_STRESS_OFF_CHAIN` — a sleeve weighted off its chain (§6); `CHECK_LIMB_ONE_BONE` — a limb part on a second bone (§6); `CHECK_SEAM_OVER_RANGE` — a joint that opens when bent (§6); `CHECK_SEAM_OVER_MOTION` — a proven body-root seam opens over the existing floor (§6) |
|
|
3891
|
+
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:` (and `loop physics:` when the rig declares a physics constraint, §7), `seam:`, the six judgement lines, then — when `motion.json` holds an animation, the idle included — `ANIMATIONS_DO_NOT_FOLD` and one `folds` line per folding mesh, rigc's own (issues #320, #333), then — where a `footwear` part rides a bone that hangs from another — `FEET_STAY_LEVEL` and one `foot` line per such part (issue #348), then — when `parts.json` names a `handwear` part and there is an idle — `STRESS_OFF_CHAIN` (issue #347), then — where `parts.json` holds a limb segment's part — `LIMB_ONE_BONE` and one `limb` line per such part (issue #349), then — where a limb record carries a `range` — `SEAM_OVER_RANGE` and one `joint` line per joint and angle (issue #360), then `SEAM_OVER_MOTION` — a SKIP naming why on a rig that declares no neighbour pair, else the pairs read in every animation; existing internal-joint and explicit-seam pairs retain their `WARN seam` policy (issue #493), while proven body-root tears print `FAIL seam` and fail a counted bar (issue #686), then `CLOTH_SETTLES` — a SKIP naming why on a rig with no physics constraint, else the chains read and one `WARN chain` line per chain whose tip comes to rest before the idle's loop length (issue #501, a warning and never a bar), then `figure_openings:` — `CLEAR` or `WARN` with the undeclared setup-overlapping pairs, animations and cell size read and one `WARN opening` line per pair and animation whose transparent interior opening exceeds the one-cell floor, or a named `SKIP` when there is no `parts.json`, no animation or no undeclared setup-overlapping pair (issue #933, a warning and never a bar), `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the bars measured — nine, ten with the fold bar, one more each with the feet-level bar, the stress bar, the limb bar, the range bar and, where proven body-root pairs exist, the body-root seam bar — and names the ones that said SKIP, then — where `SEAM_OVER_MOTION` read a pair — how many warnings it printed, `; N warning(s) (SEAM_OVER_MOTION, not a bar)` — with `CLOTH_SETTLES` named beside it, and N their sum, where the rig declares a physics constraint — which no bar counts | `check: PASS; 11 of 11 bar(s) measured, 0 skipped` on `demo` and `scarf`, whose sleeves each ride one chain; `check: PASS; 10 of 11 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN)` on `reference-demo` and `reference-sample`, whose one `sleeves` mesh rides both sleeve chains (issue #347) — no example writes `FEET_STAY_LEVEL`, every shoe riding `root`; a judgement line SKIP only where the character lacks what it reads; on a rig spec with no `parts.json` or no `idle` (a merged rig, §7 *Measuring a rig rig-parts did not assemble*), PASS with the skipped bars named | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6); `CHECK_ANIMATIONS_DO_NOT_FOLD` — a clip turns a mesh over (§6); `CHECK_STRESS_OFF_CHAIN` — a sleeve weighted off its chain (§6); `CHECK_LIMB_ONE_BONE` — a limb part on a second bone (§6); `CHECK_SEAM_OVER_RANGE` — a joint that opens when bent (§6); `CHECK_SEAM_OVER_MOTION` — a proven body-root seam opens over the existing floor (§6) |
|
|
3730
3892
|
| `check` / `build`: extremes observations | `extremes/<anim>/extremes.png` and `extremes.json` beside the other check outputs, generated by core `render --extremes-only` at 12 fps on its fitted world box at scale 1; one `EXTREMES` geometry-pair frame/distance summary per animation | open the native setup/min/max/diffs and label every changed boundary tear, ghost or clean before human review (step 9) | core's named static/observation SKIPs retain JSON without a PNG; diagnostic failure is `CHECK_EXTREMES_RENDER_FAILED` SKIP and leaves the measured verdict unchanged; `--no-extremes` runs no extra render and removes stale `extremes/` |
|
|
3731
3893
|
| `check --requirements` (and `build --requirements`) | after the lines above, one line per declared requirement, `NAME: PASS`, `FAIL` or `NOT MEASURABLE — …` with its figures, the bar as declared and the worst frame; then `requirements: N declared — M measured (P PASS, F FAIL), K NOT MEASURABLE; not declared: <kinds>`; `check.json`'s `requirements` block, first its `file` record (issue #556); the renders under `requirements/` (§7, *Declared requirements*) — `build` reads `config.requirements` in place of the flag when it is not given | every line PASS and `0 NOT MEASURABLE`; a follow's `released_copy` says whether rigc's consumer-driven door was taken on the throwaway copy; a `stretch` line's `held_by` (issue #321), right after `worst`, names the bones the worst edge's two vertices hang on and their idle chains, in `CHECK_TEXTURE_STRETCH`'s words (§6) and by the same function, on a PASS as on a FAIL | `CHECK_REQUIREMENT_MET` (a figure past its bar; the line names the frame) or `CHECK_REQUIREMENT_MEASURABLE` (the quantity is undefined in the frames: a tip or an axis on a bone of length 0, an aim with no line, a follow no frame of which reaches its least drive); the last line then reads `check: FAIL — B bar(s) not met, R declared requirement(s) not PASS` |
|
|
3732
3894
|
| `compose` | the `[compose]` lines: one per character (its offset, its scale, its placed bounds, the shift and how many first-level bones, regions on `root` and weights bound to `root` it moved, its bone, slot, track and constraint counts, its `rig_scale` and idle), the declared order and the slot count it expands to, rigc's gate lines; then the `[check]` lines as for `check`, and `scene.json` | both gate summaries `0 failed`, `check: PASS` with the bars that measured and the ones that said SKIP named — on a composed rig `4 of 9`: the gate, the loop, `CHAIN_LAG` and `TEXTURE_STRETCH` (there is no `parts.json`; each character's own `build` measured all nine). `scene.json`'s `order.slots` is the order the compiled skeleton draws | a `SCENE_*` refusal (§6, *compose*) — the scene file, a build, or the order; `COMPOSE_RIGC_GREEN` — rigc's own line, quoted |
|
|
@@ -5052,6 +5214,7 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
5052
5214
|
| <a id="CHECK_MOTION_SEAM_ROOT"></a>`CHECK_MOTION_SEAM_ROOT` | a body-root declaration has missing or ambiguous segment0, bone, parent, slot or default-skin attachment; the refusal names the part and candidates | supply the declared one-to-one hierarchy/slot/provenance binding; no nearest or mesh-influence fallback |
|
|
5053
5215
|
| <a id="CHECK_SEAM_OVER_MOTION"></a>`CHECK_SEAM_OVER_MOTION` | a proven body-root pair has an active animation gapOpen plus UNDER-owned ghost above the unchanged one-cell floor; the refusal names pair, animation, frame, cells, cell size and world box | [Source before scissors](#source-before-scissors) first; repair the declared rest overlap or authored motion; the existing internal-joint warning policy and physics reference worlds are unchanged |
|
|
5054
5216
|
| <a id="CLOTH_SETTLES"></a>`CLOTH_SETTLES` | **a warning, never a refusal** (issue #501; the owner's ruling: cloth at rest while the character stands may be intended). Every physics chain, read by its tip on the idle `check` renders — `rigc render --animation idle --fps 12 --geometry`, physics on, rigc's default environment. A chain is a **physics leaf** — a bone a physics constraint names (`rig.json`'s `constraints` or `motion.json`'s `physics`) with no such bone under it — and the run of physics bones above it; its tip is the leaf's end (origin plus `length` along its world x axis), or its origin when the leaf has no length; its speed between frames is the tip's distance over their time, from the idle's `geometry.json`. A chain **settles** at the first frame from which every speed to the idle's last frame is under the floor, and each one that does — before the loop's length is out — prints `WARN chain "<root>" .. "<tip>" (N physics link(s)) settles at <t> s of the <d> s idle — its tip's end under 0.01694 rig px/s from there to the last frame (fastest on the idle <v> rig px/s)`. **The floor**, 0.01694 rig px/s, is derived, not chosen: half the slowest tip an idle key drives on the public examples (the repository's floor rule, half the weaker figure) — measured with this port's build of each (rig-c 2.60.0), over the 31 chains a keyed bone at or above the leaf drives (`demo` 12, `reference-sample` 1, `scarf` 11, `wind` 7) the slowest frame-to-frame tip speed is `scarf`'s `skirt_l2`, 0.03388 rig px/s; the ten no key drives — `wind`'s skirt, eight trees and the swing, hung from a `hip` its idle never keys, and `reference-sample`'s `hip_jiggle` — fall under it from 1.5 to 2.75 s on a 3.333 s idle (`hip_jiggle` at 0 s: it never moves) and end at 0.0025 rig px/s or less. `check.json`'s `CLOTH_SETTLES` holds `status` (`WARN`, `CLEAR`), `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings` and one row per chain (`tip`, `root`, `links`, `point` — `end` or `origin` — `settles_at`, null when it does not, `max_rig_px_per_s`, `min_rig_px_per_s`); it moves no exit status, no `check: PASS` and no bar count — the summary's warning clause names it, `; N warning(s) (CLOTH_SETTLES, not a bar)` (with `SEAM_OVER_MOTION` before it, and N their sum, where that read a pair). It reads the idle alone, one loop from rigc's physics reset at frame 0: a chain that settles later than the idle is long is not seen. **SKIP**, named: no physics constraint (then `check.json` has no key), or no idle — nothing measured, never a pass | if the chain should move while the character stands, one of the consumer's two levers: an environment at render (`rigc render --environment`, wind and gravity in time — rig-c's AUTHORING §8.4), or a consumer's animation that moves the bone the chain hangs from. The drafted idle keys no hip, by measurement: a hip turn at the idle's period fails a bar on all five public examples (issue #502, §3 *What moves them*). A `cloth` recipe's skirt hangs from `hip`, which the drafted idle never keys (§3, *Recipe: cloth and hair that answer the world*, *What moves them*, which has a worked schedule for `wind`). If it is intended, nothing |
|
|
5217
|
+
| <a id="figure_openings"></a>`figure_openings` | **a warning, never a refusal** (issue #933; see [Openings inside the figure on undeclared layer pairs](#openings-inside-the-figure-on-undeclared-layer-pairs-issue-933)). Reads transparent openings inside the pair's own posed hole-filled union (`alpha == 0` inside `fillHoles(a ∪ b)`, excluding cells already transparent at setup) across every undeclared setup-overlapping (`alpha >= 128`) layer pair in `rig.json` draw order in every animation `motion.json` holds at 12 fps (`scale = 1 / cell`). Prints `figure_openings: CLEAR — N undeclared setup-overlapping pair(s) in M animation(s) (<names>) at 12 fps (cell <cell> world units): 0 warning(s), never a bar` when clean, `figure_openings: WARN — …` followed by one `WARN opening "<a>\|<b>" above "<above>" (under "<under>") on K frame(s) [<frames>], worst frame <f> (t = <t>s) in "<animation>": found <found_cells> cell(s), <= <allowed_cells> cell(s) allowed in <w>x<h> world units at <cell> world units/cell — transparent opening inside the figure over motion on an undeclared layer pair (not a bar)` line per opening above the one-cell floor (`found_cells > allowed_cells`), or `figure_openings: SKIP — <reason>` when there is no `parts.json`, no animation, or no undeclared setup-overlapping pair. `check.json`'s `figure_openings` is written whenever the reader ran (`status` `"CLEAR"` or `"WARN"`), holding `status`, `fps`, `cell`, `pairs`, `animations`, `warnings` and `openings` (`pair`, `under`, `above`, `animation`, `frame`, `time`, `opens`, `cell`, `found_cells`, `allowed_cells`, `box`); it moves no exit status, no `check: PASS` and no bar count | if the opening is an unintended tear where a lower layer lacks opaque art under an opaque upper layer, use `assemble.extend_painting` (not `assemble.keep_under_occluders`, which only preserves existing `alpha >= 250` lower pixels under a translucent cover), or adjust mesh weights / motion keys; if it is a deliberate silhouette loop shifting in world space, nothing |
|
|
5055
5218
|
| <a id="CHECK_FOLDS_READ"></a>`CHECK_FOLDS_READ` | the fold render's `frames.json` names no set for an animation, a `folds.json` is missing, not `rigc-folds/1`, another animation's or another rate's, or a folding mesh has no line rigc printed for it | a rigc problem; report it, with `rigc --version` |
|
|
5056
5219
|
| <a id="CHECK_SEAM_FRAME_SIZE"></a><a id="FRAMES_SIDECAR"></a><a id="CHECK_GEOMETRY_FILE"></a>`CHECK_SEAM_FRAME_SIZE`, `FRAMES_SIDECAR`, `CHECK_GEOMETRY_FILE` | rigc's render is not what its `frames.json` says, or its `geometry.json` is not a whole `rigc-geometry/1` export of the frames beside it (a frame count, a mesh whose vertex count differs from its rest entry; for the face half, another viewport than `frames.json`'s, other frame indices, the head bone or a feature's bone missing from a frame, a non-finite transform or time) | a rigc problem; report it |
|
|
5057
5220
|
| <a id="CHECK_SEAM_DOMAIN"></a>`CHECK_SEAM_DOMAIN` | the source-relative common union U cannot prove finite, bounded, exactly representable scale1 coverage of every actual drawn setup state, or official pinned.outside is not empty; U is the enlarged render and A remains the fixed authored denominator | retain the named evidence and report the unavailable or outside-art boundary; do not shrink art, pad the measured domain or infer transparency from geometry alone |
|
|
@@ -5167,6 +5330,20 @@ Renderer metadata and exact raster reads use the promised `@rig-c/core/render-sh
|
|
|
5167
5330
|
|
|
5168
5331
|
The body-root bar counts stock partitions `gapOpen` plus `ghostA` or `ghostB` owned by the pair's UNDER-owned drawing in resolved draw order on each active frame, comparing their summed cells against the unchanged strict floor `px > round(max(box width, box height) / cell)` on the original raw kind boxes (their union when both kinds contribute), never the narrowed partition boxes; `gapCovered` and OVER-owned ghost cells do not count (issue #711). The metric mode is `gap-open+under-ghost`; rows retain the exact partition counts, UNDER owner and counted total in `cells`, including excluded and under-floor readings. Missing partitions, inconsistent sums or unavailable ownership are named `FRAMES_SIDECAR` refusals, never inferred zero. An active rendered environment fails `CHECK_SEAM_OVER_MOTION` above that floor, setting overall PASS false and exits 1. Existing internal-joint and explicit-only pairs remain warnings: their PASS, exit status and bar count are unchanged; reference worlds remain informational. The root bar is counted once, and absence is not a zero or a PASS. No whole-box alpha exemption or consumer compositor is used. This gate samples the supplied animations at 12 fps, not another motion or player scene.
|
|
5169
5332
|
|
|
5333
|
+
**Openings inside the figure on undeclared layer pairs** (issue #933). Declared `SEAM_OVER_MOTION` reads only limb segments at a cut joint, proven body-root pairs, and explicit `--requirements` or `motion_seams` declarations — `0` pairs on `demo`, `reference-demo`, `reference-sample` and `scarf`, and `10` of `63` setup-overlapping pairs (`alpha >= 128`) on `wind`. An upper layer sliding off a lower layer where the lower layer lacks opaque art under it opens a sliver of transparent canvas (`alpha == 0`) inside the figure while declared `SEAM_OVER_MOTION` stays `SKIP`. To read openings inside the figure across every undeclared setup-overlapping (`alpha >= 128`) layer pair in `rig.json` draw order (`under` drawn before `above`, so `"<above>"` is the part above the opening), `readFigurePairOpenings` evaluates on each animation frame at 12 fps (`scale = 1 / cell`, so one raster cell is `cell` world units on a side — about `2.2` to `2.5` world units on the public examples) the cells inside both drawings' posed overlap bounding box and inside the pair's own posed hole-filled union (`fillHoles(a ∪ b)`, so a pocket closed only by a third part does not count against `a|b`) that are transparent across the whole skeleton (`alpha == 0`), covered at setup (`t = 0`) by both `under` and `above` (`setup.a0[p] >= 128 && setup.b0[p] >= 128`), and not already transparent at the setup pose (`setup.any0[p] === 0`, so a deliberate gap between arm and body present at rest is art, not a motion opening). A frame opening exceeds the one-cell floor when `found_cells > allowed_cells`, where `found_cells` is the open cell count and `allowed_cells = round(max(box width, box height) / cell)` on the opening's world box (`[x0, y0, x1, y1]`). Each run prints a status line (`figure_openings: CLEAR — …`, `figure_openings: WARN — …` followed by one `WARN opening` line per pair and animation above the floor, or `figure_openings: SKIP — <reason>` when there is no `parts.json`, no animation, or no undeclared setup-overlapping pair) and writes `figure_openings` in `check.json` whenever the reader ran (`status !== "SKIP"`). Each warning names the pair `"<a>|<b>"`, the part above (`above "<above>"`) and under (`under "<under>"`), the worst frame (`frame <index>`), `found <found_cells> cell(s)`, `<= <allowed_cells> cell(s) allowed` in `<w>x<h> world units at <cell> world units/cell`. Measured wall-time cost inside `check`: `0.4 s` of `186.2 s` (`0.2 %`) on `wind` (`53` undeclared pairs, `1` animation) and `0.7 s` of `41.2 s` (`1.7 %`) on `scarf` (`67` undeclared pairs, `8` animations).
|
|
5334
|
+
|
|
5335
|
+
| example | slots | total slot pairs | setup-overlapping pairs (`alpha >= 128`) | declared `SEAM_OVER_MOTION` pairs | undeclared pairs read | cell (`idle`, world units) | setup interior `alpha == 0` holes (cells / components) | `figure_openings` (`idle` warnings / all-animation warnings) | worst opening (`pair`, `above`, `animation`, `frame`, `found_cells` vs `allowed_cells` in `box` at `cell`) |
|
|
5336
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
5337
|
+
| `demo` | 23 | 253 | 67 | 0 (`SKIP`) | 67 | 2.357 | 4,654 cells / 27 | `CLEAR` on `idle` (`0`) / `WARN` across all (`1` in `8` animations) | `hair_back\|handwear_r` above `"handwear_r"` (under `"hair_back"`), `hop` frame 5 (`t = 0.417s`, frames `[5-6]`), found `40 cell(s)` (`<= 28 cell(s)` allowed in `38x70` world units at `2.517` world units/cell) |
|
|
5338
|
+
| `reference-demo` | 22 | 231 | 60 | 0 (`SKIP`) | 60 | 2.358 | 2,112 cells / 25 | `CLEAR` (`0` / `0` in `1` animation: `idle`) | none (`0` warnings) |
|
|
5339
|
+
| `reference-sample` | 20 | 190 | 51 | 0 (`SKIP`) | 51 | 2.197 | 608 cells / 12 | `CLEAR` (`0` / `0` in `2` animations: `idle, hop`) | none (`0` warnings) |
|
|
5340
|
+
| `scarf` | 24 | 276 | 67 | 0 (`SKIP`) | 67 | 2.213 | 1,639 cells / 4 | `CLEAR` (`0` / `0` in `8` animations: `idle, tilt, look_l, look_r, smile, startled, bow, hop`) | none (`0` warnings) |
|
|
5341
|
+
| `wind` | 28 | 378 | 63 | 10 (6 limb + 4 root) | 53 | 2.328 | 7,970 cells / 22 | `CLEAR` (`0` / `0` in `1` animation: `idle`) | none (`0` warnings) |
|
|
5342
|
+
|
|
5343
|
+
**Why `fillHoles(a ∪ b)` and why warning (`WARN`), not a bar:** counting only cells inside the holes of the pair's own posed union (`fillHoles(a ∪ b)`) rather than of the whole figure (`fillHoles(frameUnion)`) eliminates all `36` third-part pocket warnings on `demo` (where a cheek-to-ponytail or arm-to-waist pocket is closed only by a third part such as the collar, shoulder or torso), so all five public examples read `CLEAR` (`0` warnings) on `idle`, and `reference-demo`, `reference-sample`, `scarf` and `wind` read `CLEAR` (`0` warnings) across every animation. The single remaining warning across all five public examples (`demo` `hair_back|handwear_r` on `hop` frames `[5-6]`, worst frame `5` at `t = 0.417s`: `found 40 cell(s), <= 28 cell(s) allowed in 38x70 world units at 2.517 world units/cell`) was visually inspected and is a false positive from a silhouette loop between `hair_back` and `handwear_r` themselves: on `demo`, `hair_back`'s right twin-tail curls inward behind the right arm and touches `handwear_r` both above (at the puff sleeve) and below (at the curled hair tip near the wrist), so `hair_back ∪ handwear_r` alone encloses a small background window between the forearm and the curling hair lock; when the character jumps in `hop`, that window shifts into the world cells that `hair_back` and `handwear_r` covered at `t = 0`. Because two layers with curved silhouettes can enclose a deliberate background loop between themselves during a jump, `figure_openings` is a **warning (`WARN`, never a bar)**.
|
|
5344
|
+
|
|
5345
|
+
**How `assemble.keep_under_occluders` relates:** `assemble.keep_under_occluders` only preserves a named lower part's original See-through RGB where the lower layer **already has** `alpha >= 250` (`CORE_ALPHA`) beneath a translucent front cover (`0 < alpha < 250`, or ghost-cleaned `alpha 1..8`). Where an upper layer was opaque at setup (`alpha >= 250`) or the lower layer had no opaque core under the upper layer (or where a silhouette loop shifts in world space), `keep_under_occluders` changes nothing: measured with every `assemble.plan` part named in `keep_under_occluders`, **0 of 1** pair-animation warnings on `demo` closed (`hair_back|handwear_r` on `hop` frame `5` stayed at `40` cells over `28` allowed in `38x70` world units at `2.517` world units/cell), and `scarf` read `CLEAR` (`0` warnings) both with and without `keep_under_occluders`. To close a genuine tear where the lower layer lacks opaque art under an opaque upper layer, use `assemble.extend_painting` (or adjust mesh weights / motion keys so the upper part does not slide off the lower part's boundary). **Diagnostic preview background:** `rigc render` composites diagnostic frame PNGs over `BACKGROUND = [232, 232, 232, 255]` (`#e8e8e8` light grey), which visually masks an `alpha == 0` sliver next to light cloth, pale hair or skin; reading `alpha == 0` on the posed skeleton's transparent composite catches the opening regardless of preview background colour.
|
|
5346
|
+
|
|
5170
5347
|
### compose
|
|
5171
5348
|
|
|
5172
5349
|
Every refusal below is collected with the others and printed at once, under
|
|
@@ -5795,6 +5972,7 @@ it reads is there, and says SKIP with the reason when it is not:
|
|
|
5795
5972
|
| `STRESS_OFF_CHAIN` | `parts.json`'s tags, the idle's chains, `rig.json`'s weights, and a turned copy rigc poses | written only when `parts.json` names a `handwear` part and there is an idle (absent on a rig with no `parts.json`); SKIP when no such part rides exactly one chain, each part's reason |
|
|
5796
5973
|
| `SEAM_OVER_RANGE` | `parts.json`'s limb records with a `range`, and a bent copy rigc renders | written only when a limb record carries a `range` (absent on a rig with no `parts.json`); never SKIP — a joint it cannot read is a FAIL naming why |
|
|
5797
5974
|
| `SEAM_OVER_MOTION` | `parts.json` limb records, proven body-root bindings in `rig.json`, `--requirements` seams, and every animation through `rigc render --seam` | body-root gapOpen plus UNDER-owned ghost gates using resolved per-frame draw order; existing pairs stay warnings. SKIP names absent parts, pairs, animations or bands; unavailable is never measured zero |
|
|
5975
|
+
| `figure_openings` | `parts.json`, `rig.json` draw order, and every animation at 12 fps (`readFigurePairOpenings`) | no `parts.json`, no animation, or no undeclared setup-overlapping (`alpha >= 128`) pair (prints `figure_openings: SKIP — <reason>` and omits `figure_openings` from `check.json`; written with `status` `"CLEAR"` or `"WARN"` whenever it ran) |
|
|
5798
5976
|
| `RECOMPOSITE_HOLES` | `parts.json`'s `recomposite` block | no `parts.json`, or no block |
|
|
5799
5977
|
| `SETUP_POSE_VS_SOURCE` | the rig spec and the painting | no `--source` |
|
|
5800
5978
|
| `WIND_BAND` | the rig spec and the `--wind-band` file | no `--wind-band` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/build.ts
CHANGED
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
splitSchedule,
|
|
83
83
|
untestedIntervals,
|
|
84
84
|
} from './autoreplay.ts';
|
|
85
|
-
import { besideIdleLine, checkSummaryLine, inputFileRecord, type InputFileRecord, CLOTH_LINE, type ClothLine, clothWarnText, frameRuns, MOTION_SEAM_LINE, type MotionSeamLine, type MotionSeamPlant, motionSeamWarnText, motionJointContactLines, FOLD_LINE, type FoldsLine, LEVEL_LINE, type LevelLine, LIMB_LINE, type LimbLine, STRESS_LINE, SWEEP_LINE, type SweepLine, buildGateLines, causeLines, environmentLine, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, GATE_PROFILE, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, readCheckEnvironment, readWindBand, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR, WIND_BAND_LINE, windBandLines } from './check.ts';
|
|
85
|
+
import { besideIdleLine, checkSummaryLine, inputFileRecord, type InputFileRecord, CLOTH_LINE, type ClothLine, clothWarnText, figureOpeningLines, frameRuns, MOTION_SEAM_LINE, type MotionSeamLine, type MotionSeamPlant, motionSeamWarnText, motionJointContactLines, FOLD_LINE, type FoldsLine, LEVEL_LINE, type LevelLine, LIMB_LINE, type LimbLine, STRESS_LINE, SWEEP_LINE, type SweepLine, buildGateLines, causeLines, environmentLine, IDLE_FPS, IDLE_MAX_PX, maxRgbDiff, REQUIREMENTS_DIR, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, GATE_PROFILE, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, readCheckEnvironment, readWindBand, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR, WIND_BAND_LINE, windBandLines } from './check.ts';
|
|
86
86
|
import { type Cut, type Extend, type Limb, loadConfigAndAnimations, loadConfigWithBytes, loadEarlyConfig, type Patch, type PlanEntry } from './config.ts';
|
|
87
87
|
import { coreCompileGuideLine, failLine, PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
88
88
|
import type { MeshQualityReport, MotionSchedule } from '@rig-c/core/mesh';
|
|
@@ -1943,6 +1943,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1943
1943
|
const sweep = fig[SWEEP_LINE];
|
|
1944
1944
|
if (sweep !== undefined) for (const [i, l] of sweepCheckLines(sweep, r.sweepSteps).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1945
1945
|
for (const [i, l] of motionSeamLines(r.motionSeam).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1946
|
+
for (const [i, l] of figureOpeningLines(r.figureOpenings).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1946
1947
|
for (const [i, l] of clothLines(r.cloth).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1947
1948
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1948
1949
|
if (fig.sets !== undefined) {
|
package/src/check.ts
CHANGED
|
@@ -86,7 +86,8 @@ import { baseAnchorMoves, type AnchorRegion } from './baseanchor.ts';
|
|
|
86
86
|
import { jointContactReading, type JointContactReading } from './jointcontact.ts';
|
|
87
87
|
import { createHash } from 'node:crypto';
|
|
88
88
|
import { createRequire } from 'node:module';
|
|
89
|
-
import type { Viewport as RootCoverageViewport } from '@rig-c/core/render-shared';
|
|
89
|
+
import type { Candidate, Piece, Viewport as RootCoverageViewport } from '@rig-c/core/render-shared';
|
|
90
|
+
import { fillHoles } from './raster/morph.ts';
|
|
90
91
|
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
91
92
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
92
93
|
import { measureRecomposite, type RecompositeFigures, recompositeRecord, scenePhrase, sourceInRig, figureSilhouette } from './assemble.ts';
|
|
@@ -2706,6 +2707,8 @@ export interface CheckFigures {
|
|
|
2706
2707
|
/** Written only when the rig declares a physics constraint and has an idle (issue #501): {@link ClothLine}, a warning, never a bar. */
|
|
2707
2708
|
[CLOTH_LINE]?: ClothLine;
|
|
2708
2709
|
RECOMPOSITE_HOLES: ReportedLine;
|
|
2710
|
+
/** Issue #933: openings inside the figure over motion across undeclared setup-overlapping layer pairs; omitted when SKIP. */
|
|
2711
|
+
figure_openings?: FigureOpeningReading;
|
|
2709
2712
|
/** Written only under `--source` (issue #77): the setup pose against the painting, REPORTED. */
|
|
2710
2713
|
SETUP_POSE_VS_SOURCE?: ReportedLine;
|
|
2711
2714
|
/** Written only under `--wind-band` (issue #590): the steady-wind band each declared seam pair holds, REPORTED. */
|
|
@@ -3028,6 +3031,8 @@ export interface CheckReport {
|
|
|
3028
3031
|
besideIdle: string[];
|
|
3029
3032
|
/** `SEAM_OVER_MOTION` (issue #493): a warning, never a bar — in `check.json` only when a pair is declared; its SKIP is printed either way. */
|
|
3030
3033
|
motionSeam: MotionSeamLine;
|
|
3034
|
+
/** Issue #933: openings inside the figure over motion on undeclared setup-overlapping layer pairs — a warning, never a bar. */
|
|
3035
|
+
figureOpenings: FigureOpeningReading;
|
|
3031
3036
|
/** `CLOTH_SETTLES` (issue #501): a warning, never a bar — in `check.json` only when the rig declares a physics constraint and has an idle; its SKIP is printed either way. */
|
|
3032
3037
|
cloth: ClothLine;
|
|
3033
3038
|
/** `WIND_BAND` (issue #590): the steady-wind band each declared seam pair holds, written under `--wind-band` only. */
|
|
@@ -3088,7 +3093,7 @@ export function checkStatuses(fig: CheckFigures): Record<string, CheckStatus> {
|
|
|
3088
3093
|
const bar = (name: string, measured: boolean): CheckStatus => (!measured ? 'SKIP' : notMet.includes(name) ? 'FAIL' : 'PASS');
|
|
3089
3094
|
const out: Record<string, CheckStatus> = { gate: bar('gate', true), loop: bar('loop', fig.loop_max_diff !== null), seam: bar('seam', fig.seam_mean !== null) };
|
|
3090
3095
|
for (const n of JUDGEMENT_LINES) out[n] = fig[n].status;
|
|
3091
|
-
for (const n of [FOLD_LINE, LEVEL_LINE, STRESS_LINE, LIMB_LINE, SWEEP_LINE, MOTION_SEAM_LINE, CLOTH_LINE, ...REPORTED_LINES, SOURCE_LINE, WIND_BAND_LINE, ART_JOINT_LINE, CUT_MATERIAL_LINE, SETUP_PAINTING_LINE] as const) {
|
|
3096
|
+
for (const n of [FOLD_LINE, LEVEL_LINE, STRESS_LINE, LIMB_LINE, SWEEP_LINE, MOTION_SEAM_LINE, CLOTH_LINE, ...REPORTED_LINES, 'figure_openings', SOURCE_LINE, WIND_BAND_LINE, ART_JOINT_LINE, CUT_MATERIAL_LINE, SETUP_PAINTING_LINE] as const) {
|
|
3092
3097
|
const l = fig[n];
|
|
3093
3098
|
if (l !== undefined) out[n] = l.status;
|
|
3094
3099
|
}
|
|
@@ -5113,6 +5118,360 @@ export function motionSeamWarnText(r: MotionSeamRow): string {
|
|
|
5113
5118
|
return `WARN seam "${r.pair}" opens on ${r.opens.length} frame(s) ${frameRuns(r.opens)} max ${r.max_px} px in "${r.animation}" — over the one-cell floor${r.under_floor.length === 0 ? '' : `, ${r.under_floor.length} more frame(s) one cell thick not reported`}${evidence}`;
|
|
5114
5119
|
}
|
|
5115
5120
|
|
|
5121
|
+
/**
|
|
5122
|
+
* Issue #933: one undeclared setup-overlapping layer pair that opens a transparent hole inside the figure (`fillHoles(alpha > 0)`)
|
|
5123
|
+
* over motion, where the hole was covered at setup (`setupAlpha > 0`) and exceeds the one-cell thickness floor of the pair's
|
|
5124
|
+
* setup overlap box. Names the pair, the part under and the part above in draw order, the animation and worst frame, and the
|
|
5125
|
+
* size found against the size allowed.
|
|
5126
|
+
*/
|
|
5127
|
+
export interface FigureOpeningHit {
|
|
5128
|
+
pair: string;
|
|
5129
|
+
under: string;
|
|
5130
|
+
above: string;
|
|
5131
|
+
animation: string;
|
|
5132
|
+
frame: number;
|
|
5133
|
+
time: number;
|
|
5134
|
+
opens: number[];
|
|
5135
|
+
cell: number;
|
|
5136
|
+
found_cells: number;
|
|
5137
|
+
allowed_cells: number;
|
|
5138
|
+
box: [number, number, number, number];
|
|
5139
|
+
}
|
|
5140
|
+
|
|
5141
|
+
/** Issue #933: the all-frames reading of opened alpha inside the figure across undeclared setup-overlapping layer pairs. */
|
|
5142
|
+
export interface FigureOpeningReading {
|
|
5143
|
+
status: 'WARN' | 'CLEAR' | 'SKIP';
|
|
5144
|
+
reason?: string;
|
|
5145
|
+
fps: number;
|
|
5146
|
+
cell: number;
|
|
5147
|
+
pairs: number;
|
|
5148
|
+
animations: string[];
|
|
5149
|
+
warnings: number;
|
|
5150
|
+
openings: FigureOpeningHit[];
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
const QUAD_TRIANGLES: readonly number[] = [0, 1, 2, 0, 2, 3];
|
|
5154
|
+
|
|
5155
|
+
function rasterizePieceAlphaMask(
|
|
5156
|
+
cand: Candidate,
|
|
5157
|
+
piece: Piece,
|
|
5158
|
+
gw: number,
|
|
5159
|
+
gh: number,
|
|
5160
|
+
xMin: number,
|
|
5161
|
+
yMax: number,
|
|
5162
|
+
cell: number,
|
|
5163
|
+
out: Uint8Array,
|
|
5164
|
+
): void {
|
|
5165
|
+
const page = cand.pages.get(piece.page);
|
|
5166
|
+
const alpha = piece.tint[3];
|
|
5167
|
+
if (page === undefined || alpha <= 0) return;
|
|
5168
|
+
const tris = piece.kind === 'region' ? QUAD_TRIANGLES : piece.triangles;
|
|
5169
|
+
const verts = piece.world;
|
|
5170
|
+
const uvs = piece.uvs;
|
|
5171
|
+
const pw = page.width;
|
|
5172
|
+
const ph = page.height;
|
|
5173
|
+
const pdata = page.data;
|
|
5174
|
+
for (let t = 0; t + 2 < tris.length; t += 3) {
|
|
5175
|
+
const i0 = tris[t];
|
|
5176
|
+
const i1 = tris[t + 1];
|
|
5177
|
+
const i2 = tris[t + 2];
|
|
5178
|
+
const x0 = (verts[i0 * 2] - xMin) / cell;
|
|
5179
|
+
const y0 = (yMax - verts[i0 * 2 + 1]) / cell;
|
|
5180
|
+
const x1 = (verts[i1 * 2] - xMin) / cell;
|
|
5181
|
+
const y1 = (yMax - verts[i1 * 2 + 1]) / cell;
|
|
5182
|
+
const x2 = (verts[i2 * 2] - xMin) / cell;
|
|
5183
|
+
const y2 = (yMax - verts[i2 * 2 + 1]) / cell;
|
|
5184
|
+
const minX = Math.max(0, Math.floor(Math.min(x0, x1, x2)));
|
|
5185
|
+
const maxX = Math.min(gw - 1, Math.ceil(Math.max(x0, x1, x2)));
|
|
5186
|
+
const minY = Math.max(0, Math.floor(Math.min(y0, y1, y2)));
|
|
5187
|
+
const maxY = Math.min(gh - 1, Math.ceil(Math.max(y0, y1, y2)));
|
|
5188
|
+
const denom = (y1 - y2) * (x0 - x2) + (x2 - x1) * (y0 - y2);
|
|
5189
|
+
if (Math.abs(denom) < 1e-12) continue;
|
|
5190
|
+
const invDenom = 1 / denom;
|
|
5191
|
+
const u0 = uvs[i0 * 2];
|
|
5192
|
+
const v0 = uvs[i0 * 2 + 1];
|
|
5193
|
+
const u1 = uvs[i1 * 2];
|
|
5194
|
+
const v1 = uvs[i1 * 2 + 1];
|
|
5195
|
+
const u2 = uvs[i2 * 2];
|
|
5196
|
+
const v2 = uvs[i2 * 2 + 1];
|
|
5197
|
+
const dy12 = y1 - y2;
|
|
5198
|
+
const dx21 = x2 - x1;
|
|
5199
|
+
const dy20 = y2 - y0;
|
|
5200
|
+
const dx02 = x0 - x2;
|
|
5201
|
+
for (let py = minY; py <= maxY; py++) {
|
|
5202
|
+
const cy = py + 0.5 - y2;
|
|
5203
|
+
const rowOff = py * gw;
|
|
5204
|
+
const w0Base = dx21 * cy;
|
|
5205
|
+
const w1Base = dx02 * cy;
|
|
5206
|
+
for (let px = minX; px <= maxX; px++) {
|
|
5207
|
+
if (out[rowOff + px] === 1) continue;
|
|
5208
|
+
const cx = px + 0.5 - x2;
|
|
5209
|
+
const w0 = (dy12 * cx + w0Base) * invDenom;
|
|
5210
|
+
const w1 = (dy20 * cx + w1Base) * invDenom;
|
|
5211
|
+
const w2 = 1 - w0 - w1;
|
|
5212
|
+
if (w0 < -1e-6 || w1 < -1e-6 || w2 < -1e-6) continue;
|
|
5213
|
+
const u = w0 * u0 + w1 * u1 + w2 * u2;
|
|
5214
|
+
const v = w0 * v0 + w1 * v1 + w2 * v2;
|
|
5215
|
+
const tx = Math.max(0, Math.min(pw - 1, Math.floor(u * pw)));
|
|
5216
|
+
const ty = Math.max(0, Math.min(ph - 1, Math.floor(v * ph)));
|
|
5217
|
+
const a = pdata[(ty * pw + tx) * 4 + 3] * alpha;
|
|
5218
|
+
if (a > ART_ALPHA) out[rowOff + px] = 1;
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
/**
|
|
5225
|
+
* Issue #933: read openings inside the figure over motion across every undeclared setup-overlapping layer pair (`a|b`).
|
|
5226
|
+
* A gap already transparent at the setup pose (`setupUnion === 0`, such as a deliberate gap between arm and body) is art
|
|
5227
|
+
* and is excluded, and a pocket closed only by a third part is excluded by requiring the opened cells to lie inside the
|
|
5228
|
+
* hole-filled union of the pair's own posed drawings (`fillHoles(a_t ∪ b_t)`). Only cells covered by both layers of a pair
|
|
5229
|
+
* at frame 0 that become transparent interior holes inside both the posed figure silhouette and `fillHoles(a_t ∪ b_t)` above
|
|
5230
|
+
* the pair's one-cell thickness floor are reported, naming the part above (`above`), the frame, the size found and the size
|
|
5231
|
+
* allowed in grid cells.
|
|
5232
|
+
*/
|
|
5233
|
+
export function readFigurePairOpenings(
|
|
5234
|
+
buildDir: string,
|
|
5235
|
+
animationNames: readonly string[],
|
|
5236
|
+
declaredPairs: ReadonlySet<string> = new Set(),
|
|
5237
|
+
fps = IDLE_FPS,
|
|
5238
|
+
): FigureOpeningReading {
|
|
5239
|
+
if (animationNames.length === 0) {
|
|
5240
|
+
return { status: 'SKIP', reason: 'motion.json holds no animation, so there is no motion to read', fps, cell: 0, pairs: 0, animations: [], warnings: 0, openings: [] };
|
|
5241
|
+
}
|
|
5242
|
+
const skelPath = join(buildDir, 'skeleton.json');
|
|
5243
|
+
const atlasPath = join(buildDir, 'skeleton.atlas');
|
|
5244
|
+
if (!existsSync(skelPath)) {
|
|
5245
|
+
return { status: 'SKIP', reason: 'skeleton.json was not written', fps, cell: 0, pairs: 0, animations: [...animationNames], warnings: 0, openings: [] };
|
|
5246
|
+
}
|
|
5247
|
+
const api = officialRenderer();
|
|
5248
|
+
const cand = api.loadCandidate(
|
|
5249
|
+
{
|
|
5250
|
+
skeletonText: readFileSync(skelPath, 'utf8'),
|
|
5251
|
+
atlasText: existsSync(atlasPath) ? readFileSync(atlasPath, 'utf8') : null,
|
|
5252
|
+
atlasDir: buildDir,
|
|
5253
|
+
label: buildDir,
|
|
5254
|
+
},
|
|
5255
|
+
{ skeleton: skelPath, atlas: atlasPath },
|
|
5256
|
+
undefined,
|
|
5257
|
+
);
|
|
5258
|
+
const openings: FigureOpeningHit[] = [];
|
|
5259
|
+
let maxPairs = 0;
|
|
5260
|
+
let firstCell = 0;
|
|
5261
|
+
for (const anim of animationNames) {
|
|
5262
|
+
const sampledFrames = api.throughPoser(cand.choice, (poser) => api.sampleAnimation(poser, anim, fps)).value;
|
|
5263
|
+
if (sampledFrames.length === 0) continue;
|
|
5264
|
+
let xMin = Infinity;
|
|
5265
|
+
let yMin = Infinity;
|
|
5266
|
+
let xMax = -Infinity;
|
|
5267
|
+
let yMax = -Infinity;
|
|
5268
|
+
for (const f of sampledFrames) {
|
|
5269
|
+
for (const p of f.pieces) {
|
|
5270
|
+
for (let i = 0; i + 1 < p.world.length; i += 2) {
|
|
5271
|
+
const x = p.world[i];
|
|
5272
|
+
const y = p.world[i + 1];
|
|
5273
|
+
if (x < xMin) xMin = x;
|
|
5274
|
+
if (x > xMax) xMax = x;
|
|
5275
|
+
if (y < yMin) yMin = y;
|
|
5276
|
+
if (y > yMax) yMax = y;
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
if (!Number.isFinite(xMin) || xMax <= xMin || yMax <= yMin) continue;
|
|
5281
|
+
const pad = 4;
|
|
5282
|
+
xMin -= pad;
|
|
5283
|
+
yMin -= pad;
|
|
5284
|
+
xMax += pad;
|
|
5285
|
+
yMax += pad;
|
|
5286
|
+
const vpW = xMax - xMin;
|
|
5287
|
+
const vpH = yMax - yMin;
|
|
5288
|
+
const cell = Math.max(0.5, Math.max(vpW, vpH) / 512);
|
|
5289
|
+
const cellRounded = round3(cell);
|
|
5290
|
+
if (firstCell === 0) firstCell = cellRounded;
|
|
5291
|
+
const gw = Math.max(1, Math.ceil(vpW / cell));
|
|
5292
|
+
const gh = Math.max(1, Math.ceil(vpH / cell));
|
|
5293
|
+
const f0 = sampledFrames[0];
|
|
5294
|
+
const slotOrder: string[] = [];
|
|
5295
|
+
const setupMasks = new Map<string, Uint8Array>();
|
|
5296
|
+
const setupUnion = new Uint8Array(gw * gh);
|
|
5297
|
+
for (const p of f0.pieces) {
|
|
5298
|
+
let m = setupMasks.get(p.slot);
|
|
5299
|
+
if (m === undefined) {
|
|
5300
|
+
m = new Uint8Array(gw * gh);
|
|
5301
|
+
setupMasks.set(p.slot, m);
|
|
5302
|
+
slotOrder.push(p.slot);
|
|
5303
|
+
}
|
|
5304
|
+
rasterizePieceAlphaMask(cand, p, gw, gh, xMin, yMax, cell, m);
|
|
5305
|
+
}
|
|
5306
|
+
for (const m of setupMasks.values()) {
|
|
5307
|
+
for (let i = 0; i < gw * gh; i++) if (m[i] === 1) setupUnion[i] = 1;
|
|
5308
|
+
}
|
|
5309
|
+
interface PairSetup {
|
|
5310
|
+
pair: string;
|
|
5311
|
+
under: string;
|
|
5312
|
+
above: string;
|
|
5313
|
+
mu: Uint8Array;
|
|
5314
|
+
mv: Uint8Array;
|
|
5315
|
+
box: [number, number, number, number];
|
|
5316
|
+
allowed_cells: number;
|
|
5317
|
+
opens: number[];
|
|
5318
|
+
worstFrame: number;
|
|
5319
|
+
worstTime: number;
|
|
5320
|
+
worstCells: number;
|
|
5321
|
+
}
|
|
5322
|
+
const pairSetups: PairSetup[] = [];
|
|
5323
|
+
for (let u = 0; u < slotOrder.length; u++) {
|
|
5324
|
+
const under = slotOrder[u];
|
|
5325
|
+
const mu = setupMasks.get(under)!;
|
|
5326
|
+
for (let v = u + 1; v < slotOrder.length; v++) {
|
|
5327
|
+
const above = slotOrder[v];
|
|
5328
|
+
const pair = [under, above].sort().join('|');
|
|
5329
|
+
if (declaredPairs.has(pair)) continue;
|
|
5330
|
+
const mv = setupMasks.get(above)!;
|
|
5331
|
+
let hasOverlap = false;
|
|
5332
|
+
let bx0 = gw;
|
|
5333
|
+
let by0 = gh;
|
|
5334
|
+
let bx1 = -1;
|
|
5335
|
+
let by1 = -1;
|
|
5336
|
+
for (let py = 0; py < gh; py++) {
|
|
5337
|
+
const rowOff = py * gw;
|
|
5338
|
+
for (let px = 0; px < gw; px++) {
|
|
5339
|
+
const idx = rowOff + px;
|
|
5340
|
+
if (mu[idx] === 1 && mv[idx] === 1) {
|
|
5341
|
+
hasOverlap = true;
|
|
5342
|
+
if (px < bx0) bx0 = px;
|
|
5343
|
+
if (px > bx1) bx1 = px;
|
|
5344
|
+
if (py < by0) by0 = py;
|
|
5345
|
+
if (py > by1) by1 = py;
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
}
|
|
5349
|
+
if (!hasOverlap) continue;
|
|
5350
|
+
const box: [number, number, number, number] = [
|
|
5351
|
+
round3(xMin + bx0 * cell),
|
|
5352
|
+
round3(yMax - (by1 + 1) * cell),
|
|
5353
|
+
round3(xMin + (bx1 + 1) * cell),
|
|
5354
|
+
round3(yMax - by0 * cell),
|
|
5355
|
+
];
|
|
5356
|
+
const allowed_cells = Math.round(Math.max(box[2] - box[0], box[3] - box[1]) / cell);
|
|
5357
|
+
pairSetups.push({ pair, under, above, mu, mv, box, allowed_cells, opens: [], worstFrame: -1, worstTime: 0, worstCells: 0 });
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
if (pairSetups.length > maxPairs) maxPairs = pairSetups.length;
|
|
5361
|
+
if (pairSetups.length === 0) continue;
|
|
5362
|
+
const frameUnion = new Uint8Array(gw * gh);
|
|
5363
|
+
const pairUnion = new Uint8Array(gw * gh);
|
|
5364
|
+
const openedCells = new Int32Array(gw * gh);
|
|
5365
|
+
for (let fi = 1; fi < sampledFrames.length; fi++) {
|
|
5366
|
+
frameUnion.fill(0);
|
|
5367
|
+
const fr = sampledFrames[fi];
|
|
5368
|
+
for (const p of fr.pieces) {
|
|
5369
|
+
rasterizePieceAlphaMask(cand, p, gw, gh, xMin, yMax, cell, frameUnion);
|
|
5370
|
+
}
|
|
5371
|
+
const filled = fillHoles({ width: gw, height: gh, data: frameUnion });
|
|
5372
|
+
let openedCount = 0;
|
|
5373
|
+
for (let idx = 0; idx < gw * gh; idx++) {
|
|
5374
|
+
if (frameUnion[idx] === 0 && filled.data[idx] === 1 && setupUnion[idx] === 1) {
|
|
5375
|
+
openedCells[openedCount++] = idx;
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
if (openedCount === 0) continue;
|
|
5379
|
+
let frameSlotMasks: Map<string, Uint8Array> | null = null;
|
|
5380
|
+
const slotMaskAtFrame = (slot: string): Uint8Array => {
|
|
5381
|
+
if (frameSlotMasks === null) frameSlotMasks = new Map<string, Uint8Array>();
|
|
5382
|
+
let m = frameSlotMasks.get(slot);
|
|
5383
|
+
if (m !== undefined) return m;
|
|
5384
|
+
m = new Uint8Array(gw * gh);
|
|
5385
|
+
for (const p of fr.pieces) {
|
|
5386
|
+
if (p.slot === slot) rasterizePieceAlphaMask(cand, p, gw, gh, xMin, yMax, cell, m);
|
|
5387
|
+
}
|
|
5388
|
+
frameSlotMasks.set(slot, m);
|
|
5389
|
+
return m;
|
|
5390
|
+
};
|
|
5391
|
+
for (const ps of pairSetups) {
|
|
5392
|
+
if (openedCount <= ps.allowed_cells) continue;
|
|
5393
|
+
const mu = ps.mu;
|
|
5394
|
+
const mv = ps.mv;
|
|
5395
|
+
let preFound = 0;
|
|
5396
|
+
for (let k = 0; k < openedCount; k++) {
|
|
5397
|
+
const idx = openedCells[k];
|
|
5398
|
+
if (mu[idx] === 1 && mv[idx] === 1) preFound++;
|
|
5399
|
+
}
|
|
5400
|
+
if (preFound <= ps.allowed_cells) continue;
|
|
5401
|
+
const fmu = slotMaskAtFrame(ps.under);
|
|
5402
|
+
const fmv = slotMaskAtFrame(ps.above);
|
|
5403
|
+
for (let i = 0; i < gw * gh; i++) pairUnion[i] = fmu[i] | fmv[i];
|
|
5404
|
+
const pairFilled = fillHoles({ width: gw, height: gh, data: pairUnion }).data;
|
|
5405
|
+
let found = 0;
|
|
5406
|
+
for (let k = 0; k < openedCount; k++) {
|
|
5407
|
+
const idx = openedCells[k];
|
|
5408
|
+
if (mu[idx] === 1 && mv[idx] === 1 && pairFilled[idx] === 1) found++;
|
|
5409
|
+
}
|
|
5410
|
+
if (found > ps.allowed_cells) {
|
|
5411
|
+
ps.opens.push(fr.index);
|
|
5412
|
+
if (found > ps.worstCells) {
|
|
5413
|
+
ps.worstCells = found;
|
|
5414
|
+
ps.worstFrame = fr.index;
|
|
5415
|
+
ps.worstTime = round3(fr.time);
|
|
5416
|
+
}
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
for (const ps of pairSetups) {
|
|
5421
|
+
if (ps.opens.length > 0) {
|
|
5422
|
+
openings.push({
|
|
5423
|
+
pair: ps.pair,
|
|
5424
|
+
under: ps.under,
|
|
5425
|
+
above: ps.above,
|
|
5426
|
+
animation: anim,
|
|
5427
|
+
frame: ps.worstFrame,
|
|
5428
|
+
time: ps.worstTime,
|
|
5429
|
+
opens: ps.opens,
|
|
5430
|
+
cell: cellRounded,
|
|
5431
|
+
found_cells: ps.worstCells,
|
|
5432
|
+
allowed_cells: ps.allowed_cells,
|
|
5433
|
+
box: ps.box,
|
|
5434
|
+
});
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
}
|
|
5438
|
+
if (maxPairs === 0) {
|
|
5439
|
+
return {
|
|
5440
|
+
status: 'SKIP',
|
|
5441
|
+
reason: 'no undeclared layer pair shares a cell at the setup pose, so there is no pair to read',
|
|
5442
|
+
fps,
|
|
5443
|
+
cell: firstCell,
|
|
5444
|
+
pairs: 0,
|
|
5445
|
+
animations: [...animationNames],
|
|
5446
|
+
warnings: 0,
|
|
5447
|
+
openings: [],
|
|
5448
|
+
};
|
|
5449
|
+
}
|
|
5450
|
+
return {
|
|
5451
|
+
status: openings.length > 0 ? 'WARN' : 'CLEAR',
|
|
5452
|
+
fps,
|
|
5453
|
+
cell: firstCell,
|
|
5454
|
+
pairs: maxPairs,
|
|
5455
|
+
animations: [...animationNames],
|
|
5456
|
+
warnings: openings.length,
|
|
5457
|
+
openings,
|
|
5458
|
+
};
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
/** Issue #933: one console warning line for an undeclared layer pair opening inside the figure over motion. */
|
|
5462
|
+
export function figureOpeningWarnText(h: FigureOpeningHit): string {
|
|
5463
|
+
const spanW = Math.max(1, Math.round(h.box[2] - h.box[0]));
|
|
5464
|
+
const spanH = Math.max(1, Math.round(h.box[3] - h.box[1]));
|
|
5465
|
+
return `WARN opening "${h.pair}" above "${h.above}" (under "${h.under}") on ${h.opens.length} frame(s) ${frameRuns(h.opens)}, worst frame ${h.frame} (t = ${h.time}s) in "${h.animation}": found ${h.found_cells} cell(s), <= ${h.allowed_cells} cell(s) allowed in ${spanW}x${spanH} world units at ${h.cell} world units/cell — transparent opening inside the figure over motion on an undeclared layer pair (not a bar)`;
|
|
5466
|
+
}
|
|
5467
|
+
|
|
5468
|
+
/** Issue #933: the console lines for undeclared layer pair openings inside the figure over motion. */
|
|
5469
|
+
export function figureOpeningLines(reading: FigureOpeningReading): string[] {
|
|
5470
|
+
if (reading.status === 'SKIP') return [`figure_openings: SKIP — ${String(reading.reason)}`];
|
|
5471
|
+
const head = `figure_openings: ${reading.status} — ${reading.pairs} undeclared setup-overlapping pair(s) in ${reading.animations.length} animation(s) (${reading.animations.join(', ')}) at ${reading.fps} fps (cell ${reading.cell} world units): ${reading.warnings} warning(s), never a bar`;
|
|
5472
|
+
return [head, ...reading.openings.map(figureOpeningWarnText)];
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5116
5475
|
// ---------------------------------------------------------------------------
|
|
5117
5476
|
// cloth that comes to rest on the idle (issue #501; the line is {@link CLOTH_LINE}'s)
|
|
5118
5477
|
// ---------------------------------------------------------------------------
|
|
@@ -6441,6 +6800,11 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6441
6800
|
}
|
|
6442
6801
|
}
|
|
6443
6802
|
}
|
|
6803
|
+
const declaredPairKeys = new Set(motionSeam.pairs.map((p) => [p.a, p.b].sort().join('|')));
|
|
6804
|
+
const figureOpenings: FigureOpeningReading =
|
|
6805
|
+
inp.parts === null
|
|
6806
|
+
? { status: 'SKIP', reason: inp.noParts ?? 'no parts.json', fps: IDLE_FPS, cell: 0, pairs: 0, animations: [], warnings: 0, openings: [] }
|
|
6807
|
+
: readFigurePairOpenings(buildDir, animationNames, declaredPairKeys, IDLE_FPS);
|
|
6444
6808
|
|
|
6445
6809
|
// 3f. every physics chain read by its tip on the idle as rendered above, physics on (issue #501): a warning, never a
|
|
6446
6810
|
// bar; SKIP, named, with no idle or no physics constraint
|
|
@@ -6702,6 +7066,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6702
7066
|
...(motionSeam.pairs.length === 0 && motionSeam.roots === undefined ? {} : { [MOTION_SEAM_LINE]: motionSeam }),
|
|
6703
7067
|
...(stripped === null || idle === null ? {} : { [CLOTH_LINE]: cloth }),
|
|
6704
7068
|
RECOMPOSITE_HOLES: inp.parts === null ? { status: 'SKIP', reason: inp.noParts ?? '' } : holesLine(inp.parts),
|
|
7069
|
+
...(figureOpenings.status === 'SKIP' ? {} : { figure_openings: figureOpenings }),
|
|
6705
7070
|
...(sourceRead === null ? {} : { [SOURCE_LINE]: sourceRead }),
|
|
6706
7071
|
...(windBandRead === null ? {} : { [WIND_BAND_LINE]: windBandRead }),
|
|
6707
7072
|
...(Object.keys(skipped).length === 0 ? {} : { skipped }),
|
|
@@ -6800,6 +7165,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
6800
7165
|
requirements: reqBlock,
|
|
6801
7166
|
besideIdle: named,
|
|
6802
7167
|
motionSeam,
|
|
7168
|
+
figureOpenings,
|
|
6803
7169
|
cloth,
|
|
6804
7170
|
...(sweepSteps.size === 0 ? {} : { sweepSteps }),
|
|
6805
7171
|
...(windBandRead === null ? {} : { windBand: windBandRead }),
|
package/src/raster/morph.ts
CHANGED
|
@@ -85,30 +85,52 @@ export function fillHoles(m: Mask): Mask {
|
|
|
85
85
|
assertBinary('fillHoles', m);
|
|
86
86
|
const { width: w, height: h } = m;
|
|
87
87
|
if (w === 0 || h === 0) return newMask(w, h);
|
|
88
|
+
const data = m.data;
|
|
88
89
|
const outside = new Uint8Array(w * h);
|
|
89
|
-
const stack
|
|
90
|
-
|
|
91
|
-
if (m.data[i] === 0 && outside[i] === 0) {
|
|
92
|
-
outside[i] = 1;
|
|
93
|
-
stack.push(i);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
90
|
+
const stack = new Int32Array(w * h);
|
|
91
|
+
let top = 0;
|
|
96
92
|
for (let x = 0; x < w; x++) {
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
if (data[x] === 0 && outside[x] === 0) {
|
|
94
|
+
outside[x] = 1;
|
|
95
|
+
stack[top++] = x;
|
|
96
|
+
}
|
|
97
|
+
const b = (h - 1) * w + x;
|
|
98
|
+
if (data[b] === 0 && outside[b] === 0) {
|
|
99
|
+
outside[b] = 1;
|
|
100
|
+
stack[top++] = b;
|
|
101
|
+
}
|
|
99
102
|
}
|
|
100
103
|
for (let y = 0; y < h; y++) {
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
const l = y * w;
|
|
105
|
+
if (data[l] === 0 && outside[l] === 0) {
|
|
106
|
+
outside[l] = 1;
|
|
107
|
+
stack[top++] = l;
|
|
108
|
+
}
|
|
109
|
+
const r = l + w - 1;
|
|
110
|
+
if (data[r] === 0 && outside[r] === 0) {
|
|
111
|
+
outside[r] = 1;
|
|
112
|
+
stack[top++] = r;
|
|
113
|
+
}
|
|
103
114
|
}
|
|
104
|
-
while (
|
|
105
|
-
const i = stack
|
|
115
|
+
while (top > 0) {
|
|
116
|
+
const i = stack[--top];
|
|
106
117
|
const x = i % w;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (
|
|
118
|
+
if (x > 0 && data[i - 1] === 0 && outside[i - 1] === 0) {
|
|
119
|
+
outside[i - 1] = 1;
|
|
120
|
+
stack[top++] = i - 1;
|
|
121
|
+
}
|
|
122
|
+
if (x < w - 1 && data[i + 1] === 0 && outside[i + 1] === 0) {
|
|
123
|
+
outside[i + 1] = 1;
|
|
124
|
+
stack[top++] = i + 1;
|
|
125
|
+
}
|
|
126
|
+
if (i >= w && data[i - w] === 0 && outside[i - w] === 0) {
|
|
127
|
+
outside[i - w] = 1;
|
|
128
|
+
stack[top++] = i - w;
|
|
129
|
+
}
|
|
130
|
+
if (i + w < w * h && data[i + w] === 0 && outside[i + w] === 0) {
|
|
131
|
+
outside[i + w] = 1;
|
|
132
|
+
stack[top++] = i + w;
|
|
133
|
+
}
|
|
112
134
|
}
|
|
113
135
|
const out = newMask(w, h);
|
|
114
136
|
for (let i = 0; i < w * h; i++) out.data[i] = outside[i] === 1 ? 0 : 1;
|