@rig-c/parts 1.17.0 → 1.19.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 +8 -3
- package/cli.ts +173 -18
- package/docs/AUTHORING.md +178 -6
- package/docs/MIGRATION.md +73 -0
- package/docs/PAINTING.md +327 -0
- package/docs/STABILITY.md +7 -4
- package/package.json +2 -1
- package/skills/rig-parts/SKILL.md +29 -1
- package/src/assemble.ts +324 -25
- package/src/build.ts +32 -9
- package/src/inputs.ts +11 -1
- package/src/layers.ts +231 -18
- package/src/paintcheck.ts +277 -0
- package/src/parts.ts +81 -5
- package/src/propose.ts +11 -1
- package/src/tags.ts +47 -0
package/README.md
CHANGED
|
@@ -75,8 +75,13 @@ this port to.
|
|
|
75
75
|
|
|
76
76
|
## What it takes in
|
|
77
77
|
|
|
78
|
-
- **One painting** — a PNG of one character
|
|
79
|
-
|
|
78
|
+
- **One painting** — a PNG of one character that a rig can be cut from: front view,
|
|
79
|
+
arms clear of the body, hands apart and open, legs apart with a gap, on a plain
|
|
80
|
+
light background. [docs/PAINTING.md](docs/PAINTING.md) says what each requirement
|
|
81
|
+
is for and what breaks without it, gives one generation recipe, and describes
|
|
82
|
+
`rig-parts painting`, which checks a painting against its body-18 joints before
|
|
83
|
+
See-through runs. To set up the machine that makes the painting and the layers,
|
|
84
|
+
see [rig-c/getting-started](https://github.com/rig-c/getting-started).
|
|
80
85
|
- **Its See-through layers, twice**: one run on the whole figure (the painting padded
|
|
81
86
|
white to a square) and one on a square crop around the head, because the eyes of a
|
|
82
87
|
full-figure run are too small to separate. Either form See-through reaches you in
|
|
@@ -436,7 +441,7 @@ third-party term this package touches, See-through's included.
|
|
|
436
441
|
bun install
|
|
437
442
|
bun run typecheck # tsc --noEmit, strict
|
|
438
443
|
bun run lint # one rule: no explicit any
|
|
439
|
-
bun run fetch-examples # the public examples' paintings and layers, into examples/*/inputs (needs a network)
|
|
444
|
+
bun run fetch-examples # the public examples' paintings and layers, into examples/*/inputs (needs a network); each file held to its blob id at the pinned commit, a stale one replaced by name
|
|
440
445
|
bun run selftest # every gate's controls; with the examples fetched, build on each of them against its expected/
|
|
441
446
|
bun run smoke # pack, install into an empty directory, run from the install (needs a network)
|
|
442
447
|
```
|
package/cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
15
15
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { basename, dirname, extname, join } from 'node:path';
|
|
18
|
-
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
18
|
+
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, proposeSinglePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
19
19
|
import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage, suppliedCheckFlag } from './src/build.ts';
|
|
20
20
|
import { ACCEPTABLE_BARS, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, rigcRunnerAt, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, NO_PARTS_SENTENCE, SEAM_MEAN_BAR, SOURCE_SENTENCE, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
|
|
21
21
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
@@ -25,11 +25,12 @@ import { GUIDE_DOC, type GuideAnswer, guideRule, guideSection } from './src/guid
|
|
|
25
25
|
import { proposeHeadBox } from './src/headbox.ts';
|
|
26
26
|
import { makeInputs } from './src/inputs.ts';
|
|
27
27
|
import { idleKeysOf, isIdleKeys } from './src/rig.ts';
|
|
28
|
-
import { figuresPhrase, implausibleRules, layerFigures, type LayerSet, pct, readLayers, ruleSummary, times } from './src/layers.ts';
|
|
28
|
+
import { depthSkipLine, exportLayers, figuresPhrase, hiddenLayerLine, implausibleRules, layerFigures, type LayerSet, layerSetDiff, pct, readLayers, readWrapperLayers, ruleSummary, templateLayerNames, templatePsdBytes, times } from './src/layers.ts';
|
|
29
29
|
import { checkImageSize, choosePerson, KEYPOINTS_SPEC, loadKeypoints, type RigJoints, toRigJoints } from './src/keypoints.ts';
|
|
30
30
|
import { basisLines, checkProposal, compare, compareLines, DEFAULT_GRID_RULE, draftTurn, drawLandmarks, FIGURE_GRID_DIVISOR, FRINGE_STEP_PX, GRID_RULES, type GridRule, HIP_MIN_FRACTION, lint, lintLine, type PartSet, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
|
|
31
31
|
import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, serializeBasis } from './src/diagnostics.ts';
|
|
32
32
|
import { BEND_DEGREES, BEND_WORST_SHARE, DUST_MAX_PX, DUST_MAX_SHARE, IDLE_BOUND_PX, MESH_DRAFT_REPORT, MESH_DRAFT_SPEC, meshDraftStage, MORE_TRIANGLES_FLAG, TOLERANCE_FIGURE_DIVISOR } from './src/meshdraft.ts';
|
|
33
|
+
import { checkPainting } from './src/paintcheck.ts';
|
|
33
34
|
import { readPng, writePng } from './src/raster/png.ts';
|
|
34
35
|
import { REQUIREMENTS_SENTENCE } from './src/requirements.ts';
|
|
35
36
|
import { composeStage, IMAGE_SEP, PLATE, PROVENANCES, SCENE_REPORT_FILE, SCENE_REPORT_SPEC, SCENE_SPEC } from './src/scene.ts';
|
|
@@ -67,10 +68,32 @@ usage:
|
|
|
67
68
|
crosses a plausibility rule gets a WARN line naming the rule and the bar;
|
|
68
69
|
--propose-plan leaves it out. A layer named author:<label> is one the
|
|
69
70
|
author made by hand: its group is author, and a line says it has no
|
|
70
|
-
role.
|
|
71
|
+
role. A layer named <layer>:hidden (issue #359) is that layer's art
|
|
72
|
+
where something covers it: listed apart, never drawn, never a part. An
|
|
73
|
+
input with no depth (a PSD, or depth_median null on every layer, whose
|
|
74
|
+
manifest order is then the draw order) prints one SKIP depth line.
|
|
75
|
+
Refuses, by name, a missing file, an unknown tag, a PNG whose
|
|
71
76
|
size is not its box, and anything else outside the input contract —
|
|
72
77
|
never a WARN.
|
|
73
78
|
|
|
79
|
+
rig-parts layers --export <dir> <dir | layers.json | file.psd>
|
|
80
|
+
Print the table, then write the input as the wrapper form into <dir>
|
|
81
|
+
(absent or empty, LAYERS_EXPORT_DIR_EMPTY): layers.json and one RGBA PNG
|
|
82
|
+
per layer, <index>_<name>.png, drawn layers in draw order then the
|
|
83
|
+
hidden-art layers, depth_median as read (null for a PSD). The export is
|
|
84
|
+
read back at once and must hold the same layers, boxes, pixels and order
|
|
85
|
+
(LAYERS_EXPORT_ROUND_TRIP). Correct the parse there with any image
|
|
86
|
+
library and hand <dir> to any command that reads layers
|
|
87
|
+
(docs/AUTHORING.md, the correction loop).
|
|
88
|
+
|
|
89
|
+
rig-parts layers --template <out.psd> --canvas <W>x<H>
|
|
90
|
+
Write a PSD for an artist: one empty layer per tag name the rig reads,
|
|
91
|
+
bottom first (back hair, the body, the face, front hair; the six split
|
|
92
|
+
tags as their -r and -l halves; no head). The same canvas writes the
|
|
93
|
+
same bytes. Read back, every layer is refused as empty
|
|
94
|
+
(PSD_LAYER_HAS_PIXELS) until painted: paint the ones the figure has,
|
|
95
|
+
delete the rest.
|
|
96
|
+
|
|
74
97
|
rig-parts sheet --source <painting.png> --layers <path> [--layers <path> ...]
|
|
75
98
|
--out <sheet.png> [--cell <px>] [--cols <n>]
|
|
76
99
|
A contact sheet: the painting, then every layer or part, each on a
|
|
@@ -425,7 +448,7 @@ usage:
|
|
|
425
448
|
onto frame 0 itself. Animated WebP is not written — it needs a VP8/VP8L
|
|
426
449
|
encoder, out of scope.
|
|
427
450
|
|
|
428
|
-
rig-parts assemble --source <painting.png> --full <dir|psd> --head <dir|psd>
|
|
451
|
+
rig-parts assemble --source <painting.png> --full <dir|psd> [--head <dir|psd>]
|
|
429
452
|
--config <config.json> --out <dir> [--seam near-white|silhouette]
|
|
430
453
|
[--project core|visible]
|
|
431
454
|
Merge the full-body and head-crop See-through runs into rig-space parts:
|
|
@@ -459,7 +482,15 @@ usage:
|
|
|
459
482
|
and one \`uncovered:\` line per full-run layer the plan does not take from
|
|
460
483
|
the full run whose uncovered error holds a body (5x5-opened, 8-connected,
|
|
461
484
|
at least 150 full-run px in rig px), naming the layer, its pixels and the
|
|
462
|
-
rule that left it out.
|
|
485
|
+
rule that left it out. One \`hidden art:\` line per hidden-art layer
|
|
486
|
+
(<layer>:hidden, issue #359) names what read it: the under of a cut of
|
|
487
|
+
its layer's part, or the cap of a limb segment below a joint that takes
|
|
488
|
+
that part, when neither names a file (both: ASSEMBLE_HIDDEN_ONE_SOURCE).
|
|
489
|
+
With no --head, the full run is the one run (an artist's own file): it
|
|
490
|
+
must hold face, eyewhite-r and eyewhite-l, painted
|
|
491
|
+
(ASSEMBLE_SINGLE_RUN_FACE), every plan row takes "full" and none
|
|
492
|
+
extends below a crop (ASSEMBLE_SINGLE_RUN_PLAN), and the config sets no
|
|
493
|
+
seethrough.head_box (ASSEMBLE_SINGLE_RUN_HEAD_BOX).
|
|
463
494
|
Writes nothing unless every check passed.
|
|
464
495
|
--seam defaults to ${DEFAULT_SEAM_RULE}. --project says where a layer takes
|
|
465
496
|
the painting's pixel: core (the reference's) erodes every layer's top-most
|
|
@@ -468,7 +499,7 @@ usage:
|
|
|
468
499
|
${DEFAULT_PROJECT_RULE}.
|
|
469
500
|
|
|
470
501
|
rig-parts assemble --propose-plan --source <painting.png> --full <dir|psd>
|
|
471
|
-
--head <dir|psd> --config <config.json>
|
|
502
|
+
[--head <dir|psd>] --config <config.json>
|
|
472
503
|
Print {plan, extend_below_crop, notes} for config.assemble, from the two
|
|
473
504
|
runs. A layer \`layers\` WARNs about (PLAN_LAYER_TRANSLUCENT,
|
|
474
505
|
PLAN_LAYER_BACKGROUND, PLAN_LAYER_OVERSIZED) is not proposed, and a note
|
|
@@ -478,7 +509,9 @@ usage:
|
|
|
478
509
|
rule that skipped it and the continuation; the plan itself is not
|
|
479
510
|
changed. Reads only config.seethrough.head_box,
|
|
480
511
|
config.seethrough.resolution and config.assemble.rig_scale — the rest of
|
|
481
|
-
the config need not exist yet.
|
|
512
|
+
the config need not exist yet. With no --head, the one run is held as
|
|
513
|
+
assemble holds it and every row is written from it, and the first note
|
|
514
|
+
says so.
|
|
482
515
|
|
|
483
516
|
rig-parts inputs --source <painting.png> --config <config.json> --out <dir>
|
|
484
517
|
Cut the two images See-through is fed: <out>/st_input_full.png (the
|
|
@@ -511,7 +544,7 @@ usage:
|
|
|
511
544
|
The config needs only key and generation here; the rest comes later.
|
|
512
545
|
|
|
513
546
|
rig-parts build --config <config.json> --source <painting.png> --full <dir|psd>
|
|
514
|
-
--head <dir|psd> --out <dir> [--seam near-white|silhouette]
|
|
547
|
+
[--head <dir|psd>] --out <dir> [--seam near-white|silhouette]
|
|
515
548
|
[--project core|visible] [--page-edges pot|free]
|
|
516
549
|
[--pack-shape rect|polygon] [--loop] [--requirements <file.json>]
|
|
517
550
|
[--idle-keys ctl|direct] [--lead-in <periods>] [--seam-pairs] [--keep-stills]
|
|
@@ -604,6 +637,22 @@ usage:
|
|
|
604
637
|
line and ${SCENE_REPORT_FILE} name what was accepted, and the run exits 1.
|
|
605
638
|
Exit 0 when the check is PASS and nothing was accepted, 1 otherwise.
|
|
606
639
|
|
|
640
|
+
rig-parts painting --keypoints <keypoints.json> [--person <id>] <painting.png>
|
|
641
|
+
Before See-through: read the painting against its body-18 joints (the
|
|
642
|
+
file propose --keypoints reads, measured on this painting) and refuse,
|
|
643
|
+
by name, what a rig cannot be cut from. Background is near-white
|
|
644
|
+
(every channel above 235) or unpainted (alpha 8 or less) ground
|
|
645
|
+
connected to the edge; the figure is the rest; a gap is one background
|
|
646
|
+
pixel on the line between two joints. PAINTING_BACKGROUND_PLAIN (a
|
|
647
|
+
border pixel that is not ground), PAINTING_JOINTS_STATED (a wrist,
|
|
648
|
+
knee or ankle not observed, or a hip neither observed nor occluded
|
|
649
|
+
with a position), PAINTING_JOINT_ON_FIGURE (an observed joint, or an
|
|
650
|
+
occluded hip, on the background), PAINTING_ARM_CLEAR (no gap from a
|
|
651
|
+
wrist to its hip), PAINTING_HANDS_APART (none between the wrists),
|
|
652
|
+
PAINTING_LEGS_APART (none between the knees, or the ankles). Prints the
|
|
653
|
+
figure, every joint and every gap with its count; writes nothing.
|
|
654
|
+
docs/PAINTING.md says why each one matters. Exit 0 PASS, 1 refused.
|
|
655
|
+
|
|
607
656
|
rig-parts guide <RULE | §<n> | §<n>.<m> | §<n>.<m>.<k>>
|
|
608
657
|
Print a slice of ${GUIDE_DOC}, the guide this package ships, read
|
|
609
658
|
from the installed file at the moment of the call; nothing is kept
|
|
@@ -689,6 +738,10 @@ function printLayerTable(set: LayerSet): void {
|
|
|
689
738
|
console.log(` ${set.layers.length} layer(s): ${painted} with opaque pixels, ${set.layers.length - painted} with none`);
|
|
690
739
|
// Issue #256: a layer the author made is said to be one, so it never reads as See-through's. None, no line.
|
|
691
740
|
for (const l of set.layers) if (l.tag.group === 'author') console.log(` ${authorLayerLine(l.name)}`);
|
|
741
|
+
// Issue #359: a hidden-art layer is listed apart, never in the drawn table. None, no line.
|
|
742
|
+
for (const h of set.hidden) console.log(` ${hiddenLayerLine(h)}`);
|
|
743
|
+
// Issue #359: an input with no depth is said once, with what reads depth and that it is skipped, never passed.
|
|
744
|
+
if (set.layers.length > 0 && set.layers.every((l) => l.depth === null)) console.log(` ${depthSkipLine(set)}`);
|
|
692
745
|
// A reader refuses nothing on plausibility: it says what --propose-plan will leave out, and why.
|
|
693
746
|
let warned = 0;
|
|
694
747
|
for (const f of figures) {
|
|
@@ -703,10 +756,11 @@ function printLayerTable(set: LayerSet): void {
|
|
|
703
756
|
/**
|
|
704
757
|
* Every flag each command takes: `valued` flags carry a value, `switches` are bare. Each parser reads its row to
|
|
705
758
|
* decide what it accepts, and {@link notTaken} prints the same row in the unknown-flag refusal, so the refusal
|
|
706
|
-
* lists exactly the flags the parser accepts (CL26). `
|
|
707
|
-
*
|
|
759
|
+
* lists exactly the flags the parser accepts (CL26). `guide` takes one positional argument and no flag, so it has
|
|
760
|
+
* no row; `layers` takes one positional path beside its flags (issue #359).
|
|
708
761
|
*/
|
|
709
762
|
const TAKES = {
|
|
763
|
+
layers: { valued: ['--export', '--template', '--canvas'], switches: [] },
|
|
710
764
|
sheet: { valued: ['--source', '--layers', '--out', '--cell', '--cols'], switches: [] },
|
|
711
765
|
rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
|
|
712
766
|
propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule'], switches: ['--head-box'] },
|
|
@@ -720,6 +774,7 @@ const TAKES = {
|
|
|
720
774
|
build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in'], switches: ['--loop', '--seam-pairs', '--keep-stills'] },
|
|
721
775
|
compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
|
|
722
776
|
'mesh-draft': { valued: ['--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: ['--more-triangles'] },
|
|
777
|
+
painting: { valued: ['--keypoints', '--person'], switches: [] },
|
|
723
778
|
} as const satisfies Record<string, { valued: readonly string[]; switches: readonly string[] }>;
|
|
724
779
|
|
|
725
780
|
type Command = keyof typeof TAKES;
|
|
@@ -735,10 +790,58 @@ function notTaken(command: Command, flag: string): string {
|
|
|
735
790
|
return `${command} does not take "${flag}"; it takes ${[...row.valued, ...row.switches].join(', ')}`;
|
|
736
791
|
}
|
|
737
792
|
|
|
793
|
+
/**
|
|
794
|
+
* `layers <path>` prints the table; `layers --export <dir> <path>` writes the
|
|
795
|
+
* set as the wrapper form and reads it back (issue #359); `layers --template
|
|
796
|
+
* <out.psd> --canvas <W>x<H>` writes the artist's template. One positional
|
|
797
|
+
* path, except with --template, which reads nothing.
|
|
798
|
+
*/
|
|
738
799
|
function cmdLayers(args: string[]): number {
|
|
739
|
-
|
|
800
|
+
const flags = new Map<string, string>();
|
|
801
|
+
const paths: string[] = [];
|
|
802
|
+
for (let i = 0; i < args.length; i++) {
|
|
803
|
+
const a = args[i];
|
|
804
|
+
if (!a.startsWith('--')) {
|
|
805
|
+
paths.push(a);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
if (!takesValued('layers', a)) return usage(notTaken('layers', a));
|
|
809
|
+
const value = args[i + 1];
|
|
810
|
+
if (value === undefined) return usage(`${a} needs a value`);
|
|
811
|
+
if (flags.has(a)) return usage(`${a} is given twice`);
|
|
812
|
+
flags.set(a, value);
|
|
813
|
+
i++;
|
|
814
|
+
}
|
|
815
|
+
const template = flags.get('--template');
|
|
816
|
+
if (template !== undefined) {
|
|
817
|
+
if (flags.has('--export') || paths.length > 0) return usage(`--template writes a PSD and reads nothing; it takes --canvas only, and got ${[...[...flags.keys()].filter((f) => f !== '--template' && f !== '--canvas'), ...paths].join(', ')}`);
|
|
818
|
+
const canvasArg = flags.get('--canvas');
|
|
819
|
+
if (canvasArg === undefined) return usage('--template needs --canvas <W>x<H>, the canvas size in px (with one run, a square of seethrough.resolution)');
|
|
820
|
+
const canvas = parseCanvas(canvasArg);
|
|
821
|
+
if (canvas === null) return usage(`--canvas ${canvasArg} is not <W>x<H> in integer px`);
|
|
822
|
+
if (extname(template).toLowerCase() !== '.psd') return usage(`--template ${template} is not a .psd path`);
|
|
823
|
+
const names = templateLayerNames();
|
|
824
|
+
mkdirSync(dirname(template), { recursive: true });
|
|
825
|
+
writeFileSync(template, templatePsdBytes(canvas.w, canvas.h));
|
|
826
|
+
console.log(`rig-parts layers --template: wrote ${template}, ${canvas.w}x${canvas.h}, ${names.length} empty layer(s), bottom first: ${names.join(', ')}`);
|
|
827
|
+
console.log(' paint into the layers the figure has and delete the rest: an empty layer is refused by name (PSD_LAYER_HAS_PIXELS)');
|
|
828
|
+
return EXIT_OK;
|
|
829
|
+
}
|
|
830
|
+
if (flags.has('--canvas')) return usage('--canvas belongs to --template');
|
|
831
|
+
if (paths.length !== 1) return usage(`layers takes one path; got ${paths.length}`);
|
|
740
832
|
try {
|
|
741
|
-
|
|
833
|
+
const set = readLayers(paths[0]);
|
|
834
|
+
printLayerTable(set);
|
|
835
|
+
const dir = flags.get('--export');
|
|
836
|
+
if (dir !== undefined) {
|
|
837
|
+
const w = exportLayers(set, dir);
|
|
838
|
+
// The round trip, read now: what was written must read back as what was read.
|
|
839
|
+
const back = readWrapperLayers(dir);
|
|
840
|
+
const diff = layerSetDiff(set, back);
|
|
841
|
+
if (diff !== null) throw new PartsError([{ code: 'LAYERS_EXPORT_ROUND_TRIP', object: w.manifest, detail: `reads back unlike ${paths[0]}: ${diff}; the same layers, boxes, pixels and order are required (a bug in the exporter, not in the input)` }]);
|
|
842
|
+
console.log(`rig-parts layers --export: wrote ${w.manifest} and ${w.files.length} PNG(s) in ${dir}; read back: ${back.layers.length} drawn and ${back.hidden.length} hidden-art layer(s), boxes, pixels and order as read`);
|
|
843
|
+
console.log(` correct it there — move pixels between PNGs, change a box in layers.json with its PNG — and hand ${dir} to any command that reads layers (docs/AUTHORING.md, *The correction loop*)`);
|
|
844
|
+
}
|
|
742
845
|
return EXIT_OK;
|
|
743
846
|
} catch (err) {
|
|
744
847
|
return printRefusal(err);
|
|
@@ -1141,20 +1244,23 @@ function cmdAssemble(args: string[]): number {
|
|
|
1141
1244
|
flags.set(flag, value);
|
|
1142
1245
|
i++;
|
|
1143
1246
|
}
|
|
1144
|
-
|
|
1247
|
+
// --head is optional (issue #359): without it the full run is the one run, and it must hold the face.
|
|
1248
|
+
for (const f of ['--source', '--full', '--config']) if (!flags.has(f)) return usage(`assemble needs ${f}`);
|
|
1145
1249
|
if (propose && (flags.has('--out') || flags.has('--seam') || flags.has('--project'))) return usage('--propose-plan prints to the console; it takes none of --out, --seam, --project');
|
|
1146
1250
|
if (!propose && !flags.has('--out')) return usage('assemble needs --out <dir>');
|
|
1147
1251
|
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
1148
1252
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
1149
1253
|
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
1150
1254
|
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
1151
|
-
const [source, full,
|
|
1255
|
+
const [source, full, config] = ['--source', '--full', '--config'].map((f) => flags.get(f) as string);
|
|
1256
|
+
const head = flags.get('--head') ?? null;
|
|
1152
1257
|
try {
|
|
1153
1258
|
if (propose) {
|
|
1154
1259
|
const src = readSource(source);
|
|
1155
1260
|
const runs = readRuns(full, head);
|
|
1156
|
-
const g = proposeFields(loadEarlyConfig(config, 'layers'));
|
|
1157
|
-
const
|
|
1261
|
+
const g = proposeFields(loadEarlyConfig(config, 'layers'), runs.single ? { w: src.width, h: src.height } : null);
|
|
1262
|
+
const geometry = { sourceW: src.width, sourceH: src.height, ...g };
|
|
1263
|
+
const proposal = runs.single ? proposeSinglePlan(runs.full, geometry) : proposePlan(runs.full, runs.head, geometry);
|
|
1158
1264
|
console.log(JSON.stringify(proposal, null, 2));
|
|
1159
1265
|
return EXIT_OK;
|
|
1160
1266
|
}
|
|
@@ -1325,7 +1431,8 @@ function cmdBuild(args: string[]): number {
|
|
|
1325
1431
|
flags.set(flag, value);
|
|
1326
1432
|
i++;
|
|
1327
1433
|
}
|
|
1328
|
-
|
|
1434
|
+
// --head is optional (issue #359), as for assemble.
|
|
1435
|
+
for (const f of ['--config', '--source', '--full', '--out']) if (!flags.has(f)) return usage(`build needs ${f}`);
|
|
1329
1436
|
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
1330
1437
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
1331
1438
|
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
@@ -1340,7 +1447,8 @@ function cmdBuild(args: string[]): number {
|
|
|
1340
1447
|
const leadText = flags.get('--lead-in');
|
|
1341
1448
|
if (leadText !== undefined && !/^[1-9][0-9]*$/.test(leadText)) return usage(`--lead-in ${leadText}; a whole number of idle periods, 1 or more, is required`);
|
|
1342
1449
|
if (leadText !== undefined && !loop) return usage('--lead-in needs --loop; it says which frames the loop is encoded from');
|
|
1343
|
-
const [config, source, full,
|
|
1450
|
+
const [config, source, full, out] = ['--config', '--source', '--full', '--out'].map((f) => flags.get(f) as string);
|
|
1451
|
+
const head = flags.get('--head') ?? null;
|
|
1344
1452
|
let bin: string;
|
|
1345
1453
|
try {
|
|
1346
1454
|
bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
@@ -1424,6 +1532,52 @@ function cmdMeshDraft(args: string[]): number {
|
|
|
1424
1532
|
}
|
|
1425
1533
|
}
|
|
1426
1534
|
|
|
1535
|
+
/**
|
|
1536
|
+
* `painting --keypoints <k.json> [--person <id>] <painting.png>` (issue #350): what the painting must be before
|
|
1537
|
+
* See-through runs on it, read off its pixels and the body-18 joints `propose --keypoints` reads. Writes nothing.
|
|
1538
|
+
*/
|
|
1539
|
+
function cmdPainting(args: string[]): number {
|
|
1540
|
+
const flags = new Map<string, string>();
|
|
1541
|
+
const paths: string[] = [];
|
|
1542
|
+
for (let i = 0; i < args.length; i++) {
|
|
1543
|
+
const a = args[i];
|
|
1544
|
+
if (!a.startsWith('--')) {
|
|
1545
|
+
paths.push(a);
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
if (!takesValued('painting', a)) return usage(notTaken('painting', a));
|
|
1549
|
+
const value = args[i + 1];
|
|
1550
|
+
if (value === undefined) return usage(`${a} needs a value`);
|
|
1551
|
+
if (flags.has(a)) return usage(`${a} is given twice`);
|
|
1552
|
+
flags.set(a, value);
|
|
1553
|
+
i++;
|
|
1554
|
+
}
|
|
1555
|
+
const kpPath = flags.get('--keypoints');
|
|
1556
|
+
if (kpPath === undefined) return usage('painting needs --keypoints <file>, the body-18 joints of the figure in painting px (the file propose --keypoints reads)');
|
|
1557
|
+
if (paths.length !== 1) return usage(`painting takes one painting path; got ${paths.length}`);
|
|
1558
|
+
const source = paths[0];
|
|
1559
|
+
try {
|
|
1560
|
+
if (!existsSync(source)) throw new PartsError([{ code: 'PAINTING_SOURCE_PRESENT', object: source, detail: 'no such file; the painting to check is required' }]);
|
|
1561
|
+
let painting: ReturnType<typeof readPng>;
|
|
1562
|
+
try {
|
|
1563
|
+
painting = readPng(source);
|
|
1564
|
+
} catch (err) {
|
|
1565
|
+
throw new PartsError([{ code: 'PAINTING_SOURCE_DECODES', object: source, detail: `${(err as Error).message}; a PNG is required` }]);
|
|
1566
|
+
}
|
|
1567
|
+
const kp = loadKeypoints(kpPath);
|
|
1568
|
+
checkImageSize(kp, painting.width, painting.height, kpPath, source);
|
|
1569
|
+
const person = choosePerson(kp, flags.get('--person'), kpPath);
|
|
1570
|
+
console.log(`rig-parts painting: ${source}, joints from ${kpPath} person "${person.id}" (source: ${kp.source})`);
|
|
1571
|
+
const r = checkPainting(painting, person, source, kpPath);
|
|
1572
|
+
for (const l of r.lines) console.log(l);
|
|
1573
|
+
if (r.problems.length > 0) throw new PartsError(r.problems);
|
|
1574
|
+
console.log(' PASS the painting holds every requirement this check reads (docs/PAINTING.md); what only an eye reads is listed there');
|
|
1575
|
+
return EXIT_OK;
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
return printRefusal(err);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1427
1581
|
function main(argv: string[]): number | Promise<number> {
|
|
1428
1582
|
const [command, ...rest] = argv;
|
|
1429
1583
|
if (command === undefined || command === '--help' || command === '-h' || command === 'help') {
|
|
@@ -1447,6 +1601,7 @@ function main(argv: string[]): number | Promise<number> {
|
|
|
1447
1601
|
if (command === 'build') return cmdBuild(rest);
|
|
1448
1602
|
if (command === 'compose') return cmdCompose(rest);
|
|
1449
1603
|
if (command === 'mesh-draft') return cmdMeshDraft(rest);
|
|
1604
|
+
if (command === 'painting') return cmdPainting(rest);
|
|
1450
1605
|
if (command === 'guide') return cmdGuide(rest);
|
|
1451
1606
|
const later = LATER.find(([name]) => name === command);
|
|
1452
1607
|
if (later !== undefined) {
|