rig-parts 2.5.0 → 2.6.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 +57 -4
- package/package.json +1 -1
- package/src/check.ts +176 -20
package/docs/AUTHORING.md
CHANGED
|
@@ -3274,7 +3274,7 @@ step-up method below.
|
|
|
3274
3274
|
| `motion.duration` and `track.period` | `check`'s 12 fps grid (`1/12` s per frame) | Choose `N` frames (e.g. `N = 40`, `duration = 40 / 12 = 3.3333333333333335` s, or `N = 48`, `duration = 4.0` s) and set every body track's `period = duration`. `CHECK_LOOP_LAST_FRAME_AT_DURATION` requires `duration` to land on a `1/12` s tick, and `CONFIG_PERIOD_DIVIDES_DURATION` requires `duration / period` to be an integer. |
|
|
3275
3275
|
| Torso breath (`chest` `translatey`, `scalex`) | Figure height `H` (`parts.json` union height, kept near `1,100..1,200` rig px via `assemble.rig_scale`), torso span `L_torso = hypot(hip.x - neck.x, hip.y - neck.y)` from `bones`, and `BREATH_VISIBLE`'s `torso_heat_mean` vs `torso_floor` (pictured in `motion_heat.png`) | Floor: peak-to-peak vertical travel at `1.25 %` of `L_torso` (`amp = round2(0.00625 * L_torso)`, `base = amp`, `phase = 0.0`, i.e. `amp = 1.2` rig px on `demo`), paired with `chest` `scalex` `amp = 0.008`, `base = 1.008`, `phase = 0.0`. `check` scales the reference floor (`2.13` at `render_scale = 0.4211`) by `render_scale / 0.4211` and isolates the breath by posing only `chest`/`hip` `translate` and `scale` tracks. |
|
|
3276
3276
|
| Upper-body sway (`chest` → `neck` → `head` `rotate`) | Shoulder lever arm `R_shoulder = \|sleeve_s0.x - chest.x\|` in `bones`, and `CHAIN_LAG` / `TIP_OVER_ROOT` on `handwear` | Floor: `amp(chest) = 0.7°`, `amp(neck) = 0.85°`, `amp(head) = 1.0°` with lag `0.05` (`phase: 0.25, 0.30, 0.35`). **Set `chest` `rotate` `phase` to `0.25` (quadrature, 90° from `chest` `translatey`'s `phase: 0.0`), never near `0.0`:** at `phase ≈ 0`, vertical breath `y(t)` and shoulder lift `Δx · θ(t)` reinforce on one shoulder and cancel on the other, inflating `root_rig_px` on the reinforced sleeve and dropping `tip_rig_px / root_rig_px` below `1.4725`. At `phase = 0.25`, vertical breath and rotational sway are orthogonal on both shoulders alike. |
|
|
3277
|
-
| Sleeve physics (`sleeve_<s>0..2` under `chest`) | `CHECK_TIP_OVER_ROOT` (`ratio >= 1.4725` on each `handwear` part) and `CHECK_ANIMATIONS_DO_NOT_FOLD` | Set `"rotate": 1, "mix": 1, "inertia": 1.0, "damping": 0.75, "mass": 1` and taper `strength` down the chain: `[45, 30, 20]` for a 3-link sleeve (`1 : 0.67 : 0.44`). Stiff follow-through springs (`strength: 200`, `inertia: 0.2`) barely bend under a `3.333` s idle and move the sleeve as a rigid unit (`ratio ≈ 1.19..1.22`, failing `TIP_OVER_ROOT`); tapering `45 → 30 → 20` lets cumulative link rotation amplify cuff travel past `2.0x` the shoulder travel while keeping `0` folded triangles in `wave` and `hop`. |
|
|
3277
|
+
| Sleeve physics (`sleeve_<s>0..2` under `chest`) | `CHECK_TIP_OVER_ROOT` (`ratio >= 1.4725` on each `handwear` part) and `CHECK_ANIMATIONS_DO_NOT_FOLD` | Set `"rotate": 1, "mix": 1, "inertia": 1.0, "damping": 0.75, "mass": 1` and taper `strength` down the chain: `[45, 30, 20]` for a 3-link sleeve (`1 : 0.67 : 0.44`). Stiff follow-through springs (`strength: 200`, `inertia: 0.2`) barely bend under a `3.333` s idle and move the sleeve as a rigid unit (`ratio ≈ 1.19..1.22`, failing `TIP_OVER_ROOT`); tapering `45 → 30 → 20` lets cumulative link rotation amplify cuff travel past `2.0x` the shoulder travel while keeping `0` folded triangles in `wave` and `hop`. Those ratios are the world reading `TIP_OVER_ROOT` took until issue #987; it now reads the halves against `chest`, the bone the sleeves hang from, and this recipe on `demo` (the working build reproduced below) reads `9.383` and `9.952` there (`handwear_l`, `handwear_r`; `3.626` and `2.905` in the world, which the row keeps as `world_ratio`) — it passes the reading the line holds (rig-parts at `c17292c` with #987's change, rigc 3.2.1). |
|
|
3278
3278
|
| Hair physics (`hairback_*`, `bang_*` under `head`) | `CLOTH_SETTLES` (`>= 0.01694` rig px/s to the last frame), `CHECK_TEXTURE_STRETCH` (`<= 1.926544`), and `CHECK_ANIMATIONS_DO_NOT_FOLD` | Taper `strength` down each `n`-link chain by `strength_0 * (1 - 0.35 * i / (n - 1))`: on long back-hair chains, `strength_0 = 90` (`[90, 74.25, 58.5]` for 3 links), `inertia = 0.8`, `damping = 0.85`; on shorter fringe chains (`bang_*`), `strength = [130, 105]`, `inertia = 0.2`, `damping = 0.9`. |
|
|
3279
3279
|
| Skirt / `bottomwear` physics (`skirt_*` under `hip`) | `CHECK_TIP_OVER_ROOT` on `bottomwear` (`ratio >= 1.4725`), `CHECK_ANIMATIONS_DO_NOT_FOLD` on `hop`, and `CLOTH_SETTLES` | Keep high restoration `strength` (`[200, 165, 130]` for 3 links, `inertia = 0.5`, `damping = 0.82`) so `bottomwear` does not fold during `hop`. With `hip` unkeyed, floor `"wind": [0.4, 0.6, 0.8]` passes `TIP_OVER_ROOT` (`ratio 1.83`); with `hip` keyed in `rotate` (`amp <= 0.4°`, `phase: 0.20`), set `"wind": 0` on the upper-half link `skirt_*0` and put `"wind"` on the lower links (`[0, 15, 40]`) so `tip / root` stays above `1.4725` and `CLOTH_SETTLES` clears on the default world. |
|
|
3280
3280
|
|
|
@@ -3331,13 +3331,15 @@ at `../examples/demo/animations.json` and `generation.control.skeleton` at
|
|
|
3331
3331
|
loop: idle 41 frame(s) at 12 fps, f0000 vs f0040 (t = 3.333333333333333s) with the rig's 29 physics constraint(s) left out (named on the `loop physics:` line; rendered with them, the ends need not meet — `build --loop --lead-in <n>`): max |d| 0 (0 required)
|
|
3332
3332
|
BREATH_VISIBLE: PASS — torso_parts topwear; torso_heat_mean 48.993; torso_floor 3.809; feet_parts shoes; feet_heat_max 0; feet_ceiling 0
|
|
3333
3333
|
CHAIN_LAG: PASS — chains hip_ctl > chest_ctl > neck > head_ctl: period 3.333s, phase 0.200 0.250 0.300 0.350, amp 0.400 2.500 3.000 3.600; lags hip_ctl > chest_ctl +0.050 | chest_ctl > neck +0.050 | neck > head_ctl +0.050; min_step 0.05; step_floor 0.001; other_period none; unread none; first_violation null
|
|
3334
|
-
TIP_OVER_ROOT: PASS — instrument geometry; parts part handwear_l, root_rig_px
|
|
3334
|
+
TIP_OVER_ROOT: PASS — instrument geometry, carrier frame; parts part handwear_l, hang_from chest, root_rig_px 1.3, tip_rig_px 12.202, ratio 9.383, world_root_rig_px 5.786, world_tip_rig_px 20.983, world_ratio 3.626 | part handwear_r, hang_from chest, root_rig_px 1.053, tip_rig_px 10.48, ratio 9.952, world_root_rig_px 6.213, world_tip_rig_px 18.047, world_ratio 2.905 | part bottomwear, hang_from hip, root_rig_px 0.231, tip_rig_px 3.813, ratio 16.512, world_root_rig_px 2.742, world_tip_rig_px 4.239, world_ratio 1.546; ratio_floor 1.4725
|
|
3335
3335
|
TEXTURE_STRETCH: PASS — meshes 9; triangles 1816; frames 41; severity 1.259; severity_ceiling 1.926544
|
|
3336
3336
|
ANIMATIONS_DO_NOT_FOLD: PASS — 8 animation(s) read by rigc render --folds at 12 fps, with its 29 physics constraint(s) on and off (idle, tilt, look_l, look_r, smile, startled, wave, hop): 0 folding mesh(es), 0 not declared in may_fold (<= 0)
|
|
3337
3337
|
CLOTH_SETTLES: CLEAR — 12 physics chain(s) read by their tip on the idle as rendered (41 frame(s) at 12 fps over 3.333 s, physics on): 0 of them settle before the loop's length — 0 warning(s), never a bar
|
|
3338
3338
|
check: PASS; 11 of 11 bar(s) measured, 0 skipped; 0 warning(s) (CLOTH_SETTLES, not a bar)
|
|
3339
3339
|
```
|
|
3340
3340
|
|
|
3341
|
+
The `TIP_OVER_ROOT` line above was read again for issue #987, from this build at rig-parts `c17292c` with #987's change and rigc 3.2.1; the other lines are as first printed. Its world figures for the sleeves (`3.626`, `2.905`) differ from the first print's (`3.399`, `2.491`); `bottomwear`'s are the same.
|
|
3342
|
+
|
|
3341
3343
|
## 4. The command order
|
|
3342
3344
|
|
|
3343
3345
|
Two steps are external: the See-through runs (by any route; the optional `comfy seethrough` adapter is only a client for a ComfyUI box). Everything else is this tool.
|
|
@@ -5344,7 +5346,7 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
5344
5346
|
| <a id="CHECK_BREATH_VISIBLE"></a>`CHECK_BREATH_VISIBLE` | the breath alone barely moves the torso (`topwear`, rendered alone, the idle with and without its breath tracks — issue #411; the whole idle's figure is printed beside it, and motion that is not the breath's is not counted), or the idle has no breath track at all — or the feet (`footwear`), rendered alone, move at all | the chest's breath tracks (`motion.tracks` on `chest`), or the torso mesh's `segments`; for the feet, the bone their region rides (`regions.<part>`, `root` in both examples) |
|
|
5345
5347
|
| <a id="CHECK_BLINK_NO_HOLE"></a>`CHECK_BLINK_NO_HOLE` | with the blink held shut, the eyewhite box shows the page where the open eye had art | the layer under the eye: the `face` part has no art there. Take the face from the other run, or add a part under the eye; `motion.blink.squash` only hides the hole less |
|
|
5346
5348
|
| <a id="CHECK_CHAIN_LAG"></a>`CHECK_CHAIN_LAG` | a rotate track leads (or does not lag) the keyed bone above it, or a chain link swings less than the link above — or, in a chain that carries a part the scene file declares `rests`, more than the link above (issue #245; the object names the part and the file). A lag refusal (issue #580) prints the step, then the two phases it read — as the sine reader returns them, a negative amplitude read as amplitude > 0 at phase + 0.5 — then their difference, folded into (−¼, ¼] cycle because a sine's sign is half a cycle (so flipping an amplitude's sign does not change the step), then the child phases that pass for this parent: `phase step -0.230 cycle(s); >= 0.001 is required — … Phases read: "chest" 0.000, "sleeve_l0" 0.270 …; the step is their difference, 0.270, folded into (-1/4, 1/4] cycle … here a "sleeve_l0" phase in [0.001, 0.250] or [0.501, 0.750]` | for a lag, that chain track's `phase`/`lag`: a child phase from the parent's + 0.001 to + 0.25, modulo 0.5 — the windows the line prints. A larger phase helps only inside them: a child 0.25 to 0.5 above its parent still reads as a lead, and a negated `amps` moves nothing. For a swing, its `amps` (non-decreasing toward the tip; non-increasing on a chain declared resting) |
|
|
5347
|
-
| <a id="CHECK_TIP_OVER_ROOT"></a>`CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's art in the lower half of its box travels less than 1.4725 times as far as in the upper half, measured from the idle's posed geometry (rig px quoted) | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them), or its mode: on `scarf`'s short skirt the proposal's lattice read 1.207 and the same part in `contour` mode 1.738 with the same segments and idle (issue #118) — both carried the `chest` segment #262 removed, after which the lattice reads 3.487. When the detail says `the root travels with the breath (x px of y px): it is carried by \`<bone>\`` (issue #262) the chain is not the cause: a bone an idle `translate` track moves (the breath) moves the root more than the tip, and with that translate held still (the part posed again without it, in proportion to each vertex's weight on the bone and those under it) the ratio passes — the row adds `carried_by`, `breath_rig_px`, `root_without_breath_rig_px` and `tip_without_breath_rig_px`; take that bone's segment off the mesh, or bind the root to the bone the part hangs from. A part the scene file declares `rests` (issue #245) is held the other way, its tip half's travel at most its root half's (ratio ≤ 1); the detail then reads `the scene file's rests declares its tip at rest, so a ratio <= 1 is required`, and for it the fix is the chain's `amps` (shrinking toward the tip) or the mesh's tip weighted to a bone the idle does not swing |
|
|
5349
|
+
| <a id="CHECK_TIP_OVER_ROOT"></a>`CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's art in the lower half of its box travels less than 1.4725 times as far as in the upper half, measured from the idle's posed geometry against the bone the part hangs from (issue #987; rig px quoted, the world reading beside it) | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them), or its mode: on `scarf`'s short skirt the proposal's lattice read 1.207 and the same part in `contour` mode 1.738 with the same segments and idle (issue #118) — both carried the `chest` segment #262 removed, after which the lattice reads 3.487. When the detail says `the root travels with the breath (x px of y px): it is carried by \`<bone>\`` (issue #262) the chain is not the cause: a bone an idle `translate` track moves (the breath) moves the root more than the tip, and with that translate held still (the part posed again without it, in proportion to each vertex's weight on the bone and those under it) the ratio passes — the row adds `carried_by`, `breath_rig_px`, `root_without_breath_rig_px` and `tip_without_breath_rig_px`; take that bone's segment off the mesh, or bind the root to the bone the part hangs from, which the detail names. Since issue #987 only a breath keyed under that bone is read: one at or above it is in the frame the halves are read in and carries nothing. When the bone named is a link of the chain the part hangs on, its segment stays, and the detail says to key the translate on the bone the part hangs from or above it. `nothing in it moves against \`<bone>\`` is a part that rides the bone it hangs from whole — a rigid sleeve, whose world ratio is 1. Since issue #995 a lone swing counts as a link too — a rotate-keyed bone with no child bone, or whose only child is its control twin (a one-link chain's control) — so a skirt bound to the one bone that swings it is read against the bone above, where its swing shows. A rotate-keyed body segment other bones hang under (a pelvis carrying the legs) is not one: a part bound whole to it reads `nothing in it moves against` that bone and fails, its world ratio passing only through the lever arm. Hang swinging cloth on its own bone under the body segment; a fitted garment the rig holds rigid on purpose (shorts on the pelvis) is declared in the scene file's `not_swinging` (§7), and the line then reads it SKIP with the reason. A part the scene file declares `rests` (issue #245) is held the other way, its tip half's travel at most its root half's (ratio ≤ 1); the detail then reads `the scene file's rests declares its tip at rest, so a ratio <= 1 is required`, and for it the fix is the chain's `amps` (shrinking toward the tip) or the mesh's tip weighted to a bone the idle does not swing |
|
|
5348
5350
|
| <a id="CHECK_STILL_REGIONS_DARK"></a>`CHECK_STILL_REGIONS_DARK` | a slot of the face's still set (the `face` slots and every slot drawn over the face that the head bone alone carries) moves relative to the bone the face's slot rides, or a `footwear` slot moves on the screen, by more than the arithmetic's own rounding — measured from the idle's posed geometry (the slot, its largest displacement in rig px, the idle frame and the tolerance quoted) | on the face: a face mesh with a weight on a bone other than the head (`segments`), or one deformed by another bone; on the feet: a mesh weighted to a swinging bone, or a region on the wrong bone. A bang swinging over the face is not this line's failure: its depth is reported (`crossing`), with no bar |
|
|
5349
5351
|
| <a id="CHECK_TEXTURE_STRETCH"></a>`CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named, and — issue #314 — the bones the edge's two vertices hang on: each vertex's two largest weights from `rig.json`, `vertex <i> is weighted to "<bone>" <w>, …`, then `on the idle's chains, the chain from "<first link>" carries "<bone>", …` — each chain `CHAIN_LAG` reads named once, by its first link — with a bone on no chain said, or `none of these bones hangs on a chain of the idle`); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other. Two chains named, with opposed amplitudes: give both one sign, or drop one — `propose --from-config` measures the pair before a build (§3, *Two chains on one lattice*). An edge whose two ends disagree on whether a bone's small share survives the 0.03 cut (one vertex keeps the bone at a few hundredths, the next drops it — read the two vertices' weights the line quotes): declare `influences: {floor: "continuous"}` on that mesh first, `rule: "heat"` second; the weight `exponent` moves the figure without removing the cut (issue #115, `reference-demo` `hair_back` as contour at tolerance 1, rig-c 2.47.1: 2.535 as drafted; continuous floor 1.422, heat 1.816, both PASS; exponent 1.5 / 3 / 4 / 6 read 2.564 / 1.902 / 2.119 / 4.816, the one pass on the same cut at another edge) |
|
|
5350
5352
|
| <a id="CHECK_ANIMATIONS_DO_NOT_FOLD"></a>`CHECK_ANIMATIONS_DO_NOT_FOLD` | an animation, the idle included, turns a mesh triangle over against the setup pose on some frame, read by `rigc render --folds` over the build with its physics on — and with its physics constraints left out beside it when the rig declares any, so each row reports `physics_on` and `physics_off` side by side (issues #320, #333, #363); the object names the animation and the mesh, the detail quotes rigc's line — the frames, the worst frame, which triangles, their world box and setup-pose area — and when the fold appears only with physics on, names the physics constraint(s) on the mesh's bones and the worst frame. After rigc's line it names what the worst frame's folded triangles hang on (issue #567), in `TEXTURE_STRETCH`'s words: each vertex's dominant bone is its largest weight in `rig.json`, the triangles whose vertices share one set of dominant bones are one group (the set in `rig.json`'s bone order), and each group lists its first 6 triangles (`triangles 63, 78, 79 dominated by "knee_r", "knee_l"`, past 6 `… and N more`) and spells out its first triangle's vertices — the largest two weights of each, and the idle chains those bones hang on; 3 groups are written and the rest counted (`; … and N more group(s), M triangle(s)`). An attachment the skin does not hold, or a triangle its table does not reach, is said, not guessed. Or a `may_fold` entry of the `--requirements` file names a slot and animation that does not fold: an exception that excepts nothing | the physics constraint(s) the line names (lower `inertia`, raise `strength`, or lower `damping`), the clip's keys on the bones the line names (a bend past what the weights carry, two bones turned toward each other until the rows between them pass, a translate that carries one row of vertices past the next), or the mesh's `segments`; to look, `rigc inspect --candidate <out>/check/build --show art,folds,mesh --animation <name> --at <the worst frame's time> --zoom folds`. A fold the art means (a sleeve behind the body) is declared, `"may_fold": [{ "slot": …, "animation": …, "why": … }]` (§7, *Declared requirements*); a stale entry is taken out |
|
|
@@ -5755,7 +5757,7 @@ the two examples [observed]:
|
|
|
5755
5757
|
| <a id="BLINK_NO_HOLE"></a>`BLINK_NO_HOLE` | the setup pose with every `scaley` track on the eyewhite slots' bones held at its closed value, against the setup pose, at full size: pixels in the eyewhite box that show the page where the open eye had art | 0 px | 0 | 0 | no `eyewhite` part, or no `scaley` track on its bones goes below its first key |
|
|
5756
5758
|
| ″ (reported) | the same box: each closed-eye pixel's max-channel distance to the nearest colour the open eye's box holds — max, and pixels over 40 | none | 15; 0 | 11; 0 | as above |
|
|
5757
5759
|
| <a id="CHAIN_LAG"></a>`CHAIN_LAG` | `motion.json`'s rotate tracks read as sines (DFT of the keys: period, amplitude, phase) and arranged by the bone tree — a keyed bone's parent is its nearest keyed ancestor | every lag ≥ 0.001 cycle; amplitude non-decreasing down each unbranched chain — non-increasing down a chain that carries a part declared `rests` (issue #245) | lags 0.040 (neck to head) to 0.120; 12 chains | lags 0.040 to 0.100; 9 chains | no rotate track under another of the same period |
|
|
5758
|
-
| <a id="TIP_OVER_ROOT"></a>`TIP_OVER_ROOT` | each `handwear`/`bottomwear` part: how far the centroid of its art travels in the lower half of its box against the upper half — the art's AREA centroid inside each fixed half (cut at h/2), every art pixel (alpha above 8) carried through its attachment's posed triangles from the idle's `geometry.json` and clipped to the half exactly, rig px; `instrument: "geometry"` in the line (issue #118) | ratio ≥ 1.4725; a part declared `rests`, ratio ≤ 1 (issue #245) | `bottomwear` 3.222, `sleeves` 4.
|
|
5760
|
+
| <a id="TIP_OVER_ROOT"></a>`TIP_OVER_ROOT` | each `handwear`/`bottomwear` part: how far the centroid of its art travels in the lower half of its box against the upper half — the art's AREA centroid inside each fixed half (cut at h/2), every art pixel (alpha above 8) carried through its attachment's posed triangles from the idle's `geometry.json` and clipped to the half exactly, rig px; `instrument: "geometry"` in the line (issue #118). Since issue #987 every posed vertex is first carried into the setup place of the bone the part hangs from (`hang_from`, below), so the halves travel against it; the row keeps the world reading as `world_root_rig_px`, `world_tip_rig_px`, `world_ratio`, and the line says `instrument: "geometry, carrier frame"`. A half that travels 0 at the line's precision is 0 | ratio ≥ 1.4725; a part declared `rests`, ratio ≤ 1 (issue #245) | `bottomwear` 3.222, `sleeves` 9.891 against `chest` (world 4.762) | `bottomwear` 5.393, `sleeves` 27.482 against `chest` (world 11.830) | no such part |
|
|
5759
5761
|
| <a id="STILL_REGIONS_DARK"></a>`STILL_REGIONS_DARK` | from the idle's posed geometry (`geometry.json`), `instrument: "geometry"` in each half (issue #123). **Face:** the still set — the `face` slots, and every slot drawn after the first of them whose art overlaps a face part's at the setup pose, that the head bone alone carries (a region on it, or a mesh whose every weight is it) and that the idle does not key by slot (`deform`, `sequence`, a slot track: `driven_slots`). The head bone is the bone the `face` parts' slot rides in `rig.json`. Each still slot's art (alpha above 8), carried through its posed attachment and back through the head bone's setup-to-frame map on every idle frame, against its setup position: `max_rig_px` (and where, `at`, when it is a motion), `rms_rig_px`, per slot in `still_slots`. **Feet:** the `footwear` slots the same way in screen space — they ride the root, which the idle does not key, so the screen is their own frame. **Reported, no bar:** `crossing` — per slot drawn after the still set's first slot and not in it, how deep its art reaches into the region (`face` on top at the setup pose, clear of where `eyewhite`, `irides`, `eyelash`, `eyebrow` and `mouth` go in the head's frame; for the feet, `footwear` on top), `depth_rig_px`, and the most of it covered on one frame, `area_rig_px2` | each half's `max_rig_px` ≤ its `tolerance_rig_px`: nothing moves, up to the arithmetic's rounding, √2 × 2 × 35 × M² × ulp(C) (below) | face 4.202e-13 ≤ 2.286e-11; `hair_front` crossing 2.816 rig px deep, 89.395 rig px² (frame 47); feet 0 ≤ 2.814e-12 | face 4.418e-13 ≤ 2.286e-11 (`face`, `ear_l`, `ear_r`); `hair_front` 2.194 deep, 50.605 rig px² (frame 23); feet 0 ≤ 2.814e-12 | no `face` and no `footwear` part (one of the two absent leaves that half unmeasured; so do face parts on two slot bones, or idle frames with no `geometry.json`) |
|
|
5760
5762
|
| <a id="TEXTURE_STRETCH"></a>`TEXTURE_STRETCH` | every mesh triangle's three edges in every idle frame, read from the `geometry.json` the idle render writes (`rigc render --geometry`, rig-c 1.4.0 or later): skinned length over rest length, the rest being the setup pose's bones with no deform. The figure is the rig's worst max(ratio, 1/ratio); each mesh's largest and smallest ratio, with triangle, vertices, edge and frame, is the detail | max(ratio, 1/ratio) ≤ 1.926544 | 1.388: `hair_back` triangle 449, edge 42-43 at 0.720 in frame 26 (largest stretch 1.291, frame 2) | 1.227: `sleeves` triangle 169, edge 126-51 at 0.815 in frame 28 (largest stretch 1.196, frame 5) | the rig draws no mesh, or the render wrote no `geometry.json` |
|
|
5761
5763
|
| <a id="ANIMATIONS_DO_NOT_FOLD"></a>`ANIMATIONS_DO_NOT_FOLD` | every animation, the idle included, through `rigc render --folds` at 12 fps over the build with its physics stepping and, when the rig declares physics constraints, over the keyed build with them left out beside it (issue #363): the meshes with a triangle turned over against the setup pose on some frame, less those the requirements file's `may_fold` declares (below) | 0 undeclared folding meshes, and no `may_fold` entry that matches no fold | 0, over `idle` | 0, over `idle` and `hop`, 2 physics constraints on and off | never SKIP: written whenever `motion.json` holds an animation |
|
|
@@ -5795,6 +5797,57 @@ on the geometric figure (`scarf`'s lattice `bottomwear` 1.337, 1.316, 1.285, 1.2
|
|
|
5795
5797
|
1200, 2400, 4800, 8000), with one gap not explained: `reference-sample`'s `bottomwear` tip reads
|
|
5796
5798
|
2.342 rig px there against 2.596 from geometry, its root agreeing to 0.4 %.
|
|
5797
5799
|
|
|
5800
|
+
**`TIP_OVER_ROOT` reads the halves against the bone the part hangs from (issue #987).**
|
|
5801
|
+
Read in the world, a sleeve whose chain trails its body's sway scores the trail as the
|
|
5802
|
+
tip moving less: the body carries the root, the chain's lag carries the tip back, and the
|
|
5803
|
+
two cancel at the tip. A heavier physics sleeve read lower there, and a pendulum that
|
|
5804
|
+
turned about its middle read higher, so tuning toward the floor made the sleeve swing
|
|
5805
|
+
more or turn the wrong way. Each idle frame's posed vertices are now mapped by
|
|
5806
|
+
setup(B) · frame(B)⁻¹, B the bone the part hangs from: from the deepest common
|
|
5807
|
+
ancestor-or-self of the bones its attachments are bound to, walk up while the bone is a
|
|
5808
|
+
link — a bone of a chain `CHAIN_LAG` reads, or a bone a physics constraint drives — and
|
|
5809
|
+
B is the first that is not (the rig stage keys `<link>_ctl` and binds the mesh to its
|
|
5810
|
+
child the physics drives, so the walk passes both; no bone is read by its name). When
|
|
5811
|
+
that common bone is a link's control twin — at the link's origin, unturned, unscaled and
|
|
5812
|
+
keyed by no idle track — the walk starts from the link, so a keyed chain whose bound
|
|
5813
|
+
child no physics drives is lifted too; the twin step is taken only there, so the walk
|
|
5814
|
+
stops at `chest`, which is `chest_ctl`'s twin. The row names the bone as `hang_from`.
|
|
5815
|
+
The floor stays 1.4725, the bar set from the pixel instrument above, not chased. Every
|
|
5816
|
+
public example passes both
|
|
5817
|
+
readings; the bottomwear parts hang from `hip`, which no example keys, so they read the
|
|
5818
|
+
same:
|
|
5819
|
+
|
|
5820
|
+
| part | world (until #987) | against `hang_from` |
|
|
5821
|
+
| --- | --- | --- |
|
|
5822
|
+
| `demo` `handwear_l`, `handwear_r` | 4.761, 4.835 | 10.249, 11.911 (`chest`) |
|
|
5823
|
+
| `demo` `bottomwear` | 2.426 | 2.426 (`hip`) |
|
|
5824
|
+
| `reference-demo` `bottomwear`, `sleeves` | 3.222, 4.762 | 3.222 (`hip`), 9.891 (`chest`) |
|
|
5825
|
+
| `reference-sample` `bottomwear`, `sleeves` | 5.393, 11.830 | 5.393 (`hip`), 27.482 (`chest`) |
|
|
5826
|
+
| `scarf` `handwear_r`, `handwear_l` | 2.632, 2.460 | 9.235, 7.732 (`chest`) |
|
|
5827
|
+
| `scarf` `bottomwear` | 3.436 | 3.436 (`hip`) |
|
|
5828
|
+
| `wind` `bottomwear` | 1.816 | 1.816 (`hip`) |
|
|
5829
|
+
|
|
5830
|
+
The selftest plants the four cases the reading is for on one two-link sleeve under a
|
|
5831
|
+
body the idle carries 4 rig px: ridden rigid it reads 0 / 0 against the bone it rides
|
|
5832
|
+
and fails (world 1); turned about its middle it reads about 1 and fails (world above
|
|
5833
|
+
the floor); trailing the carry, on keyed links with physics children or on a
|
|
5834
|
+
physics-only chain, it passes (world below 1). The skirt whose waistband rides `chest`
|
|
5835
|
+
(issue #262) hangs from a bone above `chest`, so the breath still carries its root and
|
|
5836
|
+
it still fails, naming `chest`.
|
|
5837
|
+
|
|
5838
|
+
A lone swing is a link too (issue #995): a rotate-keyed bone with no child bone, or whose
|
|
5839
|
+
only child is its control twin, is the swing itself — a skirt bound to the one bone that
|
|
5840
|
+
swings it from the top of its box, or a one-link chain's control — so the walk passes it
|
|
5841
|
+
and the part is read against the bone above (in the build fixtures that is `root`, which
|
|
5842
|
+
the idle does not move, so the carrier reading equals the world one: 2.336 for both). A
|
|
5843
|
+
rotate-keyed body segment other bones hang under is not one: a garment bound whole to a
|
|
5844
|
+
pelvis that carries the legs reads 0 / 0 against it and fails (world 3.501, through the
|
|
5845
|
+
lever arm alone). `TIP_OVER_ROOT` reads every `handwear` and `bottomwear` part, declared or
|
|
5846
|
+
not, so a fitted garment held rigid on purpose is declared `not_swinging` and reads SKIP
|
|
5847
|
+
with the reason; the leg-set build fixtures declare their shorts so. A track that keys a
|
|
5848
|
+
`motion.groups` group keys each member for the walk too, as `CHAIN_LAG` reads it, so a
|
|
5849
|
+
group-keyed link is not taken for its control's unkeyed twin.
|
|
5850
|
+
|
|
5798
5851
|
**`STILL_REGIONS_DARK` is measured from geometry too, and its scope changed (issue #123).**
|
|
5799
5852
|
|
|
5800
5853
|
**What it read until #123.** The face half compared two pixel figures on the idle's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/check.ts
CHANGED
|
@@ -7889,9 +7889,117 @@ const NO_TIP_EXCEPTIONS: TipExceptions = { path: '', not_swinging: [], rises: []
|
|
|
7889
7889
|
* the idle's posed geometry, not a render of the part. Until #118 the line
|
|
7890
7890
|
* read rendered pixels — figures in frame px under `root_px`/`tip_px` — so an
|
|
7891
7891
|
* old and a new check.json are told apart by this key and by the units in
|
|
7892
|
-
* the field names.
|
|
7892
|
+
* the field names. Issue #987: the halves are read in the frame of the bone
|
|
7893
|
+
* the part hangs from ({@link hangBone}), the world reading beside them
|
|
7894
|
+
* (`world_*`), so the key names the frame too.
|
|
7893
7895
|
*/
|
|
7894
|
-
export const TIP_INSTRUMENT = 'geometry';
|
|
7896
|
+
export const TIP_INSTRUMENT = 'geometry, carrier frame';
|
|
7897
|
+
|
|
7898
|
+
/**
|
|
7899
|
+
* The bone a part hangs from (issue #987): the deepest common ancestor-or-self of the bones its attachments are
|
|
7900
|
+
* bound to ({@link boundBones}), walked up while it is a link — a bone of a chain `CHAIN_LAG` reads, a bone a
|
|
7901
|
+
* physics constraint drives, or a lone swing (a rotate-keyed bone with no child bone but its control twin) — to the first bone that is not, so a chain-hung part is read against the body that
|
|
7902
|
+
* carries the chain and not against its own first link (no bone is read by its name). When the common bone is a
|
|
7903
|
+
* link's control twin (at its origin, unturned, unscaled, keyed by no idle track), the walk starts from the link; only
|
|
7904
|
+
* there, so a body bone that is its own `_ctl`'s twin (`chest`) is where the walk stops. Null when the part is bound to
|
|
7905
|
+
* nothing or the walk leaves the tree, and the part is then read in the world.
|
|
7906
|
+
*/
|
|
7907
|
+
export function hangBone(rig: Record<string, unknown>, motion: Record<string, unknown>, part: string): string | null {
|
|
7908
|
+
const bones = (Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord).filter((b) => typeof b.name === 'string');
|
|
7909
|
+
const parent = new Map(bones.map((b) => [String(b.name), typeof b.parent === 'string' ? b.parent : null]));
|
|
7910
|
+
const upFrom = (b: string): string[] => {
|
|
7911
|
+
const path: string[] = [];
|
|
7912
|
+
for (let x: string | null = b; x !== null && !path.includes(x); x = parent.get(x) ?? null) path.push(x);
|
|
7913
|
+
return path;
|
|
7914
|
+
};
|
|
7915
|
+
const bound = [...boundBones(rig, part)];
|
|
7916
|
+
if (bound.length === 0) return null;
|
|
7917
|
+
const paths = bound.map(upFrom);
|
|
7918
|
+
const common = paths[0].find((b) => paths.every((p) => p.includes(b)));
|
|
7919
|
+
if (common === undefined) return null;
|
|
7920
|
+
// A link: a bone of a chain CHAIN_LAG reads, or a bone a physics constraint drives. The rig stage keys a chain's
|
|
7921
|
+
// `<link>_ctl` and binds the mesh to its child the physics drives, and a physics-only chain has no keys at all, so
|
|
7922
|
+
// both are walked up through until the body bone that carries them.
|
|
7923
|
+
const links = new Set(chainFigures(rig, motion).chains.filter((c) => c.length >= 2).flatMap((c) => c.map((l) => l.bone)));
|
|
7924
|
+
for (const c of Array.isArray(rig.constraints) ? rig.constraints : []) if (isRecord(c) && c.type === 'physics' && typeof c.bone === 'string') links.add(c.bone);
|
|
7925
|
+
// Every bone an idle track keys, a group track's members included (as chainFigures reads them); `rotated` keeps
|
|
7926
|
+
// the ones a rotate track keys.
|
|
7927
|
+
const idle = isRecord(motion.animations) && isRecord(motion.animations.idle) ? motion.animations.idle : {};
|
|
7928
|
+
const groups = isRecord(motion.groups) ? motion.groups : {};
|
|
7929
|
+
const keyedBones = new Set<string>();
|
|
7930
|
+
const rotated = new Set<string>();
|
|
7931
|
+
for (const t of (Array.isArray(idle.tracks) ? idle.tracks : []).filter(isRecord)) {
|
|
7932
|
+
const targets = typeof t.bone === 'string' ? [t.bone] : typeof t.group === 'string' && Array.isArray(groups[t.group]) ? (groups[t.group] as unknown[]).filter((m): m is string => typeof m === 'string') : [];
|
|
7933
|
+
for (const b of targets) {
|
|
7934
|
+
keyedBones.add(b);
|
|
7935
|
+
if (t.property === 'rotate') rotated.add(b);
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7938
|
+
const setup = new Map(bones.map((b) => [String(b.name), b]));
|
|
7939
|
+
const children = new Map<string, string[]>();
|
|
7940
|
+
for (const [b, pa] of parent) if (pa !== null) children.set(pa, [...(children.get(pa) ?? []), b]);
|
|
7941
|
+
const kids = (b: string): string[] => children.get(b) ?? [];
|
|
7942
|
+
// At its parent's origin, unturned and unscaled, keyed by no idle track: the parent carries it whole.
|
|
7943
|
+
const atOrigin = (b: string): boolean => {
|
|
7944
|
+
const r = setup.get(b);
|
|
7945
|
+
if (r === undefined || keyedBones.has(b)) return false;
|
|
7946
|
+
const zero = (k: string, at: number): boolean => r[k] === undefined || r[k] === at;
|
|
7947
|
+
return zero('x', 0) && zero('y', 0) && zero('rotation', 0) && zero('scaleX', 1) && zero('scaleY', 1) && zero('shearX', 0) && zero('shearY', 0);
|
|
7948
|
+
};
|
|
7949
|
+
// A lone swing (#995): a rotate-keyed bone with no child bones, or whose only child is its control twin with none.
|
|
7950
|
+
// That is a bone that is the swing itself, and a one-link chain's control; a rotate-keyed body segment other bones
|
|
7951
|
+
// hang under (a pelvis carrying legs) is not one, so a part bound whole to it stays read against it.
|
|
7952
|
+
for (const b of rotated) {
|
|
7953
|
+
const k = kids(b);
|
|
7954
|
+
if (k.length === 0 || (k.length === 1 && atOrigin(k[0]) && kids(k[0]).length === 0)) links.add(b);
|
|
7955
|
+
}
|
|
7956
|
+
// The rig stage's control twin: a bone at its link's origin, unturned and unscaled, that nothing keys — the link carries it whole.
|
|
7957
|
+
const twinOfLink = (b: string): boolean => {
|
|
7958
|
+
const pa = parent.get(b) ?? null;
|
|
7959
|
+
return pa !== null && links.has(pa) && atOrigin(b);
|
|
7960
|
+
};
|
|
7961
|
+
const seen = new Set<string>();
|
|
7962
|
+
let at: string | null = twinOfLink(common) ? (parent.get(common) ?? null) : common;
|
|
7963
|
+
while (at !== null && links.has(at) && !seen.has(at)) {
|
|
7964
|
+
seen.add(at);
|
|
7965
|
+
at = parent.get(at) ?? null;
|
|
7966
|
+
}
|
|
7967
|
+
return at;
|
|
7968
|
+
}
|
|
7969
|
+
|
|
7970
|
+
/**
|
|
7971
|
+
* Each idle frame's posed vertices mapped into the setup place of `bone` (issue #987): world point p on frame i
|
|
7972
|
+
* goes to setup(bone) · frame_i(bone)⁻¹ · p, so what the bone's own motion carries is taken out and what moves
|
|
7973
|
+
* against it is left. Identity when `bone` is null; null when the geometry does not record the bone on every frame.
|
|
7974
|
+
*/
|
|
7975
|
+
export function carrierFrame(geo: IdleGeometry | null, bone: string | null): ((i: number, posed: readonly number[]) => number[]) | null {
|
|
7976
|
+
if (bone === null) return (_i, posed) => posed.slice();
|
|
7977
|
+
if (geo === null) return null;
|
|
7978
|
+
const world = (bs: unknown): BoneWorld | null => {
|
|
7979
|
+
const b = Array.isArray(bs) ? bs.filter(isRecord).find((x) => x.name === bone) : undefined;
|
|
7980
|
+
if (b === undefined) return null;
|
|
7981
|
+
const f = [b.a, b.b, b.c, b.d, b.worldX, b.worldY];
|
|
7982
|
+
if (!f.every((v) => typeof v === 'number' && Number.isFinite(v))) return null;
|
|
7983
|
+
return { a: b.a as number, b: b.b as number, c: b.c as number, d: b.d as number, worldX: b.worldX as number, worldY: b.worldY as number };
|
|
7984
|
+
};
|
|
7985
|
+
const s = world(geo.setupBones);
|
|
7986
|
+
const fs = geo.frameBones.map(world);
|
|
7987
|
+
if (s === null || fs.some((f) => f === null || f.a * f.d - f.b * f.c === 0)) return null;
|
|
7988
|
+
return (i, posed) => {
|
|
7989
|
+
const m = fs[i] as BoneWorld;
|
|
7990
|
+
const det = m.a * m.d - m.b * m.c;
|
|
7991
|
+
const out = posed.slice();
|
|
7992
|
+
for (let j = 0; j + 1 < out.length; j += 2) {
|
|
7993
|
+
const dx = posed[j] - m.worldX;
|
|
7994
|
+
const dy = posed[j + 1] - m.worldY;
|
|
7995
|
+
const lx = (m.d * dx - m.b * dy) / det;
|
|
7996
|
+
const ly = (-m.c * dx + m.a * dy) / det;
|
|
7997
|
+
out[j] = s.a * lx + s.b * ly + s.worldX;
|
|
7998
|
+
out[j + 1] = s.c * lx + s.d * ly + s.worldY;
|
|
7999
|
+
}
|
|
8000
|
+
return out;
|
|
8001
|
+
};
|
|
8002
|
+
}
|
|
7895
8003
|
|
|
7896
8004
|
/**
|
|
7897
8005
|
* (4) Each swinging part's tip travels further than its root: how far the art
|
|
@@ -7915,11 +8023,19 @@ export const TIP_INSTRUMENT = 'geometry';
|
|
|
7915
8023
|
* relation: tip / root at most {@link TIP_REST_RATIO_CEILING}. Its row says
|
|
7916
8024
|
* so first (`declared: "rests"`), the list is the line's first figure after
|
|
7917
8025
|
* the other two, and the ceiling follows the floor.
|
|
8026
|
+
*
|
|
8027
|
+
* Issue #987: the halves are read against the bone the part hangs from
|
|
8028
|
+
* ({@link hangBone}, through {@link carrierFrame}), so a chain that trails its
|
|
8029
|
+
* body's carry is not scored as a tip that moves less; the row names that bone
|
|
8030
|
+
* (`hang_from`) and keeps the world reading (`world_*`). A half that travels 0
|
|
8031
|
+
* at the line's precision is 0, so a part that rides its carrier whole reads
|
|
8032
|
+
* 0 / 0 and fails, not a ratio of round-off.
|
|
7918
8033
|
*/
|
|
7919
8034
|
function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems: Problem[], excepted: TipExceptions = NO_TIP_EXCEPTIONS): JudgementLine {
|
|
7920
8035
|
const swing = partsTagged(inp.parts, SWING_TAGS);
|
|
7921
8036
|
if (swing.length === 0) return skip(`no part comes from a See-through ${tagWords(SWING_TAGS)} layer, so there is no hem or sleeve to swing`);
|
|
7922
8037
|
const poses = attachmentPoses(geometryPath, frames);
|
|
8038
|
+
const idleGeo = readGeometry(geometryPath, frames);
|
|
7923
8039
|
// Issue #231: a part declared `rises` is read up only when the chain carrying it does rise, measured at the setup pose.
|
|
7924
8040
|
// Issue #322: at its last link, the end the tip half is the art of; the row says what was read.
|
|
7925
8041
|
const risesRead = new Map<string, string>();
|
|
@@ -7964,44 +8080,65 @@ function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems
|
|
|
7964
8080
|
const [rootHalf, tipHalf] = rises ? ['lower', 'upper'] : ['upper', 'lower'];
|
|
7965
8081
|
const art = placedArt(p.x, p.y, readPng(join(inp.partsDir, `${p.name}.png`)));
|
|
7966
8082
|
const shown = poses.map((f) => f.get(p.name) ?? null);
|
|
7967
|
-
const
|
|
8083
|
+
const w = halfTravels(art, p, inp.stage, shown, rises ? 'up' : 'down');
|
|
8084
|
+
// Issue #987: the halves are read in the frame of the bone the part hangs from; the world reading rides beside them.
|
|
8085
|
+
const hangFound = hangBone(inp.rig, inp.motion, p.name);
|
|
8086
|
+
const frameOf = carrierFrame(idleGeo, hangFound);
|
|
8087
|
+
const hang = frameOf === null ? null : hangFound;
|
|
8088
|
+
const toCarrier = frameOf ?? ((_i: number, posed: readonly number[]) => posed.slice());
|
|
8089
|
+
const held = shown.map((f, i) => (f === null ? null : { rest: f.rest, triangles: f.triangles, posed: toCarrier(i, f.posed) }));
|
|
8090
|
+
const c = hang === null ? w : halfTravels(art, p, inp.stage, held, rises ? 'up' : 'down');
|
|
7968
8091
|
// A part not declared writes the row it wrote before issue #231; a declared one says which half is its tip's.
|
|
7969
8092
|
const said = { ...(rests ? { declared: 'rests' } : {}), ...(rises ? { tip_half: tipHalf, tip_read: risesRead.get(p.name) as string } : {}) };
|
|
7970
|
-
if (
|
|
7971
|
-
|
|
8093
|
+
if (w.root === null || w.tip === null || c.root === null || c.tip === null) {
|
|
8094
|
+
const half = w.root === null || c.root === null ? rootHalf : tipHalf;
|
|
8095
|
+
rows.push({ part: p.name, ...said, unmeasured: shown[0] === null ? 'its slot shows no attachment on idle frame 0' : `on idle frame 0 the ${half} half of its box holds no art its attachment draws` });
|
|
7972
8096
|
continue;
|
|
7973
8097
|
}
|
|
7974
8098
|
measured++;
|
|
8099
|
+
// Issue #987: a half that does not move against its carrier, read at the line's precision, is 0 and not the
|
|
8100
|
+
// arithmetic's residue — a rigid part reads 0 / 0, not a ratio of two round-off figures.
|
|
8101
|
+
const t = { root: round3(c.root) === 0 ? 0 : c.root, tip: round3(c.tip) === 0 ? 0 : c.tip };
|
|
7975
8102
|
const ratio = t.root > 0 ? round3(t.tip / t.root) : null;
|
|
8103
|
+
const worldRatio = w.root > 0 ? round3(w.tip / w.root) : null;
|
|
7976
8104
|
// Issue #245: a resting tip is held to the opposite relation — no further than its root, at the line's precision.
|
|
7977
|
-
const ok = rests ? (ratio === null ?
|
|
7978
|
-
rows.push({ part: p.name, ...said, root_rig_px: round3(t.root), tip_rig_px: round3(t.tip), ratio });
|
|
8105
|
+
const ok = rests ? (ratio === null ? t.tip === 0 : ratio <= TIP_REST_RATIO_CEILING) : ratio === null ? t.tip > 0 : ratio >= TIP_RATIO_FLOOR;
|
|
8106
|
+
rows.push({ part: p.name, ...said, hang_from: hang, root_rig_px: round3(t.root), tip_rig_px: round3(t.tip), ratio, world_root_rig_px: round3(w.root), world_tip_rig_px: round3(w.tip), world_ratio: worldRatio });
|
|
8107
|
+
const against = hang === null ? 'in the world' : `against \`${hang}\`, the bone it hangs from`;
|
|
8108
|
+
const inWorld = hang === null ? '' : `; in the world ${round3(w.tip)} and ${round3(w.root)}, ratio ${worldRatio ?? 'undefined'}`;
|
|
8109
|
+
const travels = `the art in its ${tipHalf} half travels ${round3(t.tip)} rig px and in its ${rootHalf} half ${round3(t.root)} rig px ${against}`;
|
|
7979
8110
|
if (!ok && rests) {
|
|
7980
8111
|
failed = true;
|
|
7981
8112
|
problems.push({
|
|
7982
8113
|
code: 'CHECK_TIP_OVER_ROOT',
|
|
7983
8114
|
object: `part "${p.name}", box ${p.x},${p.y} ${p.w}x${p.h} rig px, posed by the idle's geometry, declared rests${rises ? ' and rises' : ''}`,
|
|
7984
|
-
detail:
|
|
8115
|
+
detail: `${travels} (ratio ${ratio ?? 'undefined: the root does not move'}${inWorld}); the scene file's rests declares its tip at rest, so a ratio <= ${TIP_REST_RATIO_CEILING} is required — the tip swings: the chain's amplitudes grow toward the tip, or the tip of the mesh is weighted to a bone the idle swings`,
|
|
7985
8116
|
});
|
|
7986
8117
|
} else if (!ok) {
|
|
7987
8118
|
failed = true;
|
|
7988
8119
|
// Issue #262: a root the breath carries is named with its carrier, not blamed on the chain.
|
|
7989
8120
|
// Issue #351: when the breath carries the root and the chain is also below the bar, or when the mesh is weighted
|
|
7990
8121
|
// off its chain without a breath carrier, name that too instead of only blaming the chain's amplitudes.
|
|
7991
|
-
|
|
8122
|
+
// Issue #987: only a breath under the hang bone moves the root against it; one at or above it is in the frame.
|
|
8123
|
+
const carry = breathCarrier(inp.rig, inp.motion, p.name, geometryPath, frames, { art, box: p, stage: inp.stage, shown, toward: rises ? 'up' : 'down', root: t.root, tip: t.tip, hang, toCarrier });
|
|
7992
8124
|
if (carry !== null) rows[rows.length - 1] = { ...rows[rows.length - 1], carried_by: carry.bone, breath_rig_px: round3(carry.breath), root_without_breath_rig_px: round3(carry.root), tip_without_breath_rig_px: round3(carry.tip) };
|
|
7993
8125
|
const offChain = carry === null && round3(t.tip) > 0 ? offChainClause(inp.rig, inp.motion, p.name) : null;
|
|
7994
|
-
const misplaced = rises || carry !== null || offChain !== null ? null : chainRootsBelowArtClause(inp.rig, inp.motion, p, inp.stage,
|
|
8126
|
+
const misplaced = rises || carry !== null || offChain !== null ? null : chainRootsBelowArtClause(inp.rig, inp.motion, p, inp.stage, idleGeo?.setupBones);
|
|
8127
|
+
const hangName = hang === null ? 'the bone the part hangs from' : `\`${hang}\`, the bone the part hangs from`;
|
|
8128
|
+
const held = carry === null ? '' : `the root travels with the breath (${round3(t.root)} px of ${round3(carry.breath)} px): it is carried by \`${carry.bone}\`, which moves it more than the tip; with \`${carry.keyed}\`'s translate held still the root travels ${round3(carry.root)} rig px and the tip ${round3(carry.tip)}`;
|
|
8129
|
+
const heldRatio = carry === null ? '' : `ratio ${carry.root > 0 ? round3(carry.tip / carry.root) : 'undefined: the root moves only with it'}`;
|
|
7995
8130
|
const why =
|
|
7996
8131
|
carry === null
|
|
7997
|
-
? `the chain's amplitudes do not grow toward the tip, or the mesh is not weighted to the chain${offChain === null ? '' : `; ${offChain}`}`
|
|
7998
|
-
: carry.
|
|
7999
|
-
? `the
|
|
8000
|
-
:
|
|
8132
|
+
? `${t.root === 0 && t.tip === 0 && hang !== null ? `nothing in it moves against \`${hang}\`: it rides that bone whole; ` : ''}the chain's amplitudes do not grow toward the tip, or the mesh is not weighted to the chain${offChain === null ? '' : `; ${offChain}`}`
|
|
8133
|
+
: carry.link
|
|
8134
|
+
? `${held}, which ${carry.passes ? `the bar passes (${heldRatio})` : `is still below the bar (${heldRatio}, < ${TIP_RATIO_FLOOR})`} — \`${carry.bone}\` is a link of the chain the part hangs on, so its segment stays: key \`${carry.keyed}\`'s translate on ${hangName} or a bone above it, where this reading takes it out, not under it${carry.passes ? '' : ", and grow the chain's amplitudes toward the tip"}`
|
|
8135
|
+
: carry.passes
|
|
8136
|
+
? `${held}, which the bar passes (${heldRatio}) — bind the root to ${hangName}, not to \`${carry.bone}\``
|
|
8137
|
+
: `${held}, which is still below the bar (${heldRatio}, < ${TIP_RATIO_FLOOR}) — drop the segment on \`${carry.bone}\` from \`meshes.${p.name}.segments\` (or bind the root to ${hangName}, not to \`${carry.bone}\`) and grow the chain's amplitudes toward the tip`;
|
|
8001
8138
|
problems.push({
|
|
8002
8139
|
code: 'CHECK_TIP_OVER_ROOT',
|
|
8003
8140
|
object: `part "${p.name}", box ${p.x},${p.y} ${p.w}x${p.h} rig px, posed by the idle's geometry${rises ? ', declared rises' : ''}`,
|
|
8004
|
-
detail:
|
|
8141
|
+
detail: `${travels} (ratio ${ratio ?? (hang === null ? 'undefined: nothing moves' : `undefined: nothing moves against \`${hang}\``)}${inWorld}); a ratio >= ${TIP_RATIO_FLOOR} is required — ${misplaced ?? why}`,
|
|
8005
8142
|
});
|
|
8006
8143
|
}
|
|
8007
8144
|
}
|
|
@@ -8109,7 +8246,13 @@ const BREATH_PROPS: readonly string[] = ['translate', 'translatex', 'translatey'
|
|
|
8109
8246
|
* them. The root travels with the breath when the breath adds more to the root's travel than to the tip's and the tip
|
|
8110
8247
|
* moves above 0 at the line's precision (round3); `passes` records whether, held still, the halves pass the bar
|
|
8111
8248
|
* (tip >= {@link TIP_RATIO_FLOOR} x root). The carrier named is the bone the part is bound to ({@link boundBones}) at
|
|
8112
|
-
* or under `K` that no other such bone is above
|
|
8249
|
+
* or under `K` that no other such bone is above. Null otherwise.
|
|
8250
|
+
*
|
|
8251
|
+
* Issue #987: the halves are read in the frame of the bone the part hangs from (`m.hang`), so only a breath keyed
|
|
8252
|
+
* strictly under that bone is read, and the held pose goes through the same frame (`m.toCarrier`). Before #987 a
|
|
8253
|
+
* sleeve whose chain rides `chest` was named carried by the chain's own first link, and the advice told the author to
|
|
8254
|
+
* drop that link's segment; the breath on `chest` is now in the frame and carries nothing. `link` says the carrier
|
|
8255
|
+
* named is a link of a chain `CHAIN_LAG` reads, whose segment the advice must not tell the author to drop.
|
|
8113
8256
|
*/
|
|
8114
8257
|
function breathCarrier(
|
|
8115
8258
|
rig: Record<string, unknown>,
|
|
@@ -8117,8 +8260,18 @@ function breathCarrier(
|
|
|
8117
8260
|
part: string,
|
|
8118
8261
|
geometryPath: string,
|
|
8119
8262
|
frames: number,
|
|
8120
|
-
m: {
|
|
8121
|
-
|
|
8263
|
+
m: {
|
|
8264
|
+
art: ArtMask;
|
|
8265
|
+
box: { x: number; y: number; w: number; h: number };
|
|
8266
|
+
stage: StageBox;
|
|
8267
|
+
shown: ReadonlyArray<PosedAttachment | null>;
|
|
8268
|
+
toward: 'down' | 'up';
|
|
8269
|
+
root: number;
|
|
8270
|
+
tip: number;
|
|
8271
|
+
hang: string | null;
|
|
8272
|
+
toCarrier: (i: number, posed: readonly number[]) => number[];
|
|
8273
|
+
},
|
|
8274
|
+
): { bone: string; keyed: string; breath: number; root: number; tip: number; passes: boolean; link: boolean } | null {
|
|
8122
8275
|
const anims = isRecord(motion.animations) ? motion.animations : {};
|
|
8123
8276
|
const idle = isRecord(anims.idle) ? anims.idle : {};
|
|
8124
8277
|
const keyed = [...new Set((Array.isArray(idle.tracks) ? idle.tracks : []).filter(isRecord).filter((t) => typeof t.bone === 'string' && typeof t.property === 'string' && BREATH_PROPS.includes(t.property)).map((t) => String(t.bone)))];
|
|
@@ -8154,7 +8307,10 @@ function breathCarrier(
|
|
|
8154
8307
|
return { a: b.a as number, b: b.b as number, c: b.c as number, d: b.d as number, worldX: b.worldX as number, worldY: b.worldY as number };
|
|
8155
8308
|
};
|
|
8156
8309
|
const bound = [...boundBones(rig, part)];
|
|
8310
|
+
const links = new Set(chainFigures(rig, motion).chains.filter((c) => c.length >= 2).flatMap((c) => c.map((l) => l.bone)));
|
|
8157
8311
|
for (const k of keyed) {
|
|
8312
|
+
// Issue #987: a breath at or above the hang bone is in the frame the halves are read in, so it carries nothing.
|
|
8313
|
+
if (m.hang !== null && (k === m.hang || !atOrUnder(k, m.hang))) continue;
|
|
8158
8314
|
const under = bound.filter((b) => atOrUnder(b, k));
|
|
8159
8315
|
const carrier = under.find((b) => !under.some((o) => o !== b && atOrUnder(b, o)));
|
|
8160
8316
|
if (carrier === undefined) continue;
|
|
@@ -8174,11 +8330,11 @@ function breathCarrier(
|
|
|
8174
8330
|
const breath = Math.max(...delta.map(([x, y]) => edgeLength(x, y)));
|
|
8175
8331
|
if (!(breath > 0)) continue;
|
|
8176
8332
|
const share = weights.map((v) => v.filter((w) => atOrUnder(String(w.bone), k)).reduce((s, w) => s + (w.weight as number), 0));
|
|
8177
|
-
const held = m.shown.map((f, i) => (f === null ? null : { rest: f.rest, triangles: f.triangles, posed: f.posed.map((v, j) => v - share[j >> 1] * delta[i][j & 1]) }));
|
|
8333
|
+
const held = m.shown.map((f, i) => (f === null ? null : { rest: f.rest, triangles: f.triangles, posed: m.toCarrier(i, f.posed.map((v, j) => v - share[j >> 1] * delta[i][j & 1])) }));
|
|
8178
8334
|
const t = halfTravels(m.art, m.box, m.stage, held, m.toward);
|
|
8179
8335
|
if (t.root === null || t.tip === null) continue;
|
|
8180
8336
|
if (!(m.root - t.root > Math.max(0, m.tip - t.tip)) || round3(t.tip) === 0) continue;
|
|
8181
|
-
return { bone: carrier, keyed: k, breath, root: t.root, tip: t.tip, passes: t.tip >= TIP_RATIO_FLOOR * t.root };
|
|
8337
|
+
return { bone: carrier, keyed: k, breath, root: t.root, tip: t.tip, passes: t.tip >= TIP_RATIO_FLOOR * t.root, link: links.has(carrier) };
|
|
8182
8338
|
}
|
|
8183
8339
|
return null;
|
|
8184
8340
|
}
|