rig-parts 1.59.0 → 1.60.1
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/NOTICE.md +1 -1
- package/README.md +14 -3
- package/cli.ts +45 -3
- package/docs/AUTHORING.md +14 -4
- package/docs/MIGRATION.md +70 -0
- package/docs/PAINTING.md +2 -2
- package/docs/STABILITY.md +3 -3
- package/package.json +1 -1
- package/src/build.ts +15 -1
- package/src/check.ts +186 -16
- package/src/config.ts +4 -3
- package/src/configwrite.ts +51 -3
- package/src/meshdraft.ts +17 -4
- package/src/rig.ts +11 -1
- package/tools/configwrite.ts +1 -1
package/NOTICE.md
CHANGED
|
@@ -119,7 +119,7 @@ redistributed.
|
|
|
119
119
|
`allowDifferentLicense: false`: generated images may be used commercially and
|
|
120
120
|
the model must be credited, which this line does.
|
|
121
121
|
|
|
122
|
-
The [
|
|
122
|
+
The [rig-c/examples](https://github.com/rig-c/examples) README records
|
|
123
123
|
both, beside each character's generation record.
|
|
124
124
|
|
|
125
125
|
### Models named by the optional painting graph
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ The painting was generated for this repository with a public checkpoint (Hassaku
|
|
|
30
30
|
(Illustrious) v1.3 Style A, by Ikena, Fair AI Public License 1.0-SD) and no LoRA, on a
|
|
31
31
|
pose that holds the hands apart, then given one hand-detailer pass; its generation
|
|
32
32
|
record, See-through layers and licence are in
|
|
33
|
-
<a href="https://github.com/rig-c/
|
|
33
|
+
<a href="https://github.com/rig-c/examples">rig-c/examples</a>.
|
|
34
34
|
See-through ran twice, once on the whole figure and once on a square head crop.
|
|
35
35
|
Then one command: <code>rig-parts build --loop --lead-in 3</code>. The proposer's bones, meshes,
|
|
36
36
|
regions and motion were used as proposed but for one correction, written in the
|
|
@@ -209,7 +209,7 @@ head run on `st_input_head.png` is what gives the eyes more pixels
|
|
|
209
209
|
four, and `rig-parts comfy` is only a convenience for that route: every
|
|
210
210
|
stage after See-through reads files, whichever route wrote them. On the two
|
|
211
211
|
reference examples each run took 171–199 s through the wrapper, and on the demo 179 s (each run's
|
|
212
|
-
`meta.json`, in [
|
|
212
|
+
`meta.json`, in [rig-c/examples](https://github.com/rig-c/examples)).
|
|
213
213
|
|
|
214
214
|
### The two images See-through is fed: `inputs`
|
|
215
215
|
|
|
@@ -338,7 +338,7 @@ agent skill.
|
|
|
338
338
|
| `assemble --propose-plan … [--write-config <config>]` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule, naming in a note any full-run body no planned part covers, and naming every layer no row takes with why (empty, taken from the other run only, a plausibility rule; `nose`, never planned, with the row that lets `propose` read the head's pitch) |
|
|
339
339
|
| `assemble --source --full --head --config --out [--seam] [--project]` | merge the two runs into rig-space parts, `parts.json`, the recomposite and its error map, and list the uncovered holes |
|
|
340
340
|
| `propose --head-box --full <run> --canvas WxH [--no-back-hair] [--write-config <config>]` | propose `seethrough.head_box` from the full run, held inside the painting; `--no-back-hair` leaves the back-hair layer out of that proposal |
|
|
341
|
-
| `propose --parts --source --out [--compare <config>] [--write-config <config>]` | propose bones, meshes, regions and an idle; draw the overlay; print, per bone, which check read it or that none did and why (its role, its kind of segment), and write `basis.json` beside the proposal — what each bone rests on (a joint, a part's mask, a ratio, other bones) and, with `--compare`, whether the config's bone differs |
|
|
341
|
+
| `propose --parts --source --out [--compare <config>] [--write-config <config> [--only-missing]]` | propose bones, meshes, regions and an idle; draw the overlay; print, per bone, which check read it or that none did and why (its role, its kind of segment), and write `basis.json` beside the proposal — what each bone rests on (a joint, a part's mask, a ratio, other bones) and, with `--compare`, whether the config's bone differs |
|
|
342
342
|
| `propose … --keypoints <file> [--person <id>]` | read a posed figure's joints from one explicit file (space, image size, people, each joint observed, occluded or missing, its source): a joint with a position places the neck, hip, chest or a sleeve chain as given, a note says how every joint was used or which rule stood in, and LINT reads the torso and sleeves along the joints instead of by screen y ([AUTHORING §3](docs/AUTHORING.md)) |
|
|
343
343
|
| `propose … --grid-rule figure\|part` | how each drafted lattice's spacing is chosen: `figure` (the default) one spacing for the figure, round(H / 37) rig px, a part already coarser keeping its own, with a note on its calibration and cost; `part` the reference's rule, a tenth of the part's short side (the oracle examples are proposed under it) |
|
|
344
344
|
| `propose … --from-config <config>` | draw and LINT the config's current bones (by the joints too, given `--keypoints`) |
|
|
@@ -385,6 +385,17 @@ runs. Its already-checked splice text may be a partial config; it is not passed
|
|
|
385
385
|
through the full-config loader. If removing the staged temporary fails after
|
|
386
386
|
publication, the error may accompany an already-published complete config.
|
|
387
387
|
`--from <previous.json>` retains the previous-recipe comparison.
|
|
388
|
+
With `--only-missing` (issue #671), an ordinary proposal's `--write-config` adds
|
|
389
|
+
only `meshes.<part>` or `regions.<part>` for each proposed part the config
|
|
390
|
+
attaches by neither, entry by entry, and, for a part the 1.37.0 arm split makes
|
|
391
|
+
(`full:drape-<s>`, `full:hand-<s>`) that `assemble.plan` does not name, its plan
|
|
392
|
+
row beside the arm's row; every existing row and entry and every other byte
|
|
393
|
+
stays as read, and nothing else of the proposal is written. The candidate passes
|
|
394
|
+
the full loader first, so an entry on a bone the config does not declare is
|
|
395
|
+
refused naming the part and the bone; nothing missing leaves bytes and mtime
|
|
396
|
+
untouched. It is the upgrade path for a config proposed before the 1.37.0 arm
|
|
397
|
+
split ([MIGRATION](docs/MIGRATION.md#after-1370-merged-arm-layers-are-split-issue-490)),
|
|
398
|
+
and a usage error with `--head-box`, `--cloth` or `--from-config`.
|
|
388
399
|
`--from-config` proposes nothing and cannot be combined with `--write-config`.
|
|
389
400
|
[AUTHORING §4](docs/AUTHORING.md#4-the-command-order) describes all four modes.
|
|
390
401
|
|
package/cli.ts
CHANGED
|
@@ -279,6 +279,24 @@ usage:
|
|
|
279
279
|
keeps the original bytes and mtime. A changed source, an artifact alias
|
|
280
280
|
or a failed write is refused by name. Change lines go to stderr.
|
|
281
281
|
|
|
282
|
+
rig-parts propose --parts <dir> --source <painting.png> --out <dir> --write-config <config.json> --only-missing
|
|
283
|
+
Add to the existing config only what it lacks for the parts it attaches
|
|
284
|
+
by neither meshes.<part> nor regions.<part> (issue #671: a config proposed
|
|
285
|
+
before 1.37.0 meets the drape_<s> and hand_<s> parts the arm split makes,
|
|
286
|
+
issue #490; docs/MIGRATION.md). A part from full:drape-<s> or
|
|
287
|
+
full:hand-<s> that assemble.plan does not name gains its plan row where
|
|
288
|
+
assemble's rewrite puts it: the drape just before the full run's
|
|
289
|
+
handwear-<s> row, the hand just after it. Each part then gains its entry
|
|
290
|
+
as the proposal drafts it. Each row and entry is spliced in alone; every
|
|
291
|
+
existing row and entry and every other byte stays as read, and nothing
|
|
292
|
+
else of the proposal (bones, motion, …) is written. An unplanned part of
|
|
293
|
+
any other origin gets no row. The full config loader validates the
|
|
294
|
+
candidate first, so an added entry on a bone the config does not declare
|
|
295
|
+
is refused naming the part and the bone, and nothing is written. Nothing
|
|
296
|
+
missing keeps the bytes and mtime. One change line per added row and
|
|
297
|
+
entry goes to stderr. A usage error with --head-box, --cloth or
|
|
298
|
+
--from-config, or without --parts or --write-config.
|
|
299
|
+
|
|
282
300
|
rig-parts propose --head-box --full <dir | layers.json | file.psd> --canvas <W>x<H> [--no-back-hair]
|
|
283
301
|
[--write-config <config.json>]
|
|
284
302
|
Propose seethrough.head_box (source px, square) from the full run's
|
|
@@ -642,7 +660,15 @@ usage:
|
|
|
642
660
|
the default (AUTHORING §6, The world the readings are taken in). One
|
|
643
661
|
environment: line names the path, the step count and the sha256's first
|
|
644
662
|
16 hex; check.json's environment holds path (as given), sha256 and
|
|
645
|
-
steps. Without the flag no
|
|
663
|
+
steps. Without the flag no environment provenance key is written.
|
|
664
|
+
Physics rigs with readable declared seam pairs also report independent
|
|
665
|
+
default wind [1, 0] and still wind [0, 0], gravity [0, 1], under
|
|
666
|
+
SEAM_OVER_MOTION.worlds: gap/ghost maximum and worst frame only;
|
|
667
|
+
dir points to retained raw seam renders relative to check.json. No
|
|
668
|
+
new bar or warning, and the custom reading is unchanged. No wind
|
|
669
|
+
response or no band is SKIP; no pair or animation gives no worlds.
|
|
670
|
+
Nonphysics output is unchanged. In rig-play environment(null) returns
|
|
671
|
+
to the default world, not still air (AUTHORING, Physics seam environments).
|
|
646
672
|
|
|
647
673
|
rig-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
|
|
648
674
|
Encode a frame set rigc render wrote (its --out directory, or the set
|
|
@@ -984,7 +1010,7 @@ const TAKES = {
|
|
|
984
1010
|
layers: { valued: ['--export', '--template', '--canvas'], switches: [] },
|
|
985
1011
|
sheet: { valued: ['--source', '--layers', '--out', '--cell', '--cols'], switches: [] },
|
|
986
1012
|
rig: { valued: ['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'], switches: [] },
|
|
987
|
-
propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule', '--cloth', '--write-config', '--from'], switches: ['--head-box', '--no-back-hair'] },
|
|
1013
|
+
propose: { valued: ['--parts', '--source', '--out', '--from-config', '--compare', '--full', '--head', '--canvas', '--keypoints', '--person', '--wrist-snap', '--grid-rule', '--cloth', '--write-config', '--from'], switches: ['--head-box', '--no-back-hair', '--only-missing'] },
|
|
988
1014
|
compare: { valued: ['--left', '--right', '--map'], switches: [] },
|
|
989
1015
|
check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
|
|
990
1016
|
loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
|
|
@@ -1306,12 +1332,18 @@ function cmdPropose(args: string[]): number {
|
|
|
1306
1332
|
const flags = new Map<string, string>();
|
|
1307
1333
|
let headBox = false;
|
|
1308
1334
|
let noBackHair = false;
|
|
1335
|
+
let onlyMissing = false;
|
|
1309
1336
|
for (let i = 0; i < args.length; i++) {
|
|
1310
1337
|
const flag = args[i];
|
|
1311
1338
|
if (flag === '--head-box') {
|
|
1312
1339
|
headBox = true;
|
|
1313
1340
|
continue;
|
|
1314
1341
|
}
|
|
1342
|
+
if (flag === '--only-missing') {
|
|
1343
|
+
if (onlyMissing) return usage('--only-missing is given twice');
|
|
1344
|
+
onlyMissing = true;
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1315
1347
|
if (flag === '--no-back-hair') {
|
|
1316
1348
|
if (noBackHair) return usage('--no-back-hair is given twice');
|
|
1317
1349
|
noBackHair = true;
|
|
@@ -1324,6 +1356,16 @@ function cmdPropose(args: string[]): number {
|
|
|
1324
1356
|
flags.set(flag, value);
|
|
1325
1357
|
i++;
|
|
1326
1358
|
}
|
|
1359
|
+
// Issue #671: --only-missing adds entries to an existing config for the parts it attaches by neither section; it
|
|
1360
|
+
// belongs to the ordinary proposal's --write-config alone.
|
|
1361
|
+
if (onlyMissing) {
|
|
1362
|
+
const mode = 'propose --parts … --write-config <config.json> --only-missing';
|
|
1363
|
+
if (headBox) return usage(`--only-missing belongs to ${mode}; --head-box writes seethrough.head_box only`);
|
|
1364
|
+
if (!flags.has('--parts')) return usage(`--only-missing belongs to ${mode}; give --parts <dir>, the parts the entries are drafted from`);
|
|
1365
|
+
if (flags.has('--from-config')) return usage(`--only-missing belongs to ${mode}; --from-config proposes nothing`);
|
|
1366
|
+
if (flags.has('--cloth')) return usage(`--only-missing belongs to ${mode}; --cloth writes a new config with the recipe's entries spliced in`);
|
|
1367
|
+
if (!flags.has('--write-config')) return usage(`--only-missing belongs to ${mode}; give --write-config <config.json>, the existing config the entries are added to`);
|
|
1368
|
+
}
|
|
1327
1369
|
let staged: StagedConfigWrite | null = null;
|
|
1328
1370
|
try {
|
|
1329
1371
|
if (headBox) {
|
|
@@ -1480,7 +1522,7 @@ function cmdPropose(args: string[]): number {
|
|
|
1480
1522
|
})();
|
|
1481
1523
|
const bfile = basisFile(P, prop, basis, cfg === null || cmpPath === undefined ? null : { where: cmpPath, config: cfg });
|
|
1482
1524
|
const img = drawLandmarks(P, painting, prop.bones, 'PROPOSAL - CORRECT ME');
|
|
1483
|
-
if (target !== null) staged = stageConfigWrite(target, { kind: 'proposal', proposal: prop, sections: PROPOSAL_CONFIG_SECTIONS });
|
|
1525
|
+
if (target !== null) staged = stageConfigWrite(target, onlyMissing ? { kind: 'proposal-missing', proposal: prop, parts: P.recs } : { kind: 'proposal', proposal: prop, sections: PROPOSAL_CONFIG_SECTIONS });
|
|
1484
1526
|
else if (written !== null) staged = stageConfigWrite({ kind: 'fresh', path: written.at, text: written.text });
|
|
1485
1527
|
mkdirSync(render, { recursive: true });
|
|
1486
1528
|
writeFileSync(join(out, 'proposal.json'), serializeProposal(prop));
|
package/docs/AUTHORING.md
CHANGED
|
@@ -2888,9 +2888,10 @@ valued destination path. Its modes have different ownership:
|
|
|
2888
2888
|
| `propose --head-box` | existing regular config | `seethrough.head_box` |
|
|
2889
2889
|
| `assemble --propose-plan` | existing regular config; must name `--config` itself | `assemble.plan`, `assemble.extend_below_crop` |
|
|
2890
2890
|
| ordinary `propose --parts` without `--cloth` | existing regular config | each present `PROPOSAL_CONFIG_SECTIONS` value: `bones`, `meshes`, `regions`, `limbs`, `motion`, `constraints`, `turn`, `proposed_rig_scale` |
|
|
2891
|
+
| ordinary `propose --parts … --only-missing` (issue #671) | existing regular config | only `meshes.<part>` or `regions.<part>`, as the proposal drafts it, for each proposed part the config attaches by neither, and an `assemble.plan` row for such a part from `full:drape-<s>` / `full:hand-<s>` the plan does not name (the drape just before the full run's `handwear-<s>` row, the hand just after it); added row by row and entry by entry, every existing row and entry and every other byte as read |
|
|
2891
2892
|
| `propose --cloth <input.json>` | new file | the recipe's edits spliced into the cloth config; §3, *The config written*, gives entry ownership, hand-edit conflicts and `--from <previous.json>` |
|
|
2892
2893
|
|
|
2893
|
-
For the first three modes, each selected field replaces its whole value; other
|
|
2894
|
+
For the first three modes (not `--only-missing`), each selected field replaces its whole value; other
|
|
2894
2895
|
fields and optional proposal sections not emitted are retained. Changed whole
|
|
2895
2896
|
values are spliced into the original text through `splicedEdits`, preserving
|
|
2896
2897
|
untouched bytes. `notes` stays in the proposal. The stage's loader validates a same-directory candidate before
|
|
@@ -2903,6 +2904,13 @@ of the proposing command. A refusal leaves the target config unchanged. The
|
|
|
2903
2904
|
writer lists changes on stderr, keeping the existing head-box and plan JSON on
|
|
2904
2905
|
stdout. Without the flag, no config is changed. `--from-config` proposes nothing
|
|
2905
2906
|
and refuses this flag; `--from` belongs to the cloth mode only.
|
|
2907
|
+
`--only-missing` (issue #671) is the upgrade path for a config proposed before a stage
|
|
2908
|
+
made a part it never named — the 1.37.0 arm split's `drape_<s>` and `hand_<s>`
|
|
2909
|
+
([MIGRATION](MIGRATION.md#after-1370-merged-arm-layers-are-split-issue-490)): the same
|
|
2910
|
+
loader validates the candidate, so an added region on a bone the config does not
|
|
2911
|
+
declare is refused naming the part and the bone (`CONFIG_NAME_RESOLVES`); a part of any
|
|
2912
|
+
other origin gets no plan row; nothing missing keeps the bytes and mtime; stderr names each added row and entry. It is a usage error with `--head-box`, `--cloth`
|
|
2913
|
+
or `--from-config`, or without `--parts` or `--write-config`.
|
|
2906
2914
|
All four modes use one file-writing adapter. Fresh cloth output keeps its
|
|
2907
2915
|
already-checked splice text and intentional partial-config contract; the full
|
|
2908
2916
|
loader is not imposed on it. Publication links the complete same-directory
|
|
@@ -3591,7 +3599,7 @@ says why each requirement exists and what breaks without it.
|
|
|
3591
3599
|
|
|
3592
3600
|
| rule | means | change |
|
|
3593
3601
|
| --- | --- | --- |
|
|
3594
|
-
| <a id="RIG_PART_PRESENT"></a><a id="RIG_PART_ATTACHED"></a>`RIG_PART_PRESENT`, `RIG_PART_ATTACHED` | a `meshes`/`regions` key that is not in `parts.json`, a part with neither, or a `painting:` part with a mesh | the config and the plan must name the same parts; a patch is a region |
|
|
3602
|
+
| <a id="RIG_PART_PRESENT"></a><a id="RIG_PART_ATTACHED"></a>`RIG_PART_PRESENT`, `RIG_PART_ATTACHED` | a `meshes`/`regions` key that is not in `parts.json`, a part with neither, or a `painting:` part with a mesh | the config and the plan must name the same parts; a patch is a region. A `drape_<s>` or `hand_<s>` from `full:drape-<s>` / `full:hand-<s>` is the 1.37.0 arm split's (issue #490), new to a config proposed before it: the detail names [MIGRATION](MIGRATION.md#after-1370-merged-arm-layers-are-split-issue-490), whose remedy is `propose … --write-config <config> --only-missing` (the two plan rows and the two entries) |
|
|
3595
3603
|
| <a id="RIG_PNG_PRESENT"></a><a id="RIG_PNG_MATCHES_BOX"></a><a id="RIG_PART_HAS_ART"></a>`RIG_PNG_PRESENT`, `RIG_PNG_MATCHES_BOX`, `RIG_PART_HAS_ART` | a part's PNG is missing, the wrong size, or has no art pixel | re-run assemble |
|
|
3596
3604
|
| <a id="RIG_NAME_RESOLVES"></a><a id="RIG_SEGMENT_DEFINED"></a><a id="RIG_CHAIN_POINTS"></a>`RIG_NAME_RESOLVES`, `RIG_SEGMENT_DEFINED`, `RIG_CHAIN_POINTS` | a segment names an undeclared bone, a bone with no `tip` and no next link, or a chain too short to make a link | that bone's `tip`, or write the segment out as `[bone, [x0,y0], [x1,y1]]` |
|
|
3597
3605
|
| <a id="RIG_INFLUENCES_KEPT"></a>`RIG_INFLUENCES_KEPT` | a mesh's declared `influences` leaves a vertex with no bone: every capped share is under 0.03 (at or under it, for the continuous floor). The largest of k shares is at least 1/k, so this needs 34 or more kept bones; the default cap of 4 never reaches it (issue #284). The detail names the first vertex, how many more, and its largest share | declare `maxInfluences` 33 or less, or name fewer bones in `segments` |
|
|
@@ -3888,7 +3896,7 @@ measured). `build --loop` encodes a product and takes none. One line,
|
|
|
3888
3896
|
`environment: <path>, rigc-environment/1, <n> step(s), sha256 <16 hex>`, says which
|
|
3889
3897
|
schedule, and `check.json` carries `environment` — `path` as the command line gave it,
|
|
3890
3898
|
the `sha256` of the bytes read, `steps` — after `pack_mode`; without the flag neither
|
|
3891
|
-
is written
|
|
3899
|
+
is written; the physics seam reference worlds below are separate probes.
|
|
3892
3900
|
|
|
3893
3901
|
The world before the first step is the runtime's, so a still schedule is the one that
|
|
3894
3902
|
states it: `{ "t": 0, "wind": [1, 0], "gravity": [0, 1] }` reads what a run without the
|
|
@@ -3900,6 +3908,8 @@ wind (1, 0), then (40, 0) from 0.5 s, reads `TEXTURE_STRETCH` 32.181 (FAIL, `hai
|
|
|
3900
3908
|
triangle 258), two meshes folding under `ANIMATIONS_DO_NOT_FOLD` (FAIL; none without the
|
|
3901
3909
|
flag) and 8 `CLOTH_SETTLES` warnings for 9 (selftest `CK515`).
|
|
3902
3910
|
|
|
3911
|
+
**Physics seam environments** (issue #591). Default: wind [1, 0], gravity [0, 1]. Still: wind [0, 0], gravity [0, 1]. In rig-play, environment(null) returns to Default; still air requires an explicit wind [0, 0]. For a physics rig with readable declared pairs and animations, SEAM_OVER_MOTION adds optional `worlds.default` and `worlds.still` readings. They are informational raw gap/ghost cell counts (including cells below the warning floor), never new bars or warnings; neither changes PASS, exit status or the bar count. Each world records its vectors, status and rows; each row names its pair, animation, drawn/banded frames and gap/ghost maximum and worst frame, cell size and world box. Per-frame hits are not embedded in check.json. A rendered world's `dir` is relative to check.json (`seam_motion/default` or `seam_motion/still`): it retains the complete rigc render, including frames.json, frame images and each animation's seam.json with all raw hits. The directories are published with check.json only after the readings are validated; scratch is removed, and an unrendered SKIP has no directory reference. The original reading and any custom `--environment` schedule keep their existing meaning; the two reference worlds are independent of that schedule. A rig with no wind response reports Still as SKIP with its reason; no band is SKIP, never a measured zero. With no readable declared pair or animation no worlds block is written. Only existing declared pairs are read: neighbouring limb segments and explicit seam requirements; thigh/hem pairs are not inferred. Nonphysics output stays byte-identical, with no extra key, line or render. Reference renders start physics at frame 0 at 12 fps; they do not certify a composed player scene or another sampling rate. Malformed sidecars or renderer failures remain named refusals.
|
|
3912
|
+
|
|
3903
3913
|
### compose
|
|
3904
3914
|
|
|
3905
3915
|
Every refusal below is collected with the others and printed at once, under
|
|
@@ -4748,7 +4758,7 @@ is built (`CK83`–`CK85`, `RQ30`–`RQ35`).
|
|
|
4748
4758
|
|
|
4749
4759
|
| step | reference-sample | reference-demo | source |
|
|
4750
4760
|
| --- | --- | --- | --- |
|
|
4751
|
-
| painting (GPU) | 20.2 s | 18.3 s | `generation.json` `elapsed_s`, in [
|
|
4761
|
+
| painting (GPU) | 20.2 s | 18.3 s | `generation.json` `elapsed_s`, in [rig-c/examples](https://github.com/rig-c/examples) |
|
|
4752
4762
|
| See-through full run (GPU) | 194.9 s | 198.5 s | `inputs/layers/full/meta.json` `elapsed_s` |
|
|
4753
4763
|
| See-through head run (GPU) | 170.9 s | 171.4 s | `inputs/layers/head/meta.json` `elapsed_s` |
|
|
4754
4764
|
| `build` (CPU, user + sys) | 14.6 s | 30.7 s | measured with `/usr/bin/time -p` on an Apple M4 machine under other load, rigc's child processes included; wall clock 12.7 s and 27.5 s |
|
package/docs/MIGRATION.md
CHANGED
|
@@ -342,6 +342,76 @@ What to do: nothing. A warning names the pair, the animation, the frames and the
|
|
|
342
342
|
an opening that is not intended wants a cap that covers the turn, or a motion that bends
|
|
343
343
|
less.
|
|
344
344
|
|
|
345
|
+
## After 1.37.0: merged arm layers are split (issue #490)
|
|
346
|
+
|
|
347
|
+
Issue #490 splits a full-run `handwear-<s>` layer that See-through merged — the sleeve,
|
|
348
|
+
the bare hand at `<s>_wrist`, and an overgarment drape reaching more than a forearm's
|
|
349
|
+
length below the wrist, the drape's colour at least 80 off the sleeve's — into
|
|
350
|
+
`drape-<s>`, `handwear-<s>` and `hand-<s>`, whenever the config carries
|
|
351
|
+
`generation.control.skeleton` (the wrist and the forearm are read off it). `assemble`
|
|
352
|
+
rewrites a plan's lone `full` `handwear-<s>` row into three: `drape_<s>` behind, the
|
|
353
|
+
row's own part (the sleeve, which keeps `handwear-<s>`), and `hand_<s>` in front.
|
|
354
|
+
|
|
355
|
+
Who is affected: a config proposed before 1.37.0 whose plan takes `handwear-<s>` from
|
|
356
|
+
the full run and that carries `generation.control.skeleton` — and only where that layer
|
|
357
|
+
is a merged arm run; a config whose arm layer holds the sleeve alone builds as before.
|
|
358
|
+
Its `meshes` and `regions` never named the two new parts, so `rig` refuses each by name
|
|
359
|
+
(issue #671):
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
RIG_PART_ATTACHED: part "drape_r" — is in parts.json and has neither a config.meshes nor a config.regions entry; exactly one is required — the full run's merged arm layer was split into this part (issue #490, from 1.37.0), …
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
| change | before | after |
|
|
366
|
+
| --- | --- | --- |
|
|
367
|
+
| `assemble.plan` as `assemble` reads it, a lone `["<name>", "full", "handwear-<s>"]` row over a merged arm run | the row | `["drape_<s>", "full", "drape-<s>"]`, the row, `["hand_<s>", "full", "hand-<s>"]` |
|
|
368
|
+
| `parts.json` | one part holding sleeve, hand and drape | `drape_<s>` (`full:drape-<s>`), the sleeve part (`full:handwear-<s>`), `hand_<s>` (`full:hand-<s>`) |
|
|
369
|
+
| `rig` on a config without entries for them | — | `RIG_PART_ATTACHED` for each, the detail naming this entry and `--only-missing` |
|
|
370
|
+
| `propose … --write-config <config> --only-missing` | — | new (issue #671): adds only the plan rows and entries the config lacks for them |
|
|
371
|
+
|
|
372
|
+
What to do, keeping every entry you tuned — one command, after `assemble` has written
|
|
373
|
+
the split parts:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
rig-parts propose --parts <work/rig> --source <painting> --out <dir> --write-config config.json --only-missing
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
For each part the config attaches by neither `meshes` nor `regions`, it adds what is
|
|
380
|
+
missing and nothing else (issue #671):
|
|
381
|
+
|
|
382
|
+
- a part from `full:drape-<s>` or `full:hand-<s>` that `assemble.plan` does not name gains
|
|
383
|
+
its plan row where `assemble` rewrites it: `["drape_<s>", "full", "drape-<s>"]` just
|
|
384
|
+
before the row that takes `handwear-<s>` from the full run, `["hand_<s>", "full",
|
|
385
|
+
"hand-<s>"]` just after it. `assemble` then takes the plan as written and makes the
|
|
386
|
+
same `parts.json` byte for byte. The config loader reads `meshes` and `regions` against
|
|
387
|
+
the parts the plan makes, so the entries need the rows;
|
|
388
|
+
- the part gains `meshes.<part>` or `regions.<part>` as the proposal drafts it.
|
|
389
|
+
|
|
390
|
+
Every existing plan row, every existing entry, every other section and every other byte
|
|
391
|
+
stays as read, each addition is spliced in alone, and stderr names each row and entry
|
|
392
|
+
added. The full loader reads the result before it replaces the file: an entry on a bone
|
|
393
|
+
your config does not declare is refused naming the part and the bone, and nothing is
|
|
394
|
+
written. A second run with nothing missing changes no byte.
|
|
395
|
+
|
|
396
|
+
Or by hand, the same four additions: the two plan rows above, and a region for each part.
|
|
397
|
+
`propose` drafts both as regions: `hand_<s>` on the tip link of `sleeve_<s>` where it
|
|
398
|
+
drafts a sleeve chain (a handwear part over 500 px, or the arm drafted from
|
|
399
|
+
`--keypoints`; its note reads `hand_<s> (full:hand-<s>): split hand -> region on
|
|
400
|
+
sleeve_<s>2, the tip link of sleeve_<s>` for a three-link chain); otherwise `hand_<s>`,
|
|
401
|
+
like `drape_<s>` always, rides the trunk bone by its box's centre height — `head` above
|
|
402
|
+
the neck line, `chest` above the hip, else `hip` (its note reads `drape_<s>
|
|
403
|
+
(full:drape-<s>): no rule -> region on hip`). Confirm against the notes `propose` prints
|
|
404
|
+
for your parts; the bone must be one your config declares.
|
|
405
|
+
|
|
406
|
+
**An alternate set made before 1.37.0** that covers the split sleeve (a set over the part
|
|
407
|
+
that takes `handwear-<s>`) still covers only that part. The split made `hand_<s>` and
|
|
408
|
+
`drape_<s>` parts of their own, so while the set is active the base draws them beside the
|
|
409
|
+
set's own sleeve art, which still holds the hand it was painted with. `check --set` then
|
|
410
|
+
fails the set's seam (`CHECK_SEAM_WITHIN_BAR`, issue #671); its detail names the parts and
|
|
411
|
+
this entry. What to do: add them to the set's `hides`, for example `"hides": ["hand_r",
|
|
412
|
+
"drape_r"]`, or only `hand_<s>` if the set's painting still shows the base drape. This is
|
|
413
|
+
a judgement on the set's own art, so `--only-missing` does not write it.
|
|
414
|
+
|
|
345
415
|
## After 1.38.0: cloth that comes to rest on the idle
|
|
346
416
|
|
|
347
417
|
Issue #501 reads every physics chain by its tip on the idle `check` renders (physics on,
|
package/docs/PAINTING.md
CHANGED
|
@@ -140,7 +140,7 @@ one way that passes `painting`: the `generation` block and the pose file of seed
|
|
|
140
140
|
and chosen to be the next public example — a character for a wind demo, where hair
|
|
141
141
|
and cloth move by physics under a side gust. It is the public example `wind`: its
|
|
142
142
|
painting, See-through layers and generation record are in
|
|
143
|
-
[
|
|
143
|
+
[rig-c/examples](https://github.com/rig-c/examples) (`wind/`), and its config,
|
|
144
144
|
proposal and build are [`examples/wind`](../examples/wind/README.md). The block:
|
|
145
145
|
|
|
146
146
|
```json
|
|
@@ -350,4 +350,4 @@ To paint one without the character drifting, repaint only the limb:
|
|
|
350
350
|
|
|
351
351
|
`comfy paint` does not run an inpainting graph; the demo's hand-detailer pass is
|
|
352
352
|
the same idea at hand scale (a masked repaint on the same checkpoint, recorded
|
|
353
|
-
beside its painting in
|
|
353
|
+
beside its painting in rig-c/examples).
|
package/docs/STABILITY.md
CHANGED
|
@@ -56,7 +56,7 @@ defaults are the help's too, and none is guessed from the art.
|
|
|
56
56
|
| `comfy` | `seethrough`: `--image`, `--out`; optional `--resolution`, `--steps`, `--seed`, `--prefix`, `--offload`, `--lama`, `--nf4`, `--host`, `--wait`, `--timeout`, `--poll` |
|
|
57
57
|
| `assemble` | `--propose-plan` with `--source`, `--full`, `--config`, optional `--head`, `--write-config` (must name `--config` itself, issue #356); or `--source`, `--full`, `--config`, `--out`, optional `--head` (without it, one run: issue #359), `--seam` (near-white, silhouette), `--project` (core, visible), `--alt` (repeatable `<name>=<source>:<full>[:<head>]`, issue #454) |
|
|
58
58
|
| `propose` | `--head-box` with `--full`, `--canvas`; optional `--no-back-hair`, `--write-config` (existing config, issue #356) |
|
|
59
|
-
| `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`), `--full`, `--head` (the turn control's depths; not with `--from-config`), `--grid-rule` (`figure` or `part`, the drafted lattice's spacing; not with `--from-config`), `--cloth` (a config whose `cloth` block is the recipe, issue #364; not with `--from-config`), `--write-config` (existing config for an ordinary proposal, issue #356; a new file with `--cloth`, issue #537; not with `--from-config` — additive: without it nothing `propose` writes changes), `--from` (the config the recipe is changed from, only with `--write-config`, issue #571 — additive: without it nothing `--write-config` writes changes) |
|
|
59
|
+
| `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`), `--full`, `--head` (the turn control's depths; not with `--from-config`), `--grid-rule` (`figure` or `part`, the drafted lattice's spacing; not with `--from-config`), `--cloth` (a config whose `cloth` block is the recipe, issue #364; not with `--from-config`), `--write-config` (existing config for an ordinary proposal, issue #356; a new file with `--cloth`, issue #537; not with `--from-config` — additive: without it nothing `propose` writes changes), `--from` (the config the recipe is changed from, only with `--write-config`, issue #571 — additive: without it nothing `--write-config` writes changes), `--only-missing` (only with `--parts` and `--write-config`, not with `--cloth` or `--from-config`, issue #671 — additive: without it nothing `--write-config` writes changes) |
|
|
60
60
|
| `compare` | `--left`, `--right`; optional `--map` |
|
|
61
61
|
| `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
|
|
62
62
|
| `check` | `--rig`, `--out`; optional `--parts`, `--source`, `--page-edges`, `--pack-shape`, `--requirements`, `--seam-pairs`, `--keep-stills`, `--set` or `--all-sets` (issue #454), `--environment` (issue #538, additive) |
|
|
@@ -200,12 +200,12 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
200
200
|
| `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, `short_px` (`left`, `right`) after it only when `missing_px` is above 0 (issue #557, additive), `excluded_px` after them only on a subset set's limb part whose cap the set exclusion took a pixel of (issue #603, additive: each pixel taken out of the count it was in, so the cap's counts are the written PNG's), then `shape` and `upper_px` under the segment's `cap_shape` — `upper_px` without the shape only when the upper segment gave a pixel (issue #557, additive) — and after them `lower_px`, `lower_kept_px` and `lower_missing_px` under `cap_shape: "ball"` (issue #579, additive), and `edge`, `feather_radius` and `feathered_px` under its `edge`, and `cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and `stroke_luma_max` (when the band has a split) under its `cut_band`, issue #360), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px` — on a subset set's limb part counted after the set exclusion, issue #603 — `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242); `single_run_pad` (`file_size`, `side`, `left`, `top`) last, only for one run whose file is not the `seethrough.resolution` square (issue #366) |
|
|
201
201
|
| `rig/rig.json`, `rig/motion.json`, `rig/images/` | `rig`, `build`, `compose` | rig-c's rig spec and motion spec, `spec` `rigc-rig/1` and `rigc-motion/1`: the format is rig-c's, and this package writes only what rig-c's parser accepts; a bone carries `inherit` only where config.bones declares one, rig-c's mode name as written (issue #348) |
|
|
202
202
|
| `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones` (the sorted names the attachment's weights bind in the written `rig.json`, in every mode — since 1.49.1 (issue #536) a rig with `turn` or `stops` names the twins and `<link>_d` bones there, where the row named the segment bones they replace: a fix of a wrong name, not a format change; a rig with neither writes the bytes it wrote), `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins`, and `faint` only when an island was left out as faint — only when it declares `contour.bridges`); or `mode` "influence" with `influence` (as declared), `reach` (`farthest`, `at`, `nearest`, `capped`, `widest`) and `lattice` (rig-c's `SegmentsLatticeReport`: `cols`, `rows`, `artCells`, `keptCells`, `passes`, `islands`) (issue #378); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
|
|
203
|
-
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40`, and `step_px` (`left`, `right`) only on a joint whose cap record carries `short_px` (issue #557, additive) — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493, a warning and not a bar: `status` — `WARN`, `CLEAR` or `SKIP` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, `config` under `build` only (issue #607, additive: `path` as `--config` was typed, `sha256` of the bytes the build read; `check` and `compose` read no config and write none), a `requirements` block under `--requirements` (or `build`'s `config.requirements`) — `file` first from issue #556, additive: `path` as typed or declared, `sha256`, `keys` — and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
203
|
+
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `environment` after it under `--environment` only (issue #538, additive: `path` as given, `sha256`, `steps`), `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40`, and `step_px` (`left`, `right`) only on a joint whose cap record carries `short_px` (issue #557, additive) — and `unread` on a joint not read), `SEAM_OVER_MOTION` after it when `parts.json` holds a limb segment or `--requirements` a `seam` (issue #493, a warning and not a bar: `status` — `WARN`, `CLEAR` or `SKIP` — `reason` on a SKIP only, `fps`, `floor`, `pairs` — each `pair`, `a`, `b`, `declared`, and `unread` on a pair not read — `animations`, `warnings`, `rows` — each `pair`, `animation`, `status`, `drawn`, `banded`, `opens`, `px`, `max_px`, `under_floor`; issue #591 adds optional `worlds` on physics rigs with readable declared pairs and animations: `default` and `still`, each `wind`, `gravity`, `status` (`REPORTED` or `SKIP`), `reason` on SKIP, `dir` only when rendered (relative to check.json: `seam_motion/default` or `seam_motion/still`, the complete retained render with frames.json and each animation's raw seam.json), and `rows` — pair, animation, status, drawn, banded, `gap` and `ghost`, each `max_px` and `worst` only; worst carries `index`, `px`, `cell`, `box`. Per-frame hits stay in the raw sidecars. These raw reference readings add no bar or warning and leave the custom environment reading unchanged), `CLOTH_SETTLES` after it when the rig declares a physics constraint and has an idle (issue #501, a warning and not a bar: `status` — `WARN` or `CLEAR` — `fps`, `frames`, `duration`, `floor_rig_px_per_s`, `floor`, `levers`, `warnings`, `rows` — each `tip`, `root`, `links`, `point`, `settles_at`, `max_rig_px_per_s`, `min_rig_px_per_s`), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, `config` under `build` only (issue #607, additive: `path` as `--config` was typed, `sha256` of the bytes the build read; `check` and `compose` read no config and write none), a `requirements` block under `--requirements` (or `build`'s `config.requirements`) — `file` first from issue #556, additive: `path` as typed or declared, `sha256`, `keys` — and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
204
204
|
| `layers.json`, `<index>_<name>.png` | `layers --export` | the wrapper form `layers` reads (issue #359): `width`, `height`, then `layers` — each `name`, `filename`, `left`, `top`, `right`, `bottom`, `depth_median` (the input's own; `null` for a PSD, the manifest's order then being the draw order) — drawn layers in draw order, then the hidden-art layers; a PNG's name is its index and its layer's name, and the manifest, not the file name, says which layer it is |
|
|
205
205
|
| the template `.psd` | `layers --template` | one empty layer per name `templateLayerNames` (`src/layers.ts`) holds, bottom first; the same canvas writes the same bytes |
|
|
206
206
|
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `constraints` with `--cloth` (issue #364: a physics constraint per tree link named `<bone>_physics`, a dangling end's `<bone>_translate`, and a skirt swing's transforms `<swing>_<tree>`), `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes` |
|
|
207
207
|
| `basis.json` | `propose` | spec `spine-parts-basis/1` |
|
|
208
|
-
| an existing config named by `--write-config` | `propose --head-box`, `assemble --propose-plan`, ordinary `propose` | issue #356, additive: `assemble --propose-plan` must update `--config` itself; only the selected proposed fields are replaced by whole-value text splices, preserving untouched bytes; absent optional proposal sections and other fields remain; the stage loader validates before atomic replacement, relative paths keep their base, an unchanged update preserves bytes and mtime, and stderr lists changes; no multi-artifact transaction is promised |
|
|
208
|
+
| an existing config named by `--write-config` | `propose --head-box`, `assemble --propose-plan`, ordinary `propose` | issue #356, additive: `assemble --propose-plan` must update `--config` itself; only the selected proposed fields are replaced by whole-value text splices, preserving untouched bytes; absent optional proposal sections and other fields remain; the stage loader validates before atomic replacement, relative paths keep their base, an unchanged update preserves bytes and mtime, and stderr lists changes; no multi-artifact transaction is promised; with `--only-missing` (issue #671) only a `meshes.<part>` or `regions.<part>` entry for a proposed part the config attaches by neither is added, and an `assemble.plan` row for such a part the 1.37.0 arm split makes that the plan does not name, every existing row and entry and every other byte as read |
|
|
209
209
|
| a new `--write-config` file | `propose --cloth … --write-config` | complete checked splice text published without overwrite; an intervening creator is refused, while cleanup failure after publication may leave the complete new file present; intentional partial configs retain their existing contract; the `--cloth` config read, byte for byte, but for the entries the recipe drafts (issue #537), measured against the same parts drafted without it: `meshes.<part>` and `regions.<part>` for each part the recipe moves, and `bones`, `constraints` and `motion.tracks` entry by entry by name (issue #552) — an entry the recipe drafts replaced or inserted, one it replaces removed, every other entry kept with its bytes and its place, and a hand edit to an entry the recipe owns refused by name (`PROPOSE_WRITE_CONFIG_CONFLICT`) rather than dropped; a section the input lacks is inserted; with `--from <previous config>` (issue #571) an entry equal to the previous recipe's draft is the recipe's as well |
|
|
210
210
|
| `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw`, and `faint` only when an island was left out as faint — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
|
|
211
211
|
| `rig.json`, `motion.json`, `mesh_report.json`, `images/`, `remesh.json` | `remesh` | the input revision's files, `rig.json` and `mesh_report.json` with the part's attachment and row replaced (under `--delta` the row is the input's with its counts, `bones` and influences recomputed and `mode` "delta"), every other file byte for byte; `remesh.json` is spec `rig-parts-remesh/1`: `request` (`part`, `mesh`, `mesh_sha256`, `idle_keys`, `page_edges`, `pack_shape`; under `--delta`, issue #526: `part`, `delta` — the mesh edit as read — `delta_sha256`, `ops`, `applied` — `moved`, `added`, `removed`, `constrained` — `idle_keys`, `page_edges`, `pack_shape`), `versions` (`parts`, `rigc`), `input` (`rig` — `path`, `bytes`, `sha256` per file — `config_sha256`, `parts_json_sha256`), `baseline` (`reproduced`, `not_compared`; under `--delta` `reproduced` holds one sentence: no config is built, the input is the baseline), `target` (`before`, `after`: `vertices`, `triangles`, `hull`), `preserved` (`identical`, `spliced`), `revision` (per file as `input.rig`), `check` (`input`, `revision`: `pass`, `measured`, `not_met`, `skipped`, `physics`, then `line` and `status` — issue #541, additive, the spec stays `rig-parts-remesh/1`: `line` is the last line `check` prints on that tree, byte for byte, and `status` every block's status as that tree's `check.json` records it, by name in the file's order — `gate`, `loop` and `seam` read from their figures, every other block its own `status`, a block the file does not write absent; `scope`) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.60.1",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/build.ts
CHANGED
|
@@ -1719,7 +1719,7 @@ export function sweepCheckLines(line: SweepLine, steps: ReadonlyMap<string, { le
|
|
|
1719
1719
|
* read nothing of (its SKIP) or the line could not read. A SKIP with nothing declared prints its reason alone.
|
|
1720
1720
|
*/
|
|
1721
1721
|
export function motionSeamLines(line: MotionSeamLine, plant: MotionSeamPlant = {}): string[] {
|
|
1722
|
-
if (line.status === 'SKIP' && line.rows.length === 0 && line.pairs.every((p) => p.unread === undefined)) return [`${MOTION_SEAM_LINE}: SKIP — ${String(line.reason)}`];
|
|
1722
|
+
if (line.worlds === undefined && line.status === 'SKIP' && line.rows.length === 0 && line.pairs.every((p) => p.unread === undefined)) return [`${MOTION_SEAM_LINE}: SKIP — ${String(line.reason)}`];
|
|
1723
1723
|
const read = `${line.pairs.length} declared pair(s) in ${line.animations.length} animation(s) (${line.animations.join(', ')}), read by rigc render --seam at ${line.fps} fps on the build as played`;
|
|
1724
1724
|
const head =
|
|
1725
1725
|
line.status === 'SKIP'
|
|
@@ -1731,6 +1731,20 @@ export function motionSeamLines(line: MotionSeamLine, plant: MotionSeamPlant = {
|
|
|
1731
1731
|
...line.rows.filter((r) => r.status === 'skip').map((r) => `seam "${r.pair}" in "${r.animation}": SKIP — rigc read no band on any of the ${r.drawn} frame(s) both slots draw, so there is no seam to read`),
|
|
1732
1732
|
...(plant.silent === true ? [] : line.rows.filter((r) => r.opens.length > 0).map(motionSeamWarnText)),
|
|
1733
1733
|
...line.rows.filter((r) => r.opens.length === 0 && r.under_floor.length > 0).map((r) => `seam "${r.pair}" in "${r.animation}": under the floor — rigc names ${r.under_floor.length} frame(s) open ${frameRuns(r.under_floor)}, each opening one cell thick`),
|
|
1734
|
+
...(line.worlds === undefined ? [] : (['default', 'still'] as const).flatMap(name => {
|
|
1735
|
+
const w = line.worlds?.[name];
|
|
1736
|
+
if (w === undefined) return [];
|
|
1737
|
+
const label = 'seam world "' + name + '"';
|
|
1738
|
+
const header = label + ' wind [' + w.wind.join(', ') + '], gravity [' + w.gravity.join(', ') + ']: ' + w.status +
|
|
1739
|
+
(w.status === 'SKIP' ? ' — ' + String(w.reason) : ' — raw gap/ghost cell counts, informational, not judged') +
|
|
1740
|
+
(w.dir === undefined ? '' : '; raw render: ' + w.dir);
|
|
1741
|
+
return [header, ...w.rows.map(r => {
|
|
1742
|
+
const head = label + ' "' + r.pair + '" in "' + r.animation + '": ';
|
|
1743
|
+
if (r.status === 'skip') return head + 'SKIP — rigc read no band';
|
|
1744
|
+
const at = (kind: typeof r.gap) => kind.worst === null ? 'none' : 'frame ' + kind.worst.index + ' (t = ' + kind.worst.index / line.fps + 's)';
|
|
1745
|
+
return head + 'gap ' + r.gap.max_px + ' cell(s), worst ' + at(r.gap) + '; ghost ' + r.ghost.max_px + ' cell(s), worst ' + at(r.ghost);
|
|
1746
|
+
})];
|
|
1747
|
+
})),
|
|
1734
1748
|
];
|
|
1735
1749
|
}
|
|
1736
1750
|
|
package/src/check.ts
CHANGED
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
* `TIME_SLACK`, issue #244).
|
|
79
79
|
*/
|
|
80
80
|
import { createHash } from 'node:crypto';
|
|
81
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
82
|
-
import { basename, delimiter, dirname, join, resolve } from 'node:path';
|
|
81
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
82
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
83
83
|
import { measureRecomposite, type RecompositeFigures, recompositeRecord, scenePhrase, sourceInRig } from './assemble.ts';
|
|
84
84
|
import { type BoneTransform, computeExactFrameTransforms, cropToSpineY, normaliseDegrees, toBoneLocal } from './coords.ts';
|
|
85
85
|
import { capBox, inDisc } from './limbs.ts';
|
|
@@ -128,7 +128,7 @@ import {
|
|
|
128
128
|
type WorldBox,
|
|
129
129
|
} from './instruments.ts';
|
|
130
130
|
import { type AlternatePartsRecord, FRONT_SUFFIX, type LimbRecord, type PartRecord, type PartsFile, readParts } from './parts.ts';
|
|
131
|
-
import { poseRigBonesForDrawn, type RigBone, SETS_FILE } from './rig.ts';
|
|
131
|
+
import { ARM_SPLIT_MIGRATION, poseRigBonesForDrawn, type RigBone, SETS_FILE } from './rig.ts';
|
|
132
132
|
import { turnTwin } from './turn.ts';
|
|
133
133
|
import {
|
|
134
134
|
aimLine,
|
|
@@ -160,7 +160,7 @@ import {
|
|
|
160
160
|
} from './requirements.ts';
|
|
161
161
|
import { RIG_BONE_INHERIT, type RigSpec as RigcRigSpec } from 'rig-c/src/rig.ts';
|
|
162
162
|
import { expandStops } from 'rig-c/src/stop.ts';
|
|
163
|
-
import { ENVIRONMENT_SPEC, EnvironmentError, readEnvironmentSchedule } from 'rig-c/src/environment.ts';
|
|
163
|
+
import { ENVIRONMENT_SPEC, EnvironmentError, environmentResponse, readEnvironmentSchedule } from 'rig-c/src/environment.ts';
|
|
164
164
|
import { alphaComposite } from './raster/composite.ts';
|
|
165
165
|
import { readPng, writePng } from './raster/png.ts';
|
|
166
166
|
import { newFloatImage, newRaster, type Raster } from './raster/types.ts';
|
|
@@ -1280,6 +1280,29 @@ export function setPlacements(parts: PartsFile, alt: AlternatePartsRecord, slotO
|
|
|
1280
1280
|
return out;
|
|
1281
1281
|
}
|
|
1282
1282
|
|
|
1283
|
+
/**
|
|
1284
|
+
* Issue #671: why a set made before 1.37.0 can fail its seam after #490's arm
|
|
1285
|
+
* split. The set covers a side's sleeve (a part from `full:handwear-<s>`), and
|
|
1286
|
+
* the split made that side's `hand_<s>` / `drape_<s>` parts of their own, which
|
|
1287
|
+
* the set neither covers nor hides, so the base hand is drawn as well as the
|
|
1288
|
+
* hand inside the set's own sleeve art. Returns the hint for the set's seam
|
|
1289
|
+
* refusal, or null when nothing of the kind applies.
|
|
1290
|
+
*/
|
|
1291
|
+
export function armSplitSetHint(parts: PartsFile, alt: AlternatePartsRecord): string | null {
|
|
1292
|
+
const from = new Map(parts.parts.map((p) => [p.name, p.from]));
|
|
1293
|
+
const covered = new Set(Object.keys(alt.slots).map((n) => (n.endsWith(FRONT_SUFFIX) ? n.slice(0, -FRONT_SUFFIX.length) : n)));
|
|
1294
|
+
const hidden = new Set(alt.hides ?? []);
|
|
1295
|
+
const open: string[] = [];
|
|
1296
|
+
for (const side of ['r', 'l']) {
|
|
1297
|
+
if (![...covered].some((n) => from.get(n) === `full:handwear-${side}`)) continue;
|
|
1298
|
+
for (const p of parts.parts) {
|
|
1299
|
+
if ((p.from === `full:hand-${side}` || p.from === `full:drape-${side}`) && !covered.has(p.name) && !hidden.has(p.name)) open.push(p.name);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (open.length === 0) return null;
|
|
1303
|
+
return ` — the set covers a sleeve whose merged arm layer 1.37.0 split (issue #490), and it neither covers nor hides ${open.map((n) => `"${n}"`).join(', ')}, so the base draws ${open.length === 1 ? 'that part' : 'those parts'} beside the set's own art; a set made before 1.37.0 hides it: add ${open.map((n) => `"${n}"`).join(', ')} to the set's hides (see ${ARM_SPLIT_MIGRATION})`;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1283
1306
|
/**
|
|
1284
1307
|
* The flat stack of parts for an alternate set (issue #454), in the order
|
|
1285
1308
|
* {@link setPlacements} gives: with `slotOrder`, a front part (issue #664) is
|
|
@@ -3512,6 +3535,7 @@ export const MOTION_SEAM_LINE = 'SEAM_OVER_MOTION';
|
|
|
3512
3535
|
|
|
3513
3536
|
/** The seam reading's scratch build and renders, under `--out`; removed before the stage returns. */
|
|
3514
3537
|
const MOTION_SEAM_SCRATCH = '_seam_motion';
|
|
3538
|
+
const MOTION_SEAM_DIR = 'seam_motion';
|
|
3515
3539
|
|
|
3516
3540
|
/** rig-c's seam reading: the file it writes in each set's directory, and its format tag. */
|
|
3517
3541
|
export const SEAM_READING_FILE = 'seam.json';
|
|
@@ -3561,6 +3585,8 @@ export interface MotionSeamLine {
|
|
|
3561
3585
|
/** Rows with a frame over the floor: the WARN lines. */
|
|
3562
3586
|
warnings: number;
|
|
3563
3587
|
rows: MotionSeamRow[];
|
|
3588
|
+
/** Physics-only reference worlds (issue #591); do not participate in warnings or bars. */
|
|
3589
|
+
worlds?: MotionSeamWorlds;
|
|
3564
3590
|
}
|
|
3565
3591
|
|
|
3566
3592
|
/**
|
|
@@ -3617,7 +3643,109 @@ export interface MotionSeamPlant {
|
|
|
3617
3643
|
silent?: true;
|
|
3618
3644
|
}
|
|
3619
3645
|
|
|
3620
|
-
|
|
3646
|
+
|
|
3647
|
+
/** One raw opening, in rigc cells; its frame time is index / the line's fps. */
|
|
3648
|
+
export interface MotionSeamHit {
|
|
3649
|
+
index: number;
|
|
3650
|
+
px: number;
|
|
3651
|
+
cell: number;
|
|
3652
|
+
box: [number, number, number, number];
|
|
3653
|
+
}
|
|
3654
|
+
export interface MotionSeamWorldKind {
|
|
3655
|
+
max_px: number;
|
|
3656
|
+
worst: MotionSeamHit | null;
|
|
3657
|
+
}
|
|
3658
|
+
/** Raw gap and ghost readings, including cells under the warning floor. */
|
|
3659
|
+
export interface MotionSeamWorldRow {
|
|
3660
|
+
pair: string;
|
|
3661
|
+
animation: string;
|
|
3662
|
+
status: 'read' | 'skip';
|
|
3663
|
+
drawn: number;
|
|
3664
|
+
banded: number;
|
|
3665
|
+
gap: MotionSeamWorldKind;
|
|
3666
|
+
ghost: MotionSeamWorldKind;
|
|
3667
|
+
}
|
|
3668
|
+
export interface MotionSeamWorld {
|
|
3669
|
+
wind: [number, number];
|
|
3670
|
+
gravity: [number, number];
|
|
3671
|
+
status: 'REPORTED' | 'SKIP';
|
|
3672
|
+
reason?: string;
|
|
3673
|
+
/** Complete rigc render directory, relative to check.json; absent when not rendered. */
|
|
3674
|
+
dir?: string;
|
|
3675
|
+
rows: MotionSeamWorldRow[];
|
|
3676
|
+
}
|
|
3677
|
+
export interface MotionSeamWorlds {
|
|
3678
|
+
default: MotionSeamWorld;
|
|
3679
|
+
still: MotionSeamWorld;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
/** No callback and no output for nonphysics; a wind no constraint answers is not measured. */
|
|
3683
|
+
export function motionSeamWorlds(physics: boolean, answersWind: boolean, read: (world: 'default' | 'still') => MotionSeamWorldRow[]): MotionSeamWorlds | undefined {
|
|
3684
|
+
if (!physics) return undefined;
|
|
3685
|
+
const world = (wind: [number, number], rows: MotionSeamWorldRow[], reason?: string): MotionSeamWorld => {
|
|
3686
|
+
const measured = rows.some(r => r.status === 'read');
|
|
3687
|
+
return { wind, gravity: [0, 1], status: measured ? 'REPORTED' : 'SKIP',
|
|
3688
|
+
...(measured ? {} : { reason: reason ?? 'rigc read no band on any frame of any declared pair in any animation' }), rows };
|
|
3689
|
+
};
|
|
3690
|
+
return {
|
|
3691
|
+
default: world([1, 0], read('default')),
|
|
3692
|
+
still: answersWind ? world([0, 0], read('still')) : world([0, 0], [], 'no physics constraint answers wind; still air is not separately measurable'),
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
/** The same validated sidecars as the warning, retaining the raw gap and ghost cells. */
|
|
3697
|
+
export function readMotionSeamWorldRows(dir: string, animations: readonly string[], fps: number, pairs: readonly MotionSeamPair[]): MotionSeamWorldRow[] {
|
|
3698
|
+
const rows: MotionSeamWorldRow[] = [];
|
|
3699
|
+
readMotionSeams(dir, animations, fps, pairs, {}, rows);
|
|
3700
|
+
return rows;
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
|
|
3704
|
+
/** Publish complete validated render directories with check.json; roll back if its writer fails. */
|
|
3705
|
+
export function publishMotionSeamWorlds(out: string, scratch: string, worlds: MotionSeamWorlds, publishCheck: () => void): void {
|
|
3706
|
+
const problems: Problem[] = [];
|
|
3707
|
+
const names = (['default', 'still'] as const).filter(name => worlds[name].dir !== undefined);
|
|
3708
|
+
for (const name of names) {
|
|
3709
|
+
const world = worlds[name];
|
|
3710
|
+
const home = join(scratch, name);
|
|
3711
|
+
if (world.dir !== MOTION_SEAM_DIR + '/' + name) problems.push({ code: 'FRAMES_SIDECAR', object: 'seam world ' + name, detail: 'raw directory reference must be ' + MOTION_SEAM_DIR + '/' + name });
|
|
3712
|
+
const manifest = readJson(join(home, 'frames.json'), problems);
|
|
3713
|
+
const sets = manifest !== null && Array.isArray(manifest.sets) ? manifest.sets.filter(isRecord) : [];
|
|
3714
|
+
for (const animation of new Set(world.rows.map(row => row.animation))) {
|
|
3715
|
+
const set = sets.find(item => item.animation === animation);
|
|
3716
|
+
const rawDir = set !== undefined && typeof set.dir === 'string' ? set.dir : null;
|
|
3717
|
+
const at = rawDir === null ? null : resolve(home, rawDir);
|
|
3718
|
+
const rel = at === null ? '..' : relative(home, at);
|
|
3719
|
+
if (rawDir === null || isAbsolute(rawDir) || at === null || isAbsolute(rel) || /^\.\.(?:[\\/]|$)/.test(rel) || !existsSync(join(at, SEAM_READING_FILE))) {
|
|
3720
|
+
problems.push({ code: 'FRAMES_SIDECAR', object: home, detail: 'animation ' + JSON.stringify(animation) + ' raw dir ' + JSON.stringify(rawDir) + ' must be relative, stay inside its render directory and hold seam.json' });
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
refuseIfAny(problems);
|
|
3725
|
+
const stage = join(scratch, '_publish');
|
|
3726
|
+
const previous = join(scratch, '_previous');
|
|
3727
|
+
const kept = join(out, MOTION_SEAM_DIR);
|
|
3728
|
+
const check = join(out, 'check.json');
|
|
3729
|
+
const previousCheck = existsSync(check) ? readFileSync(check) : null;
|
|
3730
|
+
mkdirSync(out, { recursive: true });
|
|
3731
|
+
mkdirSync(stage, { recursive: true });
|
|
3732
|
+
for (const name of names) renameSync(join(scratch, name), join(stage, name));
|
|
3733
|
+
const hadPrevious = existsSync(kept);
|
|
3734
|
+
if (hadPrevious) renameSync(kept, previous);
|
|
3735
|
+
try {
|
|
3736
|
+
renameSync(stage, kept);
|
|
3737
|
+
publishCheck();
|
|
3738
|
+
} catch (err) {
|
|
3739
|
+
rmSync(kept, { recursive: true, force: true });
|
|
3740
|
+
if (hadPrevious) renameSync(previous, kept);
|
|
3741
|
+
if (previousCheck === null) rmSync(check, { force: true });
|
|
3742
|
+
else writeFileSync(check, previousCheck);
|
|
3743
|
+
throw err;
|
|
3744
|
+
}
|
|
3745
|
+
rmSync(previous, { recursive: true, force: true });
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
type SeamHitRead = MotionSeamHit;
|
|
3621
3749
|
|
|
3622
3750
|
/**
|
|
3623
3751
|
* The rows of one `rigc render --seam` run over every animation: `frames.json`
|
|
@@ -3626,7 +3754,7 @@ type SeamHitRead = { index: number; px: number; cell: number; box: [number, numb
|
|
|
3626
3754
|
* file that is missing, a file that is not the animation's or not at the rate
|
|
3627
3755
|
* asked, a pair out of the flag's order, and a field of the wrong shape.
|
|
3628
3756
|
*/
|
|
3629
|
-
export function readMotionSeams(dir: string, animations: readonly string[], fps: number, pairs: readonly MotionSeamPair[], plant: MotionSeamPlant = {}): MotionSeamRow[] {
|
|
3757
|
+
export function readMotionSeams(dir: string, animations: readonly string[], fps: number, pairs: readonly MotionSeamPair[], plant: MotionSeamPlant = {}, worldRows?: MotionSeamWorldRow[]): MotionSeamRow[] {
|
|
3630
3758
|
const problems: Problem[] = [];
|
|
3631
3759
|
const bad = (object: string, detail: string): void => {
|
|
3632
3760
|
problems.push({ code: 'FRAMES_SIDECAR', object, detail });
|
|
@@ -3663,6 +3791,7 @@ export function readMotionSeams(dir: string, animations: readonly string[], fps:
|
|
|
3663
3791
|
return;
|
|
3664
3792
|
}
|
|
3665
3793
|
const over = new Map<number, number>();
|
|
3794
|
+
const kinds: Record<'gap' | 'ghost', MotionSeamWorldKind> = { gap: { max_px: 0, worst: null }, ghost: { max_px: 0, worst: null } };
|
|
3666
3795
|
for (const kind of ['gap', 'ghost'] as const) {
|
|
3667
3796
|
const k = p[kind];
|
|
3668
3797
|
const frames = isRecord(k) && Array.isArray(k.frames) ? k.frames.map(hitOf) : null;
|
|
@@ -3670,8 +3799,12 @@ export function readMotionSeams(dir: string, animations: readonly string[], fps:
|
|
|
3670
3799
|
bad(`${at} ${kind}`, 'is not { frames: [{ index, px, cell, box }…], spans, worst } as rigc-seam/1 writes it');
|
|
3671
3800
|
return;
|
|
3672
3801
|
}
|
|
3673
|
-
|
|
3802
|
+
const hits = frames as SeamHitRead[];
|
|
3803
|
+
const worst = hits.reduce<MotionSeamHit | null>((best, h) => best === null || h.px > best.px || (h.px === best.px && h.index < best.index) ? h : best, null);
|
|
3804
|
+
kinds[kind] = { max_px: worst?.px ?? 0, worst };
|
|
3805
|
+
for (const h of hits) if (plant.noFloor === true || openingOverFloor(h)) over.set(h.index, Math.max(over.get(h.index) ?? 0, h.px));
|
|
3674
3806
|
}
|
|
3807
|
+
worldRows?.push({ pair: want.pair, animation, status: p.status, drawn: p.drawn, banded: p.banded, gap: kinds.gap, ghost: kinds.ghost });
|
|
3675
3808
|
const opens = [...over.keys()].sort((x, y) => x - y);
|
|
3676
3809
|
rows.push({
|
|
3677
3810
|
pair: want.pair,
|
|
@@ -4047,13 +4180,15 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4047
4180
|
const seamDir = join(out, SEAM_DIR);
|
|
4048
4181
|
const keptDir = join(out, STILL_DIR);
|
|
4049
4182
|
const foldDir = join(out, FOLD_SCRATCH);
|
|
4050
|
-
|
|
4183
|
+
const seamMotionDir = join(out, MOTION_SEAM_SCRATCH);
|
|
4184
|
+
for (const d of [buildDir, idleDir, stillDir, isoDir, loopDir, seamDir, keptDir, foldDir, seamMotionDir]) rmSync(d, { recursive: true, force: true });
|
|
4051
4185
|
const written: string[] = [];
|
|
4052
4186
|
const write = (name: string, data: string): void => {
|
|
4053
4187
|
writeFileSync(join(out, name), data);
|
|
4054
4188
|
written.push(name);
|
|
4055
4189
|
};
|
|
4056
4190
|
|
|
4191
|
+
try {
|
|
4057
4192
|
// 1. build, packed, under the policy profile
|
|
4058
4193
|
const build = rigc(['build', '--rig', inp.rigPath, '--motion', inp.motionPath, '--out', buildDir, ...packedBuildArgs(mode)]);
|
|
4059
4194
|
const gateHtml = buildGateLines(build.out);
|
|
@@ -4406,15 +4541,38 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4406
4541
|
else if (animationNames.length === 0) motionSeam = motionSeamLine(pairs, [], fps, [], 'motion.json holds no animation to read the declared pairs in');
|
|
4407
4542
|
else if (read.length === 0) motionSeam = motionSeamLine(pairs, animationNames, fps, []);
|
|
4408
4543
|
else {
|
|
4409
|
-
const seamMotionDir = join(out, MOTION_SEAM_SCRATCH);
|
|
4410
4544
|
rmSync(seamMotionDir, { recursive: true, force: true });
|
|
4411
|
-
|
|
4545
|
+
{
|
|
4412
4546
|
const list = read.map((p) => `${p.a}:${p.b}`).join(',');
|
|
4413
|
-
const
|
|
4547
|
+
const activeDest = stripped === null ? seamMotionDir : join(seamMotionDir, env === null ? 'default' : 'active');
|
|
4548
|
+
const renderedWorlds = new Set<'default' | 'still'>();
|
|
4549
|
+
const r = measured(['render', '--candidate', buildDir, '--seam', list, '--fps', String(fps), '--max', String(FOLD_RENDER_MAX), '--out', activeDest]);
|
|
4414
4550
|
if (r.status !== 0) refuseIfAny(rigcFailed(`render --seam ${list} --fps ${fps}`, r, r.out.split('\n').filter((l) => l.includes('FAIL') || l.includes('rigc:'))));
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4551
|
+
const activeWorldRows: MotionSeamWorldRow[] = [];
|
|
4552
|
+
motionSeam = motionSeamLine(pairs, animationNames, fps, readMotionSeams(activeDest, animationNames, fps, read, {}, stripped !== null && env === null ? activeWorldRows : undefined));
|
|
4553
|
+
if (stripped !== null) {
|
|
4554
|
+
const response = environmentResponse(readFileSync(join(buildDir, 'skeleton.json'), 'utf8'));
|
|
4555
|
+
motionSeam.worlds = motionSeamWorlds(true, response.wind.length > 0, world => {
|
|
4556
|
+
if (world === 'default' && env === null) { renderedWorlds.add(world); return activeWorldRows; }
|
|
4557
|
+
const dest = join(seamMotionDir, world);
|
|
4558
|
+
const args = ['render', '--candidate', buildDir, '--seam', list, '--fps', String(fps), '--max', String(FOLD_RENDER_MAX), '--out', dest];
|
|
4559
|
+
if (world === 'still') {
|
|
4560
|
+
const schedule = join(seamMotionDir, 'still.json');
|
|
4561
|
+
writeFileSync(schedule, JSON.stringify({ spec: ENVIRONMENT_SPEC, steps: [{ t: 0, wind: [0, 0] }] }));
|
|
4562
|
+
args.push('--environment', schedule);
|
|
4563
|
+
}
|
|
4564
|
+
// These reference worlds are independent of the caller's --environment.
|
|
4565
|
+
const rendered = rigc(args);
|
|
4566
|
+
if (rendered.status !== 0) refuseIfAny(rigcFailed('render --seam (' + world + ' reference world)', rendered, rendered.out.split('\n').filter(l => l.includes('FAIL') || l.includes('rigc:'))));
|
|
4567
|
+
const rows = readMotionSeamWorldRows(dest, animationNames, fps, read);
|
|
4568
|
+
renderedWorlds.add(world);
|
|
4569
|
+
return rows;
|
|
4570
|
+
});
|
|
4571
|
+
for (const world of renderedWorlds) {
|
|
4572
|
+
const reading = motionSeam.worlds?.[world];
|
|
4573
|
+
if (reading !== undefined) reading.dir = MOTION_SEAM_DIR + '/' + world;
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4418
4576
|
}
|
|
4419
4577
|
}
|
|
4420
4578
|
}
|
|
@@ -4447,6 +4605,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4447
4605
|
let blink: JudgementLine = skip(lacking);
|
|
4448
4606
|
let sourceRead: ReportedLine | null = null;
|
|
4449
4607
|
const setsFigures: Record<string, SetCheckFigures> = {};
|
|
4608
|
+
// Issue #671: a failing set's hint, read with its seam; never written to check.json.
|
|
4609
|
+
const setHints = new Map<string, string>();
|
|
4450
4610
|
if (inp.parts !== null || inp.source !== null) {
|
|
4451
4611
|
try {
|
|
4452
4612
|
mkdirSync(stillDir, { recursive: true });
|
|
@@ -4518,6 +4678,10 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4518
4678
|
const altMean = round3(altSeam.mean);
|
|
4519
4679
|
const altScale = round6(altStillSet.viewport.scale);
|
|
4520
4680
|
const altFailed = seamFails(altMean, altSeam.over40, altScale);
|
|
4681
|
+
if (altFailed) {
|
|
4682
|
+
const hint = armSplitSetHint(inp.parts, alt);
|
|
4683
|
+
if (hint !== null) setHints.set(alt.name, hint);
|
|
4684
|
+
}
|
|
4521
4685
|
let altPairs: SeamPairRow[] | null = null;
|
|
4522
4686
|
if (seamPairsAsked || altFailed) {
|
|
4523
4687
|
const placed = setPlacements(inp.parts, alt, rigSlotOrder).map(({ name, png, box }) => ({ name, from: box.from, x: box.x, y: box.y, image: readPng(join(inp.partsDir, `${png}.png`)) }));
|
|
@@ -4653,7 +4817,8 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4653
4817
|
`mean |d| ${setFig.seam_mean}/255 and ${setFig.seam_px_over_40} frame px over ${SEAM_PX_LEVEL} (${setFig.seam_px_over_80} over ${SEAM_PX_LEVEL_HIGH}) at frame scale ${setFig.seam_frame_scale}, ${seamRigPxText(setFig.seam_px_over_40, setFig.seam_frame_scale)} rig px (the count / s²); mean <= ${SEAM_MEAN_BAR.toFixed(1)} and <= ${SEAM_PX_BAR} rig px over ${SEAM_PX_LEVEL} are required` +
|
|
4654
4818
|
(worst === null
|
|
4655
4819
|
? `; no pixel is over ${SEAM_PX_LEVEL}, so the mean alone fails and there is no pair to name`
|
|
4656
|
-
: `; worst of ${(setFig.seam_pairs as SeamPairRow[]).length} pair(s): ${seamPairText(worst)}`)
|
|
4820
|
+
: `; worst of ${(setFig.seam_pairs as SeamPairRow[]).length} pair(s): ${seamPairText(worst)}`) +
|
|
4821
|
+
(setHints.get(setName) ?? ''),
|
|
4657
4822
|
});
|
|
4658
4823
|
}
|
|
4659
4824
|
}
|
|
@@ -4662,7 +4827,9 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4662
4827
|
problems.unshift(...barProblems);
|
|
4663
4828
|
problems.push(...foldProblems, ...levelProblems, ...stressProblems, ...limbProblems, ...sweepProblems, ...reqProblems);
|
|
4664
4829
|
figures.PASS = problems.length === 0;
|
|
4665
|
-
write('check.json', `${JSON.stringify(figures, null, 1)}\n`);
|
|
4830
|
+
const publishCheck = (): void => write('check.json', `${JSON.stringify(figures, null, 1)}\n`);
|
|
4831
|
+
if (motionSeam.worlds === undefined) publishCheck();
|
|
4832
|
+
else publishMotionSeamWorlds(out, seamMotionDir, motionSeam.worlds, publishCheck);
|
|
4666
4833
|
const bars: CheckReport['bars'] = { measured: ['gate'], skipped: [] };
|
|
4667
4834
|
const tally = (bar: string, reason: string | null): void => {
|
|
4668
4835
|
if (reason === null) bars.measured.push(bar);
|
|
@@ -4701,6 +4868,9 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
4701
4868
|
cloth,
|
|
4702
4869
|
...(sweepSteps.size === 0 ? {} : { sweepSteps }),
|
|
4703
4870
|
};
|
|
4871
|
+
} finally {
|
|
4872
|
+
rmSync(seamMotionDir, { recursive: true, force: true });
|
|
4873
|
+
}
|
|
4704
4874
|
}
|
|
4705
4875
|
|
|
4706
4876
|
// ---------------------------------------------------------------------------
|
package/src/config.ts
CHANGED
|
@@ -2997,9 +2997,10 @@ function checkRegions(c: Check, v: Json, bones: Set<string>): void {
|
|
|
2997
2997
|
return;
|
|
2998
2998
|
}
|
|
2999
2999
|
for (const [part, bone] of Object.entries(v as Record<string, Json>)) {
|
|
3000
|
-
if (typeof bone !== 'string'
|
|
3001
|
-
|
|
3002
|
-
|
|
3000
|
+
if (typeof bone !== 'string') c.fail('CONFIG_NAME_RESOLVES', `config.regions.${part}`, `is ${show(bone)}; a bone name is required`);
|
|
3001
|
+
// Issue #671: the part and the bone by name, so an entry `propose --only-missing` drafted on a bone the config
|
|
3002
|
+
// does not declare says which.
|
|
3003
|
+
else if (!bones.has(bone)) c.fail('CONFIG_NAME_RESOLVES', `config.regions.${part}`, `names the bone ${show(bone)} for the part "${part}", which config.bones does not declare; a declared bone is required`);
|
|
3003
3004
|
}
|
|
3004
3005
|
}
|
|
3005
3006
|
|
package/src/configwrite.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* The three config updates proposed by the CLI (issue #356). Pure JSON merge:
|
|
3
3
|
* no file reads/writes and no substitute for the stage's existing validator.
|
|
4
4
|
* A present proposal section replaces that whole field; an absent optional
|
|
5
|
-
* section is retained.
|
|
5
|
+
* section is retained. `proposal-missing` (issue #671, `--only-missing`) instead
|
|
6
|
+
* adds only the `meshes` / `regions` entries of proposed parts the config attaches
|
|
7
|
+
* by neither, one edit per entry, and leaves every existing entry as it is. The caller supplies the proposer's own section tuple,
|
|
6
8
|
* avoiding a second policy list or a runtime import of the proposer graph.
|
|
7
9
|
*/
|
|
8
10
|
import type { PlanProposal } from './assemble.ts';
|
|
@@ -16,10 +18,14 @@ export interface ConfigObject { [key: string]: ConfigJson }
|
|
|
16
18
|
export type ConfigWritePatch =
|
|
17
19
|
| { kind: 'head-box'; head_box: HeadBoxProposal['head_box'] }
|
|
18
20
|
| { kind: 'assemble-plan'; plan: PlanProposal['plan']; extend_below_crop: PlanProposal['extend_below_crop'] }
|
|
19
|
-
| { kind: 'proposal'; proposal: Proposal; sections: typeof PROPOSAL_CONFIG_SECTIONS }
|
|
21
|
+
| { kind: 'proposal'; proposal: Proposal; sections: typeof PROPOSAL_CONFIG_SECTIONS }
|
|
22
|
+
/** Issue #671 (`--only-missing`): only `meshes.<part>` / `regions.<part>` for a proposed part the config attaches by neither. */
|
|
23
|
+
| { kind: 'proposal-missing'; proposal: Proposal; parts: ReadonlyArray<{ name: string; from: string }> };
|
|
20
24
|
export interface ConfigWriteChange {
|
|
21
25
|
path: string;
|
|
22
26
|
kind: 'added' | 'replaced' | 'removed';
|
|
27
|
+
/** The value written, where the path alone does not say it (an array element added, issue #671). */
|
|
28
|
+
value?: string;
|
|
23
29
|
}
|
|
24
30
|
export interface ConfigWriteResult {
|
|
25
31
|
config: ConfigObject;
|
|
@@ -111,7 +117,49 @@ export function mergeConfigProposal(raw: unknown, patch: ConfigWritePatch): Conf
|
|
|
111
117
|
}
|
|
112
118
|
break;
|
|
113
119
|
}
|
|
114
|
-
|
|
120
|
+
case 'proposal-missing': {
|
|
121
|
+
// Issue #671: a config proposed before a part existed (the #490 arm split's drape_<s> and hand_<s>) gains its
|
|
122
|
+
// plan row (split parts only) and an entry for that part alone, entry by entry, in the section the proposal drafts
|
|
123
|
+
// it in. A part the config already attaches, by either section, keeps its entry as written; nothing else of the
|
|
124
|
+
// proposal is read.
|
|
125
|
+
const attached = (part: string): boolean => (object(config.meshes) && own(config.meshes, part)) || (object(config.regions) && own(config.regions, part));
|
|
126
|
+
for (const section of ['meshes', 'regions'] as const) {
|
|
127
|
+
if (config[section] !== undefined && !object(config[section])) return refuse(`config.${section}`, 'is not an object; an object of part entries is required before an entry is added to it');
|
|
128
|
+
}
|
|
129
|
+
const added: ConfigWriteChange[] = [];
|
|
130
|
+
// The #490 split's parts the plan does not name, each row where `mergedArmSplitPlan` puts it: the drape just
|
|
131
|
+
// before the full run's handwear-<s> row, the hand just after it. A part of any other origin is not planned here.
|
|
132
|
+
const assemble = config.assemble;
|
|
133
|
+
const plan = object(assemble) && Array.isArray(assemble.plan) ? assemble.plan : null;
|
|
134
|
+
if (plan !== null) {
|
|
135
|
+
const row = (e: ConfigJson): [string, string, string] | null =>
|
|
136
|
+
Array.isArray(e) && e.length === 3 && e.every((x) => typeof x === 'string') ? (e as [string, string, string]) : null;
|
|
137
|
+
for (const part of patch.parts) {
|
|
138
|
+
const m = /^full:(drape|hand)-([rl])$/.exec(part.from);
|
|
139
|
+
if (m === null) continue;
|
|
140
|
+
if (plan.some((e) => { const r = row(e); return r !== null && (r[0] === part.name || (r[1] === 'full' && r[2] === `${m[1]}-${m[2]}`)); })) continue;
|
|
141
|
+
const arm = plan.findIndex((e) => { const r = row(e); return r !== null && r[1] === 'full' && r[2] === `handwear-${m[2]}`; });
|
|
142
|
+
if (arm < 0) continue;
|
|
143
|
+
const at = m[1] === 'drape' ? arm : arm + 1;
|
|
144
|
+
const value: ConfigJson = [part.name, 'full', `${m[1]}-${m[2]}`];
|
|
145
|
+
plan.splice(at, 0, value);
|
|
146
|
+
edits.push({ path: ['assemble', 'plan', at], value, insert: true, inline: true });
|
|
147
|
+
added.push({ path: `assemble.plan[${at}]`, kind: 'added', value: JSON.stringify(value) });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const section of ['meshes', 'regions'] as const) {
|
|
151
|
+
for (const [part, entry] of Object.entries(patch.proposal[section])) {
|
|
152
|
+
if (attached(part)) continue;
|
|
153
|
+
const value = clone(entry, field(`proposal.${section}`, part));
|
|
154
|
+
if (!object(config[section])) config[section] = {};
|
|
155
|
+
(config[section] as ConfigObject)[part] = value;
|
|
156
|
+
edits.push({ path: [section, part], value });
|
|
157
|
+
added.push({ path: field(section, part), kind: 'added' });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { config, changes: added, edits };
|
|
161
|
+
}
|
|
162
|
+
default: return refuse('write-config', 'has no supported operation; head-box, assemble-plan, proposal or proposal-missing is required');
|
|
115
163
|
}
|
|
116
164
|
return { config, changes: changes(before, config), edits };
|
|
117
165
|
}
|
package/src/meshdraft.ts
CHANGED
|
@@ -589,6 +589,11 @@ export interface ConfigEdit {
|
|
|
589
589
|
path: readonly (string | number)[];
|
|
590
590
|
value: unknown;
|
|
591
591
|
insert?: boolean;
|
|
592
|
+
/**
|
|
593
|
+
* With `insert` (issue #671): in an array written on one line, the element goes in as `JSON.stringify(value)`
|
|
594
|
+
* beside its neighbours on that line, so a one-line plan stays one line; elsewhere it changes nothing.
|
|
595
|
+
*/
|
|
596
|
+
inline?: boolean;
|
|
592
597
|
}
|
|
593
598
|
|
|
594
599
|
/** The span at `path`, or null where the path does not resolve (a key absent, or a step that is not an object). */
|
|
@@ -607,10 +612,18 @@ function indentAt(text: string, at: number): string {
|
|
|
607
612
|
}
|
|
608
613
|
|
|
609
614
|
/** An element inserted into the array at `arrayPath` before its element `index` (at its length: after the last). */
|
|
610
|
-
function insertElement(text: string, arrayPath: readonly (string | number)[], index: number, value: unknown): string {
|
|
615
|
+
function insertElement(text: string, arrayPath: readonly (string | number)[], index: number, value: unknown, inline = false): string {
|
|
611
616
|
const arr = spanOrNull(text, arrayPath);
|
|
612
617
|
if (arr === null || text[arr.start] !== '[') throw new Error(`splicedEdits: ${arrayPath.join('.') || 'the document'} is not an array`);
|
|
613
618
|
const next = spanOrNull(text, [...arrayPath, index]);
|
|
619
|
+
if (inline && !text.slice(arr.start, arr.end).includes('\n')) {
|
|
620
|
+
const body = JSON.stringify(value);
|
|
621
|
+
if (next !== null) return `${text.slice(0, next.start)}${body},${text.slice(next.start)}`;
|
|
622
|
+
const last = index > 0 ? spanOrNull(text, [...arrayPath, index - 1]) : null;
|
|
623
|
+
if (index > 0 && last === null) throw new Error(`splicedEdits: ${arrayPath.join('.')} has no element ${index - 1}`);
|
|
624
|
+
if (last === null) return `${text.slice(0, arr.start + 1)}${body}${text.slice(arr.end - 1)}`;
|
|
625
|
+
return `${text.slice(0, last.end)},${body}${text.slice(last.end)}`;
|
|
626
|
+
}
|
|
614
627
|
if (next !== null) {
|
|
615
628
|
// Before the element: the new one takes its line, and the old one follows on a line of its own at the same indent.
|
|
616
629
|
const indent = indentAt(text, next.start);
|
|
@@ -626,11 +639,11 @@ function insertElement(text: string, arrayPath: readonly (string | number)[], in
|
|
|
626
639
|
}
|
|
627
640
|
|
|
628
641
|
/** One edit on `text`; every byte it does not name is the input's. */
|
|
629
|
-
function spliceOne(text: string, path: readonly (string | number)[], value: unknown, insert = false): string {
|
|
642
|
+
function spliceOne(text: string, path: readonly (string | number)[], value: unknown, insert = false, inline = false): string {
|
|
630
643
|
const step = path[path.length - 1];
|
|
631
644
|
if (insert) {
|
|
632
645
|
if (typeof step !== 'number') throw new Error(`splicedEdits: an insert names an array index; ${path.join('.')} does not`);
|
|
633
|
-
return insertElement(text, path.slice(0, -1), step, value);
|
|
646
|
+
return insertElement(text, path.slice(0, -1), step, value, inline);
|
|
634
647
|
}
|
|
635
648
|
const at = spanOrNull(text, path);
|
|
636
649
|
if (value === undefined) {
|
|
@@ -694,7 +707,7 @@ function firstMoved(got: unknown, want: unknown, path: readonly string[]): strin
|
|
|
694
707
|
*/
|
|
695
708
|
export function splicedEdits(text: string, edits: readonly ConfigEdit[]): { text: string; moved: string | null } {
|
|
696
709
|
let out = text;
|
|
697
|
-
for (const e of edits) out = spliceOne(out, e.path, e.value, e.insert === true);
|
|
710
|
+
for (const e of edits) out = spliceOne(out, e.path, e.value, e.insert === true, e.inline === true);
|
|
698
711
|
const want = JSON.parse(text) as Record<string, unknown>;
|
|
699
712
|
for (const e of edits) {
|
|
700
713
|
let o: Record<string | number, unknown> | unknown[] = want;
|
package/src/rig.ts
CHANGED
|
@@ -185,6 +185,11 @@ import { DISTANCE_EXPONENT, type FloorForm, type Influence, influences, type Inf
|
|
|
185
185
|
/** Transparent pixels added round every part image — the reference's `PAD`. */
|
|
186
186
|
export const PAD = 4;
|
|
187
187
|
|
|
188
|
+
/** Issue #671: the MIGRATION entry for 1.37.0's arm split (#490), by its heading's anchor. */
|
|
189
|
+
export const ARM_SPLIT_MIGRATION = 'docs/MIGRATION.md#after-1370-merged-arm-layers-are-split-issue-490';
|
|
190
|
+
/** Issue #671: the command that adds only what a config lacks for the arm split's parts: their plan rows and their entries. */
|
|
191
|
+
export const ARM_SPLIT_REMEDY = 'rig-parts propose --parts <parts> --source <painting> --out <dir> --write-config <config> --only-missing';
|
|
192
|
+
|
|
188
193
|
/**
|
|
189
194
|
* The places a bone's rotation and every local offset (a bone's `x, y`, a
|
|
190
195
|
* weight's bind `x, y`, a region's `x, y` and `rotation`) are written to.
|
|
@@ -1401,6 +1406,11 @@ export function buildRig(
|
|
|
1401
1406
|
}
|
|
1402
1407
|
|
|
1403
1408
|
// ---- parts ↔ config ----------------------------------------------------
|
|
1409
|
+
// Issue #671: a part 1.37.0's arm split (#490) makes from the full run is new to a config proposed before it.
|
|
1410
|
+
const armSplitHint = (from: string): string =>
|
|
1411
|
+
/^full:(drape|hand)-[rl]$/.test(from)
|
|
1412
|
+
? ` — the full run's merged arm layer was split into this part (issue #490, from 1.37.0), which a config proposed before 1.37.0 does not name; see ${ARM_SPLIT_MIGRATION}, and add only its plan row and entry with: ${ARM_SPLIT_REMEDY}`
|
|
1413
|
+
: '';
|
|
1404
1414
|
const where = (part: string): string => `parts.json holds ${parts.parts.length} part(s): ${parts.parts.map((p) => p.name).join(', ')}`;
|
|
1405
1415
|
for (const part of Object.keys(cfg.meshes)) if (!byPart.has(part)) fail('RIG_PART_PRESENT', `config.meshes.${part}`, `names the part "${part}", which is not in parts.json; ${where(part)}`);
|
|
1406
1416
|
for (const [part, bone] of Object.entries(cfg.regions)) {
|
|
@@ -1421,7 +1431,7 @@ export function buildRig(
|
|
|
1421
1431
|
for (const p of parts.parts) {
|
|
1422
1432
|
const inM = p.name in cfg.meshes;
|
|
1423
1433
|
const inR = p.name in cfg.regions;
|
|
1424
|
-
if (!inM && !inR) fail('RIG_PART_ATTACHED', `part "${p.name}"`,
|
|
1434
|
+
if (!inM && !inR) fail('RIG_PART_ATTACHED', `part "${p.name}"`, `is in parts.json and has neither a config.meshes nor a config.regions entry; exactly one is required${armSplitHint(p.from)}`);
|
|
1425
1435
|
if (inM && inR) fail('RIG_PART_ATTACHED', `part "${p.name}"`, 'has both a config.meshes and a config.regions entry; exactly one is required');
|
|
1426
1436
|
else if (inM && readFrom(p.from)?.run === PAINTING_RUN) {
|
|
1427
1437
|
fail('RIG_PART_ATTACHED', `part "${p.name}"`, `is a painting patch (${p.from}) and has a config.meshes entry; a patch is a region — config.regions.${p.name} names its bone`);
|
package/tools/configwrite.ts
CHANGED
|
@@ -199,5 +199,5 @@ export function stageConfigWrite(target: ConfigWriteTarget | FreshConfigWrite, p
|
|
|
199
199
|
}
|
|
200
200
|
/** Print to stderr so proposal JSON on stdout remains machine-readable. */
|
|
201
201
|
export function configWriteLines(target: ConfigWriteTarget, changes: readonly ConfigWriteChange[]): string[] {
|
|
202
|
-
return changes.length === 0 ? [`config ${target.path}: unchanged`] : changes.map((c) => `config ${target.path}: ${c.kind} ${c.path}`);
|
|
202
|
+
return changes.length === 0 ? [`config ${target.path}: unchanged`] : changes.map((c) => `config ${target.path}: ${c.kind} ${c.path}${c.value === undefined ? '' : ` ${c.value}`}`);
|
|
203
203
|
}
|