rig-parts 1.83.0 → 1.83.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -143
- package/docs/AUTHORING.md +206 -4
- package/docs/STABILITY.md +15 -2
- package/package.json +2 -1
- package/src/assemble.ts +38 -8
- package/src/authoredunit.ts +61 -0
- package/src/build.ts +1 -0
- package/src/parts.ts +18 -5
- package/src/provenance.ts +4 -1
- package/src/takelayers.ts +19 -2
package/README.md
CHANGED
|
@@ -184,26 +184,7 @@ pads a non-square upload before resizing; the centre-crop reported in
|
|
|
184
184
|
`st_input_full.png` supplies the explicit white padding that rig-parts maps back
|
|
185
185
|
to the original painting. This is a source inspection, not a new demo run.
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
already separates the head in a second stage of its own: it crops the head the
|
|
189
|
-
first stage found, runs the face tags on that crop at the same `resolution`, and
|
|
190
|
-
pastes the result back **resized to the full image's scale**. So the face comes
|
|
191
|
-
back at the full run's pixel density whichever route ran it, and the separate
|
|
192
|
-
head run on `st_input_head.png` is what gives the eyes more pixels
|
|
193
|
-
(read on 2026-09-28 at see-through `a25a549` and wrapper 0.5.0 `98d754b`):
|
|
194
|
-
|
|
195
|
-
- *Hugging Face Space* — the same two-stage code, with `resolution` capped at
|
|
196
|
-
1280 (768 by default): the head run is still needed.
|
|
197
|
-
- *ModelScope demo* — its code was not read, so what the head run adds on that
|
|
198
|
-
route has not been measured.
|
|
199
|
-
- *Upstream CLI* — the `.psd` frame is the `resolution`-sided square the first
|
|
200
|
-
stage ran on (1280 by default), and the head stage is pasted into it at that
|
|
201
|
-
scale: the head run is still needed.
|
|
202
|
-
- *ComfyUI wrapper* — `SeeThrough_GenerateLayers` runs both stages with no input
|
|
203
|
-
to change the second, at most `resolution` 2048, which on the examples' 2432 px
|
|
204
|
-
square is 1.19 source px per layer px against 0.77 and 0.47 for their head
|
|
205
|
-
crops; at the 1024 the examples ran, the full run's right eye white is 22×13 px
|
|
206
|
-
and the head run's 60×38: the head run is still needed.
|
|
187
|
+
The [head-run source reading](docs/AUTHORING.md#see-through-head-run-source-reading) explains why a separate head crop is needed.
|
|
207
188
|
|
|
208
189
|
**See-through is required; ComfyUI is not.** The wrapper is one route among
|
|
209
190
|
four, and `rig-parts comfy` is only a convenience for that route: every
|
|
@@ -329,86 +310,29 @@ agent skill.
|
|
|
329
310
|
|
|
330
311
|
## Commands
|
|
331
312
|
|
|
313
|
+
Command details and config saving are in the [command reference](docs/AUTHORING.md#command-reference) and [stable interface](docs/STABILITY.md#commands-and-flags).
|
|
314
|
+
|
|
332
315
|
| command | does |
|
|
333
316
|
| --- | --- |
|
|
334
|
-
| `inputs
|
|
335
|
-
| `comfy
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
338
|
-
| `
|
|
339
|
-
| `
|
|
340
|
-
| `
|
|
341
|
-
| `
|
|
342
|
-
| `
|
|
343
|
-
| `
|
|
344
|
-
| `
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
|
|
353
|
-
| `mesh-draft --config <json> --parts <dir> --out <dir>` | draft contour meshes for configured lattices, keeping each only where it holds against its lattice through rig-c; write the revised config and measurement report |
|
|
354
|
-
| `mesh-stats --config <json> --parts <dir>` | print each configured mesh's vertex and triangle counts and their total through the rig stage's construction; write nothing |
|
|
355
|
-
| `painting --keypoints <json> [--person <id>] <png>` | before See-through, read the painting against its declared joints and refuse named background, joint or limb-gap problems; write nothing |
|
|
356
|
-
| `remesh --rig <dir> --config <json> --parts <dir> --part <name> --out <dir>` | with exactly one of `--mesh <entry.json>` or `--delta <mesh-edit.json>`, re-fit one part of a gated rig into a fresh revision, preserving the input |
|
|
357
|
-
| `retune --rig <dir> --config <json> --parts <dir> --out <dir>` | splice the config's constraints and idle onto a gated rig in a fresh revision, carrying its attachments over as bytes |
|
|
358
|
-
| `guide <RULE \| §<n> \| §<n>.<m> \| §<n>.<m>.<k>>` | print the one row of [docs/AUTHORING.md](docs/AUTHORING.md) that names a rule (a refusal and the bar it enforces, `CHECK_<LINE>` and `<LINE>`, print together), or one section, `###` subsection or `####` under it, read from the installed file when it runs |
|
|
359
|
-
|
|
360
|
-
`rig-parts --help` has every flag. Exit codes: 0 done, 1 input refused (every
|
|
361
|
-
reason is a FAIL line), 2 a usage error or a command this version does not implement.
|
|
362
|
-
Every FAIL line ends with `(see rig-parts guide <RULE>)`, the command that prints the
|
|
363
|
-
guide's row for that rule, and each row is anchored on the web as
|
|
364
|
-
`docs/AUTHORING.md#<RULE>`.
|
|
365
|
-
|
|
366
|
-
What 1.0.0 holds stable — these commands and their flags, the config
|
|
367
|
-
fields, the files a build writes and their spec ids, the environment, and what this
|
|
368
|
-
package relies on in rig-c — and what is not, is [docs/STABILITY.md](docs/STABILITY.md).
|
|
369
|
-
Coming from 0.16.0: [docs/MIGRATION.md](docs/MIGRATION.md).
|
|
370
|
-
|
|
371
|
-
`--write-config <config.json>` saves the proposals above into an existing
|
|
372
|
-
regular config file: `seethrough.head_box`, `assemble.plan` plus
|
|
373
|
-
`assemble.extend_below_crop`, or the present config sections of an ordinary
|
|
374
|
-
proposal. The [neck draft](docs/AUTHORING.md#a-neck-that-moves-on-its-own)
|
|
375
|
-
(`propose … --full --head --write-config`) is another writer: for an existing
|
|
376
|
-
`turn.body` config with one neck plan row, it selects the full-run neck, drafts a
|
|
377
|
-
rigid unit and its depth, and reassembles parts under `<out>/rig`.
|
|
378
|
-
For `assemble --propose-plan`, the destination must name `--config`
|
|
379
|
-
itself; a different destination is a usage error. Other fields and absent optional
|
|
380
|
-
sections remain; proposal notes are not copied. Changed whole values are spliced
|
|
381
|
-
into the original text, preserving untouched bytes. Stage loaders validate the candidate, including `proposed_rig_scale`,
|
|
382
|
-
before atomic replacement. Relative paths keep the config's directory as their
|
|
383
|
-
base. Changes are listed on stderr; the head-box and plan JSON on stdout remain.
|
|
384
|
-
An identical update leaves bytes and mtime untouched. Without the flag no config
|
|
385
|
-
is changed. This commits the config file, not a transaction across the proposal
|
|
386
|
-
artifacts.
|
|
387
|
-
|
|
388
|
-
With `--cloth <config.json>`, the flag requires a **new** destination and applies
|
|
389
|
-
the recipe's entry ownership: recipe-owned entries are spliced into the cloth
|
|
390
|
-
config while hand entries and unrelated text survive; conflicts are refused.
|
|
391
|
-
The destination must also be separate from all four artifacts under `--out`:
|
|
392
|
-
`proposal.json`, `basis.json`, `render/landmarks.png`, `render/landmarks_head.png`.
|
|
393
|
-
Aliases, including directory symlinks and hardlinks, count as collisions too.
|
|
394
|
-
Fresh output is published without overwriting a file created while the command
|
|
395
|
-
runs. Its already-checked splice text may be a partial config; it is not passed
|
|
396
|
-
through the full-config loader. If removing the staged temporary fails after
|
|
397
|
-
publication, the error may accompany an already-published complete config.
|
|
398
|
-
`--from <previous.json>` retains the previous-recipe comparison.
|
|
399
|
-
With `--only-missing` (issue #671), an ordinary proposal's `--write-config` adds
|
|
400
|
-
only `meshes.<part>` or `regions.<part>` for each proposed part the config
|
|
401
|
-
attaches by neither, entry by entry, and, for a part the 1.37.0 arm split makes
|
|
402
|
-
(`full:drape-<s>`, `full:hand-<s>`) that `assemble.plan` does not name, its plan
|
|
403
|
-
row beside the arm's row; every existing row and entry and every other byte
|
|
404
|
-
stays as read, and nothing else of the proposal is written. The candidate passes
|
|
405
|
-
the full loader first, so an entry on a bone the config does not declare is
|
|
406
|
-
refused naming the part and the bone; nothing missing leaves bytes and mtime
|
|
407
|
-
untouched. It is the upgrade path for a config proposed before the 1.37.0 arm
|
|
408
|
-
split ([MIGRATION](docs/MIGRATION.md#after-1370-merged-arm-layers-are-split-issue-490)),
|
|
409
|
-
and a usage error with `--head-box`, `--cloth` or `--from-config`.
|
|
410
|
-
`--from-config` proposes nothing and cannot be combined with `--write-config`.
|
|
411
|
-
[AUTHORING §4](docs/AUTHORING.md#4-the-command-order) describes all four modes.
|
|
317
|
+
| `inputs` | [Prepare the two See-through input images](docs/AUTHORING.md#command-reference) |
|
|
318
|
+
| `comfy` | [Drive an optional ComfyUI painting or layer job](docs/AUTHORING.md#command-reference) |
|
|
319
|
+
| `layers` | [Inspect wrapper layers or a PSD](docs/AUTHORING.md#command-reference) |
|
|
320
|
+
| `sheet` | [Make a contact sheet](docs/AUTHORING.md#command-reference) |
|
|
321
|
+
| `assemble` | [Merge layers into measured parts](docs/AUTHORING.md#command-reference) |
|
|
322
|
+
| `propose` | [Draft or inspect a character config](docs/AUTHORING.md#command-reference) |
|
|
323
|
+
| `compare` | [Compare two skeletons by name](docs/AUTHORING.md#command-reference) |
|
|
324
|
+
| `rig` | [Author gated rig and motion specs](docs/AUTHORING.md#command-reference) |
|
|
325
|
+
| `check` | [Build, render and measure a rig](docs/AUTHORING.md#command-reference) |
|
|
326
|
+
| `loop` | [Encode rendered frames as GIF or APNG](docs/AUTHORING.md#command-reference) |
|
|
327
|
+
| `build` | [Assemble, rig and check in one process](docs/AUTHORING.md#command-reference) |
|
|
328
|
+
| `compose` | [Combine character builds into a scene](docs/AUTHORING.md#command-reference) |
|
|
329
|
+
| `mesh-draft` | [Draft and compare contour meshes](docs/AUTHORING.md#command-reference) |
|
|
330
|
+
| `mesh-stats` | [Count mesh vertices and triangles](docs/AUTHORING.md#command-reference) |
|
|
331
|
+
| `painting` | [Check a painting against declared joints](docs/AUTHORING.md#command-reference) |
|
|
332
|
+
| `remesh` | [Revise one mesh in a gated rig](docs/AUTHORING.md#command-reference) |
|
|
333
|
+
| `retune` | [Revise constraints and idle in a gated rig](docs/AUTHORING.md#command-reference) |
|
|
334
|
+
| `guide` | [Read the installed authoring reference](docs/AUTHORING.md#command-reference) |
|
|
335
|
+
|
|
412
336
|
|
|
413
337
|
## What it does not do
|
|
414
338
|
|
|
@@ -539,15 +463,7 @@ selftest and CI do; `check.json`'s `rigc_entry` records which one gated a build.
|
|
|
539
463
|
|
|
540
464
|
## Licence posture
|
|
541
465
|
|
|
542
|
-
|
|
543
|
-
product integrates a Spine Runtime to play that data, the integration is under Esoteric
|
|
544
|
-
Software's Spine Runtimes License: permitted under Section 2 of the Spine Editor License
|
|
545
|
-
Agreement, or otherwise on the condition that each user of the product obtains their own
|
|
546
|
-
Spine Editor license. What those terms turn on is the integration, as rig-c's
|
|
547
|
-
[NOTICE.md](https://github.com/rig-c/core/blob/main/NOTICE.md) states it, not the data as
|
|
548
|
-
a file. Neither package installs `spine-core`; this repository uses it in development,
|
|
549
|
-
for the round trip in its selftest and CI. [NOTICE.md](NOTICE.md) records that chain and every other
|
|
550
|
-
third-party term this package touches, See-through's included.
|
|
466
|
+
MIT; see [NOTICE.md](NOTICE.md) for third-party terms and the Spine runtime licence posture.
|
|
551
467
|
|
|
552
468
|
## Development
|
|
553
469
|
|
|
@@ -579,43 +495,14 @@ records the generated default-cap case and its preservation controls.
|
|
|
579
495
|
practice, [RELEASING.md](RELEASING.md) the cut, [docs/STABILITY.md](docs/STABILITY.md)
|
|
580
496
|
the interface it holds.
|
|
581
497
|
|
|
582
|
-
##
|
|
583
|
-
|
|
584
|
-
MIT — see [LICENSE](LICENSE). Third-party terms: [NOTICE.md](NOTICE.md).
|
|
498
|
+
## Depth-driven rigging
|
|
585
499
|
|
|
500
|
+
See the [depth-driven rigging reference](docs/AUTHORING.md#depth-driven-rigging) for sampled head and body turns and unit-depth precedence.
|
|
586
501
|
|
|
587
502
|
### Per-layer depth input
|
|
588
503
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
per-layer/run-canvas fallbacks declare image, near and zScale themselves; a
|
|
595
|
-
missing declared file is an error, not a fallback. The ordering median remains
|
|
596
|
-
separate, and its agreement reading cannot certify an image's polarity.
|
|
597
|
-
|
|
598
|
-
Assembly emits registered part sheets and references; rig emits PAD4 sheets and
|
|
599
|
-
`depth_sheets.json`. Since 1.68.0 (#777), [config.turn.roles](docs/AUTHORING.md#3-configjson-field-by-field)
|
|
600
|
-
uses depth sheets for 2.5D head turns with grid or authored weighted surfaces and `yaw_dial`/`pitch_dial`
|
|
601
|
-
controls; see [`CONFIG_TURN_ROLE_UNKNOWN`](docs/AUTHORING.md#CONFIG_TURN_ROLE_UNKNOWN)
|
|
602
|
-
and [`CONFIG_TURN_SURFACE_REQUIRED`](docs/AUTHORING.md#CONFIG_TURN_SURFACE_REQUIRED).
|
|
603
|
-
`turn.body.roles` adds independent body yaw. A weighted mesh declared as
|
|
604
|
-
`"surface"` in either group and blending head and torso bones receives both
|
|
605
|
-
fields in proportion to its skin weights. Inside the existing setup alpha
|
|
606
|
-
overlap with a single-group neighbour, a blended attachment's vertex takes
|
|
607
|
-
that neighbour's existing emitted triangle field at its setup position (#804), without changing skin weights,
|
|
608
|
-
setup or idle. A `motion.blink.still` unit neighbour supplies both emitted
|
|
609
|
-
pieces, each on its own bone and resolved depth; transparent padding does not
|
|
610
|
-
let either piece lend its field to the other's art (#807). Missing emitted
|
|
611
|
-
split support is refused by name. Conflicting neighbour groups are refused by name. Each sibling island takes only its
|
|
612
|
-
own group; parts without a blended attachment keep their existing role behavior
|
|
613
|
-
and output bytes. Unlisted meshes do not consume turn depth sheets. See the
|
|
614
|
-
[blended surface contract](docs/AUTHORING.md#per-layer-depth-sheets-773).
|
|
615
|
-
This copies the field at vertices; different triangle partitions can still
|
|
616
|
-
differ between vertices. The [corrected #804 acceptance](https://github.com/rig-c/parts/issues/804#issuecomment-6478778123)
|
|
617
|
-
checks vertex correspondence and reports those residuals per fixture; the
|
|
618
|
-
contract records the measured grid residual and preserves its seam readings.
|
|
619
|
-
No-image builds keep the legacy output bytes. `comfy seethrough --config <file> --depth-run <full|head>`
|
|
620
|
-
uses the same opt-in; its default captures art only. Standalone authored part
|
|
621
|
-
references remain active declarations. See [the input contract and refusals](docs/AUTHORING.md#per-layer-depth-sheets-773).
|
|
504
|
+
See the [per-layer depth input contract](docs/AUTHORING.md#per-layer-depth-input) for opt-in sheets, calibration and their consumers.
|
|
505
|
+
|
|
506
|
+
## Licence
|
|
507
|
+
|
|
508
|
+
MIT — see [LICENSE](LICENSE). Third-party terms: [NOTICE.md](NOTICE.md).
|
package/docs/AUTHORING.md
CHANGED
|
@@ -70,6 +70,29 @@ model stack. The wrapper README and the adapter graph use different node names
|
|
|
70
70
|
and depth-model identifiers; [the adapter notes](../README.md#the-optional-comfyui-adapter)
|
|
71
71
|
name the graph actually sent.
|
|
72
72
|
|
|
73
|
+
### See-through head-run source reading
|
|
74
|
+
|
|
75
|
+
**The head run is needed on every route that was read.** See-through's model
|
|
76
|
+
already separates the head in a second stage of its own: it crops the head the
|
|
77
|
+
first stage found, runs the face tags on that crop at the same `resolution`, and
|
|
78
|
+
pastes the result back **resized to the full image's scale**. So the face comes
|
|
79
|
+
back at the full run's pixel density whichever route ran it, and the separate
|
|
80
|
+
head run on `st_input_head.png` is what gives the eyes more pixels
|
|
81
|
+
(read on 2026-09-28 at see-through `a25a549` and wrapper 0.5.0 `98d754b`):
|
|
82
|
+
|
|
83
|
+
- *Hugging Face Space* — the same two-stage code, with `resolution` capped at
|
|
84
|
+
1280 (768 by default): the head run is still needed.
|
|
85
|
+
- *ModelScope demo* — its code was not read, so what the head run adds on that
|
|
86
|
+
route has not been measured.
|
|
87
|
+
- *Upstream CLI* — the `.psd` frame is the `resolution`-sided square the first
|
|
88
|
+
stage ran on (1280 by default), and the head stage is pasted into it at that
|
|
89
|
+
scale: the head run is still needed.
|
|
90
|
+
- *ComfyUI wrapper* — `SeeThrough_GenerateLayers` runs both stages with no input
|
|
91
|
+
to change the second, at most `resolution` 2048, which on the examples' 2432 px
|
|
92
|
+
square is 1.19 source px per layer px against 0.77 and 0.47 for their head
|
|
93
|
+
crops; at the 1024 the examples ran, the full run's right eye white is 22×13 px
|
|
94
|
+
and the head run's 60×38: the head run is still needed.
|
|
95
|
+
|
|
73
96
|
## 2. The folder
|
|
74
97
|
|
|
75
98
|
Nothing in rig-parts requires a layout; every command takes paths. The one the
|
|
@@ -668,8 +691,19 @@ Taken layers replace the target's tag, keeping the donor's tag and
|
|
|
668
691
|
`depth_median`, so existing hair/blink classification continues to read `from`.
|
|
669
692
|
Larger depth draws behind smaller depth. Numeric ties keep manifest order only
|
|
670
693
|
inside one original run. Equal or null depths **across** runs are ambiguous
|
|
671
|
-
and refused, even if the plan gives an order.
|
|
672
|
-
|
|
694
|
+
and refused, even if the plan gives an order. Plan-order checks compare only
|
|
695
|
+
different original runs' layers whose original supports intersect (both alpha
|
|
696
|
+
> 8, in the target-run canvas, without dilation). Disjoint supports have no
|
|
697
|
+
visible order; intersecting bounding boxes alone do not establish overlap.
|
|
698
|
+
Within one role band, larger depth must draw behind smaller depth. In a two-run
|
|
699
|
+
plan, full-run rows follow the proposer's head-fallback roles before scalar
|
|
700
|
+
depth: back hair, body, headwear/earwear, face, head details, front hair. Thus
|
|
701
|
+
fallback headwear is after the body and before the face, and fallback front
|
|
702
|
+
hair stays over the face even when its donor depth is larger. Roles come from
|
|
703
|
+
source tags, never part names. Head-run rows and single-run plans keep scalar
|
|
704
|
+
depth order. A supplied plan reversing an overlapping mixed-run pair under
|
|
705
|
+
these rules is refused; `--propose-plan` drafts the same order. Null/tied depth
|
|
706
|
+
ambiguity remains a refusal even for disjoint supports.
|
|
673
707
|
Replacing a layer with hidden companion art is refused because independent
|
|
674
708
|
hidden-art identity has not been declared. Active scalar depth sheets (`depth`
|
|
675
709
|
opt-in or already bound sheets) are also refused: selecting donor art does not
|
|
@@ -3597,6 +3631,90 @@ artifacts under `--out`: `proposal.json`, `basis.json`, `render/landmarks.png` a
|
|
|
3597
3631
|
existing hardlinks are refused before artifact output; the ordinary mode names
|
|
3598
3632
|
`CONFIG_WRITE_TARGET`, and the cloth mode `PROPOSE_WRITE_CONFIG_FRESH`.
|
|
3599
3633
|
|
|
3634
|
+
### Command reference
|
|
3635
|
+
|
|
3636
|
+
| command | does |
|
|
3637
|
+
| --- | --- |
|
|
3638
|
+
| `inputs --source <png> --config <json> --out <dir>` | the two images See-through is fed: the painting on a white square, and the head box's crop once the config has one |
|
|
3639
|
+
| `comfy paint --config --out [--host]` | optional: generate the painting on a ComfyUI box from the config's `generation` block |
|
|
3640
|
+
| `comfy seethrough --image --out [--host]` | optional: run the ComfyUI See-through wrapper on one image and write the form `layers` reads |
|
|
3641
|
+
| `layers <dir \| layers.json \| file.psd>` | keep the existing columns (`opaque_px` still means alpha >8) and append source-alpha boxes `drawn(a>8)` / `opaque(a>=250)` with exclusive right/bottom; name absent boxes, report the existing body-group union height at **CORE_ALPHA (>=250)** in **input-canvas px**, and show its >8 union separately; no anatomical height, ownership/erosion or painting registration is inferred ([units and rig-scale guidance](AUTHORING.md#4-the-command-order)) |
|
|
3642
|
+
| `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
|
|
3643
|
+
| `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) |
|
|
3644
|
+
| `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 |
|
|
3645
|
+
| `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 |
|
|
3646
|
+
| `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 |
|
|
3647
|
+
| `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](AUTHORING.md)) |
|
|
3648
|
+
| `propose … --full <run> --head <run> --write-config <config>` | [draft an independent neck](AUTHORING.md#a-neck-that-moves-on-its-own) in an existing sampled body turn and reassemble its full-run source under `<out>/rig` |
|
|
3649
|
+
| `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) |
|
|
3650
|
+
| `propose --parts --source --out --from-config <config> [--keypoints <file> [--person <id>]]` | draw and LINT the config's current bones (by the joints too, given `--keypoints`); proposes nothing, so takes only these flags (not `--full`, `--head`, `--cloth`, `--write-config`, `--wrist-snap`, `--grid-rule`, `--only-missing` or `--compare`) |
|
|
3651
|
+
| `compare --left <file> --right <file> [--map <bonemap.json>]` | compare two skeletons — a config, a proposal or a `rig.json` — bone by bone: origin, parent, tip, length and direction per pair, the bones neither side pairs, the required ones missing, and each side's roles read off the spec; no frame is guessed, and nothing pairs by resemblance (`propose --compare` reads origins only) |
|
|
3652
|
+
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after rig-c's gate is green; each mesh is in the mode its config entry names — `grid` (the lattice, what `propose` writes), `contour` (the traced outline with declared interior points) or `auto` (the contour mesh at alpha 1 and above, reduced and locally refined by rig-c's `reduceMesh` under the author's declared bounds, written only when its motion against the unreduced source on the idle passes the author's `motion` bounds through rig-c's `compareMeshesInMotion`; [AUTHORING §3](AUTHORING.md)); every chain link is turned along its chain and carries its `length`, so a physics constraint added later has a lever, with every offset under it in the turned frame so nothing moves; `--idle-keys` says whether the idle's keys on mesh-driving bones go through `<bone>_ctl` parents (default) or stay on the bones with `invariants.idleDrivesMeshes` declared; the config's `constraints` go to rigc as written, and a two-bone ik over keyed links needs `--idle-keys direct` (on `rig` or `build`) |
|
|
3653
|
+
| `check --rig --out [--parts] [--source] [--requirements]` | build packed, gated under `rig-play`, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json`; on a rig spec with no `parts.json` or no `idle` (a merged rig, issue #77) the gate still runs and every line that needs the missing input says SKIP by name; `--source <painting.png>` adds `SETUP_POSE_VS_SOURCE`, the setup pose against the painting by assemble's recomposite figures; `--requirements <scene.json>` measures what a scene declares of the motion — a contact, a follow fraction, an aim, a joint range, a mesh's stretch, the seam between two parts over the motion — each against the author's own bar, from rig-c's posed frames, one line each, PASS, FAIL or NOT MEASURABLE (AUTHORING §7, *Declared requirements*) |
|
|
3654
|
+
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
3655
|
+
| `build --config --source --full --head --out [--seam] [--project] [--loop] [--lead-in <periods>] [--requirements] [--seam-pairs] [--keep-stills] [--idle-keys ctl\|direct]` | assemble, rig and check in one process, stopping at the first refusal; `--requirements`, `--seam-pairs` and `--keep-stills` are forwarded to check, `--idle-keys` to rig; `--lead-in` (with `--loop`) encodes the loop from the last of n + 1 idle periods played back to back, for a rig whose physics does not end where the render starts it |
|
|
3656
|
+
| `compose --scene <scene.json> --out [--requirements] [--accept]` | bind several characters' green builds into one rig (or, under `--accept`, builds failing only the bars named — for looking, not shipping): every name prefixed `<id>:` under one shared root, each character's offset added at its first-level bones, an optional background plate (its provenance recorded, judged by nothing) drawn first, and the draw order between the characters' slots as the scene file declares it — nothing inferred, nothing scaled; gated through rig-c as `rig` is, then checked ([AUTHORING §7](AUTHORING.md), *Composing several characters*) |
|
|
3657
|
+
| `mesh-draft --config <json> --parts <dir> --out <dir>` | draft contour meshes for configured lattices, keeping each only where it holds against its lattice through rig-c; write the revised config and measurement report |
|
|
3658
|
+
| `mesh-stats --config <json> --parts <dir>` | print each configured mesh's vertex and triangle counts and their total through the rig stage's construction; write nothing |
|
|
3659
|
+
| `painting --keypoints <json> [--person <id>] <png>` | before See-through, read the painting against its declared joints and refuse named background, joint or limb-gap problems; write nothing |
|
|
3660
|
+
| `remesh --rig <dir> --config <json> --parts <dir> --part <name> --out <dir>` | with exactly one of `--mesh <entry.json>` or `--delta <mesh-edit.json>`, re-fit one part of a gated rig into a fresh revision, preserving the input |
|
|
3661
|
+
| `retune --rig <dir> --config <json> --parts <dir> --out <dir>` | splice the config's constraints and idle onto a gated rig in a fresh revision, carrying its attachments over as bytes |
|
|
3662
|
+
| `guide <RULE \| §<n> \| §<n>.<m> \| §<n>.<m>.<k>>` | print the one row of [docs/AUTHORING.md](AUTHORING.md) that names a rule (a refusal and the bar it enforces, `CHECK_<LINE>` and `<LINE>`, print together), or one section, `###` subsection or `####` under it, read from the installed file when it runs |
|
|
3663
|
+
|
|
3664
|
+
`rig-parts --help` has every flag. Exit codes: 0 done, 1 input refused (every
|
|
3665
|
+
reason is a FAIL line), 2 a usage error or a command this version does not implement.
|
|
3666
|
+
Every FAIL line ends with `(see rig-parts guide <RULE>)`, the command that prints the
|
|
3667
|
+
guide's row for that rule, and each row is anchored on the web as
|
|
3668
|
+
`docs/AUTHORING.md#<RULE>`.
|
|
3669
|
+
|
|
3670
|
+
What 1.0.0 holds stable — these commands and their flags, the config
|
|
3671
|
+
fields, the files a build writes and their spec ids, the environment, and what this
|
|
3672
|
+
package relies on in rig-c — and what is not, is [docs/STABILITY.md](STABILITY.md).
|
|
3673
|
+
Coming from 0.16.0: [docs/MIGRATION.md](MIGRATION.md).
|
|
3674
|
+
|
|
3675
|
+
### Writing proposed config fields
|
|
3676
|
+
|
|
3677
|
+
`--write-config <config.json>` saves the proposals above into an existing
|
|
3678
|
+
regular config file: `seethrough.head_box`, `assemble.plan` plus
|
|
3679
|
+
`assemble.extend_below_crop`, or the present config sections of an ordinary
|
|
3680
|
+
proposal. The [neck draft](AUTHORING.md#a-neck-that-moves-on-its-own)
|
|
3681
|
+
(`propose … --full --head --write-config`) is another writer: for an existing
|
|
3682
|
+
`turn.body` config with one neck plan row, it selects the full-run neck, drafts a
|
|
3683
|
+
rigid unit and its depth, and reassembles parts under `<out>/rig`.
|
|
3684
|
+
For `assemble --propose-plan`, the destination must name `--config`
|
|
3685
|
+
itself; a different destination is a usage error. Other fields and absent optional
|
|
3686
|
+
sections remain; proposal notes are not copied. Changed whole values are spliced
|
|
3687
|
+
into the original text, preserving untouched bytes. Stage loaders validate the candidate, including `proposed_rig_scale`,
|
|
3688
|
+
before atomic replacement. Relative paths keep the config's directory as their
|
|
3689
|
+
base. Changes are listed on stderr; the head-box and plan JSON on stdout remain.
|
|
3690
|
+
An identical update leaves bytes and mtime untouched. Without the flag no config
|
|
3691
|
+
is changed. This commits the config file, not a transaction across the proposal
|
|
3692
|
+
artifacts.
|
|
3693
|
+
|
|
3694
|
+
With `--cloth <config.json>`, the flag requires a **new** destination and applies
|
|
3695
|
+
the recipe's entry ownership: recipe-owned entries are spliced into the cloth
|
|
3696
|
+
config while hand entries and unrelated text survive; conflicts are refused.
|
|
3697
|
+
The destination must also be separate from all four artifacts under `--out`:
|
|
3698
|
+
`proposal.json`, `basis.json`, `render/landmarks.png`, `render/landmarks_head.png`.
|
|
3699
|
+
Aliases, including directory symlinks and hardlinks, count as collisions too.
|
|
3700
|
+
Fresh output is published without overwriting a file created while the command
|
|
3701
|
+
runs. Its already-checked splice text may be a partial config; it is not passed
|
|
3702
|
+
through the full-config loader. If removing the staged temporary fails after
|
|
3703
|
+
publication, the error may accompany an already-published complete config.
|
|
3704
|
+
`--from <previous.json>` retains the previous-recipe comparison.
|
|
3705
|
+
With `--only-missing` (issue #671), an ordinary proposal's `--write-config` adds
|
|
3706
|
+
only `meshes.<part>` or `regions.<part>` for each proposed part the config
|
|
3707
|
+
attaches by neither, entry by entry, and, for a part the 1.37.0 arm split makes
|
|
3708
|
+
(`full:drape-<s>`, `full:hand-<s>`) that `assemble.plan` does not name, its plan
|
|
3709
|
+
row beside the arm's row; every existing row and entry and every other byte
|
|
3710
|
+
stays as read, and nothing else of the proposal is written. The candidate passes
|
|
3711
|
+
the full loader first, so an entry on a bone the config does not declare is
|
|
3712
|
+
refused naming the part and the bone; nothing missing leaves bytes and mtime
|
|
3713
|
+
untouched. It is the upgrade path for a config proposed before the 1.37.0 arm
|
|
3714
|
+
split ([MIGRATION](MIGRATION.md#after-1370-merged-arm-layers-are-split-issue-490)),
|
|
3715
|
+
and a usage error with `--head-box`, `--cloth` or `--from-config`.
|
|
3716
|
+
`--from-config` proposes nothing and cannot be combined with `--write-config`.
|
|
3717
|
+
[AUTHORING §4](AUTHORING.md#4-the-command-order) describes all four modes.
|
|
3600
3718
|
## 5. After each stage: what to read
|
|
3601
3719
|
|
|
3602
3720
|
| stage | read | green looks like | a common red |
|
|
@@ -4451,7 +4569,7 @@ those settings. Changing a config seed alone cannot change a no-config run.
|
|
|
4451
4569
|
| <a id="ASSEMBLE_TAKE_TAG_IN_RUN"></a>`ASSEMBLE_TAKE_TAG_IN_RUN` | selected donor does not hold the tag | choose a tag present in that donor |
|
|
4452
4570
|
| <a id="ASSEMBLE_TAKE_SUPPORT"></a>`ASSEMBLE_TAKE_SUPPORT` | selected layer has zero alpha > 8 pixels | choose a nonempty donor layer |
|
|
4453
4571
|
| <a id="ASSEMBLE_TAKE_DEPTH_AMBIGUOUS"></a>`ASSEMBLE_TAKE_DEPTH_AMBIGUOUS` | different original runs have equal or null depths | use a run with distinct numeric depths; do not invent an inter-run tie order |
|
|
4454
|
-
| <a id="ASSEMBLE_TAKE_PLAN_ORDER"></a>`ASSEMBLE_TAKE_PLAN_ORDER` | supplied plan reverses mixed-run depth
|
|
4572
|
+
| <a id="ASSEMBLE_TAKE_PLAN_ORDER"></a>`ASSEMBLE_TAKE_PLAN_ORDER` | supplied plan reverses an overlapping mixed-run pair: two-run full-layer head roles first, scalar depth within a role band (head-run and single-run rows use scalar depth); names the parts and support intersection | use the order drafted by `assemble --propose-plan`; fallback headwear follows the body and precedes the face, front hair follows the face; disjoint supports impose no plan-order constraint |
|
|
4455
4573
|
| <a id="ASSEMBLE_TAKE_GEOMETRY"></a>`ASSEMBLE_TAKE_GEOMETRY` | missing target head, unequal run canvases or unequal original input dimensions | supply matching geometry; the check does not resize inputs |
|
|
4456
4574
|
| <a id="ASSEMBLE_TAKE_META"></a>`ASSEMBLE_TAKE_META` | donor `meta.json` lacks a readable prompt ID or seed | retain the original run metadata |
|
|
4457
4575
|
| <a id="ASSEMBLE_TAKE_INPUT"></a>`ASSEMBLE_TAKE_INPUT` | declared donor input PNG cannot be read | name its original PNG relative to the config |
|
|
@@ -4983,7 +5101,7 @@ and stay absent).
|
|
|
4983
5101
|
| <a id="DEPTH_SOURCE_AMBIGUOUS"></a>`DEPTH_SOURCE_AMBIGUOUS` | writer/user or layer/canvas declarations overlap | select one authoritative route explicitly |
|
|
4984
5102
|
| <a id="DEPTH_SOURCE_RESOLVES"></a>`DEPTH_SOURCE_RESOLVES` | user key names no source layer | use the exact source tag in the selected run |
|
|
4985
5103
|
| <a id="DEPTH_SOURCE_BASE"></a>`DEPTH_SOURCE_BASE` | a direct assembler caller supplied user paths without config base | pass the actual config-directory base |
|
|
4986
|
-
| <a id="DEPTH_PART_SUPPORT"></a>`DEPTH_PART_SUPPORT` | exact copied/generated material lacks source depth or compatible units | provide a declared scalar source/registration; do not infer or fill depth |
|
|
5104
|
+
| <a id="DEPTH_PART_SUPPORT"></a>`DEPTH_PART_SUPPORT` | exact copied/generated material lacks source depth or compatible units | provide a declared scalar source/registration; do not infer or fill depth; declare the part `unit` with an explicit authored `unitDepths` value when its material includes authored under-art |
|
|
4987
5105
|
| <a id="PARTS_DEPTH_REFERENCE"></a>`PARTS_DEPTH_REFERENCE` | persisted fields/provenance violate the reference shape | restore image, near, positive zScale and exact source records |
|
|
4988
5106
|
| <a id="DEPTH_SILHOUETTE_SIZE_MISMATCH"></a>`DEPTH_SILHOUETTE_SIZE_MISMATCH` | `extendDepthSilhouette` or `padTurnDepthSheet` received a source depth sheet and artwork raster of different dimensions | supply the part's matching unpadded depth sheet and artwork raster |
|
|
4989
5107
|
| <a id="CONFIG_TURN_COUNTER_RANGE"></a>`CONFIG_TURN_COUNTER_RANGE` | `config.turn.counter` (or `config.turn.body.counter`) is outside `[0, 1]` | supply a non-negative rear counter-translation fraction in `[0, 1]` |
|
|
@@ -6205,6 +6323,42 @@ art. An authored `parts.json` depth reference remains an active standalone
|
|
|
6205
6323
|
`rig` declaration even without config opt-in: its file/size/color/opacity
|
|
6206
6324
|
guards still apply. With binding inactive there is no added reference, file or
|
|
6207
6325
|
legacy-byte change. #773 adds no image generation, geometry or motion model.
|
|
6326
|
+
|
|
6327
|
+
### Unit depth precedence
|
|
6328
|
+
|
|
6329
|
+
For `"unit"` roles, a transported part's sheet is recorded and validated but is not
|
|
6330
|
+
necessarily the depth used to move its bone. `turn.unitDepths` (and the body
|
|
6331
|
+
map `turn.body.unitDepths`, which inherits the head map when omitted) uses
|
|
6332
|
+
calibrated world depths. An explicit `0` is a value, not a request to sample.
|
|
6333
|
+
With active depth binding, assembly skips scalar transport only when every declared turn group makes the part `unit` with explicit part or carried-bone `unitDepths` covering every still piece, records the resolved values in `provenance.depth` as `{kind: "authored-unit", values: [...]}`, and retains input-sheet validation and every implicit-unit or sampled-surface refusal.
|
|
6334
|
+
For each carried unit bone, the existing producer resolves depth in this order:
|
|
6335
|
+
|
|
6336
|
+
1. `unitDepths.<part>`; when both names are present, the part entry wins.
|
|
6337
|
+
2. `unitDepths.<bone>`, using the carried bone's name.
|
|
6338
|
+
3. The primary surface's extended sheet at the unit's setup landmark. The
|
|
6339
|
+
head group prefers the `face` surface, otherwise the first declared surface;
|
|
6340
|
+
the body group uses its first declared surface. This depth is relative to
|
|
6341
|
+
that surface's reference and the group's pivot. A unit mesh uses its child
|
|
6342
|
+
landmarks, or its part centre when those landmarks are absent.
|
|
6343
|
+
4. Only if still unresolved, the unit's own sheet's opaque-art median. This
|
|
6344
|
+
lower-level fallback is not an authoring switch: the documented config
|
|
6345
|
+
reader requires a surface in each active group and supplies declared region
|
|
6346
|
+
landmarks or a mesh sampling position. No documented configuration is
|
|
6347
|
+
provided here to bypass primary sampling and select the own sheet instead.
|
|
6348
|
+
|
|
6349
|
+
Outside or nonopaque primary support produces
|
|
6350
|
+
[`RIG_TURN_UNIT_DEPTH_SUPPORT`](#RIG_TURN_UNIT_DEPTH_SUPPORT), retains the
|
|
6351
|
+
extended primary sample, and does not select the own sheet. Removing all
|
|
6352
|
+
surfaces is refused by
|
|
6353
|
+
[`CONFIG_TURN_SURFACE_REQUIRED`](#CONFIG_TURN_SURFACE_REQUIRED).
|
|
6354
|
+
Consequently, removing `unitDepths` alone does not make an own-sheet change
|
|
6355
|
+
move a unit: primary-surface sampling can still mask it. In that case the own
|
|
6356
|
+
sheet remains in `depth_sheets.json` and its padded PNG, but is not consumed
|
|
6357
|
+
for that unit's turn geometry. Author an explicit calibrated `unitDepths`
|
|
6358
|
+
entry to control its rigid depth; change the primary surface only when that
|
|
6359
|
+
shared sampled field is what should change. A `"surface"` part's own sampled
|
|
6360
|
+
geometry is a separate consumer described above.
|
|
6361
|
+
|
|
6208
6362
|
### Explicit source-derived waist pivot stage
|
|
6209
6363
|
|
|
6210
6364
|
`withWaistPivot` is a pure, explicit candidate-stage operation over an already
|
|
@@ -6324,3 +6478,51 @@ they do not certify a new toolchain, input or packaging build. No tuning to meet
|
|
|
6324
6478
|
| <a id="CONFIG_WAIST_NAME_FREE"></a>`CONFIG_WAIST_NAME_FREE` | The generated bone name already exists; declare an unused name | declare an unused name |
|
|
6325
6479
|
| <a id="RIG_WAIST_DECLARATION"></a>`RIG_WAIST_DECLARATION` | Actual provenance, pair bindings or drawing ownership do not match the bounded declaration; supply the declared base two-bone mesh/rigid-region pair | supply the declared base pair |
|
|
6326
6480
|
| <a id="RIG_WAIST_MOTION"></a>`RIG_WAIST_MOTION` | Imported tracks drive the generated waist frame; retain their original authored targets | retain the original authored targets |
|
|
6481
|
+
|
|
6482
|
+
### Depth-driven rigging
|
|
6483
|
+
|
|
6484
|
+
Registered per-part depth sheets drive sampled head yaw/pitch through
|
|
6485
|
+
`turn.roles` and independent body yaw through `turn.body.roles`. Region
|
|
6486
|
+
surfaces use depth-bearing grids; authored weighted surfaces keep their
|
|
6487
|
+
skinning and receive per-influence deform offsets sampled at setup positions.
|
|
6488
|
+
These controls are exercised on public synthetic fixtures. Unit parts use the
|
|
6489
|
+
[documented depth precedence](AUTHORING.md#unit-depth-precedence), so a
|
|
6490
|
+
recorded unit sheet does not by itself promise different turn geometry.
|
|
6491
|
+
See the [authoring contract](AUTHORING.md#per-layer-depth-sheets-773)
|
|
6492
|
+
and the [getting-started guide](https://github.com/rig-c/getting-started).
|
|
6493
|
+
|
|
6494
|
+
### Per-layer depth input
|
|
6495
|
+
|
|
6496
|
+
`config.depth` explicitly enables scalar input. Without it, `depth_filename`
|
|
6497
|
+
is a legacy hint: no depth-file I/O, validation, fetch, reference or output is
|
|
6498
|
+
activated, even when files or in-memory sheets exist. Active See-through sheets
|
|
6499
|
+
are read as opaque grayscale source data. Declare positive finite `depth.zScale`;
|
|
6500
|
+
eight bits do not supply world units. See-through far-high encoding is explicitly `near: "black"`. User
|
|
6501
|
+
per-layer/run-canvas fallbacks declare image, near and zScale themselves; a
|
|
6502
|
+
missing declared file is an error, not a fallback. The ordering median remains
|
|
6503
|
+
separate, and its agreement reading cannot certify an image's polarity.
|
|
6504
|
+
|
|
6505
|
+
Assembly emits registered part sheets and references; rig emits PAD4 sheets and
|
|
6506
|
+
`depth_sheets.json`. Since 1.68.0 (#777), [config.turn.roles](AUTHORING.md#3-configjson-field-by-field)
|
|
6507
|
+
uses depth sheets for 2.5D head turns with grid or authored weighted surfaces and `yaw_dial`/`pitch_dial`
|
|
6508
|
+
controls; see [`CONFIG_TURN_ROLE_UNKNOWN`](AUTHORING.md#CONFIG_TURN_ROLE_UNKNOWN)
|
|
6509
|
+
and [`CONFIG_TURN_SURFACE_REQUIRED`](AUTHORING.md#CONFIG_TURN_SURFACE_REQUIRED).
|
|
6510
|
+
`turn.body.roles` adds independent body yaw. A weighted mesh declared as
|
|
6511
|
+
`"surface"` in either group and blending head and torso bones receives both
|
|
6512
|
+
fields in proportion to its skin weights. Inside the existing setup alpha
|
|
6513
|
+
overlap with a single-group neighbour, a blended attachment's vertex takes
|
|
6514
|
+
that neighbour's existing emitted triangle field at its setup position (#804), without changing skin weights,
|
|
6515
|
+
setup or idle. A `motion.blink.still` unit neighbour supplies both emitted
|
|
6516
|
+
pieces, each on its own bone and resolved depth; transparent padding does not
|
|
6517
|
+
let either piece lend its field to the other's art (#807). Missing emitted
|
|
6518
|
+
split support is refused by name. Conflicting neighbour groups are refused by name. Each sibling island takes only its
|
|
6519
|
+
own group; parts without a blended attachment keep their existing role behavior
|
|
6520
|
+
and output bytes. Unlisted meshes do not consume turn depth sheets. See the
|
|
6521
|
+
[blended surface contract](AUTHORING.md#per-layer-depth-sheets-773).
|
|
6522
|
+
This copies the field at vertices; different triangle partitions can still
|
|
6523
|
+
differ between vertices. The [corrected #804 acceptance](https://github.com/rig-c/parts/issues/804#issuecomment-6478778123)
|
|
6524
|
+
checks vertex correspondence and reports those residuals per fixture; the
|
|
6525
|
+
contract records the measured grid residual and preserves its seam readings.
|
|
6526
|
+
No-image builds keep the legacy output bytes. `comfy seethrough --config <file> --depth-run <full|head>`
|
|
6527
|
+
uses the same opt-in; its default captures art only. Standalone authored part
|
|
6528
|
+
references remain active declarations. See [the input contract and refusals](AUTHORING.md#per-layer-depth-sheets-773).
|
package/docs/STABILITY.md
CHANGED
|
@@ -80,6 +80,10 @@ command's flag union; `comfy paint` and `comfy seethrough` are checked separatel
|
|
|
80
80
|
Required combinations within a mode remain the CLI's; the `cli` suite's controls
|
|
81
81
|
read them from the help.
|
|
82
82
|
|
|
83
|
+
The [command reference](AUTHORING.md#command-reference) describes each mode;
|
|
84
|
+
[writing proposed config fields](AUTHORING.md#writing-proposed-config-fields)
|
|
85
|
+
records the existing-file and fresh-file destinations and byte-preservation rules.
|
|
86
|
+
|
|
83
87
|
## The config contract
|
|
84
88
|
|
|
85
89
|
`src/config.ts` is the schema and the loader, and
|
|
@@ -478,6 +482,15 @@ Assembly's optional part `depth` reference holds `image`, `near`, `zScale` and
|
|
|
478
482
|
active binding. Persisted authored part references remain active for standalone
|
|
479
483
|
rig reading regardless of config opt-in. `rig` adds padded measurement PNGs and the `rig-parts-depth-sheets/1`
|
|
480
484
|
`depth_sheets.json` companion only with references (each output `part`, actual
|
|
481
|
-
padded `size`, and `depth`; a still split names both slices).
|
|
482
|
-
|
|
485
|
+
padded `size`, and `depth`; a still split names both slices). Passive references
|
|
486
|
+
add these companions without changing weighted geometry. Active `"surface"`
|
|
487
|
+
roles in `turn.roles` and `turn.body.roles` consume registered sheets:
|
|
488
|
+
region surfaces emit Core grid generators with `generator.depth`; authored
|
|
489
|
+
weighted surfaces keep their topology and bindings and emit sampled per-influence
|
|
490
|
+
inverse-bind deform offsets in motion, without adding a Core attachment depth
|
|
491
|
+
field. Head yaw/pitch and independent body yaw use their existing turn groups.
|
|
492
|
+
Blended surfaces retain the documented per-group masks and setup-overlap
|
|
493
|
+
neighbour field rules in [AUTHORING](AUTHORING.md#per-layer-depth-sheets-773).
|
|
494
|
+
The unit-depth precedence and its primary-surface sampling are documented in
|
|
495
|
+
[AUTHORING](AUTHORING.md#unit-depth-precedence).
|
|
483
496
|
The separate ordering scalar is unchanged. No-image outputs retain legacy bytes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.83.
|
|
3
|
+
"version": "1.83.2",
|
|
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": {
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"scripts": {
|
|
59
59
|
"fetch-examples": "bash scripts/fetch-examples.sh",
|
|
60
60
|
"selftest": "bun selftest.ts",
|
|
61
|
+
"check:readme": "bun scripts/readme_policy.ts",
|
|
61
62
|
"selftest:pre": "bun selftest.ts --pre",
|
|
62
63
|
"smoke": "bun scripts/install_smoke.ts",
|
|
63
64
|
"typecheck": "bunx tsc --noEmit",
|
package/src/assemble.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { unionTakenSources } from './takesource.ts';
|
|
2
|
+
import { authoredUnitDepths, type AuthoredUnitDepth } from './authoredunit.ts';
|
|
2
3
|
import { paintingExtensionMasks, paintingExtensionProblems, type PaintingExtension } from './paintingextension.ts';
|
|
3
4
|
import { AUTOMATIC_WRITERS as AW, compactValueBaseline, pixelProvenance, rgbaEqual, type ValueBaseline } from './provenance.ts';
|
|
4
5
|
import { bindDepthRun, copyDepthMaterial, registerDepthRun, transportPartDepth, type DepthConfig, type RegisteredDepth } from './depthsheets.ts';
|
|
@@ -317,6 +318,11 @@ export function singleRunPadRecord(single: boolean, sourceW: number, sourceH: nu
|
|
|
317
318
|
}
|
|
318
319
|
|
|
319
320
|
export interface AssembleInput {
|
|
321
|
+
/** Turn context for the exact part/carrier declarations, never an exemption flag. */
|
|
322
|
+
turn?: TurnSpec;
|
|
323
|
+
regions?: Record<string, RegionSpec>;
|
|
324
|
+
meshes?: Record<string, MeshSpec>;
|
|
325
|
+
motion?: CharacterConfig['motion'];
|
|
320
326
|
depth?: DepthConfig;
|
|
321
327
|
/** Config-relative user sheet paths; caller supplies its actual base. */
|
|
322
328
|
depthBase?: string;
|
|
@@ -4638,6 +4644,7 @@ export function checkSingleRun(full: LayerSet, plan: readonly PlanEntry[] | null
|
|
|
4638
4644
|
*/
|
|
4639
4645
|
export function assemble(input: AssembleInput): AssembleResult {
|
|
4640
4646
|
const { source, extend, patches, seamRule, projectRule } = input;
|
|
4647
|
+
const authoredDepths = input.depth === undefined ? new Map<string, AuthoredUnitDepth>() : authoredUnitDepths(input);
|
|
4641
4648
|
if (input.paintingExtension !== undefined) refuseIfAny(paintingExtensionProblems(input.paintingExtension));
|
|
4642
4649
|
const single = input.single === true;
|
|
4643
4650
|
const frame = checkGeometry(
|
|
@@ -5541,7 +5548,8 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5541
5548
|
const limbIndex = limb === undefined ? -1 : lim.parts.findIndex(q => q.record.name === limb.name && q.record.segment === limb.segment);
|
|
5542
5549
|
const upperIndex = limb === undefined ? -1 : lim.parts.findIndex(q => q.record.name === limb.name && q.record.segment === limb.segment - 1);
|
|
5543
5550
|
const capSource = limbIndex < 0 ? null : lim.capFrom[limbIndex];
|
|
5544
|
-
const
|
|
5551
|
+
const authoredDepth = authoredDepths.get(name);
|
|
5552
|
+
const scalar = authoredDepth === undefined ? transportPartDepth(record, image, registeredDepth, {
|
|
5545
5553
|
map: run => runMap(frame, run), width: W, extend: extendKeys, owner: trace.from,
|
|
5546
5554
|
ring: trace.ring.data, region: region?.data ?? null,
|
|
5547
5555
|
copiedSource: (x, y) => {
|
|
@@ -5552,7 +5560,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5552
5560
|
return { key: e < 0 ? upper.key : upper.extendKeys[e], ring: upper.trace.ring.data[p] !== 0 };
|
|
5553
5561
|
},
|
|
5554
5562
|
...((limb?.cap?.image_px ?? 0) > 0 || (limb?.cap?.lower_px ?? 0) > 0 ? { unsupported: `cap copies ${(limb?.cap?.image_px ?? 0) + (limb?.cap?.lower_px ?? 0)} pixels from a separate cap image whose scalar grid is undeclared` } : {}),
|
|
5555
|
-
});
|
|
5563
|
+
}) : undefined;
|
|
5556
5564
|
if (capSource !== null && upperIndex >= 0 && capSource.codes.some(code => code === CAP_FROM_UPPER)) {
|
|
5557
5565
|
const upper = limbPieces[upperIndex];
|
|
5558
5566
|
const keys = [upper.key, ...upper.extendKeys.filter((_, i) => upper.trace.from.some(v => v === i))];
|
|
@@ -5573,7 +5581,11 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
5573
5581
|
|
|
5574
5582
|
|
|
5575
5583
|
seamOverride(placed, srcr, seamRule, seamRule === 'silhouette' ? silhouette : null, keepInOrder, input.turnMovingPair ?? null, (i, p) => { writersInOrder[i][p] = AW.seamOverride; });
|
|
5576
|
-
placed.forEach((p, i) => {
|
|
5584
|
+
placed.forEach((p, i) => {
|
|
5585
|
+
p.record.provenance = pixelProvenance(p.image, p.record.x, p.record.y, W, writersInOrder[i], editNames, 0, valueBaselines.get(p.record.from) ?? null);
|
|
5586
|
+
const depth = authoredDepths.get(p.record.name);
|
|
5587
|
+
if (depth !== undefined) p.record.provenance.depth = depth;
|
|
5588
|
+
});
|
|
5577
5589
|
// Provenance is now numbers/labels only. No later raster/figure operation
|
|
5578
5590
|
// reads report buffers; release all aliases before recomposite/error work.
|
|
5579
5591
|
valueBaselines.clear();
|
|
@@ -6048,6 +6060,18 @@ export const PROPOSE_HEAD_TAGS: readonly string[] = [
|
|
|
6048
6060
|
'eyebrow-l',
|
|
6049
6061
|
];
|
|
6050
6062
|
|
|
6063
|
+
/** #869: two-run full rows are body rows or head fallbacks, regardless of part spelling.
|
|
6064
|
+
* Within one band the run's depth order survives; the bands encode the proposer's roles.
|
|
6065
|
+
*/
|
|
6066
|
+
export function fullPlanBand(tag: string): number {
|
|
6067
|
+
if (tag === 'back hair') return 0;
|
|
6068
|
+
if (!PROPOSE_HEAD_TAGS.includes(tag)) return 1;
|
|
6069
|
+
if (tag === 'headwear' || tag === 'earwear') return 2;
|
|
6070
|
+
if (tag === 'face') return 3;
|
|
6071
|
+
if (tag === 'front hair') return 5;
|
|
6072
|
+
return 4;
|
|
6073
|
+
}
|
|
6074
|
+
|
|
6051
6075
|
/** #863: prefer suitable head layers; only unusable head candidates may fall back for these tags. */
|
|
6052
6076
|
const HEAD_FALLBACK_TAGS: readonly string[] = ['back hair', 'face', 'front hair'];
|
|
6053
6077
|
|
|
@@ -6106,7 +6130,7 @@ function fmt0(v: number): string {
|
|
|
6106
6130
|
* A suitable head candidate is retained. Every fallback names the two rig
|
|
6107
6131
|
* areas and the reason. A `headwear`/`earwear` the head run drops or
|
|
6108
6132
|
* shrinks (its rig-pixel area below 0.8 of the full run's) is taken from the
|
|
6109
|
-
* full run instead, drawn
|
|
6133
|
+
* full run instead, drawn after the body and before the face — noted. `front hair` and
|
|
6110
6134
|
* `back hair` taken from the head run are extended below the crop from the
|
|
6111
6135
|
* full run when the full run's layer reaches more than 8 rig pixels below the
|
|
6112
6136
|
* crop line. A lone `handwear` layer is named `sleeves`.
|
|
@@ -6232,7 +6256,7 @@ export function proposePlan(full0: LayerSet, head0: LayerSet, g: Geometry, minPx
|
|
|
6232
6256
|
return tag !== 'nose' && !(run === 'head' && tag === 'face' && headFaceRefusal !== null) &&
|
|
6233
6257
|
l !== undefined && l.opaqueRunPx >= floor && !implausible(run, tag, 'not proposed');
|
|
6234
6258
|
};
|
|
6235
|
-
const body = runs.full.filter((l) =>
|
|
6259
|
+
const body = runs.full.filter((l) => fullPlanBand(l.tag) === 1 && ok('full', l.tag)).map((l) => l.tag);
|
|
6236
6260
|
// Issue #256: an author-made layer is proposed from the run that holds it, at its place in that run's draw order,
|
|
6237
6261
|
// like any layer; one both runs hold is proposed once, from the full run when the full run's is proposed, and the note says so.
|
|
6238
6262
|
const authorBoth = runs.head.filter((l) => readAuthorTag(l.tag) !== null && body.includes(l.tag)).map((l) => l.tag);
|
|
@@ -6279,15 +6303,15 @@ export function proposePlan(full0: LayerSet, head0: LayerSet, g: Geometry, minPx
|
|
|
6279
6303
|
const draw = t === 'back hair' ? 'behind the body and face' : t === 'face' ? 'before head details and front hair' : 'in front of the face';
|
|
6280
6304
|
notes.push(`${t}: head run ${fmt0(hp)} rig px (${reason}); full run ${fmt0(fp)} rig px -> full-run layer, drawn ${draw}`);
|
|
6281
6305
|
}
|
|
6282
|
-
|
|
6306
|
+
// Keep the full run's order inside the accessory band, including mixed donors.
|
|
6307
|
+
for (const { tag: t } of runs.full.filter(l => fullPlanBand(l.tag) === 2)) {
|
|
6283
6308
|
const hl = find('head', t);
|
|
6284
6309
|
const fl = find('full', t);
|
|
6285
6310
|
const hp = hl !== undefined ? hl.opaqueRunPx * headArea : 0;
|
|
6286
6311
|
const fp = fl !== undefined ? fl.opaqueRunPx * fullArea : 0;
|
|
6287
6312
|
if (ok('full', t) && hp < 0.8 * fp) {
|
|
6288
6313
|
drop(t);
|
|
6289
|
-
|
|
6290
|
-
plan.splice(at, 0, [PROPOSE_ALIAS[t], 'full', t]);
|
|
6314
|
+
plan.push([PROPOSE_ALIAS[t], 'full', t]);
|
|
6291
6315
|
notes.push(`${t}: head run ${fmt0(hp)} rig px < 0.8 x full run ${fmt0(fp)} -> full-run layer, drawn behind the face`);
|
|
6292
6316
|
}
|
|
6293
6317
|
}
|
|
@@ -6475,6 +6499,7 @@ export function stageFields(
|
|
|
6475
6499
|
regions?: Record<string, RegionSpec>;
|
|
6476
6500
|
constraints?: ConfigConstraint[];
|
|
6477
6501
|
turn?: TurnSpec;
|
|
6502
|
+
motion?: CharacterConfig['motion'];
|
|
6478
6503
|
},
|
|
6479
6504
|
single: { w: number; h: number } | null = null,
|
|
6480
6505
|
): {
|
|
@@ -6500,6 +6525,10 @@ export function stageFields(
|
|
|
6500
6525
|
/** Base painting only; named final parts, explicit band and path bound (#848). */
|
|
6501
6526
|
paintingExtension?: readonly PaintingExtension[];
|
|
6502
6527
|
turnMovingPair?: (a: string, b: string) => boolean;
|
|
6528
|
+
turn?: TurnSpec;
|
|
6529
|
+
regions?: Record<string, RegionSpec>;
|
|
6530
|
+
meshes?: Record<string, MeshSpec>;
|
|
6531
|
+
motion?: CharacterConfig['motion'];
|
|
6503
6532
|
} {
|
|
6504
6533
|
const problems: Problem[] = [];
|
|
6505
6534
|
if (cfg.seethrough === undefined) {
|
|
@@ -6523,6 +6552,7 @@ export function stageFields(
|
|
|
6523
6552
|
...(cfg.assemble.keep_under_occluders === undefined ? {} : { keepUnderOccluders: cfg.assemble.keep_under_occluders }),
|
|
6524
6553
|
...(cfg.assemble.extend_painting === undefined ? {} : { paintingExtension: cfg.assemble.extend_painting }),
|
|
6525
6554
|
...(turnMovingPair === null ? {} : { turnMovingPair }),
|
|
6555
|
+
...(cfg.turn === undefined ? {} : { turn: cfg.turn, regions: cfg.regions, meshes: cfg.meshes, motion: cfg.motion }),
|
|
6526
6556
|
rigScale: cfg.assemble.rig_scale,
|
|
6527
6557
|
plan: cfg.assemble.plan,
|
|
6528
6558
|
extend: cfg.assemble.extend_below_crop ?? [],
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Fully explicit unit consumers may omit their unused scalar transport (#877). */
|
|
2
|
+
import type { MeshSpec, Motion, RegionSpec, TurnSpec } from './config.ts';
|
|
3
|
+
import { turnUnitBone } from './turn.ts';
|
|
4
|
+
|
|
5
|
+
export interface AuthoredUnitDepth {
|
|
6
|
+
kind: 'authored-unit';
|
|
7
|
+
/** An absent bone means the part override, which covers every carried piece. */
|
|
8
|
+
values: Array<{ group: 'head' | 'body'; bone?: string; value: number }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function authoredUnitDepths(cfg: {
|
|
12
|
+
turn?: TurnSpec;
|
|
13
|
+
regions?: Record<string, RegionSpec>;
|
|
14
|
+
meshes?: Record<string, MeshSpec>;
|
|
15
|
+
motion?: Pick<Motion, 'blink'>;
|
|
16
|
+
}): ReadonlyMap<string, AuthoredUnitDepth> {
|
|
17
|
+
const out = new Map<string, AuthoredUnitDepth>();
|
|
18
|
+
const turn = cfg.turn;
|
|
19
|
+
if (turn === undefined || turn === null || typeof turn !== 'object' || Array.isArray(turn)) return out;
|
|
20
|
+
const groups = [
|
|
21
|
+
{ group: 'head' as const, roles: turn.roles, depths: turn.unitDepths },
|
|
22
|
+
{ group: 'body' as const, roles: turn.body?.roles, depths: turn.body?.unitDepths ?? turn.unitDepths },
|
|
23
|
+
];
|
|
24
|
+
const number = (table: Record<string, number> | undefined, name: string): number | undefined => {
|
|
25
|
+
const value = table !== undefined && Object.hasOwn(table, name) ? table[name] : undefined;
|
|
26
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
27
|
+
};
|
|
28
|
+
const parts = new Set(groups.flatMap(g => Object.keys(g.roles ?? {})));
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
const values: AuthoredUnitDepth['values'] = [];
|
|
31
|
+
let complete = true;
|
|
32
|
+
for (const g of groups) {
|
|
33
|
+
if (g.roles === undefined || !Object.hasOwn(g.roles, part)) continue;
|
|
34
|
+
if (g.roles[part] !== 'unit') { complete = false; break; }
|
|
35
|
+
const explicitPart = number(g.depths, part);
|
|
36
|
+
if (explicitPart !== undefined) { values.push({ group: g.group, value: explicitPart }); continue; }
|
|
37
|
+
const region = cfg.regions?.[part];
|
|
38
|
+
const bone = typeof region === 'string' ? region : region?.bone;
|
|
39
|
+
// Match the producer's actual unit carrier, not a mesh segment's bone.
|
|
40
|
+
const bones = cfg.meshes !== undefined && Object.hasOwn(cfg.meshes, part)
|
|
41
|
+
? [turnUnitBone(part)]
|
|
42
|
+
: typeof bone === 'string' && bone.length > 0 ? [bone] : [];
|
|
43
|
+
if (cfg.meshes?.[part] === undefined) {
|
|
44
|
+
const still = cfg.motion?.blink?.still?.[part];
|
|
45
|
+
if (still !== undefined) {
|
|
46
|
+
if (typeof still.bone !== 'string' || still.bone.length === 0) { complete = false; break; }
|
|
47
|
+
bones.push(still.bone);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (bones.length === 0) { complete = false; break; }
|
|
51
|
+
for (const name of new Set(bones)) {
|
|
52
|
+
const value = number(g.depths, name);
|
|
53
|
+
if (value === undefined) { complete = false; break; }
|
|
54
|
+
values.push({ group: g.group, bone: name, value });
|
|
55
|
+
}
|
|
56
|
+
if (!complete) break;
|
|
57
|
+
}
|
|
58
|
+
if (complete && values.length > 0) out.set(part, { kind: 'authored-unit', values });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
package/src/build.ts
CHANGED
|
@@ -665,6 +665,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
665
665
|
...(fields.headFaceUnderfill === undefined ? {} : { headFaceUnderfill: fields.headFaceUnderfill }),
|
|
666
666
|
...(fields.yieldRefused === undefined ? {} : { yieldRefused: fields.yieldRefused }),
|
|
667
667
|
...(fields.turnMovingPair === undefined ? {} : { turnMovingPair: fields.turnMovingPair }),
|
|
668
|
+
...(fields.turn === undefined ? {} : { turn: fields.turn, regions: fields.regions, meshes: fields.meshes, motion: fields.motion }),
|
|
668
669
|
seamRule: input.seam,
|
|
669
670
|
projectRule: input.project,
|
|
670
671
|
...(altExclude.length === 0 ? {} : { exclude: altExclude }),
|
package/src/parts.ts
CHANGED
|
@@ -503,7 +503,8 @@ function checkOwnership(raw: unknown, object: string, fail: (code: string, objec
|
|
|
503
503
|
|
|
504
504
|
function orderedProvenance(p: PixelProvenance): PixelProvenance {
|
|
505
505
|
return { unchanged_layer_px: p.unchanged_layer_px, hand_px: p.hand_px, automatic_px: p.automatic_px,
|
|
506
|
-
edits: p.edits.map(e => ({ edit: e.edit, px: e.px })), automatic: p.automatic.map(e => ({ stage: e.stage, px: e.px }))
|
|
506
|
+
edits: p.edits.map(e => ({ edit: e.edit, px: e.px })), automatic: p.automatic.map(e => ({ stage: e.stage, px: e.px })),
|
|
507
|
+
...(p.depth === undefined ? {} : { depth: { kind: p.depth.kind, values: p.depth.values.map(v => ({ group: v.group, ...(v.bone === undefined ? {} : { bone: v.bone }), value: v.value })) } }) };
|
|
507
508
|
}
|
|
508
509
|
|
|
509
510
|
function serializePartList(parts: readonly PartRecord[]): Array<Record<string, unknown>> {
|
|
@@ -591,13 +592,13 @@ function show(v: unknown): string {
|
|
|
591
592
|
}
|
|
592
593
|
|
|
593
594
|
/** The same optional measurement contract for base and alternate records. */
|
|
594
|
-
function checkProvenance(raw: unknown, opaque: unknown, object: string, fail: (code: string, object: string, detail: string) => void): void {
|
|
595
|
+
function checkProvenance(raw: unknown, opaque: unknown, object: string, fail: (code: string, object: string, detail: string) => void, hasScalar = false): void {
|
|
595
596
|
const v = raw as PixelProvenance | null;
|
|
596
597
|
const count = (n: unknown): boolean => Number.isSafeInteger(n) && (n as number) >= 0;
|
|
597
598
|
const edit = (name: string): boolean => /^(cuts\[\d+\]|patches\[\d+\])$/.test(name) ||
|
|
598
599
|
(/^(full|head):author:/.test(name) && readFrom(name) !== null);
|
|
599
600
|
const shape = v !== null && typeof v === 'object' && !Array.isArray(v) &&
|
|
600
|
-
Object.keys(v).sort().join(',') === 'automatic,automatic_px,edits,hand_px,unchanged_layer_px' &&
|
|
601
|
+
Object.keys(v).filter(k => k !== 'depth').sort().join(',') === 'automatic,automatic_px,edits,hand_px,unchanged_layer_px' &&
|
|
601
602
|
[v.unchanged_layer_px, v.hand_px, v.automatic_px].every(count) && Array.isArray(v.edits) && Array.isArray(v.automatic) &&
|
|
602
603
|
v.edits.every(e => e !== null && typeof e === 'object' && Object.keys(e).sort().join(',') === 'edit,px' && typeof e.edit === 'string' && edit(e.edit) && count(e.px) && e.px > 0) &&
|
|
603
604
|
v.automatic.every(e => e !== null && typeof e === 'object' && Object.keys(e).sort().join(',') === 'px,stage' && typeof e.stage === 'string' && Object.hasOwn(AUTOMATIC_WRITERS, e.stage) && count(e.px) && e.px > 0) &&
|
|
@@ -612,6 +613,18 @@ function checkProvenance(raw: unknown, opaque: unknown, object: string, fail: (c
|
|
|
612
613
|
const hand = p.edits.reduce((n, e) => n + e.px, 0), automatic = p.automatic.reduce((n, e) => n + e.px, 0);
|
|
613
614
|
fail('PARTS_COUNTS_ADD_UP', object, `unchanged_layer_px ${p.unchanged_layer_px} + hand_px ${p.hand_px} + automatic_px ${p.automatic_px} = ${total}; opaque_px ${opaque} is required; edit subtotal ${hand} (hand_px ${p.hand_px} required), stage subtotal ${automatic} (automatic_px ${p.automatic_px} required)`);
|
|
614
615
|
}
|
|
616
|
+
if (v !== null && typeof v === 'object' && 'depth' in v) {
|
|
617
|
+
const d = v.depth;
|
|
618
|
+
const valid = !hasScalar && d !== null && typeof d === 'object' && !Array.isArray(d) &&
|
|
619
|
+
Object.keys(d).sort().join(',') === 'kind,values' && d.kind === 'authored-unit' && Array.isArray(d.values) && d.values.length > 0 &&
|
|
620
|
+
d.values.every(q => q !== null && typeof q === 'object' && !Array.isArray(q) &&
|
|
621
|
+
Object.keys(q).sort().join(',') === (Object.hasOwn(q, 'bone') ? 'bone,group,value' : 'group,value') &&
|
|
622
|
+
(q.group === 'head' || q.group === 'body') && typeof q.value === 'number' && Number.isFinite(q.value) &&
|
|
623
|
+
(!Object.hasOwn(q, 'bone') || (typeof q.bone === 'string' && q.bone.length > 0))) &&
|
|
624
|
+
new Set(d.values.map(q => `${q.group}:${q.bone ?? ''}`)).size === d.values.length &&
|
|
625
|
+
d.values.every(q => q.bone !== undefined || d.values.filter(r => r.group === q.group).length === 1);
|
|
626
|
+
if (!valid) fail('PARTS_FIELD_TYPE', object + ' depth', 'requires authored-unit with nonempty distinct head/body finite values, optional nonempty bone names, and no transported scalar reference');
|
|
627
|
+
}
|
|
615
628
|
}
|
|
616
629
|
|
|
617
630
|
function checkParts(raw: unknown, path: string): PartsFile {
|
|
@@ -675,7 +688,7 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
675
688
|
if (k in r && !(Number.isInteger(r[k]) && (r[k] as number) >= 0)) fail('PARTS_FIELD_TYPE', `${label} field "${k}"`, `is ${show(r[k])}; a non-negative integer is required`);
|
|
676
689
|
}
|
|
677
690
|
const count = (k: string): number | null => (Number.isInteger(r[k]) && (r[k] as number) >= 0 ? (r[k] as number) : null);
|
|
678
|
-
if ('provenance' in r) checkProvenance(r.provenance, r.opaque_px, label + ' provenance', fail);
|
|
691
|
+
if ('provenance' in r) checkProvenance(r.provenance, r.opaque_px, label + ' provenance', fail, 'depth' in r);
|
|
679
692
|
checkPaintingExtensionCount(r, label, fail);
|
|
680
693
|
const [op, vis, occ, vnp] = ['opaque_px', 'visible_px', 'occluded_px', 'visible_not_projected_px'].map(count);
|
|
681
694
|
if (held.length === VISIBILITY_KEYS.length && op !== null && vis !== null && occ !== null && vnp !== null) {
|
|
@@ -828,7 +841,7 @@ function checkParts(raw: unknown, path: string): PartsFile {
|
|
|
828
841
|
if (typeof p !== 'object' || p === null || Array.isArray(p)) return;
|
|
829
842
|
const r = p as Record<string, unknown>;
|
|
830
843
|
const part817 = typeof r.name === 'string' ? `part "${r.name}"` : `records[${j}]`;
|
|
831
|
-
if ('provenance' in r) checkProvenance(r.provenance, r.opaque_px, `${at} ${part817} provenance`, fail);
|
|
844
|
+
if ('provenance' in r) checkProvenance(r.provenance, r.opaque_px, `${at} ${part817} provenance`, fail, 'depth' in r);
|
|
832
845
|
checkPaintingExtensionCount(r, `${at} ${part817}`, fail);
|
|
833
846
|
const l = r[LIMB_KEY];
|
|
834
847
|
if (typeof l !== 'object' || l === null || Array.isArray(l) || !('cap' in l)) return;
|
package/src/provenance.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Report-only last-writer accounting over final output pixels (#817). */
|
|
2
2
|
import { OPAQUE_ALPHA_ABOVE } from './layers.ts';
|
|
3
3
|
import type { PartRecord } from './parts.ts';
|
|
4
|
+
import type { AuthoredUnitDepth } from './authoredunit.ts';
|
|
4
5
|
import { crop, type Raster } from './raster/index.ts';
|
|
5
6
|
|
|
6
7
|
/** Negative IDs distinguish automatic writes from positive, config-indexed hand edits. */
|
|
@@ -11,6 +12,8 @@ export const AUTOMATIC_WRITERS = {
|
|
|
11
12
|
} as const;
|
|
12
13
|
export type AutomaticWriter = keyof typeof AUTOMATIC_WRITERS;
|
|
13
14
|
export interface PixelProvenance {
|
|
15
|
+
/** Scalar transport omitted only for fully explicit unit consumers (#877). */
|
|
16
|
+
depth?: AuthoredUnitDepth;
|
|
14
17
|
unchanged_layer_px: number;
|
|
15
18
|
hand_px: number;
|
|
16
19
|
automatic_px: number;
|
|
@@ -83,7 +86,7 @@ export function provenanceLines(parts: readonly PartRecord[]): string[] {
|
|
|
83
86
|
opaque += p.opaque_px; unchanged += v.unchanged_layer_px; automatic += v.automatic_px; hand += v.hand_px;
|
|
84
87
|
for (const e of v.edits) edits.set(e.edit, (edits.get(e.edit) ?? 0) + e.px);
|
|
85
88
|
for (const e of v.automatic) stages.set(e.stage, (stages.get(e.stage) ?? 0) + e.px);
|
|
86
|
-
lines.push(` provenance: part "${p.name}" (${p.from}) opaque ${p.opaque_px} = unchanged layer ${v.unchanged_layer_px} + hand ${v.hand_px} + automatic ${v.automatic_px}; edits ${detail(new Map(v.edits.map(e => [e.edit, e.px])))}; stages ${detail(new Map(v.automatic.map(e => [e.stage, e.px])))}`);
|
|
89
|
+
lines.push(` provenance: part "${p.name}" (${p.from}) opaque ${p.opaque_px} = unchanged layer ${v.unchanged_layer_px} + hand ${v.hand_px} + automatic ${v.automatic_px}; edits ${detail(new Map(v.edits.map(e => [e.edit, e.px])))}; stages ${detail(new Map(v.automatic.map(e => [e.stage, e.px])))}${v.depth === undefined ? '' : `; depth authored-unit ${v.depth.values.map(d => `${d.group}/${d.bone ?? p.name}=${d.value}`).join(', ')}`}`);
|
|
87
90
|
}
|
|
88
91
|
lines.push(` provenance total: opaque ${opaque} = unchanged layer ${unchanged} + hand ${hand} + automatic ${automatic}; edits ${detail(edits)}; stages ${detail(stages)}; output pixels, including copies in separate parts; value equality first; last value-changing stage wins`);
|
|
89
92
|
return lines;
|
package/src/takelayers.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import type { EarlyConfig, PlanEntry, Run } from './config.ts';
|
|
5
|
-
import { CORE_KERNEL } from './assemble.ts';
|
|
5
|
+
import { CORE_KERNEL, fullPlanBand } from './assemble.ts';
|
|
6
6
|
import { makeInputs } from './inputs.ts';
|
|
7
7
|
import { OPAQUE_ALPHA_ABOVE, readLayers, type Layer, type LayerSet } from './layers.ts';
|
|
8
8
|
import { PartsError, refuseIfAny, type Problem } from './errors.ts';
|
|
@@ -51,6 +51,16 @@ function identity(layer: Layer, canvas: LayerSet['canvas'], donor: Raster, targe
|
|
|
51
51
|
export function takeLayerNote(source: TakenLayerSource): string {
|
|
52
52
|
return `take ${source.run}:${source.tag} from ${source.run_dir}; prompt_id ${source.prompt_id}; seed ${source.seed}; st_input ${source.st_input}; support ${source.support_px} run px; checked ${source.checked_px} input px; differing ${source.differing_px}; alpha > ${OPAQUE_ALPHA_ABOVE}, ${CORE_KERNEL}x${CORE_KERNEL} margin`;
|
|
53
53
|
}
|
|
54
|
+
/** Intersection of original alpha supports in the common target-run canvas; no bbox or dilation proxy. */
|
|
55
|
+
function supportOverlap(a: Layer, b: Layer): number {
|
|
56
|
+
let px = 0;
|
|
57
|
+
for (let y = Math.max(a.top, b.top); y < Math.min(a.bottom, b.bottom); y++)
|
|
58
|
+
for (let x = Math.max(a.left, b.left); x < Math.min(a.right, b.right); x++) {
|
|
59
|
+
if (a.pixels.data[((y - a.top) * a.pixels.width + x - a.left) * 4 + 3] > OPAQUE_ALPHA_ABOVE &&
|
|
60
|
+
b.pixels.data[((y - b.top) * b.pixels.width + x - b.left) * 4 + 3] > OPAQUE_ALPHA_ABOVE) px++;
|
|
61
|
+
}
|
|
62
|
+
return px;
|
|
63
|
+
}
|
|
54
64
|
/** Apply every declaration together, so replacing all tied layers from one run can retain its known order. */
|
|
55
65
|
export function applyTakenLayers(runs: { full: LayerSet; head: LayerSet; single: boolean }, painting: Raster, cfg: Pick<EarlyConfig, 'seethrough' | 'depth'> & { assemble: { take?: TakeLayers[]; plan?: PlanEntry[] } }, configPath: string): { full: LayerSet; head: LayerSet; single: boolean; notes: string[] } {
|
|
56
66
|
const declarations = cfg.assemble.take ?? [];
|
|
@@ -99,7 +109,14 @@ export function applyTakenLayers(runs: { full: LayerSet; head: LayerSet; single:
|
|
|
99
109
|
const selected = (cfg.assemble.plan ?? []).filter(p => p[1] === run).map(p => ({ part: p[0], layer: list.find(l => l.name === p[2]) }));
|
|
100
110
|
for (let i = 0; i < selected.length; i++) for (let j = i + 1; j < selected.length; j++) {
|
|
101
111
|
const a = selected[i], b = selected[j];
|
|
102
|
-
if (a.layer
|
|
112
|
+
if (a.layer === undefined || b.layer === undefined || origin(a.layer) === origin(b.layer)) continue;
|
|
113
|
+
const overlap = supportOverlap(a.layer, b.layer);
|
|
114
|
+
if (overlap === 0) continue;
|
|
115
|
+
// Only full rows in a two-run plan have the proposer's fallback roles.
|
|
116
|
+
// Head rows and one-run plans retain the scalar order, as do peers in a band.
|
|
117
|
+
const band = !runs.single && run === 'full' ? fullPlanBand(a.layer.name) - fullPlanBand(b.layer.name) : 0;
|
|
118
|
+
const reversed = band === 0 ? list.indexOf(a.layer) > list.indexOf(b.layer) : band > 0;
|
|
119
|
+
if (reversed) problems.push({ code: 'ASSEMBLE_TAKE_PLAN_ORDER', object: `${run} plan parts "${a.part}" / "${b.part}"`, detail: `plan order reverses ${band === 0 ? `mixed-run depths ${a.layer.depth} / ${b.layer.depth}; put the larger depth behind` : `two-run full-layer roles "${a.layer.name}" / "${b.layer.name}"; order back hair, body, headwear/earwear, face, head details, front hair`}; supports overlap at ${overlap} run px (alpha > ${OPAQUE_ALPHA_ABOVE}); use assemble --propose-plan` });
|
|
103
120
|
}
|
|
104
121
|
refuseIfAny(problems);
|
|
105
122
|
const replaced = new Set(declarations.filter(d => d.run === run).flatMap(d => d.tags));
|