rig-parts 2.8.0 → 2.8.1
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 +27 -3
- package/package.json +1 -1
- package/src/check.ts +39 -11
- package/src/config.ts +41 -4
- package/src/meshdraft.ts +6 -22
package/docs/AUTHORING.md
CHANGED
|
@@ -5451,7 +5451,7 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
5451
5451
|
| <a id="BUILD_REQUIREMENTS_CONFLICT"></a>`BUILD_REQUIREMENTS_CONFLICT` | `build --requirements <file>` names a file with other bytes than the one `config.requirements` declares (issue #556): the detail names both, each resolved (the flag against the working directory, the declaration against the config's) with its sha256 — or `no such file`. Refused before assemble writes anything. The same file by both, or a copy of its bytes, is not refused: the declaration is read and recorded | drop the flag to build from the config's file, or point `config.requirements` at the flag's |
|
|
5452
5452
|
| <a id="BUILD_STAGE_REQUEST"></a>`BUILD_STAGE_REQUEST` | an internal build worker was given a missing or malformed request, an unknown stage, or incomplete runner, input, output or scratch paths; the worker refuses before running a stage. This is the internal process protocol, not a public command or option | invoke `rig-parts build` with its documented flags; if that command produces this refusal, report the named request defect rather than constructing or modifying a worker request |
|
|
5453
5453
|
| <a id="REQUIREMENTS_FILE"></a>`REQUIREMENTS_FILE` | `--requirements` names no file, or one that does not parse as a JSON object. Refused before anything is built (by `build`, before assemble) | the path of a `spine-parts-requirements/1` file |
|
|
5454
|
-
| <a id="REQUIREMENTS_FIELD"></a>`REQUIREMENTS_FIELD` | a field of the file is missing, of the wrong type, or not one the format reads: `spec`, `fps` (no default), `requirements`, each requirement's `name` (letters, digits, underscores; once each), `kind`, `animation` and its kind's fields — every bar among them (`within_px`, `fraction`, `tolerance`, `least_drive` above 0, `within_degrees` from 0 to 180, `lo_degrees` ≤ `hi_degrees`, `within_ratio` ≥ 1) — and each target's `bone`, `animation` and exactly one of `at` and `keys` (times strictly increasing), and `not_planted`, `not_swinging`, `rises` and `rests` when given (each a non-empty list of part names, each once; no part both in `not_swinging` and in `rises` or `rests`), and `may_fold` when given (a non-empty list of `{ "slot", "animation", "why" }`, every one a non-blank string, each pair once; issue #320), and `pinholes` when given (an object whose one field, `required`, is `true` or `false`; issue #944), `seam_over_motion` when given (the same shape; issue #941), and `boundary_colour` when given (an object whose one field, `max_step_excess`, is an integer from 0 through 255; issue #942). `note`, `*_note` (strings) and `x-…` (any value) are read by nothing, as in the config | write the field; the bar is yours |
|
|
5454
|
+
| <a id="REQUIREMENTS_FIELD"></a>`REQUIREMENTS_FIELD` | a field of the file is missing, of the wrong type, or not one the format reads: `spec`, `fps` (no default), `requirements`, each requirement's `name` (letters, digits, underscores; once each), `kind`, `animation` and its kind's fields — every bar among them (`within_px`, `fraction`, `tolerance`, `least_drive` above 0, `within_degrees` from 0 to 180, `lo_degrees` ≤ `hi_degrees`, `within_ratio` ≥ 1) — and each target's `bone`, `animation` and exactly one of `at` and `keys` (times strictly increasing), and `not_planted`, `not_swinging`, `rises` and `rests` when given (each a non-empty list of part names, each once; no part both in `not_swinging` and in `rises` or `rests`), and `may_fold` when given (a non-empty list of `{ "slot", "animation", "why" }`, every one a non-blank string, each pair once; issue #320), and `pinholes` when given (an object whose one field, `required`, is `true` or `false`; issue #944), `seam_over_motion` when given (the same shape; issue #941), and `boundary_colour` when given (an object whose one field, `max_step_excess`, is an integer from 0 through 255; issue #942). `note`, `*_note` (strings) and `x-…` (any value) are read by nothing, as in the config | write the field; the bar is yours. For a pair read only by `seam_over_motion.required`, declare `config.motion_seams` `[{part, neighbour}]` and omit a seam requirement. See the [carrier contract](#CONFIG_MOTION_SEAM) and [background over every frame](#background-over-every-frame-issue-941). |
|
|
5455
5455
|
| <a id="REQUIREMENTS_RESOLVES"></a>`REQUIREMENTS_RESOLVES` | a bone, an `ik` or `transform` constraint (by name and `constraint_type`, as rig-c resolves it), a slot, a mesh attachment or an animation the file names is not in `rig.json` or `motion.json`, or a seam's part or a `not_planted`, `not_swinging`, `rises` or `rests` name is not in `parts.json` — the detail lists the ones that are; or an animation whose name cannot be a directory. Refused before anything is built | the name as the rig spells it |
|
|
5456
5456
|
| <a id="REQUIREMENTS_CONSTRAINT_DRIVES"></a>`REQUIREMENTS_CONSTRAINT_DRIVES` | a `follow` names a bone its constraint does not constrain, or a property it does not drive: an `ik` drives `rotate` only; a `transform` drives `rotate` when a `to` names `rotate`, `translate` when one names `x` or `y` | the constraint's own bone and property |
|
|
5457
5457
|
| <a id="REQUIREMENTS_TARGET_PARENT"></a>`REQUIREMENTS_TARGET_PARENT` | a scene target's bone is the root or is not parented to it, or the root is not at rest at the origin, or the animation keys the root — a stage point is a bone's position only under a root that stands still at the origin | a bone under the root for the scene to place |
|
|
@@ -5824,7 +5824,7 @@ the two examples [observed]:
|
|
|
5824
5824
|
| <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 |
|
|
5825
5825
|
| ″ (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 |
|
|
5826
5826
|
| <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 |
|
|
5827
|
-
| <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 |
|
|
5827
|
+
| <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 | carrier-frame 0 / 0 FAILs; otherwise a link with max(root, tip) below 0.5 rig px is SKIP (issue #994); measured 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, or every applicable row is unmeasured, including sub-floor link motion after the zero guard |
|
|
5828
5828
|
| <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`) |
|
|
5829
5829
|
| <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` |
|
|
5830
5830
|
| <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 |
|
|
@@ -5833,6 +5833,30 @@ the two examples [observed]:
|
|
|
5833
5833
|
| <a id="LIMB_ONE_BONE"></a>`LIMB_ONE_BONE` | issue #349. Each part `parts.json` records as a limb segment's (`limb`), read off `rig.json` alone, nothing rendered: the bones its attachments are bound to — a mesh's weights, or its slot's bone for a region — against its segment's bone (under `config.turn`, that bone's leaf twin counts as the bone) | every limb part bound to its one bone alone, weight 1 | not written: no limbs | not written: no limbs | never: written only when `parts.json` holds a limb part, so the examples' `check.json` keep their keys |
|
|
5834
5834
|
| <a id="SEAM_OVER_RANGE"></a>`SEAM_OVER_RANGE` | issue #360. Each limb part `parts.json` records with a `range` (`config.limbs.<limb>.segments[k].range`): a copy of the rig with its physics constraints left out, its joint's bone keyed `rotate` to the range's min, its middle and its max (each angle once, rigc's sense, counter-clockwise on screen), one `rigc build --profile spine` and two `rigc render`s on one grid — one with the joint's own art drawn under the limb (the limb's parts at rest inside the cap's disc, one pixel in from every edge of what is opaque there, on the bone the joint's bone hangs from), one with it hidden (`--hide`); per pose, the seam figures of the second against the first. Issue #557: per pose also the **contour step**, a reading and never a bar — on the frame with the joint's art hidden, lines across the upper segment (perpendicular to its bone's direction at the setup pose, origin to the joint) one frame pixel apart, from one cap radius before the joint line to one past it; on each, from the axis outward on each side, the outline's extent (the first sample within 40 of the background, out to two radii); the step on a side is the largest change between adjacent lines, in rig px, `left` and `right` as the cap's `short_px` names them. A limb whose outline the bend keeps reads one frame pixel, 1 / s rig px (1.161 at the synthetic fixtures' s 0.861413); the unfilled two-layer knee of the selftest reads 11.609 on the opening side at ±15° and the filled one 1.161. The row prints it; `check.json` carries it (`step_px` on each pose) only for a joint whose cap record carries `short_px` | the rest-pose seam's: mean \|d\| ≤ 1.0 and ≤ 50 rig px over 40 at every pose; a `cap_shape: "disc"` or `"ball"` joint also 0 frame px over 40 (issue #598: the ball over its whole disc; the row says `shape`) | not written: no limbs | not written: no limbs | never: written only when `parts.json` holds a limb record with a `range`, so the examples' `check.json` keep their keys |
|
|
5835
5835
|
|
|
5836
|
+
**Small link motion (issue #994).** After the carrier-frame zero guard, a part on
|
|
5837
|
+
one of the links identified by the hang-bone walk is unmeasured when the larger of
|
|
5838
|
+
its unrounded root and tip travels is below **0.5 rig px**. Its row retains both travels, the ratio,
|
|
5839
|
+
the world reading and `travel_floor_rig_px: 0.5`, with `unmeasured` giving
|
|
5840
|
+
"below half a rig pixel: the motion does not show on the art's own grid". If every
|
|
5841
|
+
row is unmeasured the line is SKIP and retains those rows. In a mixed line the
|
|
5842
|
+
measured rows decide PASS or FAIL.
|
|
5843
|
+
|
|
5844
|
+
Half a rig pixel is the art-grid threshold fixed before measurement, taking one
|
|
5845
|
+
rig pixel as the grid unit. The larger-travel operand was amended after measuring
|
|
5846
|
+
the existing defect plants (issue #994). A root above the floor beside a still tip
|
|
5847
|
+
is measured by the ratio bar. The continuous geometry instrument still resolves the
|
|
5848
|
+
travel below it. Its three-place reporting quantum and its 12 fps time sampling
|
|
5849
|
+
do not establish this spatial threshold. A SKIP asks the author to increase the
|
|
5850
|
+
link's motion until either half reaches the threshold when swing is intended, or to
|
|
5851
|
+
retain the low-motion choice with an explicitly unmeasured swing reading. A part
|
|
5852
|
+
that rides its carrier whole still FAILs at carrier-frame 0 / 0, even if its world
|
|
5853
|
+
travel is nonzero. A `rests` declaration also keeps this structural failure, whose
|
|
5854
|
+
detail says to declare it in `not_swinging` if it is meant to ride rigidly. Declare
|
|
5855
|
+
a deliberately fitted garment in `not_swinging`.
|
|
5856
|
+
Translate-only deformation without a link keeps its existing ratio verdict,
|
|
5857
|
+
including the nonzero-root `rests` relation. At or above 0.5 for either half the
|
|
5858
|
+
link's existing ratio bar applies.
|
|
5859
|
+
|
|
5836
5860
|
**`TIP_OVER_ROOT` is measured from geometry, not pixels (issue #118).** Until #118 the
|
|
5837
5861
|
line rendered each part alone at `--max 640` and took, per half of its frame box, the
|
|
5838
5862
|
centroid of every pixel that was not the background; its fields were `root_px` and
|
|
@@ -6502,7 +6526,7 @@ is `{ "spec": "spine-parts-requirements/1", "fps": 12, "requirements": [], "not_
|
|
|
6502
6526
|
| `range` | `bone`, `lo_degrees`, `hi_degrees` | the bone's world rotation less its parent's (the world's, for the root), less the same at the setup pose, as a signed shortest angle: the least and the greatest, and their frames | — |
|
|
6503
6527
|
| `stretch` | `slot`, `attachment` (a mesh), `within_ratio` | `TEXTURE_STRETCH`'s own per-mesh measure (imported, not rewritten): the mesh's worst max(ratio, 1/ratio) over the frames; `held_by` names what the worst edge's two vertices hang on, as `CHECK_TEXTURE_STRETCH` does (issue #321) | a rest edge of length 0; no frame shows the mesh |
|
|
6504
6528
|
| `follow` | `constraint`, `constraint_type` (`ik` or `transform`), `bone`, `property` (`rotate` or `translate`), `fraction`, `tolerance`, `least_drive` | below | no frame's drive reaches `least_drive` (the line prints the largest drive there was) |
|
|
6505
|
-
| `seam` | `part`, `neighbour` (two part names: each is drawn by the slot of its name), `within_px` | below | no seam pair reads on any frame (each pixel off its attachment's rest geometry, or the slot shows nothing) |
|
|
6529
|
+
| `seam` | `part`, `neighbour` (two part names: each is drawn by the slot of its name), `within_px` (required for parting distance). For `seam_over_motion.required` pair coverage alone, declare `config.motion_seams` `[{part, neighbour}]` and omit a seam requirement. See the [carrier contract](#CONFIG_MOTION_SEAM) and [background over every frame](#background-over-every-frame-issue-941) | below | no seam pair reads on any frame (each pixel off its attachment's rest geometry, or the slot shows nothing) |
|
|
6506
6530
|
|
|
6507
6531
|
A `contact` held against its own bone and point, and an `aim` at a point of the aimed bone itself, read 0 by construction (or, an aim at its own origin, no line at all), so they are refused before anything is built (`REQUIREMENTS_SELF_TARGET`, issue #540); a bone's tip against its own origin stays a contact, measured.
|
|
6508
6532
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.1",
|
|
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
|
@@ -236,6 +236,8 @@ export const BLINK_PATCH_LEVEL = SEAM_PX_LEVEL;
|
|
|
236
236
|
/** The smallest lag the reading resolves, in cycles: phases are written to three places. */
|
|
237
237
|
export const CHAIN_LAG_MIN_STEP = 0.001;
|
|
238
238
|
export const TIP_RATIO_FLOOR = 1.4725;
|
|
239
|
+
/** Issue #994 sets half a rig pixel as the floor on the larger half travel. */
|
|
240
|
+
export const TIP_TRAVEL_FLOOR = 0.5;
|
|
239
241
|
/**
|
|
240
242
|
* Issue #245: the bar a part the scene declares `rests` is held to — its tip
|
|
241
243
|
* half travels no further than its root half, tip / root <= 1, read at the
|
|
@@ -7908,6 +7910,11 @@ export const TIP_INSTRUMENT = 'geometry, carrier frame';
|
|
|
7908
7910
|
* nothing or the walk leaves the tree, and the part is then read in the world.
|
|
7909
7911
|
*/
|
|
7910
7912
|
export function hangBone(rig: Record<string, unknown>, motion: Record<string, unknown>, part: string): string | null {
|
|
7913
|
+
return tipHang(rig, motion, part).bone;
|
|
7914
|
+
}
|
|
7915
|
+
|
|
7916
|
+
/** The same walk also identifies a link below the carrier for the art-grid policy. */
|
|
7917
|
+
function tipHang(rig: Record<string, unknown>, motion: Record<string, unknown>, part: string): { bone: string | null; onLink: boolean } {
|
|
7911
7918
|
const bones = (Array.isArray(rig.bones) ? rig.bones : []).filter(isRecord).filter((b) => typeof b.name === 'string');
|
|
7912
7919
|
const parent = new Map(bones.map((b) => [String(b.name), typeof b.parent === 'string' ? b.parent : null]));
|
|
7913
7920
|
const upFrom = (b: string): string[] => {
|
|
@@ -7916,10 +7923,10 @@ export function hangBone(rig: Record<string, unknown>, motion: Record<string, un
|
|
|
7916
7923
|
return path;
|
|
7917
7924
|
};
|
|
7918
7925
|
const bound = [...boundBones(rig, part)];
|
|
7919
|
-
if (bound.length === 0) return null;
|
|
7926
|
+
if (bound.length === 0) return { bone: null, onLink: false };
|
|
7920
7927
|
const paths = bound.map(upFrom);
|
|
7921
7928
|
const common = paths[0].find((b) => paths.every((p) => p.includes(b)));
|
|
7922
|
-
if (common === undefined) return null;
|
|
7929
|
+
if (common === undefined) return { bone: null, onLink: false };
|
|
7923
7930
|
// A link: a bone of a chain CHAIN_LAG reads, or a bone a physics constraint drives. The rig stage keys a chain's
|
|
7924
7931
|
// `<link>_ctl` and binds the mesh to its child the physics drives, and a physics-only chain has no keys at all, so
|
|
7925
7932
|
// both are walked up through until the body bone that carries them.
|
|
@@ -7967,7 +7974,8 @@ export function hangBone(rig: Record<string, unknown>, motion: Record<string, un
|
|
|
7967
7974
|
seen.add(at);
|
|
7968
7975
|
at = parent.get(at) ?? null;
|
|
7969
7976
|
}
|
|
7970
|
-
|
|
7977
|
+
const onLink = paths.some((path) => path.slice(0, at === null ? path.length : path.indexOf(at)).some((b) => links.has(b)));
|
|
7978
|
+
return { bone: at, onLink };
|
|
7971
7979
|
}
|
|
7972
7980
|
|
|
7973
7981
|
/**
|
|
@@ -8032,7 +8040,8 @@ export function carrierFrame(geo: IdleGeometry | null, bone: string | null): ((i
|
|
|
8032
8040
|
* body's carry is not scored as a tip that moves less; the row names that bone
|
|
8033
8041
|
* (`hang_from`) and keeps the world reading (`world_*`). A half that travels 0
|
|
8034
8042
|
* at the line's precision is 0, so a part that rides its carrier whole reads
|
|
8035
|
-
* 0 / 0 and fails, not a ratio of round-off.
|
|
8043
|
+
* 0 / 0 and fails, not a ratio of round-off. Issue #994 applies the fixed
|
|
8044
|
+
* 0.5 rig px art-grid policy to max(root, tip) of a link part after this guard.
|
|
8036
8045
|
*/
|
|
8037
8046
|
function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems: Problem[], excepted: TipExceptions = NO_TIP_EXCEPTIONS): JudgementLine {
|
|
8038
8047
|
const swing = partsTagged(inp.parts, SWING_TAGS);
|
|
@@ -8067,6 +8076,7 @@ function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems
|
|
|
8067
8076
|
}
|
|
8068
8077
|
const rows: Array<Record<string, unknown>> = [];
|
|
8069
8078
|
let measured = 0;
|
|
8079
|
+
let belowGrid = 0;
|
|
8070
8080
|
let failed = false;
|
|
8071
8081
|
for (const p of swing) {
|
|
8072
8082
|
if (excepted.not_swinging.includes(p.name)) {
|
|
@@ -8085,7 +8095,8 @@ function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems
|
|
|
8085
8095
|
const shown = poses.map((f) => f.get(p.name) ?? null);
|
|
8086
8096
|
const w = halfTravels(art, p, inp.stage, shown, rises ? 'up' : 'down');
|
|
8087
8097
|
// Issue #987: the halves are read in the frame of the bone the part hangs from; the world reading rides beside them.
|
|
8088
|
-
const
|
|
8098
|
+
const hanging = tipHang(inp.rig, inp.motion, p.name);
|
|
8099
|
+
const hangFound = hanging.bone;
|
|
8089
8100
|
const frameOf = carrierFrame(idleGeo, hangFound);
|
|
8090
8101
|
const hang = frameOf === null ? null : hangFound;
|
|
8091
8102
|
const toCarrier = frameOf ?? ((_i: number, posed: readonly number[]) => posed.slice());
|
|
@@ -8098,19 +8109,27 @@ function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems
|
|
|
8098
8109
|
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` });
|
|
8099
8110
|
continue;
|
|
8100
8111
|
}
|
|
8101
|
-
measured++;
|
|
8102
8112
|
// Issue #987: a half that does not move against its carrier, read at the line's precision, is 0 and not the
|
|
8103
8113
|
// arithmetic's residue — a rigid part reads 0 / 0, not a ratio of two round-off figures.
|
|
8104
8114
|
const t = { root: round3(c.root) === 0 ? 0 : c.root, tip: round3(c.tip) === 0 ? 0 : c.tip };
|
|
8105
8115
|
const ratio = t.root > 0 ? round3(t.tip / t.root) : null;
|
|
8106
8116
|
const worldRatio = w.root > 0 ? round3(w.tip / w.root) : null;
|
|
8107
8117
|
// Issue #245: a resting tip is held to the opposite relation — no further than its root, at the line's precision.
|
|
8108
|
-
const
|
|
8118
|
+
const rigid = t.root === 0 && t.tip === 0;
|
|
8119
|
+
const ok = !rigid && (rests ? (ratio === null ? t.tip === 0 : ratio <= TIP_REST_RATIO_CEILING) : ratio === null ? t.tip > 0 : ratio >= TIP_RATIO_FLOOR);
|
|
8109
8120
|
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 });
|
|
8121
|
+
// Structural 0 / 0 stays a failure before the link-only magnitude policy (issue #994).
|
|
8122
|
+
if (!rigid && hanging.onLink && Math.max(c.root, c.tip) < TIP_TRAVEL_FLOOR) {
|
|
8123
|
+
belowGrid++;
|
|
8124
|
+
rows[rows.length - 1] = { ...rows[rows.length - 1], travel_floor_rig_px: TIP_TRAVEL_FLOOR,
|
|
8125
|
+
unmeasured: "below half a rig pixel: the motion does not show on the art's own grid" };
|
|
8126
|
+
continue;
|
|
8127
|
+
}
|
|
8128
|
+
measured++;
|
|
8110
8129
|
const against = hang === null ? 'in the world' : `against \`${hang}\`, the bone it hangs from`;
|
|
8111
8130
|
const inWorld = hang === null ? '' : `; in the world ${round3(w.tip)} and ${round3(w.root)}, ratio ${worldRatio ?? 'undefined'}`;
|
|
8112
8131
|
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}`;
|
|
8113
|
-
if (!ok && rests) {
|
|
8132
|
+
if (!ok && rests && !rigid) {
|
|
8114
8133
|
failed = true;
|
|
8115
8134
|
problems.push({
|
|
8116
8135
|
code: 'CHECK_TIP_OVER_ROOT',
|
|
@@ -8141,7 +8160,7 @@ function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems
|
|
|
8141
8160
|
problems.push({
|
|
8142
8161
|
code: 'CHECK_TIP_OVER_ROOT',
|
|
8143
8162
|
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' : ''}`,
|
|
8144
|
-
detail: `${travels} (ratio ${ratio ?? (hang === null ? 'undefined: nothing moves' : `undefined: nothing moves against \`${hang}\``)}${inWorld}); a ratio >= ${TIP_RATIO_FLOOR} is required — ${misplaced ?? why}`,
|
|
8163
|
+
detail: `${travels} (ratio ${ratio ?? (hang === null ? 'undefined: nothing moves' : `undefined: nothing moves against \`${hang}\``)}${inWorld}); a ratio >= ${TIP_RATIO_FLOOR} is required — ${misplaced ?? why}${rigid && rests ? '; declare it in `not_swinging` if it is meant to ride rigidly' : ''}`,
|
|
8145
8164
|
});
|
|
8146
8165
|
}
|
|
8147
8166
|
}
|
|
@@ -8150,8 +8169,17 @@ function tipLine(inp: FullInputs, geometryPath: string, frames: number, problems
|
|
|
8150
8169
|
...(excepted.rises.length === 0 ? {} : { declared_rises: swing.filter((p) => excepted.rises.includes(p.name)).map((p) => p.name) }),
|
|
8151
8170
|
...(excepted.rests.length === 0 ? {} : { declared_rests: swing.filter((p) => excepted.rests.includes(p.name)).map((p) => p.name) }),
|
|
8152
8171
|
};
|
|
8153
|
-
|
|
8154
|
-
|
|
8172
|
+
const figures = { ...declared, instrument: TIP_INSTRUMENT, parts: rows, ratio_floor: TIP_RATIO_FLOOR,
|
|
8173
|
+
...(excepted.rests.length === 0 ? {} : { rest_ratio_ceiling: TIP_REST_RATIO_CEILING }) };
|
|
8174
|
+
if (measured === 0) {
|
|
8175
|
+
const reasons = rows.map((r) => {
|
|
8176
|
+
const travel = r.travel_floor_rig_px === undefined ? '' : `; root ${String(r.root_rig_px)} rig px, tip ${String(r.tip_rig_px)} rig px, floor ${String(r.travel_floor_rig_px)} rig px`;
|
|
8177
|
+
return `${String(r.part)}: ${String(r.unmeasured)}${travel}`;
|
|
8178
|
+
});
|
|
8179
|
+
return { status: 'SKIP', reason: `none of ${quoteParts(swing)} could be measured: ${reasons.join('; ')}`,
|
|
8180
|
+
...(belowGrid === 0 ? {} : figures) };
|
|
8181
|
+
}
|
|
8182
|
+
return { status: failed ? 'FAIL' : 'PASS', ...figures };
|
|
8155
8183
|
}
|
|
8156
8184
|
|
|
8157
8185
|
/**
|
package/src/config.ts
CHANGED
|
@@ -1434,6 +1434,41 @@ const DOORS = 'a project\'s own record goes under a key beginning "x-" (any JSON
|
|
|
1434
1434
|
|
|
1435
1435
|
type Json = unknown;
|
|
1436
1436
|
|
|
1437
|
+
/** The loader's config-relative file fields, also used when relocating a config. */
|
|
1438
|
+
const CONFIG_FILE_FIELDS = {
|
|
1439
|
+
requirements: { path: ['requirements'], isFile: (_value: string) => true },
|
|
1440
|
+
animations: { path: ['motion', 'animations_from'], isFile: (_value: string) => true },
|
|
1441
|
+
skeleton: { path: ['generation', 'control', 'skeleton'], isFile: (value: string) => !isSkeletonName(value) },
|
|
1442
|
+
} as const;
|
|
1443
|
+
|
|
1444
|
+
function configFilePath(field: keyof typeof CONFIG_FILE_FIELDS, base: string, value: string): string | null {
|
|
1445
|
+
return CONFIG_FILE_FIELDS[field].isFile(value) ? resolve(base, value) : null;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/** A file reference in a parsed config and its setter, excluding built-in names. */
|
|
1449
|
+
export interface ConfigFileReference {
|
|
1450
|
+
at: string;
|
|
1451
|
+
value: string;
|
|
1452
|
+
set: (value: string) => void;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/** Enumerate the same file fields that the loader resolves, preserving invalid fields for its diagnostics. */
|
|
1456
|
+
export function configFileReferences(raw: unknown): ConfigFileReference[] {
|
|
1457
|
+
const out: ConfigFileReference[] = [];
|
|
1458
|
+
for (const field of Object.values(CONFIG_FILE_FIELDS)) {
|
|
1459
|
+
let parent = raw;
|
|
1460
|
+
for (const key of field.path.slice(0, -1)) parent = isPlainObject(parent) ? parent[key] : undefined;
|
|
1461
|
+
const key = field.path[field.path.length - 1];
|
|
1462
|
+
if (!isPlainObject(parent)) continue;
|
|
1463
|
+
const value = parent[key];
|
|
1464
|
+
if (typeof value !== 'string' || value === '' || !field.isFile(value)) continue;
|
|
1465
|
+
const target = parent;
|
|
1466
|
+
out.push({ at: field.path.join('.'), value, set: (next) => { target[key] = next; } });
|
|
1467
|
+
}
|
|
1468
|
+
return out;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
|
|
1437
1472
|
class Check {
|
|
1438
1473
|
readonly problems: Problem[] = [];
|
|
1439
1474
|
|
|
@@ -1836,10 +1871,12 @@ export function cloneConfig<T extends object>(cfg: T): T {
|
|
|
1836
1871
|
function poseBeside(c: Check, gen: Json, base: string): ControlPose | null {
|
|
1837
1872
|
if (!isPlainObject(gen) || !isPlainObject(gen.control)) return null;
|
|
1838
1873
|
const skel = gen.control.skeleton;
|
|
1839
|
-
if (typeof skel !== 'string' || skel === ''
|
|
1874
|
+
if (typeof skel !== 'string' || skel === '') return null;
|
|
1875
|
+
const file = configFilePath('skeleton', base, skel);
|
|
1876
|
+
if (file === null) return null;
|
|
1840
1877
|
const l = gen.latent;
|
|
1841
1878
|
const latent = Array.isArray(l) && l.length === 2 && l.every((n) => typeof n === 'number' && Number.isInteger(n) && n > 0) ? ([l[0], l[1]] as [number, number]) : null;
|
|
1842
|
-
return readControlPose(c,
|
|
1879
|
+
return readControlPose(c, file, latent);
|
|
1843
1880
|
}
|
|
1844
1881
|
|
|
1845
1882
|
/**
|
|
@@ -1992,7 +2029,7 @@ function parseConfigFrom(raw: Json, base: string | null): { config: CharacterCon
|
|
|
1992
2029
|
if ('turn' in t) checkTurn(c, t.turn, attached, names.bones, hasAltFace, parts, declaredSliders, meshParts, t.meshes, names.chains, names.parents);
|
|
1993
2030
|
if ('scene' in t) checkScene(c, t.scene);
|
|
1994
2031
|
if ('cloth' in t) checkCloth(c, t.cloth);
|
|
1995
|
-
if ('requirements' in t && c.string('config.requirements', t.requirements) && base !== null) checkRequirementsFile(c,
|
|
2032
|
+
if ('requirements' in t && c.string('config.requirements', t.requirements) && base !== null) checkRequirementsFile(c, configFilePath('requirements', base, t.requirements) as string);
|
|
1996
2033
|
const from = isPlainObject(t.motion) ? t.motion.animations_from : undefined;
|
|
1997
2034
|
const animations = base !== null && typeof from === 'string' && from !== '' ? readAnimationsFrom(c, base, from) : null;
|
|
1998
2035
|
if (animations !== null) {
|
|
@@ -2074,7 +2111,7 @@ function checkRequirementsFile(c: Check, file: string): void {
|
|
|
2074
2111
|
*/
|
|
2075
2112
|
function readAnimationsFrom(c: Check, base: string, from: string): AnimationsFrom | null {
|
|
2076
2113
|
const field = 'config.motion.animations_from';
|
|
2077
|
-
const file =
|
|
2114
|
+
const file = configFilePath('animations', base, from) as string;
|
|
2078
2115
|
const form = `{"spec": "${ANIMATIONS_FROM_SPEC}", "animations": {"<name>": {"duration": …, "tracks": [...]}}}`;
|
|
2079
2116
|
const shape = `a file in rig-c's motion-spec shape, ${form}, its path relative to the config's directory`;
|
|
2080
2117
|
const where = `names ${JSON.stringify(from)}, resolved against the config's directory ${base} (not the working directory) to ${file}`;
|
package/src/meshdraft.ts
CHANGED
|
@@ -67,7 +67,7 @@ import { MeshReductionError } from '@rig-c/core/mesh';
|
|
|
67
67
|
import { explicitFrameId, idleSchedule } from './automotion.ts';
|
|
68
68
|
import { checkStage, gateThroughRigc, type Log, RIGC_MODEL_DOCUMENT, rigStage } from './build.ts';
|
|
69
69
|
import { BARS, type CheckReport, type PackShape, type PageEdges, type RigcRunner } from './check.ts';
|
|
70
|
-
import { type CharacterConfig, type ContourMeshSpec, type LatticeMeshSpec, loadConfigAndAnimations, type Segment } from './config.ts';
|
|
70
|
+
import { type CharacterConfig, configFileReferences, type ConfigFileReference, type ContourMeshSpec, type LatticeMeshSpec, loadConfigAndAnimations, type Segment } from './config.ts';
|
|
71
71
|
import { PartsError, type Problem, problemLine } from './errors.ts';
|
|
72
72
|
import { IDLE_FPS } from './motion.ts';
|
|
73
73
|
import { type ContourBridges, FAINT_ALPHA } from './contour.ts';
|
|
@@ -743,28 +743,12 @@ export function splicedConfig(text: string, drafted: ReadonlyMap<string, unknown
|
|
|
743
743
|
return r.text;
|
|
744
744
|
}
|
|
745
745
|
|
|
746
|
-
/** A
|
|
747
|
-
export
|
|
748
|
-
at: string;
|
|
749
|
-
value: string;
|
|
750
|
-
set: (v: string) => void;
|
|
751
|
-
}
|
|
746
|
+
/** A config-relative file reference supplied by the loader. */
|
|
747
|
+
export type RelativeConfigPath = ConfigFileReference;
|
|
752
748
|
|
|
753
|
-
/**
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
*/
|
|
757
|
-
export function relativeConfigPaths(configDir: string, o: Json): RelativeConfigPath[] {
|
|
758
|
-
const out: RelativeConfigPath[] = [];
|
|
759
|
-
const motion = o.motion as Json | undefined;
|
|
760
|
-
if (motion !== undefined && typeof motion.animations_from === 'string' && !isAbsolute(motion.animations_from) && existsSync(resolve(configDir, motion.animations_from))) {
|
|
761
|
-
out.push({ at: 'motion.animations_from', value: motion.animations_from, set: (v) => (motion.animations_from = v) });
|
|
762
|
-
}
|
|
763
|
-
const control = (o.generation as Json | undefined)?.control as Json | undefined;
|
|
764
|
-
if (control !== undefined && typeof control.skeleton === 'string' && !isAbsolute(control.skeleton) && existsSync(resolve(configDir, control.skeleton))) {
|
|
765
|
-
out.push({ at: 'generation.control.skeleton', value: control.skeleton, set: (v) => (control.skeleton = v) });
|
|
766
|
-
}
|
|
767
|
-
return out;
|
|
749
|
+
/** Each relative file path the loader knows, with built-in skeleton names excluded. */
|
|
750
|
+
export function relativeConfigPaths(_configDir: string, o: Json): RelativeConfigPath[] {
|
|
751
|
+
return configFileReferences(o).filter((field) => !isAbsolute(field.value));
|
|
768
752
|
}
|
|
769
753
|
|
|
770
754
|
/**
|