@rig-c/parts 1.15.0 → 1.17.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 +4 -0
- package/cli.ts +62 -12
- package/docs/AUTHORING.md +454 -200
- package/docs/STABILITY.md +12 -8
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +11 -2
- package/src/assemble.ts +325 -23
- package/src/autoamplitude.ts +23 -3
- package/src/autoenvelope.ts +17 -5
- package/src/build.ts +81 -8
- package/src/check.ts +632 -11
- package/src/config.ts +185 -8
- package/src/errors.ts +16 -0
- package/src/guide.ts +255 -0
- package/src/limbs.ts +188 -0
- package/src/parts.ts +89 -4
- package/src/propose.ts +137 -107
- package/src/rig.ts +43 -8
- package/src/scene.ts +2 -2
package/docs/STABILITY.md
CHANGED
|
@@ -35,8 +35,9 @@ Exit codes are part of the interface: **0** the command did what it says, **1**
|
|
|
35
35
|
refused its input (every reason a `FAIL` line), **2** a usage error or a command
|
|
36
36
|
this version does not implement. No command is unimplemented at 1.0.0 (`LATER`
|
|
37
37
|
in `cli.ts` is empty). A refusal line has the shape
|
|
38
|
-
` FAIL <RULE>: <object> — <value found>; <value required
|
|
39
|
-
stable identifier,
|
|
38
|
+
` FAIL <RULE>: <object> — <value found>; <value required> (see rig-parts guide <RULE>)`:
|
|
39
|
+
the rule name is a stable identifier, the pointer at the end is fixed text, and the
|
|
40
|
+
detail between them is prose that may be reworded. A command
|
|
40
41
|
also answers to the alias `spine-parts` as long as the install keeps it
|
|
41
42
|
([MIGRATION.md](MIGRATION.md)). `-h`, `help` and `-v` are the short forms of
|
|
42
43
|
`--help` and `--version`. The flags' meanings are `rig-parts --help`'s; their
|
|
@@ -60,6 +61,7 @@ defaults are the help's too, and none is guessed from the art.
|
|
|
60
61
|
| `build` | `--config`, `--source`, `--full`, `--head`, `--out`; optional `--seam`, `--project`, `--page-edges`, `--pack-shape`, `--loop`, `--lead-in`, `--requirements`, `--idle-keys`, `--seam-pairs`, `--keep-stills` |
|
|
61
62
|
| `compose` | `--scene`, `--out`; optional `--requirements`, `--accept` |
|
|
62
63
|
| `mesh-draft` | `--config`, `--parts`, `--out`; optional `--requirements`, `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon), `--more-triangles` |
|
|
64
|
+
| `guide` | one positional argument: a rule name, `§<n>`, `§<n>.<m>` or `§<n>.<m>.<k>` |
|
|
63
65
|
| (none) | `--version`, `--help` |
|
|
64
66
|
<!-- /stable:commands -->
|
|
65
67
|
|
|
@@ -109,7 +111,7 @@ The fields, object by object (a bold path is where the object sits; `<part>` is
|
|
|
109
111
|
part name, `[i]` an entry of a list):
|
|
110
112
|
|
|
111
113
|
<!-- stable:config -->
|
|
112
|
-
- **config** — required by the full loader, in front of rig and build: `key`, `assemble`, `bones`, `meshes`, `regions`, `motion`; optional: `generation`, `seethrough`, `constraints`, `turn`, `scene`
|
|
114
|
+
- **config** — required by the full loader, in front of rig and build: `key`, `assemble`, `bones`, `meshes`, `regions`, `motion`; optional: `generation`, `seethrough`, `limbs`, `constraints`, `turn`, `scene`
|
|
113
115
|
- **generation** — required: `checkpoint`, `loras`, `trigger`, `identity`, `sampler`, `costume`, `negative_extra`, `style`, `negative_pose`, `latent`, `seed`; optional: `pose`, `control`
|
|
114
116
|
- **generation.loras[i]** — required: `name`, `strength`; optional: `strength_clip`
|
|
115
117
|
- **generation.sampler** — required: `steps`, `cfg`, `sampler`, `scheduler`
|
|
@@ -122,7 +124,9 @@ part name, `[i]` an entry of a list):
|
|
|
122
124
|
- **assemble.patches[i].draw**, when an object — required: `before`
|
|
123
125
|
- **assemble.cuts[i]** — required: `from`, `into`, `polygon`, `draw`, `overlap`; optional: `under`
|
|
124
126
|
- **assemble.cuts[i].draw**, when an object — required: `before`
|
|
125
|
-
- **
|
|
127
|
+
- **limbs.<limb>** — required: `segments` (issue #349)
|
|
128
|
+
- **limbs.<limb>.segments[i]** — required: `part`, `from`, `bone`; required below a joint (i > 0): `joint`; optional below a joint: `range`, `cap`
|
|
129
|
+
- **bones[i]**, a single bone — required: `name`, `parent`, `at`; optional: `tip`, `inherit`
|
|
126
130
|
- **bones[i]**, a chain — required: `chain`, `parent`, `points`, `tip`
|
|
127
131
|
- **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
132
|
- **meshes.<part>.influences** — optional, at least one: `maxInfluences` (a whole number, 1 or more, or null for no cap), `floor` ("cut" or "continuous")
|
|
@@ -152,7 +156,7 @@ part name, `[i]` an entry of a list):
|
|
|
152
156
|
- **turn** — required: `pivot`, `depths`
|
|
153
157
|
- **turn.depths.<part>** — a See-through depth
|
|
154
158
|
- **scene** — no field (`{}`; a record or an annotation only)
|
|
155
|
-
- **proposal.json**, the file propose writes — `bones`, `meshes`, `regions`, `motion` under the rules above, `turn` with `--full`/`--head`, and `notes`
|
|
159
|
+
- **proposal.json**, the file propose writes — `bones`, `meshes`, `regions`, `limbs` under `--keypoints` where an arm or a leg is drafted from its joints (issue #349), `motion` under the rules above, `turn` with `--full`/`--head`, and `notes`
|
|
156
160
|
<!-- /stable:config -->
|
|
157
161
|
|
|
158
162
|
`TY14` holds the set of field names in backticks above to the set of names the
|
|
@@ -169,10 +173,10 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
169
173
|
|
|
170
174
|
| file | written by | what is stable |
|
|
171
175
|
| --- | --- | --- |
|
|
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
|
-
| `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 |
|
|
176
|
+
| `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), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint` and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `missing_px`), 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) |
|
|
177
|
+
| `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; a bone carries `inherit` only where config.bones declares one, rig-c's mode name as written (issue #348) |
|
|
174
178
|
| `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`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins`, and `faint` only when an island was left out as faint — only when it declares `contour.bridges`); 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
|
-
| `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`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issue #320: `status`, `animations`, `fps`, `physics_left_out`, `folding`, `folding_undeclared`, `declared_not_folding`, `bar`), `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 |
|
|
179
|
+
| `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`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issue #320: `status`, `animations`, `fps`, `physics_left_out`, `folding`, `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `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
180
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `turn` with `--full`/`--head`, and `notes` |
|
|
177
181
|
| `basis.json` | `propose` | spec `spine-parts-basis/1` |
|
|
178
182
|
| `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw`, and `faint` only when an island was left out as faint — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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": {
|
|
@@ -13,6 +13,13 @@ rig-c, which compiles, gates, packs and renders. You cannot see the painting,
|
|
|
13
13
|
the layers or the rig. The printed lines, `docs/AUTHORING.md` and the files each
|
|
14
14
|
stage writes are the whole interface.
|
|
15
15
|
|
|
16
|
+
**A FAIL line ends with the command that prints its row of the guide:**
|
|
17
|
+
`FAIL RULE: object — detail (see rig-parts guide RULE)`. Run that command before
|
|
18
|
+
anything else. It prints the one row of `docs/AUTHORING.md` that maps the rule to
|
|
19
|
+
what has to change, read from the installed file. `rig-parts guide §<n>` prints a
|
|
20
|
+
section, `§<n>.<m>` the m-th `###` under it and `§<n>.<m>.<k>` the k-th `####`
|
|
21
|
+
under that.
|
|
22
|
+
|
|
16
23
|
## Non-negotiables
|
|
17
24
|
|
|
18
25
|
- **rig-c's gate is never bypassed.** rig-parts writes no Spine data
|
|
@@ -26,7 +33,7 @@ stage writes are the whole interface.
|
|
|
26
33
|
because the tool refused.
|
|
27
34
|
- **The refusal is the instruction.** `FAIL RULE: object — detail` names the object,
|
|
28
35
|
the value found and the value required; AUTHORING §6 maps every rule to the field
|
|
29
|
-
or input that has to change.
|
|
36
|
+
or input that has to change, and `rig-parts guide RULE` prints that row.
|
|
30
37
|
- **Green is not "looks right".** `check` measures the gates, the seam, the loop and
|
|
31
38
|
six judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`,
|
|
32
39
|
`TIP_OVER_ROOT`, `STILL_REGIONS_DARK` — its face half in the head's own frame —
|
|
@@ -86,7 +93,9 @@ stage writes are the whole interface.
|
|
|
86
93
|
an arm raised, seated in a skirt that does not start at the hip: add
|
|
87
94
|
`--keypoints <file>` (body-18 joints in painting px, each observed, occluded
|
|
88
95
|
or missing; AUTHORING §3, *A posed figure*) so the neck, hip, chest and
|
|
89
|
-
sleeves follow the pose and LINT reads the joints, not screen y.
|
|
96
|
+
sleeves follow the pose and LINT reads the joints, not screen y. Each arm and
|
|
97
|
+
leg it places comes back as `limbs` (AUTHORING §3, *Limbs*): paste it, assemble
|
|
98
|
+
again, and read each `limb cap:` line for the pixels a cap is missing. A clean
|
|
90
99
|
LINT does not show a seated hip is right; the overlay does. Never write a joint you did not get from an
|
|
91
100
|
estimator or the painting: say `missing` and let the rule stand in.
|
|
92
101
|
6. Correct against `render/landmarks.png` and `note:` lines; a
|
package/src/assemble.ts
CHANGED
|
@@ -91,11 +91,12 @@
|
|
|
91
91
|
*
|
|
92
92
|
* Pure: no clock, no randomness, no file access. The CLI reads and writes.
|
|
93
93
|
*/
|
|
94
|
-
import type { CharacterConfig, Cut, EarlyConfig, Extend, Patch, PlanEntry, Run, SeeThrough } from './config.ts';
|
|
94
|
+
import type { BoneEntry, CharacterConfig, Cut, EarlyConfig, Extend, Limb, Patch, PlanEntry, Run, SeeThrough } from './config.ts';
|
|
95
95
|
import { type Problem, refuseIfAny } from './errors.ts';
|
|
96
96
|
import { figuresPhrase, implausibleRules, type Layer, type LayerFigures, layerFigures, type LayerSet, NEAR_WHITE_MIN, OPAQUE_ALPHA_ABOVE, ruleSummary } from './layers.ts';
|
|
97
97
|
import { squarePad } from './inputs.ts';
|
|
98
|
-
import { PAINTING_RUN, type PartRecord, type PartsFile, type RecompositeRecord, type SceneRecord } from './parts.ts';
|
|
98
|
+
import { type CapRecord, type LimbRecord, PAINTING_RUN, type PartRecord, type PartsFile, type RecompositeRecord, type SceneRecord } from './parts.ts';
|
|
99
|
+
import { type BonePlace, bonePlaces, capBox, inDisc, type JointLine, jointLine, limbPolyline, limbWidth, nearestLimb, pastJoint, segmentOf } from './limbs.ts';
|
|
99
100
|
import {
|
|
100
101
|
alphaComposite,
|
|
101
102
|
connectedComponents,
|
|
@@ -240,6 +241,12 @@ export interface AssembleInput {
|
|
|
240
241
|
projectRule: ProjectRule;
|
|
241
242
|
/** `config.scene` (issue #242): the painting is a scene, so the recomposite is read inside the figure ({@link sceneFigure}). Absent or false: nothing changes. */
|
|
242
243
|
scene?: boolean;
|
|
244
|
+
/** `config.limbs` (issue #349): the parts cut at their joints ({@link LimbCount}). Absent or empty: nothing changes. */
|
|
245
|
+
limbs?: Record<string, Limb>;
|
|
246
|
+
/** `config.bones`, read for the limbs' joints; required with `limbs`. */
|
|
247
|
+
bones?: BoneEntry[];
|
|
248
|
+
/** Each segment's `cap` image, decoded by the caller, keyed `<limb>/<segment index>`; a segment naming a cap with none here is refused (`ASSEMBLE_LIMB_CAP_PRESENT`). */
|
|
249
|
+
caps?: ReadonlyMap<string, Raster>;
|
|
243
250
|
}
|
|
244
251
|
|
|
245
252
|
/** The derived numbers of the two runs' geometry. */
|
|
@@ -1537,7 +1544,10 @@ export function cutPatch(patch: Patch, srcr: Raster, silhouette: Mask | null): {
|
|
|
1537
1544
|
}
|
|
1538
1545
|
|
|
1539
1546
|
/** One place in the draw order: a plan part, a patch or a cut's new part, by its index in its list. */
|
|
1540
|
-
export type
|
|
1547
|
+
export type PlanSlot = { plan: number } | { patch: number } | { cut: number };
|
|
1548
|
+
|
|
1549
|
+
/** A slot of the draw order once the limbs are in it (issue #349, {@link limbOrder}). */
|
|
1550
|
+
export type DrawSlot = PlanSlot | { limb: number };
|
|
1541
1551
|
|
|
1542
1552
|
/**
|
|
1543
1553
|
* The draw order of plan parts, patches and cuts together, back to front:
|
|
@@ -1548,8 +1558,8 @@ export type DrawSlot = { plan: number } | { patch: number } | { cut: number };
|
|
|
1548
1558
|
* order of plan parts and patches it always was. The loader has already
|
|
1549
1559
|
* refused a `before` that names no plan part.
|
|
1550
1560
|
*/
|
|
1551
|
-
export function drawOrder(plan: readonly string[], patches: readonly Patch[], cuts: readonly Cut[] = []):
|
|
1552
|
-
const out:
|
|
1561
|
+
export function drawOrder(plan: readonly string[], patches: readonly Patch[], cuts: readonly Cut[] = []): PlanSlot[] {
|
|
1562
|
+
const out: PlanSlot[] = [];
|
|
1553
1563
|
const place = (at: (draw: Patch['draw']) => boolean): void => {
|
|
1554
1564
|
patches.forEach((q, i) => {
|
|
1555
1565
|
if (at(q.draw)) out.push({ patch: i });
|
|
@@ -1703,6 +1713,286 @@ export function cutBand(polygonMask: Mask, overlap: number): Mask {
|
|
|
1703
1713
|
return band;
|
|
1704
1714
|
}
|
|
1705
1715
|
|
|
1716
|
+
/** One layer's part as the stage builds it, before it is placed: its key, pixels, projection figures, merge trace and, when the layer is cut, the rig pixels it owns. */
|
|
1717
|
+
interface Built {
|
|
1718
|
+
key: string;
|
|
1719
|
+
r: Raster;
|
|
1720
|
+
st: ProjectionStats;
|
|
1721
|
+
extendKeys: string[];
|
|
1722
|
+
trace: MergeTrace;
|
|
1723
|
+
merged: number;
|
|
1724
|
+
/** The rig pixels this piece of the layer owns when the layer is cut, null when it is not: the counts are split by it. */
|
|
1725
|
+
region: Mask | null;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// ---------------------------------------------------------------------------
|
|
1729
|
+
// limbs — a declared limb's parts cut at its joints (issue #349, src/limbs.ts)
|
|
1730
|
+
// ---------------------------------------------------------------------------
|
|
1731
|
+
|
|
1732
|
+
/** One part a limb segment makes, in `config.limbs` order (limb, then segment). */
|
|
1733
|
+
export interface LimbPart {
|
|
1734
|
+
limb: string;
|
|
1735
|
+
segment: number;
|
|
1736
|
+
part: string;
|
|
1737
|
+
/** The part whose pixels it takes. */
|
|
1738
|
+
of: string;
|
|
1739
|
+
record: LimbRecord;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/** What the limbs did to one part they took: its opaque pixels and each segment's share (`limbLines`). */
|
|
1743
|
+
export interface LimbCount {
|
|
1744
|
+
of: string;
|
|
1745
|
+
before: number;
|
|
1746
|
+
shares: Array<{ part: string; px: number }>;
|
|
1747
|
+
/** The joints whose cap this part's segments carry, in `config.limbs` order. */
|
|
1748
|
+
joints: Array<{ limb: string; joint: string; part: string; cap: CapRecord }>;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* The lines `assemble` prints for the limbs (issue #349), one per part a limb
|
|
1753
|
+
* took — ` limb cut: "<of>" opaque <n> = "<part>" <px> + …` — and one per
|
|
1754
|
+
* joint, its cap's pixels by where they came from. Empty without limbs.
|
|
1755
|
+
*/
|
|
1756
|
+
export function limbLines(counts: readonly LimbCount[]): string[] {
|
|
1757
|
+
const out: string[] = [];
|
|
1758
|
+
for (const c of counts) {
|
|
1759
|
+
out.push(` limb cut: "${c.of}" opaque ${c.before} = ${c.shares.map((q) => `"${q.part}" ${q.px}`).join(' + ')}`);
|
|
1760
|
+
for (const j of c.joints) {
|
|
1761
|
+
const k = j.cap;
|
|
1762
|
+
const from = k.image_px === undefined ? `${k.layer_px} px from the layer` : `${k.image_px} px from the cap image, ${k.layer_px} px from the layer`;
|
|
1763
|
+
const miss =
|
|
1764
|
+
k.missing_px === 0
|
|
1765
|
+
? 'none missing'
|
|
1766
|
+
: `${k.missing_px} px missing — the layer has no pixel there (hidden in the painting, or past the limb's outline), and the tool invents none: paint them into a ${k.box[2]}x${k.box[3]} image whose pixel (i, j) is rig pixel (${k.box[0]} + i, ${k.box[1]} + j), and name it in the segment's "cap"`;
|
|
1767
|
+
out.push(
|
|
1768
|
+
` limb cap: "${j.limb}" joint "${j.joint}" at [${k.at[0]}, ${k.at[1]}], on "${j.part}": a disc of radius ${k.radius} px (the limb ${k.width} px across the joint line), box [${k.box.join(', ')}] — on the upper side of the line ${from}, ${miss}`,
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* The draw order with the limbs in it (issue #349): each part a limb takes
|
|
1777
|
+
* leaves the order, and each limb's parts stand where its first segment's
|
|
1778
|
+
* part stood, lowest segment first (drawn behind), so the upper is drawn over
|
|
1779
|
+
* the lower at every joint; limbs whose first segment takes one part stand
|
|
1780
|
+
* there in `config.limbs` order. Without limbs, `order` as it was.
|
|
1781
|
+
*/
|
|
1782
|
+
export function limbOrder(order: readonly DrawSlot[], plan: readonly PlanEntry[], cuts: readonly Cut[], parts: readonly LimbPart[]): DrawSlot[] {
|
|
1783
|
+
if (parts.length === 0) return [...order];
|
|
1784
|
+
const nameOf = (d: DrawSlot): string | null => ('plan' in d ? plan[d.plan][0] : 'cut' in d ? cuts[d.cut].into : null);
|
|
1785
|
+
const taken = new Set(parts.map((q) => q.of));
|
|
1786
|
+
const out: DrawSlot[] = [];
|
|
1787
|
+
for (const d of order) {
|
|
1788
|
+
const n = nameOf(d);
|
|
1789
|
+
if (n === null || !taken.has(n)) {
|
|
1790
|
+
out.push(d);
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const limbs = [...new Set(parts.filter((q) => q.segment === 0 && q.of === n).map((q) => q.limb))];
|
|
1794
|
+
for (const l of limbs) {
|
|
1795
|
+
const mine = parts.map((q, i) => ({ q, i })).filter(({ q }) => q.limb === l);
|
|
1796
|
+
for (const { i } of [...mine].reverse()) out.push({ limb: i });
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return out;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
/**
|
|
1803
|
+
* Cut every declared limb (module comment of `src/limbs.ts`): each part a limb
|
|
1804
|
+
* takes is split among the limbs that take it (nearest polyline), then each
|
|
1805
|
+
* limb's share among its segments (the joint lines), and each segment below a
|
|
1806
|
+
* joint gets its cap. Every refusal is collected and thrown at once:
|
|
1807
|
+
* `ASSEMBLE_LIMB_TAKES` (a limb takes a part with no pixel), `ASSEMBLE_LIMB_JOINT`
|
|
1808
|
+
* (no joint line: the upper bone has no length, or the limb folds back on
|
|
1809
|
+
* itself), `ASSEMBLE_LIMB_JOINT_ON_ART` (the joint is not on the limb's art,
|
|
1810
|
+
* so it has no width), `ASSEMBLE_LIMB_PIXELS` (a segment gets no opaque
|
|
1811
|
+
* pixel), `ASSEMBLE_LIMB_CAP_PRESENT`, `ASSEMBLE_LIMB_CAP_SIZE` and
|
|
1812
|
+
* `ASSEMBLE_LIMB_CAP_EMPTY` (a cap image the caller did not hand over, not the
|
|
1813
|
+
* box's size, or transparent over every cap pixel).
|
|
1814
|
+
*/
|
|
1815
|
+
function cutLimbs(
|
|
1816
|
+
limbs: Readonly<Record<string, Limb>>,
|
|
1817
|
+
bones: readonly BoneEntry[],
|
|
1818
|
+
caps: ReadonlyMap<string, Raster>,
|
|
1819
|
+
W: number,
|
|
1820
|
+
H: number,
|
|
1821
|
+
builtOf: (name: string) => Built | null,
|
|
1822
|
+
opaqueOf: (r: Raster) => number,
|
|
1823
|
+
mergedIn: (b: Built, region: Mask) => number,
|
|
1824
|
+
count: (m: Mask | null, region: Mask) => number,
|
|
1825
|
+
): { parts: LimbPart[]; pieces: Built[]; counts: LimbCount[] } {
|
|
1826
|
+
const names = Object.keys(limbs);
|
|
1827
|
+
if (names.length === 0) return { parts: [], pieces: [], counts: [] };
|
|
1828
|
+
const problems: Problem[] = [];
|
|
1829
|
+
const places = bonePlaces(bones);
|
|
1830
|
+
const parts: LimbPart[] = [];
|
|
1831
|
+
for (const l of names) {
|
|
1832
|
+
limbs[l].segments.forEach((sg, k) =>
|
|
1833
|
+
parts.push({ limb: l, segment: k, part: sg.part, of: sg.from, record: { name: l, segment: k, of: sg.from, bone: sg.bone, ...(k === 0 ? {} : { joint: sg.joint as string }) } }),
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
const polylines = new Map(names.map((l) => [l, limbPolyline(limbs[l], places)]));
|
|
1837
|
+
// The joint line above each segment (null above the first).
|
|
1838
|
+
const lines = new Map<string, Array<JointLine | null>>();
|
|
1839
|
+
for (const l of names) {
|
|
1840
|
+
const segs = limbs[l].segments;
|
|
1841
|
+
lines.set(
|
|
1842
|
+
l,
|
|
1843
|
+
segs.map((sg, k) => {
|
|
1844
|
+
if (k === 0) return null;
|
|
1845
|
+
const up = places.get(segs[k - 1].bone) as BonePlace;
|
|
1846
|
+
const me = places.get(sg.bone) as BonePlace;
|
|
1847
|
+
const end = me.end ?? (k + 1 < segs.length ? (places.get(segs[k + 1].bone) as BonePlace).at : null);
|
|
1848
|
+
const line = jointLine(up.at, me.at, end);
|
|
1849
|
+
if (line === null) {
|
|
1850
|
+
problems.push({
|
|
1851
|
+
code: 'ASSEMBLE_LIMB_JOINT',
|
|
1852
|
+
object: `config.limbs.${l}.segments[${k}] (joint "${String(sg.joint)}")`,
|
|
1853
|
+
detail: `has no joint line: "${segs[k - 1].bone}" at [${up.at.join(', ')}] and "${sg.bone}" at [${me.at.join(', ')}]${end === null ? '' : ` ending at [${end.join(', ')}]`} ${up.at[0] === me.at[0] && up.at[1] === me.at[1] ? 'stand on one point, so the upper bone has no direction' : 'point straight back at each other, so the two directions cancel'}; bones that give the limb an axis at the joint are required`,
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
return line;
|
|
1857
|
+
}),
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
refuseIfAny(problems);
|
|
1861
|
+
// Each part a limb takes, in the order the limbs first take them.
|
|
1862
|
+
const sources = [...new Set(parts.map((q) => q.of))];
|
|
1863
|
+
const pieces: Array<Built | null> = parts.map(() => null);
|
|
1864
|
+
const rasters = parts.map(() => newRaster(W, H));
|
|
1865
|
+
const regions = parts.map(() => newMask(W, H));
|
|
1866
|
+
/** For each part taken, which limb (index into names) owns each pixel, -1 where it has none. */
|
|
1867
|
+
const owners = new Map<string, Int32Array>();
|
|
1868
|
+
const counts: LimbCount[] = [];
|
|
1869
|
+
for (const src of sources) {
|
|
1870
|
+
const b = builtOf(src);
|
|
1871
|
+
if (b === null) continue;
|
|
1872
|
+
const on = names.filter((l) => limbs[l].segments.some((sg) => sg.from === src));
|
|
1873
|
+
const own = new Int32Array(W * H).fill(-1);
|
|
1874
|
+
const before = opaqueOf(b.r);
|
|
1875
|
+
if (before === 0) {
|
|
1876
|
+
problems.push({ code: 'ASSEMBLE_LIMB_TAKES', object: `part "${src}" (config.limbs.${on[0]})`, detail: 'has no opaque pixel to cut; a part with art is required — the limb takes its pixels' });
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
const lns = on.map((l) => polylines.get(l) as Array<[number, number]>);
|
|
1880
|
+
for (let y = 0; y < H; y++) {
|
|
1881
|
+
for (let x = 0; x < W; x++) {
|
|
1882
|
+
const p = y * W + x;
|
|
1883
|
+
if (b.r.data[p * 4 + 3] === 0) continue;
|
|
1884
|
+
const li = on.length === 1 ? 0 : nearestLimb(lns, x, y);
|
|
1885
|
+
const l = on[li];
|
|
1886
|
+
own[p] = names.indexOf(l);
|
|
1887
|
+
const segs = limbs[l].segments;
|
|
1888
|
+
const first = segs.findIndex((sg) => sg.from === src);
|
|
1889
|
+
let last = first;
|
|
1890
|
+
while (last + 1 < segs.length && segs[last + 1].from === src) last++;
|
|
1891
|
+
const k = segmentOf(lines.get(l) as Array<JointLine | null>, first, last, x, y);
|
|
1892
|
+
const at = parts.findIndex((q) => q.limb === l && q.segment === k);
|
|
1893
|
+
rasters[at].data.set(b.r.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
1894
|
+
if (b.region === null || b.region.data[p] === 1) regions[at].data[p] = 1;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
owners.set(src, own);
|
|
1898
|
+
const shares = parts.map((q, i) => ({ q, i })).filter(({ q }) => q.of === src);
|
|
1899
|
+
const counted = shares.map(({ q, i }) => ({ part: q.part, px: opaqueOf(rasters[i]) }));
|
|
1900
|
+
const sum = counted.reduce((a, c) => a + c.px, 0);
|
|
1901
|
+
if (sum !== before) {
|
|
1902
|
+
problems.push({ code: 'ASSEMBLE_COUNTS_ADD_UP', object: `part "${src}" (config.limbs)`, detail: `opaque px: ${before} before the limbs, ${sum} over their parts; equal is required (a bug in the assembler, not in the inputs)` });
|
|
1903
|
+
}
|
|
1904
|
+
for (const { q, i } of shares) {
|
|
1905
|
+
pieces[i] = { ...b, r: rasters[i], region: regions[i], merged: mergedIn(b, regions[i]) };
|
|
1906
|
+
if (count(regions[i], regions[i]) === 0 && opaqueOf(rasters[i]) === 0) {
|
|
1907
|
+
problems.push({
|
|
1908
|
+
code: 'ASSEMBLE_LIMB_PIXELS',
|
|
1909
|
+
object: `config.limbs.${q.limb}.segments[${q.segment}] (part "${q.part}")`,
|
|
1910
|
+
detail: `takes 0 of "${src}"'s ${before} opaque pixel(s); a segment with art is required — the joint lines through the bones' origins put none of the limb's pixels on this segment, or another limb's polyline lies nearer every one: move the bones onto the limb's art`,
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
counts.push({ of: src, before, shares: counted, joints: [] });
|
|
1915
|
+
}
|
|
1916
|
+
refuseIfAny(problems);
|
|
1917
|
+
// The caps, after every part is split, so a cap reads the limb's art in the layer as the painting has it.
|
|
1918
|
+
for (const l of names) {
|
|
1919
|
+
const segs = limbs[l].segments;
|
|
1920
|
+
segs.forEach((sg, k) => {
|
|
1921
|
+
if (k === 0) return;
|
|
1922
|
+
const line = (lines.get(l) as Array<JointLine | null>)[k] as JointLine;
|
|
1923
|
+
const li = names.indexOf(l);
|
|
1924
|
+
const upFrom = segs[k - 1].from;
|
|
1925
|
+
const art = (x: number, y: number): boolean => {
|
|
1926
|
+
if (x < 0 || y < 0 || x >= W || y >= H) return false;
|
|
1927
|
+
const p = y * W + x;
|
|
1928
|
+
for (const src of new Set([upFrom, sg.from])) {
|
|
1929
|
+
const b = builtOf(src);
|
|
1930
|
+
if (b !== null && owners.get(src)?.[p] === li && b.r.data[p * 4 + 3] > OPAQUE_ALPHA_ABOVE) return true;
|
|
1931
|
+
}
|
|
1932
|
+
return false;
|
|
1933
|
+
};
|
|
1934
|
+
const object = `config.limbs.${l}.segments[${k}] (joint "${String(sg.joint)}", part "${sg.part}")`;
|
|
1935
|
+
const width = limbWidth(line, art, [0, 0, W - 1, H - 1]);
|
|
1936
|
+
if (width === null) {
|
|
1937
|
+
problems.push({
|
|
1938
|
+
code: 'ASSEMBLE_LIMB_JOINT_ON_ART',
|
|
1939
|
+
object,
|
|
1940
|
+
detail: `the joint, "${sg.bone}"'s origin at [${line.at.join(', ')}], lies on none of the limb's art within 1 px along its joint line, so the limb has no width there to size the cap by; a bone origin on the limb's art (alpha above ${OPAQUE_ALPHA_ABOVE}) is required — move the bone onto the joint`,
|
|
1941
|
+
});
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
// The width to 3 places (src/round.ts): a measured length, and a radius the line and the record print as read.
|
|
1945
|
+
const w3 = pyRound(width, 3);
|
|
1946
|
+
const radius = w3 / 2;
|
|
1947
|
+
const box = capBox(line.at, radius);
|
|
1948
|
+
const at = parts.findIndex((q) => q.limb === l && q.segment === k);
|
|
1949
|
+
const img = sg.cap === undefined ? null : (caps.get(`${l}/${k}`) ?? null);
|
|
1950
|
+
if (sg.cap !== undefined && img === null) {
|
|
1951
|
+
problems.push({ code: 'ASSEMBLE_LIMB_CAP_PRESENT', object: `${object}.cap`, detail: `names ${sg.cap}, and no image was handed to the stage for it; the decoded PNG is required (a caller of assemble() reads the file)` });
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (img !== null && (img.width !== box[2] || img.height !== box[3])) {
|
|
1955
|
+
problems.push({
|
|
1956
|
+
code: 'ASSEMBLE_LIMB_CAP_SIZE',
|
|
1957
|
+
object: `${object}.cap`,
|
|
1958
|
+
detail: `${String(sg.cap)} is ${img.width}x${img.height}; the cap's box is ${box[2]}x${box[3]} at ${box[0]},${box[1]} (the pixels within ${radius} px of the joint, in rig pixels), and an image of exactly that size is required — its pixel (i, j) is rig pixel (${box[0]} + i, ${box[1]} + j)`,
|
|
1959
|
+
});
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
const lower = rasters[at];
|
|
1963
|
+
const lowerSrc = builtOf(sg.from) as Built;
|
|
1964
|
+
const ownLower = owners.get(sg.from) as Int32Array;
|
|
1965
|
+
let layerPx = 0;
|
|
1966
|
+
let imagePx = 0;
|
|
1967
|
+
let missing = 0;
|
|
1968
|
+
for (let y = Math.max(0, box[1]); y < Math.min(H, box[1] + box[3]); y++) {
|
|
1969
|
+
for (let x = Math.max(0, box[0]); x < Math.min(W, box[0] + box[2]); x++) {
|
|
1970
|
+
if (!inDisc(line.at, radius, x, y) || pastJoint(line, x, y)) continue;
|
|
1971
|
+
const p = y * W + x;
|
|
1972
|
+
const u = img === null ? -1 : ((y - box[1]) * img.width + (x - box[0])) * 4;
|
|
1973
|
+
if (img !== null && img.data[u + 3] > 0) {
|
|
1974
|
+
lower.data.set(img.data.subarray(u, u + 4), p * 4);
|
|
1975
|
+
imagePx++;
|
|
1976
|
+
} else if (ownLower[p] === li && lowerSrc.r.data[p * 4 + 3] > 0) {
|
|
1977
|
+
lower.data.set(lowerSrc.r.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
1978
|
+
layerPx++;
|
|
1979
|
+
} else missing++;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (img !== null && imagePx === 0) {
|
|
1983
|
+
problems.push({ code: 'ASSEMBLE_LIMB_CAP_EMPTY', object: `${object}.cap`, detail: `${String(sg.cap)} is transparent over every one of the cap's pixels; an image that gives at least one is required — to keep the layer's cap, drop cap` });
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const cap: CapRecord = { at: [line.at[0], line.at[1]], width: w3, radius, box, layer_px: layerPx, ...(img === null ? {} : { image_px: imagePx }), missing_px: missing };
|
|
1987
|
+
parts[at].record.cap = cap;
|
|
1988
|
+
const c = counts.find((q) => q.of === sg.from);
|
|
1989
|
+
if (c !== undefined) c.joints.push({ limb: l, joint: String(sg.joint), part: sg.part, cap });
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
refuseIfAny(problems);
|
|
1993
|
+
return { parts, pieces: pieces as Built[], counts };
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1706
1996
|
// ---------------------------------------------------------------------------
|
|
1707
1997
|
// the stage
|
|
1708
1998
|
// ---------------------------------------------------------------------------
|
|
@@ -1719,6 +2009,8 @@ export interface AssembleResult {
|
|
|
1719
2009
|
figures: RecompositeFigures;
|
|
1720
2010
|
/** What each `assemble.cuts` entry took, counted when it was made (`cutLines`); empty without cuts. */
|
|
1721
2011
|
cuts: CutCount[];
|
|
2012
|
+
/** What each `config.limbs` limb cut, per part it took and per joint (`limbLines`); empty without limbs (issue #349). */
|
|
2013
|
+
limbs: LimbCount[];
|
|
1722
2014
|
/** The full-run layers the plan does not take from the full run whose uncovered error holds a body (`uncoveredErrorLayers`); empty otherwise. */
|
|
1723
2015
|
uncovered: UncoveredErrorLayer[];
|
|
1724
2016
|
/** The parts whose `DRIFT_LIMIT` refusals hold a body another part holds in the draw order (`driftBodies`), in draw order; empty otherwise. */
|
|
@@ -1874,16 +2166,6 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
1874
2166
|
});
|
|
1875
2167
|
refuseIfAny(patchProblems);
|
|
1876
2168
|
|
|
1877
|
-
interface Built {
|
|
1878
|
-
key: string;
|
|
1879
|
-
r: Raster;
|
|
1880
|
-
st: ProjectionStats;
|
|
1881
|
-
extendKeys: string[];
|
|
1882
|
-
trace: MergeTrace;
|
|
1883
|
-
merged: number;
|
|
1884
|
-
/** The rig pixels this piece of the layer owns when the layer is cut, null when it is not: the counts are split by it. */
|
|
1885
|
-
region: Mask | null;
|
|
1886
|
-
}
|
|
1887
2169
|
const built: Array<Built | null> = plan.map(() => null);
|
|
1888
2170
|
const empty: Problem[] = [];
|
|
1889
2171
|
/** The part's alpha box and its `alpha > 8` count, over the whole rig. */
|
|
@@ -2066,9 +2348,18 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2066
2348
|
refuseIfAny(empty);
|
|
2067
2349
|
// In cut order, as the config lists them.
|
|
2068
2350
|
cutCounts.sort((a, b) => cuts.findIndex((q) => q.into === a.into) - cuts.findIndex((q) => q.into === b.into));
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2351
|
+
|
|
2352
|
+
// Issue #349: the limbs, after the cuts (a limb may take a cut's piece, and a cut's under image fills its base first).
|
|
2353
|
+
const lim = cutLimbs(input.limbs ?? {}, input.bones ?? [], input.caps ?? new Map(), W, H, (name) => {
|
|
2354
|
+
const pi = plan.findIndex((e) => e[0] === name);
|
|
2355
|
+
if (pi >= 0) return built[pi];
|
|
2356
|
+
const ci = cuts.findIndex((q) => q.into === name);
|
|
2357
|
+
return ci >= 0 ? pieces[ci] : null;
|
|
2358
|
+
}, opaqueOf, mergedIn, count);
|
|
2359
|
+
const limbPieces = lim.pieces;
|
|
2360
|
+
const order = limbOrder(order0, plan, cuts, lim.parts);
|
|
2361
|
+
const pieceAt = (d: { plan: number } | { cut: number } | { limb: number }): { name: string; b: Built } =>
|
|
2362
|
+
'plan' in d ? { name: plan[d.plan][0], b: built[d.plan] as Built } : 'cut' in d ? { name: cuts[d.cut].into, b: pieces[d.cut] as Built } : { name: lim.parts[d.limb].part, b: limbPieces[d.limb] as Built };
|
|
2072
2363
|
|
|
2073
2364
|
// Step 4b: push back a fringe the painting shows another part through, over
|
|
2074
2365
|
// the plan parts still uncropped and the patches, in draw order.
|
|
@@ -2098,20 +2389,26 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2098
2389
|
});
|
|
2099
2390
|
const pushedOf: FringePushed[][] = plan.map(() => []);
|
|
2100
2391
|
const pushedOfCut: FringePushed[][] = cuts.map(() => []);
|
|
2392
|
+
const pushedOfLimb: FringePushed[][] = lim.parts.map(() => []);
|
|
2101
2393
|
order.forEach((d, i) => {
|
|
2102
2394
|
if ('plan' in d) pushedOf[d.plan] = pushedBy[i];
|
|
2103
2395
|
if ('cut' in d) pushedOfCut[d.cut] = pushedBy[i];
|
|
2396
|
+
if ('limb' in d) pushedOfLimb[d.limb] = pushedBy[i];
|
|
2104
2397
|
});
|
|
2105
2398
|
|
|
2106
2399
|
const planned: Array<PlacedPart | null> = plan.map(() => null);
|
|
2107
2400
|
const cutPlaced: Array<PlacedPart | null> = cuts.map(() => null);
|
|
2108
2401
|
// A cut's piece records the plan part it was cut from (issue #230): parts.json is in draw order, so a piece drawn
|
|
2109
2402
|
// "back" comes before its base, and nothing else in the file says which of the two is the piece.
|
|
2110
|
-
const
|
|
2403
|
+
const limbPlaced: Array<PlacedPart | null> = lim.parts.map(() => null);
|
|
2404
|
+
// A part a limb takes is not made itself (issue #349): its pixels are the limb's parts'.
|
|
2405
|
+
const taken = new Set(lim.parts.map((q) => q.of));
|
|
2406
|
+
const pieceList: Array<{ name: string; b: Built; pushed: FringePushed[]; at: string; cutFrom: string | null; limb?: LimbRecord; set: (p: PlacedPart) => void }> = [
|
|
2111
2407
|
...plan.map(([name], pi) => ({ name, b: built[pi] as Built, pushed: pushedOf[pi], at: `config.assemble.plan[${pi}]`, cutFrom: null, set: (q: PlacedPart) => (planned[pi] = q) })),
|
|
2112
2408
|
...cuts.map((q, i) => ({ name: q.into, b: pieces[i] as Built, pushed: pushedOfCut[i], at: `config.assemble.cuts[${i}]`, cutFrom: q.from, set: (pp: PlacedPart) => (cutPlaced[i] = pp) })),
|
|
2113
|
-
|
|
2114
|
-
|
|
2409
|
+
...lim.parts.map((q, i) => ({ name: q.part, b: limbPieces[i] as Built, pushed: pushedOfLimb[i], at: `config.limbs.${q.limb}.segments[${q.segment}]`, cutFrom: null, limb: q.record, set: (pp: PlacedPart) => (limbPlaced[i] = pp) })),
|
|
2410
|
+
].filter((q) => !taken.has(q.name));
|
|
2411
|
+
pieceList.forEach(({ name, b, pushed: pushedHere, at, cutFrom, limb, set }) => {
|
|
2115
2412
|
const { key, r, st, extendKeys, trace, merged, region } = b;
|
|
2116
2413
|
// A pixel the merge wrote is judged in the extend layer's run, every other
|
|
2117
2414
|
// in the part's own. Projected = the colour came from projectSource's
|
|
@@ -2157,10 +2454,11 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2157
2454
|
seam_override_px: 0,
|
|
2158
2455
|
};
|
|
2159
2456
|
if (pushedHere.length > 0) record.fringe_pushed_back = pushedHere;
|
|
2457
|
+
if (limb !== undefined) record.limb = limb;
|
|
2160
2458
|
set({ record, image: crop(r, x0, y0, record.w, record.h) });
|
|
2161
2459
|
});
|
|
2162
2460
|
refuseIfAny(empty);
|
|
2163
|
-
const placed: PlacedPart[] = order.map((d) => ('plan' in d ? planned[d.plan] : 'cut' in d ? cutPlaced[d.cut] : cut[d.patch]) as PlacedPart);
|
|
2461
|
+
const placed: PlacedPart[] = order.map((d) => ('plan' in d ? planned[d.plan] : 'cut' in d ? cutPlaced[d.cut] : 'limb' in d ? limbPlaced[d.limb] : cut[d.patch]) as PlacedPart);
|
|
2164
2462
|
|
|
2165
2463
|
seamOverride(placed, srcr, seamRule, seamRule === 'silhouette' ? silhouette : null);
|
|
2166
2464
|
const can = recomposite(placed, W, H);
|
|
@@ -2189,6 +2487,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2189
2487
|
figure: inside === null ? null : figureImage(inside, W, H),
|
|
2190
2488
|
figures,
|
|
2191
2489
|
cuts: cutCounts,
|
|
2490
|
+
limbs: lim.counts,
|
|
2192
2491
|
uncovered,
|
|
2193
2492
|
drift,
|
|
2194
2493
|
seamRule,
|
|
@@ -2878,7 +3177,7 @@ function unplannedNotes(
|
|
|
2878
3177
|
* the loader, because `inputs` runs before it exists; `seethrough` too, for a
|
|
2879
3178
|
* full-loader config, where it is optional.
|
|
2880
3179
|
*/
|
|
2881
|
-
export function stageFields(cfg: Pick<CharacterConfig, 'seethrough' | 'assemble' | 'scene'>): {
|
|
3180
|
+
export function stageFields(cfg: Pick<CharacterConfig, 'seethrough' | 'assemble' | 'scene'> & { limbs?: Record<string, Limb>; bones?: BoneEntry[] }): {
|
|
2882
3181
|
resolution: number;
|
|
2883
3182
|
headBox: [number, number, number, number];
|
|
2884
3183
|
rigScale: number;
|
|
@@ -2887,6 +3186,8 @@ export function stageFields(cfg: Pick<CharacterConfig, 'seethrough' | 'assemble'
|
|
|
2887
3186
|
patches: Patch[];
|
|
2888
3187
|
cuts: Cut[];
|
|
2889
3188
|
scene?: boolean;
|
|
3189
|
+
limbs?: Record<string, Limb>;
|
|
3190
|
+
bones?: BoneEntry[];
|
|
2890
3191
|
} {
|
|
2891
3192
|
const problems: Problem[] = [];
|
|
2892
3193
|
if (cfg.seethrough === undefined) {
|
|
@@ -2905,6 +3206,7 @@ export function stageFields(cfg: Pick<CharacterConfig, 'seethrough' | 'assemble'
|
|
|
2905
3206
|
patches: cfg.assemble.patches ?? [],
|
|
2906
3207
|
cuts: cfg.assemble.cuts ?? [],
|
|
2907
3208
|
...(cfg.scene === undefined ? {} : { scene: true }),
|
|
3209
|
+
...(cfg.limbs === undefined ? {} : { limbs: cfg.limbs, bones: cfg.bones ?? [] }),
|
|
2908
3210
|
};
|
|
2909
3211
|
}
|
|
2910
3212
|
|
package/src/autoamplitude.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* undeclared, which would leave both rows `not-measurable` (§8: "a pair no
|
|
23
23
|
* track declares makes both rows not-measurable"). A track moves bone `b`
|
|
24
24
|
* when a bone it keys is `b` or an ancestor of `b`: Spine bones inherit
|
|
25
|
-
* their parent's rotation and scale (the rig writes no inherit mode), so a
|
|
25
|
+
* their parent's rotation and scale (where the rig writes no inherit mode), so a
|
|
26
26
|
* pair the track moves together — both under one keyed bone — or not at all
|
|
27
27
|
* keeps its relative linear part, θ 0 (§8: "a bone that does not move is
|
|
28
28
|
* declared with θ 0"); a pair it moves on one side only gets the track's θ.
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
* the author left it out — rig-c 2.29.0 then measures `MQ_DEFORM_LOAD`, which
|
|
58
58
|
* never read G, and reads `MQ_ALLOCATION_CONTRAST` `not-measurable` naming
|
|
59
59
|
* the gradation. No value is derived or defaulted here.
|
|
60
|
+
* - **A declared inherit mode** (issue #348): a bone between a keyed bone and
|
|
61
|
+
* a bound one — the bound bone itself included — whose `inherit` is not
|
|
62
|
+
* `normal` takes less than the key's rotation or scale, by its mode, so the
|
|
63
|
+
* track's θ for that pair is not composed here: the term stops, naming the
|
|
64
|
+
* bone and its mode, and the amplitude is not sent.
|
|
60
65
|
* - **Constraints** move bones by what they solve, not by a key: a rig with
|
|
61
66
|
* any constraint is refused by name, because no declared amplitude bounds
|
|
62
67
|
* what a constraint does.
|
|
@@ -73,7 +78,7 @@ import type { MotionKey, MotionSpec } from './motion.ts';
|
|
|
73
78
|
* — `rig.json`'s bones, `motion.json`, the config's constraints — so both build the one input `reductionKey` matches.
|
|
74
79
|
*/
|
|
75
80
|
export interface AmplitudeBasis {
|
|
76
|
-
bones: ReadonlyArray<{ name: string; parent?: string }>;
|
|
81
|
+
bones: ReadonlyArray<{ name: string; parent?: string; inherit?: string }>;
|
|
77
82
|
motion: MotionSpec;
|
|
78
83
|
constraints: number;
|
|
79
84
|
}
|
|
@@ -133,7 +138,7 @@ export type AmplitudeDerivation = { amplitude: MotionAmplitude } | { stops: Ampl
|
|
|
133
138
|
* reason to stop (rig-c 2.29.0 admits null).
|
|
134
139
|
*/
|
|
135
140
|
export function deriveMotionAmplitude(args: {
|
|
136
|
-
bones: ReadonlyArray<{ name: string; parent?: string }>;
|
|
141
|
+
bones: ReadonlyArray<{ name: string; parent?: string; inherit?: string }>;
|
|
137
142
|
motion: MotionSpec;
|
|
138
143
|
constraints: number;
|
|
139
144
|
bound: readonly string[];
|
|
@@ -142,6 +147,13 @@ export function deriveMotionAmplitude(args: {
|
|
|
142
147
|
}): AmplitudeDerivation {
|
|
143
148
|
const stops: AmplitudeStop[] = [];
|
|
144
149
|
const parent = new Map(args.bones.map((b) => [b.name, b.parent ?? null]));
|
|
150
|
+
const mode = new Map(args.bones.filter((b) => b.inherit !== undefined && b.inherit !== 'normal').map((b) => [b.name, b.inherit as string]));
|
|
151
|
+
/** The bones from `bone` up to `k`, `k` left out, that declare an inherit mode other than normal (issue #348). */
|
|
152
|
+
const declared = (bone: string, k: string): string[] => {
|
|
153
|
+
const out: string[] = [];
|
|
154
|
+
for (let b: string | null = bone; b !== null && b !== k; b = parent.get(b) ?? null) if (mode.has(b)) out.push(b);
|
|
155
|
+
return out;
|
|
156
|
+
};
|
|
145
157
|
const under = (bone: string, k: string): boolean => {
|
|
146
158
|
for (let b: string | null = bone; b !== null; b = parent.get(b) ?? null) if (b === k) return true;
|
|
147
159
|
return false;
|
|
@@ -159,6 +171,14 @@ export function deriveMotionAmplitude(args: {
|
|
|
159
171
|
const name = `${t.bone ?? t.group ?? '?'}.${t.property}#${i}`;
|
|
160
172
|
const moverOf = (b: string): string[] => keyed.filter((k) => under(b, k));
|
|
161
173
|
if (!bound.some((b) => moverOf(b).length > 0)) return;
|
|
174
|
+
const through = [...new Set(bound.flatMap((b) => moverOf(b).flatMap((k) => declared(b, k))))];
|
|
175
|
+
if (through.length > 0) {
|
|
176
|
+
stops.push({
|
|
177
|
+
term: `tracks[${name}].pairs[].theta`,
|
|
178
|
+
detail: `the track keys [${keyed.join(', ')}], and ${through.map((b) => `"${b}" declares inherit "${mode.get(b)}"`).join(', ')} below it, so what the key does to the bones under it is not the key's own rotation or scale; θ is composed here only through bones that inherit normally, and none is invented`,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
162
182
|
const th = trackTheta(t.property, t.keys);
|
|
163
183
|
if ('refused' in th) {
|
|
164
184
|
stops.push({ term: `tracks[${name}].pairs[].theta`, detail: th.refused });
|