@rig-c/parts 1.37.0 → 1.39.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 CHANGED
@@ -1766,7 +1766,7 @@ translate mix at 0.2 / 0.5 / 1): as drafted, no response on the translate, 1.382
1766
1766
 
1767
1767
  **What moves them.** Physics answers motion of the bones above it and the environment.
1768
1768
  The drafted idle keys `chest` (the breath) and the head chain, never `hip`, and the
1769
- recipe adds no key: under the idle alone a skirt's trees move only by the environment.
1769
+ recipe adds no key: under the idle alone a skirt's trees move only by the environment. Under rigc's default environment that motion is the physics reset at frame 0 dying out, and `check` says so by name: `CLOTH_SETTLES` (§6, issue #501) warns on each chain whose tip comes to rest before the idle's loop length — on `wind`, the skirt's eight trees and its swing, between 1.5 and 2.75 s of a 3.333 s idle.
1770
1770
  `rigc render` and `check` pose under spine-core's default, wind (1, 0) and gravity
1771
1771
  (0, 1), so on the fixture `check` reads `check: PASS`, `TEXTURE_STRETCH` 1.382,
1772
1772
  `TIP_OVER_ROOT` ratio 15.06 (root 0.095, tip 1.438 rig px), `CHAIN_LAG` PASS,
@@ -1780,6 +1780,68 @@ by up to 10.396 and 10.188, `skirt` 3.390 and 3.382, and `hair_front` 2.131 and
1780
1780
  px, either way alike; held three cycles each from a fresh track, 10.299 / 10.277,
1781
1781
  3.390 / 3.382 and 2.119 / 2.115.
1782
1782
 
1783
+ **A standing cloth is the consumer's to drive** (issue #502). The drafted idle keys no
1784
+ `hip`, and that is a measured decision, not an omission. A hip sway was tried on every
1785
+ public example: a `motion.tracks` entry `{ "bone": "hip", "prop": "rotate", "amp": a,
1786
+ "period": <the idle's>, "phase": 0 }` at the breathing period, a = 0.1°, 0.2° and 0.3°
1787
+ (this port's build of each, rig-c 2.60.0, `check`'s idle at 12 fps). Every sway keeps the
1788
+ chains it reaches above `CLOTH_SETTLES`'s floor, and every one turns a passing `check` red:
1789
+
1790
+ | example | chains no idle key drives | 0.1° | 0.2° and 0.3° | idle frames changed by 0.1° |
1791
+ | --- | --- | --- | --- | --- |
1792
+ | `demo` | 0 | `CHAIN_LAG` | `CHAIN_LAG`, `TIP_OVER_ROOT` | 11.91 % of pixels, up to 148 levels |
1793
+ | `scarf` | 0 | `CHAIN_LAG`, `TIP_OVER_ROOT` | the same | the framing box changes, 201 to 202 px wide |
1794
+ | `wind` | 9, the skirt | `CHAIN_LAG`, `BREATH_VISIBLE`, `STILL_REGIONS_DARK` | those and `TIP_OVER_ROOT` | 17.42 %, up to 203 |
1795
+ | `reference-sample` | 1, `hip_jiggle` | `CHAIN_LAG` | the same | 29.42 %, up to 121 |
1796
+ | `reference-demo` | 0 (no physics) | `CHAIN_LAG` | the same | 47.63 %, up to 222 |
1797
+
1798
+ Two of those are not the amplitude's to fix. `CHAIN_LAG` reads a lag modulo a quarter
1799
+ cycle, and the keyed chains under `hip` start at phases from the neck's 0.08 to the third
1800
+ skirt chain's 0.38: a span of 0.30 cycle on `demo`, `scarf`, `reference-sample` and
1801
+ `reference-demo`, so no phase of a hip turn at the idle's period has all of them lagging
1802
+ it. `wind`'s span, 0.08 to 0.29, admits phase 0.06, and there 0.1° still fails
1803
+ `BREATH_VISIBLE`, `STILL_REGIONS_DARK` and `TIP_OVER_ROOT`. And the legs hang from `hip`:
1804
+ a hip turn moves the feet (0.857 rig px at 0.1° on `wind`), which a standing character
1805
+ holds still. So a cloth that should move while the character stands is moved by the
1806
+ world, which the consumer sets when it plays the rig (rig-c's live track
1807
+ `setEnvironment`, rig-play's `environment()`). `rigc render --environment` reads the
1808
+ same world from a file, to look at the response before a player exists (rig-c's
1809
+ AUTHORING §8.4: each step holds from its `t`, x right and y down). A worked schedule for
1810
+ `wind`, tracked as `examples/wind/breeze.json`, is a breeze that changes every half
1811
+ second:
1812
+
1813
+ ```json
1814
+ {
1815
+ "spec": "rigc-environment/1",
1816
+ "steps": [
1817
+ { "t": 0, "wind": [0.4, 0] },
1818
+ { "t": 0.5, "wind": [1, 0] },
1819
+ { "t": 1, "wind": [0.2, 0] },
1820
+ { "t": 1.5, "wind": [0.8, 0] },
1821
+ { "t": 2, "wind": [0.3, 0] },
1822
+ { "t": 2.5, "wind": [1, 0] },
1823
+ { "t": 3, "wind": [0.4, 0] }
1824
+ ]
1825
+ }
1826
+ ```
1827
+
1828
+ ```bash
1829
+ rigc render --candidate out/check/build --animation idle --fps 12 --max 640 --geometry \
1830
+ --environment examples/wind/breeze.json --out out/breeze
1831
+ ```
1832
+
1833
+ Read by `CLOTH_SETTLES`'s own definition on the `geometry.json` that writes, none of the
1834
+ 16 chains settles. The nine that settle under the default world keep moving to the last
1835
+ frame: the skirt's eight trees end at 7.66 to 23.79 rig px/s, and the swing at 2.778
1836
+ (selftest `CH21` holds this). `check` itself always reads the default world, so `wind`'s
1837
+ `check.json` keeps its WARN. The WARN states what the rig does with nothing driving it;
1838
+ it is not a fault (`CH20` holds that the idle keys no hip and that the chains which
1839
+ settle are exactly the ones no idle key drives). A soft region with no wind response,
1840
+ such as `reference-sample`'s `hip_jiggle`, is not reached by an environment at all:
1841
+ `rigc render --environment` refuses the schedule by name, since no physics constraint
1842
+ answers wind. It answers the body, so what moves it is a consumer's animation that
1843
+ moves `hip`, such as a walk or a turn, not the idle.
1844
+
1783
1845
  **Not in this recipe.**
1784
1846
 
1785
1847
  - **Weights across neighbouring chains** (rig-c/core#1415's per-vertex weights, a vertex
@@ -2175,7 +2237,7 @@ move off it (`AS245`).
2175
2237
  | `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
2238
  | `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
2239
  | `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) |
2240
+ | `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), 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 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)` — 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) |
2179
2241
  | `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
2242
  | `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
2243
  | `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 +2955,8 @@ documents do not ship). Each is an input this package built; none is an author's
