rig-parts 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cli.ts +71 -21
- package/docs/AUTHORING.md +179 -26
- package/docs/STABILITY.md +8 -6
- package/package.json +1 -1
- package/src/assemble.ts +130 -8
- package/src/automotion.ts +119 -8
- package/src/build.ts +115 -17
- package/src/check.ts +371 -29
- package/src/comfy/painting.ts +25 -10
- package/src/config.ts +218 -12
- package/src/graphs.ts +5 -4
- package/src/instruments.ts +6 -0
- package/src/propose.ts +94 -13
- package/src/rig.ts +14 -1
- package/src/scene.ts +338 -30
- package/src/skeleton.ts +22 -8
package/README.md
CHANGED
|
@@ -288,7 +288,7 @@ agent skill.
|
|
|
288
288
|
| `check --rig --out [--parts] [--source] [--requirements]` | build packed, gated under `spine-html`, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json`; on a rig spec with no `parts.json` or no `idle` (a merged rig, issue #77) the gate still runs and every line that needs the missing input says SKIP by name; `--source <painting.png>` adds `SETUP_POSE_VS_SOURCE`, the setup pose against the painting by assemble's recomposite figures; `--requirements <scene.json>` measures what a scene declares of the motion — a contact, a follow fraction, an aim, a joint range, a mesh's stretch, the seam between two parts over the motion — each against the author's own bar, from rig-c's posed frames, one line each, PASS, FAIL or NOT MEASURABLE (AUTHORING §7, *Declared requirements*) |
|
|
289
289
|
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
290
290
|
| `build --config --source --full --head --out [--seam] [--project] [--loop] [--requirements] [--idle-keys ctl\|direct]` | assemble, rig and check in one process, stopping at the first refusal; `--requirements` is forwarded to check, `--idle-keys` to rig |
|
|
291
|
-
| `compose --scene <scene.json> --out [--requirements]` | bind several characters' green builds into one rig: every name prefixed `<id>:` under one shared root, each character's offset added at its first-level bones, an optional background plate (its provenance recorded, judged by nothing) drawn first, and the draw order between the characters' slots as the scene file declares it — nothing inferred, nothing scaled; gated through rig-c as `rig` is, then checked ([AUTHORING §7](docs/AUTHORING.md), *Composing several characters*) |
|
|
291
|
+
| `compose --scene <scene.json> --out [--requirements] [--accept]` | bind several characters' green builds into one rig (or, under `--accept`, builds failing only the bars named — for looking, not shipping): every name prefixed `<id>:` under one shared root, each character's offset added at its first-level bones, an optional background plate (its provenance recorded, judged by nothing) drawn first, and the draw order between the characters' slots as the scene file declares it — nothing inferred, nothing scaled; gated through rig-c as `rig` is, then checked ([AUTHORING §7](docs/AUTHORING.md), *Composing several characters*) |
|
|
292
292
|
|
|
293
293
|
`rig-parts --help` has every flag. Exit codes: 0 done, 1 input refused (every
|
|
294
294
|
reason is a FAIL line), 2 a usage error or a command this version does not implement.
|
package/cli.ts
CHANGED
|
@@ -17,16 +17,16 @@ import { tmpdir } from 'node:os';
|
|
|
17
17
|
import { basename, dirname, extname, join } from 'node:path';
|
|
18
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
19
|
import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
20
|
-
import { 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';
|
|
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
21
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
22
|
-
import { type CharacterConfig, loadConfig, loadEarlyConfig } from './src/config.ts';
|
|
22
|
+
import { type CharacterConfig, loadConfig, loadEarlyConfig, loadPaintConfig } from './src/config.ts';
|
|
23
23
|
import { PartsError, problemLine } from './src/errors.ts';
|
|
24
24
|
import { proposeHeadBox } from './src/headbox.ts';
|
|
25
25
|
import { makeInputs } from './src/inputs.ts';
|
|
26
26
|
import { idleKeysOf, isIdleKeys } from './src/rig.ts';
|
|
27
27
|
import { figuresPhrase, implausibleRules, layerFigures, type LayerSet, pct, readLayers, ruleSummary, times } from './src/layers.ts';
|
|
28
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 } from './src/propose.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
30
|
import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, serializeBasis } from './src/diagnostics.ts';
|
|
31
31
|
import { readPng, writePng } from './src/raster/png.ts';
|
|
32
32
|
import { REQUIREMENTS_SENTENCE } from './src/requirements.ts';
|
|
@@ -108,7 +108,7 @@ usage:
|
|
|
108
108
|
bone differs (origin, tip, parent; how far, in px). No confidence is
|
|
109
109
|
computed. It prints "wrote ${BASIS_FILE} (…)" after the coverage lines.
|
|
110
110
|
|
|
111
|
-
rig-parts propose … [--keypoints <keypoints.json> [--person <id>]]
|
|
111
|
+
rig-parts propose … [--keypoints <keypoints.json> [--person <id>] [--wrist-snap <px>]]
|
|
112
112
|
Read the figure's pose from one explicit file (spec "${KEYPOINTS_SPEC}"):
|
|
113
113
|
its space (painting-px, origin top-left, y down — another is refused),
|
|
114
114
|
the image's width and height (held to --source's, KEYPOINTS_IMAGE_SIZE),
|
|
@@ -124,7 +124,11 @@ usage:
|
|
|
124
124
|
the rule. One note per joint says which. LINT then reads the torso along
|
|
125
125
|
the neck -> hips line and each sleeve chain along shoulder -> wrist
|
|
126
126
|
instead of by screen y, and prints the rule set it used first. Without
|
|
127
|
-
--keypoints nothing changes.
|
|
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.
|
|
128
132
|
|
|
129
133
|
rig-parts propose --parts <dir> --source <painting.png> --out <dir> --from-config <config.json>
|
|
130
134
|
Draw the config's CURRENT bones instead (<out>/render/landmarks_config.png
|
|
@@ -205,7 +209,7 @@ usage:
|
|
|
205
209
|
rounding, and so is the per-frame mesh work (AUTHORING §5).
|
|
206
210
|
rig-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>]
|
|
207
211
|
[--page-edges pot|free] [--pack-shape rect|polygon]
|
|
208
|
-
[--requirements <file.json>]
|
|
212
|
+
[--requirements <file.json>] [--seam-pairs] [--keep-stills]
|
|
209
213
|
Build, gate, render and measure a rig through rig-c's CLI (the rigc at
|
|
210
214
|
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
|
|
211
215
|
a rig spec, which rigc build compiles, not a compiled skeleton.json.
|
|
@@ -222,7 +226,13 @@ usage:
|
|
|
222
226
|
entry that gated the build, and pack_mode, the two pack flags). PASS needs every gate summary
|
|
223
227
|
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
224
228
|
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
|
|
225
|
-
frame at t = duration) at max |d| 0.
|
|
229
|
+
frame at t = duration) at max |d| 0. A failing seam, or any seam under
|
|
230
|
+
--seam-pairs, is split by the pair of parts each counted pixel lies
|
|
231
|
+
between: one "seam pair" line each, worst first, with its count, frame
|
|
232
|
+
and rig boxes and a picture under seam/, and check.json's seam_pairs
|
|
233
|
+
(AUTHORING §7). --keep-stills leaves the setup-pose still the seam reads,
|
|
234
|
+
rigc's render directory with its frames.json (viewport and background),
|
|
235
|
+
under setup_still/. Then six judgement lines, each in
|
|
226
236
|
check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
|
|
227
237
|
bars (AUTHORING §7): BREATH_VISIBLE (the topwear moves, the footwear does
|
|
228
238
|
not, each rendered alone), BLINK_NO_HOLE (the setup pose with the blink
|
|
@@ -421,13 +431,18 @@ usage:
|
|
|
421
431
|
rigc refuses the pair a control splits, and the RIG_RIGC_GREEN line
|
|
422
432
|
names the flag on the command that ran.
|
|
423
433
|
|
|
424
|
-
rig-parts compose --scene <scene.json> --out <dir> [--requirements <file.json>]
|
|
434
|
+
rig-parts compose --scene <scene.json> --out <dir> [--requirements <file.json>] [--accept <bar>[,<bar>...]]
|
|
425
435
|
Bind several finished characters into one rig. The scene file (spec
|
|
426
436
|
"${SCENE_SPEC}") states the canvas {width, height}, an optional plate
|
|
427
437
|
{image, provenance: ${PROVENANCES.join(' | ')}, note?}, the characters
|
|
428
|
-
[{id, build, offset: [x, y]}] — each build the --out of a green
|
|
438
|
+
[{id, build, offset: [x, y], scale?}] — each build the --out of a green
|
|
429
439
|
\`rig-parts build\` (its check/check.json PASS), each offset a
|
|
430
|
-
translation from its rig px to canvas px
|
|
440
|
+
translation from its rig px to canvas px, each scale (a positive
|
|
441
|
+
number, 1 when absent) the character's size about its stage's top-left
|
|
442
|
+
corner: every length it carries is scaled as Spine's loader scales a
|
|
443
|
+
skeleton (bones, regions, weight binds, constraint lengths, translate
|
|
444
|
+
keys), and a physics wind or gravity, which no per-character value
|
|
445
|
+
can scale, is refused (SCENE_SCALE_EXACT) — and order: the draw order,
|
|
431
446
|
back to front, of character ids (that character's remaining slots, in
|
|
432
447
|
its own order) and "<id>:<slot>" entries, every slot exactly once.
|
|
433
448
|
Paths are relative to the scene file. Every bone, slot, attachment,
|
|
@@ -453,7 +468,7 @@ usage:
|
|
|
453
468
|
rig-c as rig gates its own (build --profile spine-html --pack, the
|
|
454
469
|
compile and the packed pages on disk); --out receives rig/ (rig.json,
|
|
455
470
|
motion.json, images/) and ${SCENE_REPORT_FILE} (spec "${SCENE_REPORT_SPEC}":
|
|
456
|
-
every setting, each character's offset, bounds and shift (and what it
|
|
471
|
+
every setting, each character's offset, scale, bounds and shift (and what it
|
|
457
472
|
moved),
|
|
458
473
|
the plate's provenance, the declared and the composed order) only when
|
|
459
474
|
it is green; then check runs over rig/ into check/, as on a rig it did
|
|
@@ -461,8 +476,14 @@ usage:
|
|
|
461
476
|
TIP_OVER_ROOT, STILL_REGIONS_DARK and RECOMPOSITE_HOLES say SKIP and
|
|
462
477
|
why), --requirements forwarded. Nothing is inferred: not the order, not
|
|
463
478
|
an overlap, not a scale, not a person; the plate's provenance and the
|
|
464
|
-
order are the author's and judged by nothing.
|
|
465
|
-
|
|
479
|
+
order are the author's and judged by nothing. --accept (or a
|
|
480
|
+
character's "accept": [...] in the scene file; the two lists are
|
|
481
|
+
joined, the command line's applying to every character) takes a build
|
|
482
|
+
whose own check failed only the bars named — ${ACCEPTABLE_BARS.join(', ')};
|
|
483
|
+
the gate, a declared requirement and an unknown name are refused
|
|
484
|
+
(SCENE_ACCEPT_KNOWN). It is for looking, not shipping: the [compose]
|
|
485
|
+
line and ${SCENE_REPORT_FILE} name what was accepted, and the run exits 1.
|
|
486
|
+
Exit 0 when the check is PASS and nothing was accepted, 1 otherwise.
|
|
466
487
|
|
|
467
488
|
rig-parts --version
|
|
468
489
|
rig-parts --help
|
|
@@ -688,7 +709,17 @@ function flags(args: string[], known: readonly string[], command: string, option
|
|
|
688
709
|
}
|
|
689
710
|
|
|
690
711
|
function cmdCheck(args: string[]): number {
|
|
691
|
-
|
|
712
|
+
// The two switches check takes (issues #202, #209): they carry no value, so they are taken out before the valued flags are read.
|
|
713
|
+
const seamPairs = args.filter((a) => a === '--seam-pairs').length;
|
|
714
|
+
if (seamPairs > 1) return usage('--seam-pairs is given twice');
|
|
715
|
+
const keepStills = args.filter((a) => a === '--keep-stills').length;
|
|
716
|
+
if (keepStills > 1) return usage('--keep-stills is given twice');
|
|
717
|
+
const f = flags(
|
|
718
|
+
args.filter((a) => a !== '--seam-pairs' && a !== '--keep-stills'),
|
|
719
|
+
['--rig', '--out'],
|
|
720
|
+
'check',
|
|
721
|
+
['--parts', '--source', '--page-edges', '--pack-shape', '--requirements'],
|
|
722
|
+
);
|
|
692
723
|
if (typeof f === 'string') return usage(f);
|
|
693
724
|
const rig = f.get('--rig') as string;
|
|
694
725
|
const out = f.get('--out') as string;
|
|
@@ -698,7 +729,12 @@ function cmdCheck(args: string[]): number {
|
|
|
698
729
|
if (!isPackShape(shape)) return usage(shape);
|
|
699
730
|
try {
|
|
700
731
|
const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
701
|
-
const r = checkStage(
|
|
732
|
+
const r = checkStage(
|
|
733
|
+
{ rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape, requirements: f.get('--requirements'), seamPairs: seamPairs === 1, keepStills: keepStills === 1 },
|
|
734
|
+
rigcRunner(bin),
|
|
735
|
+
bin,
|
|
736
|
+
console.log,
|
|
737
|
+
);
|
|
702
738
|
return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
|
|
703
739
|
} catch (err) {
|
|
704
740
|
return printRefusal(err);
|
|
@@ -732,7 +768,7 @@ function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes:
|
|
|
732
768
|
function cmdPropose(args: string[]): number {
|
|
733
769
|
const flags = new Map<string, string>();
|
|
734
770
|
let headBox = false;
|
|
735
|
-
const valued = ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--canvas', '--keypoints', '--person'];
|
|
771
|
+
const valued = ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--canvas', '--keypoints', '--person', '--wrist-snap'];
|
|
736
772
|
for (let i = 0; i < args.length; i++) {
|
|
737
773
|
const flag = args[i];
|
|
738
774
|
if (flag === '--head-box') {
|
|
@@ -775,6 +811,18 @@ function cmdPropose(args: string[]): number {
|
|
|
775
811
|
if (out === undefined) return usage('propose needs --out <dir>');
|
|
776
812
|
if (flags.has('--from-config') && flags.has('--compare')) return usage('--from-config and --compare are two modes; give one');
|
|
777
813
|
if (flags.has('--person') && !flags.has('--keypoints')) return usage('--person names a person in the --keypoints file; give --keypoints <file>');
|
|
814
|
+
// Issue #199: a declared tolerance for a wrist on no handwear part. Whole px above the built-in one: the distance is
|
|
815
|
+
// counted in whole px, so a fraction would bind what its floor binds, and a value at or under the built-in binds nothing.
|
|
816
|
+
const snapArg = flags.get('--wrist-snap');
|
|
817
|
+
let wristSnap: number | null = null;
|
|
818
|
+
if (snapArg !== undefined) {
|
|
819
|
+
if (!flags.has('--keypoints')) return usage('--wrist-snap binds a wrist joint to a handwear part; give --keypoints <file>');
|
|
820
|
+
if (flags.has('--from-config')) return usage('--wrist-snap binds a wrist while proposing; --from-config proposes nothing');
|
|
821
|
+
if (!/^[0-9]+$/.test(snapArg) || Number(snapArg) <= WRIST_ON_PART_PX) {
|
|
822
|
+
return usage(`--wrist-snap ${snapArg} is not an integer above ${WRIST_ON_PART_PX}, the px within which a wrist already holds a part`);
|
|
823
|
+
}
|
|
824
|
+
wristSnap = Number(snapArg);
|
|
825
|
+
}
|
|
778
826
|
const partsDir = existsSync(partsArg) && statSync(partsArg).isFile() ? dirname(partsArg) : partsArg;
|
|
779
827
|
if (!existsSync(source)) {
|
|
780
828
|
throw new PartsError([{ code: 'PROPOSE_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is drawn under the overlay' }]);
|
|
@@ -802,7 +850,7 @@ function cmdPropose(args: string[]): number {
|
|
|
802
850
|
}
|
|
803
851
|
const cmpPath = flags.get('--compare');
|
|
804
852
|
const cfg = cmpPath === undefined ? null : loadConfig(cmpPath);
|
|
805
|
-
const { proposal: prop, basis } = proposeWithBasis(P, joints);
|
|
853
|
+
const { proposal: prop, basis } = proposeWithBasis(P, joints, wristSnap);
|
|
806
854
|
// Emit only after green: a proposal the config loader would refuse is not written, nor a basis record that does not hold to it.
|
|
807
855
|
checkProposal(P, prop);
|
|
808
856
|
const bfile = basisFile(P, prop, basis, cfg === null || cmpPath === undefined ? null : { where: cmpPath, config: cfg });
|
|
@@ -978,7 +1026,8 @@ async function cmdComfy(args: string[]): Promise<number> {
|
|
|
978
1026
|
try {
|
|
979
1027
|
// The config is a local file, so it is answered before any host is: a
|
|
980
1028
|
// config comfy paint cannot paint from is refused with no box named.
|
|
981
|
-
const
|
|
1029
|
+
const paint = sub === 'paint' ? loadPaintConfig(configPath as string) : null;
|
|
1030
|
+
const cfg = paint === null ? null : paint.config;
|
|
982
1031
|
const host = resolveHost(v.get('--host'), process.env.COMFY_HOST);
|
|
983
1032
|
const client = new ComfyClient(host, { poll: poll as number, request: 30 });
|
|
984
1033
|
if (sub === 'seethrough') {
|
|
@@ -1017,7 +1066,7 @@ async function cmdComfy(args: string[]): Promise<number> {
|
|
|
1017
1066
|
if (typeof seed0 === 'string') return usage(seed0);
|
|
1018
1067
|
console.log(`rig-parts comfy paint: ${cfg.key} -> ${out}`);
|
|
1019
1068
|
console.log(` ${seeds} seed(s) from ${seed0}${v.has('--seed0') ? '' : ' (generation.seed)'}; wait <= ${wait} s per seed, timeout ${timeout} s`);
|
|
1020
|
-
const done = await runPainting(client, { config: cfg, out, seeds, seed0, wait: wait as number, timeout: timeout as number }, (l) => console.log(l));
|
|
1069
|
+
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));
|
|
1021
1070
|
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`);
|
|
1022
1071
|
return EXIT_OK;
|
|
1023
1072
|
} catch (err) {
|
|
@@ -1105,7 +1154,7 @@ function cmdBuild(args: string[]): number {
|
|
|
1105
1154
|
}
|
|
1106
1155
|
|
|
1107
1156
|
function cmdCompose(args: string[]): number {
|
|
1108
|
-
const f = flags(args, ['--scene', '--out'], 'compose', ['--requirements']);
|
|
1157
|
+
const f = flags(args, ['--scene', '--out'], 'compose', ['--requirements', '--accept']);
|
|
1109
1158
|
if (typeof f === 'string') return usage(f);
|
|
1110
1159
|
let bin: string;
|
|
1111
1160
|
try {
|
|
@@ -1116,8 +1165,9 @@ function cmdCompose(args: string[]): number {
|
|
|
1116
1165
|
const scratch = mkdtempSync(join(tmpdir(), 'rig-parts-compose-'));
|
|
1117
1166
|
try {
|
|
1118
1167
|
const req = f.get('--requirements');
|
|
1168
|
+
const accept = f.get('--accept');
|
|
1119
1169
|
const r = composeStage(
|
|
1120
|
-
{ scene: f.get('--scene') as string, out: f.get('--out') as string, ...(req === undefined ? {} : { requirements: req }) },
|
|
1170
|
+
{ scene: f.get('--scene') as string, out: f.get('--out') as string, ...(req === undefined ? {} : { requirements: req }), ...(accept === undefined ? {} : { accept: accept.split(',') }) },
|
|
1121
1171
|
{ rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'gate') },
|
|
1122
1172
|
console.log,
|
|
1123
1173
|
);
|