@rig-c/parts 1.16.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/cli.ts +19 -6
- package/docs/AUTHORING.md +203 -15
- package/docs/STABILITY.md +8 -6
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +3 -1
- package/src/assemble.ts +325 -23
- package/src/autoamplitude.ts +23 -3
- package/src/autoenvelope.ts +17 -5
- package/src/build.ts +78 -5
- package/src/check.ts +632 -11
- package/src/config.ts +185 -8
- 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/docs/STABILITY.md
CHANGED
|
@@ -111,7 +111,7 @@ The fields, object by object (a bold path is where the object sits; `<part>` is
|
|
|
111
111
|
part name, `[i]` an entry of a list):
|
|
112
112
|
|
|
113
113
|
<!-- stable:config -->
|
|
114
|
-
- **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`
|
|
115
115
|
- **generation** — required: `checkpoint`, `loras`, `trigger`, `identity`, `sampler`, `costume`, `negative_extra`, `style`, `negative_pose`, `latent`, `seed`; optional: `pose`, `control`
|
|
116
116
|
- **generation.loras[i]** — required: `name`, `strength`; optional: `strength_clip`
|
|
117
117
|
- **generation.sampler** — required: `steps`, `cfg`, `sampler`, `scheduler`
|
|
@@ -124,7 +124,9 @@ part name, `[i]` an entry of a list):
|
|
|
124
124
|
- **assemble.patches[i].draw**, when an object — required: `before`
|
|
125
125
|
- **assemble.cuts[i]** — required: `from`, `into`, `polygon`, `draw`, `overlap`; optional: `under`
|
|
126
126
|
- **assemble.cuts[i].draw**, when an object — required: `before`
|
|
127
|
-
- **
|
|
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`
|
|
128
130
|
- **bones[i]**, a chain — required: `chain`, `parent`, `points`, `tip`
|
|
129
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)
|
|
130
132
|
- **meshes.<part>.influences** — optional, at least one: `maxInfluences` (a whole number, 1 or more, or null for no cap), `floor` ("cut" or "continuous")
|
|
@@ -154,7 +156,7 @@ part name, `[i]` an entry of a list):
|
|
|
154
156
|
- **turn** — required: `pivot`, `depths`
|
|
155
157
|
- **turn.depths.<part>** — a See-through depth
|
|
156
158
|
- **scene** — no field (`{}`; a record or an annotation only)
|
|
157
|
-
- **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`
|
|
158
160
|
<!-- /stable:config -->
|
|
159
161
|
|
|
160
162
|
`TY14` holds the set of field names in backticks above to the set of names the
|
|
@@ -171,10 +173,10 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
171
173
|
|
|
172
174
|
| file | written by | what is stable |
|
|
173
175
|
| --- | --- | --- |
|
|
174
|
-
| `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) |
|
|
175
|
-
| `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) |
|
|
176
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) |
|
|
177
|
-
| `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 |
|
|
178
180
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `turn` with `--full`/`--head`, and `notes` |
|
|
179
181
|
| `basis.json` | `propose` | spec `spine-parts-basis/1` |
|
|
180
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": {
|
|
@@ -93,7 +93,9 @@ under that.
|
|
|
93
93
|
an arm raised, seated in a skirt that does not start at the hip: add
|
|
94
94
|
`--keypoints <file>` (body-18 joints in painting px, each observed, occluded
|
|
95
95
|
or missing; AUTHORING §3, *A posed figure*) so the neck, hip, chest and
|
|
96
|
-
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
|
|
97
99
|
LINT does not show a seated hip is right; the overlay does. Never write a joint you did not get from an
|
|
98
100
|
estimator or the painting: say `missing` and let the rule stand in.
|
|
99
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 });
|