2893
2955
  | <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
2956
  | <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
2957
  | <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` |
2958
+ | <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 |
2959
+ | <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 |
2896
2960
  | <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
2961
  | <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
2962
  | <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 +3473,7 @@ it reads is there, and says SKIP with the reason when it is not:
3409
3473
  | `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
3474
  | `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
3475
  | `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 |
3476
+ | `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
3477
  | `RECOMPOSITE_HOLES` | `parts.json`'s `recomposite` block | no `parts.json`, or no block |
3413
3478
  | `SETUP_POSE_VS_SOURCE` | the rig spec and the painting | no `--source` |
3414
3479
 
package/docs/MIGRATION.md CHANGED
@@ -322,3 +322,43 @@ 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.
344
+
345
+ ## After 1.38.0: cloth that comes to rest on the idle
346
+
347
+ Issue #501 reads every physics chain by its tip on the idle `check` renders (physics on,
348
+ rigc's default environment): a chain settles at the first frame from which its tip moves
349
+ slower than 0.01694 rig px/s to the idle's last frame, and the line warns when one
350
+ settles before the loop's length is out. It is a warning, never a bar: cloth at rest
351
+ while the character stands may be intended, so no exit status, no `check: PASS` and no
352
+ bar count moves (AUTHORING §6, `CLOTH_SETTLES`).
353
+
354
+ | change | before | after |
355
+ | --- | --- | --- |
356
+ | `check` on a rig with a physics constraint and an idle | not read | `CLOTH_SETTLES` in `check.json` after `SEAM_OVER_MOTION` (where that is written; else where it would be), its console lines after the `SEAM_OVER_MOTION` lines, one `WARN chain` line per chain that settles, and the summary's warning clause names it: `; N warning(s) (CLOTH_SETTLES, not a bar)`, or `(SEAM_OVER_MOTION, CLOTH_SETTLES, not a bar)` with N their sum where both read |
357
+ | `check` on any other rig | — | one console line, `CLOTH_SETTLES: SKIP — …`, naming why; no key in `check.json` |
358
+ | the public examples' `expected/check.json` | — | `demo`, `reference-sample`, `scarf` and `wind` gain the block: `wind`'s skirt (eight trees and the swing) and `reference-sample`'s `hip_jiggle` settle, every chain an idle key drives does not |
359
+
360
+ What to do: nothing. A warning names the chain and the second it comes to rest; a chain
361
+ that should keep moving wants an environment at render (`rigc render --environment`;
362
+ AUTHORING §3, *What moves them*, has a worked schedule for `wind`) or a consumer's
363
+ animation that moves the bone it hangs from. The drafted idle keys no hip: issue #502
364
+ measured a hip sway at the idle's period on every public example, and each failed a bar.
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`), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `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.37.0",
3
+ "version": "1.39.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.59.0"
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, CLOTH_LINE, type ClothLine, clothWarnText, 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,49 @@ 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
+
1509
+ /**
1510
+ * The cloth reading (issue #501) as the console prints it: the status, what was read, the floor, the levers and that it
1511
+ * is not a bar, then one WARN line per chain that settles. A SKIP prints its reason alone. `silent` is the mutant that
1512
+ * removes the reading's lines.
1513
+ */
1514
+ export function clothLines(line: ClothLine, plant: { silent?: boolean } = {}): string[] {
1515
+ if (plant.silent === true) return [];
1516
+ if (line.status === 'SKIP') return [`${CLOTH_LINE}: SKIP — ${String(line.reason)}`];
1517
+ const read = `${line.rows.length} physics chain(s) read by their tip on the idle as rendered (${line.frames} frame(s) at ${line.fps} fps over ${line.duration} s, physics on)`;
1518
+ return [
1519
+ `${CLOTH_LINE}: ${line.status} — ${read}: ${line.warnings} of them settle before the loop's length — ${line.warnings} warning(s), never a bar; ${line.floor}; ${line.levers}`,
1520
+ ...line.rows.filter((r) => r.settles_at !== null).map((r) => clothWarnText(r, line.duration)),
1521
+ ];
1522
+ }
1523
+
1524
+ /** The summary's warning clause: each warning line that read something, named, with the warnings they printed together; empty when none read. */
1525
+ export function warningsClause(r: Pick<CheckReport, 'motionSeam' | 'cloth'>): string {
1526
+ const read = [...(r.motionSeam.status === 'SKIP' ? [] : [{ name: MOTION_SEAM_LINE, n: r.motionSeam.warnings }]), ...(r.cloth.status === 'SKIP' ? [] : [{ name: CLOTH_LINE, n: r.cloth.warnings }])];
1527
+ if (read.length === 0) return '';
1528
+ return `; ${read.reduce((a, x) => a + x.n, 0)} warning(s) (${read.map((x) => x.name).join(', ')}, not a bar)`;
1529
+ }
1530
+
1488
1531
  /** 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
1532
  export function requirementText(name: string, line: RequirementLine): string {
1490
1533
  const rest = Object.entries(line).filter(([k]) => k !== 'status' && k !== 'reason');
@@ -1543,6 +1586,8 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
1543
1586
  if (limbs !== undefined) for (const [i, l] of limbCheckLines(limbs).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
1544
1587
  const sweep = fig[SWEEP_LINE];
1545
1588
  if (sweep !== undefined) for (const [i, l] of sweepCheckLines(sweep).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
1589
+ for (const [i, l] of motionSeamLines(r.motionSeam).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
1590
+ for (const [i, l] of clothLines(r.cloth).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
1546
1591
  for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
1547
1592
  if (fig.sets !== undefined) {
1548
1593
  for (const [setName, setFig] of Object.entries(fig.sets)) {
@@ -1560,7 +1605,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
1560
1605
  for (const p of r.problems) log(failLine(p));
1561
1606
  const reqNotPass = r.requirements === null ? 0 : r.requirements.summary.declared - r.requirements.summary.pass;
1562
1607
  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)}`);
1608
+ log(`${fig.PASS ? 'check: PASS' : `check: FAIL — ${notMet}`}; ${barsSummary(r)}${warningsClause(r)}`);
1564
1609
  return r;
1565
1610
  }
1566
1611
 
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,9 @@ 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;
1867
+ /** Written only when the rig declares a physics constraint and has an idle (issue #501): {@link ClothLine}, a warning, never a bar. */
1868
+ [CLOTH_LINE]?: ClothLine;
1866
1869
  RECOMPOSITE_HOLES: ReportedLine;
