@rig-c/parts 0.0.0-stage → 1.5.3
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/LICENSE +21 -0
- package/NOTICE.md +134 -0
- package/README.md +431 -2
- package/bin/rig-parts.cjs +43 -0
- package/cli.ts +1240 -0
- package/docs/AUTHORING.md +2387 -0
- package/docs/MIGRATION.md +137 -0
- package/docs/STABILITY.md +262 -0
- package/package.json +70 -4
- package/skills/rig-parts/SKILL.md +167 -0
- package/src/apng.ts +381 -0
- package/src/assemble.ts +2259 -0
- package/src/autoamplitude.ts +194 -0
- package/src/autoenvelope.ts +404 -0
- package/src/automesh.ts +585 -0
- package/src/automotion.ts +447 -0
- package/src/autoreplay.ts +516 -0
- package/src/build.ts +1240 -0
- package/src/check.ts +2942 -0
- package/src/comfy/client.ts +245 -0
- package/src/comfy/index.ts +19 -0
- package/src/comfy/painting.ts +136 -0
- package/src/comfy/seethrough.ts +171 -0
- package/src/config.ts +2283 -0
- package/src/contour.ts +1901 -0
- package/src/coords.ts +26 -0
- package/src/diagnostics.ts +198 -0
- package/src/errors.ts +39 -0
- package/src/gif.ts +213 -0
- package/src/graphs.ts +302 -0
- package/src/headbox.ts +173 -0
- package/src/heat.ts +313 -0
- package/src/inputs.ts +100 -0
- package/src/instruments.ts +1145 -0
- package/src/keypoints.ts +296 -0
- package/src/layers.ts +598 -0
- package/src/localweights.ts +217 -0
- package/src/mesh.ts +362 -0
- package/src/motion.ts +311 -0
- package/src/palette.ts +208 -0
- package/src/parts.ts +410 -0
- package/src/propose.ts +2540 -0
- package/src/pyfmt.ts +135 -0
- package/src/raster/blur.ts +80 -0
- package/src/raster/components.ts +165 -0
- package/src/raster/composite.ts +81 -0
- package/src/raster/index.ts +9 -0
- package/src/raster/morph.ts +116 -0
- package/src/raster/png.ts +82 -0
- package/src/raster/poly.ts +166 -0
- package/src/raster/resize.ts +301 -0
- package/src/raster/types.ts +89 -0
- package/src/raster/warp.ts +121 -0
- package/src/requirements.ts +1240 -0
- package/src/rig.ts +1440 -0
- package/src/round.ts +62 -0
- package/src/scene.ts +1414 -0
- package/src/sheet.ts +128 -0
- package/src/skeleton.ts +400 -0
- package/src/structure.ts +858 -0
- package/src/tags.ts +121 -0
- package/src/weights.ts +124 -0
- package/workflows/seethrough.json +68 -0
package/cli.ts
ADDED
|
@@ -0,0 +1,1240 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* rig-parts — one painting and its See-through layers in, Spine-ready parts
|
|
4
|
+
* and rig specs out, verified through rig-c.
|
|
5
|
+
*
|
|
6
|
+
* Every command prints named, numeric findings and nothing else an agent has
|
|
7
|
+
* to interpret: a refusal is a `FAIL` line naming the rule, the object, the
|
|
8
|
+
* value found and the value required. Exit codes are part of that interface:
|
|
9
|
+
*
|
|
10
|
+
* 0 the command did what it says
|
|
11
|
+
* 1 it refused its input — every refusal is printed as a FAIL line
|
|
12
|
+
* 2 a usage error, or a command this version does not implement
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
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';
|
|
19
|
+
import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
20
|
+
import { ACCEPTABLE_BARS, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, 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
|
+
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
22
|
+
import { type CharacterConfig, loadConfig, loadEarlyConfig, loadPaintConfig } from './src/config.ts';
|
|
23
|
+
import { PartsError, problemLine } from './src/errors.ts';
|
|
24
|
+
import { proposeHeadBox } from './src/headbox.ts';
|
|
25
|
+
import { makeInputs } from './src/inputs.ts';
|
|
26
|
+
import { idleKeysOf, isIdleKeys } from './src/rig.ts';
|
|
27
|
+
import { figuresPhrase, implausibleRules, layerFigures, type LayerSet, pct, readLayers, ruleSummary, times } from './src/layers.ts';
|
|
28
|
+
import { checkImageSize, choosePerson, KEYPOINTS_SPEC, loadKeypoints, type RigJoints, toRigJoints } from './src/keypoints.ts';
|
|
29
|
+
import { basisLines, checkProposal, compare, compareLines, drawLandmarks, HIP_MIN_FRACTION, lint, lintLine, type PartSet, proposeWithBasis, readPartSet, serializeProposal, WRIST_ON_PART_PX } from './src/propose.ts';
|
|
30
|
+
import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, serializeBasis } from './src/diagnostics.ts';
|
|
31
|
+
import { readPng, writePng } from './src/raster/png.ts';
|
|
32
|
+
import { REQUIREMENTS_SENTENCE } from './src/requirements.ts';
|
|
33
|
+
import { composeStage, IMAGE_SEP, PLATE, PROVENANCES, SCENE_REPORT_FILE, SCENE_REPORT_SPEC, SCENE_SPEC } from './src/scene.ts';
|
|
34
|
+
import { buildSheet, defaultCaption, type Tile, tilesFrom } from './src/sheet.ts';
|
|
35
|
+
import { loadComparison, requiredProblems, structureLines } from './src/structure.ts';
|
|
36
|
+
|
|
37
|
+
const EXIT_OK = 0;
|
|
38
|
+
const EXIT_REFUSED = 1;
|
|
39
|
+
const EXIT_USAGE = 2;
|
|
40
|
+
|
|
41
|
+
function version(): string {
|
|
42
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dir, 'package.json'), 'utf8')) as { version?: string };
|
|
43
|
+
return pkg.version ?? '(no version field)';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The commands a later version will carry, registered now so the surface and
|
|
48
|
+
* the help are honest about what exists. Each one exits 2 with the same
|
|
49
|
+
* sentence, and the selftest holds every name here to that. Empty in this
|
|
50
|
+
* version — every command is real — and kept so the next one has a place.
|
|
51
|
+
*/
|
|
52
|
+
const LATER: ReadonlyArray<[string, string]> = [];
|
|
53
|
+
|
|
54
|
+
const HELP = `rig-parts ${version()} — Spine-ready parts from one painting and its See-through layers
|
|
55
|
+
|
|
56
|
+
usage:
|
|
57
|
+
rig-parts layers <dir | layers.json | file.psd>
|
|
58
|
+
Read a See-through decomposition — the ComfyUI wrapper form (a directory
|
|
59
|
+
holding layers.json and one PNG per layer) or an upstream .psd — and print
|
|
60
|
+
every layer: draw order, name, tag group, box, size, opaque pixels, depth,
|
|
61
|
+
and three plausibility figures over its opaque pixels — translucent share
|
|
62
|
+
(alpha below 128), background share (min channel above 235) and area as a
|
|
63
|
+
multiple of the rest of the figure (the other layers' union). A layer that
|
|
64
|
+
crosses a plausibility rule gets a WARN line naming the rule and the bar;
|
|
65
|
+
--propose-plan leaves it out. Refuses, by name, a missing file, an
|
|
66
|
+
unknown tag, a PNG whose size is not its box, and anything else outside
|
|
67
|
+
the input contract — never a WARN.
|
|
68
|
+
|
|
69
|
+
rig-parts sheet --source <painting.png> --layers <path> [--layers <path> ...]
|
|
70
|
+
--out <sheet.png> [--cell <px>] [--cols <n>]
|
|
71
|
+
A contact sheet: the painting, then every layer or part, each on a
|
|
72
|
+
checkerboard and labelled with its name, size and opaque pixel count.
|
|
73
|
+
--layers takes anything \`layers\` reads, or a parts.json (its PNGs are read
|
|
74
|
+
from parts/ beside it). --cell defaults to 220, --cols to 8; both are
|
|
75
|
+
printed. The same information is printed as text.
|
|
76
|
+
|
|
77
|
+
rig-parts propose --parts <dir> --source <painting.png> --out <dir> [--compare <config.json>]
|
|
78
|
+
Propose bones, meshes, regions and an idle from the assembled parts (<dir>
|
|
79
|
+
holds parts.json and parts/). Roles come from each part's See-through tag,
|
|
80
|
+
never its name. Writes <out>/proposal.json (config-shaped: bones, meshes,
|
|
81
|
+
regions, motion with its blink — and a blink.still cut for a lash that
|
|
82
|
+
reaches far above its eyewhite, when a clear row allows one; no blink
|
|
83
|
+
when no part is an eyewhite, and no brows in it when no part is an
|
|
84
|
+
eyebrow, each said in a note; an iris or lash on a side with no
|
|
85
|
+
eyewhite rides head, said in a note — and notes)
|
|
86
|
+
and the overlay to correct against, <out>/render/landmarks.png and
|
|
87
|
+
landmarks_head.png. A figure with no face part gets a face box derived
|
|
88
|
+
from the head run's hair and neck, said in the first note; with no
|
|
89
|
+
head-run hair or neck it is refused (PROPOSE_FACE_PRESENT). Prints every
|
|
90
|
+
note and a LINT line for each chain link that lies off its mesh's art, for a
|
|
91
|
+
hip that is not below the chest, and for a hip above ${HIP_MIN_FRACTION} of the figure's
|
|
92
|
+
height (the shoulders); a headwear/earwear layer with hanging strands
|
|
93
|
+
gets one pendulum chain per strand and a note with each strand's x, rows
|
|
94
|
+
and width ("-- no chain proposed" is the one to act on; AUTHORING §3).
|
|
95
|
+
--compare prints each shared bone's distance, proposal to config, in px.
|
|
96
|
+
It reads bone origins only — not parents, tips, lengths, directions or
|
|
97
|
+
names, so a changed parent or tip scores 0; rig-parts compare reads those.
|
|
98
|
+
After the LINT lines, one "coverage" line per bone says which check read
|
|
99
|
+
it ("checked, clean" or "checked, LINT") or that none did and why ("not
|
|
100
|
+
checked": its role — a control binds nothing and may sit off the art —
|
|
101
|
+
a single bone or an explicit [bone, from, to] segment, which the off-art
|
|
102
|
+
check does not read, a region, a mesh naming no part), and a summary
|
|
103
|
+
counts the three apart with the roles. Writes <out>/${BASIS_FILE} beside
|
|
104
|
+
the proposal (spec "${BASIS_SPEC}"): per proposed bone, what its origin
|
|
105
|
+
and its stated tip rest on — a joint (with its state and score as the
|
|
106
|
+
file declared them), a measurement off a part, a ratio with its
|
|
107
|
+
constants, or other bones — and under --compare whether the config's
|
|
108
|
+
bone differs (origin, tip, parent; how far, in px). No confidence is
|
|
109
|
+
computed. It prints "wrote ${BASIS_FILE} (…)" after the coverage lines.
|
|
110
|
+
|
|
111
|
+
rig-parts propose … [--keypoints <keypoints.json> [--person <id>] [--wrist-snap <px>]]
|
|
112
|
+
Read the figure's pose from one explicit file (spec "${KEYPOINTS_SPEC}"):
|
|
113
|
+
its space (painting-px, origin top-left, y down — another is refused),
|
|
114
|
+
the image's width and height (held to --source's, KEYPOINTS_IMAGE_SIZE),
|
|
115
|
+
its source, and people, each an id and body-18 joints (r and l are the
|
|
116
|
+
subject's sides), each observed with a position, occluded with an
|
|
117
|
+
optional position (the producer's estimate) or missing; an unlisted
|
|
118
|
+
joint is missing. More than one person needs --person (nothing picks
|
|
119
|
+
one). Painting px reach rig px by the overlay's map, x * W/width,
|
|
120
|
+
y * H/height. A joint with a position places its bone as given: neck,
|
|
121
|
+
hip (the midpoint of r_hip and l_hip), the chest half way along neck ->
|
|
122
|
+
hip, and each sleeve chain shoulder -> elbow with its tip at the wrist
|
|
123
|
+
when every handwear part holds a wrist; a missing joint leaves its bone to
|
|
124
|
+
the rule. One note per joint says which. LINT then reads the torso along
|
|
125
|
+
the neck -> hips line and each sleeve chain along shoulder -> wrist
|
|
126
|
+
instead of by screen y, and prints the rule set it used first. Without
|
|
127
|
+
--keypoints nothing changes. A wrist holds a part when it lies within
|
|
128
|
+
${WRIST_ON_PART_PX} px of its art (the larger of the x and y gaps);
|
|
129
|
+
--wrist-snap <px>, an integer above ${WRIST_ON_PART_PX}, binds a wrist on no
|
|
130
|
+
part to the one nearest part within <px>, said in a "wrist snap:" note
|
|
131
|
+
(with --keypoints, not with --from-config). Without it nothing changes.
|
|
132
|
+
|
|
133
|
+
rig-parts propose --parts <dir> --source <painting.png> --out <dir> --from-config <config.json>
|
|
134
|
+
Draw the config's CURRENT bones instead (<out>/render/landmarks_config.png
|
|
135
|
+
and _head) and LINT them (by the joints too, given --keypoints), with the
|
|
136
|
+
coverage lines after them; no ${BASIS_FILE}, as nothing is proposed.
|
|
137
|
+
Exits 1 when any LINT line is printed.
|
|
138
|
+
|
|
139
|
+
rig-parts propose --head-box --full <dir | layers.json | file.psd> --canvas <W>x<H>
|
|
140
|
+
Propose seethrough.head_box (source px, square) from the full run's
|
|
141
|
+
layers for a painting of WxH px (portrait or landscape: the full run is
|
|
142
|
+
mapped back through the pad inputs added), held inside the painting; a
|
|
143
|
+
shift is printed when one was needed.
|
|
144
|
+
|
|
145
|
+
rig-parts compare --left <file> --right <file> [--map <bonemap.json>]
|
|
146
|
+
Compare two skeletons bone by bone. Each file is a config, a proposal.json
|
|
147
|
+
or a rig.json (rigc-rig/1), told apart by what it states: "spec" (read by
|
|
148
|
+
rig-c's own reader), "key" (the config loader) or "bones". No parts and
|
|
149
|
+
no painting are read. Both sides are brought to one form — rig px, y down;
|
|
150
|
+
a rig.json through its stage (skeleton x, y, width, height), the y flip
|
|
151
|
+
through rig-c's — and each pair prints its origin distance, its
|
|
152
|
+
parent (the same through the pairing; the right's or the left's ancestor
|
|
153
|
+
at depth k, with the bones between that are in no pair; DIFFERENT, naming
|
|
154
|
+
both; or NOT MAPPED), its tip distance, both lengths and their
|
|
155
|
+
difference, and the turn of its direction in degrees (clockwise as
|
|
156
|
+
drawn). A bone of length 0 has no tip: those three say SKIP with the
|
|
157
|
+
reason, never 0. A rig with no stage, or two configs at different
|
|
158
|
+
rig_scale, are not in one frame: origin, tip, the length difference and
|
|
159
|
+
direction say SKIP "frames not related", and parents, required bones and
|
|
160
|
+
roles still run. Then the unmapped bones of each side by name, the
|
|
161
|
+
required bones, and each side's roles read off the spec — target (an ik
|
|
162
|
+
target, a transform source), deforms (a mesh weight or segment, a region),
|
|
163
|
+
control (binds nothing; keyed, or parents others), unclassified (said why)
|
|
164
|
+
— with the control and target counts, reported and never failed.
|
|
165
|
+
With no --map a bone pairs only with the bone of the same name; nothing
|
|
166
|
+
pairs by resemblance. --map is {"spec": "spine-parts-bonemap/1"} with
|
|
167
|
+
optional "pairs" ([[left, right], ...]: the whole pairing), "required"
|
|
168
|
+
(left names that must be present on both sides) and "frame" ({"scale",
|
|
169
|
+
"offset": [x, y]}: right = scale * left + offset, numbers the author
|
|
170
|
+
wrote — nothing is fitted). Exit 1 names each required bone missing
|
|
171
|
+
(STRUCTURE_REQUIRED_PRESENT) or each refusal of a file; every other row is
|
|
172
|
+
a figure and exits 0.
|
|
173
|
+
|
|
174
|
+
rig-parts rig --config <config.json> --parts <dir> --out <dir> [--idle-keys ctl|direct]
|
|
175
|
+
[--page-edges pot|free] [--pack-shape rect|polygon]
|
|
176
|
+
Author the rig: bones at the config's landmarks (a chain makes
|
|
177
|
+
<chain>0..n, each link turned along its chain with its length, so a
|
|
178
|
+
physics constraint added later has a lever; every offset under a turned
|
|
179
|
+
link is in its frame, so nothing moves; a translate key under a turned
|
|
180
|
+
link or a scale/shear key on one is refused, RIG_KEY_FRAME_UNTURNED),
|
|
181
|
+
a square lattice mesh over every part in config.meshes (a contour
|
|
182
|
+
mesh where the entry says contour, with its regions' bones weighted
|
|
183
|
+
by their declared falloff, AUTHORING §3)
|
|
184
|
+
weighted by distance to its candidate bone segments, a region for every
|
|
185
|
+
part in config.regions (a motion.blink.still part as two: the rows above
|
|
186
|
+
its row on a second slot <part>_still, which the blink does not move),
|
|
187
|
+
and one idle of sines and a blink whose closed hold is at least one
|
|
188
|
+
12 fps frame, so the idle frames and the loop show the eyes shut
|
|
189
|
+
(RIG_BLINK_HOLD_SPANS_A_FRAME otherwise); no blink when the config
|
|
190
|
+
states no motion.blink, and a blink group that names no bone, or one
|
|
191
|
+
bone twice, is refused by the loader, CONFIG_BLINK_GROUP_MEMBERS or
|
|
192
|
+
CONFIG_BLINK_GROUP_UNIQUE, before rigc starts, as are two tracks on one
|
|
193
|
+
bone property, CONFIG_BONE_PROPERTY_KEYED_ONCE. Beside the idle,
|
|
194
|
+
every animation of the file config.motion.animations_from names is
|
|
195
|
+
written into motion.json as read, for rigc's gate to judge (AUTHORING
|
|
196
|
+
§3). --parts is the
|
|
197
|
+
directory holding parts.json and parts/<name>.png. The result is built
|
|
198
|
+
through rig-c (profile spine-html, packed with --page-edges and
|
|
199
|
+
--pack-shape as for check; rigc's build gates the compile and the packed pages on disk) in a
|
|
200
|
+
scratch directory first, and --out receives images/*.png, rig.json,
|
|
201
|
+
motion.json and mesh_report.json only when it is green. Prints one line
|
|
202
|
+
per mesh and the rigc gate lines.
|
|
203
|
+
--idle-keys says where the idle's keys on a bone a mesh is weighted to go:
|
|
204
|
+
ctl (the default) keys a same-origin <bone>_ctl parent instead, which
|
|
205
|
+
passes A15_IDLE_NO_MESH_BONE_KEYS on any rig-c; direct keys the bone
|
|
206
|
+
itself and declares invariants.idleDrivesMeshes in rig.json, which needs
|
|
207
|
+
rig-c 1.3.0 or later and makes A15 a SKIP that prints its cost (the
|
|
208
|
+
stage prints that SKIP line). The pose is the same to one level of float
|
|
209
|
+
rounding, and so is the per-frame mesh work (AUTHORING §5).
|
|
210
|
+
rig-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>]
|
|
211
|
+
[--page-edges pot|free] [--pack-shape rect|polygon]
|
|
212
|
+
[--requirements <file.json>] [--seam-pairs] [--keep-stills]
|
|
213
|
+
Build, gate, render and measure a rig through rig-c's CLI (the rigc at
|
|
214
|
+
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
|
|
215
|
+
a rig spec, which rigc build compiles, not a compiled skeleton.json.
|
|
216
|
+
motion.json is required (rigc build takes --motion; "animations": {}
|
|
217
|
+
builds); an "idle" in it is not.
|
|
218
|
+
${PARTS_HOME_SENTENCE}; ${NO_PARTS_SENTENCE}.
|
|
219
|
+
Both are only read. Into --out:
|
|
220
|
+
build/ (rigc build --profile spine-html --pack --page-edges <value>
|
|
221
|
+
--pack-shape <value>: the packed atlas is the artifact), gate_spine-html.txt (the gate lines
|
|
222
|
+
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
223
|
+
--geometry: the frames and geometry.json, the skinned vertices and
|
|
224
|
+
every bone's world transform per frame),
|
|
225
|
+
contact.png, motion_heat.png and check.json (with rigc_entry, the rigc
|
|
226
|
+
entry that gated the build, and pack_mode, the two pack flags). PASS needs every gate summary
|
|
227
|
+
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
228
|
+
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} rig px over ${SEAM_PX_LEVEL} (the frame pixels over ${SEAM_PX_LEVEL}
|
|
229
|
+
divided by s², s the frame scale the seam: line prints and check.json's
|
|
230
|
+
seam_frame_scale holds, so the bar means the same at every scale), and
|
|
231
|
+
the loop (idle frame 0 vs the frame at t = duration) at max |d| 0. A
|
|
232
|
+
failing seam, or any seam under --seam-pairs, is split by the pair of
|
|
233
|
+
parts each counted pixel lies between: one "seam pair" line each, worst
|
|
234
|
+
first, with its count in frame px, frame and rig boxes and a picture
|
|
235
|
+
under seam/, and check.json's seam_pairs (AUTHORING §7). A seam picture
|
|
236
|
+
is the setup-pose frame with that pair's counted pixels painted magenta
|
|
237
|
+
(255, 0, 255) — magenta = a counted pixel — and a cyan (0, 255, 255)
|
|
238
|
+
box one pixel outside them. --keep-stills leaves the setup-pose still the seam reads,
|
|
239
|
+
rigc's render directory with its frames.json (viewport and background),
|
|
240
|
+
under setup_still/. Then six judgement lines, each in
|
|
241
|
+
check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
|
|
242
|
+
bars (AUTHORING §7): BREATH_VISIBLE (the topwear moves, the footwear does
|
|
243
|
+
not, each rendered alone), BLINK_NO_HOLE (the setup pose with the blink
|
|
244
|
+
held shut shows no background inside the eyewhite box), CHAIN_LAG (every
|
|
245
|
+
rotate track lags its keyed ancestor and amplitude grows down each
|
|
246
|
+
chain, read off motion.json), TIP_OVER_ROOT (each handwear/bottomwear
|
|
247
|
+
part's lower half travels further than its upper half, measured from
|
|
248
|
+
the idle's posed geometry, so the render size does not move it),
|
|
249
|
+
STILL_REGIONS_DARK (the face and the parts over it the head bone alone
|
|
250
|
+
carries keep their place relative to that bone, and the feet theirs on
|
|
251
|
+
the screen, from the idle's posed geometry up to the arithmetic's own
|
|
252
|
+
rounding; how deep any other slot's art swings into the face is
|
|
253
|
+
reported, with no bar) and
|
|
254
|
+
TEXTURE_STRETCH (every mesh triangle's edges over the idle against their
|
|
255
|
+
rest length, max(ratio, 1/ratio) <= ${TEXTURE_STRETCH_CEILING}; the worst
|
|
256
|
+
triangle is named by slot, triangle, vertices, edge and frame).
|
|
257
|
+
Needs rig-c ${RIGC_GEOMETRY_VERSION} or later, whose render writes
|
|
258
|
+
geometry.json; an older rigc is refused, CHECK_RIGC_VERSION, before
|
|
259
|
+
anything is built.
|
|
260
|
+
Regions come from parts.json's See-through tags; a line with nothing to
|
|
261
|
+
read says SKIP and why — neither a pass nor a failure — and PASS needs
|
|
262
|
+
every line that measured to be PASS. Then RECOMPOSITE_HOLES: REPORTED,
|
|
263
|
+
read from parts.json's recomposite block (uncovered error px, hole count,
|
|
264
|
+
the largest hole's box and the parts bordering it) — a line with no bar,
|
|
265
|
+
never a FAIL, because a pixel no part holds is missing from both sides of
|
|
266
|
+
the seam; SKIP when parts.json has no such block. What a bar cannot
|
|
267
|
+
read it says SKIP for, by name, with the reason (AUTHORING §7, Measuring a
|
|
268
|
+
rig rig-parts did not assemble): without parts.json the seam,
|
|
269
|
+
BREATH_VISIBLE, BLINK_NO_HOLE, TIP_OVER_ROOT, STILL_REGIONS_DARK and
|
|
270
|
+
RECOMPOSITE_HOLES; without an idle the loop and every line that reads idle
|
|
271
|
+
frames, and no idle is rendered. The gate always runs. The last line says
|
|
272
|
+
how many of the nine bars measured and names the skipped ones.
|
|
273
|
+
--source adds SETUP_POSE_VS_SOURCE: REPORTED, never a FAIL — the setup
|
|
274
|
+
pose against the painting by assemble's own recomposite figures (mean |d|,
|
|
275
|
+
% within 8, error px over 40, uncovered error px where the pose has alpha
|
|
276
|
+
128 or less, holes). ${SOURCE_SENTENCE}; a painting that is not this
|
|
277
|
+
rig's is refused, CHECK_SOURCE_SIZE, before anything is built. Prints the pack line
|
|
278
|
+
beside the spineboy yardstick (${SPINEBOY_YARDSTICK}), a reference and not
|
|
279
|
+
a bar. Exit 0 on PASS, 1 on FAIL — every FAIL line names the bar, the value
|
|
280
|
+
and the value required.
|
|
281
|
+
--page-edges is handed to rigc verbatim and defaults to ${DEFAULT_PAGE_EDGES}: the
|
|
282
|
+
least-area page the parts need (on the two public examples, rect, 967x1338
|
|
283
|
+
and 479x1166, where pot writes 1024x2048 and 512x2048), at the cost rigc
|
|
284
|
+
states — region attachments sample within 1 LSB of the loose build
|
|
285
|
+
rather than exactly (measured on both examples: every idle frame within
|
|
286
|
+
1 level of the pot build's; AUTHORING §5, Page edges). pot is a power of two on both edges, for a consumer
|
|
287
|
+
that mipmaps or repeats the page (the atlas declares filter Linear,
|
|
288
|
+
Linear and no repeat). rigc's pack line carries ", page edges free" under
|
|
289
|
+
free; a line that disagrees with the value passed, or a pot page that is
|
|
290
|
+
not a power of two, is refused, CHECK_PACK_PAGE_EDGES.
|
|
291
|
+
--pack-shape is handed to rigc verbatim and defaults to ${DEFAULT_PACK_SHAPE}:
|
|
292
|
+
a region only meshes draw is packed by its emitted hull, so a neighbour
|
|
293
|
+
may sit inside its rectangle where the hull is not (on the two public
|
|
294
|
+
examples, under free, 922x1348 and 477x1151 where rect writes 967x1338
|
|
295
|
+
and 479x1166), at the cost of the same 1-level class as free (measured
|
|
296
|
+
on both examples: every idle frame within 1 level of the rect build's;
|
|
297
|
+
AUTHORING §5, Pack shape). rect keeps every region's cell apart and
|
|
298
|
+
writes the page earlier releases wrote. rigc's pack line ends
|
|
299
|
+
", shape rect" or ", shape polygon"; a line that disagrees with the
|
|
300
|
+
value passed is refused, CHECK_PACK_SHAPE.
|
|
301
|
+
${REQUIREMENTS_SENTENCE} (AUTHORING §7, Declared requirements). It is
|
|
302
|
+
read and every name in it resolved against rig.json and motion.json
|
|
303
|
+
before anything is built: a bone, constraint, slot, mesh attachment or
|
|
304
|
+
animation that does not resolve, a missing field or bar, or a follow
|
|
305
|
+
naming a property its constraint does not drive is refused by name
|
|
306
|
+
(REQUIREMENTS_*). Each named animation is rendered once with --geometry
|
|
307
|
+
into requirements/as-declared/<animation>/; scene targets (a bone whose
|
|
308
|
+
parent is the root, placed at a stage point or at stage points at stated
|
|
309
|
+
times) are written onto a throwaway copy, never the rig. Each follow is
|
|
310
|
+
measured from three poses of the same frames — as declared, released
|
|
311
|
+
(that mix forced to 0, its keys removed) and full (forced to 1) — into
|
|
312
|
+
requirements/released/<name>/ and requirements/full/<name>/. One line per
|
|
313
|
+
requirement, NAME: PASS|FAIL|NOT MEASURABLE with its figures, the bar as
|
|
314
|
+
declared and the worst frame, a requirements block in check.json, and a
|
|
315
|
+
summary line naming the kinds not declared. A FAIL is
|
|
316
|
+
CHECK_REQUIREMENT_MET; a NOT MEASURABLE (a tip or an axis on a bone of
|
|
317
|
+
length 0, an aim whose target sits on the origin on every frame, a
|
|
318
|
+
follow no frame of which reaches its least drive) is
|
|
319
|
+
CHECK_REQUIREMENT_MEASURABLE, and the run is not PASS. Without the flag
|
|
320
|
+
nothing is read, written or printed for it.
|
|
321
|
+
|
|
322
|
+
rig-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
|
|
323
|
+
Encode a frame set rigc render wrote (its --out directory, or the set
|
|
324
|
+
directory inside it) as a looping animation: .png writes an APNG
|
|
325
|
+
(acTL/fcTL/fdAT, lossless — the exactness record), .png with --palette an
|
|
326
|
+
indexed APNG (colour type 3, one palette for every frame: 255 median-cut
|
|
327
|
+
colours plus one transparent entry, alpha graded per entry, no dithering,
|
|
328
|
+
filter None — the README-sized file), .gif a GIF89a (the same 255-colour
|
|
329
|
+
median cut, no dithering, LZW, delays rounded so the loop's length is
|
|
330
|
+
exact). The indexed APNG and the GIF print their palette error, per
|
|
331
|
+
channel over every frame. The fps is read from frames.json. When the last
|
|
332
|
+
frame equals frame 0 it is dropped, and the output says so: the loop wraps
|
|
333
|
+
onto frame 0 itself. Animated WebP is not written — it needs a VP8/VP8L
|
|
334
|
+
encoder, out of scope.
|
|
335
|
+
|
|
336
|
+
rig-parts assemble --source <painting.png> --full <dir|psd> --head <dir|psd>
|
|
337
|
+
--config <config.json> --out <dir> [--seam near-white|silhouette]
|
|
338
|
+
[--project core|visible]
|
|
339
|
+
Merge the full-body and head-crop See-through runs into rig-space parts:
|
|
340
|
+
<out>/rig/parts/<name>.png (each cropped to its alpha box), <out>/rig/parts.json,
|
|
341
|
+
<out>/render/recomposite_rig.png and <out>/render/${ERROR_MAP_FILE} (the error
|
|
342
|
+
map: uncovered error px red, covered error px blue, the rest the painting in
|
|
343
|
+
light grey). Reads config.seethrough.head_box and
|
|
344
|
+
.resolution and config.assemble.rig_scale, .plan, .extend_below_crop,
|
|
345
|
+
.patches — extra parts cut from the painting itself over a rig-pixel box
|
|
346
|
+
(alpha "silhouette": the painting's figure inside it; "box": all of it),
|
|
347
|
+
drawn "back", "front" or {"before": <plan part>}, recorded in parts.json
|
|
348
|
+
as from "painting:<name>" and counted 100 % source — and .cuts — a plan
|
|
349
|
+
part's pixels inside a rig-pixel polygon taken into a new part, drawn the
|
|
350
|
+
same way, recorded with the plan part's from — and no rig section:
|
|
351
|
+
bones, meshes, regions and motion need not exist yet, because propose
|
|
352
|
+
drafts them from these parts (AUTHORING §4, the table).
|
|
353
|
+
Prints one line per part, one \`cut:\` line per part a cut split (its
|
|
354
|
+
opaque pixels = what it kept + what each cut took), the seam override
|
|
355
|
+
counts, the \`pixels:\` totals
|
|
356
|
+
(opaque = visible + occluded; taken from the painting; visible but not
|
|
357
|
+
projected), and \`recomposite vs source\` (mean |d| and % within 8 over the
|
|
358
|
+
mean channel; error px: max channel > 40; uncovered: of those, where no
|
|
359
|
+
part has alpha above 128), then the uncovered holes (8-connected) and the
|
|
360
|
+
largest ${HOLES_LISTED}, each \`uncovered hole N: <px> px at x,y wxh (between
|
|
361
|
+
"<part>" <px> px, …)\` — the list parts.json holds under recomposite.
|
|
362
|
+
Writes nothing unless every check passed.
|
|
363
|
+
--seam defaults to ${DEFAULT_SEAM_RULE}. --project says where a layer takes
|
|
364
|
+
the painting's pixel: core (the reference's) erodes every layer's top-most
|
|
365
|
+
opaque area by 5x5 first, so a part a few pixels wide takes none; visible
|
|
366
|
+
erodes only along a rim where a later layer is in front. It defaults to
|
|
367
|
+
${DEFAULT_PROJECT_RULE}.
|
|
368
|
+
|
|
369
|
+
rig-parts assemble --propose-plan --source <painting.png> --full <dir|psd>
|
|
370
|
+
--head <dir|psd> --config <config.json>
|
|
371
|
+
Print {plan, extend_below_crop, notes} for config.assemble, from the two
|
|
372
|
+
runs. A layer \`layers\` WARNs about (PLAN_LAYER_TRANSLUCENT,
|
|
373
|
+
PLAN_LAYER_BACKGROUND, PLAN_LAYER_OVERSIZED) is not proposed, and a note
|
|
374
|
+
names it, its figures and the rule. Reads only config.seethrough.head_box,
|
|
375
|
+
config.seethrough.resolution and config.assemble.rig_scale — the rest of
|
|
376
|
+
the config need not exist yet.
|
|
377
|
+
|
|
378
|
+
rig-parts inputs --source <painting.png> --config <config.json> --out <dir>
|
|
379
|
+
Cut the two images See-through is fed: <out>/st_input_full.png (the
|
|
380
|
+
painting centred on a white square of its longer side: white left and
|
|
381
|
+
right of a portrait painting, above and below a landscape one) and, when
|
|
382
|
+
the config sets seethrough.head_box, <out>/st_input_head.png (that box,
|
|
383
|
+
cropped at its exact size). Refuses a translucent painting and a head box
|
|
384
|
+
outside the painting, by name.
|
|
385
|
+
|
|
386
|
+
rig-parts comfy seethrough --image <png> --out <dir> [--host <url>]
|
|
387
|
+
[--resolution 1024] [--steps 30] [--seed 42] [--offload] [--lama] [--nf4]
|
|
388
|
+
[--prefix spine_parts] [--wait 1800] [--timeout 3600] [--poll 3]
|
|
389
|
+
Optional. Run See-through on a ComfyUI box with the jtydhr88/ComfyUI-See-through
|
|
390
|
+
wrapper installed, and write the wrapper form \`layers\` reads into <out>
|
|
391
|
+
(absent or empty): layers.json, parts/<tag>.png, meta.json, previews/.
|
|
392
|
+
The host is --host or COMFY_HOST and has no default. Checks every node and
|
|
393
|
+
input against the box's /object_info before uploading, waits for an empty
|
|
394
|
+
queue (at most --wait s), polls /history (at most --timeout s), and
|
|
395
|
+
writes <out> only after the layer reader accepts what came back.
|
|
396
|
+
--lama turns the wrapper's LaMa inpainting on and --nf4 its nf4
|
|
397
|
+
quantisation; both are off unless given, as --offload is.
|
|
398
|
+
|
|
399
|
+
rig-parts comfy paint --config <config.json> --out <dir> [--host <url>]
|
|
400
|
+
[--seeds 1] [--seed0 <generation.seed>] [--wait 1800] [--timeout 3600] [--poll 3]
|
|
401
|
+
Optional. Generate the painting from the config's inline generation block:
|
|
402
|
+
<out>/painting_<seed>.png and painting_<seed>_meta.json (the prompts
|
|
403
|
+
verbatim, checkpoint, LoRAs, sampler, control, elapsed) for --seeds
|
|
404
|
+
seeds from --seed0, and control_<skeleton>.png when generation.control
|
|
405
|
+
is set. The pose words are generation.pose, or the control skeleton's own.
|
|
406
|
+
The config needs only key and generation here; the rest comes later.
|
|
407
|
+
|
|
408
|
+
rig-parts build --config <config.json> --source <painting.png> --full <dir|psd>
|
|
409
|
+
--head <dir|psd> --out <dir> [--seam near-white|silhouette]
|
|
410
|
+
[--project core|visible] [--page-edges pot|free]
|
|
411
|
+
[--pack-shape rect|polygon] [--loop] [--requirements <file.json>]
|
|
412
|
+
[--idle-keys ctl|direct]
|
|
413
|
+
assemble, then rig, then check, in one process, each stage's own lines
|
|
414
|
+
printed under [assemble], [rig] and [check]; the first stage that refuses
|
|
415
|
+
stops the build with its own FAIL lines. The config must already carry
|
|
416
|
+
bones, meshes, regions and motion — propose is not a step of build: run it,
|
|
417
|
+
correct the proposal against its overlay, and write the result into the
|
|
418
|
+
config. Into --out: parts/ + parts.json + recomposite_rig.png +
|
|
419
|
+
${ERROR_MAP_FILE} (assemble),
|
|
420
|
+
rig/ (rig), check/ (check, with the packed build in check/build/), and with
|
|
421
|
+
--loop idle.png (lossless APNG), idle-indexed.png (indexed APNG) and
|
|
422
|
+
idle.gif from check/idle_frames/, then one loop: line with the three sizes
|
|
423
|
+
and the two palette errors. The paths it writes are cleared first. A
|
|
424
|
+
green build ends with the pack line beside the spineboy yardstick and the
|
|
425
|
+
three artifact paths — skeleton .json, .atlas and the packed page: the
|
|
426
|
+
packed atlas is the result, the loose parts are the intermediate it was
|
|
427
|
+
made from. --seam defaults to ${DEFAULT_SEAM_RULE}, --project to
|
|
428
|
+
${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES} and
|
|
429
|
+
--pack-shape to ${DEFAULT_PACK_SHAPE} (as for check; both packed builds, rig's gate
|
|
430
|
+
and check's artifact, take them). --requirements is forwarded to check
|
|
431
|
+
(as for check); the file's own shape is read before assemble runs, and
|
|
432
|
+
its names resolve against the rig in the check, before it builds.
|
|
433
|
+
--idle-keys is forwarded to the rig stage (as for rig) and defaults to
|
|
434
|
+
ctl; the first line names it when it is direct. A config with a
|
|
435
|
+
two-bone ik over chain links the idle keys needs --idle-keys direct:
|
|
436
|
+
rigc refuses the pair a control splits, and the RIG_RIGC_GREEN line
|
|
437
|
+
names the flag on the command that ran.
|
|
438
|
+
|
|
439
|
+
rig-parts compose --scene <scene.json> --out <dir> [--requirements <file.json>] [--accept <bar>[,<bar>...]]
|
|
440
|
+
Bind several finished characters into one rig. The scene file (spec
|
|
441
|
+
"${SCENE_SPEC}") states the canvas {width, height}, an optional plate
|
|
442
|
+
{image, provenance: ${PROVENANCES.join(' | ')}, note?}, the characters
|
|
443
|
+
[{id, build, offset: [x, y], scale?}] — each build the --out of a green
|
|
444
|
+
\`rig-parts build\` (its check/check.json PASS), each offset a
|
|
445
|
+
translation from its rig px to canvas px, each scale (a positive
|
|
446
|
+
number, 1 when absent) the character's size about its stage's top-left
|
|
447
|
+
corner: every length it carries is scaled as Spine's loader scales a
|
|
448
|
+
skeleton (bones, regions, weight binds, constraint lengths, translate
|
|
449
|
+
keys), and a physics wind or gravity, which no per-character value
|
|
450
|
+
can scale, is refused (SCENE_SCALE_EXACT) — and order: the draw order,
|
|
451
|
+
back to front, of character ids (that character's remaining slots, in
|
|
452
|
+
its own order) and "<id>:<slot>" entries, every slot exactly once.
|
|
453
|
+
Paths are relative to the scene file. Every bone, slot, attachment,
|
|
454
|
+
constraint, track, group and easing of a character is renamed
|
|
455
|
+
"<id>:<name>" under one shared root; its images "<id>${IMAGE_SEP}<file>" (an
|
|
456
|
+
atlas region cannot hold ":"). The offset is added once, to what the
|
|
457
|
+
character places in root's own frame: its first-level bones, and a
|
|
458
|
+
region on a slot root carries or a weight bound to root. The root is
|
|
459
|
+
shared, so a character whose idle keys it, or whose constraint names it,
|
|
460
|
+
is refused. The plate is a region on bone "${PLATE}" under
|
|
461
|
+
root, drawn first, its image at canvas (0, 0). One idle holds every
|
|
462
|
+
character's tracks; an animation beside a character's idle (its
|
|
463
|
+
config's motion.animations_from) rides along as "<id>:<name>",
|
|
464
|
+
prefixed as the idle is and scheduled by nothing. Refused by name, every problem at once: an id
|
|
465
|
+
repeated, empty or holding ":" (or "/", "\\", a leading "."); a build
|
|
466
|
+
missing a file or whose own check is not green; characters built at
|
|
467
|
+
different rig_scale; a character whose parts' boxes, placed, leave the
|
|
468
|
+
canvas; an order entry naming no character or slot; a slot named twice
|
|
469
|
+
or never; a constraint, or a --requirements name, that names a bone of
|
|
470
|
+
another character without its prefix (SCENE_NAME_PREFIXED); a plate not
|
|
471
|
+
of the canvas size; idles of different durations; a key in a build's
|
|
472
|
+
rig or motion compose does not know. The result is gated through
|
|
473
|
+
rig-c as rig gates its own (build --profile spine-html --pack, the
|
|
474
|
+
compile and the packed pages on disk); --out receives rig/ (rig.json,
|
|
475
|
+
motion.json, images/) and ${SCENE_REPORT_FILE} (spec "${SCENE_REPORT_SPEC}":
|
|
476
|
+
every setting, each character's offset, scale, bounds and shift (and what it
|
|
477
|
+
moved),
|
|
478
|
+
the plate's provenance, the declared and the composed order) only when
|
|
479
|
+
it is green; then check runs over rig/ into check/, as on a rig it did
|
|
480
|
+
not build (no parts.json: the seam, BREATH_VISIBLE, BLINK_NO_HOLE,
|
|
481
|
+
TIP_OVER_ROOT, STILL_REGIONS_DARK and RECOMPOSITE_HOLES say SKIP and
|
|
482
|
+
why), --requirements forwarded. Nothing is inferred: not the order, not
|
|
483
|
+
an overlap, not a scale, not a person; the plate's provenance and the
|
|
484
|
+
order are the author's and judged by nothing. --accept (or a
|
|
485
|
+
character's "accept": [...] in the scene file; the two lists are
|
|
486
|
+
joined, the command line's applying to every character) takes a build
|
|
487
|
+
whose own check failed only the bars named — ${ACCEPTABLE_BARS.join(', ')};
|
|
488
|
+
the gate, a declared requirement and an unknown name are refused
|
|
489
|
+
(SCENE_ACCEPT_KNOWN). It is for looking, not shipping: the [compose]
|
|
490
|
+
line and ${SCENE_REPORT_FILE} name what was accepted, and the run exits 1.
|
|
491
|
+
Exit 0 when the check is PASS and nothing was accepted, 1 otherwise.
|
|
492
|
+
|
|
493
|
+
rig-parts --version
|
|
494
|
+
rig-parts --help
|
|
495
|
+
|
|
496
|
+
${LATER.length === 0 ? '' : `not implemented in this version (each exits 2):\n${LATER.map(([name, what]) => ` ${name.padEnd(9)} ${what}`).join('\n')}\n\n`}exit codes: 0 done, 1 input refused (FAIL lines name every reason), 2 usage or not implemented
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
function printRefusal(err: unknown): number {
|
|
500
|
+
if (err instanceof PartsError) {
|
|
501
|
+
for (const p of err.problems) console.log(` FAIL ${problemLine(p)}`);
|
|
502
|
+
console.log(`refused: ${err.problems.length} problem(s)`);
|
|
503
|
+
return EXIT_REFUSED;
|
|
504
|
+
}
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function usage(message: string): number {
|
|
509
|
+
console.log(` FAIL USAGE: ${message}`);
|
|
510
|
+
console.log('rig-parts --help lists the commands');
|
|
511
|
+
return EXIT_USAGE;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function fixed(v: number | null): string {
|
|
515
|
+
return v === null ? '-' : v.toFixed(4);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function printLayerTable(set: LayerSet): void {
|
|
519
|
+
console.log(`rig-parts layers: ${set.form === 'wrapper' ? 'ComfyUI wrapper form' : 'PSD'}, ${set.source}`);
|
|
520
|
+
console.log(` canvas ${set.canvas.w}x${set.canvas.h}, ${set.layers.length} layer(s), back to front`);
|
|
521
|
+
const figures = layerFigures(set);
|
|
522
|
+
const rows = set.layers.map((l, i) => [
|
|
523
|
+
String(l.drawOrder),
|
|
524
|
+
l.name,
|
|
525
|
+
l.tag.group,
|
|
526
|
+
`${l.left},${l.top}`,
|
|
527
|
+
`${l.right},${l.bottom}`,
|
|
528
|
+
`${l.pixels.width}x${l.pixels.height}`,
|
|
529
|
+
String(l.opaquePx),
|
|
530
|
+
fixed(l.depth),
|
|
531
|
+
pct(figures[i].translucent),
|
|
532
|
+
pct(figures[i].background),
|
|
533
|
+
times(figures[i].areaRatio),
|
|
534
|
+
]);
|
|
535
|
+
const head = ['order', 'name', 'group', 'left,top', 'right,bottom', 'size', 'opaque_px', 'depth', 'translucent', 'background', 'area'];
|
|
536
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
537
|
+
const line = (cells: string[]): string => ` ${cells.map((c, i) => c.padEnd(widths[i])).join(' ')}`.trimEnd();
|
|
538
|
+
console.log(line(head));
|
|
539
|
+
for (const r of rows) console.log(line(r));
|
|
540
|
+
const painted = set.layers.filter((l) => l.opaquePx > 0).length;
|
|
541
|
+
console.log(` ${set.layers.length} layer(s): ${painted} with opaque pixels, ${set.layers.length - painted} with none`);
|
|
542
|
+
// A reader refuses nothing on plausibility: it says what --propose-plan will leave out, and why.
|
|
543
|
+
let warned = 0;
|
|
544
|
+
for (const f of figures) {
|
|
545
|
+
for (const rule of implausibleRules(f)) {
|
|
546
|
+
console.log(` WARN ${rule}: layer "${f.name}" — ${figuresPhrase(f)}; ${ruleSummary(rule)} is required, so --propose-plan leaves it out`);
|
|
547
|
+
warned++;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
console.log(` ${warned} WARN line(s)`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Every flag each command takes: `valued` flags carry a value, `switches` are bare. Each parser reads its row to
|
|
555
|
+
* decide what it accepts, and {@link notTaken} prints the same row in the unknown-flag refusal, so the refusal
|
|
556
|
+
* lists exactly the flags the parser accepts (CL26). `layers` takes one positional path and no flag, so it has no row.
|
|
557
|
+
*/
|
|
558
|
+
const TAKES = {
|
|
559
|
+
sheet: { valued: ['--source', '--layers', '--out', '--cell', '--cols'], switches: [] },
|
|
560
|
+
rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
|
|
561
|
+
propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--canvas', '--keypoints', '--person', '--wrist-snap'], switches: ['--head-box'] },
|
|
562
|
+
compare: { valued: ['--left', '--right', '--map'], switches: [] },
|
|
563
|
+
check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements'], switches: ['--seam-pairs', '--keep-stills'] },
|
|
564
|
+
loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
|
|
565
|
+
assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project'], switches: ['--propose-plan'] },
|
|
566
|
+
'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll'], switches: ['--offload', '--lama', '--nf4'] },
|
|
567
|
+
'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
|
|
568
|
+
inputs: { valued: ['--source', '--config', '--out'], switches: [] },
|
|
569
|
+
build: { valued: ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape', '--requirements', '--idle-keys'], switches: ['--loop'] },
|
|
570
|
+
compose: { valued: ['--scene', '--out', '--requirements', '--accept'], switches: [] },
|
|
571
|
+
} as const satisfies Record<string, { valued: readonly string[]; switches: readonly string[] }>;
|
|
572
|
+
|
|
573
|
+
type Command = keyof typeof TAKES;
|
|
574
|
+
|
|
575
|
+
/** Whether `command` takes `flag` as a valued flag, read off its {@link TAKES} row. */
|
|
576
|
+
function takesValued(command: Command, flag: string): boolean {
|
|
577
|
+
return (TAKES[command].valued as readonly string[]).includes(flag);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** The unknown-flag refusal: the flag, and every flag the command's {@link TAKES} row holds, valued then bare. */
|
|
581
|
+
function notTaken(command: Command, flag: string): string {
|
|
582
|
+
const row = TAKES[command];
|
|
583
|
+
return `${command} does not take "${flag}"; it takes ${[...row.valued, ...row.switches].join(', ')}`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function cmdLayers(args: string[]): number {
|
|
587
|
+
if (args.length !== 1) return usage(`layers takes one path; got ${args.length}`);
|
|
588
|
+
try {
|
|
589
|
+
printLayerTable(readLayers(args[0]));
|
|
590
|
+
return EXIT_OK;
|
|
591
|
+
} catch (err) {
|
|
592
|
+
return printRefusal(err);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function cmdSheet(args: string[]): number {
|
|
597
|
+
const layers: string[] = [];
|
|
598
|
+
let source: string | null = null;
|
|
599
|
+
let out: string | null = null;
|
|
600
|
+
let cell = 220;
|
|
601
|
+
let cols = 8;
|
|
602
|
+
for (let i = 0; i < args.length; i++) {
|
|
603
|
+
const flag = args[i];
|
|
604
|
+
const value = args[i + 1];
|
|
605
|
+
if (!takesValued('sheet', flag)) return usage(notTaken('sheet', flag));
|
|
606
|
+
if (value === undefined) return usage(`${flag} needs a value`);
|
|
607
|
+
i++;
|
|
608
|
+
if (flag === '--source') source = value;
|
|
609
|
+
else if (flag === '--layers') layers.push(value);
|
|
610
|
+
else if (flag === '--out') out = value;
|
|
611
|
+
else {
|
|
612
|
+
const n = Number(value);
|
|
613
|
+
if (!Number.isInteger(n) || n < (flag === '--cell' ? 40 : 1)) {
|
|
614
|
+
return usage(`${flag} ${value} is not an integer of at least ${flag === '--cell' ? 40 : 1}`);
|
|
615
|
+
}
|
|
616
|
+
if (flag === '--cell') cell = n;
|
|
617
|
+
else cols = n;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (source === null) return usage('sheet needs --source <painting.png>');
|
|
621
|
+
if (layers.length === 0) return usage('sheet needs at least one --layers <path>');
|
|
622
|
+
if (out === null) return usage('sheet needs --out <sheet.png>');
|
|
623
|
+
if (extname(out).toLowerCase() !== '.png') return usage(`--out ${out} is not a .png; the sheet is written as PNG only`);
|
|
624
|
+
try {
|
|
625
|
+
if (!existsSync(source)) {
|
|
626
|
+
throw new PartsError([{ code: 'SHEET_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is the first tile' }]);
|
|
627
|
+
}
|
|
628
|
+
const src = readPng(source);
|
|
629
|
+
const tiles: Tile[] = [{ name: 'source', image: src, caption: basename(source) }];
|
|
630
|
+
for (const path of layers) tiles.push(...tilesFrom(path));
|
|
631
|
+
const sheet = buildSheet(tiles, cols, cell);
|
|
632
|
+
// The README's loop writes sheets/layers.png into a folder nothing made yet;
|
|
633
|
+
// every other command creates its --out, so this one does too.
|
|
634
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
635
|
+
writePng(out, sheet);
|
|
636
|
+
console.log(`rig-parts sheet: ${out}`);
|
|
637
|
+
console.log(` ${tiles.length} tile(s) in ${cols} column(s) of ${cell} px, sheet ${sheet.width}x${sheet.height}`);
|
|
638
|
+
tiles.forEach((t, i) => console.log(` tile ${String(i).padStart(3)} ${t.name} ${t.caption ?? defaultCaption(t.image)}`));
|
|
639
|
+
return EXIT_OK;
|
|
640
|
+
} catch (err) {
|
|
641
|
+
return printRefusal(err);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** `--page-edges`, or its default; a value rig-c does not take is a usage error naming the two it does. */
|
|
646
|
+
function pageEdgesOf(value: string | undefined): PageEdges | string {
|
|
647
|
+
const edges = value ?? DEFAULT_PAGE_EDGES;
|
|
648
|
+
return (PAGE_EDGES as readonly string[]).includes(edges) ? (edges as PageEdges) : `--page-edges ${edges}; one of ${PAGE_EDGES.join(', ')} is required`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function isPageEdges(v: PageEdges | string): v is PageEdges {
|
|
652
|
+
return (PAGE_EDGES as readonly string[]).includes(v);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** `--pack-shape`, or its default; a value rig-c does not take is a usage error naming the two it does. */
|
|
656
|
+
function packShapeOf(value: string | undefined): PackShape | string {
|
|
657
|
+
const shape = value ?? DEFAULT_PACK_SHAPE;
|
|
658
|
+
return (PACK_SHAPES as readonly string[]).includes(shape) ? (shape as PackShape) : `--pack-shape ${shape}; one of ${PACK_SHAPES.join(', ')} is required`;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function isPackShape(v: PackShape | string): v is PackShape {
|
|
662
|
+
return (PACK_SHAPES as readonly string[]).includes(v);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function cmdRig(args: string[]): number {
|
|
666
|
+
let config: string | null = null;
|
|
667
|
+
let partsDir: string | null = null;
|
|
668
|
+
let out: string | null = null;
|
|
669
|
+
let idleKeys: string | null = null;
|
|
670
|
+
let pageEdges: string | null = null;
|
|
671
|
+
let packShape: string | null = null;
|
|
672
|
+
for (let i = 0; i < args.length; i++) {
|
|
673
|
+
const flag = args[i];
|
|
674
|
+
const value = args[i + 1];
|
|
675
|
+
if (!takesValued('rig', flag)) return usage(notTaken('rig', flag));
|
|
676
|
+
if (value === undefined) return usage(`${flag} needs a value`);
|
|
677
|
+
i++;
|
|
678
|
+
if (flag === '--config') config = value;
|
|
679
|
+
else if (flag === '--parts') partsDir = value;
|
|
680
|
+
else if (flag === '--idle-keys') {
|
|
681
|
+
if (idleKeys !== null) return usage('--idle-keys is given twice');
|
|
682
|
+
idleKeys = value;
|
|
683
|
+
} else if (flag === '--page-edges') {
|
|
684
|
+
if (pageEdges !== null) return usage('--page-edges is given twice');
|
|
685
|
+
pageEdges = value;
|
|
686
|
+
} else if (flag === '--pack-shape') {
|
|
687
|
+
if (packShape !== null) return usage('--pack-shape is given twice');
|
|
688
|
+
packShape = value;
|
|
689
|
+
} else out = value;
|
|
690
|
+
}
|
|
691
|
+
if (config === null) return usage('rig needs --config <config.json>');
|
|
692
|
+
if (partsDir === null) return usage('rig needs --parts <dir> (the directory holding parts.json and parts/)');
|
|
693
|
+
if (out === null) return usage('rig needs --out <dir>');
|
|
694
|
+
const keys = idleKeysOf(idleKeys ?? undefined);
|
|
695
|
+
if (!isIdleKeys(keys)) return usage(keys);
|
|
696
|
+
const edges = pageEdgesOf(pageEdges ?? undefined);
|
|
697
|
+
if (!isPageEdges(edges)) return usage(edges);
|
|
698
|
+
const shape = packShapeOf(packShape ?? undefined);
|
|
699
|
+
if (!isPackShape(shape)) return usage(shape);
|
|
700
|
+
let bin: string;
|
|
701
|
+
try {
|
|
702
|
+
bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
703
|
+
} catch (err) {
|
|
704
|
+
return printRefusal(err);
|
|
705
|
+
}
|
|
706
|
+
const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-rig-'));
|
|
707
|
+
try {
|
|
708
|
+
rigStage({ config, parts: partsDir, out, idleKeys: keys, pageEdges: edges, packShape: shape }, rigcRunner(bin), scratch, console.log);
|
|
709
|
+
return EXIT_OK;
|
|
710
|
+
} catch (err) {
|
|
711
|
+
return printRefusal(err);
|
|
712
|
+
} finally {
|
|
713
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* The rigc process, for every stage that runs rigc — `rig`'s gate, `check`, and
|
|
719
|
+
* both inside `build`: the `rigc` binary {@link findRigc} located, spawned as
|
|
720
|
+
* it is, so rig-c's own launcher (`bin/rigc.cjs`) chooses the entry —
|
|
721
|
+
* `cli.ts` and the spine-core round trip where the runtime resolves beside it,
|
|
722
|
+
* `cli_core.ts` and rigc's own validator where it does not. No stage runs a
|
|
723
|
+
* rigc source file by path: under rig-c 2.0, `cli.ts` by path is the
|
|
724
|
+
* spine-core entry whether or not spine-core is there. `src/` is pure and takes
|
|
725
|
+
* the spawn from here.
|
|
726
|
+
*/
|
|
727
|
+
function rigcRunner(bin: string): RigcRunner {
|
|
728
|
+
return (args) => {
|
|
729
|
+
const r = spawnSync(bin, [...args], { encoding: 'utf8', maxBuffer: 1 << 28 });
|
|
730
|
+
if (r.error !== undefined) return { status: 127, out: `could not start ${bin}: ${r.error.message}` };
|
|
731
|
+
return { status: r.status ?? 1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** The command's valued flags as `--flag value` pairs, each one its {@link TAKES} row holds; `required` must all be given. Its bare switches are taken out by the caller before this reads the rest. A string is a usage error. */
|
|
736
|
+
function flags(args: string[], command: Command, required: readonly string[]): Map<string, string> | string {
|
|
737
|
+
const got = new Map<string, string>();
|
|
738
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
739
|
+
const flag = args[i];
|
|
740
|
+
if (!takesValued(command, flag)) return notTaken(command, flag);
|
|
741
|
+
if (args[i + 1] === undefined) return `${flag} needs a value`;
|
|
742
|
+
if (got.has(flag)) return `${flag} is given twice`;
|
|
743
|
+
got.set(flag, args[i + 1]);
|
|
744
|
+
}
|
|
745
|
+
for (const k of required) if (!got.has(k)) return `${command} needs ${k} <value>`;
|
|
746
|
+
return got;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function cmdCheck(args: string[]): number {
|
|
750
|
+
// The two switches check takes (issues #202, #209): they carry no value, so they are taken out before the valued flags are read.
|
|
751
|
+
const seamPairs = args.filter((a) => a === '--seam-pairs').length;
|
|
752
|
+
if (seamPairs > 1) return usage('--seam-pairs is given twice');
|
|
753
|
+
const keepStills = args.filter((a) => a === '--keep-stills').length;
|
|
754
|
+
if (keepStills > 1) return usage('--keep-stills is given twice');
|
|
755
|
+
const f = flags(
|
|
756
|
+
args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills'),
|
|
757
|
+
'check',
|
|
758
|
+
['--rig', '--out'],
|
|
759
|
+
);
|
|
760
|
+
if (typeof f === 'string') return usage(f);
|
|
761
|
+
const rig = f.get('--rig') as string;
|
|
762
|
+
const out = f.get('--out') as string;
|
|
763
|
+
const edges = pageEdgesOf(f.get('--page-edges'));
|
|
764
|
+
if (!isPageEdges(edges)) return usage(edges);
|
|
765
|
+
const shape = packShapeOf(f.get('--pack-shape'));
|
|
766
|
+
if (!isPackShape(shape)) return usage(shape);
|
|
767
|
+
try {
|
|
768
|
+
const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
769
|
+
const r = checkStage(
|
|
770
|
+
{ rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1 },
|
|
771
|
+
rigcRunner(bin),
|
|
772
|
+
bin,
|
|
773
|
+
console.log,
|
|
774
|
+
);
|
|
775
|
+
return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
|
|
776
|
+
} catch (err) {
|
|
777
|
+
return printRefusal(err);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function parseCanvas(v: string): { w: number; h: number } | null {
|
|
782
|
+
const m = /^(\d+)x(\d+)$/.exec(v);
|
|
783
|
+
return m === null ? null : { w: Number(m[1]), h: Number(m[2]) };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes: CharacterConfig['meshes']; regions: CharacterConfig['regions']; motion: CharacterConfig['motion']; constraints?: CharacterConfig['constraints'] }, joints?: RigJoints): number {
|
|
787
|
+
const res = lint(P, spec, joints);
|
|
788
|
+
// Under --keypoints only: which rule set read the torso and the chains, and on what basis.
|
|
789
|
+
if (res.basis !== undefined) for (const l of basisLines(res.basis)) console.log(l);
|
|
790
|
+
for (const f of res.findings) console.log(lintLine(f));
|
|
791
|
+
for (const m of res.unknownMeshes) console.log(`note: mesh ${JSON.stringify(m)} names no part in parts.json, so it was not linted`);
|
|
792
|
+
for (const b of res.missingTorsoBones) {
|
|
793
|
+
console.log(
|
|
794
|
+
res.basis?.torso.read === true
|
|
795
|
+
? `note: no single bone named ${JSON.stringify(b)}, so it was not linted against the torso joints`
|
|
796
|
+
: `note: no single bone named ${JSON.stringify(b)}, so the hip was not linted against the chest${b === 'hip' ? ' or the figure height' : ''}`,
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
console.log(`${res.findings.length} LINT line(s) over ${Object.keys(spec.meshes).length - res.unknownMeshes.length} mesh(es) and the hip`);
|
|
800
|
+
// Issue #86: after every line above, which check read each bone, or why none did.
|
|
801
|
+
for (const l of coverageLines(res.coverage)) console.log(l);
|
|
802
|
+
return res.findings.length;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function cmdPropose(args: string[]): number {
|
|
806
|
+
const flags = new Map<string, string>();
|
|
807
|
+
let headBox = false;
|
|
808
|
+
for (let i = 0; i < args.length; i++) {
|
|
809
|
+
const flag = args[i];
|
|
810
|
+
if (flag === '--head-box') {
|
|
811
|
+
headBox = true;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (!takesValued('propose', flag)) return usage(notTaken('propose', flag));
|
|
815
|
+
const value = args[i + 1];
|
|
816
|
+
if (value === undefined) return usage(`${flag} needs a value`);
|
|
817
|
+
if (flags.has(flag)) return usage(`${flag} is given twice`);
|
|
818
|
+
flags.set(flag, value);
|
|
819
|
+
i++;
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
if (headBox) {
|
|
823
|
+
const stray = [...flags.keys()].filter((f) => f !== '--full' && f !== '--canvas');
|
|
824
|
+
if (stray.length > 0) return usage(`--head-box takes --full and --canvas only; got ${stray.join(', ')}`);
|
|
825
|
+
const full = flags.get('--full');
|
|
826
|
+
const canvasArg = flags.get('--canvas');
|
|
827
|
+
if (full === undefined) return usage('--head-box needs --full <dir | layers.json | file.psd>, the full run\'s layers');
|
|
828
|
+
if (canvasArg === undefined) return usage('--head-box needs --canvas <W>x<H>, the painting\'s size in px');
|
|
829
|
+
const canvas = parseCanvas(canvasArg);
|
|
830
|
+
if (canvas === null) return usage(`--canvas ${canvasArg} is not <W>x<H> in integer px`);
|
|
831
|
+
const r = proposeHeadBox(readLayers(full), canvas);
|
|
832
|
+
console.log(`rig-parts propose --head-box: ${full}`);
|
|
833
|
+
console.log(` painting ${canvas.w}x${canvas.h}; head layers ${r.layers.map(([n, c]) => `${n} ${c} px`).join(', ')}`);
|
|
834
|
+
if (r.shift[0] !== 0 || r.shift[1] !== 0) {
|
|
835
|
+
console.log(` clamped: the proposed box [${r.unclamped.join(', ')}] leaves the ${canvas.w}x${canvas.h} painting; shifted by ${r.shift[0]},${r.shift[1]} px at the same size`);
|
|
836
|
+
} else console.log(' inside the painting, no shift');
|
|
837
|
+
console.log(` head_box [${r.head_box.join(', ')}], ${r.head_box[2] - r.head_box[0]}x${r.head_box[3] - r.head_box[1]}`);
|
|
838
|
+
console.log(JSON.stringify({ head_box: r.head_box }));
|
|
839
|
+
return EXIT_OK;
|
|
840
|
+
}
|
|
841
|
+
for (const f of ['--full', '--canvas']) if (flags.has(f)) return usage(`${f} belongs to --head-box`);
|
|
842
|
+
const partsArg = flags.get('--parts');
|
|
843
|
+
const source = flags.get('--source');
|
|
844
|
+
const out = flags.get('--out');
|
|
845
|
+
if (partsArg === undefined) return usage('propose needs --parts <dir>, the directory holding parts.json and parts/');
|
|
846
|
+
if (source === undefined) return usage('propose needs --source <painting.png>, drawn under the overlay');
|
|
847
|
+
if (out === undefined) return usage('propose needs --out <dir>');
|
|
848
|
+
if (flags.has('--from-config') && flags.has('--compare')) return usage('--from-config and --compare are two modes; give one');
|
|
849
|
+
if (flags.has('--person') && !flags.has('--keypoints')) return usage('--person names a person in the --keypoints file; give --keypoints <file>');
|
|
850
|
+
// Issue #199: a declared tolerance for a wrist on no handwear part. Whole px above the built-in one: the distance is
|
|
851
|
+
// counted in whole px, so a fraction would bind what its floor binds, and a value at or under the built-in binds nothing.
|
|
852
|
+
const snapArg = flags.get('--wrist-snap');
|
|
853
|
+
let wristSnap: number | null = null;
|
|
854
|
+
if (snapArg !== undefined) {
|
|
855
|
+
if (!flags.has('--keypoints')) return usage('--wrist-snap binds a wrist joint to a handwear part; give --keypoints <file>');
|
|
856
|
+
if (flags.has('--from-config')) return usage('--wrist-snap binds a wrist while proposing; --from-config proposes nothing');
|
|
857
|
+
if (!/^[0-9]+$/.test(snapArg) || Number(snapArg) <= WRIST_ON_PART_PX) {
|
|
858
|
+
return usage(`--wrist-snap ${snapArg} is not an integer above ${WRIST_ON_PART_PX}, the px within which a wrist already holds a part`);
|
|
859
|
+
}
|
|
860
|
+
wristSnap = Number(snapArg);
|
|
861
|
+
}
|
|
862
|
+
const partsDir = existsSync(partsArg) && statSync(partsArg).isFile() ? dirname(partsArg) : partsArg;
|
|
863
|
+
if (!existsSync(source)) {
|
|
864
|
+
throw new PartsError([{ code: 'PROPOSE_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is drawn under the overlay' }]);
|
|
865
|
+
}
|
|
866
|
+
const P = readPartSet(partsDir);
|
|
867
|
+
const painting = readPng(source);
|
|
868
|
+
// Issue #75: the keypoint file, held to this painting's size and taken to rig px by the overlay's map.
|
|
869
|
+
const kpPath = flags.get('--keypoints');
|
|
870
|
+
let joints: RigJoints | undefined;
|
|
871
|
+
if (kpPath !== undefined) {
|
|
872
|
+
const kp = loadKeypoints(kpPath);
|
|
873
|
+
checkImageSize(kp, painting.width, painting.height, kpPath, source);
|
|
874
|
+
joints = toRigJoints(kp, choosePerson(kp, flags.get('--person'), kpPath), P.W, P.H);
|
|
875
|
+
}
|
|
876
|
+
const render = join(out, 'render');
|
|
877
|
+
const fromConfig = flags.get('--from-config');
|
|
878
|
+
if (fromConfig !== undefined) {
|
|
879
|
+
const cfg = loadConfig(fromConfig);
|
|
880
|
+
const img = drawLandmarks(P, painting, cfg.bones, 'config bones');
|
|
881
|
+
mkdirSync(render, { recursive: true });
|
|
882
|
+
writePng(join(render, 'landmarks_config.png'), img.full);
|
|
883
|
+
if (img.head !== null) writePng(join(render, 'landmarks_config_head.png'), img.head);
|
|
884
|
+
console.log(`wrote ${join(render, 'landmarks_config.png')}${img.head !== null ? ' (+_head)' : ' (no bone named "head", so no head crop)'}`);
|
|
885
|
+
return printLint(P, cfg, joints) > 0 ? EXIT_REFUSED : EXIT_OK;
|
|
886
|
+
}
|
|
887
|
+
const cmpPath = flags.get('--compare');
|
|
888
|
+
const cfg = cmpPath === undefined ? null : loadConfig(cmpPath);
|
|
889
|
+
const { proposal: prop, basis } = proposeWithBasis(P, joints, wristSnap);
|
|
890
|
+
// Emit only after green: a proposal the config loader would refuse is not written, nor a basis record that does not hold to it.
|
|
891
|
+
checkProposal(P, prop);
|
|
892
|
+
const bfile = basisFile(P, prop, basis, cfg === null || cmpPath === undefined ? null : { where: cmpPath, config: cfg });
|
|
893
|
+
const img = drawLandmarks(P, painting, prop.bones, 'PROPOSAL - CORRECT ME');
|
|
894
|
+
mkdirSync(render, { recursive: true });
|
|
895
|
+
writeFileSync(join(out, 'proposal.json'), serializeProposal(prop));
|
|
896
|
+
writeFileSync(join(out, BASIS_FILE), serializeBasis(bfile));
|
|
897
|
+
writePng(join(render, 'landmarks.png'), img.full);
|
|
898
|
+
if (img.head !== null) writePng(join(render, 'landmarks_head.png'), img.head);
|
|
899
|
+
console.log(`wrote proposal.json (${prop.bones.length} bone entries, ${Object.keys(prop.meshes).length} meshes) and render/landmarks.png (+_head) under ${out}`);
|
|
900
|
+
for (const n of prop.notes) console.log(`note: ${n}`);
|
|
901
|
+
printLint(P, prop, joints);
|
|
902
|
+
console.log(basisLine(bfile, out));
|
|
903
|
+
if (cfg !== null) for (const l of compareLines(compare(prop.bones, cfg.bones))) console.log(l);
|
|
904
|
+
return EXIT_OK;
|
|
905
|
+
} catch (err) {
|
|
906
|
+
return printRefusal(err);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function cmdCompare(args: string[]): number {
|
|
911
|
+
const flags = new Map<string, string>();
|
|
912
|
+
for (let i = 0; i < args.length; i++) {
|
|
913
|
+
const flag = args[i];
|
|
914
|
+
if (!takesValued('compare', flag)) return usage(notTaken('compare', flag));
|
|
915
|
+
const value = args[i + 1];
|
|
916
|
+
if (value === undefined) return usage(`${flag} needs a value`);
|
|
917
|
+
if (flags.has(flag)) return usage(`${flag} is given twice`);
|
|
918
|
+
flags.set(flag, value);
|
|
919
|
+
i++;
|
|
920
|
+
}
|
|
921
|
+
const left = flags.get('--left');
|
|
922
|
+
const right = flags.get('--right');
|
|
923
|
+
if (left === undefined) return usage('compare needs --left <config.json | proposal.json | rig.json>');
|
|
924
|
+
if (right === undefined) return usage('compare needs --right <config.json | proposal.json | rig.json>');
|
|
925
|
+
const map = flags.get('--map') ?? null;
|
|
926
|
+
try {
|
|
927
|
+
const c = loadComparison(left, right, map);
|
|
928
|
+
console.log(`rig-parts compare: left ${left}, right ${right}${map === null ? ', no map' : `, map ${map}`}`);
|
|
929
|
+
for (const l of structureLines(c)) console.log(l);
|
|
930
|
+
const missing = requiredProblems(c, map ?? 'the map');
|
|
931
|
+
for (const p of missing) console.log(` FAIL ${problemLine(p)}`);
|
|
932
|
+
return missing.length > 0 ? EXIT_REFUSED : EXIT_OK;
|
|
933
|
+
} catch (err) {
|
|
934
|
+
return printRefusal(err);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function cmdLoop(args: string[]): number {
|
|
939
|
+
const palettes = args.filter((a) => a === '--palette').length;
|
|
940
|
+
if (palettes > 1) return usage('--palette is given twice');
|
|
941
|
+
const f = flags(
|
|
942
|
+
args.filter((a) => a !== '--palette'),
|
|
943
|
+
'loop',
|
|
944
|
+
['--frames', '--out'],
|
|
945
|
+
);
|
|
946
|
+
if (typeof f === 'string') return usage(f);
|
|
947
|
+
const out = f.get('--out') as string;
|
|
948
|
+
const ext = extname(out).toLowerCase();
|
|
949
|
+
if (ext === '.webp') return usage(`--out ${out}: animated WebP needs a VP8/VP8L encoder, which is out of scope; write .png (APNG) or .gif`);
|
|
950
|
+
if (ext !== '.png' && ext !== '.gif') return usage(`--out ${out} is neither .png (APNG) nor .gif`);
|
|
951
|
+
if (palettes === 1 && ext !== '.png') return usage(`--palette selects the indexed APNG, so --out must be a .png; ${out} is a GIF, which is always one palette`);
|
|
952
|
+
try {
|
|
953
|
+
loopStage(f.get('--frames') as string, out, ext === '.gif' ? 'gif' : palettes === 1 ? 'indexed' : 'apng', console.log);
|
|
954
|
+
return EXIT_OK;
|
|
955
|
+
} catch (err) {
|
|
956
|
+
return printRefusal(err);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function cmdAssemble(args: string[]): number {
|
|
961
|
+
const flags = new Map<string, string>();
|
|
962
|
+
let propose = false;
|
|
963
|
+
for (let i = 0; i < args.length; i++) {
|
|
964
|
+
const flag = args[i];
|
|
965
|
+
if (flag === '--propose-plan') {
|
|
966
|
+
propose = true;
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (!takesValued('assemble', flag)) return usage(notTaken('assemble', flag));
|
|
970
|
+
const value = args[i + 1];
|
|
971
|
+
if (value === undefined) return usage(`${flag} needs a value`);
|
|
972
|
+
if (flags.has(flag)) return usage(`${flag} is given twice`);
|
|
973
|
+
flags.set(flag, value);
|
|
974
|
+
i++;
|
|
975
|
+
}
|
|
976
|
+
for (const f of ['--source', '--full', '--head', '--config']) if (!flags.has(f)) return usage(`assemble needs ${f}`);
|
|
977
|
+
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');
|
|
978
|
+
if (!propose && !flags.has('--out')) return usage('assemble needs --out <dir>');
|
|
979
|
+
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
980
|
+
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
981
|
+
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
982
|
+
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
983
|
+
const [source, full, head, config] = ['--source', '--full', '--head', '--config'].map((f) => flags.get(f) as string);
|
|
984
|
+
try {
|
|
985
|
+
if (propose) {
|
|
986
|
+
const src = readSource(source);
|
|
987
|
+
const runs = readRuns(full, head);
|
|
988
|
+
const g = proposeFields(loadEarlyConfig(config, 'layers'));
|
|
989
|
+
const proposal = proposePlan(runs.full, runs.head, { sourceW: src.width, sourceH: src.height, ...g });
|
|
990
|
+
console.log(JSON.stringify(proposal, null, 2));
|
|
991
|
+
return EXIT_OK;
|
|
992
|
+
}
|
|
993
|
+
const out = flags.get('--out') as string;
|
|
994
|
+
assembleStage(
|
|
995
|
+
{ source, full, head, config, seam: seam as SeamRule, project: project as ProjectRule },
|
|
996
|
+
{ partsJson: join(out, 'rig', 'parts.json'), partsDir: join(out, 'rig', 'parts'), recomposite: join(out, 'render', 'recomposite_rig.png'), errorMap: join(out, 'render', ERROR_MAP_FILE) },
|
|
997
|
+
console.log,
|
|
998
|
+
);
|
|
999
|
+
return EXIT_OK;
|
|
1000
|
+
} catch (err) {
|
|
1001
|
+
return printRefusal(err);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/** Parse `--flag value` pairs and bare `--switch`es, as the command's {@link TAKES} row names them; a string is a usage error. */
|
|
1006
|
+
function comfyFlags(args: string[], command: 'comfy seethrough' | 'comfy paint'): { v: Map<string, string>; on: Set<string> } | string {
|
|
1007
|
+
const switches: readonly string[] = TAKES[command].switches;
|
|
1008
|
+
const v = new Map<string, string>();
|
|
1009
|
+
const on = new Set<string>();
|
|
1010
|
+
for (let i = 0; i < args.length; i++) {
|
|
1011
|
+
const flag = args[i];
|
|
1012
|
+
if (switches.includes(flag)) {
|
|
1013
|
+
if (on.has(flag)) return `${flag} is given twice`;
|
|
1014
|
+
on.add(flag);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (!takesValued(command, flag)) return notTaken(command, flag);
|
|
1018
|
+
const value = args[i + 1];
|
|
1019
|
+
if (value === undefined) return `${flag} needs a value`;
|
|
1020
|
+
if (v.has(flag)) return `${flag} is given twice`;
|
|
1021
|
+
v.set(flag, value);
|
|
1022
|
+
i++;
|
|
1023
|
+
}
|
|
1024
|
+
return { v, on };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/** An integer flag at or above `min`, or its stated default; a string is a usage error. */
|
|
1028
|
+
function intFlag(v: Map<string, string>, flag: string, fallback: number, min: number): number | string {
|
|
1029
|
+
const raw = v.get(flag);
|
|
1030
|
+
if (raw === undefined) return fallback;
|
|
1031
|
+
const n = Number(raw);
|
|
1032
|
+
return Number.isInteger(n) && n >= min ? n : `${flag} ${raw} is not an integer of at least ${min}`;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** A seconds flag above 0, or its stated default. */
|
|
1036
|
+
function secondsFlag(v: Map<string, string>, flag: string, fallback: number): number | string {
|
|
1037
|
+
const raw = v.get(flag);
|
|
1038
|
+
if (raw === undefined) return fallback;
|
|
1039
|
+
const n = Number(raw);
|
|
1040
|
+
return Number.isFinite(n) && n > 0 ? n : `${flag} ${raw} is not a number of seconds above 0`;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function cmdComfy(args: string[]): Promise<number> {
|
|
1044
|
+
const [sub, ...rest] = args;
|
|
1045
|
+
if (sub !== 'seethrough' && sub !== 'paint') return usage(`comfy takes seethrough or paint; got ${sub === undefined ? 'nothing' : `"${sub}"`}`);
|
|
1046
|
+
const f = comfyFlags(rest, `comfy ${sub}`);
|
|
1047
|
+
if (typeof f === 'string') return usage(f);
|
|
1048
|
+
const { v, on } = f;
|
|
1049
|
+
const wait = secondsFlag(v, '--wait', 1800);
|
|
1050
|
+
const timeout = secondsFlag(v, '--timeout', 3600);
|
|
1051
|
+
const poll = secondsFlag(v, '--poll', 3);
|
|
1052
|
+
for (const x of [wait, timeout, poll]) if (typeof x === 'string') return usage(x);
|
|
1053
|
+
const out = v.get('--out');
|
|
1054
|
+
if (out === undefined) return usage(`comfy ${sub} needs --out <dir>`);
|
|
1055
|
+
const configPath = v.get('--config');
|
|
1056
|
+
if (sub === 'paint' && configPath === undefined) return usage('comfy paint needs --config <config.json>');
|
|
1057
|
+
try {
|
|
1058
|
+
// The config is a local file, so it is answered before any host is: a
|
|
1059
|
+
// config comfy paint cannot paint from is refused with no box named.
|
|
1060
|
+
const paint = sub === 'paint' ? loadPaintConfig(configPath as string) : null;
|
|
1061
|
+
const cfg = paint === null ? null : paint.config;
|
|
1062
|
+
const host = resolveHost(v.get('--host'), process.env.COMFY_HOST);
|
|
1063
|
+
const client = new ComfyClient(host, { poll: poll as number, request: 30 });
|
|
1064
|
+
if (sub === 'seethrough') {
|
|
1065
|
+
const image = v.get('--image');
|
|
1066
|
+
if (image === undefined) return usage('comfy seethrough needs --image <png>');
|
|
1067
|
+
const resolution = intFlag(v, '--resolution', 1024, 64);
|
|
1068
|
+
const steps = intFlag(v, '--steps', 30, 1);
|
|
1069
|
+
const seed = intFlag(v, '--seed', 42, 0);
|
|
1070
|
+
for (const x of [resolution, steps, seed]) if (typeof x === 'string') return usage(x);
|
|
1071
|
+
const prefix = v.get('--prefix') ?? 'spine_parts';
|
|
1072
|
+
if (!/^[A-Za-z0-9_-]+$/.test(prefix)) return usage(`--prefix ${prefix} is not letters, digits, "_" and "-"`);
|
|
1073
|
+
const run = {
|
|
1074
|
+
image,
|
|
1075
|
+
out,
|
|
1076
|
+
prefix,
|
|
1077
|
+
resolution: resolution as number,
|
|
1078
|
+
steps: steps as number,
|
|
1079
|
+
seed: seed as number,
|
|
1080
|
+
offload: on.has('--offload'),
|
|
1081
|
+
lama: on.has('--lama'),
|
|
1082
|
+
quant: on.has('--nf4') ? ('nf4' as const) : ('none' as const),
|
|
1083
|
+
wait: wait as number,
|
|
1084
|
+
timeout: timeout as number,
|
|
1085
|
+
};
|
|
1086
|
+
console.log(`rig-parts comfy seethrough: ${image} -> ${out}`);
|
|
1087
|
+
console.log(` resolution ${run.resolution}, steps ${run.steps}, seed ${run.seed}, offload ${run.offload}, lama ${run.lama}, quant ${run.quant}; wait <= ${run.wait} s, timeout ${run.timeout} s`);
|
|
1088
|
+
const r = await runSeeThrough(client, run, (l) => console.log(l));
|
|
1089
|
+
console.log(` ${r.layers.length} layer(s) on a ${r.canvas[0]}x${r.canvas[1]} canvas, read back green by the layer reader: ${r.layers.join(', ')}`);
|
|
1090
|
+
console.log(` wrote ${join(out, 'layers.json')}, meta.json, parts/ (${r.layers.length} PNG) and previews/ (${r.previews.length} PNG); GPU job ended`);
|
|
1091
|
+
return EXIT_OK;
|
|
1092
|
+
}
|
|
1093
|
+
if (cfg === null) return usage('comfy paint needs --config <config.json>');
|
|
1094
|
+
const seeds = intFlag(v, '--seeds', 1, 1);
|
|
1095
|
+
if (typeof seeds === 'string') return usage(seeds);
|
|
1096
|
+
const seed0 = intFlag(v, '--seed0', cfg.generation.seed, 0);
|
|
1097
|
+
if (typeof seed0 === 'string') return usage(seed0);
|
|
1098
|
+
console.log(`rig-parts comfy paint: ${cfg.key} -> ${out}`);
|
|
1099
|
+
console.log(` ${seeds} seed(s) from ${seed0}${v.has('--seed0') ? '' : ' (generation.seed)'}; wait <= ${wait} s per seed, timeout ${timeout} s`);
|
|
1100
|
+
const done = await runPainting(client, { config: cfg, pose: paint === null ? null : paint.pose, out, seeds, seed0, wait: wait as number, timeout: timeout as number }, (l) => console.log(l));
|
|
1101
|
+
console.log(` wrote ${done.length} painting(s): ${done.map((d) => `${d.file} ${d.size[0]}x${d.size[1]} in ${d.elapsed.toFixed(1)} s`).join(', ')}; GPU job(s) ended`);
|
|
1102
|
+
return EXIT_OK;
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
return printRefusal(err);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function cmdInputs(args: string[]): number {
|
|
1109
|
+
const f = flags(args, 'inputs', ['--source', '--config', '--out']);
|
|
1110
|
+
if (typeof f === 'string') return usage(f);
|
|
1111
|
+
const source = f.get('--source') as string;
|
|
1112
|
+
const out = f.get('--out') as string;
|
|
1113
|
+
try {
|
|
1114
|
+
if (!existsSync(source)) throw new PartsError([{ code: 'INPUTS_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is required' }]);
|
|
1115
|
+
const cfg = loadEarlyConfig(f.get('--config') as string, 'layers');
|
|
1116
|
+
const painting = readPng(source);
|
|
1117
|
+
const r = makeInputs(painting, cfg, source);
|
|
1118
|
+
mkdirSync(out, { recursive: true });
|
|
1119
|
+
writePng(join(out, 'st_input_full.png'), r.full);
|
|
1120
|
+
console.log(`rig-parts inputs: ${source} (${painting.width}x${painting.height}) -> ${out}`);
|
|
1121
|
+
console.log(
|
|
1122
|
+
r.padTop > 0
|
|
1123
|
+
? ` st_input_full.png ${r.full.width}x${r.full.height}: the painting at y ${r.padTop}, white above and below (${r.padTop} + ${r.full.height - painting.height - r.padTop} px)`
|
|
1124
|
+
: ` st_input_full.png ${r.full.width}x${r.full.height}: the painting at x ${r.padLeft}, white either side (${r.padLeft} + ${r.full.width - painting.width - r.padLeft} px)`,
|
|
1125
|
+
);
|
|
1126
|
+
if (r.head !== null && r.headBox !== null) {
|
|
1127
|
+
writePng(join(out, 'st_input_head.png'), r.head);
|
|
1128
|
+
console.log(` st_input_head.png ${r.head.width}x${r.head.height}: seethrough.head_box [${r.headBox.join(', ')}]`);
|
|
1129
|
+
} else {
|
|
1130
|
+
console.log(' no seethrough.head_box in the config, so no st_input_head.png: run the full See-through pass, then `propose --head-box`, then this again');
|
|
1131
|
+
}
|
|
1132
|
+
return EXIT_OK;
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
return printRefusal(err);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function cmdBuild(args: string[]): number {
|
|
1139
|
+
const flags = new Map<string, string>();
|
|
1140
|
+
let loop = false;
|
|
1141
|
+
for (let i = 0; i < args.length; i++) {
|
|
1142
|
+
const flag = args[i];
|
|
1143
|
+
if (flag === '--loop') {
|
|
1144
|
+
if (loop) return usage('--loop is given twice');
|
|
1145
|
+
loop = true;
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (!takesValued('build', flag)) return usage(notTaken('build', flag));
|
|
1149
|
+
const value = args[i + 1];
|
|
1150
|
+
if (value === undefined) return usage(`${flag} needs a value`);
|
|
1151
|
+
if (flags.has(flag)) return usage(`${flag} is given twice`);
|
|
1152
|
+
flags.set(flag, value);
|
|
1153
|
+
i++;
|
|
1154
|
+
}
|
|
1155
|
+
for (const f of ['--config', '--source', '--full', '--head', '--out']) if (!flags.has(f)) return usage(`build needs ${f}`);
|
|
1156
|
+
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
1157
|
+
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
1158
|
+
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
1159
|
+
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
1160
|
+
const edges = pageEdgesOf(flags.get('--page-edges'));
|
|
1161
|
+
if (!isPageEdges(edges)) return usage(edges);
|
|
1162
|
+
const shape = packShapeOf(flags.get('--pack-shape'));
|
|
1163
|
+
if (!isPackShape(shape)) return usage(shape);
|
|
1164
|
+
const keys = idleKeysOf(flags.get('--idle-keys'));
|
|
1165
|
+
if (!isIdleKeys(keys)) return usage(keys);
|
|
1166
|
+
const [config, source, full, head, out] = ['--config', '--source', '--full', '--head', '--out'].map((f) => flags.get(f) as string);
|
|
1167
|
+
let bin: string;
|
|
1168
|
+
try {
|
|
1169
|
+
bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
return printRefusal(err);
|
|
1172
|
+
}
|
|
1173
|
+
const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-build-'));
|
|
1174
|
+
try {
|
|
1175
|
+
const r = build(
|
|
1176
|
+
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges, packShape: shape, ...(flags.has('--requirements') ? { requirements: flags.get('--requirements') as string } : {}), idleKeys: keys },
|
|
1177
|
+
{ rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
|
|
1178
|
+
console.log,
|
|
1179
|
+
);
|
|
1180
|
+
return r.stoppedAt === null ? EXIT_OK : EXIT_REFUSED;
|
|
1181
|
+
} finally {
|
|
1182
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function cmdCompose(args: string[]): number {
|
|
1187
|
+
const f = flags(args, 'compose', ['--scene', '--out']);
|
|
1188
|
+
if (typeof f === 'string') return usage(f);
|
|
1189
|
+
let bin: string;
|
|
1190
|
+
try {
|
|
1191
|
+
bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
return printRefusal(err);
|
|
1194
|
+
}
|
|
1195
|
+
const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-compose-'));
|
|
1196
|
+
try {
|
|
1197
|
+
const req = f.get('--requirements');
|
|
1198
|
+
const accept = f.get('--accept');
|
|
1199
|
+
const r = composeStage(
|
|
1200
|
+
{ scene: f.get('--scene') as string, out: f.get('--out') as string, ...(req === undefined ? {} : { requirements: req }), ...(accept === undefined ? {} : { accept: accept.split(',') }) },
|
|
1201
|
+
{ rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'gate') },
|
|
1202
|
+
console.log,
|
|
1203
|
+
);
|
|
1204
|
+
return r.stoppedAt === null ? EXIT_OK : EXIT_REFUSED;
|
|
1205
|
+
} finally {
|
|
1206
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function main(argv: string[]): number | Promise<number> {
|
|
1211
|
+
const [command, ...rest] = argv;
|
|
1212
|
+
if (command === undefined || command === '--help' || command === '-h' || command === 'help') {
|
|
1213
|
+
console.log(HELP);
|
|
1214
|
+
return command === undefined ? EXIT_USAGE : EXIT_OK;
|
|
1215
|
+
}
|
|
1216
|
+
if (command === '--version' || command === '-v') {
|
|
1217
|
+
console.log(version());
|
|
1218
|
+
return EXIT_OK;
|
|
1219
|
+
}
|
|
1220
|
+
if (command === 'layers') return cmdLayers(rest);
|
|
1221
|
+
if (command === 'sheet') return cmdSheet(rest);
|
|
1222
|
+
if (command === 'rig') return cmdRig(rest);
|
|
1223
|
+
if (command === 'propose') return cmdPropose(rest);
|
|
1224
|
+
if (command === 'compare') return cmdCompare(rest);
|
|
1225
|
+
if (command === 'check') return cmdCheck(rest);
|
|
1226
|
+
if (command === 'loop') return cmdLoop(rest);
|
|
1227
|
+
if (command === 'assemble') return cmdAssemble(rest);
|
|
1228
|
+
if (command === 'inputs') return cmdInputs(rest);
|
|
1229
|
+
if (command === 'comfy') return cmdComfy(rest);
|
|
1230
|
+
if (command === 'build') return cmdBuild(rest);
|
|
1231
|
+
if (command === 'compose') return cmdCompose(rest);
|
|
1232
|
+
const later = LATER.find(([name]) => name === command);
|
|
1233
|
+
if (later !== undefined) {
|
|
1234
|
+
console.log(` FAIL NOT_IMPLEMENTED: \`rig-parts ${command}\` (${later[1]}) is not implemented in this version, ${version()}`);
|
|
1235
|
+
return EXIT_USAGE;
|
|
1236
|
+
}
|
|
1237
|
+
return usage(`unknown command "${command}"`);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
process.exit(await main(process.argv.slice(2)));
|