@rig-c/parts 1.31.0 → 1.33.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/cli.ts +12 -7
- package/docs/AUTHORING.md +24 -3
- package/docs/STABILITY.md +5 -4
- package/package.json +2 -2
- package/src/build.ts +98 -10
- package/src/check.ts +223 -6
- package/src/config.ts +148 -8
- package/src/parts.ts +7 -0
- package/src/propose.ts +1 -1
- package/src/rig.ts +213 -10
- package/src/turn.ts +302 -45
package/cli.ts
CHANGED
|
@@ -335,6 +335,7 @@ usage:
|
|
|
335
335
|
rig-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>]
|
|
336
336
|
[--page-edges pot|free] [--pack-shape rect|polygon]
|
|
337
337
|
[--requirements <file.json>] [--seam-pairs] [--keep-stills]
|
|
338
|
+
[--set <name> | --all-sets]
|
|
338
339
|
Build, gate, render and measure a rig through rig-c's CLI (the rigc at
|
|
339
340
|
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
|
|
340
341
|
a rig spec, which rigc build compiles, not a compiled skeleton.json.
|
|
@@ -567,7 +568,7 @@ usage:
|
|
|
567
568
|
[--project core|visible] [--page-edges pot|free]
|
|
568
569
|
[--pack-shape rect|polygon] [--loop] [--requirements <file.json>]
|
|
569
570
|
[--idle-keys ctl|direct] [--lead-in <periods>] [--seam-pairs] [--keep-stills]
|
|
570
|
-
[--alt <name>=<source>:<full>[:<head>]]
|
|
571
|
+
[--alt <name>=<source>:<full>[:<head>]] [--set <name> | --all-sets]
|
|
571
572
|
assemble, then rig, then check, in one process, each stage's own lines
|
|
572
573
|
printed under [assemble], [rig] and [check]; the first stage that refuses
|
|
573
574
|
stops the build with its own FAIL lines. The config must already carry
|
|
@@ -785,13 +786,13 @@ const TAKES = {
|
|
|
785
786
|
rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
|
|
786
787
|
propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule', '--cloth'], switches: ['--head-box', '--no-back-hair'] },
|
|
787
788
|
compare: { valued: ['--left', '--right', '--map'], switches: [] },
|
|
788
|
-
check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements'], switches: ['--seam-pairs', '--keep-stills'] },
|
|
789
|
+
check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
|
|
789
790
|
loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
|
|
790
791
|
assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt'], switches: ['--propose-plan'] },
|
|
791
792
|
'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll'], switches: ['--offload', '--lama', '--nf4'] },
|
|
792
793
|
'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
|
|
793
794
|
inputs: { valued: ['--source', '--config', '--out'], switches: [] },
|
|
794
|
-
build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in', '--alt'], switches: ['--loop', '--seam-pairs', '--keep-stills'] },
|
|
795
|
+
build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in', '--alt', '--set'], switches: ['--loop', '--seam-pairs', '--keep-stills', '--all-sets'] },
|
|
795
796
|
compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
|
|
796
797
|
'mesh-draft': { valued: ['--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: ['--more-triangles'] },
|
|
797
798
|
painting: { valued: ['--keypoints', '--person'], switches: [] },
|
|
@@ -1037,17 +1038,20 @@ function flags(args: string[], command: Command, required: readonly string[]): M
|
|
|
1037
1038
|
}
|
|
1038
1039
|
|
|
1039
1040
|
function cmdCheck(args: string[]): number {
|
|
1040
|
-
// The
|
|
1041
|
+
// The switches check takes (issues #202, #209, #454): they carry no value, so they are taken out before the valued flags are read.
|
|
1041
1042
|
const seamPairs = args.filter((a) => a === '--seam-pairs').length;
|
|
1042
1043
|
if (seamPairs > 1) return usage('--seam-pairs is given twice');
|
|
1043
1044
|
const keepStills = args.filter((a) => a === '--keep-stills').length;
|
|
1044
1045
|
if (keepStills > 1) return usage('--keep-stills is given twice');
|
|
1046
|
+
const allSets = args.filter((a) => a === '--all-sets').length;
|
|
1047
|
+
if (allSets > 1) return usage('--all-sets is given twice');
|
|
1045
1048
|
const f = flags(
|
|
1046
|
-
args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills'),
|
|
1049
|
+
args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills' && a !== '--all-sets'),
|
|
1047
1050
|
'check',
|
|
1048
1051
|
['--rig', '--out'],
|
|
1049
1052
|
);
|
|
1050
1053
|
if (typeof f === 'string') return usage(f);
|
|
1054
|
+
if (allSets === 1 && f.has('--set')) return usage('--set and --all-sets are two modes; give one');
|
|
1051
1055
|
const rig = f.get('--rig') as string;
|
|
1052
1056
|
const out = f.get('--out') as string;
|
|
1053
1057
|
const edges = pageEdgesOf(f.get('--page-edges'));
|
|
@@ -1057,7 +1061,7 @@ function cmdCheck(args: string[]): number {
|
|
|
1057
1061
|
try {
|
|
1058
1062
|
const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
1059
1063
|
const r = checkStage(
|
|
1060
|
-
{ rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1 },
|
|
1064
|
+
{ rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1, ...(f.has('--set') ? { set: f.get('--set') as string } : {}), allSets: allSets === 1 },
|
|
1061
1065
|
rigcRunner(bin),
|
|
1062
1066
|
bin,
|
|
1063
1067
|
console.log,
|
|
@@ -1503,6 +1507,7 @@ function cmdBuild(args: string[]): number {
|
|
|
1503
1507
|
}
|
|
1504
1508
|
// --head is optional (issue #359), as for assemble.
|
|
1505
1509
|
for (const f of ['--config', '--source', '--full', '--out']) if (!flags.has(f)) return usage(`build needs ${f}`);
|
|
1510
|
+
if (on.has('--all-sets') && flags.has('--set')) return usage('--set and --all-sets are two modes; give one');
|
|
1506
1511
|
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
1507
1512
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
1508
1513
|
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
@@ -1528,7 +1533,7 @@ function cmdBuild(args: string[]): number {
|
|
|
1528
1533
|
const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-build-'));
|
|
1529
1534
|
try {
|
|
1530
1535
|
const r = build(
|
|
1531
|
-
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges, packShape: shape, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), idleKeys: keys, ...(leadText === undefined ? {} : { leadIn: Number(leadText) }), seamPairs: on.has('--seam-pairs'), keepStills: on.has('--keep-stills'), ...(alts.length === 0 ? {} : { alts }) },
|
|
1536
|
+
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges, packShape: shape, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), idleKeys: keys, ...(leadText === undefined ? {} : { leadIn: Number(leadText) }), seamPairs: on.has('--seam-pairs'), keepStills: on.has('--keep-stills'), ...(alts.length === 0 ? {} : { alts }), ...(flags.has('--set') ? { set: flags.get('--set') as string } : {}), allSets: on.has('--all-sets') },
|
|
1532
1537
|
{ rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
|
|
1533
1538
|
console.log,
|
|
1534
1539
|
);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -108,6 +108,7 @@ read by no CPU stage).
|
|
|
108
108
|
| `assemble.extend_below_crop` | assemble | **proposed** with the plan: `{part, run, tag}` — a head-run part that reaches the bottom of the head crop is continued from a full-run layer, whole connected components |
|
|
109
109
|
| `assemble.patches` | assemble; `meshes`/`regions` keys may name them too | **authored**, optional: `[{name, box, alpha, draw}]` — an extra part cut from the **painting** itself, for figure no See-through layer holds (a hem both runs dropped). `box` is `[x0, y0, x1, y1]` in **rig** pixels, `x1`/`y1` exclusive — the space of `parts.json` and `recomposite_rig.png`, where the hole is found; `alpha` is `"silhouette"` (the painting's figure silhouette inside the box, the one `--seam silhouette` uses) or `"box"` (the whole box); `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`. A patch is always a region: its bone is `regions.<name>`, in the one place every region's bone lives, and a `meshes` entry for it is refused. See §5 for how to place one |
|
|
110
110
|
| `assemble.cuts` | assemble; `meshes`/`regions` keys may name the new parts too, and must (each is a part like any other) | **authored**, optional (issue #170): `[{from, into, polygon, draw, overlap, under, under_fill}]`, `under` and `under_fill` optional — a plan part's layer cut by a polygon the author draws: the pixels of plan part `from` whose **centre** lies inside `polygon` (even-odd; a centre exactly on an edge is outside) leave `from` and make the new part `into`, colour and alpha, so every pixel of the layer ends in exactly one of the two. `polygon` is `[[x, y], …]`, at least three points, non-negative integer **rig** pixels — the space of `parts.json` and of the patches' boxes. The file that shows the layer in those coordinates is `assemble`'s own `rig/parts/<from>.png`, placed at its `parts.json` `x`, `y` (the PNG's pixel `(i, j)` is rig pixel `(x + i, y + j)`), or `render/recomposite_rig.png`, the whole rig 1:1; the contact sheets (`sheet`) are scaled and carry no coordinates. `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`, as for a patch (to keep the setup pose, draw the piece immediately in front of `from` — `{"before": "<the plan part after from>"}`, or `"front"` when `from` is the last plan part; `{"before": "<from>"}` draws it behind `from`, which `under` refuses as `ASSEMBLE_UNDER_DRAW` because the fill would hide it). `from` must be a plan part (`CONFIG_NAME_RESOLVES`; a patch or another cut's piece is refused), `into` a new name (`CONFIG_PART_UNIQUE`), the polygon three or more integer points (`CONFIG_FIELD_TYPE`). The piece's `parts.json` record carries `from`'s `<run>:<tag>` and its own counts, each split from the layer's by where the pixel lies; it is a mesh or a region under its own name, and the coverage rule holds it like a plan part. What the config cannot know the stage refuses: a polygon point past the rig (`ASSEMBLE_CUT_INSIDE`), two cuts of one part sharing a pixel (`ASSEMBLE_CUT_OVERLAP`), a cut that takes none of the layer's opaque pixels or leaves `from` none (`ASSEMBLE_CUT_PIXELS`). `overlap` is required, a non-negative integer of rig pixels with no default (`CONFIG_FIELD_TYPE`): the **band** — the polygon's pixels within `overlap` px of its edge by Chebyshev distance (the polygon's mask less its erosion by a `2 * overlap + 1` square) — stays in `from` as well as going to the piece, so the piece's resampled edge has art under it; the counts stay a strict partition and the band is the base's extra copy, printed on the `cut:` line. At `0` the two abut and `check`'s seam bar judges the edge between them (`CHECK_SEAM_WITHIN_BAR`, §5). A band only covers the edge at rest: a piece that swings well clear of where it was painted shows the hole it leaves, and a thin piece's band shows a ghost of the piece. `under` (issue #193), optional, is the author's image of what lies under the piece: a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE` unless a non-empty string), in rig pixels over the polygon's **bounding box** — the least and greatest polygon x and y, so the image is `max x − min x` by `max y − min y` and its pixel `(i, j)` is rig pixel `(min x + i, min y + j)`. Where the cut removed pixels from `from` — the piece's pixels (alpha above 0) less the band — `from` takes the image's pixel, colour and alpha wherever the image's alpha is at least the alpha the cut removed there (**filled**); elsewhere `from` stays empty there (**left empty**): a transparent pixel is the author's statement that nothing lies under it (the background, or a part drawn behind), and a less opaque one is not half-filled. The image is read nowhere else, so its pixels over the band, over the box outside the polygon or where the layer had no art never change `from`. The `cut:` line then ends `; under <n> px filled, <m> px left empty` (`n + m` is every removed pixel), so an unintended hole is a number in the log, not a guess; the filled pixels count in `from`'s `opaque_px` and `occluded_px` (the piece is drawn over them), never as visible or projected. The stage refuses: a file that is not there (`ASSEMBLE_UNDER_PRESENT`) or not a PNG (`ASSEMBLE_UNDER_DECODES`), an image not exactly the box's size (`ASSEMBLE_UNDER_SIZE`, both sizes given), an image that fills none of the removed pixels — all transparent there, or another file (`ASSEMBLE_UNDER_EMPTY`; to leave the whole hole, drop `under`), and a piece drawn behind `from` (`ASSEMBLE_UNDER_DRAW`: the fill would hide it; draw it `"front"` or `{"before": "<the plan part after from>"}`). Producing the image is the author's, by whatever means; absent, `from` keeps only its band. `under_fill` (issue #360), optional, is the one value `"colour"` (`CONFIG_FIELD_TYPE` on any other): the same pixels are filled and left empty, and a filled pixel takes the image's **colour** at the alpha the cut removed there instead of the image's alpha, so `from`'s alpha is the uncut layer's wherever the image fills and its silhouette does not grow — an opaque image over a soft edge leaves the edge soft. It applies to the part's hidden-art layer as well as to a file; a cut with neither is refused (`ASSEMBLE_UNDER_FILL_SOURCE`). The `cut:` line's filled count then says how many took the colour only: `; under 190 px filled (190 colour only, at the alpha the cut removed), 0 px left empty`. Absent, a filled pixel takes the image's colour and alpha, as above |
|
|
111
|
+
| `assemble.alternates` | assemble, rig, check (`--set`, `--all-sets`) | **authored**, optional (issues #454, #465, #472): `[{name, source?, full?, head?, head_box?, parts?, hides?, cuts?, skeleton?, drawn?}]` — alternate painting sets assembled into `<out>/rig/parts/<set>_<part>.png` (`parts.json` `alternates`), attached on each covered slot in `rig.json` as `<set>_<part>` beside the base attachment, and indexed in `<out>/rig/sets.json` (`rig-parts-sets/1`: `sets.<set>.slots`, `.hides`, `.drawn` when present, and `.threshold: {yaw: 15, pitch: 12}` when the set replaces `face` on a rig with `turn` or `turn`/`tilt` slider animations). `source`, `full` and `head` may instead be given on the CLI with `--alt <name>=<source>:<full>[:<head>]`. `parts` lists the subset of parts the set replaces (default: every part not in `hides`); `hides` lists slots the set hides (`null` in `sets.json`). `skeleton` (a pose file path relative to the config's directory, `{"width": W, "height": H, "points": {…}}` with all 18 body-18 keypoints on the canvas, validated by the same rules as `generation.control.skeleton`: `CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON`, `CONFIG_FIELD_TYPE`, `CONFIG_KEY_KNOWN`, `CONFIG_KEY_UNIQUE`, `CONFIG_POSE_CANVAS`, `CONFIG_POSE_KEYPOINTS`, `CONFIG_POSE_IN_CANVAS`) or `drawn` (`{<bone>: <worldDegrees>}`, each bone resolved against `bones`, `CONFIG_NAME_RESOLVES`) declares the posed angles the alternate was painted at (issue #465); when `skeleton` is given, `assemble` derives `drawn` from its 18 keypoints (with `drawn` overriding any bone named in both), records both `skeleton` (scaled by `assemble.rig_scale` to rig px) and `drawn` in `parts.json` `alternates`, and the `rig` stage writes `<set>_*` region and mesh attachments in the bone-local frame of `poseRigBonesForDrawn(bones, drawn)` (weighting limb segments directly from `skeleton`'s joint endpoints when `skeleton` is present) and emits `drawn` in `sets.json` so `rig-pose` and `check --set <name>` / `--all-sets` pose the bones at `drawn` without rotating the already-posed art a second time |
|
|
111
112
|
| `limbs` | assemble (the cut and the caps), rig (through `regions`), check (`LIMB_ONE_BONE`) | **proposed** by `propose --keypoints` (issue #349), then checked: `{<limb>: {segments: [{part, from, bone}, {part, from, bone, joint, range?, cap?, edge?, cap_shape?, cut_band?}, …]}}`, the upper segment first. The rule is *Limbs: cut at their joints* below the table. `part` is a new part (`CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` against the plan, patches, cuts and every other limb part); `from` a plan part or a cut's piece (`CONFIG_NAME_RESOLVES`) — the layer the segment's pixels come from, which is cut into the limb parts and is not itself made: a `meshes` or `regions` entry for it is `CONFIG_NAME_RESOLVES`, and the segments of one limb that take one part are consecutive (`CONFIG_LIMB_FROM_RUN`); `bone` a declared bone, each segment's the child of the one before (`CONFIG_LIMB_BONES_CHAIN`); the upper segment names no `joint`, `range`, `cap`, `edge`, `cap_shape` or `cut_band` (`CONFIG_KEY_KNOWN`), every other names its `joint` (`CONFIG_FIELD_PRESENT`) — the joint is its bone's origin; `range` is `[min, max]` degrees from rest with `min <= 0 <= max` (`CONFIG_LIMB_RANGE`), which `assemble` writes into the lower part's `parts.json` record and `check` poses the joint at — its two ends and its middle — reading the seam there (`SEAM_OVER_RANGE`, issue #360; rig-c needs no rotation limit); `cap` a PNG path relative to the config's directory (`CONFIG_FIELD_TYPE`); `edge` `"feather"` and `cap_shape` `"disc"` (issue #360), each the one value its field takes (`CONFIG_FIELD_TYPE` on any other) — the cut's geometry at that joint, *Limbs: cut at their joints* below; absent, the cut is hard and the cap the layer's half-disc of half the limb's width; `cut_band` a whole number of px, 1 or more (`CONFIG_FIELD_TYPE`), on a segment that takes the segment above's part — between two layers nothing is cut, so a band there is `CONFIG_KEY_KNOWN` — places the cut within that many px of the joint line (issue #360, *Limbs: cut at their joints*); absent, the cut is the joint line. Each limb part is attached like any part — `regions.<part>`, which must be the segment's `bone` (`CONFIG_LIMB_BONE`), or a mesh, which `check` holds to that bone alone (`LIMB_ONE_BONE`). With `limbs`, `assemble` reads `bones` too (`CONFIG_FIELD_PRESENT` without it). Absent, nothing is cut at a joint and no byte of any output moves |
|
|
112
113
|
| `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip, link_inherit?}` whose links are named `<chain>0 … <chain>n`. Parents come before children. In `rig.json` each link is **turned along its chain** — `rotation` is the direction from its origin to the next link's (the last link's to `tip`) and `length` that distance (issue #73), so a physics constraint added downstream finds a lever; under `--idle-keys ctl` the link's `<link>_ctl` carries the turn and the length and the link sits at local rotation 0 beneath it. A single bone with a `tip` is turned the same way, towards its tip, with that distance as its length (issue #239); a tip on the bone's own origin is refused (`RIG_TIP_DISTINCT`). The five face bones `eye_r`, `eye_l`, `brow_r`, `brow_l` and `mouth` (by name, when one has no tip of its own) are turned to the eye line — the line through the origins of `eye_r` and `eye_l` as `bones` writes them, read image-left to image-right, drawn when parts.json holds both an `eyewhite-r` and an `eyewhite-l` part — so the blink's `scaley` squashes across the eye line on a tilted head; move an eye bone and the line moves with it. With one eyewhite or none, or an eye bone missing, the rig stage's `face axis:` line says so and they keep the page's axes. A bone under a face bone keeps the face's axis. With the line, `rig.json` also declares `invariants.axes` — one entry, `"face"`, through `eye_r` and `eye_l`, holding the turned face bones (and their controls) — so rig-c's gate holds the build to the line; the line is the one rigc draws through the same two bones, so the tolerance is only the float floor — the largest gap the emitted (float32) bones have to it, rounded up to 6 places plus one 6-place unit — and the `face axis:` line prints it. With the line the rig stage also adds `head_up` (issue #250): a slotless bone under `head`, at head's origin, turned to the eye line + 90 degrees and as long as the eye midpoint lies from head's origin along that axis — the head's frame, which a rule that needs it reads; `head` keeps its own frame, so no key on it or under it changes meaning, and a config bone of that name is refused (`RIG_HEAD_UP_NAME_FREE`). A face bone with its own `tip` is held too (issue #258), in a second entry, `"face_tips"`, when its tip lies along the line within whole-pixel rounding — a tip written in whole pixels sits at most sqrt(2)/2 px off the point of the line it stands for, so its direction may sit up to asin((sqrt(2)/2) / length) degrees off — with its tolerance measured the same way; a tip further off is the bone's own axis, and the `face axis:` line names it with its gap and that bound. A failure is rig-c's `A51_BONES_LIE_ALONG_THEIR_DECLARED_AXIS` line, which names the bone and the fix (rig-c's `docs/AUTHORING.md` §3.7 and §5.2); with no line no entry is written and A51 reports SKIP. Every region on a turned bone is turned back, so the setup pose does not move. Any other single bone keeps world rotation 0; one whose parent is a link is turned back upright. A single bone may declare `inherit` (issue #348): what it takes from its parent's transform, one of rig-c's own mode names exactly as rig-c's table spells them — `normal`, `onlyTranslation`, `noRotationOrReflection`, `noScale`, `noScaleOrReflection` (rig-c's `RigBone.inherit`) — written to that bone in `rig.json` as declared; any other spelling, `"NoScale"` included (the runtime would fold its first letter; nothing here folds or maps), is refused (`CONFIG_BONE_INHERIT`), and a chain takes no `inherit` (`CONFIG_KEY_KNOWN`). A chain may instead declare `link_inherit` (issue #369), an object mapping its own expanded link names to those same modes, for example `"link_inherit": {"leg_r1": "noScale"}`. Only the named link changes; omitted links inherit normally. A key outside this chain is `CONFIG_NAME_RESOLVES`, with this chain's valid link names in the detail; a non-object or non-string mode is `CONFIG_FIELD_TYPE`, and another mode spelling is `CONFIG_BONE_INHERIT`. The mode is forwarded to the expanded link, or to its control under `--idle-keys ctl`, by the same rule as a single bone. This is an authored opt-in: `propose` does not add it. Its setup rotation is the turn from where rig-c's own poser puts the bone with no rotation of its own under that mode, so it still stands upright (or along its tip) at the setup pose. Where the idle gives the bone a `<bone>_ctl`, the mode goes on the control: the control is the bone the parent's rotation reaches, and the bone under it inherits normally, so the idle's keys moved onto the control still turn it. A foot that does not take its shin's rotation (`noRotationOrReflection` or `onlyTranslation`) keeps its shoe level while the leg is posed (`FEET_STAY_LEVEL`, §7); which mode is the author's. `noScale` keeps a shin or shoe's shape under a scaled parent but still takes the parent's rotation: a shoe that must also stay level needs a separate rotation constraint, for example a transform constraint from an unrotated bone. This does not change the proposed feet's existing inheritance or `FEET_STAY_LEVEL` default. An automatic mesh weighted through a bone with a mode other than `normal` gets no derived amplitude (its row names the bone) and no residual envelope (rig-c's `SKINNING_RANGE_UNSUPPORTED`). Absent, `rig.json` carries no `inherit` and no byte moves |
|
|
113
114
|
| `meshes.<part>` | rig | **proposed**, then corrected. Exactly one of `grid` (the lattice: cell size, px — what `propose` writes: by default one spacing for the figure, round(H / 37) rig px with H the height of every part's box together, a part whose tenth of its short side is already coarser keeping that, and under `propose --grid-rule part` the reference's tenth of the part's short side clamped to 5..36, issue #265), `contour` (the outline mode, authored; the next row and "A contour mesh" below) or `auto` (the automatic mode, authored; "An automatic mesh" below) — two, or none, is `CONFIG_MESH_MODE`. In every mode: `r` (added to every distance before weighting: `w = 1/(d + r)²`), `segments` (a chain name, a bone name with a `tip`, or `[bone, [x0,y0], [x1,y1]]`), and, **authored**, optional, `exponent` (issue #161): a number above 0 that replaces the 2 in that rule, `w = 1/(d + r)^exponent` — a larger one hands each vertex more fully to its nearest bone; absent is 2 and changes no byte, a value that is not a number above 0 is `CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`, and a declared one is echoed as `exponent` in the mesh's `mesh_report.json` row; the exponent is the distance rule's, so declared beside `rule: "heat"` it is refused (`CONFIG_FIELD_TYPE` at `meshes.<part>.exponent`). The segment list is the one authored decision about a layer: which bones may pull it. The slot's bone is the first segment's. Optional `rule` (issue #161): `"distance"` — the rule above, and what an absent `rule` means (every byte is the same) — or `"heat"`, bone heat over the part's own silhouette, in every mode: the lattice's art (alpha above 8, less any island a declared `stray` left out) with holes filled, or for `contour` and `auto` the silhouette their outline is traced from (that mode's art — alpha above 8 for `contour`, alpha 1 and above for `auto` — less any island a declared `stray` left out, holes filled, grown by its `margin`); each bound bone's sources are the silhouette pixels whose centre lies within 0.75 px of its segment (a pixel two bones reach goes to the nearer, a tie to the first listed), held at 1 for that bone and 0 for the others; the field is solved by row-major Gauss–Seidel over-relaxation to a sweep change under 1e-7 within 20000 sweeps; a vertex reads the nearest silhouette pixel and takes the same cap and floor (and `regions`) as the distance rule; `r` is not read. The mesh's `mesh_report.json` row gains `heat` (the constants, each bone's sources and sweeps, the residual). Any other value is `CONFIG_FIELD_TYPE`; a bound bone with no source pixel is `RIG_HEAT_BONE_SOURCE`, an island no bone has a source in `RIG_HEAT_ISLAND_SOURCE`, a solve past its sweep bound `RIG_HEAT_CONVERGED` — none is weighted 0. The rule is chosen per mesh, so `RIG_HEAT_BONE_SOURCE` names three remedies: move the segment onto the art, take the bone out of `segments`, or keep this part on the distance rule — the one to take when the bone anchors the part from outside its art (a torso bone under a neck), since the first moves a bone another part placed and the second drops the anchor (issue #303). On a lattice, `RIG_HEAT_ISLAND_SOURCE` on an island that is dust — at most 4 px and at most 0.1 % of the part's art px, `mesh-draft`'s line — names `meshes.<part>.stray` and the figure that leaves it out, with the value declared; an island above that line keeps the segment remedy, a real piece being a real authoring question |
|
|
@@ -123,11 +124,28 @@ read by no CPU stage).
|
|
|
123
124
|
| `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
125
|
| `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
126
|
| `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
|
-
| `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
|
|
127
|
+
| `turn` | rig (adds the turn control) | **proposed** only with `propose --full`/`--head` (issue #251), then corrected: `{pivot, depths, deform?, band?}`. `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. Optional `deform` (`Partial<FaceDeformFractions>`, issue #436) overrides the Set B ×2.5 face surface deformation table (`FACE_DEFORM_FRACTIONS`, checked against `FACE_DEFORM_RANGES` by `CONFIG_TURN_DEFORM_RANGE`); when `applyFaceSurfaceDeform` runs on a rig whose `motion` carries `tilt` or `turn` slider `derive` keys, it promotes `face` on `head` (or `turn_face_head` when `turn` is declared) to the uneven 5×5 grid mesh, keys `face`, `hair_front` and `hair_back` deform timelines and ear/topknot tracks from `evalPitchFaceSurface` and `evalYawFaceSurface`, and samples the same deform field across each `features` member's rest span (`featRx = (24 / 106) · W`, `featRy = (12 / 128) · H`) with its depth fraction `z / zRef` as the field's angle input and chord foreshortening (`scalex` narrowing on the leading side and widening slightly on the trailing side with its outer edge anchored to the field, `scaley` on pitch) so every feature's bounding box stays inside the deformed face plate across the rated band (issue #468). Optional `band` (`number` or `{yaw?, pitch?}`, positive degrees, issue #472) declares the rig's rated face turn band (default `FACE_RATED_BAND`: `yaw <= 15°`, `pitch <= 12°`); declaring a `turn.band` or a `motion.animations_from` `turn`/`tilt` `derive.degrees` above `±15°` yaw or `±12°` pitch without an `assemble.alternates` entry that replaces `face` is refused (`CONFIG_FACE_BAND_EXCEEDED`), and on a single-plate rig `applyFaceSurfaceDeform` adds self-targeting transform constraints `yaw_clamp` (`[-15, 15]`) and `pitch_clamp` (`[-12, 12]`) ahead of the slider constraints so driving `yaw` or `pitch` beyond the rated band clamps at the boundary. 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 `pivot` nor `depths` 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
128
|
| `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
129
|
| `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 |
|
|
129
130
|
| `proposed_rig_scale` | assemble, rig, build, `propose --from-config`/`--compare` | **proposed** by `propose` (issue #354): `parts.json`'s `scale_rig_per_source`, the `assemble.rig_scale` the rig sections (`bones`, `meshes`, `limbs`, `motion`) were drafted at. Held equal to `assemble.rig_scale` when present (`CONFIG_PROPOSED_RIG_SCALE`; 0, negative or not a number: `CONFIG_FIELD_TYPE`). Absent on a hand-authored config, where no byte of any output moves |
|
|
130
131
|
|
|
132
|
+
**One drawing, one band; beyond it, another drawing (issue #472).** A single front-facing
|
|
133
|
+
`face` plate has one painted outline, one nose ridge, and two eyes in front-view topology.
|
|
134
|
+
Within the rated single-plate band (`FACE_RATED_BAND`: `yaw ±15°`, `pitch ±12°`),
|
|
135
|
+
`applyFaceSurfaceDeform` bends the 5×5 `face` mesh and carries the features along the same
|
|
136
|
+
surface field with chord foreshortening (`#468`), keeping every feature inside the face
|
|
137
|
+
plate without inventing unpainted geometry. Past `±15°` yaw or `±12°` pitch, a 3/4 or
|
|
138
|
+
profile turn is a topological change — the far cheek disappears behind the bridge of the
|
|
139
|
+
nose, the jawline shifts to a three-quarter contour, and the far eye changes silhouette —
|
|
140
|
+
which warping one front plate can only fake as a stretched rubber mask. Rather than
|
|
141
|
+
silently extrapolating a single plate into angles it does not depict, `rig-parts` refuses
|
|
142
|
+
any `turn.band` or `turn`/`tilt` `derive.degrees` above `±15°` / `±12°` unless an
|
|
143
|
+
`assemble.alternates` entry supplies a second `face` drawing (`CONFIG_FACE_BAND_EXCEEDED`),
|
|
144
|
+
clamps the single-plate `yaw` and `pitch` controls at `±15` and `±12` via `yaw_clamp` and
|
|
145
|
+
`pitch_clamp`, and, when an alternate face set is declared, omits the clamps and records
|
|
146
|
+
`"threshold": {"yaw": 15, "pitch": 12}` on that set in `sets.json` so the consumer swaps
|
|
147
|
+
plates at the rated boundary.
|
|
148
|
+
|
|
131
149
|
**A scene target is an ordinary bone.** A target the scene places — a point a hand
|
|
132
150
|
reaches for, a face something turns toward — is a single bone parented to `root`,
|
|
133
151
|
`{"name": "tgt", "parent": "root", "at": [x, y]}`, that a constraint names as its
|
|
@@ -2657,6 +2675,8 @@ See-through with another seed.
|
|
|
2657
2675
|
| <a id="CONFIG_CLOTH_SPRING_STABLE"></a>`CONFIG_CLOTH_SPRING_STABLE` | a `cloth` recipe's strength — declared, or the default 100 / 100 / 50 — whose spring is unstable at a damping the recipe runs at (`damping`, `alternate_damping`, or rig-c's 0.85 when `damping` is left out): k = strength·massInverse·step² at or past 2(1 + damping^(60·step)), step 1/60 and mass 1 (rig-c's defaults; the recipe sets neither). The detail gives k and the bound | a lower strength, or a higher damping |
|
|
2658
2676
|
| <a id="CONFIG_TURN_BONE_FREE"></a>`CONFIG_TURN_BONE_FREE` | the config has `turn` and `bones` declares a bone named `turn`, the control the rig stage adds | rename that bone |
|
|
2659
2677
|
| <a id="CONFIG_TURN_DEFORM_RANGE"></a>`CONFIG_TURN_DEFORM_RANGE` | an entry in `turn.deform` lies outside its allowed `[min, max]` interval in `FACE_DEFORM_RANGES` (`src/turn.ts`, issue #436); the detail gives the value found, the interval required and the default fraction | put that `turn.deform.<key>` inside the stated range, or remove it to take the Set B ×2.5 default |
|
|
2678
|
+
| <a id="CONFIG_TURN_EAR_DEPTH_RANGE"></a>`CONFIG_TURN_EAR_DEPTH_RANGE` | an ear track (`group: "ears"` or `bone: "ear_r" \| "ear_l"`) in `turn` or `tilt` declares a `derive.depth` outside `EAR_DEPTH_RANGE` (`[-50, 0]`, `src/turn.ts`, issue #477); the detail gives the value found and the interval required | put the ear `derive.depth` in `[-50, 0]` (at or behind the head pivot, e.g. `-14`) |
|
|
2679
|
+
| <a id="CONFIG_FACE_BAND_EXCEEDED"></a>`CONFIG_FACE_BAND_EXCEEDED` | `turn.band` or a `motion.animations_from` `turn`/`tilt` `derive.degrees` exceeds the single-plate rated band (`FACE_RATED_BAND`: `yaw <= 15°`, `pitch <= 12°`, issue #472) while no `assemble.alternates` entry replaces `face`; the detail gives the requested angle, the rated limit and both remedies | clamp the angle to `±15°` yaw / `±12°` pitch, or declare an `assemble.alternates` entry covering `face` for the 3/4 or profile plate |
|
|
2660
2680
|
| <a id="CONFIG_PROPOSED_RIG_SCALE"></a>`CONFIG_PROPOSED_RIG_SCALE` | `proposed_rig_scale` (the `scale_rig_per_source` `propose` drafted the rig sections at, issue #354) differs from `assemble.rig_scale`: the coordinates in `bones`, `meshes`, `limbs` and `motion` are in that rig canvas's pixels and do not scale when `assemble.rig_scale` changes | re-run `assemble` and `propose` at the new `assemble.rig_scale`, or restore `assemble.rig_scale` to `proposed_rig_scale` |
|
|
2661
2681
|
|
|
2662
2682
|
### inputs
|
|
@@ -2729,6 +2749,7 @@ says why each requirement exists and what breaks without it.
|
|
|
2729
2749
|
| <a id="ASSEMBLE_HIDDEN_ONE_SOURCE"></a>`ASSEMBLE_HIDDEN_ONE_SOURCE` | a cut's `under`, or a limb segment's `cap`, names a file, and the part it reads also has a hidden-art layer (`<layer>:hidden`, issue #359): reading either would be a guess | remove the file from the config, or the hidden-art layer from the input |
|
|
2730
2750
|
| <a id="ASSEMBLE_ALT_INPUT_PRESENT"></a>`ASSEMBLE_ALT_INPUT_PRESENT` | an alternate painting set (`assemble.alternates` or `--alt`, issue #454) is missing `source` or `full`, names a file or directory that is not on disk, or lacks a covered part's layer in its runs | give `source` and `full` on the alternate set, or list an occluded part in `hides` |
|
|
2731
2751
|
| <a id="ASSEMBLE_ALT_CANVAS_SIZE"></a>`ASSEMBLE_ALT_CANVAS_SIZE` | an alternate painting's canvas size differs from the primary painting's (issue #454; the detail gives both sizes) | the alternate painting: the primary painting's canvas size, so its parts land in the same rig frame |
|
|
2752
|
+
| <a id="ASSEMBLE_ALT_SKELETON"></a>`ASSEMBLE_ALT_SKELETON` | an alternate set's `skeleton` file (`assemble.alternates[i].skeleton`, issue #465) is not JSON or does not hold `{"width": W, "height": H, "points": {…the 18 body-18 keypoints}}` | that set's `skeleton` file |
|
|
2732
2753
|
| <a id="ASSEMBLE_COUNTS_ADD_UP"></a>`ASSEMBLE_COUNTS_ADD_UP` | a part's visible and occluded counts do not add up to its opaque pixels, or a projected pixel is not visible, or a cut part's pieces do not add up to it — an assembler bug, not an input problem | report it with the part named; nothing was written |
|
|
2733
2754
|
|
|
2734
2755
|
### propose
|
|
@@ -2829,7 +2850,7 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
2829
2850
|
| --- | --- | --- |
|
|
2830
2851
|
| <a id="CHECK_RIGC_PRESENT"></a>`CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
2831
2852
|
| <a id="CHECK_RIGC_PACKAGE"></a>`CHECK_RIGC_PACKAGE` | `node_modules/.bin/rigc` is missing under the package root named: this lookup (the selftest's, issue #337) takes this package's own rigc and no other — no directory above, no `PATH` | `bun install` in that root |
|
|
2832
|
-
| <a id="CHECK_INPUT_PRESENT"></a><a id="CHECK_INPUT_IS_JSON"></a><a id="CHECK_PART_PNG_PRESENT"></a><a id="CHECK_PART_PNG_MATCHES_BOX"></a><a id="CHECK_PART_SLOT_PRESENT"></a><a id="CHECK_RIG_STAGE_PRESENT"></a><a id="CHECK_RIG_STAGE_IS_THE_CANVAS"></a><a id="CHECK_RIG_ROOT_BONE"></a><a id="CHECK_IDLE_PRESENT"></a>`CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_PART_SLOT_PRESENT`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` (`CHECK_PART_SLOT_PRESENT`: a part with no slot of its own name, which the judgement lines render it by). `motion.json` is required even with no animation in it, because `rigc build` takes `--motion` (a spec with `"animations": {}` builds green); `CHECK_IDLE_PRESENT` is an `idle` that is there with no positive `duration` — an absent `idle` is measured around (§7). `CHECK_INPUT_PRESENT` on `…/parts.json`: `--parts` named a directory without `parts.json` — most often `parts/` itself — or, without `--parts`, the rig directory has none while the directory above it does (build's layout, `<out>/rig` under `<out>/parts.json`). The detail says it: "--parts names the directory holding parts.json and parts/, not parts/ itself (after build, that is build's --out, whose rig is <out>/rig); without --parts it is --rig", and names the parent when the parent holds `parts.json`. With neither (no `--parts`, and no `parts.json` beside `rig.json` or above it), the rig is measured without parts and nothing is refused (§7) | re-run rig (`build` does both); for `…/parts.json`, `--parts` — the directory above `parts/` |
|
|
2853
|
+
| <a id="CHECK_INPUT_PRESENT"></a><a id="CHECK_INPUT_IS_JSON"></a><a id="CHECK_PART_PNG_PRESENT"></a><a id="CHECK_PART_PNG_MATCHES_BOX"></a><a id="CHECK_PART_SLOT_PRESENT"></a><a id="CHECK_RIG_STAGE_PRESENT"></a><a id="CHECK_RIG_STAGE_IS_THE_CANVAS"></a><a id="CHECK_RIG_ROOT_BONE"></a><a id="CHECK_IDLE_PRESENT"></a>`CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_PART_SLOT_PRESENT`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` (`CHECK_PART_SLOT_PRESENT`: a part with no slot of its own name, which the judgement lines render it by; `CHECK_INPUT_PRESENT` on `--set` / `--all-sets`: no `parts.json`, no `alternates`, or the named set is not in `alternates`). `motion.json` is required even with no animation in it, because `rigc build` takes `--motion` (a spec with `"animations": {}` builds green); `CHECK_IDLE_PRESENT` is an `idle` that is there with no positive `duration` — an absent `idle` is measured around (§7). `CHECK_INPUT_PRESENT` on `…/parts.json`: `--parts` named a directory without `parts.json` — most often `parts/` itself — or, without `--parts`, the rig directory has none while the directory above it does (build's layout, `<out>/rig` under `<out>/parts.json`). The detail says it: "--parts names the directory holding parts.json and parts/, not parts/ itself (after build, that is build's --out, whose rig is <out>/rig); without --parts it is --rig", and names the parent when the parent holds `parts.json`. With neither (no `--parts`, and no `parts.json` beside `rig.json` or above it), the rig is measured without parts and nothing is refused (§7) | re-run rig (`build` does both); for `…/parts.json`, `--parts` — the directory above `parts/` |
|
|
2833
2854
|
| <a id="CHECK_SOURCE_SIZE"></a>`CHECK_SOURCE_SIZE` | the painting `--source` names is not this rig's: no single `rig_scale` takes its width and height to the stage's (assemble makes the canvas `trunc(w × rig_scale)` x `trunc(h × rig_scale)`), or, with `parts.json` read, its `scale_rig_per_source` does not; or the stage is not whole pixels. Refused before anything is built | the painting the rig was made from — the stage's own size, or the painting `assemble` was given |
|
|
2834
2855
|
| <a id="CHECK_SOURCE_PNG"></a>`CHECK_SOURCE_PNG` | `--source` names a file that does not read as a PNG | a PNG |
|
|
2835
2856
|
| <a id="CHECK_SOURCE_GRID"></a>`CHECK_SOURCE_GRID` | the setup-pose still and its black-tinted twin (the coverage `SETUP_POSE_VS_SOURCE` reads) did not come back on one grid over one opaque background | a rigc problem; report it |
|
|
@@ -2838,7 +2859,7 @@ documents do not ship). Each is an input this package built; none is an author's
|
|
|
2838
2859
|
| <a id="CHECK_RIGC_GREEN"></a>`CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
2839
2860
|
| <a id="CHECK_LOOP_LAST_FRAME_AT_DURATION"></a>`CHECK_LOOP_LAST_FRAME_AT_DURATION` | the idle's last frame does not sit at `duration` | `motion.duration` — a whole number of 1/12 s |
|
|
2840
2861
|
| <a id="CHECK_LOOP_CLOSES"></a>`CHECK_LOOP_CLOSES` | frame 0 and the frame at `duration` differ (max and first pixel quoted); with a physics constraint, the frames of the rig with every physics constraint left out, and the object says so (§7, *loop*) | a track whose last key is not its first |
|
|
2841
|
-
| <a id="CHECK_SEAM_WITHIN_BAR"></a>`CHECK_SEAM_WITHIN_BAR` | the setup pose does not reproduce the flat stack of parts | read the `seam pair` lines and `check/seam/` pictures (§7; magenta = a counted pixel, cyan = the box round them): the pair, its boxes and the pixels counted. Usually a region or mesh placed off its part; compare with `recomposite_rig.png
|
|
2862
|
+
| <a id="CHECK_SEAM_WITHIN_BAR"></a>`CHECK_SEAM_WITHIN_BAR` | the setup pose (or, under `--set <name>` / `--all-sets`, an alternate set's setup pose posed at its `drawn` bone angles with its `<set>_*` attachments and `hides` applied, issue #465) does not reproduce the flat stack of parts (`recomposite_rig.png`, or `recomposite_rig_<set>.png`) | read the `seam pair` lines and `check/seam/` (`check/seam_<set>/`) pictures (§7; magenta = a counted pixel, cyan = the box round them): the pair, its boxes and the pixels counted. Usually a region or mesh placed off its part; compare with `recomposite_rig.png` (`recomposite_rig_<set>.png`). A pair of one layer cut in two with `differing_copies_px` is the cut line: move the polygon's edge, not `overlap` |
|
|
2842
2863
|
| <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) |
|
|
2843
2864
|
| <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 |
|
|
2844
2865
|
| <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) | that chain track's `phase`/`lag` (a positive `lag`, a child `phase` above its parent's) or its `amps` (non-decreasing toward the tip; non-increasing on a chain declared resting) |
|
package/docs/STABILITY.md
CHANGED
|
@@ -56,9 +56,9 @@ defaults are the help's too, and none is guessed from the art.
|
|
|
56
56
|
| `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`), `--full`, `--head` (the turn control's depths; not with `--from-config`), `--grid-rule` (`figure` or `part`, the drafted lattice's spacing; not with `--from-config`), `--cloth` (a config whose `cloth` block is the recipe, issue #364; not with `--from-config`) |
|
|
57
57
|
| `compare` | `--left`, `--right`; optional `--map` |
|
|
58
58
|
| `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
|
|
59
|
-
| `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills` |
|
|
59
|
+
| `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--set` or `--all-sets` (issue #454) |
|
|
60
60
|
| `loop` | `--frames`, `--out`; optional `--palette` |
|
|
61
|
-
| `build` | `--config`, `--source`, `--full`, `--out`; optional `--head` (as for `assemble`), `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--lead-in`, `--requirements`, `--idle-keys`, `--seam-pairs`, `--keep-stills`, `--alt` (as for `assemble`, issue #454) |
|
|
61
|
+
| `build` | `--config`, `--source`, `--full`, `--out`; optional `--head` (as for `assemble`), `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--lead-in`, `--requirements`, `--idle-keys`, `--seam-pairs`, `--keep-stills`, `--alt` (as for `assemble`, issue #454), `--set` or `--all-sets` (as for `check`, issue #454) |
|
|
62
62
|
| `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
|
|
63
63
|
| `mesh-draft` | `--config`, `--parts`, `--out`; optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon), `--more-triangles` |
|
|
64
64
|
| `painting` | one positional path, the painting; `--keypoints`; optional `--person` |
|
|
@@ -121,7 +121,7 @@ part name, `[i]` an entry of a list):
|
|
|
121
121
|
- **seethrough** — required: `resolution`, `steps`, `seed`, `offload`; optional: `head_box`, `arm_split`
|
|
122
122
|
- **seethrough.arm_split** — required: `order`, and one of `x` or `line` (issue #439)
|
|
123
123
|
- **assemble** — required: `rig_scale`, `plan` (from plain `assemble` on); optional: `extend_below_crop`, `patches`, `cuts`, `alternates`
|
|
124
|
-
- **assemble.alternates[i]** — required: `name`; optional: `source`, `full`, `head`, `parts`, `hides`, `head_box`, `cuts` (
|
|
124
|
+
- **assemble.alternates[i]** — required: `name`; optional: `source`, `full`, `head`, `parts`, `hides`, `head_box`, `cuts`, `skeleton`, `drawn` (issues #454, #465)
|
|
125
125
|
- **assemble.extend_below_crop[i]** — required: `part`, `run`, `tag`
|
|
126
126
|
- **assemble.patches[i]** — required: `name`, `box`, `alpha`, `draw`
|
|
127
127
|
- **assemble.patches[i].draw**, when an object — required: `before`
|
|
@@ -162,9 +162,10 @@ part name, `[i]` an entry of a list):
|
|
|
162
162
|
- **motion.blink** — required: `t`, `eyes`, `squash`; optional: `brows`, `brow_drop`, `still`
|
|
163
163
|
- **motion.blink.still.<part>** — required: `row`, `bone`
|
|
164
164
|
- **motion.animations_from**, the file it names — required: `spec`, `animations`
|
|
165
|
-
- **turn** — required: `pivot`, `depths`; optional: `deform`
|
|
165
|
+
- **turn** — required: `pivot`, `depths`; optional: `deform`, `band`
|
|
166
166
|
- **turn.depths.<part>** — a See-through depth
|
|
167
167
|
- **turn.deform.<key>** — a per-degree fraction or coupling share (FACE_DEFORM_FRACTIONS, issue #436)
|
|
168
|
+
- **turn.band**, when an object — optional: `yaw`, `pitch` (issue #472)
|
|
168
169
|
- **scene** — no field (`{}`; a record or an annotation only)
|
|
169
170
|
- **proposal.json**, the file propose writes — `bones`, `meshes`, `regions`, `limbs` under `--keypoints` where an arm or a leg is drafted from its joints (issue #349), `stops` when supplied, `motion` under the rules above, `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes`
|
|
170
171
|
<!-- /stable:config -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"ag-psd": "^31.0.2",
|
|
64
|
-
"rig-c": "^2.
|
|
64
|
+
"rig-c": "^2.59.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@esotericsoftware/spine-core": "4.3.13",
|
package/src/build.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
44
44
|
import { dirname, join, resolve } from 'node:path';
|
|
45
45
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
46
|
-
import { assemble, type AssembleResult, cutLines, hiddenLines, SINGLE_RUN_FACE_TAGS, singleRunHeadBox, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, type ProjectRule, scenePhrase, type SeamRule, stageFields, uncoveredLine } from './assemble.ts';
|
|
46
|
+
import { assemble, type AssembleResult, cutLines, hiddenLines, SINGLE_RUN_FACE_TAGS, singleRunHeadBox, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, measureRecomposite, type ProjectRule, recomposite, recompositeRecord, scenePhrase, type SeamRule, sourceInRig, stageFields, uncoveredLine } from './assemble.ts';
|
|
47
47
|
import { allocationClause, type Reducer, stageBClause, unboundedClause } from './automesh.ts';
|
|
48
48
|
import { residualClause } from './autoenvelope.ts';
|
|
49
49
|
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, type ReplayCandidate, runComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
@@ -83,6 +83,7 @@ import { encodePngBytes, readPng, writePng } from './raster/png.ts';
|
|
|
83
83
|
import type { Raster } from './raster/types.ts';
|
|
84
84
|
import { readRequirements, type RequirementLine, summaryText } from './requirements.ts';
|
|
85
85
|
import { type AutoMeshReport, buildRig, DEFAULT_IDLE_KEYS, type FaceAxis, type IdleKeys, type MeshReport, type RigCommand, rigJsonText, type RigOutput, type RigSpec, SETS_FILE, type StrayCleared, type TurnRow } from './rig.ts';
|
|
86
|
+
import { KEYPOINT_NAMES, type KeypointName, type PosePoints } from './skeleton.ts';
|
|
86
87
|
import { AUTHOR_NO_ROLE, isAuthorTag } from './tags.ts';
|
|
87
88
|
import { TURN_BONE } from './turn.ts';
|
|
88
89
|
import { pyRound } from './round.ts';
|
|
@@ -273,6 +274,8 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
273
274
|
hides?: string[];
|
|
274
275
|
head_box?: [number, number, number, number];
|
|
275
276
|
cuts?: Cut[];
|
|
277
|
+
skeleton?: string;
|
|
278
|
+
drawn?: Record<string, number>;
|
|
276
279
|
}> = cfgAlts.map((a, i) => {
|
|
277
280
|
const cli = cliByName.get(a.name);
|
|
278
281
|
if (cli !== undefined) cliByName.delete(a.name);
|
|
@@ -286,6 +289,8 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
286
289
|
hides: a.hides,
|
|
287
290
|
head_box: a.head_box,
|
|
288
291
|
cuts: a.cuts,
|
|
292
|
+
skeleton: a.skeleton === undefined ? undefined : resolve(configDir, a.skeleton),
|
|
293
|
+
drawn: a.drawn,
|
|
289
294
|
};
|
|
290
295
|
});
|
|
291
296
|
for (const cli of cliByName.values()) {
|
|
@@ -334,6 +339,48 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
334
339
|
});
|
|
335
340
|
continue;
|
|
336
341
|
}
|
|
342
|
+
let altSkeleton: PosePoints | undefined;
|
|
343
|
+
if (alt.skeleton !== undefined) {
|
|
344
|
+
if (!existsSync(alt.skeleton) || !statSync(alt.skeleton).isFile()) {
|
|
345
|
+
altProblems.push({
|
|
346
|
+
code: 'ASSEMBLE_ALT_INPUT_PRESENT',
|
|
347
|
+
object: `${alt.skeleton} (alternate set "${alt.name}")`,
|
|
348
|
+
detail: 'no such file; the alternate pose skeleton is required when named',
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const rawSkel: unknown = JSON.parse(readFileSync(alt.skeleton, 'utf8'));
|
|
354
|
+
const skObj = typeof rawSkel === 'object' && rawSkel !== null && !Array.isArray(rawSkel) ? (rawSkel as Record<string, unknown>) : null;
|
|
355
|
+
const sw = skObj !== null && typeof skObj.width === 'number' && skObj.width > 0 ? skObj.width : null;
|
|
356
|
+
const sh = skObj !== null && typeof skObj.height === 'number' && skObj.height > 0 ? skObj.height : null;
|
|
357
|
+
const spts = skObj !== null && typeof skObj.points === 'object' && skObj.points !== null && !Array.isArray(skObj.points) ? (skObj.points as Record<string, unknown>) : null;
|
|
358
|
+
if (sw === null || sh === null || spts === null || KEYPOINT_NAMES.some((k) => !Array.isArray(spts[k]) || (spts[k] as unknown[]).length !== 2)) {
|
|
359
|
+
altProblems.push({
|
|
360
|
+
code: 'ASSEMBLE_ALT_SKELETON',
|
|
361
|
+
object: `${alt.skeleton} (alternate set "${alt.name}")`,
|
|
362
|
+
detail: 'must hold {"width": W, "height": H, "points": {…the 18 body-18 keypoints}}',
|
|
363
|
+
});
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const [rW, rH] = result.parts.rig_size;
|
|
367
|
+
const sx = rW / sw;
|
|
368
|
+
const sy = rH / sh;
|
|
369
|
+
const scaled = {} as Record<KeypointName, readonly [number, number]>;
|
|
370
|
+
for (const k of KEYPOINT_NAMES) {
|
|
371
|
+
const [px, py] = spts[k] as [number, number];
|
|
372
|
+
scaled[k] = [pyRound(px * sx, 6), pyRound(py * sy, 6)];
|
|
373
|
+
}
|
|
374
|
+
altSkeleton = scaled;
|
|
375
|
+
} catch (err) {
|
|
376
|
+
altProblems.push({
|
|
377
|
+
code: 'ASSEMBLE_ALT_SKELETON',
|
|
378
|
+
object: `${alt.skeleton} (alternate set "${alt.name}")`,
|
|
379
|
+
detail: `does not parse as JSON: ${(err as Error).message}`,
|
|
380
|
+
});
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
337
384
|
const altSrc = readSource(alt.source);
|
|
338
385
|
if (altSrc.width !== src.width || altSrc.height !== src.height) {
|
|
339
386
|
altProblems.push({
|
|
@@ -421,6 +468,17 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
421
468
|
const keptRecords = altRes.parts.parts.filter((p) => coveredSet.has(p.name));
|
|
422
469
|
const slots: Record<string, string> = {};
|
|
423
470
|
for (const r of keptRecords) slots[r.name] = `${alt.name}_${r.name}`;
|
|
471
|
+
let altRecomposite = altRes.parts.recomposite as RecompositeRecord;
|
|
472
|
+
if (alt.parts !== undefined) {
|
|
473
|
+
const [rW, rH] = result.parts.rig_size;
|
|
474
|
+
const altByName = new Map(altRes.images.map((x) => [x.record.name, x]));
|
|
475
|
+
const fullSetPlaced = result.images
|
|
476
|
+
.filter(({ record }) => !hidesSet.has(record.name))
|
|
477
|
+
.map((baseItem) => altByName.get(baseItem.record.name) ?? baseItem);
|
|
478
|
+
const altSrcRig = sourceInRig(altSrc, rW, rH);
|
|
479
|
+
const altCan = recomposite(fullSetPlaced, rW, rH);
|
|
480
|
+
altRecomposite = recompositeRecord(measureRecomposite(altCan, altSrcRig, fullSetPlaced));
|
|
481
|
+
}
|
|
424
482
|
altBuilt.push({
|
|
425
483
|
record: {
|
|
426
484
|
name: alt.name,
|
|
@@ -429,9 +487,11 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
429
487
|
slots,
|
|
430
488
|
records: keptRecords,
|
|
431
489
|
ghost_px: altRes.parts.ghost_px,
|
|
432
|
-
recomposite:
|
|
490
|
+
recomposite: altRecomposite,
|
|
433
491
|
...(altRes.parts.scene === undefined ? {} : { scene: altRes.parts.scene }),
|
|
434
492
|
...(altRes.parts.single_run_pad === undefined ? {} : { single_run_pad: altRes.parts.single_run_pad }),
|
|
493
|
+
...(altSkeleton === undefined ? {} : { skeleton: altSkeleton }),
|
|
494
|
+
...(alt.drawn === undefined ? {} : { drawn: { ...alt.drawn } }),
|
|
435
495
|
},
|
|
436
496
|
images: keptImages,
|
|
437
497
|
});
|
|
@@ -1026,12 +1086,15 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
1026
1086
|
}
|
|
1027
1087
|
}
|
|
1028
1088
|
const first = buildRig(cfg, parts, images, undefined, input.idleKeys ?? DEFAULT_IDLE_KEYS, input.reduce);
|
|
1029
|
-
const textsOf = (r: RigOutput): Array<[string, string]> =>
|
|
1030
|
-
|
|
1031
|
-
[
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1089
|
+
const textsOf = (r: RigOutput): Array<[string, string]> => {
|
|
1090
|
+
const mergedMotion = withAnimations(r.motion, from === null ? null : from.table);
|
|
1091
|
+
return [
|
|
1092
|
+
['rig.json', rigJsonText(r.rig)],
|
|
1093
|
+
['motion.json', rigJsonText(mergedMotion)],
|
|
1094
|
+
['mesh_report.json', rigJsonText(r.meshReport)],
|
|
1095
|
+
...(r.sets === undefined ? [] : [[SETS_FILE, rigJsonText(r.sets)] as [string, string]]),
|
|
1096
|
+
];
|
|
1097
|
+
};
|
|
1035
1098
|
const firstTexts = textsOf(first);
|
|
1036
1099
|
// The gate runs before the lines are printed, and its lines print where they always did: an automatic part's motion
|
|
1037
1100
|
// gate compares the model document of this very build, the one written (issue #126 item 3). mesh_report.json is staged
|
|
@@ -1210,6 +1273,10 @@ export interface CheckStageInput {
|
|
|
1210
1273
|
seamPairs?: boolean;
|
|
1211
1274
|
/** `check --keep-stills` (issue #209): leave the setup-pose still and its `frames.json` under {@link STILL_DIR}. */
|
|
1212
1275
|
keepStills?: boolean;
|
|
1276
|
+
/** `check --set <name>` (issue #454): measure one declared alternate set's setup-pose seam and recomposite figures. */
|
|
1277
|
+
set?: string;
|
|
1278
|
+
/** `check --all-sets` (issue #454): measure every declared alternate set's setup-pose seam and recomposite figures. */
|
|
1279
|
+
allSets?: boolean;
|
|
1213
1280
|
}
|
|
1214
1281
|
|
|
1215
1282
|
/**
|
|
@@ -1389,7 +1456,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1389
1456
|
const entryLine = versionLines.find((l) => l.startsWith('entry:'));
|
|
1390
1457
|
log(` ${rigcIdentity(bin, v)}${entryLine === undefined ? '' : `; ${entryLine}`}`);
|
|
1391
1458
|
const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
|
|
1392
|
-
const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true);
|
|
1459
|
+
const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source, input.requirements, input.seamPairs === true, input.keepStills === true, input.set, input.allSets === true);
|
|
1393
1460
|
log(` gate rig-play (rigc ${packedBuildLabel(mode)}), verbatim:`);
|
|
1394
1461
|
for (const l of r.gateHtml) log(l);
|
|
1395
1462
|
for (const l of packLines(r)) log(` ${l}`);
|
|
@@ -1407,6 +1474,16 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1407
1474
|
);
|
|
1408
1475
|
const pairs = fig.seam_pairs ?? [];
|
|
1409
1476
|
pairs.forEach((row, i) => log(` seam pair ${i + 1} of ${pairs.length}: ${seamPairText(row)}`));
|
|
1477
|
+
if (fig.sets !== undefined) {
|
|
1478
|
+
for (const [setName, setFig] of Object.entries(fig.sets)) {
|
|
1479
|
+
log(
|
|
1480
|
+
` seam (${setName})${setFig.seam_border === undefined ? '' : ` (config.scene: ${setFig.seam_border.frame_px_left_out} frame px on the painting's border rows and columns left out, ${setFig.seam_border.px_over_40_left_out} of them over ${SEAM_PX_LEVEL})`}: setup pose at ${setFig.render_px}, scale ${setFig.seam_frame_scale.toFixed(4)}: mean |d| ${setFig.seam_mean} (<= ${SEAM_MEAN_BAR.toFixed(1)}), ` +
|
|
1481
|
+
`${setFig.seam_px_over_40} px over ${SEAM_PX_LEVEL}, ${seamRigPxText(setFig.seam_px_over_40, setFig.seam_frame_scale)} rig px (the count / s²; <= ${SEAM_PX_BAR}), ${setFig.seam_px_over_80} px over ${SEAM_PX_LEVEL_HIGH} (reported)`,
|
|
1482
|
+
);
|
|
1483
|
+
const setPairs = setFig.seam_pairs ?? [];
|
|
1484
|
+
setPairs.forEach((row, i) => log(` seam pair (${setName}) ${i + 1} of ${setPairs.length}: ${seamPairText(row)}`));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1410
1487
|
}
|
|
1411
1488
|
for (const name of JUDGEMENT_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1412
1489
|
const folds = fig[FOLD_LINE];
|
|
@@ -1420,6 +1497,11 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1420
1497
|
const sweep = fig[SWEEP_LINE];
|
|
1421
1498
|
if (sweep !== undefined) for (const [i, l] of sweepCheckLines(sweep).entries()) log(` ${i === 0 ? '' : ' '}${l}`);
|
|
1422
1499
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
1500
|
+
if (fig.sets !== undefined) {
|
|
1501
|
+
for (const [setName, setFig] of Object.entries(fig.sets)) {
|
|
1502
|
+
log(` ${judgementLine(`RECOMPOSITE_HOLES (${setName})`, setFig.RECOMPOSITE_HOLES)}`);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1423
1505
|
const vsSource = fig[SOURCE_LINE];
|
|
1424
1506
|
if (vsSource !== undefined) log(` ${judgementLine(SOURCE_LINE, vsSource)}`);
|
|
1425
1507
|
if (r.requirements !== null) {
|
|
@@ -1707,6 +1789,10 @@ export interface BuildInput {
|
|
|
1707
1789
|
keepStills?: boolean;
|
|
1708
1790
|
/** `--alt`, forwarded to the assemble stage as `assemble` takes it (issue #454). */
|
|
1709
1791
|
alts?: readonly AlternateCliInput[];
|
|
1792
|
+
/** `--set <name>`, forwarded to the check as `check` takes it (issue #454). */
|
|
1793
|
+
set?: string;
|
|
1794
|
+
/** `--all-sets`, forwarded to the check as `check` takes it (issue #454). */
|
|
1795
|
+
allSets?: boolean;
|
|
1710
1796
|
}
|
|
1711
1797
|
|
|
1712
1798
|
/** What `build` does with one flag `check` takes: hands it to its check stage, takes it as its own, or refuses it because build supplies the value itself. */
|
|
@@ -1729,6 +1815,8 @@ export const CHECK_FLAGS_IN_BUILD: Readonly<Record<string, CheckFlagInBuild>> =
|
|
|
1729
1815
|
'--page-edges': { as: 'forwarded' },
|
|
1730
1816
|
'--pack-shape': { as: 'forwarded' },
|
|
1731
1817
|
'--requirements': { as: 'forwarded' },
|
|
1818
|
+
'--set': { as: 'forwarded' },
|
|
1819
|
+
'--all-sets': { as: 'forwarded' },
|
|
1732
1820
|
'--seam-pairs': { as: 'forwarded' },
|
|
1733
1821
|
'--keep-stills': { as: 'forwarded' },
|
|
1734
1822
|
};
|
|
@@ -1841,7 +1929,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
1841
1929
|
|
|
1842
1930
|
let report: CheckReport;
|
|
1843
1931
|
try {
|
|
1844
|
-
report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(input.requirements === undefined ? {} : { requirements: input.requirements }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true }, run.check, run.checkBin, prefixed('check'));
|
|
1932
|
+
report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(input.requirements === undefined ? {} : { requirements: input.requirements }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true, ...(input.set === undefined ? {} : { set: input.set }), allSets: input.allSets === true }, run.check, run.checkBin, prefixed('check'));
|
|
1845
1933
|
} catch (err) {
|
|
1846
1934
|
return refused('check', err);
|
|
1847
1935
|
}
|