rig-parts 1.57.1 → 1.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +22 -0
- package/README.md +82 -10
- package/cli.ts +63 -16
- package/docs/AUTHORING.md +149 -19
- package/docs/STABILITY.md +12 -7
- package/package.json +2 -1
- package/src/build.ts +43 -7
- package/src/check.ts +55 -27
- package/src/config.ts +97 -3
- package/src/configwrite.ts +117 -0
- package/src/diagnostics.ts +62 -1
- package/src/errors.ts +12 -0
- package/src/parts.ts +27 -0
- package/src/propose.ts +8 -0
- package/src/rig.ts +91 -11
- package/src/scene.ts +3 -2
- package/tools/configwrite.ts +203 -0
package/NOTICE.md
CHANGED
|
@@ -122,6 +122,28 @@ redistributed.
|
|
|
122
122
|
The [parts-examples](https://github.com/rig-c/parts-examples) README records
|
|
123
123
|
both, beside each character's generation record.
|
|
124
124
|
|
|
125
|
+
### Models named by the optional painting graph
|
|
126
|
+
|
|
127
|
+
**[observed 2026-10-22]** In `src/graphs.ts`, the intended OpenPose ControlNet is
|
|
128
|
+
[xinsir/controlnet-openpose-sdxl-1.0](https://huggingface.co/xinsir/controlnet-openpose-sdxl-1.0/blob/23f966cd5cfdd3f7729c903e243d87152162d2b7/README.md).
|
|
129
|
+
That revision's card declares `license: apache-2.0`. Its
|
|
130
|
+
[`diffusion_pytorch_model.safetensors`](https://huggingface.co/xinsir/controlnet-openpose-sdxl-1.0/blob/23f966cd5cfdd3f7729c903e243d87152162d2b7/diffusion_pytorch_model.safetensors)
|
|
131
|
+
is the source for the host-local name `controlnet-openpose-sdxl-1.0.safetensors`.
|
|
132
|
+
The graph uses that name only when `generation.control` is configured and
|
|
133
|
+
`generation.control.model` is absent; a configured model filename replaces it.
|
|
134
|
+
The local filename alone does not identify or verify the installed weights.
|
|
135
|
+
|
|
136
|
+
**[observed 2026-10-22]** The graph always names `4x-AnimeSharp.pth` for model
|
|
137
|
+
upscaling, followed by Lanczos resizing. The source is
|
|
138
|
+
[Kim2091/AnimeSharp at `7696d95`](https://huggingface.co/Kim2091/AnimeSharp/blob/7696d95ced82b0c1f2a41f6ac73336133f0a90e1/4x-AnimeSharp.pth),
|
|
139
|
+
and the [creator's card at that revision](https://huggingface.co/Kim2091/AnimeSharp/blob/7696d95ced82b0c1f2a41f6ac73336133f0a90e1/README.md)
|
|
140
|
+
declares CC BY-NC-SA 4.0, including its NonCommercial condition. rig-parts ships
|
|
141
|
+
neither model and grants no additional rights to their weights. These observations
|
|
142
|
+
record the models' declared terms; they do not assign a blanket licence to output
|
|
143
|
+
images. The existing upscaler selection is unchanged. Whether to require explicit
|
|
144
|
+
selection is the separate, deferred owner decision in
|
|
145
|
+
[#428](https://github.com/rig-c/parts/issues/428).
|
|
146
|
+
|
|
125
147
|
## 4. Behaviour reimplemented, not code copied
|
|
126
148
|
|
|
127
149
|
The raster operations in `src/raster/` reproduce the documented behaviour of
|
package/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# rig-parts
|
|
2
2
|
|
|
3
3
|
**AI-authored 2D character rigs from one anime painting, verified before they
|
|
4
|
-
are written.** rig-parts
|
|
4
|
+
are written.** rig-parts generates no images. The painting and See-through layers
|
|
5
|
+
come from models the user runs on their GPU or a web demo; this package ships no
|
|
6
|
+
weights. The optional `comfy` adapter drives the user's own ComfyUI host and models.
|
|
7
|
+
rig-parts takes a single character painting and the layers
|
|
5
8
|
[See-through](https://github.com/shitagaki-lab/see-through) decomposed it into,
|
|
6
9
|
merges them into measured rig-space parts, authors weighted meshes, bone chains
|
|
7
10
|
and a looping idle over them in [rig-c](https://github.com/rig-c/core)'s
|
|
@@ -168,11 +171,19 @@ rig-parts at what it wrote:
|
|
|
168
171
|
|
|
169
172
|
| Route | Where | Output rig-parts reads |
|
|
170
173
|
| --- | --- | --- |
|
|
171
|
-
| Hugging Face Space | [24yearsold/see-through-demo](https://huggingface.co/spaces/24yearsold/see-through-demo) (ZeroGPU;
|
|
174
|
+
| Hugging Face Space | [24yearsold/see-through-demo](https://huggingface.co/spaces/24yearsold/see-through-demo) (ZeroGPU) | the `.psd` it produces; upload `st_input_full.png`, the white-padded square from `inputs`, for the full run, and `st_input_head.png` for the separate head run |
|
|
172
175
|
| ModelScope demo | [ljsabc/See-Through](https://modelscope.cn/studios/ljsabc/See-Through), linked from the upstream README | the `.psd` it produces |
|
|
173
176
|
| Upstream CLI | `python inference/scripts/inference_psd.py --srcp <image> --save_to_psd` in a checkout of [shitagaki-lab/see-through](https://github.com/shitagaki-lab/see-through) | the `.psd` in `workspace/layerdiff_output/` |
|
|
174
177
|
| ComfyUI wrapper | [jtydhr88/ComfyUI-See-through](https://github.com/jtydhr88/ComfyUI-See-through) on your own ComfyUI box; the optional `comfy` adapter (`rig-parts comfy seethrough`) drives it | the `layers.json` + PNGs it writes |
|
|
175
178
|
|
|
179
|
+
Use the square `inputs` outputs on the Space too. Its [app calls `apply_layerdiff`](https://huggingface.co/spaces/24yearsold/see-through-demo/blob/049a1ab07908e404aad03f73431dd1a98f88202c/app.py#L110-L118),
|
|
180
|
+
which [calls `center_square_pad_resize`](https://huggingface.co/spaces/24yearsold/see-through-demo/blob/049a1ab07908e404aad03f73431dd1a98f88202c/common/utils/inference_utils.py#L78-L83).
|
|
181
|
+
That [preprocessing function](https://huggingface.co/spaces/24yearsold/see-through-demo/blob/049a1ab07908e404aad03f73431dd1a98f88202c/common/utils/cv.py#L254-L274)
|
|
182
|
+
pads a non-square upload before resizing; the centre-crop reported in
|
|
183
|
+
[#356](https://github.com/rig-c/parts/issues/356) is not the current source path.
|
|
184
|
+
`st_input_full.png` supplies the explicit white padding that rig-parts maps back
|
|
185
|
+
to the original painting. This is a source inspection, not a new demo run.
|
|
186
|
+
|
|
176
187
|
**The head run is needed on every route that was read.** See-through's model
|
|
177
188
|
already separates the head in a second stage of its own: it crops the head the
|
|
178
189
|
first stage found, runs the face tags on that crop at the same `resolution`, and
|
|
@@ -217,10 +228,10 @@ commands drive it; nothing else in the pipeline needs one:
|
|
|
217
228
|
|
|
218
229
|
```sh
|
|
219
230
|
rig-parts comfy paint --config config.json --out <dir> --host http://<box>:8188
|
|
220
|
-
rig-parts comfy seethrough --image st_input_full.png --out layers/full --host http://<box>:8188
|
|
231
|
+
rig-parts comfy seethrough --image st_input_full.png --out layers/full --host http://<box>:8188
|
|
221
232
|
```
|
|
222
233
|
|
|
223
|
-
`comfy paint`
|
|
234
|
+
`comfy paint` asks the user's models to generate `painting_<seed>.png` from the config's inline
|
|
224
235
|
`generation` block — checkpoint, LoRAs, sampler, and an optional OpenPose
|
|
225
236
|
control skeleton it draws itself — and records the prompts verbatim beside it.
|
|
226
237
|
`comfy seethrough` runs the [ComfyUI-See-through](https://github.com/jtydhr88/ComfyUI-See-through)
|
|
@@ -230,6 +241,32 @@ the graph is checked against the box's `/object_info`, so a missing node class
|
|
|
230
241
|
or model is refused by name, and the adapter waits for an empty queue rather
|
|
231
242
|
than queueing behind someone else's job.
|
|
232
243
|
|
|
244
|
+
The painting graph uses `4x-AnimeSharp.pth`, then Lanczos resizing. The
|
|
245
|
+
[creator's card](https://huggingface.co/Kim2091/AnimeSharp/blob/7696d95ced82b0c1f2a41f6ac73336133f0a90e1/README.md)
|
|
246
|
+
declares CC BY-NC-SA 4.0. When `generation.control` is set, the intended OpenPose
|
|
247
|
+
ControlNet is [xinsir/controlnet-openpose-sdxl-1.0](https://huggingface.co/xinsir/controlnet-openpose-sdxl-1.0/blob/23f966cd5cfdd3f7729c903e243d87152162d2b7/README.md),
|
|
248
|
+
whose card declares Apache-2.0. Install its `diffusion_pytorch_model.safetensors`
|
|
249
|
+
on the host as `controlnet-openpose-sdxl-1.0.safetensors`, or select its installed
|
|
250
|
+
filename with `generation.control.model`. A filename alone does not verify the
|
|
251
|
+
weights. [NOTICE](NOTICE.md#models-named-by-the-optional-painting-graph) pins the
|
|
252
|
+
sources and records their declared terms; those declarations do not assign a
|
|
253
|
+
blanket licence to output images. The graph's automatic upscaler selection is
|
|
254
|
+
unchanged; the owner decision is tracked separately in [#428](https://github.com/rig-c/parts/issues/428).
|
|
255
|
+
|
|
256
|
+
`--offload` selects the wrapper's CUDA group-offload path on the **ComfyUI host**:
|
|
257
|
+
its [LayerDiff and depth loaders](https://github.com/jtydhr88/ComfyUI-See-through/blob/98d754bf04f668647919ab750eccb0e0640faa81/nodes.py#L395-L475)
|
|
258
|
+
call `enable_group_offload('cuda', ...)`. Omit it for a local Mac/MPS host. A Mac
|
|
259
|
+
running this CLI may pass it to a remote CUDA host. This describes that offload
|
|
260
|
+
path; it is not a claim that the remaining model stack has been verified on MPS.
|
|
261
|
+
|
|
262
|
+
The [wrapper README at `98d754b`](https://github.com/jtydhr88/ComfyUI-See-through/blob/98d754bf04f668647919ab750eccb0e0640faa81/README.md#L22-L28)
|
|
263
|
+
describes a combined Decompose node and names `24yearsold/seethroughv0.0.1_marigold`
|
|
264
|
+
for depth. The shipped `workflows/seethrough.json` instead sends
|
|
265
|
+
`SeeThrough_GenerateLayers`, `SeeThrough_GenerateDepth` and `SeeThrough_PostProcess`,
|
|
266
|
+
and selects `layerdifforg/seethroughv0.0.1_marigold`, matching the wrapper's
|
|
267
|
+
[implemented depth default](https://github.com/jtydhr88/ComfyUI-See-through/blob/98d754bf04f668647919ab750eccb0e0640faa81/nodes.py#L82-L85).
|
|
268
|
+
Use the graph's node classes and the host's `/object_info` when checking the adapter.
|
|
269
|
+
|
|
233
270
|
## The loop, for an agent
|
|
234
271
|
|
|
235
272
|
```sh
|
|
@@ -261,8 +298,10 @@ rig-parts build --config config.json --source painting.png --full layers/full --
|
|
|
261
298
|
|
|
262
299
|
At each step the config holds only what that step reads; the one table of what
|
|
263
300
|
that is, step by step, is [docs/AUTHORING.md §4](docs/AUTHORING.md#4-the-command-order).
|
|
264
|
-
The
|
|
265
|
-
from a config holding only what the first step reads (`RL01`).
|
|
301
|
+
The command-order controls exercise the hand-copy route above on each fetched
|
|
302
|
+
example from a config holding only what the first step reads (`RL01`). The optional
|
|
303
|
+
`--write-config` forms below save the proposed fields; `RL01` does not exercise
|
|
304
|
+
that flag. On `demo` the
|
|
266
305
|
proposal pasted uncorrected stops at the last step, naming its `bottomwear` on
|
|
267
306
|
`CHECK_TIP_OVER_ROOT` — the correction its tracked config records there, the two outer
|
|
268
307
|
skirt chains moved (issue #227, [its README](examples/demo/README.md)); on `scarf`,
|
|
@@ -296,10 +335,10 @@ agent skill.
|
|
|
296
335
|
| `comfy seethrough --image --out [--host]` | optional: run the ComfyUI See-through wrapper on one image and write the form `layers` reads |
|
|
297
336
|
| `layers <dir \| layers.json \| file.psd>` | print every layer of a decomposition: draw order, name, tag group, box, size, opaque pixels, depth, and its translucent, background and area figures; a `WARN` line for a layer `--propose-plan` will leave out |
|
|
298
337
|
| `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
|
|
299
|
-
| `assemble --propose-plan
|
|
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) |
|
|
300
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 |
|
|
301
|
-
| `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
|
|
302
|
-
| `propose --parts --source --out [--compare <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 |
|
|
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 |
|
|
303
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)) |
|
|
304
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) |
|
|
305
344
|
| `propose … --from-config <config>` | draw and LINT the config's current bones (by the joints too, given `--keypoints`) |
|
|
@@ -322,10 +361,40 @@ fields, the files a build writes and their spec ids, the environment, and what t
|
|
|
322
361
|
package relies on in rig-c — and what is not, is [docs/STABILITY.md](docs/STABILITY.md).
|
|
323
362
|
Coming from 0.16.0: [docs/MIGRATION.md](docs/MIGRATION.md).
|
|
324
363
|
|
|
364
|
+
`--write-config <config.json>` saves the three proposals above into an existing
|
|
365
|
+
regular config file: `seethrough.head_box`, `assemble.plan` plus
|
|
366
|
+
`assemble.extend_below_crop`, or the present config sections of an ordinary
|
|
367
|
+
proposal. For `assemble --propose-plan`, the destination must name `--config`
|
|
368
|
+
itself; a different destination is a usage error. Other fields and absent optional
|
|
369
|
+
sections remain; proposal notes are not copied. Changed whole values are spliced
|
|
370
|
+
into the original text, preserving untouched bytes. Stage loaders validate the candidate, including `proposed_rig_scale`,
|
|
371
|
+
before atomic replacement. Relative paths keep the config's directory as their
|
|
372
|
+
base. Changes are listed on stderr; the head-box and plan JSON on stdout remain.
|
|
373
|
+
An identical update leaves bytes and mtime untouched. Without the flag no config
|
|
374
|
+
is changed. This commits the config file, not a transaction across the proposal
|
|
375
|
+
artifacts.
|
|
376
|
+
|
|
377
|
+
With `--cloth <config.json>`, the flag requires a **new** destination and applies
|
|
378
|
+
the recipe's entry ownership: recipe-owned entries are spliced into the cloth
|
|
379
|
+
config while hand entries and unrelated text survive; conflicts are refused.
|
|
380
|
+
The destination must also be separate from all four artifacts under `--out`:
|
|
381
|
+
`proposal.json`, `basis.json`, `render/landmarks.png`, `render/landmarks_head.png`.
|
|
382
|
+
Aliases, including directory symlinks and hardlinks, count as collisions too.
|
|
383
|
+
Fresh output is published without overwriting a file created while the command
|
|
384
|
+
runs. Its already-checked splice text may be a partial config; it is not passed
|
|
385
|
+
through the full-config loader. If removing the staged temporary fails after
|
|
386
|
+
publication, the error may accompany an already-published complete config.
|
|
387
|
+
`--from <previous.json>` retains the previous-recipe comparison.
|
|
388
|
+
`--from-config` proposes nothing and cannot be combined with `--write-config`.
|
|
389
|
+
[AUTHORING §4](docs/AUTHORING.md#4-the-command-order) describes all four modes.
|
|
390
|
+
|
|
325
391
|
## What it does not do
|
|
326
392
|
|
|
327
393
|
These are limits of the approach, stated so nobody reads more into a green run:
|
|
328
394
|
|
|
395
|
+
- **rig-parts generates no images.** The painting and See-through layers are made
|
|
396
|
+
by models the user runs on their GPU or a web demo. No weights ship here; the
|
|
397
|
+
optional `comfy` adapter drives the user's own ComfyUI host and models.
|
|
329
398
|
- **One depth value per layer.** See-through gives each layer a single depth, so a
|
|
330
399
|
layer that is in front of another in one place and behind it in another is drawn
|
|
331
400
|
right in the still (the painting's own pixels are projected onto it) and wrong once
|
|
@@ -354,7 +423,10 @@ These are limits of the approach, stated so nobody reads more into a green run:
|
|
|
354
423
|
file of joints comes from any pose
|
|
355
424
|
estimator or a hand; this package does not estimate a pose itself, and its
|
|
356
425
|
keypoint support has been measured on generated figures and on files read off the
|
|
357
|
-
examples' own configs,
|
|
426
|
+
examples' own configs, and on one reported estimator output: RTMPose whole-body
|
|
427
|
+
through rtmlib on one painting built green with the estimated file unedited
|
|
428
|
+
([#356](https://github.com/rig-c/parts/issues/356)). That is one measurement,
|
|
429
|
+
not an estimator success rate; it has not been re-run here.
|
|
358
430
|
- **The proposer reads tags, not pictures.** A swinging element painted inside another
|
|
359
431
|
layer (a sash tail in the skirt), hair that is none of the shapes it knows, and
|
|
360
432
|
whether an accessory swings are the corrector's to add. The one accessory shape it
|
package/cli.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { type AlternateCliInput, assembleStage, build, checkStage, ERROR_MAP_FIL
|
|
|
20
20
|
import { ACCEPTABLE_BARS, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, rigcRunnerAt, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, rigcRanClause, type RigcRunner, NO_PARTS_SENTENCE, SEAM_MEAN_BAR, SOURCE_SENTENCE, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
|
|
21
21
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
22
22
|
import { type CharacterConfig, loadClothConfig, loadConfig, loadEarlyConfig, loadPaintConfig } from './src/config.ts';
|
|
23
|
+
import { configWriteLines, readConfigWriteTarget, separateConfigWriteTarget, stageConfigWrite, type StagedConfigWrite } from './tools/configwrite.ts';
|
|
23
24
|
import { failLine, guidePointer, PartsError } from './src/errors.ts';
|
|
24
25
|
import { GUIDE_DOC, type GuideAnswer, guideRule, guideSection } from './src/guide.ts';
|
|
25
26
|
import { proposeHeadBox } from './src/headbox.ts';
|
|
@@ -27,8 +28,8 @@ import { makeInputs } from './src/inputs.ts';
|
|
|
27
28
|
import { idleKeysOf, isIdleKeys } from './src/rig.ts';
|
|
28
29
|
import { depthSkipLine, exportLayers, figuresPhrase, hiddenLayerLine, implausibleRules, layerFigures, type LayerSet, layerSetDiff, pct, readLayers, readWrapperLayers, ruleSummary, templateLayerNames, templatePsdBytes, times } from './src/layers.ts';
|
|
29
30
|
import { checkImageSize, choosePerson, KEYPOINTS_SPEC, loadKeypoints, type RigJoints, toRigJoints } from './src/keypoints.ts';
|
|
30
|
-
import { basisLines, branchSqueezeLine, checkProposal, compare, compareLines, DEFAULT_GRID_RULE, draftTurn, drawLandmarks, FIGURE_GRID_DIVISOR, FRINGE_STEP_PX, GRID_RULES, type GridRule, HIP_MIN_FRACTION, type DraftLintFinding, lint, lintLine, type PartSet, proposalAsConfig, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
|
|
31
|
-
import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, serializeBasis } from './src/diagnostics.ts';
|
|
31
|
+
import { basisLines, branchSqueezeLine, checkProposal, compare, compareLines, DEFAULT_GRID_RULE, draftTurn, drawLandmarks, FIGURE_GRID_DIVISOR, FRINGE_STEP_PX, GRID_RULES, type GridRule, HIP_MIN_FRACTION, type DraftLintFinding, lint, lintLine, type PartSet, PROPOSAL_CONFIG_SECTIONS, proposalAsConfig, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
|
|
32
|
+
import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, legChainChoice, serializeBasis } from './src/diagnostics.ts';
|
|
32
33
|
import { BEND_DEGREES, BEND_WORST_SHARE, DUST_MAX_PX, DUST_MAX_SHARE, IDLE_BOUND_PX, MESH_DRAFT_REPORT, MESH_DRAFT_SPEC, meshDraftStage, MORE_TRIANGLES_FLAG, relativeConfigPaths, TOLERANCE_FIGURE_DIVISOR } from './src/meshdraft.ts';
|
|
33
34
|
import { meshStatsStage } from './src/meshstats.ts';
|
|
34
35
|
import { chainsLeftBehind, clothConfigEdits, editPaths, plainCount, previousCount, writtenClothConfig } from './src/clothconfig.ts';
|
|
@@ -269,13 +270,24 @@ usage:
|
|
|
269
270
|
coverage lines after them; no ${BASIS_FILE}, as nothing is proposed.
|
|
270
271
|
Exits 1 when any LINT line is printed.
|
|
271
272
|
|
|
273
|
+
rig-parts propose … --write-config <config.json>
|
|
274
|
+
Without --cloth, update an existing config with the whole proposal's
|
|
275
|
+
present config sections, including proposed_rig_scale; notes stay in the
|
|
276
|
+
proposal. Absent optional sections and all other fields are kept. The
|
|
277
|
+
full config loader validates a same-directory candidate before atomic
|
|
278
|
+
replacement; relative paths resolve beside the config. An equal update
|
|
279
|
+
keeps the original bytes and mtime. A changed source, an artifact alias
|
|
280
|
+
or a failed write is refused by name. Change lines go to stderr.
|
|
281
|
+
|
|
272
282
|
rig-parts propose --head-box --full <dir | layers.json | file.psd> --canvas <W>x<H> [--no-back-hair]
|
|
283
|
+
[--write-config <config.json>]
|
|
273
284
|
Propose seethrough.head_box (source px, square) from the full run's
|
|
274
285
|
layers for a painting of WxH px (portrait or landscape: the full run is
|
|
275
286
|
mapped back through the pad inputs added), held inside the painting; a
|
|
276
287
|
shift is printed when one was needed. When "back hair" contributes, prints
|
|
277
288
|
the box without it and how much of the side and area it took;
|
|
278
|
-
--no-back-hair leaves "back hair" out of the box.
|
|
289
|
+
--no-back-hair leaves "back hair" out of the box. --write-config updates
|
|
290
|
+
only seethrough.head_box in an existing config through the layers loader.
|
|
279
291
|
|
|
280
292
|
rig-parts compare --left <file> --right <file> [--map <bonemap.json>]
|
|
281
293
|
Compare two skeletons bone by bone. Each file is a config, a proposal.json
|
|
@@ -697,7 +709,7 @@ usage:
|
|
|
697
709
|
${DEFAULT_PROJECT_RULE}.
|
|
698
710
|
|
|
699
711
|
rig-parts assemble --propose-plan --source <painting.png> --full <dir|psd>
|
|
700
|
-
[--head <dir|psd>] --config <config.json>
|
|
712
|
+
[--head <dir|psd>] --config <config.json> [--write-config <config.json>]
|
|
701
713
|
Print {plan, extend_below_crop, notes} for config.assemble, from the two
|
|
702
714
|
runs. A layer \`layers\` WARNs about (PLAN_LAYER_TRANSLUCENT,
|
|
703
715
|
PLAN_LAYER_BACKGROUND, PLAN_LAYER_OVERSIZED) is not proposed, and a note
|
|
@@ -709,7 +721,8 @@ usage:
|
|
|
709
721
|
config.seethrough.resolution and config.assemble.rig_scale — the rest of
|
|
710
722
|
the config need not exist yet. With no --head, the one run is held as
|
|
711
723
|
assemble holds it and every row is written from it, and the first note
|
|
712
|
-
says so.
|
|
724
|
+
says so. --write-config must name --config itself; it updates the whole
|
|
725
|
+
assemble.plan and assemble.extend_below_crop through the assemble loader.
|
|
713
726
|
|
|
714
727
|
rig-parts inputs --source <painting.png> --config <config.json> --out <dir>
|
|
715
728
|
Cut the two images See-through is fed: <out>/st_input_full.png (the
|
|
@@ -975,7 +988,7 @@ const TAKES = {
|
|
|
975
988
|
compare: { valued: ['--left', '--right', '--map'], switches: [] },
|
|
976
989
|
check: { valued: ['--rig', '--out', '--parts', '--source', '--page-edges', '--pack-shape', '--requirements', '--set', '--environment'], switches: ['--seam-pairs', '--keep-stills', '--all-sets'] },
|
|
977
990
|
loop: { valued: ['--frames', '--out'], switches: ['--palette'] },
|
|
978
|
-
assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt'], switches: ['--propose-plan'] },
|
|
991
|
+
assemble: { valued: ['--source', '--full', '--head', '--config', '--out', '--seam', '--project', '--alt', '--write-config'], switches: ['--propose-plan'] },
|
|
979
992
|
'comfy seethrough': { valued: ['--image', '--out', '--resolution', '--steps', '--seed', '--prefix', '--host', '--wait', '--timeout', '--poll'], switches: ['--offload', '--lama', '--nf4'] },
|
|
980
993
|
'comfy paint': { valued: ['--config', '--out', '--seeds', '--seed0', '--host', '--wait', '--timeout', '--poll'], switches: [] },
|
|
981
994
|
inputs: { valued: ['--source', '--config', '--out'], switches: [] },
|
|
@@ -1267,7 +1280,7 @@ function parseCanvas(v: string): { w: number; h: number } | null {
|
|
|
1267
1280
|
return m === null ? null : { w: Number(m[1]), h: Number(m[2]) };
|
|
1268
1281
|
}
|
|
1269
1282
|
|
|
1270
|
-
function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes: CharacterConfig['meshes']; regions: CharacterConfig['regions']; motion: CharacterConfig['motion']; constraints?: CharacterConfig['constraints'] }, joints?: RigJoints, drafted: readonly DraftLintFinding[] = []): number {
|
|
1283
|
+
function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes: CharacterConfig['meshes']; regions: CharacterConfig['regions']; motion: CharacterConfig['motion']; constraints?: CharacterConfig['constraints']; limbs?: CharacterConfig['limbs']; notes?: readonly string[] }, joints?: RigJoints, drafted: readonly DraftLintFinding[] = []): number {
|
|
1271
1284
|
const res = lint(P, spec, joints, drafted);
|
|
1272
1285
|
// Under --keypoints only: which rule set read the torso and the chains, and on what basis.
|
|
1273
1286
|
if (res.basis !== undefined) for (const l of basisLines(res.basis)) console.log(l);
|
|
@@ -1285,7 +1298,7 @@ function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes:
|
|
|
1285
1298
|
}
|
|
1286
1299
|
console.log(`${res.findings.length} LINT line(s) over ${Object.keys(spec.meshes).length - res.unknownMeshes.length} mesh(es) and the hip`);
|
|
1287
1300
|
// Issue #86: after every line above, which check read each bone, or why none did.
|
|
1288
|
-
for (const l of coverageLines(res.coverage)) console.log(l);
|
|
1301
|
+
for (const l of coverageLines(res.coverage, legChainChoice(P, spec, joints))) console.log(l);
|
|
1289
1302
|
return res.findings.length;
|
|
1290
1303
|
}
|
|
1291
1304
|
|
|
@@ -1311,17 +1324,24 @@ function cmdPropose(args: string[]): number {
|
|
|
1311
1324
|
flags.set(flag, value);
|
|
1312
1325
|
i++;
|
|
1313
1326
|
}
|
|
1327
|
+
let staged: StagedConfigWrite | null = null;
|
|
1314
1328
|
try {
|
|
1315
1329
|
if (headBox) {
|
|
1316
|
-
const stray = [...flags.keys()].filter((f) => f !== '--full' && f !== '--canvas');
|
|
1317
|
-
if (stray.length > 0) return usage(`--head-box takes --full and --
|
|
1330
|
+
const stray = [...flags.keys()].filter((f) => f !== '--full' && f !== '--canvas' && f !== '--write-config');
|
|
1331
|
+
if (stray.length > 0) return usage(`--head-box takes --full, --canvas and optional --write-config only; got ${stray.join(', ')}`);
|
|
1318
1332
|
const full = flags.get('--full');
|
|
1319
1333
|
const canvasArg = flags.get('--canvas');
|
|
1320
1334
|
if (full === undefined) return usage('--head-box needs --full <dir | layers.json | file.psd>, the full run\'s layers');
|
|
1321
1335
|
if (canvasArg === undefined) return usage('--head-box needs --canvas <W>x<H>, the painting\'s size in px');
|
|
1322
1336
|
const canvas = parseCanvas(canvasArg);
|
|
1323
1337
|
if (canvas === null) return usage(`--canvas ${canvasArg} is not <W>x<H> in integer px`);
|
|
1338
|
+
const target = flags.has('--write-config') ? readConfigWriteTarget(flags.get('--write-config') as string) : null;
|
|
1324
1339
|
const r = proposeHeadBox(readLayers(full), canvas, { backHair: !noBackHair });
|
|
1340
|
+
if (target !== null) {
|
|
1341
|
+
staged = stageConfigWrite(target, { kind: 'head-box', head_box: r.head_box });
|
|
1342
|
+
staged.commit();
|
|
1343
|
+
for (const line of configWriteLines(target, staged.changes)) console.error(line);
|
|
1344
|
+
}
|
|
1325
1345
|
console.log(`rig-parts propose --head-box: ${full}`);
|
|
1326
1346
|
console.log(` painting ${canvas.w}x${canvas.h}; head layers ${r.layers.map(([n, c]) => `${n} ${c} px`).join(', ')}`);
|
|
1327
1347
|
if (r.shift[0] !== 0 || r.shift[1] !== 0) {
|
|
@@ -1372,16 +1392,21 @@ function cmdPropose(args: string[]): number {
|
|
|
1372
1392
|
// Issue #537: the --cloth config's own text with what its recipe drafts spliced in, beside proposal.json.
|
|
1373
1393
|
const writeArg = flags.get('--write-config');
|
|
1374
1394
|
if (writeArg !== undefined && flags.has('--from-config')) return usage('--write-config writes what the recipe drafts into its config; --from-config proposes nothing');
|
|
1375
|
-
if (writeArg !== undefined && clothArg === undefined) return usage('--write-config writes the --cloth config with what its recipe drafts spliced in; give --cloth <config.json>');
|
|
1376
1395
|
// Issue #571: the config the recipe is changed from; an entry equal to its recipe's draft is the recipe's, not a hand edit.
|
|
1377
1396
|
const fromArg = flags.get('--from');
|
|
1378
1397
|
if (fromArg !== undefined && writeArg === undefined) return usage('--from names the config the recipe is changed from, for --write-config; give --write-config <out.json>');
|
|
1379
|
-
if (
|
|
1380
|
-
|
|
1398
|
+
if (fromArg !== undefined && clothArg === undefined) return usage('--from names a previous cloth recipe; give --cloth <config.json>');
|
|
1399
|
+
if (writeArg !== undefined && clothArg !== undefined) {
|
|
1400
|
+
const ours = ['proposal.json', BASIS_FILE, 'render/landmarks.png', 'render/landmarks_head.png'].map((f) => resolve(out, f));
|
|
1381
1401
|
if (existsSync(writeArg) || ours.includes(resolve(writeArg))) {
|
|
1382
1402
|
throw new PartsError([{ code: 'PROPOSE_WRITE_CONFIG_FRESH', object: writeArg, detail: existsSync(writeArg) ? 'exists; the config is written to a new file, never over the --cloth config or an earlier one' : `is a file this run writes under ${out}; name another file` }]);
|
|
1383
1403
|
}
|
|
1404
|
+
separateConfigWriteTarget({ path: resolve(writeArg) }, ours, 'PROPOSE_WRITE_CONFIG_FRESH');
|
|
1384
1405
|
}
|
|
1406
|
+
// The existing cloth writer keeps its fresh-file, per-entry splice contract.
|
|
1407
|
+
// Without --cloth, #356 updates only an existing config through the stage loader.
|
|
1408
|
+
const target = writeArg === undefined || clothArg !== undefined ? null : readConfigWriteTarget(writeArg);
|
|
1409
|
+
if (target !== null) separateConfigWriteTarget(target, [join(out, 'proposal.json'), join(out, BASIS_FILE), join(out, 'render', 'landmarks.png'), join(out, 'render', 'landmarks_head.png')]);
|
|
1385
1410
|
const cloth = clothArg === undefined ? null : loadClothConfig(clothArg).cloth;
|
|
1386
1411
|
const previousCloth = fromArg === undefined ? null : loadClothConfig(fromArg).cloth;
|
|
1387
1412
|
const partsDir = existsSync(partsArg) && statSync(partsArg).isFile() ? dirname(partsArg) : partsArg;
|
|
@@ -1455,14 +1480,16 @@ function cmdPropose(args: string[]): number {
|
|
|
1455
1480
|
})();
|
|
1456
1481
|
const bfile = basisFile(P, prop, basis, cfg === null || cmpPath === undefined ? null : { where: cmpPath, config: cfg });
|
|
1457
1482
|
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 });
|
|
1484
|
+
else if (written !== null) staged = stageConfigWrite({ kind: 'fresh', path: written.at, text: written.text });
|
|
1458
1485
|
mkdirSync(render, { recursive: true });
|
|
1459
1486
|
writeFileSync(join(out, 'proposal.json'), serializeProposal(prop));
|
|
1460
1487
|
writeFileSync(join(out, BASIS_FILE), serializeBasis(bfile));
|
|
1461
1488
|
writePng(join(render, 'landmarks.png'), img.full);
|
|
1462
1489
|
if (img.head !== null) writePng(join(render, 'landmarks_head.png'), img.head);
|
|
1463
|
-
if (
|
|
1464
|
-
|
|
1465
|
-
|
|
1490
|
+
if (staged !== null) {
|
|
1491
|
+
staged.commit();
|
|
1492
|
+
if (target !== null) for (const line of configWriteLines(target, staged.changes)) console.error(line);
|
|
1466
1493
|
}
|
|
1467
1494
|
console.log(`wrote proposal.json (${prop.bones.length} bone entries, ${Object.keys(prop.meshes).length} meshes) and render/landmarks.png (+_head) under ${out}`);
|
|
1468
1495
|
for (const n of prop.notes) console.log(`note: ${n}`);
|
|
@@ -1484,7 +1511,12 @@ function cmdPropose(args: string[]): number {
|
|
|
1484
1511
|
if (cfg !== null) for (const l of compareLines(compare(prop.bones, cfg.bones))) console.log(l);
|
|
1485
1512
|
return EXIT_OK;
|
|
1486
1513
|
} catch (err) {
|
|
1514
|
+
if (staged !== null) {
|
|
1515
|
+
try { staged.abort(err); } catch (combined) { return printRefusal(combined); }
|
|
1516
|
+
}
|
|
1487
1517
|
return printRefusal(err);
|
|
1518
|
+
} finally {
|
|
1519
|
+
staged?.cancel();
|
|
1488
1520
|
}
|
|
1489
1521
|
}
|
|
1490
1522
|
|
|
@@ -1566,6 +1598,7 @@ function cmdAssemble(args: string[]): number {
|
|
|
1566
1598
|
// --head is optional (issue #359): without it the full run is the one run, and it must hold the face.
|
|
1567
1599
|
for (const f of ['--source', '--full', '--config']) if (!flags.has(f)) return usage(`assemble needs ${f}`);
|
|
1568
1600
|
if (propose && (flags.has('--out') || flags.has('--seam') || flags.has('--project') || alts.length > 0)) return usage('--propose-plan prints to the console; it takes none of --out, --seam, --project, --alt');
|
|
1601
|
+
if (!propose && flags.has('--write-config')) return usage('--write-config belongs to assemble --propose-plan');
|
|
1569
1602
|
if (!propose && !flags.has('--out')) return usage('assemble needs --out <dir>');
|
|
1570
1603
|
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
1571
1604
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
@@ -1573,13 +1606,22 @@ function cmdAssemble(args: string[]): number {
|
|
|
1573
1606
|
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
1574
1607
|
const [source, full, config] = ['--source', '--full', '--config'].map((f) => flags.get(f) as string);
|
|
1575
1608
|
const head = flags.get('--head') ?? null;
|
|
1609
|
+
const writePath = flags.get('--write-config');
|
|
1610
|
+
if (writePath !== undefined && resolve(writePath) !== resolve(config)) return usage('--write-config must name the same file as --config for assemble --propose-plan');
|
|
1611
|
+
let staged: StagedConfigWrite | null = null;
|
|
1576
1612
|
try {
|
|
1577
1613
|
if (propose) {
|
|
1614
|
+
const target = writePath === undefined ? null : readConfigWriteTarget(writePath);
|
|
1578
1615
|
const src = readSource(source);
|
|
1579
1616
|
const runs = readRuns(full, head);
|
|
1580
1617
|
const g = proposeFields(loadEarlyConfig(config, 'layers'), runs.single ? { w: src.width, h: src.height } : null);
|
|
1581
1618
|
const geometry = { sourceW: src.width, sourceH: src.height, ...g };
|
|
1582
1619
|
const proposal = runs.single ? proposeSinglePlan(runs.full, geometry) : proposePlan(runs.full, runs.head, geometry);
|
|
1620
|
+
if (target !== null) {
|
|
1621
|
+
staged = stageConfigWrite(target, { kind: 'assemble-plan', plan: proposal.plan, extend_below_crop: proposal.extend_below_crop });
|
|
1622
|
+
staged.commit();
|
|
1623
|
+
for (const line of configWriteLines(target, staged.changes)) console.error(line);
|
|
1624
|
+
}
|
|
1583
1625
|
console.log(JSON.stringify(proposal, null, 2));
|
|
1584
1626
|
return EXIT_OK;
|
|
1585
1627
|
}
|
|
@@ -1591,7 +1633,12 @@ function cmdAssemble(args: string[]): number {
|
|
|
1591
1633
|
);
|
|
1592
1634
|
return EXIT_OK;
|
|
1593
1635
|
} catch (err) {
|
|
1636
|
+
if (staged !== null) {
|
|
1637
|
+
try { staged.abort(err); } catch (combined) { return printRefusal(combined); }
|
|
1638
|
+
}
|
|
1594
1639
|
return printRefusal(err);
|
|
1640
|
+
} finally {
|
|
1641
|
+
staged?.cancel();
|
|
1595
1642
|
}
|
|
1596
1643
|
}
|
|
1597
1644
|
|