rig-parts 1.51.0 → 1.52.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 +30 -0
- package/docs/AUTHORING.md +96 -11
- package/docs/PAINTING.md +27 -2
- package/docs/STABILITY.md +1 -0
- package/package.json +1 -1
- package/src/assemble.ts +117 -1
- package/src/build.ts +23 -5
- package/src/config.ts +25 -9
- package/src/meshstats.ts +57 -0
- package/src/parts.ts +15 -2
package/cli.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { checkImageSize, choosePerson, KEYPOINTS_SPEC, loadKeypoints, type RigJo
|
|
|
30
30
|
import { basisLines, branchSqueezeLine, checkProposal, compare, compareLines, DEFAULT_GRID_RULE, draftTurn, drawLandmarks, FIGURE_GRID_DIVISOR, FRINGE_STEP_PX, GRID_RULES, type GridRule, HIP_MIN_FRACTION, type DraftLintFinding, lint, lintLine, type PartSet, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
|
|
31
31
|
import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, serializeBasis } from './src/diagnostics.ts';
|
|
32
32
|
import { BEND_DEGREES, BEND_WORST_SHARE, DUST_MAX_PX, DUST_MAX_SHARE, IDLE_BOUND_PX, MESH_DRAFT_REPORT, MESH_DRAFT_SPEC, meshDraftStage, MORE_TRIANGLES_FLAG, relativeConfigPaths, TOLERANCE_FIGURE_DIVISOR } from './src/meshdraft.ts';
|
|
33
|
+
import { meshStatsStage } from './src/meshstats.ts';
|
|
33
34
|
import { clothConfigEdits, editPaths, previousCount, writtenClothConfig } from './src/clothconfig.ts';
|
|
34
35
|
import { PROVENANCE_FILE, REMESH_SPEC, remeshStage } from './src/remesh.ts';
|
|
35
36
|
import { RETUNE_FILE, RETUNE_SPEC, retuneStage } from './src/retune.ts';
|
|
@@ -373,6 +374,21 @@ usage:
|
|
|
373
374
|
Written only when the drafted config's check is PASS. --requirements, --idle-keys,
|
|
374
375
|
--page-edges and --pack-shape are as for rig and check.
|
|
375
376
|
|
|
377
|
+
rig-parts mesh-stats --config <config.json> --parts <dir>
|
|
378
|
+
Print the vertices and triangles of every mesh the config states, as the
|
|
379
|
+
rig stage would make them from the parts in --parts (the directory
|
|
380
|
+
holding parts.json and parts/<name>.png), and their total, in seconds:
|
|
381
|
+
the rig stage's own mesh construction, without rigc, without assemble
|
|
382
|
+
and without check (issue #566). One line per mesh in parts.json's order,
|
|
383
|
+
"mesh-stats: <part> — <mode> <V> vertices, <T> triangles" (mode lattice,
|
|
384
|
+
contour, auto or influence), then "mesh-stats: total <N> triangles over
|
|
385
|
+
<M> mesh(es)"; the counts are the ones the rig stage writes to
|
|
386
|
+
mesh_report.json, except an auto part's when rigc's motion gate refuses
|
|
387
|
+
its reduction and the rig stage replays an earlier accepted step, which
|
|
388
|
+
the auto line says. A problem the rig stage refuses (a missing part PNG,
|
|
389
|
+
an outline that does not close, a bone no config entry declares) is the
|
|
390
|
+
rig stage's own refusal, exit 1. Writes nothing.
|
|
391
|
+
|
|
376
392
|
rig-parts remesh --rig <dir> --config <config.json> --parts <dir> --part <name>
|
|
377
393
|
--mesh <entry.json> --out <dir> [--requirements <file.json>]
|
|
378
394
|
(or --delta <mesh-edit.json> in place of --mesh)
|
|
@@ -934,6 +950,7 @@ const TAKES = {
|
|
|
934
950
|
build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in', '--alt', '--set', '--environment'], switches: ['--loop', '--seam-pairs', '--keep-stills', '--all-sets'] },
|
|
935
951
|
compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
|
|
936
952
|
'mesh-draft': { valued: ['--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: ['--more-triangles'] },
|
|
953
|
+
'mesh-stats': { valued: ['--config', '--parts'], switches: [] },
|
|
937
954
|
remesh: { valued: ['--rig', '--config', '--parts', '--part', '--mesh', '--delta', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
|
|
938
955
|
retune: { valued: ['--rig', '--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
|
|
939
956
|
painting: { valued: ['--keypoints', '--person'], switches: [] },
|
|
@@ -1789,6 +1806,18 @@ function cmdMeshDraft(args: string[]): number {
|
|
|
1789
1806
|
}
|
|
1790
1807
|
}
|
|
1791
1808
|
|
|
1809
|
+
/** `mesh-stats --config <config.json> --parts <dir>` (issue #566): every configured mesh's counts, through the rig stage's own `buildRig`, without rigc. Writes nothing. */
|
|
1810
|
+
function cmdMeshStats(args: string[]): number {
|
|
1811
|
+
const f = flags(args, 'mesh-stats', ['--config', '--parts']);
|
|
1812
|
+
if (typeof f === 'string') return usage(f);
|
|
1813
|
+
try {
|
|
1814
|
+
meshStatsStage({ config: f.get('--config') as string, parts: f.get('--parts') as string }, console.log);
|
|
1815
|
+
return EXIT_OK;
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
return printRefusal(err);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1792
1821
|
/** `remesh` (issue #495): one part of a gated rig re-fitted under a requested mesh entry, into a new revision. */
|
|
1793
1822
|
function cmdRemesh(args: string[]): number {
|
|
1794
1823
|
const f = flags(args, 'remesh', ['--rig', '--config', '--parts', '--part', '--out']);
|
|
@@ -1937,6 +1966,7 @@ function main(argv: string[]): number | Promise<number> {
|
|
|
1937
1966
|
if (command === 'build') return cmdBuild(rest);
|
|
1938
1967
|
if (command === 'compose') return cmdCompose(rest);
|
|
1939
1968
|
if (command === 'mesh-draft') return cmdMeshDraft(rest);
|
|
1969
|
+
if (command === 'mesh-stats') return cmdMeshStats(rest);
|
|
1940
1970
|
if (command === 'remesh') return cmdRemesh(rest);
|
|
1941
1971
|
if (command === 'retune') return cmdRetune(rest);
|
|
1942
1972
|
if (command === 'painting') return cmdPainting(rest);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -107,7 +107,7 @@ read by no CPU stage).
|
|
|
107
107
|
| `assemble.plan` | assemble; also the list every `meshes`/`regions` key must come from | **proposed** by `assemble --propose-plan`. `[part name, "full" \| "head", See-through tag]`, in draw order back to front. Names are free; roles come from the tag. A layer the author made and added to a run takes `author:<label>` in the tag's place, and has no role (§5, *A layer the author made*). When `generation.control.skeleton` is present and a full-run `handwear-r` or `handwear-l` layer merges sleeve, bare hand and overgarment drape into one layer — a skin-coloured hand cluster (`>= 40` run px, `GHOST_MIN_AREA`, measured `516..4,787` run px across the 9-standee corpus) lies within `0.35` forearm lengths of `<s>_wrist` and non-skin pixels reach more than `1.0x` the forearm length (`<s>_elbow -> <s>_wrist`) below `<s>_wrist` (issue #490; measured `-0.15x..0.45x` on the 6 clean standees vs `1.25x..2.02x` on the 3 merged standees, placing `1.0x` inside the `0.45x..1.25x` gap) — the layer is split into `drape-<s>`, `handwear-<s>` and `hand-<s>` (back to front) when the Euclidean RGB distance between the sleeve median colour above `<s>_wrist` and the drape median colour below `<s>_wrist` is at least `80` (`ARM_DRAPE_COLOR_DISTANCE`, measured `103.6..236.0` on the two distinct-colour merged standees vs `20.8..28.2` on the same-colour merged standee, placing `80` between `28.2` and `103.6`), or refused with `SEETHROUGH_ARM_RUN_MERGED` when the distance is under `80` |
|
|
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
|
-
| `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 |
|
|
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; a part a limb takes may be cut where no segment of the limb holds it, issue #564, §5 *Limbs*), `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`), a cut of a part a limb takes that takes a pixel a segment of the limb holds (`ASSEMBLE_CUT_CROSSES_LIMB`). `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
111
|
| `assemble.alternates` | assemble, rig, check (`--set`, `--all-sets`) | **authored**, optional (issues #454, #465, #472, #473, #476): `[{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`); a **subset set** (issue #473, e.g. mouth visemes `a`, `i`, `u` with `"parts": ["mouth"]`) cuts only its covered parts from the alternate runs (refusing with `ASSEMBLE_ALT_INPUT_PRESENT` if a covered part's layer is missing or empty), writes only `{slot: "<set>_<slot>"}` in `sets.json` so `@rig-c/play`'s `setSet(name)` switches those slots while leaving every other slot on its base attachment (and `setSet(null)` restores the base pose), may be keyed on a slot attachment track in `motion.animations_from` as `{"t": …, "set": "<name>" \| null}` (`CONFIG_SET_COVERS_SLOT` if `"set"` is undeclared or does not cover the track's slot), and under `check --set <name>` counts `ANIMATIONS_DO_NOT_FOLD` only on its own covered slots; `hides` lists slots the set hides (`null` in `sets.json`). A **gesture subset set** (issue #476) covers a limb chain declared in `config.limbs.<limb>` (all of its segment `part`s; with `drawn` alone, cut at the base's joints posed to the set's own `drawn` angles by `poseBoneEntriesForDrawn`, each segment keeping the base's length; with `skeleton` (issue #539), cut at the skeleton's own joint positions by `placeBoneEntriesForSkeleton` — each covered bone the body-18 table names (`sleeve_*`, `arm_*0`/`arm_*1`, `leg_*0`/`leg_*1`) at its skeleton origin with the skeleton segment's own length and direction (`drawn` overriding the direction of a bone it names), and a covered bone the table does not name (a hand link `arm_r2`, a single `foot_r`) placed from its parent's new place — its base offset from the parent, stretched along the parent by the parent's length change and turned by its turn, so a link or a foot at the parent's end lands on the skeleton's end — with its base length and the drawn or base direction, so a foreshortened shin or forearm is cut where it is painted; naming the unsplit `from` layer, covering only some segments of the limb, or declaring polygon `cuts` on a limb set is refused with `CONFIG_LIMB_SET_JOINTS`) and carries `drawn` (or `skeleton`) for the covered bones only (giving `drawn` for an uncovered bone is refused with `CONFIG_SET_COVERS_BONE` / `RIG_SET_COVERS_BONE`; uncovered bones keep the base's `drawn`). `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 (filtered to covered bones on a subset set, 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, when the skeleton placed the set's limb bones (issue #539), `drawnLength` (`{<bone>: <ratio>}`, each covered table bone's skeleton segment length over its base length, 6 places) and `registration` (`{offset: [dx, dy], scale}`: the similarity without rotation, about the canvas origin, that maps the skeleton's torso — the mean of its shoulders `r_shoulder`/`l_shoulder` to the mean of its hips `r_hip`/`l_hip`, read where the base declares the bone rooted there, `sleeve_*0`/`arm_*0` and `leg_*0` — onto those base bones' origins, 6 places), both also written to `sets.json`; the set's painting must be registered, its torso within 1 rig px and 1 % in scale of the base's, or the set is refused by name before any joint is cut (`ASSEMBLE_ALT_SKELETON_REGISTERED`, with the offset and scale the painting would need) — 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 |
|
|
112
112
|
| `assemble.head_face_underfill` | assemble, `assemble --propose-plan` | **authored**, optional (issue #492): `boolean` (`CONFIG_FIELD_TYPE` on any other). When both runs exist and `full:face` has at least `200` opaque run px (`PROPOSE_MIN_PX`), the stage maps `full:face` into the head canvas via `fullLayerToHead` and measures how much of its hole-filled silhouette (`alpha > 8`) is covered by `head:face` (`alpha > 8`). A head-run `face` covering less than `75 %` (`HEAD_FACE_COVERAGE_FLOOR`, measured `0.0 %..48.6 %` on hollow/omitted head runs vs `84.8 %..99.6 %` on clean head runs across the 4 public examples and 9-standee corpus) is refused with `SEETHROUGH_HEAD_RUN_HOLLOW` unless `head_face_underfill: true` is declared. When `head_face_underfill: true` is declared and `head:face` exists (`opaqueRunPx >= 200`), `fullLayerToHead(full:face)` is composited under `head:face` on the head canvas (`alpha = max(headAlpha, fullAlpha)`, RGB blended by `headAlpha / 255`) before `head:face` is placed in rig space (`underfill=<N>px(full:face)` on the `face` part log line and a `face: head_face_underfill composited …` note in `assemble --propose-plan`). The fill restores transparent pixels only and leaves the cut's own edge where it is — a hollow whose edge is not under hair stays visible, and that is the author's to fix at the cut. When `head:face` has fewer than `200` opaque pixels, `SEETHROUGH_HEAD_RUN_HOLLOW` still fires even under `head_face_underfill: true` because the head run omitted the face layer and cannot supply a jawline |
|
|
113
113
|
| `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 |
|
|
@@ -597,6 +597,23 @@ layers, as follows; every step is arithmetic on the declared bones and the layer
|
|
|
597
597
|
line crosses <m> (stroke: luma <T> or below, the Otsu split of the <px> px …)`, and the record
|
|
598
598
|
`cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and, when the band has
|
|
599
599
|
a split, `stroke_luma_max`.
|
|
600
|
+
- **A cut from a limb's layer** (issue #564). An `assemble.cuts` entry may take pixels from a part a
|
|
601
|
+
limb takes — the top of a shoe See-through painted into `legwear`, which the shin would otherwise
|
|
602
|
+
hold and draw a second time over the foot. The cut is made first and the limb cuts what the part
|
|
603
|
+
keeps, so the pixels leave the shin and are drawn once, by the piece; `parts.json` records the
|
|
604
|
+
piece's `cut_from` as the limb's part, which the reader finds as the segments' `limb.of`. The cut
|
|
605
|
+
may lie only where no segment of the limb holds the layer: the segments taking the part run from
|
|
606
|
+
the joint above the first of them (when it sits below one) to the joint below the last (when one
|
|
607
|
+
follows), and every pixel the cut takes must lie **past the lower line** (the ankle under a shin) or
|
|
608
|
+
**before the upper line and outside that joint's cap disc**, which the run's own layer fills there.
|
|
609
|
+
A pixel anywhere between is `ASSEMBLE_CUT_CROSSES_LIMB`, naming the limb, the first pixel, the
|
|
610
|
+
segment's band it lies in (or the cap) and the line it must lie past. The piece is a part like any
|
|
611
|
+
other: give it a `regions` (or `meshes`) entry — for a piece past the lower line, the bone of the
|
|
612
|
+
segment below that joint, which the `cut:` line names
|
|
613
|
+
(`; "shoe_top" lies past joint "ankle" of limb "leg_r", in the band of "foot_r" on bone "foot_r"`).
|
|
614
|
+
A cap is read from the parts after the cut, so its `short_px` counts what the cut left; a piece
|
|
615
|
+
past the lower line takes nothing a cap reads (a cap is the disc's upper side), and leaves it as it
|
|
616
|
+
was.
|
|
600
617
|
- **Draw order.** A limb stands where its upper segment's part stood in the plan, its parts
|
|
601
618
|
lowest first, so the upper is drawn over the lower at every joint: upper arm over forearm,
|
|
602
619
|
thigh over shin, shin over foot. The parts it took leave the order.
|
|
@@ -1325,23 +1342,51 @@ that crosses the region with no point of it inside to split at — is still stop
|
|
|
1325
1342
|
|
|
1326
1343
|
The package proposes no expression and keys nothing but the idle; what it gives is a rig whose
|
|
1327
1344
|
face is bones (`eye_r`, `eye_l`, `brow_r`, `brow_l`, `mouth` on `reference-sample`) and one door for
|
|
1328
|
-
animations you write over them, `motion.animations_from` (the row above,
|
|
1329
|
-
file in rig-c's motion-spec shape
|
|
1345
|
+
animations you write over them, `motion.animations_from` (the row above, issues #183 and #569).
|
|
1346
|
+
Name a file in rig-c's motion-spec shape, its path relative to the directory `config.json` sits
|
|
1347
|
+
in — not the working directory:
|
|
1330
1348
|
|
|
1331
1349
|
```jsonc
|
|
1332
1350
|
// config.json
|
|
1333
1351
|
"motion": { "duration": 4.0, "tracks": [ … ], "blink": { … },
|
|
1334
|
-
"animations_from": "
|
|
1352
|
+
"animations_from": "animations.json" }
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
A whole file, one looping animation over two bones the config declares — an expression is the
|
|
1356
|
+
same shape over the face bones, and `examples/demo/animations.json` holds seven more:
|
|
1335
1357
|
|
|
1336
|
-
|
|
1358
|
+
```json
|
|
1337
1359
|
{ "spec": "rigc-motion/1",
|
|
1338
1360
|
"animations": {
|
|
1339
|
-
"
|
|
1340
|
-
{ "bone": "
|
|
1341
|
-
|
|
1342
|
-
{ "bone": "
|
|
1361
|
+
"sway": { "duration": 2, "loop": true, "tracks": [
|
|
1362
|
+
{ "bone": "chest", "property": "rotate", "keys": [
|
|
1363
|
+
{ "t": 0, "v": [0] }, { "t": 0.5, "v": [3] }, { "t": 1.5, "v": [-3] }, { "t": 2, "v": [0] } ] },
|
|
1364
|
+
{ "bone": "head", "property": "rotate", "keys": [
|
|
1365
|
+
{ "t": 0, "v": [0] }, { "t": 1, "v": [-2] }, { "t": 2, "v": [0] } ] } ] } } }
|
|
1343
1366
|
```
|
|
1344
1367
|
|
|
1368
|
+
- **`spec` is required.** Without it the loader refuses
|
|
1369
|
+
`CONFIG_FIELD_PRESENT: config.motion.animations_from (<file>).spec — is absent and required:
|
|
1370
|
+
write "spec": "rigc-motion/1" beside "animations", …`, and another value is `CONFIG_FIELD_TYPE`
|
|
1371
|
+
naming the value found and `"rigc-motion/1"`.
|
|
1372
|
+
- **Key times strictly increase within a track, and the last is at `duration`** — rig-c's rule
|
|
1373
|
+
(its AUTHORING §4.5, "`t` is in seconds and must strictly increase"; R7: no key past the
|
|
1374
|
+
declared duration, and a duration more than a frame longer than the last key is refused).
|
|
1375
|
+
`loop` is a player hint rigc does not emit (its §4.3).
|
|
1376
|
+
- **The cycle-boundary trap.** A looping animation's boundary is one key, at `t = duration`; to
|
|
1377
|
+
close the loop without a jump give it the value of the key at `t = 0`, as both tracks above do.
|
|
1378
|
+
Writing that boundary key twice (`t: 2` and `t: 2` here) is a duplicate time, and it is rigc's
|
|
1379
|
+
verdict at the rig stage's gate, not this loader's: `RIG_RIGC_GREEN` quotes rigc's
|
|
1380
|
+
`CE_COMPILE_KEY_TIMES_NOT_INCREASING` line, `animation "<name>" bone "<bone>" <property>: key
|
|
1381
|
+
times must strictly increase (at t=<t>)`. That line names the animation, the track and the time,
|
|
1382
|
+
and the file it came from is the one the rig stage's line just above it names
|
|
1383
|
+
(`animations beside the idle, from <file> as written: …`) — rigc reads the merged `motion.json`
|
|
1384
|
+
in a scratch directory, so its own path is not yours. Remove the second key.
|
|
1385
|
+
- **The path resolves beside the config.** A file that is not there is `CONFIG_FILE_PRESENT:
|
|
1386
|
+
config.motion.animations_from — names "<as typed>", resolved against the config's directory
|
|
1387
|
+
<dir> (not the working directory) to <file>, which is no such file; …`: move the file there, or
|
|
1388
|
+
write the path from that directory.
|
|
1389
|
+
|
|
1345
1390
|
**Which way a key moves a bone.** A `rotate` value is degrees added to the bone's setup
|
|
1346
1391
|
rotation, counter-clockwise positive in the rig's y-up space — rig-c's convention (its
|
|
1347
1392
|
AUTHORING §3.2, `rotation`: "degrees, counter-clockwise, y up"; its `src/transform.ts`, "World
|
|
@@ -1543,6 +1588,15 @@ fragment below, cut to the bust:
|
|
|
1543
1588
|
- **A `physics` constraint** on that bone in `constraints`, in rig-c's shape; every field but
|
|
1544
1589
|
the bone is rigc's to accept.
|
|
1545
1590
|
|
|
1591
|
+
A region adds vertices: its `spacing` is finer than the mesh's, and every one of them is a
|
|
1592
|
+
triangle more for the runtime to skin. Before the first build, count them:
|
|
1593
|
+
`rig-parts mesh-stats --config config.json --parts work/rig` (issue #566; `work` is step 7's `assemble --out`, §4) runs the rig stage's own
|
|
1594
|
+
mesh construction over the assembled parts, without rigc, without `check`, and prints one line
|
|
1595
|
+
per mesh — `mesh-stats: topwear — contour 320 vertices, 568 triangles` on `reference-sample` —
|
|
1596
|
+
and the total, `mesh-stats: total 1908 triangles over 6 mesh(es)`, the counts the build's
|
|
1597
|
+
`mesh_report.json` will hold (§4, *Counting the triangles before a build*). On `reference-sample`
|
|
1598
|
+
it runs in under a second; the build it saves runs the render and `check` as well.
|
|
1599
|
+
|
|
1546
1600
|
`build` on `reference-sample` as tracked (PR #187; the values above are the ones tuned for its `hop`, issue #183):
|
|
1547
1601
|
|
|
1548
1602
|
```
|
|
@@ -2187,6 +2241,31 @@ The selftest runs this order, with the README's flags, on each fetched example f
|
|
|
2187
2241
|
a config holding only `key`, `seethrough` and `assemble.rig_scale`, pasting each
|
|
2188
2242
|
proposal in as the steps above say (`RL01`); every step must exit 0.
|
|
2189
2243
|
|
|
2244
|
+
### Counting the triangles before a build (`mesh-stats`, issue #566)
|
|
2245
|
+
|
|
2246
|
+
`rig-parts mesh-stats --config config.json --parts work/rig` prints the vertices and triangles of
|
|
2247
|
+
every mesh the config states and their total, against the parts `assemble` wrote (step 7's
|
|
2248
|
+
`work/rig`, or a `build --out` directory, which holds `parts.json` and `parts/` itself), in seconds.
|
|
2249
|
+
It is the rig stage's mesh construction itself — the same function the rig stage writes
|
|
2250
|
+
`mesh_report.json` from — run without rigc, without `check` and without writing a file, so a
|
|
2251
|
+
config can be tuned to a triangle budget before the first build and between builds:
|
|
2252
|
+
|
|
2253
|
+
```
|
|
2254
|
+
mesh-stats: hair_back — lattice 155 vertices, 226 triangles
|
|
2255
|
+
…
|
|
2256
|
+
mesh-stats: total 1816 triangles over 9 mesh(es)
|
|
2257
|
+
```
|
|
2258
|
+
|
|
2259
|
+
(`demo` as tracked, nine lattices.) The package holds no triangle budget: a budget is the
|
|
2260
|
+
consumer's, and the command prints no line against one. One line per mesh, in parts.json's
|
|
2261
|
+
order, the row order of `mesh_report.json`; the mode is `lattice` for a `grid` entry,
|
|
2262
|
+
`contour`, `auto` or `influence`. An `auto` part's count is its reduction's result: when rigc's
|
|
2263
|
+
motion gate refuses that result, the rig stage replays an earlier accepted step, which needs
|
|
2264
|
+
rigc, and the line says so. A problem the rig stage would refuse — a part with no PNG, an
|
|
2265
|
+
outline that does not close, a name `bones` does not declare — is the rig stage's own refusal,
|
|
2266
|
+
by the same name, exit 1; it takes no `--requirements`, because the rig stage's meshes read
|
|
2267
|
+
none, and no `--idle-keys`, because where the idle's keys go moves no vertex.
|
|
2268
|
+
|
|
2190
2269
|
### For artists: your own layered file (issue #359)
|
|
2191
2270
|
|
|
2192
2271
|
An artist's layered file is an input in its own right: you paint the figure in
|
|
@@ -2579,6 +2658,11 @@ pass, which the records are counted after):
|
|
|
2579
2658
|
|
|
2580
2659
|
cut: "topwear" opaque 134452 = "topwear" 118973 + "scarf" 15479; band 593 px held by both
|
|
2581
2660
|
|
|
2661
|
+
A part a limb takes may be cut too — the shoe's top painted into `legwear`, cut below the
|
|
2662
|
+
ankle so the shin does not hold it — where no segment of the limb holds the pixels; the `cut:`
|
|
2663
|
+
line then names the joint the piece lies past and the bone of the segment there (§5, *Limbs*,
|
|
2664
|
+
*A cut from a limb's layer*, issue #564).
|
|
2665
|
+
|
|
2582
2666
|
Give the piece a `meshes` entry over the bones that should carry it, and take those
|
|
2583
2667
|
bones out of the part it left: on the public `scarf`, the scarf end `robe_r` runs down,
|
|
2584
2668
|
cut out of `topwear` into its own mesh on `robe_r` (and `robe_r` dropped from `topwear`'s
|
|
@@ -2806,8 +2890,8 @@ See-through with another seed.
|
|
|
2806
2890
|
|
|
2807
2891
|
| rule | means | change |
|
|
2808
2892
|
| --- | --- | --- |
|
|
2809
|
-
| <a id="CONFIG_FILE_PRESENT"></a><a id="CONFIG_IS_JSON"></a>`CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON` | no such file, or not JSON | `--config
|
|
2810
|
-
| <a id="CONFIG_KEY_KNOWN"></a><a id="CONFIG_KEY_RETIRED"></a><a id="CONFIG_FIELD_PRESENT"></a><a id="CONFIG_FIELD_TYPE"></a>`CONFIG_KEY_KNOWN`, `CONFIG_KEY_RETIRED`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE` | an unknown key (a retired one says what replaces it), a missing one, a wrong type (`bones[i].link_inherit` must be an object whose values are strings); a project's own record under a plain key, or an annotation (`note`, `<name>_note`) holding something other than a string; in `meshes.<part>.auto` (§3, *An automatic mesh*), a missing or out-of-range number, a Stage B opt-in that is not exactly a value rig-c accepts, or a field the mode does not have — a bound on one of the five allocation rows, or an authored `motionAmplitude` (it is derived, never written); under `comfy paint`, `CONFIG_FIELD_PRESENT` on `config.generation`: a config with no `generation` block (the line names the fields it holds) | the named field; a record goes under a key beginning `x-` (any JSON, read by nothing), a remark under `note` or `<name>_note` (a string) — §3; under `comfy paint`, `config.generation` |
|
|
2893
|
+
| <a id="CONFIG_FILE_PRESENT"></a><a id="CONFIG_IS_JSON"></a>`CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON` | no such file, or not JSON; for a file the config names (`motion.animations_from`, `requirements`), the path is read relative to the config's directory, and the `motion.animations_from` line names the path as typed, that directory and what the two resolved to (issue #569) | `--config`; a file the config names: put it at the resolved path, or write the path from the config's directory |
|
|
2894
|
+
| <a id="CONFIG_KEY_KNOWN"></a><a id="CONFIG_KEY_RETIRED"></a><a id="CONFIG_FIELD_PRESENT"></a><a id="CONFIG_FIELD_TYPE"></a>`CONFIG_KEY_KNOWN`, `CONFIG_KEY_RETIRED`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE` | an unknown key (a retired one says what replaces it), a missing one (a `motion.animations_from` file with no `spec` is told to write `"spec": "rigc-motion/1"` and the whole file's shape, issue #569), a wrong type (`bones[i].link_inherit` must be an object whose values are strings); a project's own record under a plain key, or an annotation (`note`, `<name>_note`) holding something other than a string; in `meshes.<part>.auto` (§3, *An automatic mesh*), a missing or out-of-range number, a Stage B opt-in that is not exactly a value rig-c accepts, or a field the mode does not have — a bound on one of the five allocation rows, or an authored `motionAmplitude` (it is derived, never written); under `comfy paint`, `CONFIG_FIELD_PRESENT` on `config.generation`: a config with no `generation` block (the line names the fields it holds) | the named field; a record goes under a key beginning `x-` (any JSON, read by nothing), a remark under `note` or `<name>_note` (a string) — §3; under `comfy paint`, `config.generation` |
|
|
2811
2895
|
| <a id="CONFIG_KEY_UNIQUE"></a>`CONFIG_KEY_UNIQUE` | a key written twice in one object of a file the config names — the pose file `generation.control.skeleton` names, or the `motion.animations_from` file; JSON keeps only the last value, so every other would be dropped without a word | that file: each key once |
|
|
2812
2896
|
| <a id="CONFIG_ANIMATION_NAME_FREE"></a>`CONFIG_ANIMATION_NAME_FREE` | the `motion.animations_from` file holds an animation named `idle`, the animation `config.motion` writes | rename that animation |
|
|
2813
2897
|
| <a id="CONFIG_HEAD_BOX_SQUARE"></a>`CONFIG_HEAD_BOX_SQUARE` | `seethrough.head_box` is not a non-empty square | `head_box` — take `propose --head-box`'s |
|
|
@@ -2912,6 +2996,7 @@ says why each requirement exists and what breaks without it.
|
|
|
2912
2996
|
| <a id="ASSEMBLE_CUT_INSIDE"></a>`ASSEMBLE_CUT_INSIDE` | a cut's polygon has a point past the rig (the detail gives the points and the size) | that cut's `polygon` — it is in rig pixels, not painting pixels |
|
|
2913
2997
|
| <a id="ASSEMBLE_CUT_OVERLAP"></a>`ASSEMBLE_CUT_OVERLAP` | two cuts of one part share a rig pixel (the detail gives the count and the first, `x,y`) | one of the two polygons: a pixel goes to one part |
|
|
2914
2998
|
| <a id="ASSEMBLE_CUT_PIXELS"></a>`ASSEMBLE_CUT_PIXELS` | a cut takes none of its part's opaque pixels (alpha above 8), or a part's cuts leave it none | draw the polygon over the art; to move the whole layer, rename the plan part instead |
|
|
2999
|
+
| <a id="ASSEMBLE_CUT_CROSSES_LIMB"></a>`ASSEMBLE_CUT_CROSSES_LIMB` | a cut whose `from` a limb takes takes a pixel inside the limb: between the joint above its segments that take the part and the joint below them, or in the upper joint's cap disc (the detail gives the limb, the count, the first `x,y`, the segment's band or the cap, and the line it must lie past) | the polygon: past the lower joint's line or before the upper's and outside its cap — or leave the pixels to the limb's segments (§5, *Limbs*) |
|
|
2915
3000
|
| <a id="ASSEMBLE_UNDER_PRESENT"></a><a id="ASSEMBLE_UNDER_DECODES"></a>`ASSEMBLE_UNDER_PRESENT`, `ASSEMBLE_UNDER_DECODES` | a cut's `under` names no file (relative to the config's directory), or a file that is not a PNG; every cut's is named in one refusal | that cut's `under` |
|
|
2916
3001
|
| <a id="ASSEMBLE_UNDER_SIZE"></a>`ASSEMBLE_UNDER_SIZE` | a cut's `under` image is not the size of its polygon's bounding box (the detail gives both, and the box's corner) | the image: `max x − min x` by `max y − min y` of the polygon, in rig pixels |
|
|
2917
3002
|
| <a id="ASSEMBLE_UNDER_EMPTY"></a>`ASSEMBLE_UNDER_EMPTY` | a cut's `under` image fills none of the pixels the cut removed: its alpha is below the alpha removed at every one (all transparent there, or another file) | the image or its path; to leave the whole hole, drop `under` |
|
package/docs/PAINTING.md
CHANGED
|
@@ -59,6 +59,27 @@ nothing. It prints the figure, every joint and every gap with its count, then
|
|
|
59
59
|
with `(see rig-parts guide <RULE>)`, which prints the rule's row in
|
|
60
60
|
[AUTHORING §6](AUTHORING.md#6-refusals-the-rule-and-what-has-to-change).
|
|
61
61
|
|
|
62
|
+
**DWPose on a faceless figure** (issue #578). Measured on eight seeds of one
|
|
63
|
+
frontal, faceless wooden-mannequin painting (SDXL base 1.0, legs apart,
|
|
64
|
+
1664x2432), DWPose `dw-ll_ucoco_384` through ComfyUI's DWPreprocessor at
|
|
65
|
+
resolution 1024, it did three things a keypoint file must not keep:
|
|
66
|
+
|
|
67
|
+
- **It may find no person.** With its default bbox detector (`yolox_l.onnx`) it
|
|
68
|
+
returned no person on 8 of the 8 seeds; with `bbox_detector` None it returned
|
|
69
|
+
one on 8 of 8. Run it with `bbox_detector` None.
|
|
70
|
+
- **It may read the figure from behind.** With `bbox_detector` None the sides came
|
|
71
|
+
back mirrored on 7 of the 8: `r_shoulder` image-right of `l_shoulder` (on one
|
|
72
|
+
seed x 978 against 697) and `r_hip` image-right of `l_hip` (907 against 753).
|
|
73
|
+
The keypoint file's `r` / `l` are the subject's sides, so on a figure facing the
|
|
74
|
+
viewer `r_shoulder` is on the image's left (`src/keypoints.ts`, AUTHORING §3).
|
|
75
|
+
Check that `r_shoulder` lies image-left of `l_shoulder` and `r_hip` image-left of
|
|
76
|
+
`l_hip`, and swap each `r_` / `l_` pair of a file where they do not.
|
|
77
|
+
- **Its hips may sit on the pelvis.** On that seed, after the swap, the hips were
|
|
78
|
+
at painting y 1117.0 and 1113.1 and the hip domes at y 1246 — 129 and 133
|
|
79
|
+
painting px lower (64.5 and 66.5 rig px at an `assemble.rig_scale` of 0.5). The shoulders
|
|
80
|
+
landed on the torso's edge (x 697.4 and 978.5) rather than on the balls (652 and
|
|
81
|
+
1010). Move the hips and the shoulders onto the joints before `propose`.
|
|
82
|
+
|
|
62
83
|
What it reads, each a definition with no tuned number:
|
|
63
84
|
|
|
64
85
|
- **Ground** — a pixel the package already calls background: alpha 8 or less
|
|
@@ -100,8 +121,12 @@ joints line counts occluded hips apart (`joints: 16 observed, 2 occluded with a
|
|
|
100
121
|
position (r_hip, l_hip), 18 on the figure`), and an arm's gap line says when it was
|
|
101
122
|
read from one.
|
|
102
123
|
|
|
103
|
-
What it does not read: a front view
|
|
104
|
-
|
|
124
|
+
What it does not read: a front view (a file with every `r_` / `l_` pair swapped
|
|
125
|
+
draws the same lines between the same points — the two arm lines trade names, the
|
|
126
|
+
hands, knees and ankles lines run between the same two joints — so it reads as the
|
|
127
|
+
unswapped file does: on the selftest's painting fixture both print the same counts
|
|
128
|
+
and `PASS`), the feet, a prop that does not join the hands, anything on an elbow (a
|
|
129
|
+
cape, a sleeve's frill, a bow), the hands' fingers,
|
|
105
130
|
the face. A green `painting` says
|
|
106
131
|
the joints and the silhouette hold the requirements it reads, not that the
|
|
107
132
|
painting is good. A shadow or a floor enclosed by the background is figure to it;
|
package/docs/STABILITY.md
CHANGED
|
@@ -61,6 +61,7 @@ defaults are the help's too, and none is guessed from the art.
|
|
|
61
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), `--environment` (as for `check`, issue #538, additive) |
|
|
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
|
+
| `mesh-stats` | `--config`, `--parts` (issue #566, additive: prints, writes nothing) |
|
|
64
65
|
| `remesh` | `--rig`, `--config`, `--parts`, `--part`, `--out`, and one of `--mesh` or `--delta` (issue #526); optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
|
|
65
66
|
| `retune` | `--rig`, `--config`, `--parts`, `--out`; optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
|
|
66
67
|
| `painting` | one positional path, the painting; `--keypoints`; optional `--person` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/assemble.ts
CHANGED
|
@@ -2571,6 +2571,8 @@ export interface CutCount {
|
|
|
2571
2571
|
underEmpty?: number;
|
|
2572
2572
|
/** With `under_fill: "colour"` only (issue #360): the `under` pixels took the image's colour at the alpha the cut removed; absent without it. */
|
|
2573
2573
|
underFill?: 'colour';
|
|
2574
|
+
/** Issue #564, only on a cut from a part a limb takes, lying past the limb: the joint it lies past and the segment below it, with its bone. */
|
|
2575
|
+
limb?: { limb: string; joint: string; part: string; bone: string };
|
|
2574
2576
|
}
|
|
2575
2577
|
|
|
2576
2578
|
/**
|
|
@@ -2588,7 +2590,8 @@ export function cutLines(cuts: readonly CutCount[]): string[] {
|
|
|
2588
2590
|
const colour = cs.some((c) => c.underFill === 'colour') ? ` (${cs.reduce((a, c) => a + (c.underFill === 'colour' ? (c.under ?? 0) : 0), 0)} colour only, at the alpha the cut removed)` : '';
|
|
2589
2591
|
return (
|
|
2590
2592
|
` cut: "${from}" opaque ${cs[0].before} = "${from}" ${cs[0].kept} + ${cs.map((c) => `"${c.into}" ${c.taken}`).join(' + ')}; band ${cs.reduce((a, c) => a + c.band, 0)} px held by both` +
|
|
2591
|
-
(cs.some((c) => c.under !== undefined) ? `; under ${cs.reduce((a, c) => a + (c.under ?? 0), 0)} px filled${colour}, ${cs.reduce((a, c) => a + (c.underEmpty ?? 0), 0)} px left empty` : '')
|
|
2593
|
+
(cs.some((c) => c.under !== undefined) ? `; under ${cs.reduce((a, c) => a + (c.under ?? 0), 0)} px filled${colour}, ${cs.reduce((a, c) => a + (c.underEmpty ?? 0), 0)} px left empty` : '') +
|
|
2594
|
+
cs.map((c) => (c.limb === undefined ? '' : `; "${c.into}" lies past joint "${c.limb.joint}" of limb "${c.limb.limb}", in the band of "${c.limb.part}" on bone "${c.limb.bone}"`)).join('')
|
|
2592
2595
|
);
|
|
2593
2596
|
});
|
|
2594
2597
|
}
|
|
@@ -2838,6 +2841,108 @@ function shiftedAt(line: JointLine, offset: number): [number, number] {
|
|
|
2838
2841
|
return [pyRound(line.at[0] + offset * line.axis[0], 3), pyRound(line.at[1] + offset * line.axis[1], 3)];
|
|
2839
2842
|
}
|
|
2840
2843
|
|
|
2844
|
+
/**
|
|
2845
|
+
* Issue #564: where an `assemble.cuts` entry from a part a limb takes lies
|
|
2846
|
+
* against that limb. The segments of a limb that take the part are a run
|
|
2847
|
+
* `k0 .. k1` (`CONFIG_LIMB_FROM_RUN` keeps them consecutive), and the run's
|
|
2848
|
+
* pixels lie between two joint lines: the line of the joint above `k0` when
|
|
2849
|
+
* `k0` sits below one (a joint between two layers), and the line of the joint
|
|
2850
|
+
* below `k1` when a segment follows it (the ankle under a shin that takes the
|
|
2851
|
+
* legwear). A pixel the cut takes (alpha above 0 in its piece) is outside the
|
|
2852
|
+
* run when it lies past the lower line, or not past the upper line and — read
|
|
2853
|
+
* after the limbs are cut, with `caps` — outside the disc of that joint's cap,
|
|
2854
|
+
* which the run's own layer fills there first. Anything else lies in a
|
|
2855
|
+
* segment's band, and the cut is refused: the pixels would leave a segment
|
|
2856
|
+
* that holds them, and the bend would open where the layer was cut. Every
|
|
2857
|
+
* limb taking the part is read. `past` is the segment a taken pixel lies in
|
|
2858
|
+
* when every one lies past the lower line — the one below that joint, or
|
|
2859
|
+
* below the deepest joint line all of them are past — and is what the `cut:`
|
|
2860
|
+
* line names. Without `caps` the lines alone are read; with them only the
|
|
2861
|
+
* discs, for the pixels the lines let through.
|
|
2862
|
+
*/
|
|
2863
|
+
function limbBands(
|
|
2864
|
+
cuts: readonly Cut[],
|
|
2865
|
+
pieces: ReadonlyArray<Built | null>,
|
|
2866
|
+
limbs: Readonly<Record<string, Limb>>,
|
|
2867
|
+
bones: readonly BoneEntry[],
|
|
2868
|
+
W: number,
|
|
2869
|
+
H: number,
|
|
2870
|
+
caps: ((limb: string, segment: number) => CapRecord | undefined) | null,
|
|
2871
|
+
): { problems: Problem[]; past: Map<number, { limb: string; joint: string; part: string; bone: string }> } {
|
|
2872
|
+
const problems: Problem[] = [];
|
|
2873
|
+
const past = new Map<number, { limb: string; joint: string; part: string; bone: string }>();
|
|
2874
|
+
const places = bonePlaces(bones);
|
|
2875
|
+
const lineAbove = (l: string, k: number): JointLine | null => {
|
|
2876
|
+
const segs = limbs[l].segments;
|
|
2877
|
+
const up = places.get(segs[k - 1].bone);
|
|
2878
|
+
const me = places.get(segs[k].bone);
|
|
2879
|
+
if (up === undefined || me === undefined) return null;
|
|
2880
|
+
const end = me.end ?? (k + 1 < segs.length ? (places.get(segs[k + 1].bone)?.at ?? null) : null);
|
|
2881
|
+
return jointLine(up.at, me.at, end);
|
|
2882
|
+
};
|
|
2883
|
+
const fmt = (q: readonly number[]): string => `[${q.map((n) => pyRound(n, 3)).join(', ')}]`;
|
|
2884
|
+
cuts.forEach((q, i) => {
|
|
2885
|
+
const piece = pieces[i];
|
|
2886
|
+
if (piece === null) return;
|
|
2887
|
+
const taken: number[] = [];
|
|
2888
|
+
for (let p = 0; p < W * H; p++) if (piece.r.data[p * 4 + 3] > 0) taken.push(p);
|
|
2889
|
+
for (const l of Object.keys(limbs)) {
|
|
2890
|
+
const segs = limbs[l].segments;
|
|
2891
|
+
const k0 = segs.findIndex((sg) => sg.from === q.from);
|
|
2892
|
+
if (k0 < 0) continue;
|
|
2893
|
+
let k1 = k0;
|
|
2894
|
+
while (k1 + 1 < segs.length && segs[k1 + 1].from === q.from) k1++;
|
|
2895
|
+
const upper = k0 > 0 ? lineAbove(l, k0) : null;
|
|
2896
|
+
const lower = k1 + 1 < segs.length ? lineAbove(l, k1 + 1) : null;
|
|
2897
|
+
// A joint with no line is ASSEMBLE_LIMB_JOINT, refused by cutLimbs by name; nothing is read about it here.
|
|
2898
|
+
if ((k0 > 0 && upper === null) || (k1 + 1 < segs.length && lower === null)) continue;
|
|
2899
|
+
const cap = caps === null || upper === null ? undefined : caps(l, k0);
|
|
2900
|
+
const inside: number[] = [];
|
|
2901
|
+
let below = 0;
|
|
2902
|
+
for (const p of taken) {
|
|
2903
|
+
const x = p % W;
|
|
2904
|
+
const y = Math.floor(p / W);
|
|
2905
|
+
if (lower !== null && pastJoint(lower, x, y)) {
|
|
2906
|
+
below++;
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
if (upper !== null && !pastJoint(upper, x, y)) {
|
|
2910
|
+
if (caps === null || cap === undefined || !inDisc(cap.at, cap.radius, x, y)) continue;
|
|
2911
|
+
} else if (caps !== null) continue;
|
|
2912
|
+
inside.push(p);
|
|
2913
|
+
}
|
|
2914
|
+
const object = `config.assemble.cuts[${i}] (part "${q.into}")`;
|
|
2915
|
+
const wanted = [
|
|
2916
|
+
...(lower === null ? [] : [`past joint "${String(segs[k1 + 1].joint)}"'s line (through ${fmt(lower.at)}, axis ${fmt(lower.axis)})`]),
|
|
2917
|
+
...(upper === null ? [] : [`before joint "${String(segs[k0].joint)}"'s line (through ${fmt(upper.at)}, axis ${fmt(upper.axis)})${cap === undefined ? '' : ` and outside its cap's disc, radius ${cap.radius} px about ${fmt(cap.at)}`}`]),
|
|
2918
|
+
];
|
|
2919
|
+
if (inside.length > 0) {
|
|
2920
|
+
const f = inside[0];
|
|
2921
|
+
const fx = f % W;
|
|
2922
|
+
const fy = Math.floor(f / W);
|
|
2923
|
+
let k = k0;
|
|
2924
|
+
if (caps === null) {
|
|
2925
|
+
for (let ln = k < k1 ? lineAbove(l, k + 1) : null; ln !== null && pastJoint(ln, fx, fy); ln = k < k1 ? lineAbove(l, k + 1) : null) k++;
|
|
2926
|
+
}
|
|
2927
|
+
const band =
|
|
2928
|
+
caps !== null
|
|
2929
|
+
? `the cap of joint "${String(segs[k0].joint)}" on "${segs[k0].part}" (radius ${(cap as CapRecord).radius} px about ${fmt((cap as CapRecord).at)}), which "${q.from}"'s pixels fill there`
|
|
2930
|
+
: `the band of "${segs[k].part}"${k > 0 ? ` below joint "${String(segs[k].joint)}"` : ''}${k + 1 < segs.length ? `${k > 0 ? ' and' : ''} above joint "${String(segs[k + 1].joint)}"` : ' to the limb\'s end'}`;
|
|
2931
|
+
problems.push({
|
|
2932
|
+
code: 'ASSEMBLE_CUT_CROSSES_LIMB',
|
|
2933
|
+
object,
|
|
2934
|
+
detail: `takes ${inside.length} of its ${taken.length} pixel(s) of "${q.from}" inside limb "${l}", the first at ${fx},${fy}, in ${band}; "${q.from}" is the layer config.limbs.${l} cuts at its joints, and a cut from it may lie only where no segment of the limb holds it — every pixel it takes ${wanted.length === 0 ? `outside the limb, and the limb has no joint before or past its segments that take "${q.from}" (they are all of it), so no pixel is` : wanted.join(', or ')} is required: move the polygon, or give the pixels to a segment of the limb instead`,
|
|
2935
|
+
});
|
|
2936
|
+
} else if (caps === null && lower !== null && below === taken.length && taken.length > 0) {
|
|
2937
|
+
let j = k1 + 1;
|
|
2938
|
+
for (let ln = j + 1 < segs.length ? lineAbove(l, j + 1) : null; ln !== null && taken.every((p) => pastJoint(ln as JointLine, p % W, Math.floor(p / W))); ln = j + 1 < segs.length ? lineAbove(l, j + 1) : null) j++;
|
|
2939
|
+
if (!past.has(i)) past.set(i, { limb: l, joint: String(segs[j].joint), part: segs[j].part, bone: segs[j].bone });
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
});
|
|
2943
|
+
return { problems, past };
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2841
2946
|
/**
|
|
2842
2947
|
* Cut every declared limb (module comment of `src/limbs.ts`): each part a limb
|
|
2843
2948
|
* takes is split among the limbs that take it (nearest polyline), then each
|
|
@@ -3719,6 +3824,15 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
3719
3824
|
// In cut order, as the config lists them.
|
|
3720
3825
|
cutCounts.sort((a, b) => cuts.findIndex((q) => q.into === a.into) - cuts.findIndex((q) => q.into === b.into));
|
|
3721
3826
|
|
|
3827
|
+
// Issue #564: a cut from a part a limb takes lies outside the limb's segments, read before the limbs are cut.
|
|
3828
|
+
const bands = input.limbs === undefined || cuts.length === 0 ? null : limbBands(cuts, pieces, input.limbs, input.bones ?? [], W, H, null);
|
|
3829
|
+
if (bands !== null) {
|
|
3830
|
+
refuseIfAny(bands.problems);
|
|
3831
|
+
for (const [i, at] of bands.past) {
|
|
3832
|
+
const c = cutCounts.find((q) => q.into === cuts[i].into);
|
|
3833
|
+
if (c !== undefined) c.limb = at;
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3722
3836
|
// Issue #349: the limbs, after the cuts (a limb may take a cut's piece, and a cut's under image fills its base first).
|
|
3723
3837
|
const lim = cutLimbs(input.limbs ?? {}, input.bones ?? [], input.caps ?? new Map(), W, H, (name) => {
|
|
3724
3838
|
const pi = plan.findIndex((e) => e[0] === name);
|
|
@@ -3726,6 +3840,8 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
3726
3840
|
const ci = cuts.findIndex((q) => q.into === name);
|
|
3727
3841
|
return ci >= 0 ? pieces[ci] : null;
|
|
3728
3842
|
}, opaqueOf, mergedIn, count, (name) => hiddenArt.get(name) ?? null, (layer, by) => hiddenUses.get(layer)?.push(by));
|
|
3843
|
+
// Issue #564: and the cap a run's own layer fills above its upper joint, read once the limbs are cut.
|
|
3844
|
+
if (bands !== null) refuseIfAny(limbBands(cuts, pieces, input.limbs ?? {}, input.bones ?? [], W, H, (l, k) => lim.parts.find((q) => q.limb === l && q.segment === k)?.record.cap).problems);
|
|
3729
3845
|
const limbPieces = lim.pieces;
|
|
3730
3846
|
const order = limbOrder(order0, plan, cuts, lim.parts);
|
|
3731
3847
|
const pieceAt = (d: { plan: number } | { cut: number } | { limb: number }): { name: string; b: Built } =>
|
package/src/build.ts
CHANGED
|
@@ -1146,21 +1146,39 @@ function multiIntervalRun(
|
|
|
1146
1146
|
* the gate is green — and, for every automatic mesh, only when its motion gate
|
|
1147
1147
|
* accepted it (issue #126 item 3; {@link motionGates}).
|
|
1148
1148
|
*/
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1149
|
+
/** What the rig stage reads before it builds anything: the config with its `animations_from` table, parts.json, and each part's PNG (and each alternate set's) that exists. */
|
|
1150
|
+
export interface RigInputs {
|
|
1151
|
+
cfg: ReturnType<typeof loadConfigAndAnimations>['config'];
|
|
1152
|
+
from: ReturnType<typeof loadConfigAndAnimations>['animations'];
|
|
1153
|
+
parts: ReturnType<typeof readParts>;
|
|
1154
|
+
images: Map<string, Raster>;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* The rig stage's inputs, read as {@link rigStage} reads them: `rigStage` and `mesh-stats` (issue #566,
|
|
1159
|
+
* `src/meshstats.ts`) both start here, so the two read the same config, the same parts and the same images, and a
|
|
1160
|
+
* PNG that is missing is left out of the map for `buildRig` to refuse by name (`RIG_PNG_PRESENT`).
|
|
1161
|
+
*/
|
|
1162
|
+
export function readRigInputs(config: string, partsDir: string): RigInputs {
|
|
1163
|
+
const { config: cfg, animations: from } = loadConfigAndAnimations(config);
|
|
1164
|
+
const parts = readParts(join(partsDir, 'parts.json'));
|
|
1152
1165
|
const images = new Map<string, Raster>();
|
|
1153
1166
|
for (const p of parts.parts) {
|
|
1154
|
-
const png = join(
|
|
1167
|
+
const png = join(partsDir, 'parts', `${p.name}.png`);
|
|
1155
1168
|
if (existsSync(png)) images.set(p.name, readPng(png));
|
|
1156
1169
|
}
|
|
1157
1170
|
for (const alt of parts.alternates ?? []) {
|
|
1158
1171
|
for (const p of alt.records) {
|
|
1159
1172
|
const attName = `${alt.name}_${p.name}`;
|
|
1160
|
-
const png = join(
|
|
1173
|
+
const png = join(partsDir, 'parts', `${attName}.png`);
|
|
1161
1174
|
if (existsSync(png)) images.set(attName, readPng(png));
|
|
1162
1175
|
}
|
|
1163
1176
|
}
|
|
1177
|
+
return { cfg, from, parts, images };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string, log: Log): RigOutput {
|
|
1181
|
+
const { cfg, from, parts, images } = readRigInputs(input.config, input.parts);
|
|
1164
1182
|
const first = buildRig(cfg, parts, images, undefined, input.idleKeys ?? DEFAULT_IDLE_KEYS, input.reduce);
|
|
1165
1183
|
const textsOf = (r: RigOutput): Array<[string, string]> => {
|
|
1166
1184
|
const mergedMotion = withAnimations(r.motion, from === null ? null : from.table);
|
package/src/config.ts
CHANGED
|
@@ -213,7 +213,9 @@ export interface Patch {
|
|
|
213
213
|
* `draw` places a patch. What the config alone cannot know — whether the
|
|
214
214
|
* polygon lies inside the rig, takes any of the layer's art, or overlaps
|
|
215
215
|
* another cut of the same part — the assemble stage refuses
|
|
216
|
-
* (`ASSEMBLE_CUT_INSIDE`, `ASSEMBLE_CUT_PIXELS`, `ASSEMBLE_CUT_OVERLAP`)
|
|
216
|
+
* (`ASSEMBLE_CUT_INSIDE`, `ASSEMBLE_CUT_PIXELS`, `ASSEMBLE_CUT_OVERLAP`), and,
|
|
217
|
+
* for a `from` a limb takes (issue #564), whether the polygon takes a pixel a
|
|
218
|
+
* segment of the limb holds (`ASSEMBLE_CUT_CROSSES_LIMB`).
|
|
217
219
|
*/
|
|
218
220
|
export interface Cut {
|
|
219
221
|
from: string;
|
|
@@ -1537,7 +1539,7 @@ function parseConfigFrom(raw: Json, base: string | null): { config: CharacterCon
|
|
|
1537
1539
|
if ('cloth' in t) checkCloth(c, t.cloth);
|
|
1538
1540
|
if ('requirements' in t && c.string('config.requirements', t.requirements) && base !== null) checkRequirementsFile(c, resolve(base, t.requirements));
|
|
1539
1541
|
const from = isPlainObject(t.motion) ? t.motion.animations_from : undefined;
|
|
1540
|
-
const animations = base !== null && typeof from === 'string' && from !== '' ? readAnimationsFrom(c,
|
|
1542
|
+
const animations = base !== null && typeof from === 'string' && from !== '' ? readAnimationsFrom(c, base, from) : null;
|
|
1541
1543
|
if (animations !== null) {
|
|
1542
1544
|
checkFaceAnimationBand(c, animations, hasAltFace, t.turn, attached);
|
|
1543
1545
|
checkAnimationSetKeys(c, animations, t.assemble, new Set<string>([...parts, ...patches, ...cuts, ...(limbs?.parts.keys() ?? [])]));
|
|
@@ -1592,13 +1594,18 @@ function checkRequirementsFile(c: Check, file: string): void {
|
|
|
1592
1594
|
* Read the file `motion.animations_from` names (issue #183) and vouch for what
|
|
1593
1595
|
* merging its animations beside the idle needs, every problem collected in `c`:
|
|
1594
1596
|
*
|
|
1595
|
-
* - `CONFIG_FILE_PRESENT`: no such file, or a file that cannot be read.
|
|
1597
|
+
* - `CONFIG_FILE_PRESENT`: no such file, or a file that cannot be read. The
|
|
1598
|
+
* line names the path as typed, the config's directory it was resolved
|
|
1599
|
+
* against and what that gave (issue #569): the path is read beside the
|
|
1600
|
+
* config, not in the working directory, and a reader who cannot see the
|
|
1601
|
+
* disk needs all three to tell which was wrong.
|
|
1596
1602
|
* - `CONFIG_IS_JSON`: the text does not parse.
|
|
1597
1603
|
* - `CONFIG_FIELD_TYPE`: the file is not an object, its `spec` is not
|
|
1598
1604
|
* "rigc-motion/1", its `animations` is not an object, or an animation in it
|
|
1599
1605
|
* is not one. `CONFIG_KEY_KNOWN` / `CONFIG_FIELD_PRESENT`: a key besides
|
|
1600
1606
|
* `spec` and `animations` (and the annotation and record doors), or one of
|
|
1601
|
-
* the two absent
|
|
1607
|
+
* the two absent; an absent `spec` is told the line to write and the whole
|
|
1608
|
+
* file's shape (issue #569), from {@link ANIMATIONS_FROM_SPEC}. rig-c's `easings`, `groups`, `setup`, `physics` and `mix`
|
|
1602
1609
|
* are refused rather than dropped: `motion.json`'s own are this package's,
|
|
1603
1610
|
* and a table read and not written would be a value lost without a word.
|
|
1604
1611
|
* - `CONFIG_ANIMATION_NAME_FREE`: an animation named `idle`, the one the
|
|
@@ -1609,18 +1616,21 @@ function checkRequirementsFile(c: Check, file: string): void {
|
|
|
1609
1616
|
*
|
|
1610
1617
|
* Returns the animations when this file added no problem, else null.
|
|
1611
1618
|
*/
|
|
1612
|
-
function readAnimationsFrom(c: Check,
|
|
1619
|
+
function readAnimationsFrom(c: Check, base: string, from: string): AnimationsFrom | null {
|
|
1613
1620
|
const field = 'config.motion.animations_from';
|
|
1614
|
-
const
|
|
1621
|
+
const file = resolve(base, from);
|
|
1622
|
+
const form = `{"spec": "${ANIMATIONS_FROM_SPEC}", "animations": {"<name>": {"duration": …, "tracks": [...]}}}`;
|
|
1623
|
+
const shape = `a file in rig-c's motion-spec shape, ${form}, its path relative to the config's directory`;
|
|
1624
|
+
const where = `names ${JSON.stringify(from)}, resolved against the config's directory ${base} (not the working directory) to ${file}`;
|
|
1615
1625
|
if (!existsSync(file) || !statSync(file).isFile()) {
|
|
1616
|
-
c.fail('CONFIG_FILE_PRESENT', field,
|
|
1626
|
+
c.fail('CONFIG_FILE_PRESENT', field, `${where}, which is no such file; ${shape} is required`);
|
|
1617
1627
|
return null;
|
|
1618
1628
|
}
|
|
1619
1629
|
let text: string;
|
|
1620
1630
|
try {
|
|
1621
1631
|
text = readFileSync(file, 'utf8');
|
|
1622
1632
|
} catch (err) {
|
|
1623
|
-
c.fail('CONFIG_FILE_PRESENT', field,
|
|
1633
|
+
c.fail('CONFIG_FILE_PRESENT', field, `${where}, which cannot be read: ${(err as Error).message}; ${shape} is required`);
|
|
1624
1634
|
return null;
|
|
1625
1635
|
}
|
|
1626
1636
|
let raw: Json;
|
|
@@ -1643,7 +1653,13 @@ function readAnimationsFrom(c: Check, file: string): AnimationsFrom | null {
|
|
|
1643
1653
|
`names "${r.key}" ${r.count === 2 ? 'twice' : `${r.count} times`}; each key once is required — JSON keeps only the last, so ${r.at === 'animations' ? 'every animation of that name but the last' : 'every value but the last'} would be dropped without a word`,
|
|
1644
1654
|
);
|
|
1645
1655
|
}
|
|
1646
|
-
|
|
1656
|
+
// Both keys are required; `c.object` is given them as optional so an absent
|
|
1657
|
+
// `spec` can be told the line to write, in the order `c.object` names them.
|
|
1658
|
+
const o = c.object(at, raw, [], ['spec', 'animations']);
|
|
1659
|
+
if (o !== null && !('spec' in o)) {
|
|
1660
|
+
c.fail('CONFIG_FIELD_PRESENT', `${at}.spec`, `is absent and required: write "spec": "${ANIMATIONS_FROM_SPEC}" beside "animations", the motion-spec version motion.json states — the whole file is ${form}`);
|
|
1661
|
+
}
|
|
1662
|
+
if (o !== null && !('animations' in o)) c.fail('CONFIG_FIELD_PRESENT', `${at}.animations`, 'is absent and required');
|
|
1647
1663
|
if (o !== null && 'spec' in o && o.spec !== ANIMATIONS_FROM_SPEC) {
|
|
1648
1664
|
c.fail('CONFIG_FIELD_TYPE', `${at}.spec`, `is ${show(o.spec)}; "${ANIMATIONS_FROM_SPEC}" is required, the motion-spec version motion.json states`);
|
|
1649
1665
|
}
|
package/src/meshstats.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mesh-stats` (issue #566): the vertices and triangles of every mesh a config states, against an assembled parts
|
|
3
|
+
* directory, before a build.
|
|
4
|
+
*
|
|
5
|
+
* It is the rig stage without rigc: the same inputs ({@link readRigInputs}) through the same `buildRig`, whose
|
|
6
|
+
* `meshReport` rows are what `rig/mesh_report.json` is written from. Nothing triangulates a second time, so a count
|
|
7
|
+
* printed here is the count the rig stage writes for the same config and parts — with one exception, which the line
|
|
8
|
+
* says: an `auto` part whose motion gate refuses the full reduction is replayed by the rig stage to an earlier
|
|
9
|
+
* accepted step (`rigStage`, `src/autoreplay.ts`), and that search runs rigc, so it does not run here. A problem
|
|
10
|
+
* `buildRig` refuses is the build's own refusal, by the same name. Nothing is written: no skeleton data leaves memory,
|
|
11
|
+
* no image, no report; rigc's gate is the gate of the commands that emit, and this one emits nothing.
|
|
12
|
+
*/
|
|
13
|
+
import { readRigInputs } from './build.ts';
|
|
14
|
+
import { buildRig, DEFAULT_IDLE_KEYS, type MeshReport } from './rig.ts';
|
|
15
|
+
|
|
16
|
+
/** One mesh's counts, as the rig stage's `mesh_report.json` row holds them; `mode` is `lattice` for a `grid` entry, whose row carries no mode. */
|
|
17
|
+
export interface MeshStatsRow {
|
|
18
|
+
part: string;
|
|
19
|
+
mode: 'lattice' | 'contour' | 'auto' | 'influence';
|
|
20
|
+
vertices: number;
|
|
21
|
+
triangles: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The counts of each row of a mesh report, in its order (parts.json's, the order the rig stage writes). */
|
|
25
|
+
export function meshStatsRows(report: readonly MeshReport[]): MeshStatsRow[] {
|
|
26
|
+
return report.map((m) => ({ part: m.part, mode: 'mode' in m ? m.mode : 'lattice', vertices: m.vertices, triangles: m.triangles }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What `auto` adds to its line: the count is the reduction's, before the motion gate that may replay it. */
|
|
30
|
+
export const AUTO_BEFORE_GATE = "the reduction's result; when rigc's motion gate refuses it, the rig stage replays an earlier accepted step, which this command does not run";
|
|
31
|
+
|
|
32
|
+
/** The printed lines: one per mesh, then the total. No budget line: the tree holds no triangle budget. */
|
|
33
|
+
export function meshStatsLines(rows: readonly MeshStatsRow[]): string[] {
|
|
34
|
+
const out = rows.map((r) => `mesh-stats: ${r.part} — ${r.mode} ${r.vertices} vertices, ${r.triangles} triangles${r.mode === 'auto' ? ` (${AUTO_BEFORE_GATE})` : ''}`);
|
|
35
|
+
const total = rows.reduce((n, r) => n + r.triangles, 0);
|
|
36
|
+
out.push(`mesh-stats: total ${total} triangles over ${rows.length} mesh(es)`);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MeshStatsInput {
|
|
41
|
+
/** The config, as `rig --config` reads it. */
|
|
42
|
+
config: string;
|
|
43
|
+
/** The directory holding parts.json and parts/<name>.png, as `rig --parts` reads it. */
|
|
44
|
+
parts: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The counts of every mesh `buildRig` makes from the config and the parts, printed through `log`. The idle's keys
|
|
49
|
+
* go where `rig` puts them by default (`ctl`): where they go moves no vertex and no triangle, since a control bone
|
|
50
|
+
* stands at its bone's origin and no mesh is weighted to it. Throws the rig stage's own `PartsError` on a refusal.
|
|
51
|
+
*/
|
|
52
|
+
export function meshStatsStage(input: MeshStatsInput, log: (line: string) => void): MeshStatsRow[] {
|
|
53
|
+
const { cfg, parts, images } = readRigInputs(input.config, input.parts);
|
|
54
|
+
const rows = meshStatsRows(buildRig(cfg, parts, images, undefined, DEFAULT_IDLE_KEYS).meshReport);
|
|
55
|
+
for (const l of meshStatsLines(rows)) log(l);
|
|
56
|
+
return rows;
|
|
57
|
+
}
|
package/src/parts.ts
CHANGED
|
@@ -42,7 +42,9 @@
|
|
|
42
42
|
* cut from, which shares its `from`. The record is in draw order, and a piece
|
|
43
43
|
* drawn `"back"` comes before its base, so order cannot say which of two parts
|
|
44
44
|
* sharing a layer is the piece; this field does. A `parts.json` written before
|
|
45
|
-
* 1.6.0 has none, and still reads.
|
|
45
|
+
* 1.6.0 has none, and still reads. A piece cut from a part a limb took
|
|
46
|
+
* (issue #564) names that part, which is not written itself: its base is the
|
|
47
|
+
* limb segments whose `limb.of` it is, with the same `from`. `limb`, written only on a part a
|
|
46
48
|
* `config.limbs` segment made (issue #349), names the limb, the segment's
|
|
47
49
|
* index (0 the upper), the part it was cut from (`of`), the one bone it rides
|
|
48
50
|
* and, below a joint, the joint, its declared `range` when there is one
|
|
@@ -563,6 +565,14 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
563
565
|
// single plan part behind it; the config loader refuses one).
|
|
564
566
|
const recs = o.parts.filter((p): p is Record<string, unknown> => typeof p === 'object' && p !== null && !Array.isArray(p));
|
|
565
567
|
const byName = new Map<unknown, Record<string, unknown>>(recs.map((r) => [r.name, r]));
|
|
568
|
+
// Issue #564: a plan part a limb took is not written itself; its segments are, each naming it (`limb.of`) with its layer.
|
|
569
|
+
const limbOf = new Map<string, Record<string, unknown>>();
|
|
570
|
+
for (const r of recs) {
|
|
571
|
+
const l = r[LIMB_KEY];
|
|
572
|
+
if (typeof l !== 'object' || l === null || Array.isArray(l)) continue;
|
|
573
|
+
const of = (l as Record<string, unknown>).of;
|
|
574
|
+
if (typeof of === 'string' && !limbOf.has(of)) limbOf.set(of, r);
|
|
575
|
+
}
|
|
566
576
|
for (const r of recs) {
|
|
567
577
|
if (!(CUT_FROM_KEY in r)) continue;
|
|
568
578
|
const object = `part "${String(r.name)}" field "${CUT_FROM_KEY}"`;
|
|
@@ -572,7 +582,10 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
572
582
|
continue;
|
|
573
583
|
}
|
|
574
584
|
const b = byName.get(base);
|
|
575
|
-
|
|
585
|
+
const seg = b === undefined ? limbOf.get(base) : undefined;
|
|
586
|
+
if (seg !== undefined) {
|
|
587
|
+
if (seg.from !== r.from) fail('PARTS_CUT_FROM_KNOWN', object, `names "${base}", which limb segment "${String(seg.name)}" was cut from, with from ${show(seg.from)}; this piece's is ${show(r.from)}, and a piece shares its base's layer`);
|
|
588
|
+
} else if (b === undefined) fail('PARTS_CUT_FROM_KNOWN', object, `names "${base}", which is no part of this file and no limb segment's "of"; the plan part the cut took this piece from is required`);
|
|
576
589
|
else if (base === r.name) fail('PARTS_CUT_FROM_KNOWN', object, `names the piece itself; the plan part the cut took it from is required`);
|
|
577
590
|
else if (CUT_FROM_KEY in b) fail('PARTS_CUT_FROM_KNOWN', object, `names "${base}", which is itself a piece (cut from ${show(b[CUT_FROM_KEY])}); a plan part is required, as a cut of a cut has no single layer behind it`);
|
|
578
591
|
else if (b.from !== r.from) fail('PARTS_CUT_FROM_KNOWN', object, `names "${base}", whose from is ${show(b.from)}; this piece's is ${show(r.from)}, and a piece shares its base's layer`);
|