@rig-c/parts 1.11.0 → 1.12.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/README.md +7 -7
- package/docs/AUTHORING.md +88 -25
- package/docs/STABILITY.md +2 -2
- package/package.json +2 -2
- package/src/assemble.ts +85 -2
- package/src/build.ts +5 -1
- package/src/config.ts +22 -1
- package/src/heat.ts +42 -6
- package/src/mesh.ts +61 -0
- package/src/meshdraft.ts +7 -7
- package/src/rig.ts +19 -5
package/README.md
CHANGED
|
@@ -41,13 +41,13 @@ from the full run, because the twin tails leave the head crop sideways (a plan e
|
|
|
41
41
|
<code>loop: idle 41 frame(s) at 12 fps, f0000 vs f0040 (t = 3.333333333333333s): max |d| 0 (0 required)</code><br/>
|
|
42
42
|
<code>seam: setup pose at 654x1216, scale 0.9438: mean |d| 0.147 (<= 1.0), 0 px over 40, 0.00 rig px (the count / s²; <= 50), 0 px over 80 (reported)</code><br/>
|
|
43
43
|
<code>pack: skeleton.png 1051x972, 23 region(s), 96.9% covered, padding 2, page edges free, shape polygon</code><br/>
|
|
44
|
-
The loop is that build's indexed APNG, 40 frames at 12 fps,
|
|
45
|
-
256-entry palette at a measured error of max
|
|
46
|
-
frame (the lossless APNG beside it, the exactness record, is 4,
|
|
47
|
-
GIF, at the same error, 760,
|
|
44
|
+
The loop is that build's indexed APNG, 40 frames at 12 fps, 703,909 bytes, one
|
|
45
|
+
256-entry palette at a measured error of max 71, mean 0.636 per channel over every
|
|
46
|
+
frame (the lossless APNG beside it, the exactness record, is 4,449,233 bytes; the
|
|
47
|
+
GIF, at the same error, 760,058). The rig's hair, skirt, sleeves, bows and earrings
|
|
48
48
|
carry physics, which rigc's render starts from rest, so the idle is played three
|
|
49
49
|
periods ahead and the loop is the fourth: the frame one period after its first differs
|
|
50
|
-
from it by max
|
|
50
|
+
from it by max 1 level, 0 px over 40 (<code>--lead-in</code>). Frame 29 (2.417 s) is the
|
|
51
51
|
closed eye: the blink holds for 0.084 s, one 12 fps frame rounded up; the painting is
|
|
52
52
|
shown at half size, resampled and written by this package's PNG codec (nothing here
|
|
53
53
|
encodes JPEG). <code>rig-parts sheet</code> made the contact sheet. Its
|
|
@@ -278,7 +278,7 @@ agent skill.
|
|
|
278
278
|
| `comfy seethrough --image --out [--host]` | optional: run the ComfyUI See-through wrapper on one image and write the form `layers` reads |
|
|
279
279
|
| `layers <dir \| layers.json \| file.psd>` | print every layer of a decomposition: draw order, name, tag group, box, size, opaque pixels, depth, and its translucent, background and area figures; a `WARN` line for a layer `--propose-plan` will leave out |
|
|
280
280
|
| `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
|
|
281
|
-
| `assemble --propose-plan …` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule,
|
|
281
|
+
| `assemble --propose-plan …` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule, naming in a note any full-run body no planned part covers, and naming every layer no row takes with why (empty, taken from the other run only, a plausibility rule; `nose`, never planned, with the row that lets `propose` read the head's pitch) |
|
|
282
282
|
| `assemble --source --full --head --config --out [--seam] [--project]` | merge the two runs into rig-space parts, `parts.json`, the recomposite and its error map, and list the uncovered holes |
|
|
283
283
|
| `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
|
|
284
284
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay; print, per bone, which check read it or that none did and why (its role, its kind of segment), and write `basis.json` beside the proposal — what each bone rests on (a joint, a part's mask, a ratio, other bones) and, with `--compare`, whether the config's bone differs |
|
|
@@ -355,7 +355,7 @@ for each — a config fragment, the `check` lines it passes with, and its limit:
|
|
|
355
355
|
and a `physics` constraint on it; `examples/reference-sample` carries one. Under its slow
|
|
356
356
|
4 s idle the bust moves at most 0.066 rig px and the hips not at all: the spring answers
|
|
357
357
|
faster keys on the body. Its `hop`, a named animation keying only `hip`, moves the
|
|
358
|
-
bust up to 6.
|
|
358
|
+
bust up to 6.702 and the hips up to 4.609 rig px; the region's `band` caps how far a patch may
|
|
359
359
|
go before the texture-stretch ceiling, and `check` measures the idle alone.
|
|
360
360
|
|
|
361
361
|
### How much of a rig the model painted
|
package/docs/AUTHORING.md
CHANGED
|
@@ -6,12 +6,17 @@ the files it writes. Every refusal the tool prints names a rule, an object, the
|
|
|
6
6
|
value found and the value required — §6 maps each rule to the field or input that
|
|
7
7
|
has to change.
|
|
8
8
|
|
|
9
|
-
The worked example throughout is [`examples/reference-sample`](
|
|
9
|
+
The worked example throughout is [`examples/reference-sample`](https://github.com/rig-c/parts/tree/main/examples/reference-sample): its
|
|
10
10
|
`config.json` is a complete, loading config, `proposal.json` is what the proposer
|
|
11
11
|
wrote for it, and `expected/` is what the reference implementation produced from
|
|
12
12
|
the same inputs — except `check.json`, which this port's `build` regenerates since
|
|
13
13
|
the reference has no judgement lines (§7). `bun run fetch-examples` puts its painting and See-through layers
|
|
14
|
-
into `examples/reference-sample/inputs
|
|
14
|
+
into `examples/reference-sample/inputs/` in a clone of the repository.
|
|
15
|
+
|
|
16
|
+
A path this page names that the installed package does not hold — anything under
|
|
17
|
+
`examples/`, `docs/evidence/`, `tools/`, `fixtures/` or `scripts/`, and the scripts
|
|
18
|
+
`bun run` names — is in the repository, https://github.com/rig-c/parts; the package holds the files
|
|
19
|
+
`package.json` lists under `files` (`npm pack --dry-run` prints them).
|
|
15
20
|
|
|
16
21
|
The examples' names: `reference-sample` and `reference-demo` are the two characters
|
|
17
22
|
the reference implementation ran (published as `sample` and `demo` until issue #227),
|
|
@@ -92,10 +97,11 @@ read by no CPU stage).
|
|
|
92
97
|
| `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 |
|
|
93
98
|
| `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` 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 |
|
|
94
99
|
| `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip}` 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 |
|
|
95
|
-
| `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) 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 |
|
|
100
|
+
| `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 |
|
|
96
101
|
| `meshes.<part>.contour` | rig | **authored**, optional (issue #84): `{tolerance, margin, spacing, maxEdge?, budget?, stray?, regions?}`. `tolerance` — rig-c's Douglas–Peucker tolerance on the traced outline, px, 0 or more; `margin` — how far the silhouette grows before it is traced, px: 0, or 1 or more (a pixel joins when its centre lies within `margin` of an art pixel's centre; a value above 0 and below 1 adds nothing and is refused, `CONFIG_FIELD_TYPE`); `spacing` — the background interior spacing, px, above 0; `maxEdge` (issue #265) — the longest outline edge, px, above 0: every simplified edge longer than it is cut into `ceil(length / maxEdge)` equal pieces, each cut point on the 1/256 px grid, before any region or rib touches the outline (absent: nothing is cut, and no byte moves; under 2/256 px the rig stage refuses it, `CONTOUR_PARAMETER`, because two cut points could snap together); the row echoes it in `params` and counts the points it added in `contour.outlineCut`, both only when declared; `budget` — the most vertices the mesh may have (refused above it, nothing thinned); `stray` — the largest island, in art pixels, that may be left out of the mesh (absent: none is); `regions` — local deformation regions, each `{name, shape, bone, spacing, band}` with `shape` `"circle"` (`cx, cy, r`) or `"polygon"` (`points`, 3 or more): `bone` is the region's control bone (any bone `bones` declares), `spacing` the interior spacing inside the region and its band, `band` the width the bone's weight falls across. Every length and position is **rig px**, like every other in the config; the part image is the rig less `(x − 4, y − 4)` (its box less the pad), a translation, so a length is the same number in both. A region's `cx`, `cy`, `r`, `band` and `points` are multiples of 1/256 px (`CONFIG_FIELD_TYPE` otherwise); region names are unique per mesh (`CONFIG_REGION_NAME_UNIQUE`). The alpha threshold is not a field: art is alpha above 8, the lattice's and `check`'s |
|
|
97
102
|
| `meshes.<part>.ribs` | rig | **authored**, optional (issue #188), in the `contour` and `auto` modes: `{<chain>: {stations}, …}`, each key a chain this mesh's `segments` name. Each link of the chain gets a row of vertices across the part at its origin and at `stations` points evenly between its origin and its end (`stations`, a whole number, 0 or more, required; a chain of L links carries L × (stations + 1) ribs), each rib a row of vertices across the part, kept as a row of locked edges; the rule is "Ribs along a chain" under "A contour mesh". Absent places none and changes no byte. Refused: a lattice mesh with ribs (`CONFIG_RIBS_MODE` — its vertices are its grid's corners), a key that is not one of the mesh's chains (`CONFIG_NAME_RESOLVES`), an object naming no chain or a `stations` that is not a whole number from 0 (`CONFIG_FIELD_TYPE`); at the rig stage `CONTOUR_RIB_ART`, `CONTOUR_RIB_LINK`, `CONTOUR_RIB_CROSSING`, `CONTOUR_RIB_EDGE` (the contour refusals). The mesh's `mesh_report.json` row gains `ribs` — the declaration, the ribs placed and the vertices they hold — and its contour report (the source's, in `auto`) lists each rib's vertices |
|
|
98
103
|
| `meshes.<part>.influences` | rig | **authored**, optional (issue #284), in the `grid` and `contour` modes: `{maxInfluences?, floor?}`, at least one. `maxInfluences` — the most bones that pull one vertex, a whole number 1 or more, or `null`, no cap (every bone the mesh's `segments` name; `null` is the config's spelling of a bound declared absent, as in `maxOvershoot` and `weightJump`); absent is 4. `floor` — `"cut"` (the capped shares normalised, any under 0.03 dropped, the rest normalised again; what an absent `floor` means) or `"continuous"` (0.03 subtracted from every capped share, those at or below 0 dropped, the rest normalised again, so a bone fades out of a vertex instead of leaving it in one step). Absent, the mesh is weighted at 4 and the cut and every byte is the one written before the field existed; declared, it applies under either `rule`, and is echoed as `influences` in the mesh's `mesh_report.json` row. `propose` never writes it, and `mesh-draft` carries a lattice's declaration to the contour candidate it drafts. A cap that is not a whole number 1 or more or `null` (the string `"none"` included), another `floor`, or an object naming neither is `CONFIG_FIELD_TYPE`; a vertex the declared cut leaves with no bone is `RIG_INFLUENCES_KEPT`; beside `auto` it is `CONFIG_INFLUENCES_MODE` (that mode's cap and floor are `auto.influences`). What each setting trades is "The weight cut" below |
|
|
104
|
+
| `meshes.<part>.stray` | rig | **authored**, optional (issue #303), in the `grid` mode: the largest island, in art pixels, that may be left out of the lattice — the contour mode's `contour.stray`, with the same meaning, over the same islands (the 4-connected islands of the art, alpha above 8): every island of `stray` art px or fewer is cleared from the art the lattice and, under `rule: "heat"`, its silhouette are built over, except an island as large as the largest, which is never left out (two equal largest pieces both stay). Unlike the contour mode an island above the figure is not refused — the lattice bridges every island it keeps, as it always has. An island that sits in a cell the lattice keeps for other art leaves the lattice unchanged: there `stray` changes only the silhouette `rule: "heat"` reads, and gives back no triangle (demo `bottomwear`'s 2 px island, in the cell kept for the hem's tip, is that case). Absent, nothing is left out and every byte is the one written before the field existed; a figure that leaves nothing out changes no byte of `rig.json` or `motion.json`. The part's image is not touched: a pixel left out stays in it, and is drawn only where a cell kept for other art happens to cover it; `art_coverage` is over all the art, the islands left out included. Declared, the mesh's `mesh_report.json` row echoes `stray` and adds `stray_cleared` (`islands`, `pixels`, `art_pixels`, after `influences`), and the rig stage's mesh line ends `stray <n> left out <i> island(s), <p> of <a> art px`. A value that is not a whole number from 0 is `CONFIG_FIELD_TYPE`; beside `contour` or `auto` it is `CONFIG_STRAY_MODE` (those modes state it in `contour.stray` and `auto.source.stray`). `propose` and `mesh-draft` never write it |
|
|
99
105
|
| `regions.<part>` | rig | **proposed**: the bone a rigid part rides. Every plan part is exactly one of a mesh or a region (`CONFIG_PART_ATTACHED`) |
|
|
100
106
|
| `motion.duration` | rig; check (the loop is measured at this time) | proposed as 40 frames at 12 fps, `40/12` s, written `3.3333333333333335` (issue #252; the reference proposed 4 s, and every period it drafted is scaled with it so each track keeps its cycles per idle); a whole number of 1/12 s ticks, because `check` renders at 12 fps and compares frame 0 with the frame at this time — read within 10^-9 s (`TIME_SLACK`, issue #244), since a whole number of frames is often not exact in binary: rigc's 40th frame sits at `40 × (1/12)` = `3.333333333333333`. Half a frame off is `CHECK_LOOP_LAST_FRAME_AT_DURATION` |
|
|
101
107
|
| `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`). A bone property is keyed by one track (`CONFIG_BONE_PROPERTY_KEYED_ONCE`): a chain keys `rotate` on every link, the blink's `eyes` group `scaley` and its `brows` group `translatey` on every member, so a single track on any of those is a second track on it — rigc would refuse it one stage later, `animation "idle" has two tracks on eye.scaley`. The proposed breath (issues #252, #270) is `chest` `translatey` travelling 2.5 % of the torso's length peak to peak — the distance from the `neck` bone to the `hip` bone as the proposal writes them, the run the chest sits half way along, whichever rule placed them — (amplitude half of it to two decimals, `base` the amplitude, so the chest rises from the setup pose and returns); the `breath:` note prints the fraction, the length, the two bones and the pixels. Until #270 it was 2.5 % of the `topwear` part's height, and a sweater to the thighs (`scarf`, 690 px) breathed 2.9 times as deep as `demo`; on neck → hip `scarf` reads 431 px (10.78 px) and `demo` 192 px (4.80 px). Neck and hip on one point: no length to scale by, so the breath keeps the reference's amplitude 1.3 (2.6 px peak to peak) and the note says so — scale it to the figure. `chest` `scalex` and the `head` and `neck` rolls are a ratio and angles and are proposed the same on every figure. A chain link is turned along its chain (the `bones` row), so a `translatex`/`translatey` key on a bone whose parent is a turned link, or a `scale`/`shear` key on a turned link, would move or stretch along the link and not along the picture's axis: the rig stage refuses it (`RIG_KEY_FRAME_UNTURNED`); `rotate` is keyed on any bone |
|
|
@@ -396,7 +402,7 @@ The default does not move; a fold is today found by looking at a build that pass
|
|
|
396
402
|
bar, and this field is what there is to turn once one is seen.
|
|
397
403
|
|
|
398
404
|
**What it trades.** Every figure below is issue #284 Stage E's, verbatim from
|
|
399
|
-
[`docs/evidence/weight-cap.md`](evidence/weight-cap.md), on every lattice mesh of `demo`,
|
|
405
|
+
[`docs/evidence/weight-cap.md`](https://github.com/rig-c/parts/blob/main/docs/evidence/weight-cap.md), on every lattice mesh of `demo`,
|
|
400
406
|
`scarf` and the trailer (`rig-c/trailer` at a99d976): the inverted triangles at the bend
|
|
401
407
|
sets (±15° on every chain link; on the trailer also ± the film's own bends, after the
|
|
402
408
|
slash), the largest idle displacement against today's rig in rig px (the part in the
|
|
@@ -1054,7 +1060,7 @@ file in rig-c's motion-spec shape beside the config and name it:
|
|
|
1054
1060
|
```
|
|
1055
1061
|
|
|
1056
1062
|
`build` writes `surprised` into `rig/motion.json` after `idle`, gates it with the rig, and
|
|
1057
|
-
`check` names it on a line of its own (`examples/reference-sample` carries one, `animations.json` with `hop`, the
|
|
1063
|
+
`check` names it on a line of its own ([`examples/reference-sample`](https://github.com/rig-c/parts/tree/main/examples/reference-sample), in the repository, carries one, `animations.json` with `hop`, the
|
|
1058
1064
|
recipe below). With exactly this file beside `examples/reference-sample`'s config in its place,
|
|
1059
1065
|
`build` printed (this tree, rig-c as `package.json` pins it):
|
|
1060
1066
|
|
|
@@ -1067,10 +1073,10 @@ recipe below). With exactly this file beside `examples/reference-sample`'s confi
|
|
|
1067
1073
|
A bone the rig lacks is rigc's refusal at the rig stage, in rigc's words (`RIG_RIGC_GREEN`); a
|
|
1068
1074
|
fault in the file itself is the loader's (the row above names each one).
|
|
1069
1075
|
|
|
1070
|
-
**Worked sets.** `examples/demo/animations.json` and `examples/scarf/animations.json`
|
|
1071
|
-
(issue #224) carry `tilt`, `look_l`, `look_r`, `smile`, `startled`, a gesture (`wave` on
|
|
1076
|
+
**Worked sets.** [`examples/demo/animations.json`](https://github.com/rig-c/parts/blob/main/examples/demo/animations.json) and [`examples/scarf/animations.json`](https://github.com/rig-c/parts/blob/main/examples/scarf/animations.json)
|
|
1077
|
+
(issue #224; in the repository, not the package) carry `tilt`, `look_l`, `look_r`, `smile`, `startled`, a gesture (`wave` on
|
|
1072
1078
|
`demo`, `bow` on `scarf`) and `hop`, each a one-shot that keys only what it needs; the
|
|
1073
|
-
examples' READMEs list what each keys and why. Two rules make a clip layer over the idle on
|
|
1079
|
+
examples' READMEs ([demo](https://github.com/rig-c/parts/blob/main/examples/demo/README.md), [scarf](https://github.com/rig-c/parts/blob/main/examples/scarf/README.md)) list what each keys and why. Two rules make a clip layer over the idle on
|
|
1074
1080
|
a player's second track rather than replace it:
|
|
1075
1081
|
|
|
1076
1082
|
- **Key the bones the idle leaves alone.** The idle turns and breathes the `*_ctl` bones
|
|
@@ -1083,10 +1089,33 @@ a player's second track rather than replace it:
|
|
|
1083
1089
|
|
|
1084
1090
|
**Strands that trail.** A chain whose links lag the body is one `physics` constraint per
|
|
1085
1091
|
link in `constraints`, on the link bone (`hairback_r0`, not `hairback_r0_ctl`), driving
|
|
1086
|
-
`rotate`; the examples soften it down the chain
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1092
|
+
`rotate`; the examples soften it down the chain: every link takes the root link's `inertia`,
|
|
1093
|
+
`damping`, `mass` and `mix`, and link i of n (i = 0 at the root) takes the root link's `strength` times
|
|
1094
|
+
1 − 0.35 · i / (n − 1), so the last link is the softest at 0.65 of the root's. `demo`'s right
|
|
1095
|
+
twin tail, the three links of chain `hairback_r`, as
|
|
1096
|
+
[`examples/demo/config.json`](https://github.com/rig-c/parts/blob/main/examples/demo/config.json) (in the repository)
|
|
1097
|
+
holds it:
|
|
1098
|
+
|
|
1099
|
+
```json
|
|
1100
|
+
"constraints": [
|
|
1101
|
+
{ "type": "physics", "name": "hairback_r0_phys", "bone": "hairback_r0", "rotate": 1, "inertia": 0.8, "strength": 90.0, "damping": 0.85, "mass": 1, "mix": 1 },
|
|
1102
|
+
{ "type": "physics", "name": "hairback_r1_phys", "bone": "hairback_r1", "rotate": 1, "inertia": 0.8, "strength": 74.25, "damping": 0.85, "mass": 1, "mix": 1 },
|
|
1103
|
+
{ "type": "physics", "name": "hairback_r2_phys", "bone": "hairback_r2", "rotate": 1, "inertia": 0.8, "strength": 58.5, "damping": 0.85, "mass": 1, "mix": 1 },
|
|
1104
|
+
… ]
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
`scarf`'s scarf ends, four links each, take the same shape at inertia 0.7: `strength` 70,
|
|
1108
|
+
61.8333, 53.6667 and 45.5 down `robe_r` ([`examples/scarf/config.json`](https://github.com/rig-c/parts/blob/main/examples/scarf/config.json)).
|
|
1109
|
+
What these values do, measured on the clips and not by any bar: in `demo`'s `hop` the last
|
|
1110
|
+
link of each tail sat 46.6 and 45.3 rig px from where the keys put it relative to `head` at
|
|
1111
|
+
the landing (0.73 s); inertia 1 tore `hair_back` in that hop (`TEXTURE_STRETCH` 3.258 against
|
|
1112
|
+
1.926544), and lowering `strength` instead of raising inertia saturates (the demo README,
|
|
1113
|
+
*The strands*). A single bone with no `length` (a bow, an earring, a bun) cannot take a
|
|
1114
|
+
`rotate` drive (rig-c's `A23`) and takes `x`/`y` instead — `demo`'s bows `"x": 0.5, "y": 1`,
|
|
1115
|
+
inertia 0.3, strength 160. The values that read on the examples, and the ones refused on the
|
|
1116
|
+
way there, are in their READMEs, in the repository:
|
|
1117
|
+
[`examples/demo/README.md`](https://github.com/rig-c/parts/blob/main/examples/demo/README.md) and
|
|
1118
|
+
[`examples/scarf/README.md`](https://github.com/rig-c/parts/blob/main/examples/scarf/README.md).
|
|
1090
1119
|
|
|
1091
1120
|
**Limits.** Every slot holds one attachment, the part's own image, so an expression moves,
|
|
1092
1121
|
turns and scales the face's parts and never swaps one for another: there is no second mouth
|
|
@@ -1101,7 +1130,7 @@ against 1.926544).
|
|
|
1101
1130
|
|
|
1102
1131
|
A jiggle (a bust, the hips) is a patch of one part that lags behind the body. The package
|
|
1103
1132
|
proposes none and judges none; what it gives is the three pieces to build one, all in the
|
|
1104
|
-
config. `examples/reference-sample` carries a worked one (issue #183, PR #187), and its config is the
|
|
1133
|
+
config. [`examples/reference-sample`](https://github.com/rig-c/parts/tree/main/examples/reference-sample) carries a worked one (issue #183, PR #187), and its config is the
|
|
1105
1134
|
fragment below, cut to the bust:
|
|
1106
1135
|
|
|
1107
1136
|
```jsonc
|
|
@@ -1154,10 +1183,12 @@ idle moves the bust at most 0.066 rig px, idle frame 2). Faster keys on the pare
|
|
|
1154
1183
|
a turn, in a named animation (the recipe above) or a consumer's own — are what the spring
|
|
1155
1184
|
answers. `reference-sample`'s `animations.json` holds one, `hop` (1 s: `hip` `translate` y down 5, up 24,
|
|
1156
1185
|
landing at -6 at 0.45 s, settling at 0.55 s; nothing keys the jiggle bones): rendered at 30 fps
|
|
1157
|
-
against the same build without its constraints, the bust moves at most 6.
|
|
1158
|
-
(`hip_phys` inertia 0.2, strength 160, damping 0.88, mass 1.5) at most 4.
|
|
1159
|
-
|
|
1160
|
-
|
|
1186
|
+
against the same build without its constraints, the bust moves at most 6.702 rig px and the hips
|
|
1187
|
+
(`hip_phys` inertia 0.2, strength 160, damping 0.88, mass 1.5) at most 4.609 (frame 14, at the
|
|
1188
|
+
landing), and both overshoot after it, 3.27 and 1.35 rig px (frames 21 and 22). These are rig-c
|
|
1189
|
+
2.45.2's renders, whose physics no longer ends a frame on a step's tie; on rig-c 2.40.0 the same
|
|
1190
|
+
build read 6.583 and 4.432 (frame 13) and 3.38 and 1.38, which
|
|
1191
|
+
[`docs/evidence/sample-hop.png`](https://github.com/rig-c/parts/blob/main/docs/evidence/sample-hop.png) shows. No bar here judges the jiggle, and two limits follow from that:
|
|
1161
1192
|
|
|
1162
1193
|
- **The region's `band` caps the displacement.** The patch moves as one piece and the band is
|
|
1163
1194
|
where the mesh stretches to follow it, so physics values that carry the patch further than
|
|
@@ -1166,8 +1197,8 @@ into the landing), and both overshoot after it, 3.38 and 1.38 rig px (frames 21
|
|
|
1166
1197
|
geometry with PR #187's values (inertia 0.5, strength 100, damping 0.85, mass 1, a bone
|
|
1167
1198
|
displacement of 9.85 rig px against bands of 16 and 12) gives
|
|
1168
1199
|
`CHECK_TEXTURE_STRETCH: mesh "bottomwear" triangle 13 (vertices 5 183 6), edge 6-5, idle frame 13 — the edge is 0.233 times its rest length (max(ratio, 1/ratio) 4.29); <= 1.926544 is required`
|
|
1169
|
-
and `topwear` 2.597; with the values tracked now it reads 1.
|
|
1170
|
-
(`bottomwear`). Tune the constraint against the animation that drives it, not the idle.
|
|
1200
|
+
and `topwear` 2.597; with the values tracked now it reads 1.719 (`topwear`) and 1.602
|
|
1201
|
+
(`bottomwear`) (1.697 and 1.566 on rig-c 2.40.0, before 2.45.2's render clock). Tune the constraint against the animation that drives it, not the idle.
|
|
1171
1202
|
|
|
1172
1203
|
#### A part only physics moves, in the automatic mode
|
|
1173
1204
|
|
|
@@ -1252,8 +1283,26 @@ full loader before its assemble stage writes anything.
|
|
|
1252
1283
|
6. `rig-parts assemble --propose-plan --source … --full … --head … --config config.json`
|
|
1253
1284
|
→ paste `plan` and `extend_below_crop` into `config.assemble`. Config: the
|
|
1254
1285
|
`layers` row, with `head_box`.
|
|
1255
|
-
Read `notes`:
|
|
1256
|
-
|
|
1286
|
+
Read `notes`: every layer not planned — each tag a layer of either run holds that no
|
|
1287
|
+
row takes — is named there once, `<tag>: in no plan row — …`, with why, run by run: the
|
|
1288
|
+
run holds no such layer; the proposer takes that tag from the other run only
|
|
1289
|
+
(`PROPOSE_HEAD_TAGS` in `src/assemble.ts` lists the tags it takes from the head run,
|
|
1290
|
+
and every other tag comes from the full run); the layer is empty, its opaque count (run
|
|
1291
|
+
pixels of alpha above 8 after ghost clean-up) below the proposer's floor of 150
|
|
1292
|
+
(`PROPOSE_MIN_PX`, `src/assemble.ts`); or it crosses a plausibility rule, and the
|
|
1293
|
+
layer's own note, last, gives its figures and the rule (§6, *Plausibility*; a layer
|
|
1294
|
+
only such a note leaves out is named by it alone). On the four examples the
|
|
1295
|
+
`neckwear`, `tail`, `wings`, `objects` and `eyewear` layers hold 62 to 74 px in each
|
|
1296
|
+
run, so each is named empty (or taken from the other run only). `nose` is never
|
|
1297
|
+
planned — the reference implementation's rule, which this port keeps; the reference
|
|
1298
|
+
records no reason — and its note gives both runs' counts and what a nose part buys:
|
|
1299
|
+
`propose`'s `head pitch:` note reads where the nose sits between the eye midpoint and
|
|
1300
|
+
the mouth only off a part tagged `nose`, so the note names the row to add,
|
|
1301
|
+
`["nose", "head", "nose"]` after the row that takes `face` (the run whose layer holds
|
|
1302
|
+
more of the nose). With that one row added to a copy of each config, `demo` reads
|
|
1303
|
+
0.474 and `scarf` 0.564 (the figures `FRONTAL_REFERENCE` in `src/propose.ts` quotes),
|
|
1304
|
+
and the recomposite moves by 2 error px on `demo` (14,570 to 14,568) and none on
|
|
1305
|
+
`scarf`, uncovered error px unchanged on both. A full-run layer holding a body
|
|
1257
1306
|
no planned part covers — long hair that leaves the head crop sideways, or a
|
|
1258
1307
|
head-run part that stops above the crop line so `extend_below_crop` continues
|
|
1259
1308
|
nothing — is named with the body, the rule that skipped it and the two
|
|
@@ -1305,7 +1354,10 @@ full loader before its assemble stage writes anything.
|
|
|
1305
1354
|
rules for `bones`, `meshes`, `regions` and `motion`.
|
|
1306
1355
|
9. `rig-parts build --config config.json --source inputs/painting.png --full … --head … --out out [--loop]`
|
|
1307
1356
|
— assemble, rig and check in one process. `propose` is not part of it, on purpose:
|
|
1308
|
-
a proposal is a draft, and the config you corrected is the input.
|
|
1357
|
+
a proposal is a draft, and the config you corrected is the input. How long it takes
|
|
1358
|
+
is §8 (the two reference examples, 12.7 s and 27.5 s wall clock on one machine): a
|
|
1359
|
+
run your harness stopped before its last line printed no verdict, which is not a
|
|
1360
|
+
failed build — the build's verdict is its `check:` line or a named refusal (§5, §6).
|
|
1309
1361
|
`--idle-keys direct` is forwarded to the rig stage as `rig` takes it; a config
|
|
1310
1362
|
with a two-bone ik over chain links the idle keys needs it (§3, `constraints`).
|
|
1311
1363
|
Every flag `check` takes is forwarded to the check stage, taken by `build` as
|
|
@@ -1396,6 +1448,14 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
|
|
|
1396
1448
|
| `compose` | the `[compose]` lines: one per character (its offset, its scale, its placed bounds, the shift and how many first-level bones, regions on `root` and weights bound to `root` it moved, its bone, slot, track and constraint counts, its `rig_scale` and idle), the declared order and the slot count it expands to, rigc's gate lines; then the `[check]` lines as for `check`, and `scene.json` | both gate summaries `0 failed`, `check: PASS` with the bars that measured and the ones that said SKIP named — on a composed rig `4 of 9`: the gate, the loop, `CHAIN_LAG` and `TEXTURE_STRETCH` (there is no `parts.json`; each character's own `build` measured all nine). `scene.json`'s `order.slots` is the order the compiled skeleton draws | a `SCENE_*` refusal (§6, *compose*) — the scene file, a build, or the order; `COMPOSE_RIGC_GREEN` — rigc's own line, quoted |
|
|
1397
1449
|
| `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line (under `build --lead-in`, the lead-in line in its place: the frames kept and the jump at the wrap), each file's line, then `loop: idle.png N B (lossless); idle-indexed.png N B (max …, mean …); idle.gif N B (max …, mean …)` | `f0048.png equals f0000.png byte for byte, so it is dropped` | `LOOP_ENCODE` (§6) |
|
|
1398
1450
|
|
|
1451
|
+
`RECOMPOSITE_HOLES` is `REPORTED`, never judged, and its count reads large: for scale, the
|
|
1452
|
+
four examples, each a green build, report 259 (`reference-sample`), 291 (`scarf`), 453
|
|
1453
|
+
(`reference-demo`) and 1,020 (`demo`) uncovered holes, holding 1,185, 1,365, 1,564 and
|
|
1454
|
+
2,939 uncovered error px, the largest hole 94, 63, 70 and 48 px (each example's
|
|
1455
|
+
`expected/parts.json`, its `recomposite` block, in the repository) — hundreds of slivers
|
|
1456
|
+
along part edges are what a green build reports; one hole of hundreds of px is the red
|
|
1457
|
+
the `assemble` row above describes.
|
|
1458
|
+
|
|
1399
1459
|
`loop` writes three files from one frame set, and they are not interchangeable.
|
|
1400
1460
|
`idle.png` (`loop --out x.png`) is the lossless APNG: every frame decodes to the
|
|
1401
1461
|
rendered frame byte for byte, so it is the exactness record. `idle-indexed.png`
|
|
@@ -1406,8 +1466,10 @@ entry, no dithering, filter None) — and is the small file to show. `idle.gif`
|
|
|
1406
1466
|
print their palette error, per channel over R, G and B of every frame (and alpha's
|
|
1407
1467
|
max for the APNG); a figure is a measurement of the file, not a bar. On the demo
|
|
1408
1468
|
(the README's animation since issue #224's follow-up: `build --loop --lead-in 3`), with
|
|
1409
|
-
the breath of issue #270 (2.5 % of neck -> hip, 4.80 px peak to peak): 4,
|
|
1410
|
-
|
|
1469
|
+
the breath of issue #270 (2.5 % of neck -> hip, 4.80 px peak to peak): 4,449,233 B lossless,
|
|
1470
|
+
703,909 B indexed and 760,058 B GIF, both palette files at max 71, mean 0.636 (rig-c 2.45.2
|
|
1471
|
+
and later, whose render no longer ends a frame on a physics step's tie; on rig-c 2.40.0,
|
|
1472
|
+
4,446,263, 704,009 and 760,294 B at max 70, mean 0.631); on the
|
|
1411
1473
|
40-frame idle of issue #252 (rig-c 2.40.0, the breath 2.5 % of the topwear's height, 5.92 px):
|
|
1412
1474
|
4,514,756 B lossless, 725,845 B indexed and 784,074 B GIF, at the same error; on the 4 s idle before it
|
|
1413
1475
|
(rig-c 2.33.0): 5,148,992, 727,486 and 781,526 B at max 69, mean 0.630 — the two share one quantiser, so their error is the same by
|
|
@@ -1424,7 +1486,7 @@ renders the idle played n + 1 times back to back (`idle_lead_in`, the build's ot
|
|
|
1424
1486
|
animations kept so the viewport is fitted as for the idle frames), keeps the last
|
|
1425
1487
|
period, drops its end frame (the next period's first) and prints how far that end frame
|
|
1426
1488
|
is from the first kept one — the jump the loop will show. The settling is the rig's, not
|
|
1427
|
-
a constant: on the demo's 4 s idle, `--lead-in 1` and `2` each still read max |d| 117, 305 px over 40, and `3` reads max 1, 0 px over 40; on its 40-frame idle (issue #252, a shorter period and a breath twice as far), `3` reads max
|
|
1489
|
+
a constant: on the demo's 4 s idle, `--lead-in 1` and `2` each still read max |d| 117, 305 px over 40, and `3` reads max 1, 0 px over 40; on its 40-frame idle (issue #252, a shorter period and a breath twice as far), `3` reads max 1, 0 px over 40 (max 7 before rig-c 2.45.2, whose render no longer ends a frame on a physics step's tie). The idle
|
|
1428
1490
|
must repeat without an invented value — every track keyed from 0 to the duration, ending
|
|
1429
1491
|
where it starts — or the build refuses it (`LOOP_LEAD_IN_IDLE`). `check` and its bars
|
|
1430
1492
|
read `check/idle_frames/` as before; only the loop files move. On reference-demo with `--seam silhouette`
|
|
@@ -1844,6 +1906,7 @@ See-through with another seed.
|
|
|
1844
1906
|
| `CONFIG_PART_ATTACHED` | a plan part or a cut's piece with neither or both of a mesh and a region; a patch with a mesh, or with no region | `meshes` / `regions` |
|
|
1845
1907
|
| `CONFIG_RIBS_MODE` | `meshes.<part>.ribs` on a lattice (`grid`) mesh: its vertices are its grid's corners, which no row can move (issue #188) | put the part in `contour` or `auto`, or remove `ribs` |
|
|
1846
1908
|
| `CONFIG_INFLUENCES_MODE` | `meshes.<part>.influences` on an automatic (`auto`) mesh: that mode's cap and floor are `auto.influences`, which rig-c's reduction is handed (issue #284) | declare them in `auto.influences`, or remove `influences` |
|
|
1909
|
+
| `CONFIG_STRAY_MODE` | `meshes.<part>.stray` on a contour (`contour`) or automatic (`auto`) mesh: those modes state the largest island they may leave out in `contour.stray` and `auto.source.stray`, which their trace reads (issue #303) | declare it there, or remove `stray` |
|
|
1847
1910
|
| `CONFIG_PATCH_BOX` | an `assemble.patches` box with `x1 <= x0` or `y1 <= y0` | that patch's `box` (`x1`, `y1` are exclusive) |
|
|
1848
1911
|
| `CONFIG_AMPS_MATCH_CHAIN` | a chain track's `amps` is not one per link | that track's `amps` |
|
|
1849
1912
|
| `CONFIG_PERIOD_DIVIDES_DURATION` | a period that is not a whole fraction of the idle — the loop could not close | that track's `period`, or `motion.duration` |
|
package/docs/STABILITY.md
CHANGED
|
@@ -124,7 +124,7 @@ part name, `[i]` an entry of a list):
|
|
|
124
124
|
- **assemble.cuts[i].draw**, when an object — required: `before`
|
|
125
125
|
- **bones[i]**, a single bone — required: `name`, `parent`, `at`; optional: `tip`
|
|
126
126
|
- **bones[i]**, a chain — required: `chain`, `parent`, `points`, `tip`
|
|
127
|
-
- **meshes.<part>** — exactly one of `grid`, `contour`, `auto`; required in every mode: `r`, `segments`; optional in every mode: `exponent`, `rule`; optional with `contour` or `auto`: `ribs`; optional with `grid` or `contour`: `influences` (issue #284; beside `auto` it is refused, CONFIG_INFLUENCES_MODE)
|
|
127
|
+
- **meshes.<part>** — exactly one of `grid`, `contour`, `auto`; required in every mode: `r`, `segments`; optional in every mode: `exponent`, `rule`; optional with `contour` or `auto`: `ribs`; optional with `grid` or `contour`: `influences` (issue #284; beside `auto` it is refused, CONFIG_INFLUENCES_MODE); optional with `grid`: `stray` (issue #303; beside `contour` or `auto` it is refused, CONFIG_STRAY_MODE)
|
|
128
128
|
- **meshes.<part>.influences** — optional, at least one: `maxInfluences` (a whole number, 1 or more, or null for no cap), `floor` ("cut" or "continuous")
|
|
129
129
|
- **meshes.<part>.ribs.<chain>** — required: `stations`
|
|
130
130
|
- **meshes.<part>.contour** — required: `tolerance`, `margin`, `spacing`; optional: `maxEdge`, `budget`, `stray`, `regions`
|
|
@@ -171,7 +171,7 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
171
171
|
| --- | --- | --- |
|
|
172
172
|
| `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px`, `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242) |
|
|
173
173
|
| `rig/rig.json`, `rig/motion.json`, `rig/images/` | `rig`, `build`, `compose` | rig-c's rig spec and motion spec, `spec` `rigc-rig/1` and `rigc-motion/1`: the format is rig-c's, and this package writes only what rig-c's parser accepts |
|
|
174
|
-
| `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
|
|
174
|
+
| `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
|
|
175
175
|
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
176
176
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `turn` with `--full`/`--head`, and `notes` |
|
|
177
177
|
| `basis.json` | `propose` | spec `spine-parts-basis/1` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.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": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"ag-psd": "^31.0.2",
|
|
63
|
-
"rig-c": "^2.
|
|
63
|
+
"rig-c": "^2.47.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@esotericsoftware/spine-core": "4.3.13",
|
package/src/assemble.ts
CHANGED
|
@@ -2654,6 +2654,14 @@ function fmt0(v: number): string {
|
|
|
2654
2654
|
* no role. One both runs hold is proposed once, from the full run, and a note
|
|
2655
2655
|
* says how to take the head run's too. No public example holds one, so no
|
|
2656
2656
|
* proposal of theirs moves.
|
|
2657
|
+
*
|
|
2658
|
+
* ⚖️ This port's too (issue #308): every tag a layer of either run holds that
|
|
2659
|
+
* no plan row or extend entry takes is named once, with why, run by run
|
|
2660
|
+
* (`unplannedNotes`), after the uncovered bodies and before the plausibility
|
|
2661
|
+
* drops. The plan is not changed. [observed] On the four public examples the
|
|
2662
|
+
* plan, the extend entries and every earlier note are byte for byte as
|
|
2663
|
+
* before; each gains one such note per tag left out (demo 6, scarf 9,
|
|
2664
|
+
* reference-demo 7, reference-sample 9).
|
|
2657
2665
|
*/
|
|
2658
2666
|
export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx = PROPOSE_MIN_PX): PlanProposal {
|
|
2659
2667
|
const frame = checkGeometry(g, { full, head });
|
|
@@ -2778,8 +2786,83 @@ export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx =
|
|
|
2778
2786
|
why,
|
|
2779
2787
|
);
|
|
2780
2788
|
const bodyNotes = left.map((u) => uncoveredNote(u, plan, minBody));
|
|
2781
|
-
|
|
2782
|
-
|
|
2789
|
+
const unplanned = unplannedNotes(runs, plan, extend, noted, figures, minPx, (run, tag) => (find(run, tag)?.opaqueRunPx ?? 0) * (run === 'head' ? headArea : fullArea), new Set(left.map((u) => u.tag)));
|
|
2790
|
+
// The reference's notes, then the uncovered bodies, then the layers in no row (issue #308), then the plausibility drops — a drop note is last, as it was.
|
|
2791
|
+
return { plan, extend_below_crop: extend, notes: [...notes, ...bodyNotes, ...unplanned, ...dropNotes] };
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
/** What every `--propose-plan` note on a layer in no row says its pixel figures are. */
|
|
2795
|
+
export const UNPLANNED_PX = 'px: run pixels of alpha above 8 after ghost clean-up';
|
|
2796
|
+
|
|
2797
|
+
/**
|
|
2798
|
+
* One note per tag that a layer of either run holds and no plan row or
|
|
2799
|
+
* extend entry takes (issue #308), in the full run's draw order and then the
|
|
2800
|
+
* head run's, saying for each run why: it holds no such layer; the proposer
|
|
2801
|
+
* takes the tag from the other run only (`PROPOSE_HEAD_TAGS`); its layer is
|
|
2802
|
+
* empty, its opaque count below the floor (`PROPOSE_MIN_PX`); it crosses a
|
|
2803
|
+
* plausibility rule (that layer's own note, which follows, names its figures);
|
|
2804
|
+
* or, for a full-run `headwear` or `earwear`, the head run's was not below 0.8
|
|
2805
|
+
* of it in rig px. A tag every holding run of which a plausibility note
|
|
2806
|
+
* already names gets no second note. `nose`, which the proposer never plans
|
|
2807
|
+
* (the reference's rule), gets a note of its own: the counts, and the row that
|
|
2808
|
+
* lets `propose` read the head's pitch, the only reading in this package made
|
|
2809
|
+
* off a part tagged `nose` (`headFrameNotes`, `src/propose.ts`). The plan is
|
|
2810
|
+
* not changed.
|
|
2811
|
+
*/
|
|
2812
|
+
function unplannedNotes(
|
|
2813
|
+
runs: Record<Run, RunLayer[]>,
|
|
2814
|
+
plan: readonly PlanEntry[],
|
|
2815
|
+
extend: readonly Extend[],
|
|
2816
|
+
noted: ReadonlySet<string>,
|
|
2817
|
+
figures: Record<Run, LayerFigures[]>,
|
|
2818
|
+
minPx: number,
|
|
2819
|
+
rigPx: (run: Run, tag: string) => number,
|
|
2820
|
+
bodies: ReadonlySet<string>,
|
|
2821
|
+
): string[] {
|
|
2822
|
+
const inRow = new Set<string>([...plan.map((p) => p[2]), ...extend.map((e) => e.tag)]);
|
|
2823
|
+
const find = (run: Run, tag: string): RunLayer | undefined => runs[run].find((l) => l.tag === tag);
|
|
2824
|
+
const floor = minPx === PROPOSE_MIN_PX ? `the proposer's floor of ${minPx} (PROPOSE_MIN_PX, src/assemble.ts)` : `the floor of ${minPx} this proposal was given`;
|
|
2825
|
+
const out: string[] = [];
|
|
2826
|
+
const seen = new Set<string>();
|
|
2827
|
+
for (const { tag } of [...runs.full, ...runs.head]) {
|
|
2828
|
+
if (seen.has(tag) || inRow.has(tag)) continue;
|
|
2829
|
+
seen.add(tag);
|
|
2830
|
+
const held = (['full', 'head'] as const).filter((r) => find(r, tag) !== undefined);
|
|
2831
|
+
const body = bodies.has(tag) ? "; the full run's layer holds a body no planned part covers, named in its note above" : '';
|
|
2832
|
+
if (tag === 'nose') {
|
|
2833
|
+
const counts = (['full', 'head'] as const).map((r) => `${r} run ${find(r, tag)?.opaqueRunPx ?? 'no layer'}${find(r, tag) === undefined ? '' : ' px'}`).join(', ');
|
|
2834
|
+
const best = (['head', 'full'] as const).reduce((a, b) => ((find(b, tag)?.opaqueRunPx ?? 0) > (find(a, tag)?.opaqueRunPx ?? 0) ? b : a));
|
|
2835
|
+
const most = find(best, tag)?.opaqueRunPx ?? 0;
|
|
2836
|
+
const after = plan.some((p) => p[2] === 'face') ? ` after the row that takes "face"` : '';
|
|
2837
|
+
const how =
|
|
2838
|
+
most === 0
|
|
2839
|
+
? 'neither run\'s layer holds a pixel, so there is no nose part to add'
|
|
2840
|
+
: `propose reads the head's pitch (where the nose sits between the eye midpoint and the mouth) only off a part tagged nose, and without one prints "not read: no part tagged nose"; to have it read, add ["nose", "${best}", "nose"] to assemble.plan${after} (the ${best} run's layer holds the more of it) and run assemble and propose again`;
|
|
2841
|
+
out.push(`nose: in no plan row — the proposer never plans nose (the reference implementation's rule, which this port keeps); ${counts} (${UNPLANNED_PX}); ${how}${body}`);
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
if (held.every((r) => noted.has(`${r}:${tag}`))) continue;
|
|
2845
|
+
const reason = (run: Run): string => {
|
|
2846
|
+
const l = find(run, tag);
|
|
2847
|
+
if (l === undefined) return `${run} run: no layer`;
|
|
2848
|
+
const px = `${run} run: ${l.opaqueRunPx} px`;
|
|
2849
|
+
const headTag = PROPOSE_HEAD_TAGS.includes(tag);
|
|
2850
|
+
const fallback = tag === 'headwear' || tag === 'earwear';
|
|
2851
|
+
if (run === 'full' && headTag && !fallback) return `${px}, but the proposer takes ${tag} from the head run only (PROPOSE_HEAD_TAGS, src/assemble.ts)`;
|
|
2852
|
+
if (run === 'head' && !headTag && readAuthorTag(tag) === null) return `${px}, but the proposer takes ${tag} from the full run only (it is not one of PROPOSE_HEAD_TAGS, src/assemble.ts)`;
|
|
2853
|
+
if (l.opaqueRunPx < minPx) return `${px}, empty: below ${floor}`;
|
|
2854
|
+
if (noted.has(`${run}:${tag}`)) {
|
|
2855
|
+
const f = figures[run].find((x) => x.name === tag) as LayerFigures;
|
|
2856
|
+
return `${px}, crosses ${implausibleRules(f).join(', ')} (named with its figures in the note below)`;
|
|
2857
|
+
}
|
|
2858
|
+
if (run === 'full' && fallback) {
|
|
2859
|
+
return `${px}, but the proposer takes ${tag} from the full run only when the head run's is below 0.8 of it in rig px (head run ${fmt0(rigPx('head', tag))}, full run ${fmt0(rigPx('full', tag))})`;
|
|
2860
|
+
}
|
|
2861
|
+
throw new Error(`unplannedNotes: no rule left ${run}:${tag} out of the plan, yet no row takes it`);
|
|
2862
|
+
};
|
|
2863
|
+
out.push(`${tag}: in no plan row — ${(['full', 'head'] as const).map(reason).join('; ')} (${UNPLANNED_PX})${body}`);
|
|
2864
|
+
}
|
|
2865
|
+
return out;
|
|
2783
2866
|
}
|
|
2784
2867
|
|
|
2785
2868
|
// ---------------------------------------------------------------------------
|
package/src/build.ts
CHANGED
|
@@ -834,7 +834,11 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
834
834
|
const stray = c.strayIslands === 0 ? '' : ` left out ${c.strayIslands} island(s), ${c.strayPixels} px`;
|
|
835
835
|
const regions = m.regions.map((rg) => ` region ${rg.name}->${rg.bone} reaches ${rg.reached} (${rg.whole} whole)`).join('');
|
|
836
836
|
log(`${head}contour tol=${m.params.tolerance} margin=${m.params.margin} spacing=${m.params.spacing}${m.params.maxEdge === undefined ? '' : ` maxEdge=${m.params.maxEdge} (outline +${c.outlineCut ?? 0})`} ${infl} overshoot ${c.overshoot} min angle ${c.smallestAngle.value}${stray}${regions}`);
|
|
837
|
-
} else
|
|
837
|
+
} else {
|
|
838
|
+
// issue #303: only beside a declared stray, so a lattice line without one reads as it always did.
|
|
839
|
+
const stray = m.stray_cleared === undefined ? '' : ` stray ${m.stray} left out ${m.stray_cleared.islands} island(s), ${m.stray_cleared.pixels} of ${m.stray_cleared.art_pixels} art px`;
|
|
840
|
+
log(`${head}grid=${m.grid} ${infl} loop passes ${rig.loopPasses[m.part]}${stray}`);
|
|
841
|
+
}
|
|
838
842
|
}
|
|
839
843
|
const regions = rig.rig.slots.length - rig.meshReport.length;
|
|
840
844
|
const tracks = rig.motion.animations.idle.tracks;
|
package/src/config.ts
CHANGED
|
@@ -278,6 +278,11 @@ export interface LatticeMeshSpec {
|
|
|
278
278
|
rule?: WeightRule;
|
|
279
279
|
/** The cap and the floor's form (issue #284); absent is 4 and the cut at 0.03, the rule as it always was. */
|
|
280
280
|
influences?: MeshInfluences;
|
|
281
|
+
/**
|
|
282
|
+
* The largest island, in art pixels, that may be left out of the lattice (issue #303) — the contour mode's
|
|
283
|
+
* `contour.stray`, with the same meaning (`latticeStray`, `src/mesh.ts`); absent leaves none out.
|
|
284
|
+
*/
|
|
285
|
+
stray?: number;
|
|
281
286
|
}
|
|
282
287
|
|
|
283
288
|
export interface ContourMeshSpec {
|
|
@@ -1665,7 +1670,7 @@ function checkMeshes(c: Check, v: Json, bones: Set<string>, chains: Map<string,
|
|
|
1665
1670
|
const at = `config.meshes.${part}`;
|
|
1666
1671
|
// The known keys keep their old order (grid, r, segments) with contour after them, and `r` and `segments` are
|
|
1667
1672
|
// required in the place and words the object check uses, so a lattice mesh's refusals read as they did.
|
|
1668
|
-
const m = c.object(at, spec, [], ['grid', 'r', 'segments', 'contour', 'auto', 'exponent', 'rule', 'ribs', 'influences']);
|
|
1673
|
+
const m = c.object(at, spec, [], ['grid', 'r', 'segments', 'contour', 'auto', 'exponent', 'rule', 'ribs', 'influences', 'stray']);
|
|
1669
1674
|
if (m === null) continue;
|
|
1670
1675
|
for (const key of ['r', 'segments']) if (!(key in m)) c.fail('CONFIG_FIELD_PRESENT', `${at}.${key}`, 'is absent and required');
|
|
1671
1676
|
const modes = ['grid', 'contour', 'auto'].filter((k) => k in m);
|
|
@@ -1707,6 +1712,7 @@ function checkMeshes(c: Check, v: Json, bones: Set<string>, chains: Map<string,
|
|
|
1707
1712
|
}
|
|
1708
1713
|
if ('ribs' in m) checkRibs(c, `${at}.ribs`, m, chains);
|
|
1709
1714
|
if ('influences' in m) checkMeshInfluences(c, `${at}.influences`, m);
|
|
1715
|
+
if ('stray' in m) checkLatticeStray(c, `${at}.stray`, m);
|
|
1710
1716
|
if ('auto' in m && typeof m.auto === 'object' && m.auto !== null && !Array.isArray(m.auto) && 'stimulus' in m.auto) {
|
|
1711
1717
|
checkStimulus(c, `${at}.auto.stimulus`, (m.auto as Record<string, Json>).stimulus, m, chains);
|
|
1712
1718
|
}
|
|
@@ -1775,6 +1781,21 @@ function checkMeshInfluences(c: Check, at: string, m: Record<string, Json>): voi
|
|
|
1775
1781
|
}
|
|
1776
1782
|
}
|
|
1777
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* `meshes.<part>.stray` (issue #303): the lattice's largest island, in art pixels, that may be left out — a whole
|
|
1786
|
+
* number, 0 or more, as the contour mode's `contour.stray` is. The contour and automatic modes already state it in
|
|
1787
|
+
* their own place (`contour.stray`, `auto.source.stray`), so beside either it is refused by name
|
|
1788
|
+
* (`CONFIG_STRAY_MODE`) rather than read by nothing.
|
|
1789
|
+
*/
|
|
1790
|
+
function checkLatticeStray(c: Check, at: string, m: Record<string, Json>): void {
|
|
1791
|
+
const elsewhere = 'contour' in m ? ['a contour mesh (contour)', 'contour.stray'] : 'auto' in m ? ['an automatic mesh (auto)', 'auto.source.stray'] : null;
|
|
1792
|
+
if (elsewhere !== null && !('grid' in m)) {
|
|
1793
|
+
c.fail('CONFIG_STRAY_MODE', at, `is ${show(m.stray)} on ${elsewhere[0]}; that mode states the largest island it may leave out in ${elsewhere[1]}, which its trace reads — declare it there, or remove stray`);
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
c.int(at, m.stray, 0);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1778
1799
|
/**
|
|
1779
1800
|
* `meshes.<part>.ribs` (issue #188): an object of chain name → `{ stations }`.
|
|
1780
1801
|
* Each key must be a chain this mesh's `segments` name (a rib is placed along
|
package/src/heat.ts
CHANGED
|
@@ -44,10 +44,19 @@
|
|
|
44
44
|
* silhouette, or one whose every reached pixel is nearer another bone. A
|
|
45
45
|
* bone named in `segments` is a bone the author said may pull the layer;
|
|
46
46
|
* giving it 0 everywhere would be a silent answer to a question the
|
|
47
|
-
* config asked.
|
|
47
|
+
* config asked. The rule is chosen per mesh (`meshes.<part>.rule`), so the
|
|
48
|
+
* refusal names the third remedy too (issue #303): this part keeps the
|
|
49
|
+
* distance rule — the one an author whose bone anchors the part from
|
|
50
|
+
* outside its art (a torso bone on a neck) can take without moving a bone
|
|
51
|
+
* another part placed or dropping the anchor.
|
|
48
52
|
* - `RIG_HEAT_ISLAND_SOURCE`: a 4-connected island of the silhouette that no
|
|
49
53
|
* bone has a source in. With no fixed temperature the equation has no
|
|
50
|
-
* answer there; nothing nearest is borrowed.
|
|
54
|
+
* answer there; nothing nearest is borrowed. When the caller can say the
|
|
55
|
+
* island is dust that its `stray` would leave out ({@link HeatDust}: the
|
|
56
|
+
* lattice, issue #303) the refusal names that field and the figure that
|
|
57
|
+
* leaves the island out instead of a segment nobody would place on it; an
|
|
58
|
+
* island above the dust line keeps the segment remedy, a real piece being
|
|
59
|
+
* a real authoring question.
|
|
51
60
|
* - `RIG_HEAT_CONVERGED`: above.
|
|
52
61
|
*
|
|
53
62
|
* Two runs on the same input write the same bytes: the claims, the sweep
|
|
@@ -55,6 +64,8 @@
|
|
|
55
64
|
*/
|
|
56
65
|
import type { Point } from './config.ts';
|
|
57
66
|
import { artMask, growSilhouette } from './contour.ts';
|
|
67
|
+
import { DUST_MAX_PX, DUST_MAX_SHARE, isDust } from './mesh.ts';
|
|
68
|
+
import { pyRound } from './round.ts';
|
|
58
69
|
import type { Problem } from './errors.ts';
|
|
59
70
|
import { connectedComponents, fillHoles, type Mask } from './raster/index.ts';
|
|
60
71
|
import { capAndFloor, type Influence, type InfluenceLimits, type Segment, segmentDistance } from './weights.ts';
|
|
@@ -120,11 +131,34 @@ export interface HeatRow {
|
|
|
120
131
|
|
|
121
132
|
const fmt = (v: number): string => String(Math.round(v * 1000) / 1000);
|
|
122
133
|
|
|
134
|
+
/**
|
|
135
|
+
* What a mesh's `stray` can say about a silhouette island no bone reaches (issue #303), handed in by the mode that has
|
|
136
|
+
* the field (the lattice): the field's path, its declared figure (null when absent), and the part's art islands it
|
|
137
|
+
* reads — the art px (`isDust`'s share is of them) and the largest island's area, which `stray` never leaves out. It
|
|
138
|
+
* is read only when an island is refused, so a build that solves never computes it.
|
|
139
|
+
*/
|
|
140
|
+
export interface HeatDust {
|
|
141
|
+
field: string;
|
|
142
|
+
declared: number | null;
|
|
143
|
+
artPixels: number;
|
|
144
|
+
largest: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The refusal's remedy for an island of `area` px no bone reaches: the stray field when the island is dust it would leave out, else a segment. */
|
|
148
|
+
function islandRemedy(area: number, dust: HeatDust | null): string {
|
|
149
|
+
if (dust !== null && isDust(area, dust.artPixels) && area < dust.largest) {
|
|
150
|
+
const now = dust.declared === null ? 'it is absent' : `it is ${dust.declared}`;
|
|
151
|
+
return `the island is dust — at most ${DUST_MAX_PX} px and at most ${pyRound(DUST_MAX_SHARE * 100, 6)} % of the part's ${dust.artPixels} art px, mesh-draft's dust line — which no bone is meant to sit on: declare ${dust.field} at ${area} or more (${now}), which leaves an island that small out of the mesh and the silhouette heat solves over`;
|
|
152
|
+
}
|
|
153
|
+
return `under rule "heat" every island needs a segment within ${HEAT_SOURCE_BAND} px of one of its pixel centres — nothing is borrowed from the nearest island`;
|
|
154
|
+
}
|
|
155
|
+
|
|
123
156
|
/**
|
|
124
157
|
* Solve bone heat over `sil` (part-image pixels) for `segs` (rig px; the image is the rig less `(ox, oy)`).
|
|
125
|
-
* `object` names the mesh in every refusal. Every problem found is returned at once.
|
|
158
|
+
* `object` names the mesh in every refusal. Every problem found is returned at once. `dust`, when the mesh's mode has
|
|
159
|
+
* a `stray` field, is read only to word an island refusal ({@link HeatDust}).
|
|
126
160
|
*/
|
|
127
|
-
export function boneHeat(object: string, sil: Mask, segs: readonly Segment[], ox: number, oy: number, limits: HeatLimits = HEAT_LIMITS): HeatField | Problem[] {
|
|
161
|
+
export function boneHeat(object: string, sil: Mask, segs: readonly Segment[], ox: number, oy: number, limits: HeatLimits = HEAT_LIMITS, dust: (() => HeatDust) | null = null): HeatField | Problem[] {
|
|
128
162
|
const { width: w, height: h, data } = sil;
|
|
129
163
|
const n = w * h;
|
|
130
164
|
const bones: string[] = [];
|
|
@@ -166,19 +200,21 @@ export function boneHeat(object: string, sil: Mask, segs: readonly Segment[], ox
|
|
|
166
200
|
problems.push({
|
|
167
201
|
code: 'RIG_HEAT_BONE_SOURCE',
|
|
168
202
|
object,
|
|
169
|
-
detail: `bone "${bone}" — its segment ${where} (rig px) ${why}, so it holds no source pixel; under rule "heat" every bound bone needs at least one: a silhouette pixel (${silPixels} px) whose centre lies within ${HEAT_SOURCE_BAND} px of the segment and nearer it than any other bound bone's. A bone with no source is not weighted 0 — move its segment onto the art,
|
|
203
|
+
detail: `bone "${bone}" — its segment ${where} (rig px) ${why}, so it holds no source pixel; under rule "heat" every bound bone needs at least one: a silhouette pixel (${silPixels} px) whose centre lies within ${HEAT_SOURCE_BAND} px of the segment and nearer it than any other bound bone's. A bone with no source is not weighted 0 — move its segment onto the art, take the bone out of segments, or keep this part on the distance rule (rule "distance", or no rule, on this mesh: the rule is chosen per mesh)`,
|
|
170
204
|
});
|
|
171
205
|
});
|
|
172
206
|
const cc = connectedComponents(sil, 4);
|
|
173
207
|
const sourced = new Uint8Array(cc.count);
|
|
174
208
|
for (let i = 0; i < n; i++) if (owner[i] >= 0) sourced[cc.labels[i]] = 1;
|
|
209
|
+
let dustRead: HeatDust | null = null;
|
|
175
210
|
for (let c = 1; c < cc.count; c++) {
|
|
176
211
|
if (sourced[c]) continue;
|
|
177
212
|
const st = cc.stats[c];
|
|
213
|
+
if (dust !== null && dustRead === null) dustRead = dust();
|
|
178
214
|
problems.push({
|
|
179
215
|
code: 'RIG_HEAT_ISLAND_SOURCE',
|
|
180
216
|
object,
|
|
181
|
-
detail: `the silhouette's 4-connected island of ${st.area} px at rig (${st.left + ox}, ${st.top + oy}) holds no bound bone's source pixel, so the heat equation has no fixed temperature there and no answer;
|
|
217
|
+
detail: `the silhouette's 4-connected island of ${st.area} px at rig (${st.left + ox}, ${st.top + oy}) holds no bound bone's source pixel, so the heat equation has no fixed temperature there and no answer; ${islandRemedy(st.area, dustRead)}`,
|
|
182
218
|
});
|
|
183
219
|
}
|
|
184
220
|
if (problems.length > 0) return problems;
|
package/src/mesh.ts
CHANGED
|
@@ -360,3 +360,64 @@ export function latticeMesh(part: string, art: Mask, grid: number, maxPasses: nu
|
|
|
360
360
|
}
|
|
361
361
|
return mesh;
|
|
362
362
|
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Dust (issue #265, Stage B's rule; moved here from `src/meshdraft.ts` by issue #303, so the rig stage's heat refusal
|
|
366
|
+
* and `mesh-draft` read one line): an extra 4-connected island of the art of at most this many art px …
|
|
367
|
+
*/
|
|
368
|
+
export const DUST_MAX_PX = 4;
|
|
369
|
+
|
|
370
|
+
/** … and at most this share of the part's art px. */
|
|
371
|
+
export const DUST_MAX_SHARE = 0.001;
|
|
372
|
+
|
|
373
|
+
/** Whether an island of `area` art px is dust in a part of `artPixels` art px ({@link DUST_MAX_PX}, {@link DUST_MAX_SHARE}). */
|
|
374
|
+
export function isDust(area: number, artPixels: number): boolean {
|
|
375
|
+
return area <= DUST_MAX_PX && area <= DUST_MAX_SHARE * artPixels;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** What a declared `meshes.<part>.stray` left out of a lattice's art (issue #303). */
|
|
379
|
+
export interface LatticeStray {
|
|
380
|
+
/** The art the lattice is built over: `art` itself when nothing was left out. */
|
|
381
|
+
art: Mask;
|
|
382
|
+
/** Islands left out, and their art px. */
|
|
383
|
+
islands: number;
|
|
384
|
+
pixels: number;
|
|
385
|
+
/** The art px of the part before anything was left out. */
|
|
386
|
+
artPixels: number;
|
|
387
|
+
/** The area of the largest island; an island this large is never left out. */
|
|
388
|
+
largest: number;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The lattice's `stray` (issue #303), the contour mode's field with the same meaning — the largest island, in art
|
|
393
|
+
* pixels, that may be left out of the mesh — read over the same islands: the 4-connected islands of the art (alpha
|
|
394
|
+
* above {@link ART_ALPHA}). Every island of `stray` art px or fewer is cleared from the art the lattice and its heat
|
|
395
|
+
* silhouette are built over, except an island as large as the largest, which is never left out (two equal largest
|
|
396
|
+
* pieces both stay: leaving one would be a choice the declaration does not make). Unlike the contour mode, an island
|
|
397
|
+
* above the figure is not refused: the lattice bridges every island it keeps (step 2 above), so it is meshed as it
|
|
398
|
+
* always was. The part's image is not touched; `artCoverage` is read over all the art, the islands left out included.
|
|
399
|
+
*/
|
|
400
|
+
export function latticeStray(art: Mask, stray: number): LatticeStray {
|
|
401
|
+
const comp = connectedComponents(art, 4);
|
|
402
|
+
let largest = 0;
|
|
403
|
+
let artPixels = 0;
|
|
404
|
+
for (let l = 1; l < comp.count; l++) {
|
|
405
|
+
largest = Math.max(largest, comp.stats[l].area);
|
|
406
|
+
artPixels += comp.stats[l].area;
|
|
407
|
+
}
|
|
408
|
+
const out = new Uint8Array(comp.count);
|
|
409
|
+
let islands = 0;
|
|
410
|
+
let pixels = 0;
|
|
411
|
+
for (let l = 1; l < comp.count; l++) {
|
|
412
|
+
const a = comp.stats[l].area;
|
|
413
|
+
if (a <= stray && a < largest) {
|
|
414
|
+
out[l] = 1;
|
|
415
|
+
islands++;
|
|
416
|
+
pixels += a;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (islands === 0) return { art, islands, pixels, artPixels, largest };
|
|
420
|
+
const data = new Uint8Array(art.data);
|
|
421
|
+
for (let i = 0; i < data.length; i++) if (out[comp.labels[i]] === 1) data[i] = 0;
|
|
422
|
+
return { art: { width: art.width, height: art.height, data }, islands, pixels, artPixels, largest };
|
|
423
|
+
}
|
package/src/meshdraft.ts
CHANGED
|
@@ -68,7 +68,7 @@ import { BARS, type CheckReport, type PackShape, type PageEdges, type RigcRunner
|
|
|
68
68
|
import { type CharacterConfig, type ContourMeshSpec, type LatticeMeshSpec, loadConfigAndAnimations, type Segment } from './config.ts';
|
|
69
69
|
import { PartsError, type Problem, problemLine } from './errors.ts';
|
|
70
70
|
import { IDLE_FPS } from './motion.ts';
|
|
71
|
-
import { ART_ALPHA } from './mesh.ts';
|
|
71
|
+
import { ART_ALPHA, DUST_MAX_PX, DUST_MAX_SHARE, isDust } from './mesh.ts';
|
|
72
72
|
import { type PartsFile, readParts } from './parts.ts';
|
|
73
73
|
import { alphaAbove, connectedComponents } from './raster/index.ts';
|
|
74
74
|
import { readPng } from './raster/png.ts';
|
|
@@ -90,11 +90,11 @@ export const TOLERANCE_FIGURE_DIVISOR = 554;
|
|
|
90
90
|
/** The heights of the three figures the two divisors were measured on, rig px (demo, the trailer, scarf). */
|
|
91
91
|
export const CALIBRATION_HEIGHTS: readonly number[] = [1106, 1108, 1185];
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
export
|
|
93
|
+
/**
|
|
94
|
+
* Dust (Stage B: 1–2 px dots were the measured case): `src/mesh.ts` holds the line since issue #303, which the rig
|
|
95
|
+
* stage's heat refusal reads too; re-exported here, where it was first written.
|
|
96
|
+
*/
|
|
97
|
+
export { DUST_MAX_PX, DUST_MAX_SHARE } from './mesh.ts';
|
|
98
98
|
|
|
99
99
|
/** The bend set's turn per link, degrees: the fallback Stage C stated, a physics constraint's reach not being derivable from it. */
|
|
100
100
|
export const BEND_DEGREES = 15;
|
|
@@ -182,7 +182,7 @@ export function dustOf(image: Raster): DustReading {
|
|
|
182
182
|
if (islands.length === 0) return { keep: `its image has no pixel with alpha above ${ART_ALPHA}`, islands };
|
|
183
183
|
if (islands.length > 1 && islands[0] === islands[1]) return { keep: `its art is ${islands.length} islands (${islands.join(', ')} px) of which two share the largest count, so no one island is the largest`, islands };
|
|
184
184
|
const extra = islands.slice(1);
|
|
185
|
-
const pieces = extra.filter((n) => !(n
|
|
185
|
+
const pieces = extra.filter((n) => !isDust(n, art));
|
|
186
186
|
if (pieces.length > 0) {
|
|
187
187
|
return {
|
|
188
188
|
keep: `its art is ${islands.length} islands (${islands.join(', ')} px); ${pieces.length} beside the largest ${pieces.length === 1 ? 'is a real piece' : 'are real pieces'}, not dust (at most ${DUST_MAX_PX} px and at most ${pyRound(DUST_MAX_SHARE * 100, 6)} % of the part's ${art} art px), and the contour mode meshes one island`,
|
package/src/rig.ts
CHANGED
|
@@ -153,13 +153,13 @@ import {
|
|
|
153
153
|
} from './automesh.ts';
|
|
154
154
|
import { type ContourChainRibs, type ContourReport, contourMesh, type ContourRegion, type ContourRib } from './contour.ts';
|
|
155
155
|
import { type LocalInfluence, localInfluences } from './localweights.ts';
|
|
156
|
-
import { artCoverage, ART_ALPHA, latticeMesh, ONE_LOOP_PASSES } from './mesh.ts';
|
|
156
|
+
import { artCoverage, ART_ALPHA, latticeMesh, latticeStray, ONE_LOOP_PASSES } from './mesh.ts';
|
|
157
157
|
import { BLINK, blinkHoldMisses, blinkSpan, CONTROL_SUFFIX, controlledBones, IDLE_FPS, idleMotion, type MotionSpec, moveKeysToControls } from './motion.ts';
|
|
158
158
|
import { PAINTING_RUN, type PartsFile, readFrom, tagOf } from './parts.ts';
|
|
159
159
|
import { EYE_GROUP_TAGS } from './propose.ts';
|
|
160
160
|
import { alphaAbove, crop, pad, type Raster } from './raster/index.ts';
|
|
161
161
|
import { pyRound } from './round.ts';
|
|
162
|
-
import { boneHeat, heatInfluences, heatRow, type HeatRow, latticeSilhouette, tracedSilhouette } from './heat.ts';
|
|
162
|
+
import { boneHeat, type HeatDust, heatInfluences, HEAT_LIMITS, heatRow, type HeatRow, latticeSilhouette, tracedSilhouette } from './heat.ts';
|
|
163
163
|
import { TURN_BONE, type TurnSpec, turnConstraintName, turnFractions, turnTwin } from './turn.ts';
|
|
164
164
|
import { DISTANCE_EXPONENT, type FloorForm, type Influence, influences, type InfluenceLimits, type MeshInfluences, meshLimits, type Segment } from './weights.ts';
|
|
165
165
|
|
|
@@ -650,6 +650,10 @@ export interface LatticeMeshReport {
|
|
|
650
650
|
exponent?: number;
|
|
651
651
|
/** The cap and floor form the mesh declared, as declared (issue #284); absent when it declared none and ran at 4 and the cut. */
|
|
652
652
|
influences?: MeshInfluences;
|
|
653
|
+
/** The `stray` the mesh declared (issue #303); absent when it declared none. */
|
|
654
|
+
stray?: number;
|
|
655
|
+
/** Only beside a declared `stray` (issue #303): the islands it left out of the lattice, their art px, and the part's art px. */
|
|
656
|
+
stray_cleared?: StrayCleared;
|
|
653
657
|
/** Only under `rule: "heat"` (issue #161): the rule and the solve it ran. Absent otherwise, so a distance mesh writes the row it always wrote. */
|
|
654
658
|
heat?: HeatRow;
|
|
655
659
|
}
|
|
@@ -1745,7 +1749,11 @@ export function buildRig(
|
|
|
1745
1749
|
meshReport.push(row.report);
|
|
1746
1750
|
continue;
|
|
1747
1751
|
}
|
|
1748
|
-
const
|
|
1752
|
+
const allArt = alphaAbove(img, ART_ALPHA);
|
|
1753
|
+
// issue #303: under a declared `stray`, the islands it leaves out are cleared from the art the lattice and its heat
|
|
1754
|
+
// silhouette are built over; absent, `art` is `allArt` itself and nothing downstream moves.
|
|
1755
|
+
const stray = mesh.stray === undefined ? null : latticeStray(allArt, mesh.stray);
|
|
1756
|
+
const art = stray === null ? allArt : stray.art;
|
|
1749
1757
|
const lm = latticeMesh(p.name, art, mesh.grid, maxLoopPasses);
|
|
1750
1758
|
if ('code' in lm) {
|
|
1751
1759
|
problems.push(lm);
|
|
@@ -1755,7 +1763,12 @@ export function buildRig(
|
|
|
1755
1763
|
const ox = p.x - PAD;
|
|
1756
1764
|
const oy = p.y - PAD;
|
|
1757
1765
|
// issue #161: under rule "heat", the field over the lattice's art (alpha above 8) with its holes filled.
|
|
1758
|
-
|
|
1766
|
+
// issue #303: an island no bone reaches is worded against the lattice's own `stray` when it is dust.
|
|
1767
|
+
const dust = (): HeatDust => {
|
|
1768
|
+
const read = stray ?? latticeStray(allArt, 0);
|
|
1769
|
+
return { field: `config.meshes.${p.name}.stray`, declared: mesh.stray ?? null, artPixels: read.artPixels, largest: read.largest };
|
|
1770
|
+
};
|
|
1771
|
+
const heat = mesh.rule === 'heat' ? boneHeat(`config.meshes.${p.name}`, latticeSilhouette(art), segs, ox, oy, HEAT_LIMITS, dust) : null;
|
|
1759
1772
|
if (Array.isArray(heat)) {
|
|
1760
1773
|
problems.push(...heat);
|
|
1761
1774
|
continue;
|
|
@@ -1803,10 +1816,11 @@ export function buildRig(
|
|
|
1803
1816
|
bones: [...new Set(segs.map((s) => s.bone))].sort(),
|
|
1804
1817
|
max_influences: maxInfl,
|
|
1805
1818
|
mean_influences: pyRound(infl / lm.vertices.length, 2),
|
|
1806
|
-
art_coverage: pyRound(artCoverage(lm,
|
|
1819
|
+
art_coverage: pyRound(artCoverage(lm, allArt), 5),
|
|
1807
1820
|
grid: mesh.grid,
|
|
1808
1821
|
...(mesh.exponent === undefined ? {} : { exponent: mesh.exponent }),
|
|
1809
1822
|
...(mesh.influences === undefined ? {} : { influences: mesh.influences }),
|
|
1823
|
+
...(stray === null ? {} : { stray: mesh.stray as number, stray_cleared: { islands: stray.islands, pixels: stray.pixels, art_pixels: stray.artPixels } }),
|
|
1810
1824
|
...(heat === null ? {} : { heat: heatRow(heat) }),
|
|
1811
1825
|
});
|
|
1812
1826
|
}
|