@rig-c/parts 1.16.0 → 1.18.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 +191 -24
- package/docs/AUTHORING.md +381 -21
- package/docs/MIGRATION.md +52 -0
- package/docs/PAINTING.md +172 -0
- package/docs/STABILITY.md +14 -9
- package/package.json +2 -1
- package/skills/rig-parts/SKILL.md +31 -2
- package/src/assemble.ts +642 -41
- package/src/autoamplitude.ts +23 -3
- package/src/autoenvelope.ts +17 -5
- package/src/build.ts +109 -13
- package/src/check.ts +632 -11
- package/src/config.ts +185 -8
- package/src/inputs.ts +11 -1
- package/src/layers.ts +231 -18
- package/src/limbs.ts +188 -0
- package/src/paintcheck.ts +250 -0
- package/src/parts.ts +167 -6
- package/src/propose.ts +148 -108
- package/src/rig.ts +43 -8
- 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
|
|
@@ -137,7 +160,11 @@ usage:
|
|
|
137
160
|
hip (the midpoint of r_hip and l_hip), the chest half way along neck ->
|
|
138
161
|
hip, and each sleeve chain shoulder -> elbow with its tip at the wrist
|
|
139
162
|
when every handwear part holds a wrist; a missing joint leaves its bone to
|
|
140
|
-
the rule. One note per joint says which.
|
|
163
|
+
the rule. One note per joint says which. Each arm and leg so placed is a
|
|
164
|
+
limb (issue #349): proposal.json's "limbs" cuts its layer at the elbow or
|
|
165
|
+
the knee into parts rigid on its links (and, when every ankle lies on a
|
|
166
|
+
footwear part, a foot on a bone of its own at the ankle that keeps its
|
|
167
|
+
setup angle); assemble with the block makes the cut. LINT then reads the torso along
|
|
141
168
|
the neck -> hips line and each sleeve chain along shoulder -> wrist
|
|
142
169
|
instead of by screen y, and prints the rule set it used first. Without
|
|
143
170
|
--keypoints nothing changes. A wrist holds a part when it lies within
|
|
@@ -351,9 +378,13 @@ usage:
|
|
|
351
378
|
holds an animation, ANIMATIONS_DO_NOT_FOLD reads each, the idle included,
|
|
352
379
|
with rigc render --folds, physics constraints left out: a mesh triangle turned
|
|
353
380
|
over against the setup pose fails, unless the --requirements file's
|
|
354
|
-
may_fold declares that slot and animation with a why.
|
|
355
|
-
|
|
356
|
-
|
|
381
|
+
may_fold declares that slot and animation with a why. Where a footwear
|
|
382
|
+
part rides a bone that hangs from another, FEET_STAY_LEVEL reads the
|
|
383
|
+
same render's geometry: while a bone above that bone is turned, the
|
|
384
|
+
part's drawing must stay within ±10 degrees of its setup pose (HQ's
|
|
385
|
+
line); config.bones' "inherit" on the foot is what keeps it there. The
|
|
386
|
+
last line says how many of the bars measured — nine, ten with the fold
|
|
387
|
+
bar, eleven with the feet-level bar — and names the skipped ones.
|
|
357
388
|
--source adds SETUP_POSE_VS_SOURCE: REPORTED, never a FAIL — the setup
|
|
358
389
|
pose against the painting by assemble's own recomposite figures (mean |d|,
|
|
359
390
|
% within 8, error px over 40, uncovered error px where the pose has alpha
|
|
@@ -417,7 +448,7 @@ usage:
|
|
|
417
448
|
onto frame 0 itself. Animated WebP is not written — it needs a VP8/VP8L
|
|
418
449
|
encoder, out of scope.
|
|
419
450
|
|
|
420
|
-
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>]
|
|
421
452
|
--config <config.json> --out <dir> [--seam near-white|silhouette]
|
|
422
453
|
[--project core|visible]
|
|
423
454
|
Merge the full-body and head-crop See-through runs into rig-space parts:
|
|
@@ -431,11 +462,16 @@ usage:
|
|
|
431
462
|
drawn "back", "front" or {"before": <plan part>}, recorded in parts.json
|
|
432
463
|
as from "painting:<name>" and counted 100 % source — and .cuts — a plan
|
|
433
464
|
part's pixels inside a rig-pixel polygon taken into a new part, drawn the
|
|
434
|
-
same way, recorded with the plan part's from — and
|
|
465
|
+
same way, recorded with the plan part's from — and .limbs (issue #349),
|
|
466
|
+
with the bones they name: each limb's layer cut at its joints into one
|
|
467
|
+
part per segment, each lower part carrying a cap past its joint, the
|
|
468
|
+
upper drawn over the lower — and no other rig section:
|
|
435
469
|
bones, meshes, regions and motion need not exist yet, because propose
|
|
436
470
|
drafts them from these parts (AUTHORING §4, the table).
|
|
437
471
|
Prints one line per part, one \`cut:\` line per part a cut split (its
|
|
438
|
-
opaque pixels = what it kept + what each cut took),
|
|
472
|
+
opaque pixels = what it kept + what each cut took), one \`limb cut:\`
|
|
473
|
+
line per part a limb took and one \`limb cap:\` line per joint (the cap's
|
|
474
|
+
pixels from the layer and missing, and the box to paint them into), the seam override
|
|
439
475
|
counts, the \`pixels:\` totals
|
|
440
476
|
(opaque = visible + occluded; taken from the painting; visible but not
|
|
441
477
|
projected), and \`recomposite vs source\` (mean |d| and % within 8 over the
|
|
@@ -446,7 +482,15 @@ usage:
|
|
|
446
482
|
and one \`uncovered:\` line per full-run layer the plan does not take from
|
|
447
483
|
the full run whose uncovered error holds a body (5x5-opened, 8-connected,
|
|
448
484
|
at least 150 full-run px in rig px), naming the layer, its pixels and the
|
|
449
|
-
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).
|
|
450
494
|
Writes nothing unless every check passed.
|
|
451
495
|
--seam defaults to ${DEFAULT_SEAM_RULE}. --project says where a layer takes
|
|
452
496
|
the painting's pixel: core (the reference's) erodes every layer's top-most
|
|
@@ -455,7 +499,7 @@ usage:
|
|
|
455
499
|
${DEFAULT_PROJECT_RULE}.
|
|
456
500
|
|
|
457
501
|
rig-parts assemble --propose-plan --source <painting.png> --full <dir|psd>
|
|
458
|
-
--head <dir|psd> --config <config.json>
|
|
502
|
+
[--head <dir|psd>] --config <config.json>
|
|
459
503
|
Print {plan, extend_below_crop, notes} for config.assemble, from the two
|
|
460
504
|
runs. A layer \`layers\` WARNs about (PLAN_LAYER_TRANSLUCENT,
|
|
461
505
|
PLAN_LAYER_BACKGROUND, PLAN_LAYER_OVERSIZED) is not proposed, and a note
|
|
@@ -465,7 +509,9 @@ usage:
|
|
|
465
509
|
rule that skipped it and the continuation; the plan itself is not
|
|
466
510
|
changed. Reads only config.seethrough.head_box,
|
|
467
511
|
config.seethrough.resolution and config.assemble.rig_scale — the rest of
|
|
468
|
-
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.
|
|
469
515
|
|
|
470
516
|
rig-parts inputs --source <painting.png> --config <config.json> --out <dir>
|
|
471
517
|
Cut the two images See-through is fed: <out>/st_input_full.png (the
|
|
@@ -498,7 +544,7 @@ usage:
|
|
|
498
544
|
The config needs only key and generation here; the rest comes later.
|
|
499
545
|
|
|
500
546
|
rig-parts build --config <config.json> --source <painting.png> --full <dir|psd>
|
|
501
|
-
--head <dir|psd> --out <dir> [--seam near-white|silhouette]
|
|
547
|
+
[--head <dir|psd>] --out <dir> [--seam near-white|silhouette]
|
|
502
548
|
[--project core|visible] [--page-edges pot|free]
|
|
503
549
|
[--pack-shape rect|polygon] [--loop] [--requirements <file.json>]
|
|
504
550
|
[--idle-keys ctl|direct] [--lead-in <periods>] [--seam-pairs] [--keep-stills]
|
|
@@ -591,6 +637,21 @@ usage:
|
|
|
591
637
|
line and ${SCENE_REPORT_FILE} name what was accepted, and the run exits 1.
|
|
592
638
|
Exit 0 when the check is PASS and nothing was accepted, 1 otherwise.
|
|
593
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, hip,
|
|
648
|
+
knee or ankle not observed), PAINTING_JOINT_ON_FIGURE (an observed
|
|
649
|
+
joint on the background), PAINTING_ARM_CLEAR (no gap from a wrist to
|
|
650
|
+
its hip), PAINTING_HANDS_APART (none between the wrists),
|
|
651
|
+
PAINTING_LEGS_APART (none between the knees, or the ankles). Prints the
|
|
652
|
+
figure, every joint and every gap with its count; writes nothing.
|
|
653
|
+
docs/PAINTING.md says why each one matters. Exit 0 PASS, 1 refused.
|
|
654
|
+
|
|
594
655
|
rig-parts guide <RULE | §<n> | §<n>.<m> | §<n>.<m>.<k>>
|
|
595
656
|
Print a slice of ${GUIDE_DOC}, the guide this package ships, read
|
|
596
657
|
from the installed file at the moment of the call; nothing is kept
|
|
@@ -676,6 +737,10 @@ function printLayerTable(set: LayerSet): void {
|
|
|
676
737
|
console.log(` ${set.layers.length} layer(s): ${painted} with opaque pixels, ${set.layers.length - painted} with none`);
|
|
677
738
|
// Issue #256: a layer the author made is said to be one, so it never reads as See-through's. None, no line.
|
|
678
739
|
for (const l of set.layers) if (l.tag.group === 'author') console.log(` ${authorLayerLine(l.name)}`);
|
|
740
|
+
// Issue #359: a hidden-art layer is listed apart, never in the drawn table. None, no line.
|
|
741
|
+
for (const h of set.hidden) console.log(` ${hiddenLayerLine(h)}`);
|
|
742
|
+
// Issue #359: an input with no depth is said once, with what reads depth and that it is skipped, never passed.
|
|
743
|
+
if (set.layers.length > 0 && set.layers.every((l) => l.depth === null)) console.log(` ${depthSkipLine(set)}`);
|
|
679
744
|
// A reader refuses nothing on plausibility: it says what --propose-plan will leave out, and why.
|
|
680
745
|
let warned = 0;
|
|
681
746
|
for (const f of figures) {
|
|
@@ -690,10 +755,11 @@ function printLayerTable(set: LayerSet): void {
|
|
|
690
755
|
/**
|
|
691
756
|
* Every flag each command takes: `valued` flags carry a value, `switches` are bare. Each parser reads its row to
|
|
692
757
|
* decide what it accepts, and {@link notTaken} prints the same row in the unknown-flag refusal, so the refusal
|
|
693
|
-
* lists exactly the flags the parser accepts (CL26). `
|
|
694
|
-
*
|
|
758
|
+
* lists exactly the flags the parser accepts (CL26). `guide` takes one positional argument and no flag, so it has
|
|
759
|
+
* no row; `layers` takes one positional path beside its flags (issue #359).
|
|
695
760
|
*/
|
|
696
761
|
const TAKES = {
|
|
762
|
+
layers: { valued: ['--export', '--template', '--canvas'], switches: [] },
|
|
697
763
|
sheet: { valued: ['--source', '--layers', '--out', '--cell', '--cols'], switches: [] },
|
|
698
764
|
rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
|
|
699
765
|
propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule'], switches: ['--head-box'] },
|
|
@@ -707,6 +773,7 @@ const TAKES = {
|
|
|
707
773
|
build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys', '--lead-in'], switches: ['--loop', '--seam-pairs', '--keep-stills'] },
|
|
708
774
|
compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
|
|
709
775
|
'mesh-draft': { valued: ['--config', '--parts', '--out', '--requirements', '--idle-keys', '--page-edges', '--pack-shape'], switches: ['--more-triangles'] },
|
|
776
|
+
painting: { valued: ['--keypoints', '--person'], switches: [] },
|
|
710
777
|
} as const satisfies Record<string, { valued: readonly string[]; switches: readonly string[] }>;
|
|
711
778
|
|
|
712
779
|
type Command = keyof typeof TAKES;
|
|
@@ -722,10 +789,58 @@ function notTaken(command: Command, flag: string): string {
|
|
|
722
789
|
return `${command} does not take "${flag}"; it takes ${[...row.valued, ...row.switches].join(', ')}`;
|
|
723
790
|
}
|
|
724
791
|
|
|
792
|
+
/**
|
|
793
|
+
* `layers <path>` prints the table; `layers --export <dir> <path>` writes the
|
|
794
|
+
* set as the wrapper form and reads it back (issue #359); `layers --template
|
|
795
|
+
* <out.psd> --canvas <W>x<H>` writes the artist's template. One positional
|
|
796
|
+
* path, except with --template, which reads nothing.
|
|
797
|
+
*/
|
|
725
798
|
function cmdLayers(args: string[]): number {
|
|
726
|
-
|
|
799
|
+
const flags = new Map<string, string>();
|
|
800
|
+
const paths: string[] = [];
|
|
801
|
+
for (let i = 0; i < args.length; i++) {
|
|
802
|
+
const a = args[i];
|
|
803
|
+
if (!a.startsWith('--')) {
|
|
804
|
+
paths.push(a);
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (!takesValued('layers', a)) return usage(notTaken('layers', a));
|
|
808
|
+
const value = args[i + 1];
|
|
809
|
+
if (value === undefined) return usage(`${a} needs a value`);
|
|
810
|
+
if (flags.has(a)) return usage(`${a} is given twice`);
|
|
811
|
+
flags.set(a, value);
|
|
812
|
+
i++;
|
|
813
|
+
}
|
|
814
|
+
const template = flags.get('--template');
|
|
815
|
+
if (template !== undefined) {
|
|
816
|
+
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(', ')}`);
|
|
817
|
+
const canvasArg = flags.get('--canvas');
|
|
818
|
+
if (canvasArg === undefined) return usage('--template needs --canvas <W>x<H>, the canvas size in px (with one run, a square of seethrough.resolution)');
|
|
819
|
+
const canvas = parseCanvas(canvasArg);
|
|
820
|
+
if (canvas === null) return usage(`--canvas ${canvasArg} is not <W>x<H> in integer px`);
|
|
821
|
+
if (extname(template).toLowerCase() !== '.psd') return usage(`--template ${template} is not a .psd path`);
|
|
822
|
+
const names = templateLayerNames();
|
|
823
|
+
mkdirSync(dirname(template), { recursive: true });
|
|
824
|
+
writeFileSync(template, templatePsdBytes(canvas.w, canvas.h));
|
|
825
|
+
console.log(`rig-parts layers --template: wrote ${template}, ${canvas.w}x${canvas.h}, ${names.length} empty layer(s), bottom first: ${names.join(', ')}`);
|
|
826
|
+
console.log(' paint into the layers the figure has and delete the rest: an empty layer is refused by name (PSD_LAYER_HAS_PIXELS)');
|
|
827
|
+
return EXIT_OK;
|
|
828
|
+
}
|
|
829
|
+
if (flags.has('--canvas')) return usage('--canvas belongs to --template');
|
|
830
|
+
if (paths.length !== 1) return usage(`layers takes one path; got ${paths.length}`);
|
|
727
831
|
try {
|
|
728
|
-
|
|
832
|
+
const set = readLayers(paths[0]);
|
|
833
|
+
printLayerTable(set);
|
|
834
|
+
const dir = flags.get('--export');
|
|
835
|
+
if (dir !== undefined) {
|
|
836
|
+
const w = exportLayers(set, dir);
|
|
837
|
+
// The round trip, read now: what was written must read back as what was read.
|
|
838
|
+
const back = readWrapperLayers(dir);
|
|
839
|
+
const diff = layerSetDiff(set, back);
|
|
840
|
+
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)` }]);
|
|
841
|
+
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`);
|
|
842
|
+
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*)`);
|
|
843
|
+
}
|
|
729
844
|
return EXIT_OK;
|
|
730
845
|
} catch (err) {
|
|
731
846
|
return printRefusal(err);
|
|
@@ -1128,20 +1243,23 @@ function cmdAssemble(args: string[]): number {
|
|
|
1128
1243
|
flags.set(flag, value);
|
|
1129
1244
|
i++;
|
|
1130
1245
|
}
|
|
1131
|
-
|
|
1246
|
+
// --head is optional (issue #359): without it the full run is the one run, and it must hold the face.
|
|
1247
|
+
for (const f of ['--source', '--full', '--config']) if (!flags.has(f)) return usage(`assemble needs ${f}`);
|
|
1132
1248
|
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');
|
|
1133
1249
|
if (!propose && !flags.has('--out')) return usage('assemble needs --out <dir>');
|
|
1134
1250
|
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
1135
1251
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
1136
1252
|
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
1137
1253
|
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
1138
|
-
const [source, full,
|
|
1254
|
+
const [source, full, config] = ['--source', '--full', '--config'].map((f) => flags.get(f) as string);
|
|
1255
|
+
const head = flags.get('--head') ?? null;
|
|
1139
1256
|
try {
|
|
1140
1257
|
if (propose) {
|
|
1141
1258
|
const src = readSource(source);
|
|
1142
1259
|
const runs = readRuns(full, head);
|
|
1143
|
-
const g = proposeFields(loadEarlyConfig(config, 'layers'));
|
|
1144
|
-
const
|
|
1260
|
+
const g = proposeFields(loadEarlyConfig(config, 'layers'), runs.single ? { w: src.width, h: src.height } : null);
|
|
1261
|
+
const geometry = { sourceW: src.width, sourceH: src.height, ...g };
|
|
1262
|
+
const proposal = runs.single ? proposeSinglePlan(runs.full, geometry) : proposePlan(runs.full, runs.head, geometry);
|
|
1145
1263
|
console.log(JSON.stringify(proposal, null, 2));
|
|
1146
1264
|
return EXIT_OK;
|
|
1147
1265
|
}
|
|
@@ -1312,7 +1430,8 @@ function cmdBuild(args: string[]): number {
|
|
|
1312
1430
|
flags.set(flag, value);
|
|
1313
1431
|
i++;
|
|
1314
1432
|
}
|
|
1315
|
-
|
|
1433
|
+
// --head is optional (issue #359), as for assemble.
|
|
1434
|
+
for (const f of ['--config', '--source', '--full', '--out']) if (!flags.has(f)) return usage(`build needs ${f}`);
|
|
1316
1435
|
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
1317
1436
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
1318
1437
|
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
@@ -1327,7 +1446,8 @@ function cmdBuild(args: string[]): number {
|
|
|
1327
1446
|
const leadText = flags.get('--lead-in');
|
|
1328
1447
|
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`);
|
|
1329
1448
|
if (leadText !== undefined && !loop) return usage('--lead-in needs --loop; it says which frames the loop is encoded from');
|
|
1330
|
-
const [config, source, full,
|
|
1449
|
+
const [config, source, full, out] = ['--config', '--source', '--full', '--out'].map((f) => flags.get(f) as string);
|
|
1450
|
+
const head = flags.get('--head') ?? null;
|
|
1331
1451
|
let bin: string;
|
|
1332
1452
|
try {
|
|
1333
1453
|
bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
@@ -1411,6 +1531,52 @@ function cmdMeshDraft(args: string[]): number {
|
|
|
1411
1531
|
}
|
|
1412
1532
|
}
|
|
1413
1533
|
|
|
1534
|
+
/**
|
|
1535
|
+
* `painting --keypoints <k.json> [--person <id>] <painting.png>` (issue #350): what the painting must be before
|
|
1536
|
+
* See-through runs on it, read off its pixels and the body-18 joints `propose --keypoints` reads. Writes nothing.
|
|
1537
|
+
*/
|
|
1538
|
+
function cmdPainting(args: string[]): number {
|
|
1539
|
+
const flags = new Map<string, string>();
|
|
1540
|
+
const paths: string[] = [];
|
|
1541
|
+
for (let i = 0; i < args.length; i++) {
|
|
1542
|
+
const a = args[i];
|
|
1543
|
+
if (!a.startsWith('--')) {
|
|
1544
|
+
paths.push(a);
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (!takesValued('painting', a)) return usage(notTaken('painting', a));
|
|
1548
|
+
const value = args[i + 1];
|
|
1549
|
+
if (value === undefined) return usage(`${a} needs a value`);
|
|
1550
|
+
if (flags.has(a)) return usage(`${a} is given twice`);
|
|
1551
|
+
flags.set(a, value);
|
|
1552
|
+
i++;
|
|
1553
|
+
}
|
|
1554
|
+
const kpPath = flags.get('--keypoints');
|
|
1555
|
+
if (kpPath === undefined) return usage('painting needs --keypoints <file>, the body-18 joints of the figure in painting px (the file propose --keypoints reads)');
|
|
1556
|
+
if (paths.length !== 1) return usage(`painting takes one painting path; got ${paths.length}`);
|
|
1557
|
+
const source = paths[0];
|
|
1558
|
+
try {
|
|
1559
|
+
if (!existsSync(source)) throw new PartsError([{ code: 'PAINTING_SOURCE_PRESENT', object: source, detail: 'no such file; the painting to check is required' }]);
|
|
1560
|
+
let painting: ReturnType<typeof readPng>;
|
|
1561
|
+
try {
|
|
1562
|
+
painting = readPng(source);
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
throw new PartsError([{ code: 'PAINTING_SOURCE_DECODES', object: source, detail: `${(err as Error).message}; a PNG is required` }]);
|
|
1565
|
+
}
|
|
1566
|
+
const kp = loadKeypoints(kpPath);
|
|
1567
|
+
checkImageSize(kp, painting.width, painting.height, kpPath, source);
|
|
1568
|
+
const person = choosePerson(kp, flags.get('--person'), kpPath);
|
|
1569
|
+
console.log(`rig-parts painting: ${source}, joints from ${kpPath} person "${person.id}" (source: ${kp.source})`);
|
|
1570
|
+
const r = checkPainting(painting, person, source, kpPath);
|
|
1571
|
+
for (const l of r.lines) console.log(l);
|
|
1572
|
+
if (r.problems.length > 0) throw new PartsError(r.problems);
|
|
1573
|
+
console.log(' PASS the painting holds every requirement this check reads (docs/PAINTING.md); what only an eye reads is listed there');
|
|
1574
|
+
return EXIT_OK;
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
return printRefusal(err);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1414
1580
|
function main(argv: string[]): number | Promise<number> {
|
|
1415
1581
|
const [command, ...rest] = argv;
|
|
1416
1582
|
if (command === undefined || command === '--help' || command === '-h' || command === 'help') {
|
|
@@ -1434,6 +1600,7 @@ function main(argv: string[]): number | Promise<number> {
|
|
|
1434
1600
|
if (command === 'build') return cmdBuild(rest);
|
|
1435
1601
|
if (command === 'compose') return cmdCompose(rest);
|
|
1436
1602
|
if (command === 'mesh-draft') return cmdMeshDraft(rest);
|
|
1603
|
+
if (command === 'painting') return cmdPainting(rest);
|
|
1437
1604
|
if (command === 'guide') return cmdGuide(rest);
|
|
1438
1605
|
const later = LATER.find(([name]) => name === command);
|
|
1439
1606
|
if (later !== undefined) {
|