@rig-c/parts 1.29.0 → 1.30.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 +17 -6
- package/package.json +1 -1
- package/src/propose.ts +96 -5
package/docs/AUTHORING.md
CHANGED
|
@@ -122,7 +122,7 @@ read by no CPU stage).
|
|
|
122
122
|
| `motion.blink.still` | rig | **proposed** only for a lash `propose` notes (below), then checked: `{<part>: {row, bone}}` — the rows of that region part above `row` (rig px, y down) are drawn by a second slot `<part>_still` on `bone` and do not blink; rows from `row` down keep the part's slot and blink. The part must be a region on a bone `eyes` names, `bone` one it does not (`CONFIG_STILL_OFF_THE_BLINK`), and `row` a row of the part with no art across its whole width (`RIG_STILL_ROW_CLEAR`) |
|
|
123
123
|
| `motion.animations_from` | rig; check (named; read for folds) | optional, never proposed (issue #183): a path, relative to the config file's directory, to a file in rig-c's motion-spec shape, `{"spec": "rigc-motion/1", "animations": {"<name>": {"duration": …, "tracks": [...]}, …}}` — an expression, a gesture, anything beside the idle. On every `rig` and `build` each of its animations is written into `rig/motion.json` after `idle`, as read: nothing added, re-keyed or judged here, and each is rigc's to accept or refuse at the rig stage's gate, in rigc's words. It may name what `motion.json` already holds — the bones `bones` declares, the easings `shut` and `open`, the groups `eyes` and `brows` when the blink writes them — and nothing else from the file reaches it. The loader refuses, every problem at once: no such file or one that cannot be read (`CONFIG_FILE_PRESENT`), text that is not JSON (`CONFIG_IS_JSON`), a file that is not an object, a `spec` other than `"rigc-motion/1"`, an `animations` that is not a table of objects (`CONFIG_FIELD_TYPE`), any other key — rig-c's `easings`, `groups`, `setup`, `physics`, `mix`, `archetype` and `cut` included, which would be read and not written (`CONFIG_KEY_KNOWN`), an animation named `idle` (`CONFIG_ANIMATION_NAME_FREE`), and a key written twice in one object of the file, which JSON would fold into its last value without a word (`CONFIG_KEY_UNIQUE`). `check` names the others on one line (`beside the idle: …`) and reads each for folds, with the idle — `ANIMATIONS_DO_NOT_FOLD` (§7, issues #320, #333); every other bar reads the idle. Absent, `motion.json` holds the idle alone, byte for byte as before, and `check` reads that idle for folds |
|
|
124
124
|
| `constraints` | rig (handed to rig-c as `rig.json`'s `constraints`); `compare` and the coverage lines (roles) | **authored**, optional; `propose` writes none, but with `--cloth` (issue #364), where it writes the recipe's physics and swing transforms (*Recipe: cloth and hair that answer the world*, below). A list of constraints in **rig-c's own rig-spec shape** (`RigConstraint`: `ik`, `transform`, `path`, `physics`, `slider` — rig-c's `src/rig.ts` documents every field), in the order rigc applies them. The loader checks only what this package owns: each entry is an object with a `type` rigc names (`CONFIG_CONSTRAINT_TYPE_KNOWN`) and a non-empty `name`, unique within its kind as rigc finds constraints (`CONFIG_CONSTRAINT_NAME_UNIQUE`; an ik and a transform may share one); every bone it names — `bones`, an ik's `target`, a transform's `source`, a physics or slider constraint's `bone` — is a bone `bones` declares, a chain link as `<chain><i>` (`CONFIG_NAME_RESOLVES`); the bone a constraint follows does not sit under a bone it drives (`CONFIG_CONSTRAINT_TARGET_DETACHED`). Every other field (`mix`, `softness`, `properties`, a path's `slot`, a slider's `animation`) and every value is rigc's to accept or refuse at the rig stage's gate, in rigc's words (`RIG_RIGC_GREEN`) — an ik's `bones` as a shape too: from rig-c 2.15.0 rigc refuses an ik over more than two bones, or over a pair whose second bone is not the first's child (issue #103; this loader refused both itself until then). A note and an `x-` record may ride on an entry; the rig stage leaves them out of what it hands rigc. Under the default `--idle-keys ctl` a two-bone ik whose child link the idle keys is split by that link's control, and rigc refuses the pair: run `rig --idle-keys direct` or `build --idle-keys direct` (the rig stage adds that sentence, naming the flag on the command that ran, to rigc's line; `build` forwards it to its rig stage, issue #95). Each bone a constraint follows is declared to rigc as `invariants.detached` from every bone it drives, which rigc's gate rule `A25` checks |
|
|
125
|
-
| `stops` | rig (handed to rig-c as `rig.json`'s `stops`, and moves slots, mesh weights and child bones on stopped links onto `<link>_d`) | **authored**, optional (
|
|
125
|
+
| `stops` | rig (handed to rig-c as `rig.json`'s `stops`, and moves slots, mesh weights and child bones on stopped links onto `<link>_d`); `propose --from-config` (`LINT stop on <chain> …`) | **authored**, optional (issues #404, #447): `[{chain, links?, against, at, inward, outward, lower?: {inward, outward}}]` — rig-c's leg stops (`RigStop`, rig-c 2.56.0, `rig-c/src/stop.ts`). `chain` is a chain `bones` declares (`CONFIG_NAME_RESOLVES`, `RIG_NAME_RESOLVES`); `links`, optional, is a non-empty list of links of that chain (`CONFIG_NAME_RESOLVES`, `CONFIG_FIELD_TYPE`), or absent for every link of `chain` in chain order; `against` is a bone `bones` declares, a chain link as `<chain><i>` (`CONFIG_NAME_RESOLVES`, `RIG_NAME_RESOLVES`); `at` is `[x, y]` in `against`'s local space, as rig-c's stop takes it (`CONFIG_FIELD_TYPE`): since each chain link (`leg_r0`, `leg_r1`) is turned to point along its segment (`+x` from origin to next link or tip, `+y` 90° counter-clockwise in Spine world), `at: [d, n]` sits `d` px from `against`'s origin toward its end and `n` px perpendicular to it; `inward`, `outward` and `lower.inward`, `lower.outward` are finite numbers (`CONFIG_FIELD_TYPE`). Every other stop rule — non-negative bounds, each stopped link written by a constraint, `against` not under the stopped chain, the aim not degenerate, `\|rest\| + outward <= 180°` (`RIG_STOP_BOUNDS`) — is rigc's to accept or refuse at the rig stage's gate, in rigc's words (`RIG_RIGC_GREEN`). **Rising-limb / prone geometry (issue #447):** rig-c's `expandStops` aims `<chain>_rest` from `links[0]`'s setup origin at `<chain>_touch` (`at` in `against`'s local frame) and clamps `<link>_d` inside `[rest − inward, rest + outward]`; Spine wraps rotation at `±180°`, so `\|rest\| + outward <= 180°` is required (`RIG_STOP_BOUNDS`). Whenever `<chain>_touch` lies **above** `links[0]`'s root in rig pixels (`ty < y0`, i.e. higher Spine `y` — e.g. stopping a hanging cloth chain against a rising shin `leg_<s1>` in a prone or kneeling pose where the knee sits below `links[0]` and the ankle above it, so positive `at[0]` walks upward along the shin past `links[0]`'s root), `rest` lands in the upper half-plane (`\|rest\| > 90°`) and `\|rest\| + outward` exceeds `180°`. Stop against the descending thigh (`leg_<s0>`), set `links` to start at a link whose root sits above `<chain>_touch`, or omit the stop when the rising shin sits outside the hanging chain's sweep; `propose --from-config` prints `LINT stop on <chain> …` with the exact coordinates and angles when `<chain>_touch` lies above `links[0]`'s root or `\|rest\| + outward > 180°`. For every stopped link `<link>`, the rig stage moves any slot on `<link>`, any mesh weight on `<link>`, and any bone whose parent is `<link>` (a branched cloth tree's branches hanging from `<trunk_last>`, before `--idle-keys ctl` adds `<bone>_ctl` and before `turn` adds `turn_<part>_<bone>`) onto `<link>_d`, the drawn link bone rig-c's `expandStops` creates at `<link>`'s setup frame to carry the clamped rotation. Absent, `rig.json` carries no `stops` and no byte of any output moves |
|
|
126
126
|
| `turn` | rig (adds the turn control) | **proposed** only with `propose --full`/`--head` (issue #251), then corrected: `{pivot, depths}`. `depths` holds, per part, the See-through layer's `depth_median` the part came from (larger = further back), read from the run the part was assembled from; a `painting:` patch has none and is not carried. A rider (issue #271) — a part from an `eyewhite`, `irides`, `eyelash`, `eyebrow`, `nose` or `mouth` layer — is drafted at the `face` part's depth, 0.001 nearer when parts.json draws it after the face and 0.001 further when before: See-through makes no depth of its own for the eye group (it cuts them from one depth map of the combined eyes) and clamps nose and mouth to the face's median − 0.001, and the two runs' own medians for these layers disagreed by up to 0.47 and swapped their order against the face. Every other layer — ears, headwear and earwear included — keeps its own median. With no `face` part, or more than one, the riders keep their own and a note says so. One note lists each part's depth and where it came from; a carrier the other run places more than 10 % of `pivot − nearest` away gets a note naming both values and the run read — the two runs are not reconciled. `pivot` is the depth that stays put; `propose` drafts the mean of the parts' depths weighted by `opaque_px`, the one depth at which the painted pixels' centroid does not slide under the dial. The rig stage adds a bone `turn` under `root` at its origin that nothing keys — its local x is the dial, in rig px — and carries each part by `(pivot − depth) / (pivot − nearest depth)` of it: the nearest part all of it, a part behind the pivot the other way, a part at the pivot not at all. Each part rides leaf twins (`turn_<part>_<bone>`, an identity child of every bone its slot and weights bind) moved by one `transform` constraint (`turn_<part>`, after the config's own); the idle's keys, the `<bone>_ctl` controls and a physics constraint stay on the bones, and the dial moves none of them. At rest: with `turn` declared, no pixel differs by more than 1 level at dial 0 (not byte-identical; the cause is float noise in the identity child bones, and the constraints add nothing); without `turn`, byte-identical. Neither field has a default (`CONFIG_FIELD_PRESENT`); the pivot must lie behind the nearest part (`CONFIG_TURN_PIVOT_BEHIND_NEAREST`). Absent, no control is added and no byte of any output moves |
|
|
127
127
|
| `scene` | assemble (the recomposite, its holes and the figure's mask); check, through `parts.json`'s `scene` block (`RECOMPOSITE_HOLES`, the seam) | **authored**, optional; `propose` writes none (issue #242). `{}` declares the painting a scene: the figure and its surroundings in one image, See-through's full run holding only the figure. With it, `assemble` reads the recomposite figures, the hole list and the `uncovered:` lines inside the figure — the rig pixels where any full-run layer's alpha is above 8 (`OPAQUE_ALPHA_ABOVE`, the cut every layer's painted pixels are read by) — each line starting `inside the figure (config.scene: …, <n> of <m> rig px)`; writes that figure as `figure_rig.png` (white on black) beside the recomposite, and a `scene` block in `parts.json`; flags error pixels on the error map inside it only (outside, the painting grey); and `check` leaves out of the seam every frame pixel whose composite sample reaches the painting's outermost row or column, saying how many (`seam_border` in `check.json`, and on the seam line). It takes no field: the figure and its threshold are derived. A key in it other than a record or an annotation is `CONFIG_KEY_KNOWN`. Absent, no byte of any output moves |
|
|
128
128
|
| `cloth` | `propose --cloth` only; no other stage reads it, so it moves no byte of any build | **authored**, optional (issue #364): one recipe per tag, `{"bottomwear": {…}, "back hair": {…}, "front hair": {…}}`, each `{chains, links, wind, gravity, split?, length_ratio?, strength?, damping?, alternate_damping?, translate?, swing?, part?}`. `chains` — trees across the piece (for `back hair`, per side of the eye axis), a whole number, 1 or more; `links` — links from root to tip on every path, 1 or more; `split` — `{at, into}`: the first `at` links are the trunk, which forks into `into` (2 or more) branches; `at` below `links` (`CONFIG_CLOTH_SPLIT_BEFORE_TIP`); `length_ratio` — a path's last link over its first, above 0, default 0.5; `strength` — `{trunk, branch, edge}`, each above 0 and below the spring's stability bound (`CONFIG_CLOTH_SPRING_STABLE`), default 100 / 100 / 50; `damping` — in [0, 1], absent: rig-c's default; `alternate_damping` — the odd trees' damping, in [0, 1], beside `damping` and different from it (`CONFIG_CLOTH_ALTERNATE_DIFFERS`); `wind`, `gravity` — `[root, tip]`, each 0 or more, tip at or above root (`CONFIG_CLOTH_RESPONSE_GROWS_TO_TIP`); `translate` — the x and y mix of a dangling end's flutter, above 0 and at most 1; `swing` — `bottomwear` only, `{scales}`, one positive scale per tree (`CONFIG_CLOTH_SWING_SCALE_PER_TREE`); `part` — `bottomwear` only (`CONFIG_CLOTH_PART_ON_BOTTOMWEAR`), a non-empty part or cut-piece name: the `topwear` part that holds a skirt See-through returned inside it (a dress), or its skirt piece cut by `assemble.cuts`, which takes the skirt's trees (issue #402, *A skirt inside topwear* below; the part is checked by `propose`, `PROPOSE_CLOTH_PART`). The two defaults are the recipe's own, measured on its synthetic fixture (the recipe below). The recipe below says what each field drafts |
|
|
@@ -293,7 +293,7 @@ What a joint with a position places, as given — no ratio is applied over it:
|
|
|
293
293
|
| `neck` | `neck` | the rule: half way from the chin to the neck part's bottom (or 0.12 face heights below the chin), on the eye axis |
|
|
294
294
|
| `hip` | the midpoint of `r_hip` and `l_hip` — a point between two joints, said as that | the hip rule (bottomwear top, the waist, or 0.32 of the figure); one hip alone is said to be unused |
|
|
295
295
|
| `chest` | its rule, half way from neck to hip, taken along the line between the placed ends | the rule as before when neither end is a joint |
|
|
296
|
-
| `sleeve_<s>` | `<s>_shoulder` → `<s>_elbow` (two links), tip at `<s>_wrist`, with the two-blob rule's first two amplitudes; the arm is a limb (issue #349), `arm_<s>`: its handwear part is cut at the elbow into `upperarm_<s>` on link 0 and `forearm_<s>` on link 1, each a region — no mesh, no chest stub — when every handwear part over 500 px holds one such wrist within 15 px of its art (the larger of the x and y gaps, the LINT's own tolerance), or is bound one by `--wrist-snap` (below) | the sleeve rule places every sleeve chain, and each arm joint's note says why: for a wrist on no part, `… lies on the art of no handwear part within 15 px, so it does not say which sleeve it ends; the nearest is <part>, <d> px from its art by the larger of the x and y gaps (--wrist-snap <d> would bind it)` — `(beyond --wrist-snap <px>)` when the flag was given and fell short, and `the nearest are <a> and <b>, each <d> px …, so no tolerance says which` when two parts tie |
|
|
296
|
+
| `sleeve_<s>` | `<s>_shoulder` → `<s>_elbow` (two links), tip at `<s>_wrist`, with the two-blob rule's first two amplitudes; the arm is a limb (issue #349), `arm_<s>`: its handwear part is cut at the elbow into `upperarm_<s>` on link 0 and `forearm_<s>` on link 1, each a region — no mesh, no chest stub — when every handwear part over 500 px holds one such wrist within 15 px of its art (the larger of the x and y gaps, the LINT's own tolerance), or is bound one by `--wrist-snap` (below); when pre-separated `handwear-r` and `handwear-l` parts cross so that `<s>_wrist` lies on the opaque art of its own side's `handwear-<s>` part and also within 15 px of the opposite side's `handwear-<other>` part, it resolves to its own side (issue #458) | the sleeve rule places every sleeve chain, and each arm joint's note says why: for a wrist on no part, `… lies on the art of no handwear part within 15 px, so it does not say which sleeve it ends; the nearest is <part>, <d> px from its art by the larger of the x and y gaps (--wrist-snap <d> would bind it)` — `(beyond --wrist-snap <px>)` when the flag was given and fell short, and `the nearest are <a> and <b>, each <d> px …, so no tolerance says which` when two parts tie |
|
|
297
297
|
| `eye_<s>`, `head`, the face box | not moved: they are measured off the parts | the note prints the eye joint's distance from the eye bone, with no bar |
|
|
298
298
|
| `leg_<s>` (issue #241), `foot_<s>` | on a `legwear` part over 500 px: `<s>_hip` → `<s>_knee` (two links), tip at `<s>_ankle`, parent `hip` — when every such part holds the ankle of a leg whose three joints are given, within 15 px of its art; a part holding both ankles gets a chain per leg. The leg is a limb (issue #349), `leg_<s>`: the part is cut at the knee into `thigh_<s>` on link 0 and `shin_<s>` on link 1, each a region — no mesh, no `hip` stub (until #349 the thigh was meshed with one, which pinned it to the hip as the chest stub pinned a sleeve); two legs on one part are split between the two limbs by the cut. When every leg's ankle lies within 15 px of the art of exactly one `footwear` part over 500 px, each leg gets `foot_<s>`, a single bone at the ankle under `leg_<s>1` with `inherit` `"noRotationOrReflection"`, and the footwear part is cut between the feet as `foot_<s>`, the limb's third segment; otherwise no foot is drafted and the note gives each ankle's distance to the nearest footwear art. No idle track: the chain is drafted whatever the pose, and whether the legs move is the scene's. A note per limb says what was cut and why | no leg with all three joints: no chain, and the part is the trunk rule's region with the note it always had, byte for byte. A complete leg whose ankle lies on no legwear part, or on two: no chain, and a note `<parts>: no leg chain — <why>; the trunk rule places it` |
|
|
299
299
|
| the trunk parts' segments (issue #243) | on the `neck` (head run), `topwear` and `bottomwear` meshes, each `[bone, from, to]` segment the rule drafts down the screen is laid along neck → hip, the line the chest is placed on — `neck` + 18 px along it to `chest`; a topwear part's `chest` segment from its art's nearest reach toward the neck end to 20 px short of `hip`, and its `hip` segment from there to 100 px past `hip`; a bottomwear part's `chest` segment, when it has one (issue #262: only when its art reaches above `chest`), from 30 px short of `chest` to 20 px short of `hip` — and the bottomwear waist segment runs across that line, 5 px past `hip`, over 0.09 to 0.91 of the art's width across it. The `head` segment on the neck mesh and the one-blob sleeve rule's chest segment are not moved. A note, `trunk segments (<parts>): laid along neck -> hip …`, gives the line's angle | neither end a joint: the vertical draft, byte for byte. Neck and hip on one point: no line, the vertical draft, and a note says so. An upright line over boxes tight on their art writes the vertical draft's bytes |
|
|
@@ -1757,9 +1757,20 @@ px, either way alike; held three cycles each from a fresh track, 10.299 / 10.277
|
|
|
1757
1757
|
then calm) no skirt link that clears the thighs at rest crosses into one: 0 crossings.
|
|
1758
1758
|
A consumer's hip motion can make the hem cross: on the same copy, under a hip sway
|
|
1759
1759
|
applied to it through the live track (±8 degrees and ±12 px, a 2 s period), skirt links
|
|
1760
|
-
cross on 224 (calm) and 228 (wind (1, 0)) of 360 steps. That case is
|
|
1761
|
-
|
|
1762
|
-
|
|
1760
|
+
cross on 224 (calm) and 228 (wind (1, 0)) of 360 steps. That case is `config.stops`
|
|
1761
|
+
(issues #404, #447, §2 `stops`): each entry clamps its chain's links against a limb
|
|
1762
|
+
bone (`against`, e.g. `leg_r0`) at `at: [x, y]` in `against`'s local frame (`+x`
|
|
1763
|
+
along the bone from its origin to its next link or tip). Because rig-c's stop aims
|
|
1764
|
+
`<chain>_rest` from `links[0]`'s origin at `<chain>_touch` (`at` on `against`) and
|
|
1765
|
+
requires `|rest| + outward <= 180°` so Spine's `±180°` wrap sits outside the outward
|
|
1766
|
+
bound (`RIG_STOP_BOUNDS`), `<chain>_touch` must lie below `links[0]`'s root in rig
|
|
1767
|
+
pixels (`ty >= y0`). On a prone or kneeling pose where the thigh (`leg_<s0>`, hip ->
|
|
1768
|
+
knee) descends and the shin (`leg_<s1>`, knee -> ankle) rises upward toward the ankle,
|
|
1769
|
+
`leg_<s1>`'s local `+x` points upward, so a positive `at[0]` on `leg_<s1>` can place
|
|
1770
|
+
`<chain>_touch` above `links[0]`'s root (`|rest| > 90°`): stop against the descending
|
|
1771
|
+
thigh `leg_<s0>` instead, restrict `links` to links rooted above `<chain>_touch`, or
|
|
1772
|
+
omit the stop on a rising shin that lies outside the hanging chain's sweep. When
|
|
1773
|
+
`--keypoints` drafts leg chains, a note says so.
|
|
1763
1774
|
|
|
1764
1775
|
## 4. The command order
|
|
1765
1776
|
|
|
@@ -2129,7 +2140,7 @@ move off it (`AS245`).
|
|
|
2129
2140
|
| `layers` | the table, then one `author-made: layer "author:<label>" — …` line per layer the author made (group `author` in the table; §5, *A layer the author made*), then the `WARN` lines | every tag the plan will need has opaque pixels; one `face` in the head run; `0 WARN line(s)` | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`); a `WARN PLAN_LAYER_…` line (§6, *Plausibility*) |
|
|
2130
2141
|
| `sheet` of both runs | the tile list (and the sheet, if you can see) | eyes, irises, lashes and brows as left/right pairs in the head run | a head box that cut off an ornament: move `head_box`, re-run the head crop |
|
|
2131
2142
|
| `assemble` | one line per part — its counts (`op=` opaque, `vis=` visible, `src=` taken / candidates, `unproj=` visible but not projected, `drift=<n> (<p>% of core)`, `merged=`, `seam=`; *parts.json*, below). `drift=` is `refused_drift_px` in rig px and its share of the part's candidates, `projected_core_px`, which it is a subset of (issue #314); no bar reads either. On the four public examples the share runs to 94.5 % (`scarf` `hair_back`, 479 of 507) and the count to 26,472 (`scarf` `topwear`, 22.3 %), and the seam override repaints refused top-most pixels afterwards, so a large count says where See-through's colour was not taken, not that the part is unlike the painting: the `drift:` line below is the one that names a body of them another part holds. Then the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`, then one `uncovered: <n> of the <N> uncovered error px lie on the full run's "<tag>", in a body of <px> px at x,y wxh; <the rule that left it out>` line per full-run layer the plan does not take from the full run whose uncovered error holds a body (5x5-opened, 8-connected, at least 150 full-run px in rig px), then one `drift: "<part>" refuses <n> px of <run>:<tag> (its colour more than 90 off the painting, max channel), in a body of <px> px at x,y wxh (…); by the plan's draw order "<holder>" holds <px> px of it (…), "<holder>" drawn in front of "<part>"` line per part whose drift refusals hold a body (the same opening and bar) that another part holds in the draw order — that layer is not what the painting shows there (none on the five public paintings); and look at `render/recomposite_error_rig.png`. Between the part lines and the totals, one `author-made: part "<part>" (<run>:author:<label>) — …` line per part from a layer the author made (below, *A layer the author made*), then one `cut: "<part>" opaque <n> = "<part>" <kept> + "<piece>" <taken> …` line per part an `assemble.cuts` entry cut (below; it ends `; under <n> px filled, <m> px left empty` when a cut of that part has `under`), then — with `config.limbs` (issue #349) — one `limb cut: "<part>" opaque <n> = "<segment part>" <px> + …` line per part a limb takes and one `limb cap:` line per joint, its cap's pixels from the cap image, from the layer and missing, and its box — under `cap_shape: "disc"` also from the upper segment, under `edge: "feather"` the pixels faded, under `cut_band` the line the cut was placed on and the stroke pixels it and the joint line cross (issue #360) (§3, *Limbs: cut at their joints*), then one `fringe pushed back: "<part>" <px> px, where the painting shows "<holder>" <px> px, …` line per part whose fringe was cleared (below) | on the examples: `reference-sample` 0.83 / 98.0 % / 4,359 / 1,185, 259 holes, the largest 94 px; `reference-demo` 2.34 / 95.8 % / 10,340 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (reference-demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. When neither run holds it at all, an `assemble.patches` entry cuts it from the painting (below) |
|
|
2132
|
-
| `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 or whose link or tip leaves its mass's column range (issue #445): omit `chains` on `cloth."back hair"` or draft one tree 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 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 |
|
|
2143
|
+
| `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 or whose link or tip leaves its mass's column range (issue #445): omit `chains` on `cloth."back hair"` or draft one tree 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 |
|
|
2133
2144
|
| `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 |
|
|
2134
2145
|
| `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` |
|
|
2135
2146
|
| `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) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.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/propose.ts
CHANGED
|
@@ -127,7 +127,8 @@
|
|
|
127
127
|
import { existsSync } from 'node:fs';
|
|
128
128
|
import { join } from 'node:path';
|
|
129
129
|
import { drawText, GLYPH_H } from 'rig-c/tools/font5x7.ts';
|
|
130
|
-
import { type BoneEntry, type ConfigConstraint, type Limb, type MeshSpec, parseConfig, type Point, type Segment, type SkeletonSections } from './config.ts';
|
|
130
|
+
import { type BoneEntry, type ConfigConstraint, type ConfigStop, type Limb, type MeshSpec, parseConfig, type Point, type Segment, type SkeletonSections } from './config.ts';
|
|
131
|
+
import { computeExactFrameTransforms, cropToSpineY, normaliseDegrees, toWorld } from './coords.ts';
|
|
131
132
|
import { PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
132
133
|
import { eyeLineDegrees, type FrameFeature, headAxes, type HeadAxes, headFrame, type HeadFrame } from './headframe.ts';
|
|
133
134
|
import { type LayerSet, OPAQUE_ALPHA_ABOVE } from './layers.ts';
|
|
@@ -3471,7 +3472,10 @@ function artDistance(m: Mask, at: readonly [number, number]): number {
|
|
|
3471
3472
|
* carries one: a part left to the rule beside parts placed from joints would
|
|
3472
3473
|
* be read by two rules at once. Sides come from the joints, not from the eye
|
|
3473
3474
|
* axis, so a figure seen from behind or lying across the axis is paired by
|
|
3474
|
-
* where its wrists are.
|
|
3475
|
+
* where its wrists are. Issue #458: when pre-separated `handwear-r` and
|
|
3476
|
+
* `handwear-l` parts cross so that `<s>_wrist` lies on the opaque art of its
|
|
3477
|
+
* own side's `handwear-<s>` part and also within 15 px of the opposite side's
|
|
3478
|
+
* `handwear-<other>` part, it resolves to its own side's part.
|
|
3475
3479
|
*
|
|
3476
3480
|
* Issue #199: a wrist on no part within that tolerance is measured to every
|
|
3477
3481
|
* part by the same Chebyshev distance ({@link artDistance}). With
|
|
@@ -3498,7 +3502,18 @@ function armsFromJoints(P: PartSet, hw: readonly PartRecord[], joints: RigJoints
|
|
|
3498
3502
|
const near = new Map(hw.map((p) => [p, dilate(P.alpha(p), 2 * WRIST_ON_PART_PX + 1)]));
|
|
3499
3503
|
const arms: JointArm[] = [];
|
|
3500
3504
|
for (const a of complete) {
|
|
3501
|
-
const
|
|
3505
|
+
const raw = hw.filter((p) => isOn(near.get(p) as Mask, a.wrist[0], a.wrist[1]));
|
|
3506
|
+
const ownTag = `handwear-${a.side}`;
|
|
3507
|
+
const oppTag = `handwear-${a.side === 'r' ? 'l' : 'r'}`;
|
|
3508
|
+
const own =
|
|
3509
|
+
raw.length > 1 &&
|
|
3510
|
+
raw.every((p) => {
|
|
3511
|
+
const t = splitFrom(p.from)[1];
|
|
3512
|
+
return t === ownTag || t === oppTag;
|
|
3513
|
+
})
|
|
3514
|
+
? raw.filter((p) => splitFrom(p.from)[1] === ownTag && isOn(P.alpha(p), a.wrist[0], a.wrist[1]))
|
|
3515
|
+
: [];
|
|
3516
|
+
const on = own.length === 1 ? own : raw;
|
|
3502
3517
|
if (on.length > 1) return { arms: null, why: `${a.side}_wrist at ${fmtAt(a.wrist)} lies on the art of ${on.length} handwear parts (${on.map((p) => p.name).join(', ')}), so it does not say which sleeve it ends` };
|
|
3503
3518
|
if (on.length === 1) {
|
|
3504
3519
|
arms.push({ ...a, part: on[0], snap: null });
|
|
@@ -4166,6 +4181,23 @@ export interface CrossedLimbsFinding {
|
|
|
4166
4181
|
at: [number, number];
|
|
4167
4182
|
}
|
|
4168
4183
|
|
|
4184
|
+
/**
|
|
4185
|
+
* Issue #447: a stop's contact point `<chain>_touch` (`at` in `against`'s local frame) lies above the first stopped
|
|
4186
|
+
* link's root (`links[0]` at `[x0, y0]`) in rig pixels (`ty < y0`, i.e. higher Spine y), so `<chain>_rest` aims
|
|
4187
|
+
* upward into the upper half-plane (`|rest| > 90°`) and `|rest| + outward > 180°` wraps past ±180° (`RIG_STOP_BOUNDS`).
|
|
4188
|
+
*/
|
|
4189
|
+
export interface StopTouchAboveRootFinding {
|
|
4190
|
+
kind: 'stop-touch-above-root';
|
|
4191
|
+
chain: string;
|
|
4192
|
+
firstLink: string;
|
|
4193
|
+
rootAt: [number, number];
|
|
4194
|
+
against: string;
|
|
4195
|
+
at: [number, number];
|
|
4196
|
+
touchAt: [number, number];
|
|
4197
|
+
rest: number;
|
|
4198
|
+
outward: number;
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4169
4201
|
export type LintFinding =
|
|
4170
4202
|
| OffArtFinding
|
|
4171
4203
|
| OffLockFinding
|
|
@@ -4179,7 +4211,8 @@ export type LintFinding =
|
|
|
4179
4211
|
| NearEmptyHandFinding
|
|
4180
4212
|
| BackHairMassesFinding
|
|
4181
4213
|
| OverlappingArmsFinding
|
|
4182
|
-
| CrossedLimbsFinding
|
|
4214
|
+
| CrossedLimbsFinding
|
|
4215
|
+
| StopTouchAboveRootFinding;
|
|
4183
4216
|
|
|
4184
4217
|
/** Which rule set read the torso and the sleeve chains, under `--keypoints`. */
|
|
4185
4218
|
export interface LintBasis {
|
|
@@ -4279,7 +4312,7 @@ export function coverageOutcome(c: BoneCoverage): CoverageOutcome {
|
|
|
4279
4312
|
*/
|
|
4280
4313
|
export function lint(
|
|
4281
4314
|
P: PartSet,
|
|
4282
|
-
spec: { bones: readonly BoneEntry[]; meshes: Readonly<Record<string, MeshSpec>>; regions?: Readonly<Record<string, string>>; motion?: SkeletonSections['motion']; constraints?: readonly ConfigConstraint[]; limbs?: Readonly<Record<string, Limb
|
|
4315
|
+
spec: { bones: readonly BoneEntry[]; meshes: Readonly<Record<string, MeshSpec>>; regions?: Readonly<Record<string, string>>; motion?: SkeletonSections['motion']; constraints?: readonly ConfigConstraint[]; limbs?: Readonly<Record<string, Limb>>; stops?: readonly ConfigStop[] },
|
|
4283
4316
|
joints?: RigJoints,
|
|
4284
4317
|
): LintResult {
|
|
4285
4318
|
const { byName } = expand(spec.bones);
|
|
@@ -4648,6 +4681,61 @@ export function lint(
|
|
|
4648
4681
|
}
|
|
4649
4682
|
}
|
|
4650
4683
|
}
|
|
4684
|
+
// Issue #447: when a stop's contact point `<chain>_touch` (`at` in `against`'s local frame) lies above the first
|
|
4685
|
+
// stopped link's root (`links[0]`) in rig pixels (`ty < y0`, i.e. higher Spine y), `<chain>_rest` aims into the
|
|
4686
|
+
// upper half-plane (`|rest| > 90°`) and `|rest| + outward > 180°` wraps past ±180° (`RIG_STOP_BOUNDS`).
|
|
4687
|
+
if (spec.stops !== undefined && spec.stops.length > 0) {
|
|
4688
|
+
const boneEnds = new Map<string, Point>();
|
|
4689
|
+
const chainLinks = new Map<string, string[]>();
|
|
4690
|
+
for (const e of spec.bones) {
|
|
4691
|
+
if ('chain' in e) {
|
|
4692
|
+
const names = e.points.map((_, k) => `${e.chain}${k}`);
|
|
4693
|
+
chainLinks.set(e.chain, names);
|
|
4694
|
+
e.points.forEach((_, k) => {
|
|
4695
|
+
boneEnds.set(names[k], k + 1 < e.points.length ? e.points[k + 1] : e.tip);
|
|
4696
|
+
});
|
|
4697
|
+
} else if (e.tip !== undefined) {
|
|
4698
|
+
boneEnds.set(e.name, e.tip);
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
for (const s of spec.stops) {
|
|
4702
|
+
const links = s.links ?? chainLinks.get(s.chain) ?? [];
|
|
4703
|
+
if (links.length === 0) continue;
|
|
4704
|
+
const firstLink = links[0];
|
|
4705
|
+
const rootBone = byName.get(firstLink);
|
|
4706
|
+
const againstBone = byName.get(s.against);
|
|
4707
|
+
if (rootBone === undefined || againstBone === undefined) continue;
|
|
4708
|
+
const againstEnd = boneEnds.get(s.against);
|
|
4709
|
+
let againstRot = 0;
|
|
4710
|
+
if (againstEnd !== undefined) {
|
|
4711
|
+
const dx = againstEnd[0] - againstBone.x;
|
|
4712
|
+
const dy = cropToSpineY(againstEnd[1], P.H) - cropToSpineY(againstBone.y, P.H);
|
|
4713
|
+
if (dx !== 0 || dy !== 0) againstRot = (Math.atan2(dy, dx) * 180) / Math.PI;
|
|
4714
|
+
}
|
|
4715
|
+
const frame = computeExactFrameTransforms([
|
|
4716
|
+
{ name: s.against, x: againstBone.x, y: cropToSpineY(againstBone.y, P.H), rotation: againstRot },
|
|
4717
|
+
]).get(s.against)!;
|
|
4718
|
+
const [wx, wy] = toWorld(frame, s.at[0], s.at[1]);
|
|
4719
|
+
const touchAt: [number, number] = [pyRound(wx * 10) / 10, pyRound(cropToSpineY(wy, P.H) * 10) / 10];
|
|
4720
|
+
const aimDx = wx - rootBone.x;
|
|
4721
|
+
const aimDy = wy - cropToSpineY(rootBone.y, P.H);
|
|
4722
|
+
if (aimDx === 0 && aimDy === 0) continue;
|
|
4723
|
+
const rest = pyRound(normaliseDegrees((Math.atan2(aimDy, aimDx) * 180) / Math.PI) * 100) / 100;
|
|
4724
|
+
if (touchAt[1] < rootBone.y || Math.abs(rest) + s.outward > 180) {
|
|
4725
|
+
findings.push({
|
|
4726
|
+
kind: 'stop-touch-above-root',
|
|
4727
|
+
chain: s.chain,
|
|
4728
|
+
firstLink,
|
|
4729
|
+
rootAt: [rootBone.x, rootBone.y],
|
|
4730
|
+
against: s.against,
|
|
4731
|
+
at: [s.at[0], s.at[1]],
|
|
4732
|
+
touchAt,
|
|
4733
|
+
rest,
|
|
4734
|
+
outward: s.outward,
|
|
4735
|
+
});
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4651
4739
|
// Issue #314: every pair of keyed chains a lattice mesh names, swung on the rig stage's own lattice and weights, held
|
|
4652
4740
|
// to check's TEXTURE_STRETCH bar (src/squeeze.ts). Only with a motion: without one nothing swings.
|
|
4653
4741
|
const partImages = new Map(P.recs.filter((p) => P.images.has(p.name)).map((p) => [p.name, { x: p.x, y: p.y, image: P.images.get(p.name) as Raster }]));
|
|
@@ -4686,6 +4774,7 @@ function findingBones(f: LintFinding, bones: readonly BoneEntry[]): string[] {
|
|
|
4686
4774
|
case 'near-empty-hand':
|
|
4687
4775
|
case 'overlapping-arms':
|
|
4688
4776
|
case 'crossed-limbs':
|
|
4777
|
+
case 'stop-touch-above-root':
|
|
4689
4778
|
return [];
|
|
4690
4779
|
case 'back-hair-masses': {
|
|
4691
4780
|
const out = new Set<string>();
|
|
@@ -4878,6 +4967,8 @@ export function lintLine(f: LintFinding): string {
|
|
|
4878
4967
|
return `LINT ${f.part} (${f.from}) holds both arms in one ${f.opaquePx} px component (r_wrist at ${fmtAt(f.rAt)}, l_wrist at ${fmtAt(f.lAt)}): declare config.seethrough.arm_split (x or line, and order: ["handwear-r", "handwear-l"] or ["handwear-l", "handwear-r"]) to separate the overlapping arms into two layers`;
|
|
4879
4968
|
case 'crossed-limbs':
|
|
4880
4969
|
return `LINT ${f.a} and ${f.b} on ${f.part}${f.from === '' ? '' : ` (${f.from})`} cross at ${fmtAt(f.at)} with no per-limb split: nearest-bone splitting cuts a straight line through the crossing; declare config.limbs.<covering>.split (line: [[x, y], ...] or edge: "painting") on the covering limb so the seam follows the limb`;
|
|
4970
|
+
case 'stop-touch-above-root':
|
|
4971
|
+
return `LINT stop on ${f.chain} (${f.firstLink} at ${fmtAt(f.rootAt)}) against ${f.against} (at ${fmtAt(f.at)}) places ${f.chain}_touch at ${fmtAt(f.touchAt)}, above ${f.firstLink}'s root (rest ${pyFixed(f.rest, 2)} deg, |rest| + outward ${pyFixed(Math.abs(f.rest) + f.outward, 2)} deg): stop against the descending limb bone below ${f.firstLink}, set links to start at a link below ${f.chain}_touch, or omit the stop on a rising limb`;
|
|
4881
4972
|
case 'back-hair-masses': {
|
|
4882
4973
|
const massDesc =
|
|
4883
4974
|
f.masses.length === 0
|