rig-parts 1.62.0 → 1.64.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 +3 -0
- package/docs/AUTHORING.md +88 -5
- package/docs/STABILITY.md +6 -3
- package/package.json +2 -2
- package/skills/rig-parts/SKILL.md +1 -1
- package/src/check.ts +257 -18
- package/src/config.ts +120 -27
- package/src/diagnostics.ts +2 -2
- package/src/explicitmesh.ts +108 -12
- package/src/limblayout.ts +53 -11
- package/src/propose.ts +2 -2
- package/src/rig.ts +89 -17
- package/src/scene.ts +6 -2
- package/src/structure.ts +2 -2
package/README.md
CHANGED
|
@@ -546,9 +546,12 @@ bun run typecheck # tsc --noEmit, strict
|
|
|
546
546
|
bun run lint # one rule: no explicit any
|
|
547
547
|
bun run fetch-examples # the public examples' paintings and layers, into examples/*/inputs (needs a network); each file held to its blob id at the pinned commit, a stale one replaced by name
|
|
548
548
|
bun run selftest # every gate's controls; with the examples fetched, build on each of them against its expected/
|
|
549
|
+
bun run selftest -- --summary-json summary.json # also write rig-selftest-summary/1 (per-suite PASS/FAIL/SKIP counts, holes, partial flag and exit)
|
|
549
550
|
bun run smoke # pack, install into an empty directory, run from the install (needs a network)
|
|
550
551
|
```
|
|
551
552
|
|
|
553
|
+
`bun run selftest -- --summary-json <file>` writes a machine-readable summary (`spec: "rig-selftest-summary/1"`, `tool: "rig-parts"`) derived from the same `RunTally` that prints the console summary: `exit` (`0 | 1 | 2`), `partial` (`boolean`), `only` (`string[] | null`), `faults`, `totals` (`suites`, `ran`, `skipped_suites`, `controls`, `passed`, `failed`, `skipped`, `holes`), per-suite `suites` entries (`suite`, `ran`, `controls`, `passed`, `failed`, `skipped`, `status`, `reason`), `skips` (`{ suite, name, reason, hole }`), and `holes` (`{ suite, name, reason }`). See [AUTHORING §8](docs/AUTHORING.md#machine-readable-selftest-summary---summary-json-issue-725) for the full shape.
|
|
554
|
+
|
|
552
555
|
The generated `foot-ownership` suite can be run alone with
|
|
553
556
|
`bun run selftest -- --only foot-ownership` (exit 2 means a green partial run,
|
|
554
557
|
not a full-tree verdict). Issue #638's revised acceptance requires zero foreign-leg
|
package/docs/AUTHORING.md
CHANGED
|
@@ -726,8 +726,10 @@ with the selected lower one. A target limb is its whole exclusive block; a targe
|
|
|
726
726
|
part uses its actual generated slot family, including a still piece, without
|
|
727
727
|
suffix matching. A requested segment or target with mixed bindings is refused.
|
|
728
728
|
|
|
729
|
-
|
|
730
|
-
|
|
729
|
+
Several placements may share one stationary target (a star); between same-side
|
|
730
|
+
co-movers base draw order decides, and a depth-driven order among them is not
|
|
731
|
+
provided. Self, reciprocal, chained, shared-mover, mover-as-target, and partially
|
|
732
|
+
overlapping target conflicts name both declaration paths and the shared slot; no
|
|
731
733
|
constraint solver chooses a winner. Internal block order and all unselected
|
|
732
734
|
slots' mutual order are stable. Declaring placement may change visible occlusion;
|
|
733
735
|
assembly's part/PNG/recomposite order and every setup seam bar remain unchanged,
|
|
@@ -1014,7 +1016,7 @@ still judges the mesh before the rig stage publishes anything.
|
|
|
1014
1016
|
|
|
1015
1017
|
`segments` is required in this mode too, but names bones directly: first the
|
|
1016
1018
|
accepted slot bone, then any other weighted bones. All weight bones must be
|
|
1017
|
-
listed. A declared stopped link may be named by its emitted `<link>_d` name; no other unknown name resolves by suffix. Chain shorthand and coordinate segments are refused here. These names
|
|
1019
|
+
listed. A declared stopped link may be named by its emitted `<link>_d` name; no other unknown name resolves by suffix (`RIG_EXPLICIT_BINDING` refuses a raw stopped link that `stops` would rewrite). When the part is also in `turn.depths` (issue #740), state `segments`, `weights` and any island `bone` in those pre-turn bone names; `turn` wraps them into `turn_<part>_<bone>` at zero offset (`x: 0, y: 0`) after the stop-binding check, leaving the bone-local bind coordinates `(x, y, weight)` unchanged. Chain shorthand and coordinate segments are refused here. These names
|
|
1018
1020
|
are bindings, not a recipe to generate geometry. Do not supply `r`, `grid`,
|
|
1019
1021
|
`contour`, `auto`, `influence`, `exponent`, `rule`, `ribs`, `influences` or `stray`
|
|
1020
1022
|
beside `explicit` (`CONFIG_EXPLICIT_MODE`). `gather: false` retains its usual
|
|
@@ -1044,6 +1046,21 @@ influence counts and art coverage and records the verified guards. It does not
|
|
|
1044
1046
|
carry an earlier generator's report. Consumer provenance, formatting and files
|
|
1045
1047
|
written by other tools are not reproduced by declaring a mesh payload.
|
|
1046
1048
|
|
|
1049
|
+
When an accepted rig splits one part across island slots (`<part>` and
|
|
1050
|
+
`<part>__island2`, `<part>__island3`, …; issue #734), state `explicit.islands`
|
|
1051
|
+
(at least two entries) in place of top-level `uvs`, `triangles`, `hull` and
|
|
1052
|
+
`weights` (`CONFIG_EXPLICIT_ISLANDS`). Entry `i` (`0`-based) carries `slot`
|
|
1053
|
+
(`"<part>"` at `i = 0`, `"<part>__island<i+1>"` at `i >= 1`;
|
|
1054
|
+
`CONFIG_EXPLICIT_ISLAND_SLOT`), optional `bone` (defaulting to `segments[0]`;
|
|
1055
|
+
must be declared in `segments`), optional `before` on `i >= 1` (the base part
|
|
1056
|
+
slot in `rig.json` to splice `<part>__island<i+1>` immediately before; when
|
|
1057
|
+
omitted, `<part>__island<i+1>` sits immediately after the part's previous
|
|
1058
|
+
island slot), and that island's `uvs`, `triangles`, `hull` and `weights`. On
|
|
1059
|
+
`<part>__island2` and later, `rig` writes `"path": "<part>"` alongside
|
|
1060
|
+
`"image": "<part>.png"` so the slot name and atlas region stay distinct, and
|
|
1061
|
+
refuses when the declared island count disagrees with the non-dust 4-connected
|
|
1062
|
+
art islands in `parts/<part>.png` (`RIG_EXPLICIT_ISLAND_COUNT`).
|
|
1063
|
+
|
|
1047
1064
|
### The weight cut (`meshes.<part>.influences`, issue #284)
|
|
1048
1065
|
|
|
1049
1066
|
This section and the mesh sections after it are for parts with no joints; a limb is cut at
|
|
@@ -1738,6 +1755,8 @@ exactly a value rig-c accepts, a field the mode does not have (`CONFIG_FIELD_PRE
|
|
|
1738
1755
|
| <a id="CONFIG_EXPLICIT_MODE"></a>`CONFIG_EXPLICIT_MODE` | a generator field beside `explicit`; remove it to transfer the accepted payload without generation |
|
|
1739
1756
|
| <a id="CONFIG_EXPLICIT_HASH"></a>`CONFIG_EXPLICIT_HASH` | an explicit image or region guard is not 64 lowercase hexadecimal SHA-256 digits; derive both from the immutable accepted input as stated in §3 |
|
|
1740
1757
|
| <a id="CONFIG_EXPLICIT_SHAPE"></a>`CONFIG_EXPLICIT_SHAPE` | unpaired UVs, non-triple or out-of-range triangle indices, a hull past the vertex count or a weights row count unlike the vertices; transfer the whole accepted payload together |
|
|
1758
|
+
| <a id="CONFIG_EXPLICIT_ISLANDS"></a>`CONFIG_EXPLICIT_ISLANDS` | `explicit.islands` declared beside top-level `uvs`, `triangles`, `hull` or `weights`, or with fewer than 2 entries, or `before` declared on island 1 (`<part>`); state either a single-slot payload or at least two `islands` entries |
|
|
1759
|
+
| <a id="CONFIG_EXPLICIT_ISLAND_SLOT"></a>`CONFIG_EXPLICIT_ISLAND_SLOT` | `explicit.islands[i].slot` is not `"<part>"` for `i = 0` or `"<part>__island<i+1>"` for `i >= 1` |
|
|
1741
1760
|
| <a id="CONFIG_MESH_MODE"></a>`CONFIG_MESH_MODE` | two of `grid`, `contour`, `auto`, `influence` and `explicit`, or no mode at all |
|
|
1742
1761
|
| <a id="CONFIG_REGION_NAME_UNIQUE"></a>`CONFIG_REGION_NAME_UNIQUE` | a region named twice in one mesh's `regions` (or `bust_r` / `bust_l` declared in `contour.regions` alongside `contour.bust`) — every one named in one run |
|
|
1743
1762
|
| <a id="CONFIG_REGION_BAND_NEEDS_BONE"></a><a id="CONFIG_REGION_BONE_NEEDS_BAND"></a>`CONFIG_REGION_BAND_NEEDS_BONE`, `CONFIG_REGION_BONE_NEEDS_BAND` | a region with a `band` and no `bone`, or a `bone` and no `band`: write both for a region that weights its bone, neither for a density-only region (issue #155) |
|
|
@@ -3819,12 +3838,13 @@ says why each requirement exists and what breaks without it.
|
|
|
3819
3838
|
| <a id="RIG_SET_FRONT_BOTH"></a>`RIG_SET_FRONT_BOTH` | a `parts.json` alternate record (issue #664) maps both a part's slot and its front slot `<part>_front`; the detail names the set and the part | re-run `assemble`, which maps one or the other |
|
|
3820
3839
|
| <a id="RIG_LAYOUT_NONEXCLUSIVE"></a>`RIG_LAYOUT_NONEXCLUSIVE` | a requested segment family/target, or an accepted replay's bindings, span owners or leave the selected segment scope across base/alternate attachments | correct the named bindings or scope; mixed ownership is never forced and no stale proof is emitted |
|
|
3821
3840
|
| <a id="RIG_LAYOUT_UNREPRESENTABLE"></a>`RIG_LAYOUT_UNREPRESENTABLE` | the named slot family is absent, a target is not contiguous, the accepted slot population/adjacency changed, or final rig text is stale | use an emitted contiguous target and the exact final accepted source; unrelated slots are not absorbed |
|
|
3822
|
-
| <a id="RIG_LAYOUT_CONFLICT"></a>`RIG_LAYOUT_CONFLICT` | layout declarations share a slot, target themselves,
|
|
3841
|
+
| <a id="RIG_LAYOUT_CONFLICT"></a>`RIG_LAYOUT_CONFLICT` | layout declarations share a moving slot, use a moving slot as a target, target themselves, form a reciprocal/chained pair, or partially overlap a shared target block; both field paths are printed | move each participating slot in at most one placement, and share only a stationary target block |
|
|
3823
3842
|
| <a id="RIG_STILL_ROW_INSIDE_PART"></a><a id="RIG_STILL_ROW_CLEAR"></a><a id="RIG_STILL_PIECES_HAVE_ART"></a><a id="RIG_STILL_NAME_FREE"></a>`RIG_STILL_ROW_INSIDE_PART`, `RIG_STILL_ROW_CLEAR`, `RIG_STILL_PIECES_HAVE_ART`, `RIG_STILL_NAME_FREE` | a `motion.blink.still` row that is not strictly inside the part, that crosses art (a cut through art changes the render even at rest), that leaves one piece with no art, or whose `<part>_still` slot name another part already has | that entry's `row` — a row with no art between the crease and the lash line — or rename the other part |
|
|
3824
3843
|
| <a id="RIG_SET_COVERS_BONE"></a>`RIG_SET_COVERS_BONE` | `assemble.alternates[i].drawn.<bone>` (issue #476): a subset set in `parts.json` `alternates` (`parts` present) states a `drawn` world angle for a bone none of its covered slots ride; the detail names the bone, the set, and the covered bones | remove the uncovered bone from that set's `drawn`, or add the part that rides it to `parts` |
|
|
3825
|
-
| <a id="RIG_EXPLICIT_BINDING"></a>`RIG_EXPLICIT_BINDING` | the declared stops
|
|
3844
|
+
| <a id="RIG_EXPLICIT_BINDING"></a>`RIG_EXPLICIT_BINDING` | the declared stops would change the accepted payload's bindings | transfer the accepted final emitted bone names and local coordinates, or use a generated mesh; no normalization is applied |
|
|
3826
3845
|
| <a id="RIG_EXPLICIT_IMAGE_HASH"></a>`RIG_EXPLICIT_IMAGE_HASH` | the decoded part RGBA differs from `explicit.image_sha256`; both digests are named | restore the accepted image or transfer its matching accepted payload and guard; nothing is written |
|
|
3827
3846
|
| <a id="RIG_EXPLICIT_REGION_HASH"></a>`RIG_EXPLICIT_REGION_HASH` | the canvas or placed part rectangle differs from `explicit.region_sha256`; both digests are named | restore the accepted region or transfer a payload bound to this placement; nothing is written |
|
|
3847
|
+
| <a id="RIG_EXPLICIT_ISLAND_COUNT"></a>`RIG_EXPLICIT_ISLAND_COUNT` | `explicit.islands.length` differs from the non-dust 4-connected art island count of `parts/<part>.png`; the missing or extra `<part>__island<N>` slot names and island pixel counts are named | supply one `explicit.islands` entry per non-dust art island (`<part>`, `<part>__island2`, …); nothing is written |
|
|
3828
3848
|
| <a id="RIG_EXPLICIT_ALTERNATE"></a>`RIG_EXPLICIT_ALTERNATE` | an alternate set replaces a part bound to one accepted explicit payload | use a generated mesh for alternate artwork; no alternate is inferred from a base payload |
|
|
3829
3849
|
| <a id="RIG_INFLUENCE_REACH"></a>`RIG_INFLUENCE_REACH` | an influence mesh's `radius` is at or under the distance from its farthest lattice vertex to that vertex's nearest link (issue #378): a link pulls only when it is nearer than `radius`, so that vertex would be pulled by none. The vertex, where it is and the distance are named | a `radius` above the distance named, or a segment nearer that part of the art |
|
|
3830
3850
|
| <a id="RIG_INFLUENCE_LATTICE"></a>`RIG_INFLUENCE_LATTICE` | rig-c's segments lattice refused the part at the declared `cell` (no pixel of the part holds art, or the cell is not a whole number of at least 1); rig-c's message is quoted | a part with art, or a whole `cell` of at least 1 |
|
|
@@ -5084,3 +5104,66 @@ machine under a load average of 5 to 11, the difference in user + sys was +5.6 a
|
|
|
5084
5104
|
the load moves the same build by (the pre-change reference-demo build alone took 43.6 to
|
|
5085
5105
|
45.8 s there). Since issue #123 that build and render are gone again — the still regions read the
|
|
5086
5106
|
idle's geometry — and the `build` row was not re-measured for it either.
|
|
5107
|
+
|
|
5108
|
+
### Machine-readable selftest summary (`--summary-json`, issue #725)
|
|
5109
|
+
|
|
5110
|
+
`bun run selftest -- --summary-json <file>` writes a JSON summary beside the normal console output
|
|
5111
|
+
so automated lanes and reports read the run's own `RunTally` rather than regex-parsing logs. It is
|
|
5112
|
+
supported on full runs, partial `--only` runs, and `--merge` runs (and refused with exit `2` when
|
|
5113
|
+
missing a path, given twice, or combined with `--shard`):
|
|
5114
|
+
|
|
5115
|
+
```json
|
|
5116
|
+
{
|
|
5117
|
+
"spec": "rig-selftest-summary/1",
|
|
5118
|
+
"tool": "rig-parts",
|
|
5119
|
+
"exit": 0,
|
|
5120
|
+
"partial": false,
|
|
5121
|
+
"only": null,
|
|
5122
|
+
"faults": [],
|
|
5123
|
+
"totals": {
|
|
5124
|
+
"suites": 63,
|
|
5125
|
+
"ran": 62,
|
|
5126
|
+
"skipped_suites": 1,
|
|
5127
|
+
"controls": 2250,
|
|
5128
|
+
"passed": 2250,
|
|
5129
|
+
"failed": 0,
|
|
5130
|
+
"skipped": 1,
|
|
5131
|
+
"holes": 1
|
|
5132
|
+
},
|
|
5133
|
+
"suites": [
|
|
5134
|
+
{
|
|
5135
|
+
"suite": "png",
|
|
5136
|
+
"ran": true,
|
|
5137
|
+
"controls": 2,
|
|
5138
|
+
"passed": 2,
|
|
5139
|
+
"failed": 0,
|
|
5140
|
+
"skipped": 0,
|
|
5141
|
+
"status": "pass",
|
|
5142
|
+
"reason": null
|
|
5143
|
+
}
|
|
5144
|
+
],
|
|
5145
|
+
"skips": [
|
|
5146
|
+
{
|
|
5147
|
+
"suite": "corpus",
|
|
5148
|
+
"name": "corpus",
|
|
5149
|
+
"reason": "no --corpus <dir>, no RIG_PARTS_CORPUS (or SPINE_PARTS_CORPUS) and no fetched examples/*/inputs",
|
|
5150
|
+
"hole": true
|
|
5151
|
+
}
|
|
5152
|
+
],
|
|
5153
|
+
"holes": [
|
|
5154
|
+
{
|
|
5155
|
+
"suite": "corpus",
|
|
5156
|
+
"name": "corpus",
|
|
5157
|
+
"reason": "no --corpus <dir>, no RIG_PARTS_CORPUS (or SPINE_PARTS_CORPUS) and no fetched examples/*/inputs"
|
|
5158
|
+
}
|
|
5159
|
+
]
|
|
5160
|
+
}
|
|
5161
|
+
```
|
|
5162
|
+
|
|
5163
|
+
- Every count in `totals` and `suites` is read from the same `RunTally` instance that prints the
|
|
5164
|
+
console summary (`RT15`).
|
|
5165
|
+
- Only two-space-indented `SKIP` gutter lines enter `skips`, and only those followed by a
|
|
5166
|
+
`This is a HOLE in this run` continuation line (or a suite returning `null` outside `--only`)
|
|
5167
|
+
enter `holes`; check names or indented detail lines containing `HOLE` or `SKIP` (such as
|
|
5168
|
+
`RG82_BLINK_NO_HOLE_…` or `SO21_ZERO_SETUP_BANDS_REMAIN_NAMED_SKIPS_…`) are counted as `PASS` or
|
|
5169
|
+
`FAIL` controls and never as `skips` or `holes` (`RT16`, `RT17`).
|
package/docs/STABILITY.md
CHANGED
|
@@ -143,7 +143,8 @@ part name, `[i]` an entry of a list):
|
|
|
143
143
|
- **stops[i]** — required: `chain`, `against`, `at`, `inward`, `outward`; optional: `links`, `lower` (issue #404)
|
|
144
144
|
- **stops[i].lower** — required: `inward`, `outward`
|
|
145
145
|
- **meshes.<part>** — exactly one of `grid`, `contour`, `auto`, `influence`, `explicit`; required in every mode: `segments`, and `r` in generated modes other than `influence`; `explicit` refuses generation fields and uses direct bone names in `segments`, the first the slot bone; optional in every mode: `exponent`, `rule`, `gather` (issue #680, false only: preserve an exclusive non-segment rider's authored draw position without changing its binding; absent, output bytes unchanged); 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)
|
|
146
|
-
- **meshes.<part>.explicit** — required: `image_sha256`, `region_sha256`, `uvs`, `triangles`, `hull`, `weights` (issue #658); direct payload numbers are transferred without normalization; weights require `bone`, `x`, `y`, `weight`. Image SHA-256 is over decoded unpadded RGBA bytes; region SHA-256 is over JSON.stringify([rig width, rig height, part x, part y, part w, part h]). Both must match before output. All weighted bones must occur in segments; declared stopped links may use their emitted `<link>_d` names. Other unknown names remain refused. Alternate artwork for this part is refused; use a generated mesh for it.
|
|
146
|
+
- **meshes.<part>.explicit** — required: `image_sha256`, `region_sha256`, and either `uvs`, `triangles`, `hull`, `weights` (single-slot, issue #658) or `islands` (at least two island entries, issue #734); direct payload numbers are transferred without normalization; weights require `bone`, `x`, `y`, `weight`. Image SHA-256 is over decoded unpadded RGBA bytes; region SHA-256 is over JSON.stringify([rig width, rig height, part x, part y, part w, part h]). Both must match before output, and when `islands` is declared its entry count must match the part's non-dust 4-connected art island count. All weighted and island slot bones must occur in segments; declared stopped links may use their emitted `<link>_d` names. Other unknown names remain refused. Alternate artwork for this part is refused; use a generated mesh for it.
|
|
147
|
+
- **meshes.<part>.explicit.islands[i]** — required: `slot` (`<part>` at index 0, `<part>__island<i+1>` at index i >= 1), `uvs`, `triangles`, `hull`, `weights`; optional: `bone` (defaults to `segments[0]`), `before` (index i >= 1 only: the base part slot in `rig.json` to splice `<part>__island<i+1>` immediately before; absent, immediately after the part's previous island slot)
|
|
147
148
|
- **meshes.<part>.influence** — required: `cell`, `radius`, `across`, `max`, `shape` (issue #378); optional: `hold_tip` (issue #615, the value true only); beside it `r`, `exponent`, `rule`, `influences`, `stray` and `ribs` are refused by name
|
|
148
149
|
- **meshes.<part>.influences** — optional, at least one: `maxInfluences` (a whole number, 1 or more, or null for no cap), `floor` ("cut" or "continuous")
|
|
149
150
|
- **meshes.<part>.ribs.<chain>** — required: `stations`
|
|
@@ -289,8 +290,10 @@ in the package.
|
|
|
289
290
|
|
|
290
291
|
## rig-c
|
|
291
292
|
|
|
292
|
-
`dependencies` takes `rig-c` `^2.69.0`: 2.69.0 or any later 2.x. Bun 1.
|
|
293
|
-
|
|
293
|
+
`dependencies` takes `rig-c` `^2.69.0`: 2.69.0 or any later 2.x. Bun 1.4 or later
|
|
294
|
+
is the declared floor (`engines.bun` `>=1.4.0`; older 1.4 releases are not tested in
|
|
295
|
+
CI, which runs `latest`, and compatibility with an older 1.4 release is handled when
|
|
296
|
+
reported as an issue). What this package relies on in rig-c, every import
|
|
294
297
|
read off `src/` and `cli.ts`:
|
|
295
298
|
|
|
296
299
|
- the named entries `rig-c/mesh` (the outline functions, `measureAuthoredMeshFit`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.64.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": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"spine-parts": "./bin/rig-parts.cjs"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
|
-
"bun": ">=1.
|
|
11
|
+
"bun": ">=1.4.0"
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "firejune",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rig-parts
|
|
3
3
|
description: Turn one anime character painting and its See-through layer decomposition into a verified 2D character rig (Spine 4.3 skeleton data) — measured parts, weighted meshes, bone chains and a looping idle — with rig-parts, which gates everything through rig-c before it is written. Use for a request to rig or animate a single character painting, to check a painting before See-through (rig-parts painting), to run or read rig-parts layers, sheet, assemble, propose, rig, check, loop or build, or to write or fix a rig-parts config.json. Not for cutting art into parts by hand, for authoring a rig from loose PNGs (that is the rigc skill), for lip-sync or swapping a part's image, or for running See-through itself.
|
|
4
4
|
license: MIT
|
|
5
|
-
compatibility: Requires Bun 1.
|
|
5
|
+
compatibility: Requires Bun 1.4 or later (declared in engines.bun >=1.4.0; CI tests latest only). The tool is the npm package @rig-c/parts (npm i -D @rig-c/parts, or bun add -d @rig-c/parts), whose command is rig-parts; rig-c is installed with it. See-through is run separately.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# rig-parts — a painting to a Spine rig, for agents
|
package/src/check.ts
CHANGED
|
@@ -164,7 +164,10 @@ import {
|
|
|
164
164
|
import { RIG_BONE_INHERIT, type RigSpec as RigcRigSpec } from 'rig-c/src/rig.ts';
|
|
165
165
|
import { expandStops } from 'rig-c/src/stop.ts';
|
|
166
166
|
import { ENVIRONMENT_SPEC, EnvironmentError, environmentResponse, readEnvironmentSchedule } from 'rig-c/src/environment.ts';
|
|
167
|
+
import { ART_ALPHA } from './mesh.ts';
|
|
168
|
+
import { connectedComponents } from './raster/components.ts';
|
|
167
169
|
import { alphaComposite } from './raster/composite.ts';
|
|
170
|
+
import { alphaAbove } from './raster/index.ts';
|
|
168
171
|
import { readPng, writePng } from './raster/png.ts';
|
|
169
172
|
import { newFloatImage, newRaster, type Raster } from './raster/types.ts';
|
|
170
173
|
import type { BaseTag } from './tags.ts';
|
|
@@ -1240,16 +1243,180 @@ export interface SeamFigures {
|
|
|
1240
1243
|
map: { sx: number; sy: number; tx: number; ty: number };
|
|
1241
1244
|
}
|
|
1242
1245
|
|
|
1246
|
+
/**
|
|
1247
|
+
* Partition a multi-island part's raster into one raster per declared island
|
|
1248
|
+
* slot (`<part>`, `<part>__island2..<part>__island<N>`), ordered by descending
|
|
1249
|
+
* `alpha > ART_ALPHA` 4-connected component area (tie-broken by ascending
|
|
1250
|
+
* row-major label). Anti-aliased fringe pixels (`1 <= alpha <= ART_ALPHA`)
|
|
1251
|
+
* propagate from their connected island seed via 4-connected BFS over
|
|
1252
|
+
* `alpha > 0`, with any disconnected sub-threshold pixel assigned to the
|
|
1253
|
+
* nearest seed centroid so every non-transparent pixel of `image` lands in
|
|
1254
|
+
* exactly one island raster.
|
|
1255
|
+
*/
|
|
1256
|
+
export function splitPartIslands(image: Raster, count: number): Raster[] {
|
|
1257
|
+
if (count <= 1) return [image];
|
|
1258
|
+
const { width, height, data } = image;
|
|
1259
|
+
const cc = connectedComponents(alphaAbove(image, ART_ALPHA), 4);
|
|
1260
|
+
const seeds = cc.stats
|
|
1261
|
+
.slice(1)
|
|
1262
|
+
.sort((a, b) => b.area - a.area || a.label - b.label)
|
|
1263
|
+
.slice(0, count);
|
|
1264
|
+
if (seeds.length <= 1) return [image];
|
|
1265
|
+
const owner = new Int16Array(width * height).fill(-1);
|
|
1266
|
+
const labelToSeed = new Int16Array(cc.count + 1).fill(-1);
|
|
1267
|
+
seeds.forEach((s, idx) => {
|
|
1268
|
+
labelToSeed[s.label] = idx;
|
|
1269
|
+
});
|
|
1270
|
+
const queue = new Int32Array(width * height);
|
|
1271
|
+
let head = 0;
|
|
1272
|
+
let tail = 0;
|
|
1273
|
+
for (let i = 0; i < width * height; i++) {
|
|
1274
|
+
const seedIdx = labelToSeed[cc.labels[i]];
|
|
1275
|
+
if (seedIdx >= 0) {
|
|
1276
|
+
owner[i] = seedIdx;
|
|
1277
|
+
queue[tail++] = i;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
while (head < tail) {
|
|
1281
|
+
const cur = queue[head++];
|
|
1282
|
+
const seedIdx = owner[cur];
|
|
1283
|
+
const x = cur % width;
|
|
1284
|
+
const y = (cur - x) / width;
|
|
1285
|
+
if (x > 0) {
|
|
1286
|
+
const n = cur - 1;
|
|
1287
|
+
if (owner[n] < 0 && data[n * 4 + 3] > 0) {
|
|
1288
|
+
owner[n] = seedIdx;
|
|
1289
|
+
queue[tail++] = n;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (x + 1 < width) {
|
|
1293
|
+
const n = cur + 1;
|
|
1294
|
+
if (owner[n] < 0 && data[n * 4 + 3] > 0) {
|
|
1295
|
+
owner[n] = seedIdx;
|
|
1296
|
+
queue[tail++] = n;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
if (y > 0) {
|
|
1300
|
+
const n = cur - width;
|
|
1301
|
+
if (owner[n] < 0 && data[n * 4 + 3] > 0) {
|
|
1302
|
+
owner[n] = seedIdx;
|
|
1303
|
+
queue[tail++] = n;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (y + 1 < height) {
|
|
1307
|
+
const n = cur + width;
|
|
1308
|
+
if (owner[n] < 0 && data[n * 4 + 3] > 0) {
|
|
1309
|
+
owner[n] = seedIdx;
|
|
1310
|
+
queue[tail++] = n;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const out = Array.from({ length: count }, () => newRaster(width, height));
|
|
1315
|
+
for (let y = 0; y < height; y++) {
|
|
1316
|
+
for (let x = 0; x < width; x++) {
|
|
1317
|
+
const i = y * width + x;
|
|
1318
|
+
const p = i * 4;
|
|
1319
|
+
if (data[p + 3] === 0) continue;
|
|
1320
|
+
let idx = owner[i];
|
|
1321
|
+
if (idx < 0) {
|
|
1322
|
+
let best = 0;
|
|
1323
|
+
let bestD2 = Infinity;
|
|
1324
|
+
seeds.forEach((s, si) => {
|
|
1325
|
+
const dx = x - s.cx;
|
|
1326
|
+
const dy = y - s.cy;
|
|
1327
|
+
const d2 = dx * dx + dy * dy;
|
|
1328
|
+
if (d2 < bestD2) {
|
|
1329
|
+
bestD2 = d2;
|
|
1330
|
+
best = si;
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
idx = best;
|
|
1334
|
+
}
|
|
1335
|
+
out[idx].data.set(data.subarray(p, p + 4), p);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return out;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
export interface ReferencePlacement {
|
|
1342
|
+
name: string;
|
|
1343
|
+
png: string;
|
|
1344
|
+
box: PartRecord;
|
|
1345
|
+
islandIndex?: number;
|
|
1346
|
+
islandCount?: number;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function partIslandSlots(part: string, slotOrder?: readonly string[]): string[] {
|
|
1350
|
+
if (slotOrder === undefined || !slotOrder.includes(`${part}__island2`)) return [part];
|
|
1351
|
+
const out = [part];
|
|
1352
|
+
for (let k = 2; slotOrder.includes(`${part}__island${k}`); k++) {
|
|
1353
|
+
out.push(`${part}__island${k}`);
|
|
1354
|
+
}
|
|
1355
|
+
return out;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
export function resolvePlacedAlpha(placements: readonly ReferencePlacement[], partsDir: string): PlacedAlpha[] {
|
|
1359
|
+
const rawCache = new Map<string, Raster>();
|
|
1360
|
+
const splitCache = new Map<string, Raster[]>();
|
|
1361
|
+
const readRaw = (png: string): Raster => {
|
|
1362
|
+
const hit = rawCache.get(png);
|
|
1363
|
+
if (hit !== undefined) return hit;
|
|
1364
|
+
const img = readPng(join(partsDir, `${png}.png`));
|
|
1365
|
+
rawCache.set(png, img);
|
|
1366
|
+
return img;
|
|
1367
|
+
};
|
|
1368
|
+
return placements.map((pl) => {
|
|
1369
|
+
const raw = readRaw(pl.png);
|
|
1370
|
+
if (pl.islandCount !== undefined && pl.islandCount > 1 && pl.islandIndex !== undefined) {
|
|
1371
|
+
const key = `${pl.png}:${pl.islandCount}`;
|
|
1372
|
+
let islands = splitCache.get(key);
|
|
1373
|
+
if (islands === undefined) {
|
|
1374
|
+
islands = splitPartIslands(raw, pl.islandCount);
|
|
1375
|
+
splitCache.set(key, islands);
|
|
1376
|
+
}
|
|
1377
|
+
return { name: pl.name, from: pl.box.from, x: pl.box.x, y: pl.box.y, image: islands[pl.islandIndex] ?? raw };
|
|
1378
|
+
}
|
|
1379
|
+
return { name: pl.name, from: pl.box.from, x: pl.box.x, y: pl.box.y, image: raw };
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
export function basePlacements(parts: PartsFile, slotOrder?: readonly string[]): ReferencePlacement[] {
|
|
1384
|
+
return setPlacements(
|
|
1385
|
+
parts,
|
|
1386
|
+
{
|
|
1387
|
+
name: '',
|
|
1388
|
+
slots: {},
|
|
1389
|
+
records: [],
|
|
1390
|
+
ghost_px: {},
|
|
1391
|
+
recomposite: parts.recomposite ?? {
|
|
1392
|
+
mean_abs: 0,
|
|
1393
|
+
within_limit: 0,
|
|
1394
|
+
within_share: 1,
|
|
1395
|
+
error_limit: 0,
|
|
1396
|
+
error_px: 0,
|
|
1397
|
+
covered_alpha: 0,
|
|
1398
|
+
uncovered_error_px: 0,
|
|
1399
|
+
hole_count: 0,
|
|
1400
|
+
holes_listed: 0,
|
|
1401
|
+
holes: [],
|
|
1402
|
+
},
|
|
1403
|
+
},
|
|
1404
|
+
slotOrder,
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1243
1408
|
/**
|
|
1244
1409
|
* The flat stack of parts over the background, in rig pixels: PIL's
|
|
1245
|
-
* `alpha_composite` of every part at its `x, y`, in parts.json order
|
|
1246
|
-
*
|
|
1410
|
+
* `alpha_composite` of every part at its `x, y`, in parts.json order (with
|
|
1411
|
+
* multi-island explicit slots placed at their declared `slotOrder` positions
|
|
1412
|
+
* when `slotOrder` is supplied), over an opaque canvas of the render's own
|
|
1413
|
+
* background colour.
|
|
1247
1414
|
*/
|
|
1248
|
-
export function flatComposite(parts: PartsFile, partsDir: string, background: readonly [number, number, number, number]): Raster {
|
|
1415
|
+
export function flatComposite(parts: PartsFile, partsDir: string, background: readonly [number, number, number, number], slotOrder?: readonly string[]): Raster {
|
|
1249
1416
|
const [W, H] = parts.rig_size;
|
|
1250
1417
|
let canvas = newRaster(W, H);
|
|
1251
1418
|
for (let i = 0; i < W * H; i++) canvas.data.set(background, i * 4);
|
|
1252
|
-
for (const p of parts
|
|
1419
|
+
for (const p of resolvePlacedAlpha(basePlacements(parts, slotOrder), partsDir)) canvas = alphaComposite(canvas, p.image, p.x, p.y);
|
|
1253
1420
|
return canvas;
|
|
1254
1421
|
}
|
|
1255
1422
|
|
|
@@ -1260,9 +1427,11 @@ export function flatComposite(parts: PartsFile, partsDir: string, background: re
|
|
|
1260
1427
|
* slot (`alt.front`) is drawn from its record although `alt.hides` names it (the
|
|
1261
1428
|
* hide is its base slot's) — with `slotOrder`, the rig's final setup slot names,
|
|
1262
1429
|
* right after the nearest part slot before `<part>_front` there; without it (or
|
|
1263
|
-
* with no such slot), in its own place, as the base slot would draw it.
|
|
1430
|
+
* with no such slot), in its own place, as the base slot would draw it. When
|
|
1431
|
+
* `slotOrder` names multi-island explicit slots (`<part>__island2..<part>__island<N>`),
|
|
1432
|
+
* each island's placement follows its declared slot position in `slotOrder`.
|
|
1264
1433
|
*/
|
|
1265
|
-
export function setPlacements(parts: PartsFile, alt: AlternatePartsRecord, slotOrder?: readonly string[]):
|
|
1434
|
+
export function setPlacements(parts: PartsFile, alt: AlternatePartsRecord, slotOrder?: readonly string[]): ReferencePlacement[] {
|
|
1266
1435
|
const hides = new Set(alt.hides ?? []);
|
|
1267
1436
|
const front = new Set(alt.front ?? []);
|
|
1268
1437
|
const byName = new Map(alt.records.map((r) => [r.name, r]));
|
|
@@ -1281,18 +1450,73 @@ export function setPlacements(parts: PartsFile, alt: AlternatePartsRecord, slotO
|
|
|
1281
1450
|
}
|
|
1282
1451
|
}
|
|
1283
1452
|
const moved = [...after.keys()].sort((a, b) => (slotOrder as readonly string[]).indexOf(`${a}${FRONT_SUFFIX}`) - (slotOrder as readonly string[]).indexOf(`${b}${FRONT_SUFFIX}`));
|
|
1284
|
-
|
|
1453
|
+
type PlacedIslandEntry = ReferencePlacement & { at: number };
|
|
1454
|
+
const inlineIslands = new Map<string, PlacedIslandEntry[]>();
|
|
1455
|
+
const beforeTarget = new Map<string | null, PlacedIslandEntry[]>();
|
|
1456
|
+
if (slotOrder !== undefined) {
|
|
1457
|
+
for (const p of parts.parts) {
|
|
1458
|
+
if (byName.has(p.name)) continue;
|
|
1459
|
+
const islandSlots = partIslandSlots(p.name, slotOrder);
|
|
1460
|
+
if (islandSlots.length <= 1) continue;
|
|
1461
|
+
const baseIdx = slotOrder.indexOf(p.name);
|
|
1462
|
+
for (let k = 1; k < islandSlots.length; k++) {
|
|
1463
|
+
const islSlot = islandSlots[k];
|
|
1464
|
+
if ((hides.has(p.name) && !front.has(p.name)) || hides.has(islSlot)) continue;
|
|
1465
|
+
const at = slotOrder.indexOf(islSlot);
|
|
1466
|
+
let reordered = at < baseIdx;
|
|
1467
|
+
if (!reordered) {
|
|
1468
|
+
for (let j = baseIdx + 1; j < at; j++) {
|
|
1469
|
+
if (partNames.has(slotOrder[j]) && !after.has(slotOrder[j])) {
|
|
1470
|
+
reordered = true;
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
const entry: PlacedIslandEntry = { name: islSlot, png: p.name, box: p, islandIndex: k, islandCount: islandSlots.length, at };
|
|
1476
|
+
if (!reordered) {
|
|
1477
|
+
const list = inlineIslands.get(p.name) ?? [];
|
|
1478
|
+
list.push(entry);
|
|
1479
|
+
inlineIslands.set(p.name, list);
|
|
1480
|
+
} else {
|
|
1481
|
+
let target: string | null = null;
|
|
1482
|
+
for (let j = at + 1; j < slotOrder.length; j++) {
|
|
1483
|
+
if (partNames.has(slotOrder[j]) && !after.has(slotOrder[j])) {
|
|
1484
|
+
target = slotOrder[j];
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const list = beforeTarget.get(target) ?? [];
|
|
1489
|
+
list.push(entry);
|
|
1490
|
+
beforeTarget.set(target, list);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
for (const list of inlineIslands.values()) list.sort((a, b) => a.at - b.at);
|
|
1495
|
+
for (const list of beforeTarget.values()) list.sort((a, b) => a.at - b.at);
|
|
1496
|
+
}
|
|
1497
|
+
const out: ReferencePlacement[] = [];
|
|
1498
|
+
const pushIsland = ({ at: _at, ...pl }: PlacedIslandEntry): void => {
|
|
1499
|
+
out.push(pl);
|
|
1500
|
+
};
|
|
1285
1501
|
const draw = (name: string, base: PartRecord): void => {
|
|
1286
1502
|
if (hides.has(name) && !front.has(name)) return;
|
|
1287
1503
|
const rec = byName.get(name);
|
|
1504
|
+
const islandSlots = rec === undefined ? partIslandSlots(name, slotOrder) : [name];
|
|
1505
|
+
if (islandSlots.length > 1) {
|
|
1506
|
+
out.push({ name, png: name, box: base, islandIndex: 0, islandCount: islandSlots.length });
|
|
1507
|
+
for (const isl of inlineIslands.get(name) ?? []) pushIsland(isl);
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1288
1510
|
out.push({ name, png: rec === undefined ? name : `${alt.name}_${rec.name}`, box: rec ?? base });
|
|
1289
1511
|
};
|
|
1290
1512
|
const baseOf = new Map(parts.parts.map((p) => [p.name, p]));
|
|
1291
1513
|
for (const p of parts.parts) {
|
|
1292
1514
|
if (after.has(p.name)) continue;
|
|
1515
|
+
for (const isl of beforeTarget.get(p.name) ?? []) pushIsland(isl);
|
|
1293
1516
|
draw(p.name, p);
|
|
1294
1517
|
for (const m of moved) if (after.get(m) === p.name) draw(m, baseOf.get(m) as PartRecord);
|
|
1295
1518
|
}
|
|
1519
|
+
for (const isl of beforeTarget.get(null) ?? []) pushIsland(isl);
|
|
1296
1520
|
return out;
|
|
1297
1521
|
}
|
|
1298
1522
|
|
|
@@ -1328,16 +1552,17 @@ export function flatCompositeForSet(parts: PartsFile, alt: AlternatePartsRecord,
|
|
|
1328
1552
|
const [W, H] = parts.rig_size;
|
|
1329
1553
|
let canvas = newRaster(W, H);
|
|
1330
1554
|
for (let i = 0; i < W * H; i++) canvas.data.set(background, i * 4);
|
|
1331
|
-
for (const
|
|
1555
|
+
for (const p of resolvePlacedAlpha(setPlacements(parts, alt, slotOrder), partsDir)) canvas = alphaComposite(canvas, p.image, p.x, p.y);
|
|
1332
1556
|
return canvas;
|
|
1333
1557
|
}
|
|
1334
1558
|
|
|
1335
1559
|
/**
|
|
1336
1560
|
* The setup-pose rig spec with an alternate set applied (issues #454, #465):
|
|
1337
|
-
* hidden slots (and `<slot>_still` for any hidden or
|
|
1338
|
-
* setup attachment, covered slots have `alt.slots[slot]`,
|
|
1339
|
-
* carries `drawn` bone rotations its bones are posed at those
|
|
1340
|
-
* so the throwaway still renders the alternate in the pose it
|
|
1561
|
+
* hidden slots (and `<slot>_still` or `<slot>__island<k>` for any hidden or
|
|
1562
|
+
* covered slot) have no setup attachment, covered slots have `alt.slots[slot]`,
|
|
1563
|
+
* and when the set carries `drawn` bone rotations its bones are posed at those
|
|
1564
|
+
* world rotations so the throwaway still renders the alternate in the pose it
|
|
1565
|
+
* was painted in.
|
|
1341
1566
|
*/
|
|
1342
1567
|
export function stillRigForSet(
|
|
1343
1568
|
rig: Record<string, unknown>,
|
|
@@ -1348,7 +1573,13 @@ export function stillRigForSet(
|
|
|
1348
1573
|
const slots = (Array.isArray(rig.slots) ? rig.slots : []).map((s) => {
|
|
1349
1574
|
if (!isRecord(s) || typeof s.name !== 'string') return s;
|
|
1350
1575
|
const stillBase = s.name.endsWith('_still') ? s.name.slice(0, -'_still'.length) : null;
|
|
1351
|
-
|
|
1576
|
+
const islandMatch = /^(.+)__island[2-9]\d*$/.exec(s.name);
|
|
1577
|
+
const islandBase = islandMatch !== null ? islandMatch[1] : null;
|
|
1578
|
+
if (
|
|
1579
|
+
hides.has(s.name) ||
|
|
1580
|
+
(stillBase !== null && (hides.has(stillBase) || Object.hasOwn(alt.slots, stillBase))) ||
|
|
1581
|
+
(islandBase !== null && (hides.has(islandBase) || Object.hasOwn(alt.slots, islandBase)))
|
|
1582
|
+
) {
|
|
1352
1583
|
return { ...s, attachment: null };
|
|
1353
1584
|
}
|
|
1354
1585
|
if (Object.hasOwn(alt.slots, s.name)) return { ...s, attachment: alt.slots[s.name] };
|
|
@@ -5110,7 +5341,15 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
5110
5341
|
const checkedAlt = set !== undefined && !allSets && inp.parts !== null ? (inp.parts.alternates ?? []).find((a) => a.name === set) : undefined;
|
|
5111
5342
|
const subsetFoldSlots =
|
|
5112
5343
|
checkedAlt !== undefined && checkedAlt.parts !== undefined
|
|
5113
|
-
?
|
|
5344
|
+
? (() => {
|
|
5345
|
+
const baseSet = new Set<string>([...Object.keys(checkedAlt.slots), ...(checkedAlt.hides ?? [])]);
|
|
5346
|
+
for (const sl of (Array.isArray(inp.rig.slots) ? inp.rig.slots : []).filter(isRecord)) {
|
|
5347
|
+
if (typeof sl.name !== 'string') continue;
|
|
5348
|
+
const m = /^(.+)__island[2-9]\d*$/.exec(sl.name);
|
|
5349
|
+
if (m !== null && baseSet.has(m[1])) baseSet.add(sl.name);
|
|
5350
|
+
}
|
|
5351
|
+
return baseSet;
|
|
5352
|
+
})()
|
|
5114
5353
|
: null;
|
|
5115
5354
|
const filterFolds = (map: Map<string, FoldingMesh[]>): Map<string, FoldingMesh[]> => {
|
|
5116
5355
|
if (subsetFoldSlots === null) return map;
|
|
@@ -5526,9 +5765,10 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
5526
5765
|
const canonicalSet = canonical(join(stillDir, 'build'), join(stillDir, 'seam'));
|
|
5527
5766
|
seamViewport = canonicalSet.viewport;
|
|
5528
5767
|
if (keepStills) cpSync(join(stillDir, 'seam'), keptDir, { recursive: true });
|
|
5529
|
-
const
|
|
5768
|
+
const rigSlotOrder = (Array.isArray(inp.rig.slots) ? inp.rig.slots : []).filter(isRecord).map((sl) => String(sl.name));
|
|
5769
|
+
const composite = flatComposite(inp.parts, inp.partsDir, stillSet.background, rigSlotOrder);
|
|
5530
5770
|
const autoSeam = seamFigures(composite, stillSet.frames[0].image, inp.stage, stillSet.viewport, stillSet.background, undefined, inp.parts.scene !== undefined);
|
|
5531
|
-
const placed = inp.parts
|
|
5771
|
+
const placed = resolvePlacedAlpha(basePlacements(inp.parts, rigSlotOrder), inp.partsDir);
|
|
5532
5772
|
const basePopulation = createSeamPopulation(placed, inp.parts.rig_size, canonicalSet.viewport);
|
|
5533
5773
|
seam = seamFigures(composite, canonicalSet.frames[0].image, inp.stage, canonicalSet.viewport, canonicalSet.background, undefined, inp.parts.scene !== undefined, undefined, true, basePopulation.observe);
|
|
5534
5774
|
seamScope = { mode: 'source-grid/base', domain, denominator_px: canonicalDenominator(seam), auto_frame_raw: seamReading(autoSeam, stillSet.viewport), whole: seamReading(seam, canonicalSet.viewport), population: basePopulation.finish() };
|
|
@@ -5547,11 +5787,10 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
5547
5787
|
if (asr.status !== 0) refuseIfAny(rigcFailed(`render (the setup-pose still of set "${alt.name}")`, asr, asr.out.split('\n').filter((l) => l.includes('FAIL'))));
|
|
5548
5788
|
const altStillSet = readFrameSet(join(setDir, 'render'));
|
|
5549
5789
|
if (keepStills) cpSync(join(setDir, 'render'), join(out, 'setup_still_auto', alt.name), { recursive: true });
|
|
5550
|
-
const rigSlotOrder = (Array.isArray(inp.rig.slots) ? inp.rig.slots : []).filter(isRecord).map((sl) => String(sl.name));
|
|
5551
5790
|
const altComposite = flatCompositeForSet(inp.parts, alt, inp.partsDir, altStillSet.background, rigSlotOrder);
|
|
5552
5791
|
const altCanonical = canonical(join(setDir, 'build'), join(setDir, 'seam'));
|
|
5553
5792
|
if (keepStills) cpSync(join(setDir, 'seam'), join(keptDir, alt.name), { recursive: true });
|
|
5554
|
-
const altPlaced = setPlacements(inp.parts, alt, rigSlotOrder)
|
|
5793
|
+
const altPlaced = resolvePlacedAlpha(setPlacements(inp.parts, alt, rigSlotOrder), inp.partsDir);
|
|
5555
5794
|
const altSupport = seamSupport(join(setDir, 'build'), join(setDir, 'rig.json'), altCanonical, altPlaced, seamStacks, inp.stage);
|
|
5556
5795
|
const effects = seamEffectSelection(baseSupport, altSupport);
|
|
5557
5796
|
const autoAltSeam = seamFigures(altComposite, altStillSet.frames[0].image, inp.stage, altStillSet.viewport, altStillSet.background, undefined, alt.scene !== undefined);
|