@rig-c/parts 1.37.0 → 1.38.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 +3 -1
- package/docs/MIGRATION.md +19 -0
- package/docs/STABILITY.md +1 -1
- package/package.json +2 -2
- package/src/build.ts +24 -2
- package/src/check.ts +280 -1
package/docs/AUTHORING.md
CHANGED
|
@@ -2175,7 +2175,7 @@ move off it (`AS245`).
|
|
|
2175
2175
|
| `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 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 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 — cut the part with `assemble.cuts` to separate them; `LINT chest at … is not between the neck joint …`, `LINT hip at … is not past chest …`, `LINT hip at … is nearer the neck joint …` and `LINT <chain><i> at … does not advance from <s>_shoulder toward <s>_wrist` are the joint-frame lines: move that bone between, past or along the joints they name |
|
|
2176
2176
|
| `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 |
|
|
2177
2177
|
| `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` |
|
|
2178
|
-
| `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), `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 and the range bar — and names the ones that said SKIP | `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) |
|
|
2178
|
+
| `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 and one `WARN seam` line per pair and animation over the one-cell floor (issue #493, 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 and the range 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)`, 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) |
|
|
2179
2179
|
| `check --requirements` (and `build --requirements`) | after the lines above, one line per declared requirement, `NAME: PASS`, `FAIL` or `NOT MEASURABLE — …` with its figures, the bar as declared and the worst frame; then `requirements: N declared — M measured (P PASS, F FAIL), K NOT MEASURABLE; not declared: <kinds>`; `check.json`'s `requirements` block; the renders under `requirements/` (§7, *Declared requirements*) | every line PASS and `0 NOT MEASURABLE`; a follow's `released_copy` says whether rigc's consumer-driven door was taken on the throwaway copy; a `stretch` line's `held_by` (issue #321), right after `worst`, names the bones the worst edge's two vertices hang on and their idle chains, in `CHECK_TEXTURE_STRETCH`'s words (§6) and by the same function, on a PASS as on a FAIL | `CHECK_REQUIREMENT_MET` (a figure past its bar; the line names the frame) or `CHECK_REQUIREMENT_MEASURABLE` (the quantity is undefined in the frames: a tip or an axis on a bone of length 0, an aim with no line, a follow no frame of which reaches its least drive); the last line then reads `check: FAIL — B bar(s) not met, R declared requirement(s) not PASS` |
|
|
2180
2180
|
| `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 |
|
|
2181
2181
|
| `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line, or, when the end frame is not frame 0, the jump line: its max \|d\| and px over 40, then why — under `build`, `the rig holds N physics constraint(s), and rigc's render starts every one at rest at frame 0` with the remedy `build --loop --lead-in <n>`, or `the rig holds no physics constraint, so the idle itself does not close` (`CHECK_LOOP_CLOSES`); under `loop --frames`, which reads frames alone, both causes (issue #325); under `build --lead-in`, the lead-in line in its place (the frames kept and the jump at the wrap); each file's line, then `loop: idle.png N B (lossless); idle-indexed.png N B (max …, mean …); idle.gif N B (max …, mean …)` | `f0048.png equals f0000.png byte for byte, so it is dropped` | `LOOP_ENCODE` (§6) |
|
|
@@ -2893,6 +2893,7 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
2893
2893
|
| <a id="CHECK_STRESS_OFF_CHAIN"></a>`CHECK_STRESS_OFF_CHAIN` | a mesh from a See-through `handwear` layer that rides one chain of the idle does not turn rigidly with it: with the chain's first link turned 90°, −90° or 180° on a throwaway copy (physics left out, posed by rigc, issue #347), its worst edge reads above 1.0000 as max(ratio, 1/ratio) against the setup pose. The object names the mesh, the link and the turn; the detail quotes the edge and names each bone the mesh is weighted to off the chain, with how many vertices carry it and the largest weight. Or the turned render shows no mesh on the slot: not read is not a pass | the mesh's `segments`: drop the segment on the bone the line names (a 16 px `chest` stub at the shoulder, as `propose` drafted until issue #347), so the sleeve rides its chain alone. Measured (rigc 2.47.1): `demo`'s sleeves read 2.46 to 254.7 with the stub and 1.0000 without it, and the setup-pose seam does not move (mean 0.147, 0 px over 40, both ways) |
|
|
2894
2894
|
| <a id="CHECK_LIMB_ONE_BONE"></a>`CHECK_LIMB_ONE_BONE` | a part a `config.limbs` segment made is bound in `rig.json` to a bone besides its segment's — a mesh weighted to a second bone, or a slot on another bone — or to none (issue #349); the object names the part, its limb and its bone, the detail every other bone | `regions.<part>` the segment's bone, or a mesh whose `segments` name that bone alone: a bend is a rotation at the joint |
|
|
2895
2895
|
| <a id="CHECK_SEAM_OVER_RANGE"></a>`CHECK_SEAM_OVER_RANGE` | a limb joint with a declared `range` (issue #360), bent to one of its range's ends or its middle, fails the rest-pose seam's bar against the same pose with the joint's own art drawn under the limb — the bend uncovers the joint; the object names the joint, its limb and segment, the part, the bone and the angle. Under `cap_shape: "disc"` (issue #360, unit B), also any pixel of the disc over 40 at a pose, under the seam's bar or not. Also a joint the line cannot read (its bone has no parent in `rig.json`, no slot shows its limb, or its cap's disc holds no opaque art), and, refused, two renders of the bent copy that rigc did not put on one grid | a cap that covers the turn: the layer's art within the cap's radius on the upper side of the joint line, or a `cap` image painting what the layer lacks (`limb cap:` names the missing pixels); or a narrower `range` |
|
|
2896
|
+
| <a id="SEAM_OVER_MOTION"></a>`SEAM_OVER_MOTION` | **a warning, never a refusal** (issue #493; the owner's ruling, 2026-10-18: a seam that opens in motion may be intended). Every declared neighbour pair — each limb's two segments at a joint, the upper and the one below (`parts.json`'s limb records, `config.limbs`), and under `--requirements` each `seam`'s `part` and `neighbour`, one pair however many declare it — read in every animation `motion.json` holds, the idle included, by rig-c's seam reading: one `rigc render --seam <a>:<b>,… --fps 12` over the packed build as played (physics on), whose `seam.json` (`rigc-seam/1`, rig-c's AUTHORING §8.8) names per pair and frame a **gap** (background inside the joint) or a **ghost** (one piece's overlap band swung clear of its partner), each frame's cells (`px`, one texel of the coarser drawing — a rig px on a build at scale 1), its cell size and its world box. Printed per pair and animation with a frame over the floor: `WARN seam "<a>\|<b>" opens on N frame(s) [<frames>] max <px> px in "<animation>" — over the one-cell floor`, then how many more frames rigc named that were under it. **The floor**: a frame's gap or ghost is reported when it holds more cells than its box is long (its longer side, in cells) — thicker than one cell. A cell is drawn when the renderer paints its centre at half opacity, so an outline that moves by less than a cell flips the cells along it: a line one cell deep, joined or scattered, holds at most as many cells as its box is long, and a hole of any area more. The rule types no number; measured on `examples/wind`'s idle, rigc names 43 openings over its six limb pairs (the two elbows, 1 to 5 cells, boxes up to 17 cells long), every one one cell thick, and on rig-c's `gallery/walk` 13 joint cracks of 1 to 3 cells, all one cell thick. `check.json`'s `SEAM_OVER_MOTION` holds `status` (`WARN`, `CLEAR`), `fps`, `floor`, `pairs`, `animations`, `warnings` and one row per pair and animation (`opens` and `px` over the floor, `max_px`, `under_floor`, rigc's `status`, `drawn` and `banded`); it moves no exit status, no `check: PASS` and no bar count — the summary ends `; N warning(s) (SEAM_OVER_MOTION, not a bar)`. **SKIP**, named: no pair declared (no limb segment, no `seam`; then `check.json` has no key), no `parts.json`, no animation, every pair naming a slot `rig.json` lacks (each printed `not read`), or rigc reading no band on any frame of any pair (rigc's own `skip`: the two drawings share no cell at rest) — nothing measured, never a pass | if the opening is not intended: a cap that covers the turn (`config.limbs`, *Limbs: cut at their joints*; `limb cap:` names the pixels the layer lacks), or a motion that bends that joint less; if it is intended, nothing |
|
|
2896
2897
|
| <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` |
|
|
2897
2898
|
| <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 |
|
|
2898
2899
|
| <a id="LOOP_ENCODE"></a>`LOOP_ENCODE` | the loop encoder refused a frame (translucent pixel in a GIF, a size change) | the frames; for a translucent frame write the lossless or the indexed APNG, which keep alpha |
|
|
@@ -3409,6 +3410,7 @@ it reads is there, and says SKIP with the reason when it is not:
|
|
|
3409
3410
|
| `ANIMATIONS_DO_NOT_FOLD` | every animation, the idle included, through `rigc render --folds` | never SKIP: written when `motion.json` holds an animation, the idle alone included (issue #333), absent when it holds none (with or without a `parts.json`) |
|
|
3410
3411
|
| `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 |
|
|
3411
3412
|
| `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 |
|
|
3413
|
+
| `SEAM_OVER_MOTION` | `parts.json`'s limb records and `--requirements` seams, and every animation through `rigc render --seam` | a warning, not a bar: SKIP, printed and named, with no `parts.json` (absent from `check.json`), no pair declared, or no animation |
|
|
3412
3414
|
| `RECOMPOSITE_HOLES` | `parts.json`'s `recomposite` block | no `parts.json`, or no block |
|
|
3413
3415
|
| `SETUP_POSE_VS_SOURCE` | the rig spec and the painting | no `--source` |
|
|
3414
3416
|
|
package/docs/MIGRATION.md
CHANGED
|
@@ -322,3 +322,22 @@ their `parts.json` and `check.json` to `expected/`).
|
|
|
322
322
|
|
|
323
323
|
What to do: nothing. To have a joint read, declare its `range`; a joint that opens when
|
|
324
324
|
bent wants a cap that covers the turn.
|
|
325
|
+
|
|
326
|
+
## After 1.37.0: the seam in motion
|
|
327
|
+
|
|
328
|
+
Issue #493 reads every declared neighbour pair — each limb's two segments at a joint
|
|
329
|
+
(`config.limbs`), and each `--requirements` `seam`'s two parts — in every animation
|
|
330
|
+
`motion.json` holds, the idle included, by rig-c's seam reading (`rigc render --seam`,
|
|
331
|
+
rig-c 2.60.0, now the dependency's floor). It is a warning, never a bar: a seam that
|
|
332
|
+
opens in motion may be intended, so no exit status, no `check: PASS` and no bar count
|
|
333
|
+
moves (AUTHORING §6, `SEAM_OVER_MOTION`).
|
|
334
|
+
|
|
335
|
+
| change | before | after |
|
|
336
|
+
| --- | --- | --- |
|
|
337
|
+
| the `rig-c` dependency | `^2.59.0` | `^2.60.0`, the first with `render --seam` |
|
|
338
|
+
| `check` on a rig whose `parts.json` holds a limb segment, or under a `--requirements` with a `seam` | not read | `SEAM_OVER_MOTION` in `check.json` after `SEAM_OVER_RANGE`, its console lines after the range lines, one `WARN seam` line per pair and animation over the one-cell floor, and `; N warning(s) (SEAM_OVER_MOTION, not a bar)` at the end of the summary |
|
|
339
|
+
| `check` on any other rig | — | one console line, `SEAM_OVER_MOTION: SKIP — …`, naming why; no key in `check.json`, nothing more rendered |
|
|
340
|
+
|
|
341
|
+
What to do: nothing. A warning names the pair, the animation, the frames and the cells;
|
|
342
|
+
an opening that is not intended wants a cap that covers the turn, or a motion that bends
|
|
343
|
+
less.
|
package/docs/STABILITY.md
CHANGED
|
@@ -190,7 +190,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
190
190
|
| `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, then `shape` and `upper_px` under the segment's `cap_shape`, and `edge`, `feather_radius` and `feathered_px` under its `edge`, and `cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and `stroke_luma_max` (when the band has a split) under its `cut_band`, issue #360), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px`, `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242); `single_run_pad` (`file_size`, `side`, `left`, `top`) last, only for one run whose file is not the `seethrough.resolution` square (issue #366) |
|
|
191
191
|
| `rig/rig.json`, `rig/motion.json`, `rig/images/` | `rig`, `build`, `compose` | rig-c's rig spec and motion spec, `spec` `rigc-rig/1` and `rigc-motion/1`: the format is rig-c's, and this package writes only what rig-c's parser accepts; a bone carries `inherit` only where config.bones declares one, rig-c's mode name as written (issue #348) |
|
|
192
192
|
| `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins`, and `faint` only when an island was left out as faint — only when it declares `contour.bridges`); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
|
|
193
|
-
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40` — and `unread` on a joint not read), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
193
|
+
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40` — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493, a warning and not a bar: `status` — `WARN`, `CLEAR` or `SKIP` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
194
194
|
| `layers.json`, `<index>_<name>.png` | `layers --export` | the wrapper form `layers` reads (issue #359): `width`, `height`, then `layers` — each `name`, `filename`, `left`, `top`, `right`, `bottom`, `depth_median` (the input's own; `null` for a PSD, the manifest's order then being the draw order) — drawn layers in draw order, then the hidden-art layers; a PNG's name is its index and its layer's name, and the manifest, not the file name, says which layer it is |
|
|
195
195
|
| the template `.psd` | `layers --template` | one empty layer per name `templateLayerNames` (`src/layers.ts`) holds, bottom first; the same canvas writes the same bytes |
|
|
196
196
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `constraints` with `--cloth` (issue #364: a physics constraint per tree link named `<bone>_physics`, a dangling end's `<bone>_translate`, and a skirt swing's transforms `<swing>_<tree>`), `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.38.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": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"ag-psd": "^31.0.2",
|
|
64
|
-
"rig-c": "^2.
|
|
64
|
+
"rig-c": "^2.60.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@esotericsoftware/spine-core": "4.3.13",
|
package/src/build.ts
CHANGED
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
splitSchedule,
|
|
71
71
|
untestedIntervals,
|
|
72
72
|
} from './autoreplay.ts';
|
|
73
|
-
import { besideIdleLine, FOLD_LINE, type FoldsLine, LEVEL_LINE, type LevelLine, LIMB_LINE, type LimbLine, STRESS_LINE, SWEEP_LINE, type SweepLine, buildGateLines, causeLines, 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, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
|
|
73
|
+
import { besideIdleLine, frameRuns, MOTION_SEAM_LINE, type MotionSeamLine, type MotionSeamPlant, motionSeamWarnText, FOLD_LINE, type FoldsLine, LEVEL_LINE, type LevelLine, LIMB_LINE, type LimbLine, STRESS_LINE, SWEEP_LINE, type SweepLine, buildGateLines, causeLines, 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, type PackShape, type PageEdges, readFrameSet, rigcFailed, rigcIdentity, rigcRanClause, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_DIR, SEAM_MEAN_BAR, seamPairText, seamRigPxText, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK, STILL_DIR } from './check.ts';
|
|
74
74
|
import { type Cut, type Extend, type Limb, loadConfig, loadConfigAndAnimations, loadEarlyConfig, type Patch, type PlanEntry } from './config.ts';
|
|
75
75
|
import { failLine, PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
76
76
|
import type { MeshQualityReport, MotionSchedule } from 'rig-c/mesh';
|
|
@@ -1485,6 +1485,27 @@ export function sweepCheckLines(line: SweepLine): string[] {
|
|
|
1485
1485
|
];
|
|
1486
1486
|
}
|
|
1487
1487
|
|
|
1488
|
+
/**
|
|
1489
|
+
* The seam over every animation (issue #493) as the console prints it: the status, what was read, the floor and that
|
|
1490
|
+
* it is not a bar, then one WARN line per pair and animation with a frame over the floor, and a line per pair rigc
|
|
1491
|
+
* read nothing of (its SKIP) or the line could not read. A SKIP with nothing declared prints its reason alone.
|
|
1492
|
+
*/
|
|
1493
|
+
export function motionSeamLines(line: MotionSeamLine, plant: MotionSeamPlant = {}): string[] {
|
|
1494
|
+
if (line.status === 'SKIP' && line.rows.length === 0 && line.pairs.every((p) => p.unread === undefined)) return [`${MOTION_SEAM_LINE}: SKIP — ${String(line.reason)}`];
|
|
1495
|
+
const read = `${line.pairs.length} declared pair(s) in ${line.animations.length} animation(s) (${line.animations.join(', ')}), read by rigc render --seam at ${line.fps} fps on the build as played`;
|
|
1496
|
+
const head =
|
|
1497
|
+
line.status === 'SKIP'
|
|
1498
|
+
? `${MOTION_SEAM_LINE}: SKIP — ${String(line.reason)}; ${read}`
|
|
1499
|
+
: `${MOTION_SEAM_LINE}: ${line.status} — ${read}: ${line.warnings} warning(s), never a bar; ${line.floor}`;
|
|
1500
|
+
return [
|
|
1501
|
+
head,
|
|
1502
|
+
...line.pairs.filter((p) => p.unread !== undefined).map((p) => `seam "${p.pair}" (${p.declared.join('; ')}): not read — ${String(p.unread)}`),
|
|
1503
|
+
...line.rows.filter((r) => r.status === 'skip').map((r) => `seam "${r.pair}" in "${r.animation}": SKIP — rigc read no band on any of the ${r.drawn} frame(s) both slots draw, so there is no seam to read`),
|
|
1504
|
+
...(plant.silent === true ? [] : line.rows.filter((r) => r.opens.length > 0).map(motionSeamWarnText)),
|
|
1505
|
+
...line.rows.filter((r) => r.opens.length === 0 && r.under_floor.length > 0).map((r) => `seam "${r.pair}" in "${r.animation}": under the floor — rigc names ${r.under_floor.length} frame(s) open ${frameRuns(r.under_floor)}, each opening one cell thick`),
|
|
1506
|
+
];
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1488
1509
|
/** A requirement's line as the console prints it: its name and status, the reason first when NOT MEASURABLE, then its figures as check.json holds them. */
|
|
1489
1510
|
export function requirementText(name: string, line: RequirementLine): string {
|
|
1490
1511
|
const rest = Object.entries(line).filter(([k]) => k !== 'status' && k !== 'reason');
|
|
@@ -1543,6 +1564,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1543
1564
|
if (limbs !== undefined) for (const [i, l] of limbCheckLines(limbs).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1544
1565
|
const sweep = fig[SWEEP_LINE];
|
|
1545
1566
|
if (sweep !== undefined) for (const [i, l] of sweepCheckLines(sweep).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1567
|
+
for (const [i, l] of motionSeamLines(r.motionSeam).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1546
1568
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1547
1569
|
if (fig.sets !== undefined) {
|
|
1548
1570
|
for (const [setName, setFig] of Object.entries(fig.sets)) {
|
|
@@ -1560,7 +1582,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1560
1582
|
for (const p of r.problems) log(failLine(p));
|
|
1561
1583
|
const reqNotPass = r.requirements === null ? 0 : r.requirements.summary.declared - r.requirements.summary.pass;
|
|
1562
1584
|
const notMet = r.requirements === null ? `${r.problems.length} bar(s) not met` : `${r.problems.length - reqNotPass} bar(s) not met, ${reqNotPass} declared requirement(s) not PASS`;
|
|
1563
|
-
log(`${fig.PASS ? 'check: PASS' : `check: FAIL — ${notMet}`}; ${barsSummary(r)}`);
|
|
1585
|
+
log(`${fig.PASS ? 'check: PASS' : `check: FAIL — ${notMet}`}; ${barsSummary(r)}${r.motionSeam.status === 'SKIP' ? '' : `; ${r.motionSeam.warnings} warning(s) (${MOTION_SEAM_LINE}, not a bar)`}`);
|
|
1564
1586
|
return r;
|
|
1565
1587
|
}
|
|
1566
1588
|
|
package/src/check.ts
CHANGED
|
@@ -125,7 +125,7 @@ import {
|
|
|
125
125
|
type Toward,
|
|
126
126
|
type WorldBox,
|
|
127
127
|
} from './instruments.ts';
|
|
128
|
-
import { type AlternatePartsRecord, type PartRecord, type PartsFile, readParts } from './parts.ts';
|
|
128
|
+
import { type AlternatePartsRecord, type LimbRecord, type PartRecord, type PartsFile, readParts } from './parts.ts';
|
|
129
129
|
import { poseRigBonesForDrawn, type RigBone, SETS_FILE } from './rig.ts';
|
|
130
130
|
import { turnTwin } from './turn.ts';
|
|
131
131
|
import {
|
|
@@ -1863,6 +1863,7 @@ export interface CheckFigures {
|
|
|
1863
1863
|
[LIMB_LINE]?: LimbLine;
|
|
1864
1864
|
/** Written only where parts.json holds a limb record with a `range` (issue #360): {@link SweepLine}. */
|
|
1865
1865
|
[SWEEP_LINE]?: SweepLine;
|
|
1866
|
+
[MOTION_SEAM_LINE]?: MotionSeamLine;
|
|
1866
1867
|
RECOMPOSITE_HOLES: ReportedLine;
|
|
1867
1868
|
/** Written only under `--source` (issue #77): the setup pose against the painting, REPORTED. */
|
|
1868
1869
|
SETUP_POSE_VS_SOURCE?: ReportedLine;
|
|
@@ -2125,6 +2126,8 @@ export interface CheckReport {
|
|
|
2125
2126
|
* reads the idle. Empty when the idle is alone.
|
|
2126
2127
|
*/
|
|
2127
2128
|
besideIdle: string[];
|
|
2129
|
+
/** `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. */
|
|
2130
|
+
motionSeam: MotionSeamLine;
|
|
2128
2131
|
}
|
|
2129
2132
|
|
|
2130
2133
|
/**
|
|
@@ -3154,6 +3157,249 @@ export function sweepLine(
|
|
|
3154
3157
|
};
|
|
3155
3158
|
}
|
|
3156
3159
|
|
|
3160
|
+
// ---------------------------------------------------------------------------
|
|
3161
|
+
// the seam over every animation (issue #493; the line is {@link MOTION_SEAM_LINE}'s)
|
|
3162
|
+
// ---------------------------------------------------------------------------
|
|
3163
|
+
|
|
3164
|
+
/**
|
|
3165
|
+
* `SEAM_OVER_MOTION` (issue #493): every declared neighbour pair read in every
|
|
3166
|
+
* animation `motion.json` holds, the idle included, by rig-c's seam reading
|
|
3167
|
+
* (`rigc render --seam`, rig-c 2.60.0) — a **warning, never a bar**. The
|
|
3168
|
+
* owner's ruling (2026-10-18): a seam opening in motion may be intentional, so
|
|
3169
|
+
* the line names the pair, the animation, the frames and the cells, and moves
|
|
3170
|
+
* neither the exit status nor `check: PASS`; the summary counts it apart from
|
|
3171
|
+
* the bars.
|
|
3172
|
+
*
|
|
3173
|
+
* The pairs are the ones the inputs declare and nothing else: each limb's
|
|
3174
|
+
* consecutive segments in `parts.json` (`config.limbs`, the upper and the
|
|
3175
|
+
* segment below each joint) and, under `--requirements`, each `seam`
|
|
3176
|
+
* requirement's part and neighbour. None declared, the line is a SKIP naming
|
|
3177
|
+
* why, never a pass, and `check.json` carries no key for it.
|
|
3178
|
+
*
|
|
3179
|
+
* **The floor** ({@link openingOverFloor}): a frame's gap or ghost is reported
|
|
3180
|
+
* when it holds more cells than its box is long — when it is thicker than one
|
|
3181
|
+
* cell. rigc counts a cell drawn when the renderer paints its centre at half
|
|
3182
|
+
* opacity or more, so a boundary that moves by less than a cell flips the
|
|
3183
|
+
* cells along it in or out: a line of cells one deep, scattered or joined,
|
|
3184
|
+
* holds at most as many cells as its box's longer side. A hole of any area
|
|
3185
|
+
* holds more. The rule types no number; it is read off rigc's own cell and
|
|
3186
|
+
* box, and measured, not chosen: on `examples/wind`'s idle rigc names 43
|
|
3187
|
+
* openings over its six limb pairs (28 and 15 frames of the two elbows, 1 to
|
|
3188
|
+
* 5 cells, boxes up to 17 cells long) and every one is one cell thick; on
|
|
3189
|
+
* rig-c's `gallery/walk` its 13 joint cracks of 1 to 3 cells are too; the
|
|
3190
|
+
* ghost rig-c's own reading names on a skirt swung clear of a leg (1564 cells
|
|
3191
|
+
* in a box 92 long) is far over it.
|
|
3192
|
+
*/
|
|
3193
|
+
export const MOTION_SEAM_LINE = 'SEAM_OVER_MOTION';
|
|
3194
|
+
|
|
3195
|
+
/** The seam reading's scratch build and renders, under `--out`; removed before the stage returns. */
|
|
3196
|
+
const MOTION_SEAM_SCRATCH = '_seam_motion';
|
|
3197
|
+
|
|
3198
|
+
/** rig-c's seam reading: the file it writes in each set's directory, and its format tag. */
|
|
3199
|
+
export const SEAM_READING_FILE = 'seam.json';
|
|
3200
|
+
export const SEAM_READING_SPEC = 'rigc-seam/1';
|
|
3201
|
+
|
|
3202
|
+
/** The floor as the console and `check.json` state it. */
|
|
3203
|
+
export const MOTION_SEAM_FLOOR = "a frame opens when its gap or its ghost holds more cells than its box is long (rig-c's cell, its box's longer side) — thicker than one cell; a crack one cell deep is the half-opacity cut moving by less than a cell, and is not reported";
|
|
3204
|
+
|
|
3205
|
+
/** One declared pair: the two slots, and every declaration that names it. */
|
|
3206
|
+
export interface MotionSeamPair {
|
|
3207
|
+
/** `"<a>|<b>"`, the two slots in the order declared first. */
|
|
3208
|
+
pair: string;
|
|
3209
|
+
a: string;
|
|
3210
|
+
b: string;
|
|
3211
|
+
declared: string[];
|
|
3212
|
+
/** Why the pair is not read: a slot `rig.json` does not have. */
|
|
3213
|
+
unread?: string;
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
/** One pair in one animation, as the floor reads rigc's file. */
|
|
3217
|
+
export interface MotionSeamRow {
|
|
3218
|
+
pair: string;
|
|
3219
|
+
animation: string;
|
|
3220
|
+
/** rigc's: `skip` when no frame has a band to read — nothing measured, not a seam that held. */
|
|
3221
|
+
status: 'read' | 'skip';
|
|
3222
|
+
/** Frames both slots draw, and of those the frames read (rigc's `drawn`, `banded`). */
|
|
3223
|
+
drawn: number;
|
|
3224
|
+
banded: number;
|
|
3225
|
+
/** The frames over the floor, ascending: the warning's frames. */
|
|
3226
|
+
opens: number[];
|
|
3227
|
+
/** Beside `opens`, frame by frame: the most cells of either kind on that frame. */
|
|
3228
|
+
px: number[];
|
|
3229
|
+
/** The most cells of either kind on any frame over the floor; 0 when none is. */
|
|
3230
|
+
max_px: number;
|
|
3231
|
+
/** The frames rigc names open whose every opening is one cell thick: under the floor, not reported. */
|
|
3232
|
+
under_floor: number[];
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
/** The line: `WARN` with at least one warning, `CLEAR` with none, `SKIP` with nothing declared or nothing to read. */
|
|
3236
|
+
export interface MotionSeamLine {
|
|
3237
|
+
status: 'WARN' | 'CLEAR' | 'SKIP';
|
|
3238
|
+
reason?: string;
|
|
3239
|
+
fps: number;
|
|
3240
|
+
floor: string;
|
|
3241
|
+
pairs: MotionSeamPair[];
|
|
3242
|
+
animations: string[];
|
|
3243
|
+
/** Rows with a frame over the floor: the WARN lines. */
|
|
3244
|
+
warnings: number;
|
|
3245
|
+
rows: MotionSeamRow[];
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
/**
|
|
3249
|
+
* The neighbour pairs the inputs declare, in their order: each limb's
|
|
3250
|
+
* consecutive segments (`parts.json`'s limb records, by segment), then each
|
|
3251
|
+
* `seam` requirement's part and neighbour. A pair named twice, in either
|
|
3252
|
+
* order, is one pair with both declarations.
|
|
3253
|
+
*/
|
|
3254
|
+
export function motionSeamPairs(parts: PartsFile, seams: ReadonlyArray<{ name: string; part: string; neighbour: string }>): MotionSeamPair[] {
|
|
3255
|
+
const out: MotionSeamPair[] = [];
|
|
3256
|
+
const add = (a: string, b: string, declared: string): void => {
|
|
3257
|
+
const hit = out.find((p) => (p.a === a && p.b === b) || (p.a === b && p.b === a));
|
|
3258
|
+
if (hit !== undefined) hit.declared.push(declared);
|
|
3259
|
+
else out.push({ pair: `${a}|${b}`, a, b, declared: [declared] });
|
|
3260
|
+
};
|
|
3261
|
+
const limbs: string[] = [];
|
|
3262
|
+
for (const p of parts.parts) if (p.limb !== undefined && !limbs.includes(p.limb.name)) limbs.push(p.limb.name);
|
|
3263
|
+
for (const limb of limbs) {
|
|
3264
|
+
const segs = parts.parts.filter((p) => p.limb?.name === limb).sort((x, y) => (x.limb as LimbRecord).segment - (y.limb as LimbRecord).segment);
|
|
3265
|
+
for (let i = 1; i < segs.length; i++) {
|
|
3266
|
+
const lower = segs[i].limb as LimbRecord;
|
|
3267
|
+
add(segs[i - 1].name, segs[i].name, `config.limbs.${limb} segment ${lower.segment}${lower.joint === undefined ? '' : `, joint "${lower.joint}"`}`);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
for (const r of seams) add(r.part, r.neighbour, `requirement "${r.name}" (seam)`);
|
|
3271
|
+
return out;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
/** A frame's opening of one kind over the floor ({@link MOTION_SEAM_FLOOR}): more cells than its box, in cells, is long. */
|
|
3275
|
+
export function openingOverFloor(hit: { px: number; cell: number; box: readonly [number, number, number, number] }): boolean {
|
|
3276
|
+
const long = Math.round(Math.max(hit.box[2] - hit.box[0], hit.box[3] - hit.box[1]) / hit.cell);
|
|
3277
|
+
return hit.px > long;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
/** Frame indices as runs, `[0-6, 8, 13]`: the form rigc prints a frame list in. */
|
|
3281
|
+
export function frameRuns(frames: readonly number[]): string {
|
|
3282
|
+
const runs: string[] = [];
|
|
3283
|
+
for (let i = 0; i < frames.length; ) {
|
|
3284
|
+
let j = i;
|
|
3285
|
+
while (j + 1 < frames.length && frames[j + 1] === frames[j] + 1) j++;
|
|
3286
|
+
runs.push(i === j ? String(frames[i]) : `${frames[i]}-${frames[j]}`);
|
|
3287
|
+
i = j + 1;
|
|
3288
|
+
}
|
|
3289
|
+
return `[${runs.join(', ')}]`;
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
/**
|
|
3293
|
+
* A planted defect, for the suite's mutants only (`SO` controls in `selftest.ts`). Every caller in `src/` passes none.
|
|
3294
|
+
*/
|
|
3295
|
+
export interface MotionSeamPlant {
|
|
3296
|
+
/** Report every frame rigc names open, the one-cell cracks included: the floor removed. */
|
|
3297
|
+
noFloor?: true;
|
|
3298
|
+
/** Print the line's head and none of its WARN lines: the warning suppressed. */
|
|
3299
|
+
silent?: true;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
type SeamHitRead = { index: number; px: number; cell: number; box: [number, number, number, number] };
|
|
3303
|
+
|
|
3304
|
+
/**
|
|
3305
|
+
* The rows of one `rigc render --seam` run over every animation: `frames.json`
|
|
3306
|
+
* names each set's directory, each `seam.json` (`rigc-seam/1`) its pairs in
|
|
3307
|
+
* the flag's order. Refuses (`FRAMES_SIDECAR`), every problem at once, a set or
|
|
3308
|
+
* file that is missing, a file that is not the animation's or not at the rate
|
|
3309
|
+
* asked, a pair out of the flag's order, and a field of the wrong shape.
|
|
3310
|
+
*/
|
|
3311
|
+
export function readMotionSeams(dir: string, animations: readonly string[], fps: number, pairs: readonly MotionSeamPair[], plant: MotionSeamPlant = {}): MotionSeamRow[] {
|
|
3312
|
+
const problems: Problem[] = [];
|
|
3313
|
+
const bad = (object: string, detail: string): void => {
|
|
3314
|
+
problems.push({ code: 'FRAMES_SIDECAR', object, detail });
|
|
3315
|
+
};
|
|
3316
|
+
const rows: MotionSeamRow[] = [];
|
|
3317
|
+
const sidecar = readJson(join(dir, 'frames.json'), problems);
|
|
3318
|
+
const sets = sidecar !== null && Array.isArray(sidecar.sets) ? sidecar.sets.filter(isRecord) : [];
|
|
3319
|
+
const nums = (v: unknown): v is number[] => Array.isArray(v) && v.every((x) => typeof x === 'number');
|
|
3320
|
+
const hitOf = (h: unknown): SeamHitRead | null =>
|
|
3321
|
+
isRecord(h) && typeof h.index === 'number' && typeof h.px === 'number' && typeof h.cell === 'number' && h.cell > 0 && nums(h.box) && h.box.length === 4 ? { index: h.index, px: h.px, cell: h.cell, box: [h.box[0], h.box[1], h.box[2], h.box[3]] } : null;
|
|
3322
|
+
for (const animation of animations) {
|
|
3323
|
+
const set = sets.find((x) => x.animation === animation);
|
|
3324
|
+
if (set === undefined || typeof set.dir !== 'string') {
|
|
3325
|
+
bad(join(dir, 'frames.json'), `names no frame set for animation ${JSON.stringify(animation)}; \`rigc render --seam\` over the build writes one per animation`);
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3328
|
+
const path = join(dir, set.dir, SEAM_READING_FILE);
|
|
3329
|
+
const file = readJson(path, problems);
|
|
3330
|
+
if (file === null) continue;
|
|
3331
|
+
if (file.spec !== SEAM_READING_SPEC) bad(path, `spec is ${JSON.stringify(file.spec)}; "${SEAM_READING_SPEC}" is required`);
|
|
3332
|
+
if (file.animation !== animation) bad(path, `animation is ${JSON.stringify(file.animation)}; ${JSON.stringify(animation)} is required`);
|
|
3333
|
+
if (file.fps !== fps) bad(path, `fps is ${JSON.stringify(file.fps)}; ${fps}, the rate asked for, is required`);
|
|
3334
|
+
const read = Array.isArray(file.pairs) ? file.pairs : [];
|
|
3335
|
+
if (read.length !== pairs.length) bad(path, `holds ${read.length} pair(s); the ${pairs.length} asked for, in the flag's order, are required`);
|
|
3336
|
+
pairs.forEach((want, i) => {
|
|
3337
|
+
const p = read[i];
|
|
3338
|
+
const at = `${path} pairs[${i}]`;
|
|
3339
|
+
if (!isRecord(p) || p.a !== want.a || p.b !== want.b) {
|
|
3340
|
+
bad(at, `is ${JSON.stringify(isRecord(p) ? `${String(p.a)}|${String(p.b)}` : p)}; "${want.pair}", the flag's entry ${i}, is required`);
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
if ((p.status !== 'read' && p.status !== 'skip') || typeof p.drawn !== 'number' || typeof p.banded !== 'number' || !nums(p.opens)) {
|
|
3344
|
+
bad(at, 'is not { a, b, drawn, banded, status, opens, maxPx, gap, ghost } as rigc-seam/1 writes it');
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
const over = new Map<number, number>();
|
|
3348
|
+
for (const kind of ['gap', 'ghost'] as const) {
|
|
3349
|
+
const k = p[kind];
|
|
3350
|
+
const frames = isRecord(k) && Array.isArray(k.frames) ? k.frames.map(hitOf) : null;
|
|
3351
|
+
if (frames === null || frames.some((h) => h === null)) {
|
|
3352
|
+
bad(`${at} ${kind}`, 'is not { frames: [{ index, px, cell, box }…], spans, worst } as rigc-seam/1 writes it');
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
for (const h of frames as SeamHitRead[]) if (plant.noFloor === true || openingOverFloor(h)) over.set(h.index, Math.max(over.get(h.index) ?? 0, h.px));
|
|
3356
|
+
}
|
|
3357
|
+
const opens = [...over.keys()].sort((x, y) => x - y);
|
|
3358
|
+
rows.push({
|
|
3359
|
+
pair: want.pair,
|
|
3360
|
+
animation,
|
|
3361
|
+
status: p.status,
|
|
3362
|
+
drawn: p.drawn,
|
|
3363
|
+
banded: p.banded,
|
|
3364
|
+
opens,
|
|
3365
|
+
px: opens.map((f) => over.get(f) ?? 0),
|
|
3366
|
+
max_px: opens.reduce((m, f) => Math.max(m, over.get(f) ?? 0), 0),
|
|
3367
|
+
under_floor: p.opens.filter((f) => !over.has(f)),
|
|
3368
|
+
});
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
refuseIfAny(problems);
|
|
3372
|
+
return rows;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
/** The line from its rows: WARN when a row has a frame over the floor, CLEAR when none has, SKIP when no row was read. */
|
|
3376
|
+
export function motionSeamLine(pairs: MotionSeamPair[], animations: string[], fps: number, rows: MotionSeamRow[], reason?: string): MotionSeamLine {
|
|
3377
|
+
const warnings = rows.filter((r) => r.opens.length > 0).length;
|
|
3378
|
+
const read = rows.some((r) => r.status === 'read');
|
|
3379
|
+
const skipWhy =
|
|
3380
|
+
reason ??
|
|
3381
|
+
(read
|
|
3382
|
+
? undefined
|
|
3383
|
+
: pairs.every((p) => p.unread !== undefined)
|
|
3384
|
+
? `no declared pair is read: ${pairs.map((p) => `"${p.pair}" ${String(p.unread)}`).join('; ')}`
|
|
3385
|
+
: `rigc read no band on any frame of any declared pair in any animation — nothing measured, which is not a seam that held`);
|
|
3386
|
+
return {
|
|
3387
|
+
status: skipWhy !== undefined ? 'SKIP' : warnings > 0 ? 'WARN' : 'CLEAR',
|
|
3388
|
+
...(skipWhy === undefined ? {} : { reason: skipWhy }),
|
|
3389
|
+
fps,
|
|
3390
|
+
floor: MOTION_SEAM_FLOOR,
|
|
3391
|
+
pairs,
|
|
3392
|
+
animations,
|
|
3393
|
+
warnings,
|
|
3394
|
+
rows,
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
/** One warning as the console prints it (issue #493's form, the floor's clause after it). */
|
|
3399
|
+
export function motionSeamWarnText(r: MotionSeamRow): string {
|
|
3400
|
+
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`}`;
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3157
3403
|
/**
|
|
3158
3404
|
* One `CHECK_RIGC_GREEN` per line that says why `rigc <what>` was not green:
|
|
3159
3405
|
* its FAIL and compile-error lines, or, when there are none, {@link causeLines}.
|
|
@@ -3570,6 +3816,37 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
3570
3816
|
}
|
|
3571
3817
|
}
|
|
3572
3818
|
|
|
3819
|
+
// 3e. every declared neighbour pair read in every animation by rigc's seam reading (issue #493): a warning, never a
|
|
3820
|
+
// bar — the build as played, at the idle's rate; SKIP, named, with no pair declared or no animation to read
|
|
3821
|
+
let motionSeam: MotionSeamLine;
|
|
3822
|
+
{
|
|
3823
|
+
const fps = IDLE_FPS;
|
|
3824
|
+
const seamReqs = reqFile === null || seams === null ? [] : reqFile.requirements.flatMap((r) => (r.kind === 'seam' && seams?.has(r.name) === true ? [{ name: r.name, part: r.part, neighbour: r.neighbour }] : []));
|
|
3825
|
+
const pairs = inp.parts === null ? [] : motionSeamPairs(inp.parts, seamReqs);
|
|
3826
|
+
const slotNames = new Set((Array.isArray(inp.rig.slots) ? inp.rig.slots : []).filter(isRecord).map((sl) => String(sl.name)));
|
|
3827
|
+
for (const p of pairs) {
|
|
3828
|
+
const missing = [p.a, p.b].filter((n) => !slotNames.has(n));
|
|
3829
|
+
if (missing.length > 0) p.unread = `names ${missing.map((n) => `slot "${n}"`).join(' and ')}, which rig.json does not have`;
|
|
3830
|
+
}
|
|
3831
|
+
const read = pairs.filter((p) => p.unread === undefined);
|
|
3832
|
+
if (inp.parts === null) motionSeam = motionSeamLine([], animationNames, fps, [], inp.noParts ?? 'no parts.json');
|
|
3833
|
+
else if (pairs.length === 0) motionSeam = motionSeamLine([], animationNames, fps, [], 'no neighbour pair is declared: parts.json holds no limb segment below a joint (config.limbs) and no --requirements seam names two parts');
|
|
3834
|
+
else if (animationNames.length === 0) motionSeam = motionSeamLine(pairs, [], fps, [], 'motion.json holds no animation to read the declared pairs in');
|
|
3835
|
+
else if (read.length === 0) motionSeam = motionSeamLine(pairs, animationNames, fps, []);
|
|
3836
|
+
else {
|
|
3837
|
+
const seamMotionDir = join(out, MOTION_SEAM_SCRATCH);
|
|
3838
|
+
rmSync(seamMotionDir, { recursive: true, force: true });
|
|
3839
|
+
try {
|
|
3840
|
+
const list = read.map((p) => `${p.a}:${p.b}`).join(',');
|
|
3841
|
+
const r = rigc(['render', '--candidate', buildDir, '--seam', list, '--fps', String(fps), '--max', String(FOLD_RENDER_MAX), '--out', seamMotionDir]);
|
|
3842
|
+
if (r.status !== 0) refuseIfAny(rigcFailed(`render --seam ${list} --fps ${fps}`, r, r.out.split('\n').filter((l) => l.includes('FAIL') || l.includes('rigc:'))));
|
|
3843
|
+
motionSeam = motionSeamLine(pairs, animationNames, fps, readMotionSeams(seamMotionDir, animationNames, fps, read));
|
|
3844
|
+
} finally {
|
|
3845
|
+
rmSync(seamMotionDir, { recursive: true, force: true });
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3573
3850
|
// 4. the setup pose as a one-key throwaway animation — the seam reads it against the parts, the source line against
|
|
3574
3851
|
// the painting — and, beside it, the same pose with the eyes shut
|
|
3575
3852
|
let seam: SeamFigures | null = null;
|
|
@@ -3749,6 +4026,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
3749
4026
|
...(stress === null ? {} : { [STRESS_LINE]: stress }),
|
|
3750
4027
|
...(limbs === null ? {} : { [LIMB_LINE]: limbs }),
|
|
3751
4028
|
...(sweep === null ? {} : { [SWEEP_LINE]: sweep }),
|
|
4029
|
+
...(motionSeam.pairs.length === 0 ? {} : { [MOTION_SEAM_LINE]: motionSeam }),
|
|
3752
4030
|
RECOMPOSITE_HOLES: inp.parts === null ? { status: 'SKIP', reason: inp.noParts ?? '' } : holesLine(inp.parts),
|
|
3753
4031
|
...(sourceRead === null ? {} : { [SOURCE_LINE]: sourceRead }),
|
|
3754
4032
|
...(Object.keys(skipped).length === 0 ? {} : { skipped }),
|
|
@@ -3832,6 +4110,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
3832
4110
|
written,
|
|
3833
4111
|
requirements: reqBlock,
|
|
3834
4112
|
besideIdle: named,
|
|
4113
|
+
motionSeam,
|
|
3835
4114
|
};
|
|
3836
4115
|
}
|
|
3837
4116
|
|