1867
1870
  /** Written only under `--source` (issue #77): the setup pose against the painting, REPORTED. */
1868
1871
  SETUP_POSE_VS_SOURCE?: ReportedLine;
@@ -2125,6 +2128,10 @@ export interface CheckReport {
2125
2128
  * reads the idle. Empty when the idle is alone.
2126
2129
  */
2127
2130
  besideIdle: string[];
2131
+ /** `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. */
2132
+ motionSeam: MotionSeamLine;
2133
+ /** `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. */
2134
+ cloth: ClothLine;
2128
2135
  }
2129
2136
 
2130
2137
  /**
@@ -3154,6 +3161,408 @@ export function sweepLine(
3154
3161
  };
3155
3162
  }
3156
3163
 
3164
+ // ---------------------------------------------------------------------------
3165
+ // the seam over every animation (issue #493; the line is {@link MOTION_SEAM_LINE}'s)
3166
+ // ---------------------------------------------------------------------------
3167
+
3168
+ /**
3169
+ * `SEAM_OVER_MOTION` (issue #493): every declared neighbour pair read in every
3170
+ * animation `motion.json` holds, the idle included, by rig-c's seam reading
3171
+ * (`rigc render --seam`, rig-c 2.60.0) — a **warning, never a bar**. The
3172
+ * owner's ruling (2026-10-18): a seam opening in motion may be intentional, so
3173
+ * the line names the pair, the animation, the frames and the cells, and moves
3174
+ * neither the exit status nor `check: PASS`; the summary counts it apart from
3175
+ * the bars.
3176
+ *
3177
+ * The pairs are the ones the inputs declare and nothing else: each limb's
3178
+ * consecutive segments in `parts.json` (`config.limbs`, the upper and the
3179
+ * segment below each joint) and, under `--requirements`, each `seam`
3180
+ * requirement's part and neighbour. None declared, the line is a SKIP naming
3181
+ * why, never a pass, and `check.json` carries no key for it.
3182
+ *
3183
+ * **The floor** ({@link openingOverFloor}): a frame's gap or ghost is reported
3184
+ * when it holds more cells than its box is long — when it is thicker than one
3185
+ * cell. rigc counts a cell drawn when the renderer paints its centre at half
3186
+ * opacity or more, so a boundary that moves by less than a cell flips the
3187
+ * cells along it in or out: a line of cells one deep, scattered or joined,
3188
+ * holds at most as many cells as its box's longer side. A hole of any area
3189
+ * holds more. The rule types no number; it is read off rigc's own cell and
3190
+ * box, and measured, not chosen: on `examples/wind`'s idle rigc names 43
3191
+ * openings over its six limb pairs (28 and 15 frames of the two elbows, 1 to
3192
+ * 5 cells, boxes up to 17 cells long) and every one is one cell thick; on
3193
+ * rig-c's `gallery/walk` its 13 joint cracks of 1 to 3 cells are too; the
3194
+ * ghost rig-c's own reading names on a skirt swung clear of a leg (1564 cells
3195
+ * in a box 92 long) is far over it.
3196
+ */
3197
+ export const MOTION_SEAM_LINE = 'SEAM_OVER_MOTION';
3198
+
3199
+ /** The seam reading's scratch build and renders, under `--out`; removed before the stage returns. */
3200
+ const MOTION_SEAM_SCRATCH = '_seam_motion';
3201
+
3202
+ /** rig-c's seam reading: the file it writes in each set's directory, and its format tag. */
3203
+ export const SEAM_READING_FILE = 'seam.json';
3204
+ export const SEAM_READING_SPEC = 'rigc-seam/1';
3205
+
3206
+ /** The floor as the console and `check.json` state it. */
3207
+ 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";
3208
+
3209
+ /** One declared pair: the two slots, and every declaration that names it. */
3210
+ export interface MotionSeamPair {
3211
+ /** `"<a>|<b>"`, the two slots in the order declared first. */
3212
+ pair: string;
3213
+ a: string;
3214
+ b: string;
3215
+ declared: string[];
3216
+ /** Why the pair is not read: a slot `rig.json` does not have. */
3217
+ unread?: string;
3218
+ }
3219
+
3220
+ /** One pair in one animation, as the floor reads rigc's file. */
3221
+ export interface MotionSeamRow {
3222
+ pair: string;
3223
+ animation: string;
3224
+ /** rigc's: `skip` when no frame has a band to read — nothing measured, not a seam that held. */
3225
+ status: 'read' | 'skip';
3226
+ /** Frames both slots draw, and of those the frames read (rigc's `drawn`, `banded`). */
3227
+ drawn: number;
3228
+ banded: number;
3229
+ /** The frames over the floor, ascending: the warning's frames. */
3230
+ opens: number[];
3231
+ /** Beside `opens`, frame by frame: the most cells of either kind on that frame. */
3232
+ px: number[];
3233
+ /** The most cells of either kind on any frame over the floor; 0 when none is. */
3234
+ max_px: number;
3235
+ /** The frames rigc names open whose every opening is one cell thick: under the floor, not reported. */
3236
+ under_floor: number[];
3237
+ }
3238
+
3239
+ /** The line: `WARN` with at least one warning, `CLEAR` with none, `SKIP` with nothing declared or nothing to read. */
3240
+ export interface MotionSeamLine {
3241
+ status: 'WARN' | 'CLEAR' | 'SKIP';
3242
+ reason?: string;
3243
+ fps: number;
3244
+ floor: string;
3245
+ pairs: MotionSeamPair[];
3246
+ animations: string[];
3247
+ /** Rows with a frame over the floor: the WARN lines. */
3248
+ warnings: number;
3249
+ rows: MotionSeamRow[];
3250
+ }
3251
+
3252
+ /**
3253
+ * The neighbour pairs the inputs declare, in their order: each limb's
3254
+ * consecutive segments (`parts.json`'s limb records, by segment), then each
3255
+ * `seam` requirement's part and neighbour. A pair named twice, in either
3256
+ * order, is one pair with both declarations.
3257
+ */
3258
+ export function motionSeamPairs(parts: PartsFile, seams: ReadonlyArray<{ name: string; part: string; neighbour: string }>): MotionSeamPair[] {
3259
+ const out: MotionSeamPair[] = [];
3260
+ const add = (a: string, b: string, declared: string): void => {
3261
+ const hit = out.find((p) => (p.a === a && p.b === b) || (p.a === b && p.b === a));
3262
+ if (hit !== undefined) hit.declared.push(declared);
3263
+ else out.push({ pair: `${a}|${b}`, a, b, declared: [declared] });
3264
+ };
3265
+ const limbs: string[] = [];
3266
+ for (const p of parts.parts) if (p.limb !== undefined && !limbs.includes(p.limb.name)) limbs.push(p.limb.name);
3267
+ for (const limb of limbs) {
3268
+ const segs = parts.parts.filter((p) => p.limb?.name === limb).sort((x, y) => (x.limb as LimbRecord).segment - (y.limb as LimbRecord).segment);
3269
+ for (let i = 1; i < segs.length; i++) {
3270
+ const lower = segs[i].limb as LimbRecord;
3271
+ add(segs[i - 1].name, segs[i].name, `config.limbs.${limb} segment ${lower.segment}${lower.joint === undefined ? '' : `, joint "${lower.joint}"`}`);
3272
+ }
3273
+ }
3274
+ for (const r of seams) add(r.part, r.neighbour, `requirement "${r.name}" (seam)`);
3275
+ return out;
3276
+ }
3277
+
3278
+ /** A frame's opening of one kind over the floor ({@link MOTION_SEAM_FLOOR}): more cells than its box, in cells, is long. */
3279
+ export function openingOverFloor(hit: { px: number; cell: number; box: readonly [number, number, number, number] }): boolean {
3280
+ const long = Math.round(Math.max(hit.box[2] - hit.box[0], hit.box[3] - hit.box[1]) / hit.cell);
3281
+ return hit.px > long;
3282
+ }
3283
+
3284
+ /** Frame indices as runs, `[0-6, 8, 13]`: the form rigc prints a frame list in. */
3285
+ export function frameRuns(frames: readonly number[]): string {
3286
+ const runs: string[] = [];
3287
+ for (let i = 0; i < frames.length; ) {
3288
+ let j = i;
3289
+ while (j + 1 < frames.length && frames[j + 1] === frames[j] + 1) j++;
3290
+ runs.push(i === j ? String(frames[i]) : `${frames[i]}-${frames[j]}`);
3291
+ i = j + 1;
3292
+ }
3293
+ return `[${runs.join(', ')}]`;
3294
+ }
3295
+
3296
+ /**
3297
+ * A planted defect, for the suite's mutants only (`SO` controls in `selftest.ts`). Every caller in `src/` passes none.
3298
+ */
3299
+ export interface MotionSeamPlant {
3300
+ /** Report every frame rigc names open, the one-cell cracks included: the floor removed. */
3301
+ noFloor?: true;
3302
+ /** Print the line's head and none of its WARN lines: the warning suppressed. */
3303
+ silent?: true;
3304
+ }
3305
+
3306
+ type SeamHitRead = { index: number; px: number; cell: number; box: [number, number, number, number] };
3307
+
3308
+ /**
3309
+ * The rows of one `rigc render --seam` run over every animation: `frames.json`
3310
+ * names each set's directory, each `seam.json` (`rigc-seam/1`) its pairs in
3311
+ * the flag's order. Refuses (`FRAMES_SIDECAR`), every problem at once, a set or
3312
+ * file that is missing, a file that is not the animation's or not at the rate
3313
+ * asked, a pair out of the flag's order, and a field of the wrong shape.
3314
+ */
3315
+ export function readMotionSeams(dir: string, animations: readonly string[], fps: number, pairs: readonly MotionSeamPair[], plant: MotionSeamPlant = {}): MotionSeamRow[] {
3316
+ const problems: Problem[] = [];
3317
+ const bad = (object: string, detail: string): void => {
3318
+ problems.push({ code: 'FRAMES_SIDECAR', object, detail });
3319
+ };
3320
+ const rows: MotionSeamRow[] = [];
3321
+ const sidecar = readJson(join(dir, 'frames.json'), problems);
3322
+ const sets = sidecar !== null && Array.isArray(sidecar.sets) ? sidecar.sets.filter(isRecord) : [];
3323
+ const nums = (v: unknown): v is number[] => Array.isArray(v) && v.every((x) => typeof x === 'number');
3324
+ const hitOf = (h: unknown): SeamHitRead | null =>
3325
+ 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;
3326
+ for (const animation of animations) {
3327
+ const set = sets.find((x) => x.animation === animation);
3328
+ if (set === undefined || typeof set.dir !== 'string') {
3329
+ bad(join(dir, 'frames.json'), `names no frame set for animation ${JSON.stringify(animation)}; \`rigc render --seam\` over the build writes one per animation`);
3330
+ continue;
3331
+ }
3332
+ const path = join(dir, set.dir, SEAM_READING_FILE);
3333
+ const file = readJson(path, problems);
3334
+ if (file === null) continue;
3335
+ if (file.spec !== SEAM_READING_SPEC) bad(path, `spec is ${JSON.stringify(file.spec)}; "${SEAM_READING_SPEC}" is required`);
3336
+ if (file.animation !== animation) bad(path, `animation is ${JSON.stringify(file.animation)}; ${JSON.stringify(animation)} is required`);
3337
+ if (file.fps !== fps) bad(path, `fps is ${JSON.stringify(file.fps)}; ${fps}, the rate asked for, is required`);
3338
+ const read = Array.isArray(file.pairs) ? file.pairs : [];
3339
+ if (read.length !== pairs.length) bad(path, `holds ${read.length} pair(s); the ${pairs.length} asked for, in the flag's order, are required`);
3340
+ pairs.forEach((want, i) => {
3341
+ const p = read[i];
3342
+ const at = `${path} pairs[${i}]`;
3343
+ if (!isRecord(p) || p.a !== want.a || p.b !== want.b) {
3344
+ bad(at, `is ${JSON.stringify(isRecord(p) ? `${String(p.a)}|${String(p.b)}` : p)}; "${want.pair}", the flag's entry ${i}, is required`);
3345
+ return;
3346
+ }
3347
+ if ((p.status !== 'read' && p.status !== 'skip') || typeof p.drawn !== 'number' || typeof p.banded !== 'number' || !nums(p.opens)) {
3348
+ bad(at, 'is not { a, b, drawn, banded, status, opens, maxPx, gap, ghost } as rigc-seam/1 writes it');
3349
+ return;
3350
+ }
3351
+ const over = new Map<number, number>();
3352
+ for (const kind of ['gap', 'ghost'] as const) {
3353
+ const k = p[kind];
3354
+ const frames = isRecord(k) && Array.isArray(k.frames) ? k.frames.map(hitOf) : null;
3355
+ if (frames === null || frames.some((h) => h === null)) {
3356
+ bad(`${at} ${kind}`, 'is not { frames: [{ index, px, cell, box }…], spans, worst } as rigc-seam/1 writes it');
3357
+ return;
3358
+ }
3359
+ 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));
3360
+ }
3361
+ const opens = [...over.keys()].sort((x, y) => x - y);
3362
+ rows.push({
3363
+ pair: want.pair,
3364
+ animation,
3365
+ status: p.status,
3366
+ drawn: p.drawn,
3367
+ banded: p.banded,
3368
+ opens,
3369
+ px: opens.map((f) => over.get(f) ?? 0),
3370
+ max_px: opens.reduce((m, f) => Math.max(m, over.get(f) ?? 0), 0),
3371
+ under_floor: p.opens.filter((f) => !over.has(f)),
3372
+ });
3373
+ });
3374
+ }
3375
+ refuseIfAny(problems);
3376
+ return rows;
3377
+ }
3378
+
3379
+ /** 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. */
3380
+ export function motionSeamLine(pairs: MotionSeamPair[], animations: string[], fps: number, rows: MotionSeamRow[], reason?: string): MotionSeamLine {
3381
+ const warnings = rows.filter((r) => r.opens.length > 0).length;
3382
+ const read = rows.some((r) => r.status === 'read');
3383
+ const skipWhy =
3384
+ reason ??
3385
+ (read
3386
+ ? undefined
3387
+ : pairs.every((p) => p.unread !== undefined)
3388
+ ? `no declared pair is read: ${pairs.map((p) => `"${p.pair}" ${String(p.unread)}`).join('; ')}`
3389
+ : `rigc read no band on any frame of any declared pair in any animation — nothing measured, which is not a seam that held`);
3390
+ return {
3391
+ status: skipWhy !== undefined ? 'SKIP' : warnings > 0 ? 'WARN' : 'CLEAR',
3392
+ ...(skipWhy === undefined ? {} : { reason: skipWhy }),
3393
+ fps,
3394
+ floor: MOTION_SEAM_FLOOR,
3395
+ pairs,
3396
+ animations,
3397
+ warnings,
3398
+ rows,
3399
+ };
3400
+ }
3401
+
3402
+ /** One warning as the console prints it (issue #493's form, the floor's clause after it). */
3403
+ export function motionSeamWarnText(r: MotionSeamRow): string {
3404
+ 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`}`;
3405
+ }
3406
+
3407
+ // ---------------------------------------------------------------------------
3408
+ // cloth that comes to rest on the idle (issue #501; the line is {@link CLOTH_LINE}'s)
3409
+ // ---------------------------------------------------------------------------
3410
+
3411
+ /**
3412
+ * `CLOTH_SETTLES` (issue #501): every physics chain read by its tip on the
3413
+ * idle as `check` renders it (physics on, rigc's default environment) — a
3414
+ * **warning, never a bar**. The owner's ruling: cloth at rest while the
3415
+ * character stands can be intended, so the line names the chain, the time its
3416
+ * tip comes to rest and the floor, and moves neither the exit status nor
3417
+ * `check: PASS`; the summary counts it apart from the bars.
3418
+ *
3419
+ * Why it exists: `CHAIN_LAG` reads the idle's keyed chains off `motion.json`
3420
+ * and `TIP_OVER_ROOT` the idle's posed parts, and neither reads what a
3421
+ * physics constraint does with a chain nothing above it moves. A skirt hung
3422
+ * from `hip` under an idle that keys only the breath is pushed by rigc's
3423
+ * reset at frame 0 and then comes to rest, and every bar passes.
3424
+ *
3425
+ * A chain is a physics leaf — a bone carrying a physics constraint (rig.json's
3426
+ * `constraints` or motion.json's `physics`) with no such bone under it — and
3427
+ * the physics bones above it, as far as the run of them goes. Its tip is the
3428
+ * leaf's end (origin plus `length` along its world x axis), or its origin when
3429
+ * the leaf has no length. Its speed between two frames is the tip's distance
3430
+ * over their time; it **settles** at the first frame from which every speed
3431
+ * to the idle's last frame is under {@link CLOTH_SETTLE_FLOOR}, and the line
3432
+ * warns when one does — that is, before the loop's length is out.
3433
+ */
3434
+ export const CLOTH_LINE = 'CLOTH_SETTLES';
3435
+
3436
+ /**
3437
+ * The floor, rig px per second — half the slowest tip a keyed bone drives on
3438
+ * the public examples, the repository's floor rule (half the weaker figure).
3439
+ * Measured for issue #501 with this port's build of each example (rig-c
3440
+ * 2.60.0, `check`'s idle at 12 fps, physics on): over the 31 physics chains an
3441
+ * idle key drives — a keyed bone at or above the leaf (`demo` 12,
3442
+ * `reference-sample` 1, `scarf` 11, `wind` 7) — the slowest frame-to-frame tip
3443
+ * speed is `scarf`'s `skirt_l2`, 0.03388; the ten no key drives (`wind`'s
3444
+ * skirt, its eight trees and its swing, and `reference-sample`'s `hip_jiggle`)
3445
+ * fall under half of it, from 0 s (`hip_jiggle`, which never moves) to 2.75 s
3446
+ * on a 3.333 s idle, and end at 0.0025 rig px/s or less. `reference-demo`
3447
+ * declares no physics constraint.
3448
+ */
3449
+ export const CLOTH_SETTLE_FLOOR = 0.01694;
3450
+
3451
+ /** The floor as the console and `check.json` state it. */
3452
+ export const CLOTH_FLOOR_TEXT = `a chain settles at the first frame from which its tip moves slower than ${CLOTH_SETTLE_FLOOR} rig px/s to the idle's last frame — half the slowest tip an idle key drives on the public examples (scarf's skirt_l2, 0.03388 rig px/s)`;
3453
+
3454
+ /** The consumer's two levers, as the console and `check.json` state them. */
3455
+ export const CLOTH_LEVERS_TEXT = "a chain at rest moves again when the world or the body does: an environment at render (rigc render --environment, wind and gravity in time; AUTHORING §3, What moves them, has a worked schedule) or a consumer's animation that moves the bone it hangs from; the drafted idle keys no hip (rig-c/parts#502)";
3456
+
3457
+ /** One physics chain as the line reads it. */
3458
+ export interface ClothRow {
3459
+ /** The leaf: the physics bone with no physics bone under it. */
3460
+ tip: string;
3461
+ /** The topmost physics bone of the run the leaf ends. */
3462
+ root: string;
3463
+ /** Physics bones from root to tip. */
3464
+ links: number;
3465
+ /** What was tracked: the leaf's end, or its origin when it has no `length`. */
3466
+ point: 'end' | 'origin';
3467
+ /** The time of the frame from which the tip stays under the floor; null when it never does. */
3468
+ settles_at: number | null;
3469
+ /** The fastest and slowest frame-to-frame speeds of the point, rig px/s, four significant figures. */
3470
+ max_rig_px_per_s: number;
3471
+ min_rig_px_per_s: number;
3472
+ }
3473
+
3474
+ /** The line: `WARN` with a chain that settles, `CLEAR` with none, `SKIP` with no chain or no idle. */
3475
+ export interface ClothLine {
3476
+ status: 'WARN' | 'CLEAR' | 'SKIP';
3477
+ reason?: string;
3478
+ fps: number;
3479
+ frames: number;
3480
+ duration: number;
3481
+ floor_rig_px_per_s: number;
3482
+ floor: string;
3483
+ levers: string;
3484
+ /** Rows that settle: the WARN lines. */
3485
+ warnings: number;
3486
+ rows: ClothRow[];
3487
+ }
3488
+
3489
+ /**
3490
+ * The physics chains of a rig, leaf by leaf in rig.json's bone order: each
3491
+ * bone a physics constraint names with no such bone under it, and how far up
3492
+ * the run of physics bones above it goes.
3493
+ */
3494
+ export function clothChains(rig: Record<string, unknown>, physics: readonly LoopPhysics[]): Array<{ tip: string; root: string; links: number; length: number }> {
3495
+ const bones = (Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord);
3496
+ const parent = new Map(bones.map((b) => [String(b.name), typeof b.parent === 'string' ? b.parent : null]));
3497
+ const length = new Map(bones.map((b) => [String(b.name), typeof b.length === 'number' && Number.isFinite(b.length) ? b.length : 0]));
3498
+ const held = new Set(physics.map((p) => p.bone));
3499
+ const above = (b: string, a: string): boolean => {
3500
+ const seen = new Set<string>();
3501
+ for (let p = parent.get(b) ?? null; p !== null && !seen.has(p); p = parent.get(p) ?? null) {
3502
+ if (p === a) return true;
3503
+ seen.add(p);
3504
+ }
3505
+ return false;
3506
+ };
3507
+ const out: Array<{ tip: string; root: string; links: number; length: number }> = [];
3508
+ for (const b of bones.map((x) => String(x.name))) {
3509
+ if (!held.has(b) || [...held].some((o) => o !== b && above(o, b))) continue;
3510
+ let root = b;
3511
+ let links = 1;
3512
+ for (let p = parent.get(b) ?? null; p !== null && held.has(p); p = parent.get(p) ?? null) {
3513
+ root = p;
3514
+ links++;
3515
+ }
3516
+ out.push({ tip: b, root, links, length: length.get(b) ?? 0 });
3517
+ }
3518
+ return out;
3519
+ }
3520
+
3521
+ /** One chain's row off its leaf's bone track: the point, the speeds, and when it settles under the floor. */
3522
+ export function clothRow(chain: { tip: string; root: string; links: number; length: number }, track: BoneTrack, floor: number = CLOTH_SETTLE_FLOOR): ClothRow {
3523
+ const L = chain.length;
3524
+ const pts = track.frames.map((w) => [w.worldX + L * w.a, w.worldY + L * w.c]);
3525
+ const speeds: number[] = [];
3526
+ for (let i = 1; i < pts.length; i++) {
3527
+ const dt = track.times[i] - track.times[i - 1];
3528
+ speeds.push(dt > 0 ? edgeLength(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]) / dt : Infinity);
3529
+ }
3530
+ let s = speeds.length;
3531
+ while (s > 0 && speeds[s - 1] < floor) s--;
3532
+ return {
3533
+ tip: chain.tip,
3534
+ root: chain.root,
3535
+ links: chain.links,
3536
+ point: L > 0 ? 'end' : 'origin',
3537
+ settles_at: speeds.length > 0 && s < speeds.length ? round3(track.times[s]) : null,
3538
+ max_rig_px_per_s: speeds.length === 0 ? 0 : sig4(Math.max(...speeds)),
3539
+ min_rig_px_per_s: speeds.length === 0 ? 0 : sig4(Math.min(...speeds)),
3540
+ };
3541
+ }
3542
+
3543
+ /** The line from its rows: WARN when a chain settles, CLEAR when none does, SKIP with the reason when nothing was read. */
3544
+ export function clothLine(rows: ClothRow[], fps: number, frames: number, duration: number, reason?: string): ClothLine {
3545
+ const warnings = rows.filter((r) => r.settles_at !== null).length;
3546
+ return {
3547
+ status: reason !== undefined ? 'SKIP' : warnings > 0 ? 'WARN' : 'CLEAR',
3548
+ ...(reason === undefined ? {} : { reason }),
3549
+ fps,
3550
+ frames,
3551
+ duration: round3(duration),
3552
+ floor_rig_px_per_s: CLOTH_SETTLE_FLOOR,
3553
+ floor: CLOTH_FLOOR_TEXT,
3554
+ levers: CLOTH_LEVERS_TEXT,
3555
+ warnings,
3556
+ rows,
3557
+ };
3558
+ }
3559
+
3560
+ /** One warning as the console prints it. */
3561
+ export function clothWarnText(r: ClothRow, duration: number): string {
3562
+ const chain = r.links === 1 ? `"${r.tip}"` : `"${r.root}" .. "${r.tip}"`;
3563
+ return `WARN chain ${chain} (${r.links} physics link(s)) settles at ${r.settles_at ?? ''} s of the ${round3(duration)} s idle — its tip's ${r.point} under ${CLOTH_SETTLE_FLOOR} rig px/s from there to the last frame (fastest on the idle ${r.max_rig_px_per_s} rig px/s)`;
3564
+ }
3565
+
3157
3566
  /**
3158
3567
  * One `CHECK_RIGC_GREEN` per line that says why `rigc <what>` was not green:
3159
3568
  * its FAIL and compile-error lines, or, when there are none, {@link causeLines}.
@@ -3465,11 +3874,13 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
3465
3874
  let chain: JudgementLine = skip(inp.noIdle ?? '');
3466
3875
  let stretch: JudgementLine = skip(inp.noIdle ?? '');
3467
3876
  let still: JudgementLine = skip(lacking);
3877
+ let idleGeometry: IdleGeometry | null = null;
3468
3878
  if (idle !== null) {
3469
3879
  chain = chainLine(inp, problems, reqFile === null ? NO_CHAIN_RESTS : { path: reqFile.path, rests: reqFile.rests });
3470
3880
  // One reader for the one geometry file: the stretch reads its meshes, the still regions every slot and the head bone.
3471
3881
  const geometryPath = join(idle.dir, GEOMETRY_FILE);
3472
3882
  const geometry = readGeometry(geometryPath, idle.written);
3883
+ idleGeometry = geometry;
3473
3884
  stretch = stretchLine(geometry, geometryPath, problems, inp);
3474
3885
  if (full !== null) still = stillLine(full, idle, geometry, geometryPath, problems, notPlanted);
3475
3886
  }
@@ -3570,6 +3981,57 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
3570
3981
  }
3571
3982
  }
3572
3983
 
3984
+ // 3e. every declared neighbour pair read in every animation by rigc's seam reading (issue #493): a warning, never a
3985
+ // bar — the build as played, at the idle's rate; SKIP, named, with no pair declared or no animation to read
3986
+ let motionSeam: MotionSeamLine;
3987
+ {
3988
+ const fps = IDLE_FPS;
3989
+ 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 }] : []));
3990
+ const pairs = inp.parts === null ? [] : motionSeamPairs(inp.parts, seamReqs);
3991
+ const slotNames = new Set((Array.isArray(inp.rig.slots) ? inp.rig.slots : []).filter(isRecord).map((sl) => String(sl.name)));
3992
+ for (const p of pairs) {
3993
+ const missing = [p.a, p.b].filter((n) => !slotNames.has(n));
3994
+ if (missing.length > 0) p.unread = `names ${missing.map((n) => `slot "${n}"`).join(' and ')}, which rig.json does not have`;
3995
+ }
3996
+ const read = pairs.filter((p) => p.unread === undefined);
3997
+ if (inp.parts === null) motionSeam = motionSeamLine([], animationNames, fps, [], inp.noParts ?? 'no parts.json');
3998
+ 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');
3999
+ else if (animationNames.length === 0) motionSeam = motionSeamLine(pairs, [], fps, [], 'motion.json holds no animation to read the declared pairs in');
4000
+ else if (read.length === 0) motionSeam = motionSeamLine(pairs, animationNames, fps, []);
4001
+ else {
4002
+ const seamMotionDir = join(out, MOTION_SEAM_SCRATCH);
4003
+ rmSync(seamMotionDir, { recursive: true, force: true });
4004
+ try {
4005
+ const list = read.map((p) => `${p.a}:${p.b}`).join(',');
4006
+ const r = rigc(['render', '--candidate', buildDir, '--seam', list, '--fps', String(fps), '--max', String(FOLD_RENDER_MAX), '--out', seamMotionDir]);
4007
+ if (r.status !== 0) refuseIfAny(rigcFailed(`render --seam ${list} --fps ${fps}`, r, r.out.split('\n').filter((l) => l.includes('FAIL') || l.includes('rigc:'))));
4008
+ motionSeam = motionSeamLine(pairs, animationNames, fps, readMotionSeams(seamMotionDir, animationNames, fps, read));
4009
+ } finally {
4010
+ rmSync(seamMotionDir, { recursive: true, force: true });
4011
+ }
4012
+ }
4013
+ }
4014
+
4015
+ // 3f. every physics chain read by its tip on the idle as rendered above, physics on (issue #501): a warning, never a
4016
+ // bar; SKIP, named, with no idle or no physics constraint
4017
+ let cloth: ClothLine;
4018
+ if (idle === null) cloth = clothLine([], IDLE_FPS, 0, 0, inp.noIdle ?? 'no idle');
4019
+ else if (stripped === null) cloth = clothLine([], idle.fps, idle.frames.length, idle.duration, "the rig declares no physics constraint (rig.json's constraints, motion.json's physics), so there is no chain to read");
4020
+ else {
4021
+ const geometryPath = join(idle.dir, GEOMETRY_FILE);
4022
+ const rows: ClothRow[] = [];
4023
+ let unread: string | undefined;
4024
+ for (const c of clothChains(inp.rig, stripped.physics)) {
4025
+ const track = boneTrackOf(idleGeometry, geometryPath, idle, c.tip);
4026
+ if (typeof track === 'string') {
4027
+ unread = track;
4028
+ break;
4029
+ }
4030
+ rows.push(clothRow(c, track));
4031
+ }
4032
+ cloth = unread === undefined ? clothLine(rows, idle.fps, idle.frames.length, idle.duration) : clothLine([], idle.fps, idle.frames.length, idle.duration, unread);
4033
+ }
4034
+
3573
4035
  // 4. the setup pose as a one-key throwaway animation — the seam reads it against the parts, the source line against
3574
4036
  // the painting — and, beside it, the same pose with the eyes shut
3575
4037
  let seam: SeamFigures | null = null;
@@ -3749,6 +4211,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
3749
4211
  ...(stress === null ? {} : { [STRESS_LINE]: stress }),
3750
4212
  ...(limbs === null ? {} : { [LIMB_LINE]: limbs }),
3751
4213
  ...(sweep === null ? {} : { [SWEEP_LINE]: sweep }),
4214
+ ...(motionSeam.pairs.length === 0 ? {} : { [MOTION_SEAM_LINE]: motionSeam }),
4215
+ ...(stripped === null || idle === null ? {} : { [CLOTH_LINE]: cloth }),
3752
4216
  RECOMPOSITE_HOLES: inp.parts === null ? { status: 'SKIP', reason: inp.noParts ?? '' } : holesLine(inp.parts),
3753
4217
  ...(sourceRead === null ? {} : { [SOURCE_LINE]: sourceRead }),
3754
4218
  ...(Object.keys(skipped).length === 0 ? {} : { skipped }),
@@ -3832,6 +4296,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
3832
4296
  written,
3833
4297
  requirements: reqBlock,
3834
4298
  besideIdle: named,
4299
+ motionSeam,
4300
+ cloth,
3835
4301
  };
3836
4302
  }
3837
4303