spine-parts 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -11
- package/cli.ts +100 -30
- package/docs/AUTHORING.md +326 -42
- package/package.json +2 -2
- package/skills/spine-parts/SKILL.md +23 -4
- package/src/assemble.ts +369 -18
- package/src/build.ts +45 -15
- package/src/check.ts +63 -7
- package/src/config.ts +265 -43
- package/src/instruments.ts +8 -4
- package/src/layers.ts +169 -0
- package/src/motion.ts +127 -26
- package/src/parts.ts +178 -7
- package/src/propose.ts +517 -56
- package/src/rig.ts +164 -11
package/README.md
CHANGED
|
@@ -43,9 +43,12 @@ is taken from the full run, because the twin tails leave the head crop sideways
|
|
|
43
43
|
blouse (recomposite error pixels 11,050 → 9,540); the default stays
|
|
44
44
|
<code>near-white</code>, the reference implementation's rule, so the examples stay
|
|
45
45
|
comparable with it. The loop is <code>spine-parts loop --palette</code>'s indexed APNG, 48 frames at
|
|
46
|
-
12 fps, 1,706,
|
|
46
|
+
12 fps, 1,706,785 bytes, one 256-entry palette at a measured error of max 57, mean 1.601
|
|
47
47
|
per channel over every frame (the lossless APNG beside it, the exactness record, is
|
|
48
|
-
13,645,
|
|
48
|
+
13,645,519 bytes; the GIF, at the same error, 1,812,625). Frame 29 (2.417 s) is the
|
|
49
|
+
closed eye: the blink holds for 0.084 s, one 12 fps frame rounded up, where the
|
|
50
|
+
reference implementation held 0.04 s and no frame of its loop showed the eyes shut
|
|
51
|
+
(issue #32); the painting is shown at half size, resampled and written
|
|
49
52
|
by this package's PNG codec (nothing here encodes JPEG). <code>spine-parts sheet</code>
|
|
50
53
|
made the contact sheet.
|
|
51
54
|
</em></p>
|
|
@@ -85,7 +88,7 @@ page** (issue #2):
|
|
|
85
88
|
| path under `--out` | what |
|
|
86
89
|
| --- | --- |
|
|
87
90
|
| `check/build/skeleton.json`, `skeleton.atlas`, `skeleton.png` | **the artifact** — Spine 4.3 skeleton data and one packed page, written by `rigc build --pack` and gated under both profiles |
|
|
88
|
-
| `parts/*.png`, `parts.json`, `recomposite_rig.png` | the loose parts, each cropped to its alpha box; the record of where every part came from
|
|
91
|
+
| `parts/*.png`, `parts.json`, `recomposite_rig.png`, `recomposite_error_rig.png` | the loose parts, each cropped to its alpha box; the record of where every part came from, how many of its pixels were re-taken from the painting, and the recomposite's uncovered holes with their boxes; the flat stack of parts; its error map — red where no part covers a pixel the painting has, blue where a part covers it in the wrong colour |
|
|
89
92
|
| `rig/` | `rig.json` and `motion.json` in spine-rigc's spec, `mesh_report.json`, the padded `images/` |
|
|
90
93
|
| `check/` | both gate files verbatim, the idle's frames, `contact.png`, `motion_heat.png`, `check.json` |
|
|
91
94
|
| `idle.png`, `idle-indexed.png`, `idle.gif` | with `--loop`: the idle as a lossless APNG (the exactness record), an indexed APNG with one shared palette (the small one) and a GIF; the last two print their palette error |
|
|
@@ -168,9 +171,9 @@ than queueing behind someone else's job.
|
|
|
168
171
|
## The loop, for an agent
|
|
169
172
|
|
|
170
173
|
```sh
|
|
171
|
-
spine-parts inputs --source painting.png --config config.json --out inputs # st_input_full.png
|
|
174
|
+
spine-parts inputs --source painting.png --config config.json --out inputs # st_input_full.png
|
|
172
175
|
# See-through on st_input_full.png (external, or `spine-parts comfy seethrough`) -> layers/full
|
|
173
|
-
spine-parts layers layers/full # every layer: box, opaque px, depth
|
|
176
|
+
spine-parts layers layers/full # every layer: box, opaque px, depth, plausibility figures; WARN lines
|
|
174
177
|
spine-parts propose --head-box --full layers/full --canvas 1664x2432
|
|
175
178
|
# -> seethrough.head_box into config.json
|
|
176
179
|
spine-parts inputs --source painting.png --config config.json --out inputs # now st_input_head.png too
|
|
@@ -179,6 +182,10 @@ spine-parts sheet --source painting.png --layers layers/full --layers layers/hea
|
|
|
179
182
|
spine-parts assemble --propose-plan --source painting.png --full layers/full --head layers/head --config config.json
|
|
180
183
|
# -> assemble.plan and extend_below_crop
|
|
181
184
|
spine-parts assemble --source painting.png --full layers/full --head layers/head --config config.json --out work
|
|
185
|
+
# -> work/rig: parts.json and parts/; the config holds no bones, meshes, regions or motion yet
|
|
186
|
+
# -> read the `uncovered hole N:` lines and look at work/render/recomposite_error_rig.png:
|
|
187
|
+
# red is painting that no part holds, and no later gate can see it
|
|
188
|
+
# -> a large red hole neither run holds? add an assemble.patches entry (cut from the painting) and assemble again
|
|
182
189
|
spine-parts propose --parts work/rig --source painting.png --out work
|
|
183
190
|
# -> proposal.json and render/landmarks.png; correct it, copy bones/meshes/regions/motion into config.json
|
|
184
191
|
spine-parts propose --parts work/rig --source painting.png --out work --from-config config.json
|
|
@@ -187,6 +194,11 @@ spine-parts build --config config.json --source painting.png --full layers/full
|
|
|
187
194
|
# -> read out/check/check.json; every FAIL line names what has to change
|
|
188
195
|
```
|
|
189
196
|
|
|
197
|
+
At each step the config holds only what that step reads; the one table of what
|
|
198
|
+
that is, step by step, is [docs/AUTHORING.md §4](docs/AUTHORING.md#4-the-command-order).
|
|
199
|
+
The selftest runs this block in order, command by command, on each fetched example,
|
|
200
|
+
from a config holding only what the first step reads (`RL01`).
|
|
201
|
+
|
|
190
202
|
`propose` is deliberately not a step of `build`: the proposal is a draft to correct
|
|
191
203
|
against its overlay, and a config with bones is `build`'s input. `rig`, `check` and
|
|
192
204
|
`loop` are the same stages one at a time. [docs/AUTHORING.md](docs/AUTHORING.md) is
|
|
@@ -202,15 +214,15 @@ agent skill.
|
|
|
202
214
|
| `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 |
|
|
203
215
|
| `comfy paint --config --out [--host]` | optional: generate the painting on a ComfyUI box from the config's `generation` block |
|
|
204
216
|
| `comfy seethrough --image --out [--host]` | optional: run the ComfyUI See-through wrapper on one image and write the form `layers` reads |
|
|
205
|
-
| `layers <dir \| layers.json \| file.psd>` | print every layer of a decomposition: draw order, name, tag group, box, size, opaque pixels, depth |
|
|
217
|
+
| `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 |
|
|
206
218
|
| `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
|
|
207
|
-
| `assemble --propose-plan …` | propose `assemble.plan` and `extend_below_crop` from the two runs |
|
|
208
|
-
| `assemble --source --full --head --config --out [--seam] [--project]` | merge the two runs into rig-space parts, `parts.json
|
|
219
|
+
| `assemble --propose-plan …` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule |
|
|
220
|
+
| `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 |
|
|
209
221
|
| `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
|
|
210
222
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
|
|
211
223
|
| `propose … --from-config <config>` | draw and LINT the config's current bones |
|
|
212
|
-
| `rig --config --parts --out` | author `rig.json` + `motion.json`, written only after spine-rigc's round trip is green |
|
|
213
|
-
| `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam, loop and the five judgement lines |
|
|
224
|
+
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after spine-rigc's round trip is green; `--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 |
|
|
225
|
+
| `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam, loop and the five judgement lines, and report the recomposite's holes from `parts.json` |
|
|
214
226
|
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
215
227
|
| `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
216
228
|
|
|
@@ -239,7 +251,9 @@ These are limits of the approach, stated so nobody reads more into a green run:
|
|
|
239
251
|
encoder, which this package does not carry.
|
|
240
252
|
- **The proposer reads tags, not pictures.** A swinging element painted inside another
|
|
241
253
|
layer (a sash tail in the skirt), hair that is none of the shapes it knows, and
|
|
242
|
-
whether an accessory swings are the corrector's to add.
|
|
254
|
+
whether an accessory swings are the corrector's to add. The one accessory shape it
|
|
255
|
+
measures is a hanging strand on a headwear or earwear layer: each gets a pendulum
|
|
256
|
+
chain, and a strand it cannot chain is named in a note rather than left stiff.
|
|
243
257
|
|
|
244
258
|
### How much of a rig the model painted
|
|
245
259
|
|
package/cli.ts
CHANGED
|
@@ -16,16 +16,17 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, wri
|
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { basename, dirname, extname, join } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
20
|
-
import { assembleStage, build, checkStage, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
21
|
-
import { findRigc, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK } from './src/check.ts';
|
|
19
|
+
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
20
|
+
import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
21
|
+
import { findRigc, PARTS_HOME_SENTENCE, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK } from './src/check.ts';
|
|
22
22
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
23
23
|
import { type CharacterConfig, loadConfig, loadEarlyConfig } from './src/config.ts';
|
|
24
24
|
import { PartsError, problemLine } from './src/errors.ts';
|
|
25
25
|
import { proposeHeadBox } from './src/headbox.ts';
|
|
26
26
|
import { makeInputs } from './src/inputs.ts';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
27
|
+
import { DEFAULT_IDLE_KEYS, IDLE_KEYS, type IdleKeys } from './src/rig.ts';
|
|
28
|
+
import { figuresPhrase, implausibleRules, layerFigures, type LayerSet, pct, readLayers, ruleSummary, times } from './src/layers.ts';
|
|
29
|
+
import { checkProposal, compare, compareLines, drawLandmarks, HIP_MIN_FRACTION, lint, lintLine, type PartSet, propose, readPartSet, serializeProposal } from './src/propose.ts';
|
|
29
30
|
import { readPng, writePng } from './src/raster/png.ts';
|
|
30
31
|
import { buildSheet, defaultCaption, type Tile, tilesFrom } from './src/sheet.ts';
|
|
31
32
|
|
|
@@ -52,9 +53,14 @@ usage:
|
|
|
52
53
|
spine-parts layers <dir | layers.json | file.psd>
|
|
53
54
|
Read a See-through decomposition — the ComfyUI wrapper form (a directory
|
|
54
55
|
holding layers.json and one PNG per layer) or an upstream .psd — and print
|
|
55
|
-
every layer: draw order, name, tag group, box, size, opaque pixels, depth
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
every layer: draw order, name, tag group, box, size, opaque pixels, depth,
|
|
57
|
+
and three plausibility figures over its opaque pixels — translucent share
|
|
58
|
+
(alpha below 128), background share (min channel above 235) and area as a
|
|
59
|
+
multiple of the rest of the figure (the other layers' union). A layer that
|
|
60
|
+
crosses a plausibility rule gets a WARN line naming the rule and the bar;
|
|
61
|
+
--propose-plan leaves it out. Refuses, by name, a missing file, an
|
|
62
|
+
unknown tag, a PNG whose size is not its box, and anything else outside
|
|
63
|
+
the input contract — never a WARN.
|
|
58
64
|
|
|
59
65
|
spine-parts sheet --source <painting.png> --layers <path> [--layers <path> ...]
|
|
60
66
|
--out <sheet.png> [--cell <px>] [--cols <n>]
|
|
@@ -68,9 +74,17 @@ usage:
|
|
|
68
74
|
Propose bones, meshes, regions and an idle from the assembled parts (<dir>
|
|
69
75
|
holds parts.json and parts/). Roles come from each part's See-through tag,
|
|
70
76
|
never its name. Writes <out>/proposal.json (config-shaped: bones, meshes,
|
|
71
|
-
regions, motion with its blink
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
regions, motion with its blink — and a blink.still cut for a lash that
|
|
78
|
+
reaches far above its eyewhite, when a clear row allows one; no blink
|
|
79
|
+
when no part is an eyewhite, and no brows in it when no part is an
|
|
80
|
+
eyebrow, each said in a note — and notes)
|
|
81
|
+
and the overlay to correct against, <out>/render/landmarks.png and
|
|
82
|
+
landmarks_head.png. Prints every
|
|
83
|
+
note and a LINT line for each chain link that lies off its mesh's art, for a
|
|
84
|
+
hip that is not below the chest, and for a hip above ${HIP_MIN_FRACTION} of the figure's
|
|
85
|
+
height (the shoulders); a headwear/earwear layer with hanging strands
|
|
86
|
+
gets one pendulum chain per strand and a note with each strand's x, rows
|
|
87
|
+
and width ("-- no chain proposed" is the one to act on; AUTHORING §3).
|
|
74
88
|
--compare prints each shared bone's distance, proposal to config, in px.
|
|
75
89
|
|
|
76
90
|
spine-parts propose --parts <dir> --source <painting.png> --out <dir> --from-config <config.json>
|
|
@@ -82,21 +96,35 @@ usage:
|
|
|
82
96
|
layers for a painting of WxH px, held inside the painting; a shift is
|
|
83
97
|
printed when one was needed.
|
|
84
98
|
|
|
85
|
-
spine-parts rig --config <config.json> --parts <dir> --out <dir>
|
|
99
|
+
spine-parts rig --config <config.json> --parts <dir> --out <dir> [--idle-keys ctl|direct]
|
|
86
100
|
Author the rig: unrotated bones at the config's landmarks (a chain makes
|
|
87
101
|
<chain>0..n), a square lattice mesh over every part in config.meshes
|
|
88
102
|
weighted by distance to its candidate bone segments, a region for every
|
|
89
|
-
part in config.regions
|
|
103
|
+
part in config.regions (a motion.blink.still part as two: the rows above
|
|
104
|
+
its row on a second slot <part>_still, which the blink does not move),
|
|
105
|
+
and one idle of sines and a blink whose closed hold is at least one
|
|
106
|
+
12 fps frame, so the idle frames and the loop show the eyes shut
|
|
107
|
+
(RIG_BLINK_HOLD_SPANS_A_FRAME otherwise); no blink when the config
|
|
108
|
+
states no motion.blink, and a blink group that names no bone is refused
|
|
109
|
+
by the loader, CONFIG_BLINK_GROUP_MEMBERS, before rigc starts. --parts is the
|
|
90
110
|
directory holding parts.json and parts/<name>.png. The result is built
|
|
91
111
|
through spine-rigc (profile spine-html, packed, then validated under
|
|
92
112
|
profile spine) in a scratch directory first, and --out receives
|
|
93
113
|
images/*.png, rig.json, motion.json and mesh_report.json only when both
|
|
94
114
|
are green. Prints one line per mesh and the rigc gate lines.
|
|
115
|
+
--idle-keys says where the idle's keys on a bone a mesh is weighted to go:
|
|
116
|
+
ctl (the default) keys a same-origin <bone>_ctl parent instead, which
|
|
117
|
+
passes A15_IDLE_NO_MESH_BONE_KEYS on any spine-rigc; direct keys the bone
|
|
118
|
+
itself and declares invariants.idleDrivesMeshes in rig.json, which needs
|
|
119
|
+
spine-rigc 1.3.0 or later and makes A15 a SKIP that prints its cost (the
|
|
120
|
+
stage prints that SKIP line). The pose is the same to one level of float
|
|
121
|
+
rounding, and so is the per-frame mesh work (AUTHORING §5).
|
|
95
122
|
spine-parts check --rig <dir> --out <dir> [--parts <dir>]
|
|
96
123
|
Build, gate, render and measure a rig through spine-rigc's CLI (the rigc at
|
|
97
124
|
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json
|
|
98
|
-
(with an "idle")
|
|
99
|
-
|
|
125
|
+
(with an "idle").
|
|
126
|
+
${PARTS_HOME_SENTENCE}.
|
|
127
|
+
Both are only read. Into --out:
|
|
100
128
|
build/ (rigc build --profile spine-html --pack: the packed atlas is the
|
|
101
129
|
artifact), gate_spine-html.txt and gate_spine.txt (the gate lines
|
|
102
130
|
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640),
|
|
@@ -114,7 +142,11 @@ usage:
|
|
|
114
142
|
STILL_REGIONS_DARK (the heat map over the face outline and the feet).
|
|
115
143
|
Regions come from parts.json's See-through tags; a line with nothing to
|
|
116
144
|
read says SKIP and why — neither a pass nor a failure — and PASS needs
|
|
117
|
-
every line that measured to be PASS.
|
|
145
|
+
every line that measured to be PASS. Then RECOMPOSITE_HOLES: REPORTED,
|
|
146
|
+
read from parts.json's recomposite block (uncovered error px, hole count,
|
|
147
|
+
the largest hole's box and the parts bordering it) — a line with no bar,
|
|
148
|
+
never a FAIL, because a pixel no part holds is missing from both sides of
|
|
149
|
+
the seam; SKIP when parts.json has no such block. Prints the pack line
|
|
118
150
|
beside the spineboy yardstick (${SPINEBOY_YARDSTICK}), a reference and not
|
|
119
151
|
a bar. Exit 0 on PASS, 1 on FAIL — every FAIL line names the bar, the value
|
|
120
152
|
and the value required.
|
|
@@ -137,14 +169,25 @@ usage:
|
|
|
137
169
|
--config <config.json> --out <dir> [--seam near-white|silhouette]
|
|
138
170
|
[--project core|visible]
|
|
139
171
|
Merge the full-body and head-crop See-through runs into rig-space parts:
|
|
140
|
-
<out>/rig/parts/<name>.png (each cropped to its alpha box), <out>/rig/parts.json
|
|
141
|
-
|
|
142
|
-
|
|
172
|
+
<out>/rig/parts/<name>.png (each cropped to its alpha box), <out>/rig/parts.json,
|
|
173
|
+
<out>/render/recomposite_rig.png and <out>/render/${ERROR_MAP_FILE} (the error
|
|
174
|
+
map: uncovered error px red, covered error px blue, the rest the painting in
|
|
175
|
+
light grey). Reads config.seethrough.head_box and
|
|
176
|
+
.resolution and config.assemble.rig_scale, .plan, .extend_below_crop and
|
|
177
|
+
.patches — extra parts cut from the painting itself over a rig-pixel box
|
|
178
|
+
(alpha "silhouette": the painting's figure inside it; "box": all of it),
|
|
179
|
+
drawn "back", "front" or {"before": <plan part>}, recorded in parts.json
|
|
180
|
+
as from "painting:<name>" and counted 100 % source — and no rig section:
|
|
181
|
+
bones, meshes, regions and motion need not exist yet, because propose
|
|
182
|
+
drafts them from these parts (AUTHORING §4, the table).
|
|
143
183
|
Prints one line per part, the seam override counts, the \`pixels:\` totals
|
|
144
184
|
(opaque = visible + occluded; taken from the painting; visible but not
|
|
145
185
|
projected), and \`recomposite vs source\` (mean |d| and % within 8 over the
|
|
146
186
|
mean channel; error px: max channel > 40; uncovered: of those, where no
|
|
147
|
-
part has alpha above 128)
|
|
187
|
+
part has alpha above 128), then the uncovered holes (8-connected) and the
|
|
188
|
+
largest ${HOLES_LISTED}, each \`uncovered hole N: <px> px at x,y wxh (between
|
|
189
|
+
"<part>" <px> px, …)\` — the list parts.json holds under recomposite.
|
|
190
|
+
Writes nothing unless every check passed.
|
|
148
191
|
--seam defaults to ${DEFAULT_SEAM_RULE}. --project says where a layer takes
|
|
149
192
|
the painting's pixel: core (the reference's) erodes every layer's top-most
|
|
150
193
|
opaque area by 5x5 first, so a part a few pixels wide takes none; visible
|
|
@@ -154,8 +197,11 @@ usage:
|
|
|
154
197
|
spine-parts assemble --propose-plan --source <painting.png> --full <dir|psd>
|
|
155
198
|
--head <dir|psd> --config <config.json>
|
|
156
199
|
Print {plan, extend_below_crop, notes} for config.assemble, from the two
|
|
157
|
-
runs.
|
|
158
|
-
|
|
200
|
+
runs. A layer \`layers\` WARNs about (PLAN_LAYER_TRANSLUCENT,
|
|
201
|
+
PLAN_LAYER_BACKGROUND, PLAN_LAYER_OVERSIZED) is not proposed, and a note
|
|
202
|
+
names it, its figures and the rule. Reads only config.seethrough.head_box,
|
|
203
|
+
config.seethrough.resolution and config.assemble.rig_scale — the rest of
|
|
204
|
+
the config need not exist yet.
|
|
159
205
|
|
|
160
206
|
spine-parts inputs --source <painting.png> --config <config.json> --out <dir>
|
|
161
207
|
Cut the two images See-through is fed: <out>/st_input_full.png (the
|
|
@@ -194,7 +240,8 @@ usage:
|
|
|
194
240
|
stops the build with its own FAIL lines. The config must already carry
|
|
195
241
|
bones, meshes, regions and motion — propose is not a step of build: run it,
|
|
196
242
|
correct the proposal against its overlay, and write the result into the
|
|
197
|
-
config. Into --out: parts/ + parts.json + recomposite_rig.png
|
|
243
|
+
config. Into --out: parts/ + parts.json + recomposite_rig.png +
|
|
244
|
+
${ERROR_MAP_FILE} (assemble),
|
|
198
245
|
rig/ (rig), check/ (check, with the packed build in check/build/), and with
|
|
199
246
|
--loop idle.png (lossless APNG), idle-indexed.png (indexed APNG) and
|
|
200
247
|
idle.gif from check/idle_frames/, then one loop: line with the three sizes
|
|
@@ -233,7 +280,8 @@ function fixed(v: number | null): string {
|
|
|
233
280
|
function printLayerTable(set: LayerSet): void {
|
|
234
281
|
console.log(`spine-parts layers: ${set.form === 'wrapper' ? 'ComfyUI wrapper form' : 'PSD'}, ${set.source}`);
|
|
235
282
|
console.log(` canvas ${set.canvas.w}x${set.canvas.h}, ${set.layers.length} layer(s), back to front`);
|
|
236
|
-
const
|
|
283
|
+
const figures = layerFigures(set);
|
|
284
|
+
const rows = set.layers.map((l, i) => [
|
|
237
285
|
String(l.drawOrder),
|
|
238
286
|
l.name,
|
|
239
287
|
l.tag.group,
|
|
@@ -242,14 +290,26 @@ function printLayerTable(set: LayerSet): void {
|
|
|
242
290
|
`${l.pixels.width}x${l.pixels.height}`,
|
|
243
291
|
String(l.opaquePx),
|
|
244
292
|
fixed(l.depth),
|
|
293
|
+
pct(figures[i].translucent),
|
|
294
|
+
pct(figures[i].background),
|
|
295
|
+
times(figures[i].areaRatio),
|
|
245
296
|
]);
|
|
246
|
-
const head = ['order', 'name', 'group', 'left,top', 'right,bottom', 'size', 'opaque_px', 'depth'];
|
|
297
|
+
const head = ['order', 'name', 'group', 'left,top', 'right,bottom', 'size', 'opaque_px', 'depth', 'translucent', 'background', 'area'];
|
|
247
298
|
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
248
299
|
const line = (cells: string[]): string => ` ${cells.map((c, i) => c.padEnd(widths[i])).join(' ')}`.trimEnd();
|
|
249
300
|
console.log(line(head));
|
|
250
301
|
for (const r of rows) console.log(line(r));
|
|
251
302
|
const painted = set.layers.filter((l) => l.opaquePx > 0).length;
|
|
252
303
|
console.log(` ${set.layers.length} layer(s): ${painted} with opaque pixels, ${set.layers.length - painted} with none`);
|
|
304
|
+
// A reader refuses nothing on plausibility: it says what --propose-plan will leave out, and why.
|
|
305
|
+
let warned = 0;
|
|
306
|
+
for (const f of figures) {
|
|
307
|
+
for (const rule of implausibleRules(f)) {
|
|
308
|
+
console.log(` WARN ${rule}: layer "${f.name}" — ${figuresPhrase(f)}; ${ruleSummary(rule)} is required, so --propose-plan leaves it out`);
|
|
309
|
+
warned++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
console.log(` ${warned} WARN line(s)`);
|
|
253
313
|
}
|
|
254
314
|
|
|
255
315
|
function cmdLayers(args: string[]): number {
|
|
@@ -298,6 +358,9 @@ function cmdSheet(args: string[]): number {
|
|
|
298
358
|
const tiles: Tile[] = [{ name: 'source', image: src, caption: basename(source) }];
|
|
299
359
|
for (const path of layers) tiles.push(...tilesFrom(path));
|
|
300
360
|
const sheet = buildSheet(tiles, cols, cell);
|
|
361
|
+
// The README's loop writes sheets/layers.png into a folder nothing made yet;
|
|
362
|
+
// every other command creates its --out, so this one does too.
|
|
363
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
301
364
|
writePng(out, sheet);
|
|
302
365
|
console.log(`spine-parts sheet: ${out}`);
|
|
303
366
|
console.log(` ${tiles.length} tile(s) in ${cols} column(s) of ${cell} px, sheet ${sheet.width}x${sheet.height}`);
|
|
@@ -326,22 +389,28 @@ function cmdRig(args: string[]): number {
|
|
|
326
389
|
let config: string | null = null;
|
|
327
390
|
let partsDir: string | null = null;
|
|
328
391
|
let out: string | null = null;
|
|
392
|
+
let idleKeys: string | null = null;
|
|
329
393
|
for (let i = 0; i < args.length; i++) {
|
|
330
394
|
const flag = args[i];
|
|
331
395
|
const value = args[i + 1];
|
|
332
|
-
if (!['--config', '--parts', '--out'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
396
|
+
if (!['--config', '--parts', '--out', '--idle-keys'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
333
397
|
if (value === undefined) return usage(`${flag} needs a value`);
|
|
334
398
|
i++;
|
|
335
399
|
if (flag === '--config') config = value;
|
|
336
400
|
else if (flag === '--parts') partsDir = value;
|
|
337
|
-
else
|
|
401
|
+
else if (flag === '--idle-keys') {
|
|
402
|
+
if (idleKeys !== null) return usage('--idle-keys is given twice');
|
|
403
|
+
idleKeys = value;
|
|
404
|
+
} else out = value;
|
|
338
405
|
}
|
|
339
406
|
if (config === null) return usage('rig needs --config <config.json>');
|
|
340
407
|
if (partsDir === null) return usage('rig needs --parts <dir> (the directory holding parts.json and parts/)');
|
|
341
408
|
if (out === null) return usage('rig needs --out <dir>');
|
|
409
|
+
const keys = idleKeys ?? DEFAULT_IDLE_KEYS;
|
|
410
|
+
if (!(IDLE_KEYS as readonly string[]).includes(keys)) return usage(`--idle-keys ${keys}; one of ${IDLE_KEYS.join(', ')} is required`);
|
|
342
411
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-rig-'));
|
|
343
412
|
try {
|
|
344
|
-
rigStage({ config, parts: partsDir, out }, rigGateRunner(), scratch, console.log);
|
|
413
|
+
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys }, rigGateRunner(), scratch, console.log);
|
|
345
414
|
return EXIT_OK;
|
|
346
415
|
} catch (err) {
|
|
347
416
|
return printRefusal(err);
|
|
@@ -396,7 +465,8 @@ function printLint(P: PartSet, spec: { bones: CharacterConfig['bones']; meshes:
|
|
|
396
465
|
const res = lint(P, spec);
|
|
397
466
|
for (const f of res.findings) console.log(lintLine(f));
|
|
398
467
|
for (const m of res.unknownMeshes) console.log(`note: mesh ${JSON.stringify(m)} names no part in parts.json, so it was not linted`);
|
|
399
|
-
console.log(
|
|
468
|
+
for (const b of res.missingTorsoBones) console.log(`note: no single bone named ${JSON.stringify(b)}, so the hip was not linted against the chest${b === 'hip' ? ' or the figure height' : ''}`);
|
|
469
|
+
console.log(`${res.findings.length} LINT line(s) over ${Object.keys(spec.meshes).length - res.unknownMeshes.length} mesh(es) and the hip`);
|
|
400
470
|
return res.findings.length;
|
|
401
471
|
}
|
|
402
472
|
|
|
@@ -541,7 +611,7 @@ function cmdAssemble(args: string[]): number {
|
|
|
541
611
|
const out = flags.get('--out') as string;
|
|
542
612
|
assembleStage(
|
|
543
613
|
{ source, full, head, config, seam: seam as SeamRule, project: project as ProjectRule },
|
|
544
|
-
{ partsJson: join(out, 'rig', 'parts.json'), partsDir: join(out, 'rig', 'parts'), recomposite: join(out, 'render', 'recomposite_rig.png') },
|
|
614
|
+
{ partsJson: join(out, 'rig', 'parts.json'), partsDir: join(out, 'rig', 'parts'), recomposite: join(out, 'render', 'recomposite_rig.png'), errorMap: join(out, 'render', ERROR_MAP_FILE) },
|
|
545
615
|
console.log,
|
|
546
616
|
);
|
|
547
617
|
return EXIT_OK;
|