@rig-c/parts 1.25.0 → 1.27.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 +1 -1
- package/cli.ts +20 -5
- package/docs/AUTHORING.md +61 -38
- package/docs/MIGRATION.md +1 -1
- package/docs/STABILITY.md +7 -7
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +1 -1
- package/src/assemble.ts +131 -25
- package/src/build.ts +23 -9
- package/src/check.ts +220 -40
- package/src/cloth.ts +45 -6
- package/src/config.ts +80 -11
- package/src/headbox.ts +68 -34
- package/src/propose.ts +506 -41
- package/src/rig.ts +74 -6
- package/src/scene.ts +87 -7
- package/src/squeeze.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.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": {
|
|
@@ -85,7 +85,7 @@ under that.
|
|
|
85
85
|
gap See-through left (a skirt split into two legs), fixed in the plan or the layers,
|
|
86
86
|
not in the rig. Then `rig-parts propose --parts work/rig
|
|
87
87
|
--source <painting> --out work` → copy `bones`, `meshes`, `regions`, `limbs`,
|
|
88
|
-
`motion`, `constraints`, `turn` from `proposal.json` into the config, each it
|
|
88
|
+
`motion`, `constraints`, `turn`, `proposed_rig_scale` from `proposal.json` into the config, each it
|
|
89
89
|
wrote (`limbs` only with `--keypoints`, `constraints` only with `--cloth`, `turn`
|
|
90
90
|
only with `--full`/`--head`; a `limbs` block left out is refused at the next step). `assemble` needs the plan and none of
|
|
91
91
|
those sections; do not stub them to get past it (AUTHORING §4 says what each step
|
package/src/assemble.ts
CHANGED
|
@@ -1208,7 +1208,7 @@ function keepArtWhole(part: PlacedPart, cleared: Cleared[], holderAt: (q: number
|
|
|
1208
1208
|
* hold the pixel after it); and every pixel at alpha >= `CORE_ALPHA`, which
|
|
1209
1209
|
* is the seam override's.
|
|
1210
1210
|
*/
|
|
1211
|
-
export function pushBackFringe(parts: PlacedPart[], srcr: Raster, rule: SeamRule, silhouette: Mask | null): FringePushed[][] {
|
|
1211
|
+
export function pushBackFringe(parts: PlacedPart[], srcr: Raster, rule: SeamRule, silhouette: Mask | null, feathered: ReadonlyArray<Mask | null> | null = null): FringePushed[][] {
|
|
1212
1212
|
const { width: W } = srcr;
|
|
1213
1213
|
const { holder, cand } = seamCandidates(parts, srcr, rule, silhouette);
|
|
1214
1214
|
const at = (i: number, d: number): number => {
|
|
@@ -1226,6 +1226,7 @@ export function pushBackFringe(parts: PlacedPart[], srcr: Raster, rule: SeamRule
|
|
|
1226
1226
|
// cover itself the loop below has nothing to clear, and the seam override acts on the pixel as before.
|
|
1227
1227
|
if (cand[d] === 0 || !shows(d)) continue;
|
|
1228
1228
|
for (let i = holder[d] + 1; i < parts.length; i++) {
|
|
1229
|
+
if (feathered?.[i]?.data[d] === 1) continue;
|
|
1229
1230
|
const s = at(i, d);
|
|
1230
1231
|
if (s < 0) continue;
|
|
1231
1232
|
const img = parts[i].image.data;
|
|
@@ -1561,6 +1562,61 @@ export function holeLines(f: RecompositeFigures, scene: string | null = null): s
|
|
|
1561
1562
|
];
|
|
1562
1563
|
}
|
|
1563
1564
|
|
|
1565
|
+
/** One kind of uncovered error pixel (issue #351): total pixel count, 8-connected component count, and largest component box. */
|
|
1566
|
+
export interface HoleKindSummary {
|
|
1567
|
+
px: number;
|
|
1568
|
+
count: number;
|
|
1569
|
+
largest: { px: number; x: number; y: number; w: number; h: number } | null;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* The partition of uncovered error pixels into two disjoint causes (issue #351):
|
|
1574
|
+
* - `noLayer`: pixels where no See-through layer in either run has `alpha > OPAQUE_ALPHA_ABOVE` (an `assemble.patches` box from the painting is the only repair).
|
|
1575
|
+
* - `betweenLayers`: pixels where at least one See-through layer has `alpha > OPAQUE_ALPHA_ABOVE`, but no placed part covers the pixel at `alpha > COVERED_ALPHA`.
|
|
1576
|
+
*/
|
|
1577
|
+
export interface HoleKinds {
|
|
1578
|
+
noLayer: HoleKindSummary;
|
|
1579
|
+
betweenLayers: HoleKindSummary;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function summarizeKindMask(mask: Mask): HoleKindSummary {
|
|
1583
|
+
let px = 0;
|
|
1584
|
+
for (let p = 0; p < mask.data.length; p++) px += mask.data[p];
|
|
1585
|
+
if (px === 0) return { px: 0, count: 0, largest: null };
|
|
1586
|
+
const cc = connectedComponents(mask, 8);
|
|
1587
|
+
const all = cc.stats.slice(1).filter((s) => s.area > 0);
|
|
1588
|
+
all.sort((a, b) => b.area - a.area || a.top - b.top || a.left - b.left);
|
|
1589
|
+
const s = all[0];
|
|
1590
|
+
return { px, count: all.length, largest: s === undefined ? null : { px: s.area, x: s.left, y: s.top, w: s.width, h: s.height } };
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
export function holeKinds(err: Uint8Array, covered: Uint8Array, anyLayer: Uint8Array, W: number, H: number): HoleKinds {
|
|
1594
|
+
const noLayerMask = newMask(W, H);
|
|
1595
|
+
const betweenMask = newMask(W, H);
|
|
1596
|
+
for (let p = 0; p < W * H; p++) {
|
|
1597
|
+
if (err[p] !== 1 || covered[p] !== 0) continue;
|
|
1598
|
+
if (anyLayer[p] === 0) noLayerMask.data[p] = 1;
|
|
1599
|
+
else betweenMask.data[p] = 1;
|
|
1600
|
+
}
|
|
1601
|
+
return { noLayer: summarizeKindMask(noLayerMask), betweenLayers: summarizeKindMask(betweenMask) };
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* The summary line under `holeLines` (issue #351): partitions `uncoveredErrorPx`
|
|
1606
|
+
* into pixels where no See-through layer has `alpha > 8` (`noLayer`, repaired
|
|
1607
|
+
* only by `assemble.patches`) and pixels where a layer has `alpha > 8` (`betweenLayers`).
|
|
1608
|
+
*/
|
|
1609
|
+
export function holeKindsLine(k: HoleKinds, scene: string | null = null): string {
|
|
1610
|
+
const fmt = (s: HoleKindSummary): string =>
|
|
1611
|
+
s.px === 0 || s.largest === null
|
|
1612
|
+
? '0 px'
|
|
1613
|
+
: `${s.px} px in ${s.count} component(s) (largest ${s.largest.px} px at ${s.largest.x},${s.largest.y} ${s.largest.w}x${s.largest.h})`;
|
|
1614
|
+
return (
|
|
1615
|
+
`${scene === null ? '' : `${scene}: `}uncovered error by cause: no layer (alpha <= ${OPAQUE_ALPHA_ABOVE} on every layer; only assemble.patches from the painting can cover): ${fmt(k.noLayer)}; ` +
|
|
1616
|
+
`on a layer (alpha > ${OPAQUE_ALPHA_ABOVE} on a layer, below covered alpha ${COVERED_ALPHA} in the parts): ${fmt(k.betweenLayers)}`
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1564
1620
|
/** The `parts.json` `recomposite` block: the four figures, the limits they were measured at, and the listed holes. */
|
|
1565
1621
|
export function recompositeRecord(f: RecompositeFigures): RecompositeRecord {
|
|
1566
1622
|
return {
|
|
@@ -1834,6 +1890,8 @@ interface Built {
|
|
|
1834
1890
|
merged: number;
|
|
1835
1891
|
/** 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. */
|
|
1836
1892
|
region: Mask | null;
|
|
1893
|
+
/** The rig pixels on an upper limb segment whose alpha `edge: "feather"` scaled down (issue #444). */
|
|
1894
|
+
feathered?: Mask;
|
|
1837
1895
|
}
|
|
1838
1896
|
|
|
1839
1897
|
// ---------------------------------------------------------------------------
|
|
@@ -2208,6 +2266,9 @@ function cutLimbs(
|
|
|
2208
2266
|
let featheredPx = 0;
|
|
2209
2267
|
let band = 0;
|
|
2210
2268
|
if (sg.edge === 'feather') {
|
|
2269
|
+
const upperRegion = regions[upAt];
|
|
2270
|
+
const upPiece = pieces[upAt] as Built;
|
|
2271
|
+
const featheredMask = (upPiece.feathered ??= newMask(W, H));
|
|
2211
2272
|
band = pyRound(Math.max(0, coveredRadius(line.at, radius, (x, y) => x >= 0 && y >= 0 && x < W && y < H && lower.data[(y * W + x) * 4 + 3] === 255)), 3);
|
|
2212
2273
|
const fb = capBox(line.at, band);
|
|
2213
2274
|
for (let y = Math.max(0, fb[1]); y < Math.min(H, fb[1] + fb[3]); y++) {
|
|
@@ -2221,6 +2282,11 @@ function cutLimbs(
|
|
|
2221
2282
|
if (na === ua) continue;
|
|
2222
2283
|
if (na === 0) upper.data.fill(0, p * 4, p * 4 + 4);
|
|
2223
2284
|
else upper.data[p * 4 + 3] = na;
|
|
2285
|
+
// Issue #444: a pixel the feather ramp fades to alpha <= OPAQUE_ALPHA_ABOVE is no longer opaque on the
|
|
2286
|
+
// upper piece (its opacity is carried by the lower segment's opaque cap beneath), so it leaves the upper
|
|
2287
|
+
// piece's region and its remaining ramp is marked feathered so pushBackFringe does not clear the fade.
|
|
2288
|
+
if (na <= OPAQUE_ALPHA_ABOVE) upperRegion.data[p] = 0;
|
|
2289
|
+
else featheredMask.data[p] = 1;
|
|
2224
2290
|
featheredPx++;
|
|
2225
2291
|
}
|
|
2226
2292
|
}
|
|
@@ -2278,6 +2344,8 @@ export interface AssembleResult {
|
|
|
2278
2344
|
limbs: LimbCount[];
|
|
2279
2345
|
/** The full-run layers the plan does not take from the full run whose uncovered error holds a body (`uncoveredErrorLayers`); empty otherwise. */
|
|
2280
2346
|
uncovered: UncoveredErrorLayer[];
|
|
2347
|
+
/** The partition of uncovered error pixels into no-layer and on-a-layer components (`holeKinds`, issue #351). */
|
|
2348
|
+
holeKinds: HoleKinds;
|
|
2281
2349
|
/** The parts whose `DRIFT_LIMIT` refusals hold a body another part holds in the draw order (`driftBodies`), in draw order; empty otherwise. */
|
|
2282
2350
|
drift: DriftBodies[];
|
|
2283
2351
|
/** Every hidden-art layer of the runs read (issue #359) and what read it (`hiddenLines`); empty when the runs hold none. */
|
|
@@ -2783,7 +2851,8 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2783
2851
|
const whole: PartRecord = { name, from: b.key, x: 0, y: 0, w: W, h: H, opaque_px: 0, projected_core_px: 0, source_px_taken: 0, refused_drift_px: 0, merged_px: 0, seam_override_px: 0 };
|
|
2784
2852
|
return { record: whole, image: b.r };
|
|
2785
2853
|
});
|
|
2786
|
-
const
|
|
2854
|
+
const featheredInOrder: Array<Mask | null> = order.map((d) => ('patch' in d ? null : (pieceAt(d).b.feathered ?? null)));
|
|
2855
|
+
const pushedBy = pushBackFringe(stack, srcr, seamRule, seamRule === 'silhouette' ? silhouette : null, featheredInOrder);
|
|
2787
2856
|
// Issue #255: a part's drift refusals in a body, and who holds the body in the draw order (the push-back clears
|
|
2788
2857
|
// only fringe below CORE_ALPHA, and the seam override only recolours, so the holders read here are final).
|
|
2789
2858
|
const holderAt = new Int32Array(W * H).fill(-1);
|
|
@@ -2792,13 +2861,26 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2792
2861
|
});
|
|
2793
2862
|
const stackNames = stack.map((q) => q.record.name);
|
|
2794
2863
|
const drift: DriftBodies[] = [];
|
|
2795
|
-
order.forEach((d) => {
|
|
2864
|
+
order.forEach((d, orderIdx) => {
|
|
2796
2865
|
if ('patch' in d) return;
|
|
2797
2866
|
const { name, b } = pieceAt(d);
|
|
2798
2867
|
if (b.st.refusedMask === null) return;
|
|
2799
2868
|
const refused = newMask(W, H);
|
|
2800
2869
|
for (let p = 0; p < W * H; p++) if (b.st.refusedMask.data[p] === 1 && (b.region === null || b.region.data[p] === 1)) refused.data[p] = 1;
|
|
2801
|
-
const
|
|
2870
|
+
const behindAt = new Int32Array(W * H).fill(-1);
|
|
2871
|
+
for (let j = 0; j < orderIdx; j++) {
|
|
2872
|
+
const under = stack[j].image.data;
|
|
2873
|
+
for (let p = 0; p < W * H; p++) {
|
|
2874
|
+
if (under[p * 4 + 3] < CORE_ALPHA) continue;
|
|
2875
|
+
const dMax = Math.max(
|
|
2876
|
+
Math.abs(under[p * 4] - srcr.data[p * 4]),
|
|
2877
|
+
Math.abs(under[p * 4 + 1] - srcr.data[p * 4 + 1]),
|
|
2878
|
+
Math.abs(under[p * 4 + 2] - srcr.data[p * 4 + 2]),
|
|
2879
|
+
);
|
|
2880
|
+
if (dMax <= DRIFT_LIMIT) behindAt[p] = j;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
const found = driftBodies(name, b.key, refused, bodyMinPx(frame), holderAt, stackNames, behindAt);
|
|
2802
2884
|
if (found !== null) drift.push(found);
|
|
2803
2885
|
});
|
|
2804
2886
|
const pushedOf: FringePushed[][] = plan.map(() => []);
|
|
@@ -2880,11 +2962,16 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2880
2962
|
const inside = input.scene === true ? sceneFigure([...rig.entries()].filter(([k]) => k.startsWith('full:')).map(([, r]) => r), W, H) : null;
|
|
2881
2963
|
const sceneRecord: SceneRecord | null = inside === null ? null : { figure_alpha_above: OPAQUE_ALPHA_ABOVE, figure_px: inside.reduce((a, b) => a + b, 0), figure_mask: FIGURE_MASK_FILE };
|
|
2882
2964
|
const figures = measureRecomposite(can, srcr, placed, inside);
|
|
2965
|
+
const covered = coveredMask(placed, W, H);
|
|
2966
|
+
const err = errorMask(can, srcr, inside);
|
|
2967
|
+
const anyLayer = new Uint8Array(W * H);
|
|
2968
|
+
for (const r of rig.values()) for (let p = 0; p < W * H; p++) if (r.data[p * 4 + 3] > OPAQUE_ALPHA_ABOVE) anyLayer[p] = 1;
|
|
2969
|
+
const kinds = holeKinds(err, covered, anyLayer, W, H);
|
|
2883
2970
|
const fullRig = fullTags.map((tag) => ({ tag, rig: rig.get(`full:${tag}`) as Raster }));
|
|
2884
2971
|
const uncovered = uncoveredErrorLayers(
|
|
2885
2972
|
fullRig,
|
|
2886
|
-
|
|
2887
|
-
|
|
2973
|
+
covered,
|
|
2974
|
+
err,
|
|
2888
2975
|
figures.uncoveredErrorPx,
|
|
2889
2976
|
plan,
|
|
2890
2977
|
extend,
|
|
@@ -2911,6 +2998,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2911
2998
|
cuts: cutCounts,
|
|
2912
2999
|
limbs: lim.counts,
|
|
2913
3000
|
uncovered,
|
|
3001
|
+
holeKinds: kinds,
|
|
2914
3002
|
drift,
|
|
2915
3003
|
hidden: [...hiddenUses].map(([layer, readBy]) => ({ layer, readBy })),
|
|
2916
3004
|
seamRule,
|
|
@@ -2939,11 +3027,18 @@ export interface DriftBodies {
|
|
|
2939
3027
|
* (`seamCandidates`) — with its pixel count, most first, then by name.
|
|
2940
3028
|
*/
|
|
2941
3029
|
holders: Array<{ part: string; px: number }>;
|
|
3030
|
+
/**
|
|
3031
|
+
* Over every pixel of the bodies, the top-most part drawn behind `part` at
|
|
3032
|
+
* alpha >= `CORE_ALPHA` whose colour is within `DRIFT_LIMIT` of the painting
|
|
3033
|
+
* (issue #351), with its pixel count, most first, then by name.
|
|
3034
|
+
*/
|
|
3035
|
+
behind?: Array<{ part: string; px: number }>;
|
|
2942
3036
|
}
|
|
2943
3037
|
|
|
2944
3038
|
/**
|
|
2945
3039
|
* A part's refused bodies that another part holds in the draw order (issue
|
|
2946
|
-
* #255)
|
|
3040
|
+
* #255) or that a part drawn behind it matches within `DRIFT_LIMIT` of the
|
|
3041
|
+
* painting (issue #351).
|
|
2947
3042
|
*
|
|
2948
3043
|
* ⚖️ Invariant: `refused` is the part's projection core pixels whose layer
|
|
2949
3044
|
* colour is more than `DRIFT_LIMIT` off the painting (`projectSource`'s
|
|
@@ -2953,32 +3048,36 @@ export interface DriftBodies {
|
|
|
2953
3048
|
* uncovered layer's, and the stage passes the same bar (`bodyMinPx`).
|
|
2954
3049
|
* `holderAt` is, per rig pixel, the index into `names` of the top-most part
|
|
2955
3050
|
* at alpha >= `CORE_ALPHA` in draw order (-1: none), as the seam pass reads a
|
|
2956
|
-
* holder.
|
|
2957
|
-
*
|
|
2958
|
-
*
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
* hundreds to tens of thousands of pixels that no other part touches (the
|
|
2964
|
-
* pull request that added this has the figures); a line there would name no
|
|
2965
|
-
* owner, which is what the line exists to name.
|
|
2966
|
-
*/
|
|
2967
|
-
export function driftBodies(part: string, from: string, refused: Mask, minPx: number, holderAt: Int32Array, names: readonly string[]): DriftBodies | null {
|
|
3051
|
+
* holder. `behindAt` (optional) is, per rig pixel, the index into `names` of
|
|
3052
|
+
* the top-most part drawn behind `part` at alpha >= `CORE_ALPHA` whose colour
|
|
3053
|
+
* is within `DRIFT_LIMIT` of the painting (-1: none). The result is non-null
|
|
3054
|
+
* exactly when the bodies hold a pixel whose holder is another part or whose
|
|
3055
|
+
* `behindAt` names a part drawn behind `part` that matches the painting.
|
|
3056
|
+
*/
|
|
3057
|
+
export function driftBodies(part: string, from: string, refused: Mask, minPx: number, holderAt: Int32Array, names: readonly string[], behindAt?: Int32Array): DriftBodies | null {
|
|
2968
3058
|
let refusedPx = 0;
|
|
2969
3059
|
for (let p = 0; p < refused.data.length; p++) refusedPx += refused.data[p];
|
|
2970
3060
|
if (refusedPx === 0) return null;
|
|
2971
3061
|
const { bodies, pixels } = bodiesWithPixels(refused, minPx);
|
|
2972
3062
|
if (bodies.length === 0) return null;
|
|
2973
3063
|
const count = new Map<string, number>();
|
|
3064
|
+
const behindCount = new Map<string, number>();
|
|
2974
3065
|
for (let p = 0; p < pixels.data.length; p++) {
|
|
2975
|
-
if (pixels.data[p] === 0
|
|
2976
|
-
|
|
2977
|
-
|
|
3066
|
+
if (pixels.data[p] === 0) continue;
|
|
3067
|
+
if (holderAt[p] >= 0) {
|
|
3068
|
+
const n = names[holderAt[p]];
|
|
3069
|
+
count.set(n, (count.get(n) ?? 0) + 1);
|
|
3070
|
+
}
|
|
3071
|
+
if (behindAt !== undefined && behindAt[p] >= 0) {
|
|
3072
|
+
const b = names[behindAt[p]];
|
|
3073
|
+
if (b !== part) behindCount.set(b, (behindCount.get(b) ?? 0) + 1);
|
|
3074
|
+
}
|
|
2978
3075
|
}
|
|
2979
|
-
|
|
3076
|
+
const hasFront = [...count.keys()].some((n) => n !== part);
|
|
3077
|
+
if (!hasFront && behindCount.size === 0) return null;
|
|
2980
3078
|
const holders = [...count.entries()].map(([q, px]) => ({ part: q, px })).sort((a, b) => b.px - a.px || (a.part < b.part ? -1 : a.part > b.part ? 1 : 0));
|
|
2981
|
-
|
|
3079
|
+
const behind = [...behindCount.entries()].map(([q, px]) => ({ part: q, px })).sort((a, b) => b.px - a.px || (a.part < b.part ? -1 : a.part > b.part ? 1 : 0));
|
|
3080
|
+
return { part, from, refusedPx, minPx, bodies, holders, ...(behind.length > 0 ? { behind } : {}) };
|
|
2982
3081
|
}
|
|
2983
3082
|
|
|
2984
3083
|
/**
|
|
@@ -2988,15 +3087,22 @@ export function driftBodies(part: string, from: string, refused: Mask, minPx: nu
|
|
|
2988
3087
|
* off the painting, max channel), in a body of 196 px at 16,12 14x14 (at
|
|
2989
3088
|
* least 150 px past a 5x5 opening); by the plan's draw order "face" holds 196
|
|
2990
3089
|
* px of it (the top-most part at alpha >= 250), drawn in front of "topwear"`.
|
|
3090
|
+
* When `behind` is non-empty (issue #351), names the part(s) drawn behind
|
|
3091
|
+
* `part` whose colour is within `DRIFT_LIMIT` of the painting.
|
|
2991
3092
|
*/
|
|
2992
3093
|
export function driftLine(d: DriftBodies): string {
|
|
2993
3094
|
const total = d.bodies.reduce((a, b) => a + b.px, 0);
|
|
2994
3095
|
const big = d.bodies[0];
|
|
2995
3096
|
const held = d.bodies.length > 1 ? `${d.bodies.length} bodies of ${total} px, the largest ${big.px} px at ${bodyBox(big)}` : `a body of ${big.px} px at ${bodyBox(big)}`;
|
|
2996
3097
|
const others = d.holders.filter((h) => h.part !== d.part).map((h) => `"${h.part}"`);
|
|
3098
|
+
const frontClause = others.length > 0 ? `, ${others.join(', ')} drawn in front of "${d.part}"` : '';
|
|
3099
|
+
const behindClause =
|
|
3100
|
+
d.behind !== undefined && d.behind.length > 0
|
|
3101
|
+
? `; the painting is within ${DRIFT_LIMIT} of ${d.behind.map((h) => `"${h.part}" (${h.px} px at alpha >= ${CORE_ALPHA}, drawn behind "${d.part}")`).join(', ')}`
|
|
3102
|
+
: '';
|
|
2997
3103
|
return (
|
|
2998
3104
|
`drift: "${d.part}" refuses ${d.refusedPx} px of ${d.from} (its colour more than ${DRIFT_LIMIT} off the painting, max channel), in ${held} (at least ${fmtPx(d.minPx)} px past a ${BODY_KERNEL}x${BODY_KERNEL} opening); ` +
|
|
2999
|
-
`by the plan's draw order ${d.holders.map((h) => `"${h.part}" holds ${h.px} px`).join(', ')} of it (the top-most part at alpha >= ${CORE_ALPHA})
|
|
3105
|
+
`by the plan's draw order ${d.holders.map((h) => `"${h.part}" holds ${h.px} px`).join(', ')} of it (the top-most part at alpha >= ${CORE_ALPHA})${frontClause}${behindClause}`
|
|
3000
3106
|
);
|
|
3001
3107
|
}
|
|
3002
3108
|
|
package/src/build.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
44
44
|
import { dirname, join, resolve } from 'node:path';
|
|
45
45
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
46
|
-
import { assemble, type AssembleResult, cutLines, hiddenLines, SINGLE_RUN_FACE_TAGS, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeLines, type ProjectRule, scenePhrase, type SeamRule, stageFields, uncoveredLine } from './assemble.ts';
|
|
46
|
+
import { assemble, type AssembleResult, cutLines, hiddenLines, SINGLE_RUN_FACE_TAGS, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, type ProjectRule, scenePhrase, type SeamRule, stageFields, uncoveredLine } from './assemble.ts';
|
|
47
47
|
import { allocationClause, type Reducer, stageBClause, unboundedClause } from './automesh.ts';
|
|
48
48
|
import { residualClause } from './autoenvelope.ts';
|
|
49
49
|
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, type ReplayCandidate, runComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
@@ -237,11 +237,12 @@ export function readCaps(configPath: string, limbs: Readonly<Record<string, Limb
|
|
|
237
237
|
|
|
238
238
|
/** Read, assemble, and write only after every refusal has had its chance. Throws a PartsError on a refusal, having written nothing. */
|
|
239
239
|
export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs, log: Log): AssembleResult {
|
|
240
|
-
const src = readSource(input.source);
|
|
241
|
-
const runs = readRuns(input.full, input.head);
|
|
242
240
|
// The early door: assemble runs before propose has drafted bones, meshes,
|
|
243
241
|
// regions and motion, so it requires only what it reads (issue #20).
|
|
244
|
-
const
|
|
242
|
+
const cfg = loadEarlyConfig(input.config, 'assemble');
|
|
243
|
+
const src = readSource(input.source);
|
|
244
|
+
const runs = readRuns(input.full, input.head);
|
|
245
|
+
const fields = stageFields(cfg, runs.single ? { w: src.width, h: src.height } : null);
|
|
245
246
|
const under = readUnders(input.config, fields.cuts);
|
|
246
247
|
const caps = fields.limbs === undefined ? undefined : readCaps(input.config, fields.limbs);
|
|
247
248
|
const result = assemble({ source: src, full: runs.full, head: runs.head, ...fields, under, ...(caps === undefined ? {} : { caps }), seamRule: input.seam, projectRule: input.project });
|
|
@@ -292,6 +293,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
292
293
|
const scene = result.parts.scene === undefined ? null : scenePhrase(result.parts.scene, W * H);
|
|
293
294
|
log(figuresLine(result.figures, scene));
|
|
294
295
|
for (const l of holeLines(result.figures, scene)) log(l);
|
|
296
|
+
if (result.figures.uncoveredErrorPx > 0) log(holeKindsLine(result.holeKinds, scene));
|
|
295
297
|
for (const u of result.uncovered) log(uncoveredLine(u, scene));
|
|
296
298
|
for (const d of result.drift) log(driftLine(d));
|
|
297
299
|
log(`wrote ${outs.partsJson}, ${result.images.length} PNG(s) in ${outs.partsDir}, ${outs.recomposite}, ${outs.errorMap} (uncovered error px red, covered error px blue, the painting grey)${figurePath === null ? '' : `, ${figurePath} (the figure, white, the recomposite was read inside)`}`);
|
|
@@ -1088,17 +1090,29 @@ export function judgementLine(name: string, line: JudgementLine | ReportedLine):
|
|
|
1088
1090
|
}
|
|
1089
1091
|
|
|
1090
1092
|
/**
|
|
1091
|
-
* The fold line (issues #320, #333) as the console prints it: the status,
|
|
1092
|
-
* read and at what rate, and then each folding mesh on a line of its
|
|
1093
|
-
*
|
|
1093
|
+
* The fold line (issues #320, #333, #363) as the console prints it: the status,
|
|
1094
|
+
* what was read and at what rate, and then each folding mesh on a line of its
|
|
1095
|
+
* own, with physics on and off side by side (and its physics constraints named
|
|
1096
|
+
* when only physics folds it), rigc's own words, and the declaration's why
|
|
1097
|
+
* where `may_fold` takes it.
|
|
1094
1098
|
*/
|
|
1095
1099
|
export function foldsLines(line: FoldsLine): string[] {
|
|
1096
|
-
const physics = line.physics_left_out === 0 ? '' : `, its ${line.physics_left_out} physics constraint(s)
|
|
1100
|
+
const physics = line.physics_left_out === 0 ? '' : `, with its ${line.physics_left_out} physics constraint(s) on and off`;
|
|
1097
1101
|
const head =
|
|
1098
1102
|
`${FOLD_LINE}: ${line.status} — ${line.animations.length} animation(s) read by rigc render --folds at ${line.fps} fps${physics} (${line.animations.join(', ')}): ` +
|
|
1099
1103
|
`${line.folding.length} folding mesh(es), ${line.folding_undeclared} not declared in may_fold (<= ${line.bar})` +
|
|
1100
1104
|
(line.declared_not_folding.length === 0 ? '' : `; may_fold declares ${line.declared_not_folding.map((d) => `slot "${d.slot}" in "${d.animation}"`).join(', ')}, which do(es) not fold`);
|
|
1101
|
-
return [
|
|
1105
|
+
return [
|
|
1106
|
+
head,
|
|
1107
|
+
...line.folding.map((r) => {
|
|
1108
|
+
const side =
|
|
1109
|
+
line.physics_left_out === 0
|
|
1110
|
+
? ''
|
|
1111
|
+
: ` — physics on: ${r.physics_on === null ? '0 frame(s)' : `${r.physics_on.frames} frame(s), worst frame ${r.physics_on.worst_frame} (${r.physics_on.worst_triangles}/${r.triangles} triangle(s))`}; physics off: ${r.physics_off === null ? '0 frame(s)' : `${r.physics_off.frames} frame(s), worst frame ${r.physics_off.worst_frame} (${r.physics_off.worst_triangles}/${r.triangles} triangle(s))`}` +
|
|
1112
|
+
(r.physics_constraints.length === 0 ? '' : `; physics constraint(s): ${r.physics_constraints.join(', ')}`);
|
|
1113
|
+
return ` folds ${r.line}${side}${r.declared === null ? '' : ` — declared in may_fold: ${r.declared}`}`;
|
|
1114
|
+
}),
|
|
1115
|
+
];
|
|
1102
1116
|
}
|
|
1103
1117
|
|
|
1104
1118
|
/**
|