@rig-c/parts 1.9.0 → 1.10.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 +5 -4
- package/cli.ts +128 -15
- package/docs/AUTHORING.md +128 -25
- package/docs/STABILITY.md +16 -7
- package/package.json +1 -1
- package/src/assemble.ts +100 -22
- package/src/autoenvelope.ts +2 -1
- package/src/automotion.ts +41 -0
- package/src/build.ts +69 -12
- package/src/check.ts +152 -33
- package/src/config.ts +52 -6
- package/src/contour.ts +40 -2
- package/src/headframe.ts +2 -1
- package/src/instruments.ts +9 -5
- package/src/layers.ts +12 -7
- package/src/length.ts +21 -0
- package/src/meshdraft.ts +887 -0
- package/src/parts.ts +41 -10
- package/src/propose.ts +184 -68
- package/src/requirements.ts +24 -8
- package/src/rig.ts +4 -3
- package/src/skeleton.ts +2 -1
- package/src/structure.ts +5 -4
- package/src/tags.ts +86 -0
- package/src/turn.ts +31 -0
package/src/assemble.ts
CHANGED
|
@@ -95,7 +95,7 @@ import type { CharacterConfig, Cut, EarlyConfig, Extend, Patch, PlanEntry, Run,
|
|
|
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 } from './parts.ts';
|
|
98
|
+
import { PAINTING_RUN, type PartRecord, type PartsFile, type RecompositeRecord, type SceneRecord } from './parts.ts';
|
|
99
99
|
import {
|
|
100
100
|
alphaComposite,
|
|
101
101
|
connectedComponents,
|
|
@@ -114,6 +114,7 @@ import {
|
|
|
114
114
|
warpAffine,
|
|
115
115
|
} from './raster/index.ts';
|
|
116
116
|
import { pyRound } from './round.ts';
|
|
117
|
+
import { AUTHOR_NO_ROLE, readAuthorTag } from './tags.ts';
|
|
117
118
|
|
|
118
119
|
const f32 = Math.fround;
|
|
119
120
|
|
|
@@ -237,6 +238,8 @@ export interface AssembleInput {
|
|
|
237
238
|
under?: ReadonlyArray<Raster | null>;
|
|
238
239
|
seamRule: SeamRule;
|
|
239
240
|
projectRule: ProjectRule;
|
|
241
|
+
/** `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
|
+
scene?: boolean;
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
/** The derived numbers of the two runs' geometry. */
|
|
@@ -1239,11 +1242,12 @@ function coveredMask(parts: PlacedPart[], W: number, H: number): Uint8Array {
|
|
|
1239
1242
|
return covered;
|
|
1240
1243
|
}
|
|
1241
1244
|
|
|
1242
|
-
/** Per rig pixel: 1 where the recomposite's max-channel difference from the painting is above `ERROR_LIMIT
|
|
1243
|
-
function errorMask(can: Raster, srcr: Raster): Uint8Array {
|
|
1245
|
+
/** Per rig pixel: 1 where the recomposite's max-channel difference from the painting is above `ERROR_LIMIT` — and, with `inside` (a scene, issue #242), the pixel is the figure's. */
|
|
1246
|
+
function errorMask(can: Raster, srcr: Raster, inside: Uint8Array | null = null): Uint8Array {
|
|
1244
1247
|
const n = srcr.width * srcr.height;
|
|
1245
1248
|
const err = new Uint8Array(n);
|
|
1246
1249
|
for (let p = 0; p < n; p++) {
|
|
1250
|
+
if (inside !== null && inside[p] === 0) continue;
|
|
1247
1251
|
let m = 0;
|
|
1248
1252
|
for (let c = 0; c < 3; c++) m = Math.max(m, Math.abs(can.data[p * 4 + c] - srcr.data[p * 4 + c]));
|
|
1249
1253
|
if (m > ERROR_LIMIT) err[p] = 1;
|
|
@@ -1270,10 +1274,10 @@ function errorMask(can: Raster, srcr: Raster): Uint8Array {
|
|
|
1270
1274
|
* This is not in the reference, whose report stopped at the count. The count
|
|
1271
1275
|
* cannot say where to look; a box and the parts round it can.
|
|
1272
1276
|
*/
|
|
1273
|
-
export function uncoveredHoles(can: Raster, srcr: Raster, parts: PlacedPart[]): { count: number; holes: RecompositeHole[] } {
|
|
1277
|
+
export function uncoveredHoles(can: Raster, srcr: Raster, parts: PlacedPart[], inside: Uint8Array | null = null): { count: number; holes: RecompositeHole[] } {
|
|
1274
1278
|
const { width: W, height: H } = srcr;
|
|
1275
1279
|
const covered = coveredMask(parts, W, H);
|
|
1276
|
-
const err = errorMask(can, srcr);
|
|
1280
|
+
const err = errorMask(can, srcr, inside);
|
|
1277
1281
|
const mask = newMask(W, H);
|
|
1278
1282
|
for (let p = 0; p < W * H; p++) if (err[p] === 1 && covered[p] === 0) mask.data[p] = 1;
|
|
1279
1283
|
const cc = connectedComponents(mask, 8);
|
|
@@ -1312,14 +1316,23 @@ export function uncoveredHoles(can: Raster, srcr: Raster, parts: PlacedPart[]):
|
|
|
1312
1316
|
return { count: all.length, holes };
|
|
1313
1317
|
}
|
|
1314
1318
|
|
|
1315
|
-
|
|
1319
|
+
/**
|
|
1320
|
+
* The recomposite's four figures and its holes. With `inside` (a scene, issue
|
|
1321
|
+
* #242; {@link sceneFigure}) every one is read over the figure's pixels alone:
|
|
1322
|
+
* the mean and the within share are over them, and a pixel outside is neither an
|
|
1323
|
+
* error pixel nor a hole's. Without it, over the whole rig, as always.
|
|
1324
|
+
*/
|
|
1325
|
+
export function measureRecomposite(can: Raster, srcr: Raster, parts: PlacedPart[], inside: Uint8Array | null = null): RecompositeFigures {
|
|
1316
1326
|
const { width: W, height: H } = srcr;
|
|
1317
1327
|
const covered = coveredMask(parts, W, H);
|
|
1318
1328
|
let sum = 0;
|
|
1319
1329
|
let within = 0;
|
|
1320
1330
|
let errorPx = 0;
|
|
1321
1331
|
let uncoveredErrorPx = 0;
|
|
1332
|
+
let read = 0;
|
|
1322
1333
|
for (let p = 0; p < W * H; p++) {
|
|
1334
|
+
if (inside !== null && inside[p] === 0) continue;
|
|
1335
|
+
read++;
|
|
1323
1336
|
let s = 0;
|
|
1324
1337
|
let m = 0;
|
|
1325
1338
|
for (let c = 0; c < 3; c++) {
|
|
@@ -1335,8 +1348,44 @@ export function measureRecomposite(can: Raster, srcr: Raster, parts: PlacedPart[
|
|
|
1335
1348
|
if (covered[p] === 0) uncoveredErrorPx++;
|
|
1336
1349
|
}
|
|
1337
1350
|
}
|
|
1338
|
-
const { count, holes } = uncoveredHoles(can, srcr, parts);
|
|
1339
|
-
|
|
1351
|
+
const { count, holes } = uncoveredHoles(can, srcr, parts, inside);
|
|
1352
|
+
// Over the whole rig `read` is W * H, and the two quotients are the ones they always were.
|
|
1353
|
+
return { meanAbs: inside === null ? sum / (W * H) : read === 0 ? 0 : sum / read, within: inside === null ? within / (W * H) : read === 0 ? 0 : within / read, errorPx, uncoveredErrorPx, holeCount: count, holes };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/** The figure mask's file name, beside the recomposite (issue #242): `recomposite_rig.png` -> `figure_rig.png`. */
|
|
1357
|
+
export const FIGURE_MASK_FILE = 'figure_rig.png';
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* The figure of a scene painting (issue #242): per rig pixel, 1 where any
|
|
1361
|
+
* full-run layer — See-through's decomposition of the whole figure, each layer
|
|
1362
|
+
* placed on the rig as the stage places it — has alpha above
|
|
1363
|
+
* `OPAQUE_ALPHA_ABOVE` (8), the cut this package already reads a layer's
|
|
1364
|
+
* painted pixels by (ghost clean-up, the sheets, every part's opaque count).
|
|
1365
|
+
* The threshold is that one, not a new one: a pixel no layer paints is the
|
|
1366
|
+
* scene's, and a pixel any layer paints is the figure's, by the same rule that
|
|
1367
|
+
* says what a layer holds. The head run is not read: it is a crop of the same
|
|
1368
|
+
* figure, inside the full run's.
|
|
1369
|
+
*/
|
|
1370
|
+
export function sceneFigure(fullRig: readonly Raster[], W: number, H: number): Uint8Array {
|
|
1371
|
+
const inside = new Uint8Array(W * H);
|
|
1372
|
+
for (const r of fullRig) for (let p = 0; p < W * H; p++) if (r.data[p * 4 + 3] > OPAQUE_ALPHA_ABOVE) inside[p] = 1;
|
|
1373
|
+
return inside;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/** The figure as an image, the mask `assemble` writes beside the recomposite: opaque white where the figure is, opaque black elsewhere. */
|
|
1377
|
+
export function figureImage(inside: Uint8Array, W: number, H: number): Raster {
|
|
1378
|
+
const r = newRaster(W, H);
|
|
1379
|
+
for (let p = 0; p < W * H; p++) {
|
|
1380
|
+
const v = inside[p] === 1 ? 255 : 0;
|
|
1381
|
+
r.data.set([v, v, v, 255], p * 4);
|
|
1382
|
+
}
|
|
1383
|
+
return r;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/** What a scene declaration says first on each recomposite line (issue #242): where it was read, and the figure's size against the rig's. */
|
|
1387
|
+
export function scenePhrase(s: SceneRecord, rigPx: number): string {
|
|
1388
|
+
return `inside the figure (config.scene: the union of the full run's layers at alpha above ${s.figure_alpha_above}, ${s.figure_px} of ${rigPx} rig px)`;
|
|
1340
1389
|
}
|
|
1341
1390
|
|
|
1342
1391
|
/** The error map's colour for an uncovered error pixel: no part is there, so the page shows through. */
|
|
@@ -1359,10 +1408,10 @@ export const MAP_MISMATCHED: readonly [number, number, number] = [0, 0, 255];
|
|
|
1359
1408
|
* both), so the two colours mean exactly the two masks. Integer arithmetic
|
|
1360
1409
|
* only, so the bytes are deterministic.
|
|
1361
1410
|
*/
|
|
1362
|
-
export function recompositeErrorMap(can: Raster, srcr: Raster, parts: PlacedPart[]): Raster {
|
|
1411
|
+
export function recompositeErrorMap(can: Raster, srcr: Raster, parts: PlacedPart[], inside: Uint8Array | null = null): Raster {
|
|
1363
1412
|
const { width: W, height: H } = srcr;
|
|
1364
1413
|
const covered = coveredMask(parts, W, H);
|
|
1365
|
-
const err = errorMask(can, srcr);
|
|
1414
|
+
const err = errorMask(can, srcr, inside);
|
|
1366
1415
|
const map = newRaster(W, H);
|
|
1367
1416
|
for (let p = 0; p < W * H; p++) {
|
|
1368
1417
|
const s = p * 4;
|
|
@@ -1381,9 +1430,9 @@ export function recompositeErrorMap(can: Raster, srcr: Raster, parts: PlacedPart
|
|
|
1381
1430
|
return map;
|
|
1382
1431
|
}
|
|
1383
1432
|
|
|
1384
|
-
export function figuresLine(f: RecompositeFigures): string {
|
|
1433
|
+
export function figuresLine(f: RecompositeFigures, scene: string | null = null): string {
|
|
1385
1434
|
return (
|
|
1386
|
-
`recomposite vs source: mean |d|=${f.meanAbs.toFixed(2)}, within ${WITHIN_LIMIT}: ${(100 * f.within).toFixed(1)}%, ` +
|
|
1435
|
+
`${scene === null ? '' : `${scene}: `}recomposite vs source: mean |d|=${f.meanAbs.toFixed(2)}, within ${WITHIN_LIMIT}: ${(100 * f.within).toFixed(1)}%, ` +
|
|
1387
1436
|
`error px > ${ERROR_LIMIT}: ${f.errorPx}, uncovered error px: ${f.uncoveredErrorPx}`
|
|
1388
1437
|
);
|
|
1389
1438
|
}
|
|
@@ -1398,8 +1447,8 @@ export function holeBox(h: RecompositeHole): string {
|
|
|
1398
1447
|
* hole — `uncovered hole 1: 2340 px at 412,1088 30x78 (between "legwear_r"
|
|
1399
1448
|
* 120 px, "legwear_l" 96 px)`, or `(borders no part)`.
|
|
1400
1449
|
*/
|
|
1401
|
-
export function holeLines(f: RecompositeFigures): string[] {
|
|
1402
|
-
const head = `uncovered holes (8-connected): ${f.holeCount}${f.holeCount > f.holes.length ? `, the largest ${f.holes.length} listed` : ''}`;
|
|
1450
|
+
export function holeLines(f: RecompositeFigures, scene: string | null = null): string[] {
|
|
1451
|
+
const head = `${scene === null ? '' : `${scene}: `}uncovered holes (8-connected): ${f.holeCount}${f.holeCount > f.holes.length ? `, the largest ${f.holes.length} listed` : ''}`;
|
|
1403
1452
|
return [
|
|
1404
1453
|
head,
|
|
1405
1454
|
...f.holes.map(
|
|
@@ -1665,6 +1714,8 @@ export interface AssembleResult {
|
|
|
1665
1714
|
recomposite: Raster;
|
|
1666
1715
|
/** `recompositeErrorMap`: uncovered error pixels red, covered ones blue, the painting dimmed grey. */
|
|
1667
1716
|
errorMap: Raster;
|
|
1717
|
+
/** Under `config.scene` (issue #242), the figure the recomposite was read inside ({@link figureImage}); null otherwise. */
|
|
1718
|
+
figure: Raster | null;
|
|
1668
1719
|
figures: RecompositeFigures;
|
|
1669
1720
|
/** What each `assemble.cuts` entry took, counted when it was made (`cutLines`); empty without cuts. */
|
|
1670
1721
|
cuts: CutCount[];
|
|
@@ -2113,12 +2164,15 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2113
2164
|
|
|
2114
2165
|
seamOverride(placed, srcr, seamRule, seamRule === 'silhouette' ? silhouette : null);
|
|
2115
2166
|
const can = recomposite(placed, W, H);
|
|
2116
|
-
|
|
2167
|
+
// Issue #242: on a scene painting the figure is read off the full run, and every recomposite figure inside it.
|
|
2168
|
+
const inside = input.scene === true ? sceneFigure([...rig.entries()].filter(([k]) => k.startsWith('full:')).map(([, r]) => r), W, H) : null;
|
|
2169
|
+
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 };
|
|
2170
|
+
const figures = measureRecomposite(can, srcr, placed, inside);
|
|
2117
2171
|
const fullRig = fullTags.map((tag) => ({ tag, rig: rig.get(`full:${tag}`) as Raster }));
|
|
2118
2172
|
const uncovered = uncoveredErrorLayers(
|
|
2119
2173
|
fullRig,
|
|
2120
2174
|
coveredMask(placed, W, H),
|
|
2121
|
-
errorMask(can, srcr),
|
|
2175
|
+
errorMask(can, srcr, inside),
|
|
2122
2176
|
figures.uncoveredErrorPx,
|
|
2123
2177
|
plan,
|
|
2124
2178
|
extend,
|
|
@@ -2128,10 +2182,11 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2128
2182
|
bodyMinPx(frame),
|
|
2129
2183
|
);
|
|
2130
2184
|
return {
|
|
2131
|
-
parts: { rig_size: [W, H], scale_rig_per_source: frame.S, parts: placed.map((p) => p.record), ghost_px: ghost, recomposite: recompositeRecord(figures) },
|
|
2185
|
+
parts: { rig_size: [W, H], scale_rig_per_source: frame.S, parts: placed.map((p) => p.record), ghost_px: ghost, recomposite: recompositeRecord(figures), ...(sceneRecord === null ? {} : { scene: sceneRecord }) },
|
|
2132
2186
|
images: placed,
|
|
2133
2187
|
recomposite: can,
|
|
2134
|
-
errorMap: recompositeErrorMap(can, srcr, placed),
|
|
2188
|
+
errorMap: recompositeErrorMap(can, srcr, placed, inside),
|
|
2189
|
+
figure: inside === null ? null : figureImage(inside, W, H),
|
|
2135
2190
|
figures,
|
|
2136
2191
|
cuts: cutCounts,
|
|
2137
2192
|
uncovered,
|
|
@@ -2459,12 +2514,12 @@ export function uncoveredErrorLayers(
|
|
|
2459
2514
|
* of the 240 uncovered error px lie on the full run's "back hair", in a body
|
|
2460
2515
|
* of 240 px at 36,22 8x30; hair_back takes the head run's "back hair"; …`.
|
|
2461
2516
|
*/
|
|
2462
|
-
export function uncoveredLine(d: UncoveredErrorLayer): string {
|
|
2517
|
+
export function uncoveredLine(d: UncoveredErrorLayer, scene: string | null = null): string {
|
|
2463
2518
|
const u = d.layer;
|
|
2464
2519
|
const total = u.bodies.reduce((a, b) => a + b.px, 0);
|
|
2465
2520
|
const big = u.bodies[0];
|
|
2466
2521
|
const held = u.bodies.length > 1 ? `${u.bodies.length} bodies of ${total} px, the largest ${big.px} px at ${bodyBox(big)}` : `a body of ${big.px} px at ${bodyBox(big)}`;
|
|
2467
|
-
return `uncovered: ${d.errorPx} of the ${d.of} uncovered error px lie on the full run's "${u.tag}", in ${held}; ${u.rule}`;
|
|
2522
|
+
return `${scene === null ? '' : `${scene}: `}uncovered: ${d.errorPx} of the ${d.of} uncovered error px lie on the full run's "${u.tag}", in ${held}; ${u.rule}`;
|
|
2468
2523
|
}
|
|
2469
2524
|
|
|
2470
2525
|
/**
|
|
@@ -2534,7 +2589,10 @@ export const PROPOSE_ALIAS: Readonly<Record<string, string>> = {
|
|
|
2534
2589
|
earwear: 'earring',
|
|
2535
2590
|
};
|
|
2536
2591
|
|
|
2592
|
+
/** An author-made layer's part is named by its label as the author wrote it (issue #256): the label is already a part name (`authorLabelOk`). */
|
|
2537
2593
|
function aliasOf(tag: string): string {
|
|
2594
|
+
const author = readAuthorTag(tag);
|
|
2595
|
+
if (author !== null) return author.label;
|
|
2538
2596
|
return PROPOSE_ALIAS[tag] ?? tag.replaceAll(' ', '_').replaceAll('-', '_');
|
|
2539
2597
|
}
|
|
2540
2598
|
|
|
@@ -2587,6 +2645,15 @@ function fmt0(v: number): string {
|
|
|
2587
2645
|
* changed. [observed] reference-sample's proposal is the reference's byte for
|
|
2588
2646
|
* byte; demo's, reference-demo's and scarf's each gain one note, on the hair
|
|
2589
2647
|
* their configs had already corrected by hand.
|
|
2648
|
+
*
|
|
2649
|
+
* ⚖️ This port's too (issue #256): an author-made layer (`author:<label>`,
|
|
2650
|
+
* `src/tags.ts`) is proposed from the run that holds it, at its place in that
|
|
2651
|
+
* run's draw order, as the part named by its label, under the same floor and
|
|
2652
|
+
* plausibility rules as any layer — its draw position is then the plan's,
|
|
2653
|
+
* where the author moves it. A note per such part says it has no tag and so
|
|
2654
|
+
* no role. One both runs hold is proposed once, from the full run, and a note
|
|
2655
|
+
* says how to take the head run's too. No public example holds one, so no
|
|
2656
|
+
* proposal of theirs moves.
|
|
2590
2657
|
*/
|
|
2591
2658
|
export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx = PROPOSE_MIN_PX): PlanProposal {
|
|
2592
2659
|
const frame = checkGeometry(g, { full, head });
|
|
@@ -2611,7 +2678,10 @@ export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx =
|
|
|
2611
2678
|
return tag !== 'nose' && l !== undefined && l.opaqueRunPx >= minPx && !implausible(run, tag, 'not proposed');
|
|
2612
2679
|
};
|
|
2613
2680
|
const body = runs.full.filter((l) => !PROPOSE_HEAD_TAGS.includes(l.tag) && ok('full', l.tag)).map((l) => l.tag);
|
|
2614
|
-
|
|
2681
|
+
// Issue #256: an author-made layer is proposed from the run that holds it, at its place in that run's draw order,
|
|
2682
|
+
// like any layer; one both runs hold is proposed once, from the full run when the full run's is proposed, and the note says so.
|
|
2683
|
+
const authorBoth = runs.head.filter((l) => readAuthorTag(l.tag) !== null && body.includes(l.tag)).map((l) => l.tag);
|
|
2684
|
+
const heads = runs.head.filter((l) => (PROPOSE_HEAD_TAGS.includes(l.tag) || (readAuthorTag(l.tag) !== null && !authorBoth.includes(l.tag))) && ok('head', l.tag)).map((l) => l.tag);
|
|
2615
2685
|
const hw = body.filter((t) => t.startsWith('handwear'));
|
|
2616
2686
|
const plan: PlanEntry[] = [];
|
|
2617
2687
|
const drop = (t: string): void => {
|
|
@@ -2647,6 +2717,12 @@ export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx =
|
|
|
2647
2717
|
}
|
|
2648
2718
|
}
|
|
2649
2719
|
for (const t of heads) plan.push([aliasOf(t), 'head', t]);
|
|
2720
|
+
for (const [part, run, tag] of plan) {
|
|
2721
|
+
if (readAuthorTag(tag) !== null) notes.push(`${tag}: ${AUTHOR_NO_ROLE}; proposed as "${part}" from the ${run} run, at its place in that run's draw order`);
|
|
2722
|
+
}
|
|
2723
|
+
for (const tag of authorBoth) {
|
|
2724
|
+
notes.push(`${tag}: the head run holds a layer of this name too, and the proposer drafts one part per author-made layer, from the full run; to take the head run's as well, add ["<a part name>", "head", "${tag}"] to the plan`);
|
|
2725
|
+
}
|
|
2650
2726
|
const extend: Extend[] = [];
|
|
2651
2727
|
/** A hair tag the proposer did not continue below the crop: how far its full-run layer reaches, rig px (null: no opaque row). */
|
|
2652
2728
|
const reaches = new Map<string, number | null>();
|
|
@@ -2719,7 +2795,7 @@ export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx =
|
|
|
2719
2795
|
* the loader, because `inputs` runs before it exists; `seethrough` too, for a
|
|
2720
2796
|
* full-loader config, where it is optional.
|
|
2721
2797
|
*/
|
|
2722
|
-
export function stageFields(cfg: Pick<CharacterConfig, 'seethrough' | 'assemble'>): {
|
|
2798
|
+
export function stageFields(cfg: Pick<CharacterConfig, 'seethrough' | 'assemble' | 'scene'>): {
|
|
2723
2799
|
resolution: number;
|
|
2724
2800
|
headBox: [number, number, number, number];
|
|
2725
2801
|
rigScale: number;
|
|
@@ -2727,6 +2803,7 @@ export function stageFields(cfg: Pick<CharacterConfig, 'seethrough' | 'assemble'
|
|
|
2727
2803
|
extend: Extend[];
|
|
2728
2804
|
patches: Patch[];
|
|
2729
2805
|
cuts: Cut[];
|
|
2806
|
+
scene?: boolean;
|
|
2730
2807
|
} {
|
|
2731
2808
|
const problems: Problem[] = [];
|
|
2732
2809
|
if (cfg.seethrough === undefined) {
|
|
@@ -2744,6 +2821,7 @@ export function stageFields(cfg: Pick<CharacterConfig, 'seethrough' | 'assemble'
|
|
|
2744
2821
|
extend: cfg.assemble.extend_below_crop ?? [],
|
|
2745
2822
|
patches: cfg.assemble.patches ?? [],
|
|
2746
2823
|
cuts: cfg.assemble.cuts ?? [],
|
|
2824
|
+
...(cfg.scene === undefined ? {} : { scene: true }),
|
|
2747
2825
|
};
|
|
2748
2826
|
}
|
|
2749
2827
|
|
package/src/autoenvelope.ts
CHANGED
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
import { type BoneMotionRange, type EnvelopeBoneRanges, MeshReductionError, type MeshQualityReport, type MeshReductionInput, type SkinningEnvelope, type SkinningEnvelopeBone, skinningEnvelopeBone } from 'rig-c/mesh';
|
|
83
83
|
import type { ConfigConstraint } from './config.ts';
|
|
84
84
|
import type { MotionKey, MotionSpec } from './motion.ts';
|
|
85
|
+
import { edgeLength } from './length.ts';
|
|
85
86
|
|
|
86
87
|
/** What the derivation reads off the rig besides the part: the bones (name, parent), each bone's setup joint in crop px, the idle as written, the constraints. */
|
|
87
88
|
export interface EnvelopeBasis {
|
|
@@ -168,7 +169,7 @@ export function boneRange(basis: EnvelopeBasis, bone: string, origin: readonly [
|
|
|
168
169
|
rotate,
|
|
169
170
|
scaleX,
|
|
170
171
|
scaleY,
|
|
171
|
-
translate:
|
|
172
|
+
translate: edgeLength(tx, ty),
|
|
172
173
|
setup: { ...SETUP },
|
|
173
174
|
};
|
|
174
175
|
}
|
package/src/automotion.ts
CHANGED
|
@@ -344,6 +344,47 @@ export function motionRowText(r: MeasureRow): string {
|
|
|
344
344
|
return `${r.code}${region} ${r.state}: ${r.reason ?? ''}`;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Where a row of a comparison was read at its worst, in the words a reader can act on (issue #280), with its
|
|
349
|
+
* preposition: a declared stimulus by its animation's name (`at stimulus tail_l -10`, the frame rig-c ids
|
|
350
|
+
* `stimulus tail_l -10@explicit@0`), an idle frame as `at idle frame <id>`, a geometry row — read on the mesh, at
|
|
351
|
+
* no frame — as `in its geometry`, and a motion row that names no frame as `on the idle`.
|
|
352
|
+
*/
|
|
353
|
+
export function readingPlace(r: MeasureRow, stimulus: readonly StimulusPose[] | null, geometry: boolean): { text: string; stimulus: boolean } {
|
|
354
|
+
const id = r.worst?.frame?.id;
|
|
355
|
+
if (id === undefined) return { text: geometry ? 'in its geometry' : 'on the idle', stimulus: false };
|
|
356
|
+
const name = (stimulus ?? []).flatMap((st) => stimulusAnimationNames(st)).find((n) => explicitFrameId(n) === id);
|
|
357
|
+
return name === undefined ? { text: `at idle frame ${id}`, stimulus: false } : { text: `at ${name}`, stimulus: true };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Issue #280: the source compared with itself failed, so no replayed step can pass — the one sentence that says
|
|
362
|
+
* so (`the unreduced source itself fails <where> (<row> <value> against <bound>), so …`): where (a declared stimulus by name, or the idle frame), each failing row and its value against its bound,
|
|
363
|
+
* and the remedy, which is the source's (its spacing or outline), or the stimulus's when the failure is read at
|
|
364
|
+
* one. A row the source fails against itself is an absolute reading (`MQ_INVERSION` counts the source's own
|
|
365
|
+
* flipped triangles), which is why no reduction of it can pass. Null when the comparison has no failing row.
|
|
366
|
+
*/
|
|
367
|
+
export function sourceSelfFailure(self: MeshQualityReport, stimulus: readonly StimulusPose[] | null): { where: string; sentence: string } | null {
|
|
368
|
+
const rows = failingRows(self);
|
|
369
|
+
if (rows.length === 0) return null;
|
|
370
|
+
const geometryRows = new Set(self.candidates[0]?.geometry?.rows ?? []);
|
|
371
|
+
const places = rows.map((r) => readingPlace(r, stimulus, geometryRows.has(r)));
|
|
372
|
+
const distinct = [...new Set(places.map((p) => p.text))];
|
|
373
|
+
const where = distinct.join(', ');
|
|
374
|
+
// One place: the rows say only their values; several: each row says its own.
|
|
375
|
+
const read = rows
|
|
376
|
+
.map((r, i) => {
|
|
377
|
+
const at = distinct.length > 1 ? ` ${places[i].text}` : '';
|
|
378
|
+
return r.state === 'fail' || r.state === 'pass' || r.state === 'undeclared' ? `${r.code} ${r.value}${r.bound === null ? '' : ` against ${r.bound.op} ${r.bound.value}`}${at}` : `${r.code} ${r.state}${at}: ${r.reason ?? ''}`;
|
|
379
|
+
})
|
|
380
|
+
.join('; ');
|
|
381
|
+
const atStimulus = places.some((p) => p.stimulus);
|
|
382
|
+
return {
|
|
383
|
+
where,
|
|
384
|
+
sentence: `the unreduced source itself fails ${where} (${read}), so no replayed step can pass — change the source (its spacing or its outline)${atStimulus ? ' or lower the stimulus' : ''}`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
347
388
|
/** The candidate's rows with a declared bound that did not pass, geometry then motion. */
|
|
348
389
|
export function failingRows(report: MeshQualityReport): MeasureRow[] {
|
|
349
390
|
const c = report.candidates[0];
|
package/src/build.ts
CHANGED
|
@@ -43,10 +43,10 @@
|
|
|
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, driftLine, figuresLine, holeLines, type ProjectRule, type SeamRule, stageFields, uncoveredLine } from './assemble.ts';
|
|
46
|
+
import { assemble, type AssembleResult, cutLines, driftLine, FIGURE_MASK_FILE, figuresLine, 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
|
-
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, type ReplayCandidate, runComparison, stimulusMotion, withStimulus } from './automotion.ts';
|
|
49
|
+
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, type ReplayCandidate, runComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
50
50
|
import {
|
|
51
51
|
bisectAccepted,
|
|
52
52
|
budgetProblem,
|
|
@@ -78,11 +78,12 @@ import { encodeGif } from './gif.ts';
|
|
|
78
78
|
import { CONTROL_SUFFIX, withAnimations } from './motion.ts';
|
|
79
79
|
import type { PaletteError } from './palette.ts';
|
|
80
80
|
import { type LayerSet, readLayers } from './layers.ts';
|
|
81
|
-
import { type PartRecord, readParts, writeParts } from './parts.ts';
|
|
81
|
+
import { type PartRecord, readParts, tagOf, writeParts } from './parts.ts';
|
|
82
82
|
import { encodePngBytes, readPng, writePng } from './raster/png.ts';
|
|
83
83
|
import type { Raster } from './raster/types.ts';
|
|
84
84
|
import { readRequirements, type RequirementLine, summaryText } from './requirements.ts';
|
|
85
85
|
import { type AutoMeshReport, buildRig, DEFAULT_IDLE_KEYS, type FaceAxis, type IdleKeys, type MeshReport, type RigCommand, rigJsonText, type RigOutput, type RigSpec, type StrayCleared, type TurnRow } from './rig.ts';
|
|
86
|
+
import { AUTHOR_NO_ROLE, isAuthorTag } from './tags.ts';
|
|
86
87
|
import { TURN_BONE } from './turn.ts';
|
|
87
88
|
import { pyRound } from './round.ts';
|
|
88
89
|
|
|
@@ -161,6 +162,16 @@ export function fringeLines(parts: readonly PartRecord[]): string[] {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* One line per part whose layer the author made (issue #256), in draw order:
|
|
167
|
+
* ` author-made: part "hand" (full:author:hand) — an author-made layer, …`.
|
|
168
|
+
* No line when no part came from one, so a plan of See-through layers prints
|
|
169
|
+
* what it printed before.
|
|
170
|
+
*/
|
|
171
|
+
export function authorPartLines(parts: readonly PartRecord[]): string[] {
|
|
172
|
+
return parts.filter((p) => isAuthorTag(tagOf(p.from) ?? '')).map((p) => ` author-made: part "${p.name}" (${p.from}) — ${AUTHOR_NO_ROLE}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
164
175
|
/**
|
|
165
176
|
* Each cut's `under` image (issue #193), read from its path relative to the
|
|
166
177
|
* config's directory, null for a cut without one. Every cut is read before
|
|
@@ -207,6 +218,9 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
207
218
|
for (const { record, image } of result.images) writePng(join(outs.partsDir, `${record.name}.png`), image);
|
|
208
219
|
writePng(outs.recomposite, result.recomposite);
|
|
209
220
|
writePng(outs.errorMap, result.errorMap);
|
|
221
|
+
// Issue #242: under config.scene, the figure the recomposite was read inside, beside the recomposite.
|
|
222
|
+
const figurePath = result.figure === null ? null : join(dirname(outs.recomposite), FIGURE_MASK_FILE);
|
|
223
|
+
if (figurePath !== null) writePng(figurePath, result.figure as Raster);
|
|
210
224
|
const [W, H] = result.parts.rig_size;
|
|
211
225
|
log(`rig-parts assemble: ${result.images.length} part(s) on a ${W}x${H} rig (${result.parts.scale_rig_per_source} rig px per source px), seam rule ${result.seamRule}, projection rule ${result.projectRule}`);
|
|
212
226
|
for (const p of result.parts.parts) {
|
|
@@ -216,6 +230,8 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
216
230
|
`unproj=${String(p.visible_not_projected_px).padStart(5)} drift=${String(p.refused_drift_px).padStart(5)} merged=${p.merged_px} seam=${p.seam_override_px}`,
|
|
217
231
|
);
|
|
218
232
|
}
|
|
233
|
+
// Issue #256: one line per part from an author-made layer, so it never reads as See-through's. None without one.
|
|
234
|
+
for (const l of authorPartLines(result.parts.parts)) log(l);
|
|
219
235
|
// One line per plan part an assemble.cuts entry cut, before the fringe lines: the cut is made first. None without cuts.
|
|
220
236
|
for (const l of cutLines(result.cuts)) log(l);
|
|
221
237
|
for (const l of fringeLines(result.parts.parts)) log(l);
|
|
@@ -228,11 +244,12 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
228
244
|
);
|
|
229
245
|
const ghosts = Object.values(result.parts.ghost_px).reduce((a, b) => a + b, 0);
|
|
230
246
|
log(` ghost px removed: ${ghosts} over ${Object.keys(result.parts.ghost_px).length} layer(s)`);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (const
|
|
247
|
+
const scene = result.parts.scene === undefined ? null : scenePhrase(result.parts.scene, W * H);
|
|
248
|
+
log(figuresLine(result.figures, scene));
|
|
249
|
+
for (const l of holeLines(result.figures, scene)) log(l);
|
|
250
|
+
for (const u of result.uncovered) log(uncoveredLine(u, scene));
|
|
234
251
|
for (const d of result.drift) log(driftLine(d));
|
|
235
|
-
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)`);
|
|
252
|
+
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)`}`);
|
|
236
253
|
return result;
|
|
237
254
|
}
|
|
238
255
|
|
|
@@ -582,8 +599,12 @@ export function replaySearch(
|
|
|
582
599
|
const self = runComparison(c.object, motionInput(c, motion, refModel, refModel, probeSchedule));
|
|
583
600
|
if ('code' in self) return refuse(self);
|
|
584
601
|
if (motionVerdict(c.object, self) !== null) {
|
|
585
|
-
|
|
586
|
-
|
|
602
|
+
// Issue #280: the refusal says, in its own sentence, that the source fails itself — where, the row, the remedy;
|
|
603
|
+
// the reduction's reading follows it for the record. The line names the stimulus where it is read at one.
|
|
604
|
+
const why = sourceSelfFailure(self, c.stimulus);
|
|
605
|
+
lines.push(`replay "${c.part}": the unreduced source is not accepted against itself ${why === null ? 'on its selection frames' : why.where}; no search runs`);
|
|
606
|
+
const problems = why === null ? run.problems : run.problems.map((p) => (p.code === 'AUTO_MESH_MOTION' ? { ...p, detail: `${why.sentence}. The reduction's own reading, for the record: ${p.detail}` } : p));
|
|
607
|
+
return { part: c.part, chosen: null, problems, lines };
|
|
587
608
|
}
|
|
588
609
|
const selection = motion.selection;
|
|
589
610
|
if (selection !== undefined && selection.maxProbes < maxReplays(N, I)) return refuse(budgetProblem(c.object, selection.maxProbes, N, I));
|
|
@@ -812,7 +833,7 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
812
833
|
const c = m.contour;
|
|
813
834
|
const stray = c.strayIslands === 0 ? '' : ` left out ${c.strayIslands} island(s), ${c.strayPixels} px`;
|
|
814
835
|
const regions = m.regions.map((rg) => ` region ${rg.name}->${rg.bone} reaches ${rg.reached} (${rg.whole} whole)`).join('');
|
|
815
|
-
log(`${head}contour tol=${m.params.tolerance} margin=${m.params.margin} spacing=${m.params.spacing} ${infl} overshoot ${c.overshoot} min angle ${c.smallestAngle.value}${stray}${regions}`);
|
|
836
|
+
log(`${head}contour tol=${m.params.tolerance} margin=${m.params.margin} spacing=${m.params.spacing}${m.params.maxEdge === undefined ? '' : ` maxEdge=${m.params.maxEdge} (outline +${c.outlineCut ?? 0})`} ${infl} overshoot ${c.overshoot} min angle ${c.smallestAngle.value}${stray}${regions}`);
|
|
816
837
|
} else log(`${head}grid=${m.grid} ${infl} loop passes ${rig.loopPasses[m.part]}`);
|
|
817
838
|
}
|
|
818
839
|
const regions = rig.rig.slots.length - rig.meshReport.length;
|
|
@@ -1024,7 +1045,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
1024
1045
|
if (r.seamViewport === null || fig.seam_mean === null) log(` seam: SKIP — ${fig.skipped?.seam ?? ''}`);
|
|
1025
1046
|
else {
|
|
1026
1047
|
log(
|
|
1027
|
-
` seam: setup pose at ${r.seamViewport.pixelWidth}x${r.seamViewport.pixelHeight}, scale ${r.seamViewport.scale.toFixed(4)}: mean |d| ${fig.seam_mean} (<= ${SEAM_MEAN_BAR.toFixed(1)}), ` +
|
|
1048
|
+
` seam${fig.seam_border === undefined ? '' : ` (config.scene: ${fig.seam_border.frame_px_left_out} frame px on the painting's border rows and columns left out, ${fig.seam_border.px_over_40_left_out} of them over ${SEAM_PX_LEVEL})`}: setup pose at ${r.seamViewport.pixelWidth}x${r.seamViewport.pixelHeight}, scale ${r.seamViewport.scale.toFixed(4)}: mean |d| ${fig.seam_mean} (<= ${SEAM_MEAN_BAR.toFixed(1)}), ` +
|
|
1028
1049
|
`${fig.seam_px_over_40} px over ${SEAM_PX_LEVEL}, ${fig.seam_frame_scale === null || fig.seam_px_over_40 === null ? '' : seamRigPxText(fig.seam_px_over_40, fig.seam_frame_scale)} rig px (the count / s²; <= ${SEAM_PX_BAR}), ${fig.seam_px_over_80} px over ${SEAM_PX_LEVEL_HIGH} (reported)`,
|
|
1029
1050
|
);
|
|
1030
1051
|
const pairs = fig.seam_pairs ?? [];
|
|
@@ -1274,6 +1295,40 @@ export interface BuildInput {
|
|
|
1274
1295
|
* `check/idle_frames/`; absent, the loop is as before.
|
|
1275
1296
|
*/
|
|
1276
1297
|
leadIn?: number;
|
|
1298
|
+
/** `--seam-pairs`, forwarded to the check as `check` takes it (issue #257): the seam split by pair on a pass too; absent, nothing moves. */
|
|
1299
|
+
seamPairs?: boolean;
|
|
1300
|
+
/** `--keep-stills`, forwarded to the check as `check` takes it (issue #257): the setup-pose still under `check/setup_still/`; absent, nothing moves. */
|
|
1301
|
+
keepStills?: boolean;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/** What `build` does with one flag `check` takes: hands it to its check stage, takes it as its own, or refuses it because build supplies the value itself. */
|
|
1305
|
+
export type CheckFlagInBuild =
|
|
1306
|
+
| { as: 'forwarded' }
|
|
1307
|
+
| { as: 'own'; why: string }
|
|
1308
|
+
| { as: 'supplied'; why: string };
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Every flag `check` takes and what `build` does with it (issue #257). `cli.ts`
|
|
1312
|
+
* refuses a `supplied` flag on `build` with its `why`; the selftest holds the
|
|
1313
|
+
* table's keys to the flags `check`'s own refusal lists, so a flag added to
|
|
1314
|
+
* `check` without a line here is a named failure, not a flag `build` drops.
|
|
1315
|
+
*/
|
|
1316
|
+
export const CHECK_FLAGS_IN_BUILD: Readonly<Record<string, CheckFlagInBuild>> = {
|
|
1317
|
+
'--rig': { as: 'supplied', why: 'its check reads the rig its rig stage writes, <out>/rig; run check --rig to measure another rig' },
|
|
1318
|
+
'--out': { as: 'own', why: 'build\'s own --out; its check writes under <out>/check' },
|
|
1319
|
+
'--parts': { as: 'supplied', why: 'its check reads the parts its assemble stage writes, <out>; run check --parts to measure against others' },
|
|
1320
|
+
'--source': { as: 'own', why: 'the painting assemble reads; it is not handed to the check, so check.json carries no SETUP_POSE_VS_SOURCE — run check --rig <out>/rig --parts <out> --source <painting> for that line' },
|
|
1321
|
+
'--page-edges': { as: 'forwarded' },
|
|
1322
|
+
'--pack-shape': { as: 'forwarded' },
|
|
1323
|
+
'--requirements': { as: 'forwarded' },
|
|
1324
|
+
'--seam-pairs': { as: 'forwarded' },
|
|
1325
|
+
'--keep-stills': { as: 'forwarded' },
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
/** The `build` refusal of a `check` flag build supplies itself ({@link CHECK_FLAGS_IN_BUILD}), or null for any other flag. */
|
|
1329
|
+
export function suppliedCheckFlag(flag: string): string | null {
|
|
1330
|
+
const row = Object.hasOwn(CHECK_FLAGS_IN_BUILD, flag) ? CHECK_FLAGS_IN_BUILD[flag] : undefined;
|
|
1331
|
+
return row?.as === 'supplied' ? `build does not take "${flag}"; ${row.why}` : null;
|
|
1277
1332
|
}
|
|
1278
1333
|
|
|
1279
1334
|
/**
|
|
@@ -1347,6 +1402,8 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
1347
1402
|
const out = input.out;
|
|
1348
1403
|
mkdirSync(out, { recursive: true });
|
|
1349
1404
|
for (const p of BUILD_OWNS) rmSync(join(out, p), { recursive: true, force: true });
|
|
1405
|
+
// Issue #242: the figure mask is written only under config.scene; one left by an earlier scene build is not this build's.
|
|
1406
|
+
rmSync(join(out, FIGURE_MASK_FILE), { force: true });
|
|
1350
1407
|
// The idle-keys value is named only when it is not the default, so a build without the flag, or with the default, prints the line it printed before issue #95.
|
|
1351
1408
|
const idleKeys = input.idleKeys ?? DEFAULT_IDLE_KEYS;
|
|
1352
1409
|
log(`rig-parts build: ${input.config} -> ${out}, seam rule ${input.seam}, projection rule ${input.project}, page edges ${input.pageEdges}, pack shape ${input.packShape ?? DEFAULT_PACK_SHAPE}${idleKeys === DEFAULT_IDLE_KEYS ? '' : `, idle keys ${idleKeys}`}${input.loop ? `, with the idle loop${input.leadIn === undefined ? '' : ` after a lead-in of ${input.leadIn} period(s)`}` : ''}`);
|
|
@@ -1376,7 +1433,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
1376
1433
|
|
|
1377
1434
|
let report: CheckReport;
|
|
1378
1435
|
try {
|
|
1379
|
-
report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(input.requirements === undefined ? {} : { requirements: input.requirements }) }, run.check, run.checkBin, prefixed('check'));
|
|
1436
|
+
report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape, ...(input.requirements === undefined ? {} : { requirements: input.requirements }), seamPairs: input.seamPairs === true, keepStills: input.keepStills === true }, run.check, run.checkBin, prefixed('check'));
|
|
1380
1437
|
} catch (err) {
|
|
1381
1438
|
return refused('check', err);
|
|
1382
1439
|
}
|