spine-parts 0.8.2 → 0.10.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 +5 -3
- package/cli.ts +44 -19
- package/docs/AUTHORING.md +103 -23
- package/package.json +1 -1
- package/src/assemble.ts +19 -11
- package/src/build.ts +33 -11
- package/src/check.ts +444 -145
- package/src/comfy/painting.ts +6 -4
- package/src/config.ts +38 -12
- package/src/coords.ts +10 -1
- package/src/graphs.ts +11 -1
- package/src/headbox.ts +8 -13
- package/src/inputs.ts +37 -20
- package/src/propose.ts +118 -22
- package/src/rig.ts +205 -13
package/README.md
CHANGED
|
@@ -77,7 +77,9 @@ built from its own proposal with no hand edit at all —
|
|
|
77
77
|
- **A character config** (`config.json`) — the part plan, the head box, the bones,
|
|
78
78
|
which bones may pull which layer, and the idle's sines. `src/config.ts` is its
|
|
79
79
|
schema and documents every field; the loader refuses an unknown or a missing field
|
|
80
|
-
by name
|
|
80
|
+
by name, and reads nothing under `note`, a `<name>_note` (a string) or a key
|
|
81
|
+
beginning `x-` (any JSON — a project's own records, such as a past build's gate
|
|
82
|
+
results). [docs/AUTHORING.md](docs/AUTHORING.md) says where each value comes from.
|
|
81
83
|
|
|
82
84
|
## What it gives out
|
|
83
85
|
|
|
@@ -262,8 +264,8 @@ agent skill.
|
|
|
262
264
|
| `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
|
|
263
265
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
|
|
264
266
|
| `propose … --from-config <config>` | draw and LINT the config's current bones |
|
|
265
|
-
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after spine-rigc's gate 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 |
|
|
266
|
-
| `check --rig --out [--parts]` | build packed, gated under `spine-html`, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json` |
|
|
267
|
+
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after spine-rigc's gate is green; 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 |
|
|
268
|
+
| `check --rig --out [--parts] [--source]` | build packed, gated under `spine-html`, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json`; on a rig spec with no `parts.json` or no `idle` (a merged rig, issue #77) the gate still runs and every line that needs the missing input says SKIP by name; `--source <painting.png>` adds `SETUP_POSE_VS_SOURCE`, the setup pose against the painting by assemble's recomposite figures |
|
|
267
269
|
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
268
270
|
| `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
269
271
|
|
package/cli.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { tmpdir } from 'node:os';
|
|
|
17
17
|
import { basename, dirname, extname, join } from 'node:path';
|
|
18
18
|
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
19
19
|
import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
20
|
-
import { DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
|
|
20
|
+
import { DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, NO_PARTS_SENTENCE, SEAM_MEAN_BAR, SOURCE_SENTENCE, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
|
|
21
21
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
22
22
|
import { type CharacterConfig, loadConfig, loadEarlyConfig } from './src/config.ts';
|
|
23
23
|
import { PartsError, problemLine } from './src/errors.ts';
|
|
@@ -79,7 +79,9 @@ usage:
|
|
|
79
79
|
eyebrow, each said in a note; an iris or lash on a side with no
|
|
80
80
|
eyewhite rides head, said in a note — and notes)
|
|
81
81
|
and the overlay to correct against, <out>/render/landmarks.png and
|
|
82
|
-
landmarks_head.png.
|
|
82
|
+
landmarks_head.png. A figure with no face part gets a face box derived
|
|
83
|
+
from the head run's hair and neck, said in the first note; with no
|
|
84
|
+
head-run hair or neck it is refused (PROPOSE_FACE_PRESENT). Prints every
|
|
83
85
|
note and a LINT line for each chain link that lies off its mesh's art, for a
|
|
84
86
|
hip that is not below the chest, and for a hip above ${HIP_MIN_FRACTION} of the figure's
|
|
85
87
|
height (the shoulders); a headwear/earwear layer with hanging strands
|
|
@@ -93,13 +95,18 @@ usage:
|
|
|
93
95
|
|
|
94
96
|
spine-parts propose --head-box --full <dir | layers.json | file.psd> --canvas <W>x<H>
|
|
95
97
|
Propose seethrough.head_box (source px, square) from the full run's
|
|
96
|
-
layers for a painting of WxH px
|
|
97
|
-
|
|
98
|
+
layers for a painting of WxH px (portrait or landscape: the full run is
|
|
99
|
+
mapped back through the pad inputs added), held inside the painting; a
|
|
100
|
+
shift is printed when one was needed.
|
|
98
101
|
|
|
99
102
|
spine-parts rig --config <config.json> --parts <dir> --out <dir> [--idle-keys ctl|direct]
|
|
100
103
|
[--page-edges pot|free] [--pack-shape rect|polygon]
|
|
101
|
-
Author the rig:
|
|
102
|
-
<chain>0..n
|
|
104
|
+
Author the rig: bones at the config's landmarks (a chain makes
|
|
105
|
+
<chain>0..n, each link turned along its chain with its length, so a
|
|
106
|
+
physics constraint added later has a lever; every offset under a turned
|
|
107
|
+
link is in its frame, so nothing moves; a translate key under a turned
|
|
108
|
+
link or a scale/shear key on one is refused, RIG_KEY_FRAME_UNTURNED),
|
|
109
|
+
a square lattice mesh over every part in config.meshes
|
|
103
110
|
weighted by distance to its candidate bone segments, a region for every
|
|
104
111
|
part in config.regions (a motion.blink.still part as two: the rows above
|
|
105
112
|
its row on a second slot <part>_still, which the blink does not move),
|
|
@@ -123,12 +130,14 @@ usage:
|
|
|
123
130
|
spine-rigc 1.3.0 or later and makes A15 a SKIP that prints its cost (the
|
|
124
131
|
stage prints that SKIP line). The pose is the same to one level of float
|
|
125
132
|
rounding, and so is the per-frame mesh work (AUTHORING §5).
|
|
126
|
-
spine-parts check --rig <dir> --out <dir> [--parts <dir>] [--
|
|
127
|
-
[--pack-shape rect|polygon]
|
|
133
|
+
spine-parts check --rig <dir> --out <dir> [--parts <dir>] [--source <painting.png>]
|
|
134
|
+
[--page-edges pot|free] [--pack-shape rect|polygon]
|
|
128
135
|
Build, gate, render and measure a rig through spine-rigc's CLI (the rigc at
|
|
129
|
-
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json:
|
|
137
|
+
a rig spec, which rigc build compiles, not a compiled skeleton.json.
|
|
138
|
+
motion.json is required (rigc build takes --motion; "animations": {}
|
|
139
|
+
builds); an "idle" in it is not.
|
|
140
|
+
${PARTS_HOME_SENTENCE}; ${NO_PARTS_SENTENCE}.
|
|
132
141
|
Both are only read. Into --out:
|
|
133
142
|
build/ (rigc build --profile spine-html --pack --page-edges <value>
|
|
134
143
|
--pack-shape <value>: the packed atlas is the artifact), gate_spine-html.txt (the gate lines
|
|
@@ -163,7 +172,18 @@ usage:
|
|
|
163
172
|
read from parts.json's recomposite block (uncovered error px, hole count,
|
|
164
173
|
the largest hole's box and the parts bordering it) — a line with no bar,
|
|
165
174
|
never a FAIL, because a pixel no part holds is missing from both sides of
|
|
166
|
-
the seam; SKIP when parts.json has no such block.
|
|
175
|
+
the seam; SKIP when parts.json has no such block. What a bar cannot
|
|
176
|
+
read it says SKIP for, by name, with the reason (AUTHORING §7, Measuring a
|
|
177
|
+
rig spine-parts did not assemble): without parts.json the seam,
|
|
178
|
+
BREATH_VISIBLE, BLINK_NO_HOLE, TIP_OVER_ROOT, STILL_REGIONS_DARK and
|
|
179
|
+
RECOMPOSITE_HOLES; without an idle the loop and every line that reads idle
|
|
180
|
+
frames, and no idle is rendered. The gate always runs. The last line says
|
|
181
|
+
how many of the nine bars measured and names the skipped ones.
|
|
182
|
+
--source adds SETUP_POSE_VS_SOURCE: REPORTED, never a FAIL — the setup
|
|
183
|
+
pose against the painting by assemble's own recomposite figures (mean |d|,
|
|
184
|
+
% within 8, error px over 40, uncovered error px where the pose has alpha
|
|
185
|
+
128 or less, holes). ${SOURCE_SENTENCE}; a painting that is not this
|
|
186
|
+
rig's is refused, CHECK_SOURCE_SIZE, before anything is built. Prints the pack line
|
|
167
187
|
beside the spineboy yardstick (${SPINEBOY_YARDSTICK}), a reference and not
|
|
168
188
|
a bar. Exit 0 on PASS, 1 on FAIL — every FAIL line names the bar, the value
|
|
169
189
|
and the value required.
|
|
@@ -242,10 +262,11 @@ usage:
|
|
|
242
262
|
|
|
243
263
|
spine-parts inputs --source <painting.png> --config <config.json> --out <dir>
|
|
244
264
|
Cut the two images See-through is fed: <out>/st_input_full.png (the
|
|
245
|
-
painting centred on a white square
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
painting centred on a white square of its longer side: white left and
|
|
266
|
+
right of a portrait painting, above and below a landscape one) and, when
|
|
267
|
+
the config sets seethrough.head_box, <out>/st_input_head.png (that box,
|
|
268
|
+
cropped at its exact size). Refuses a translucent painting and a head box
|
|
269
|
+
outside the painting, by name.
|
|
249
270
|
|
|
250
271
|
spine-parts comfy seethrough --image <png> --out <dir> [--host <url>]
|
|
251
272
|
[--resolution 1024] [--steps 30] [--seed 42] [--offload] [--lama] [--nf4]
|
|
@@ -516,7 +537,7 @@ function flags(args: string[], known: readonly string[], command: string, option
|
|
|
516
537
|
}
|
|
517
538
|
|
|
518
539
|
function cmdCheck(args: string[]): number {
|
|
519
|
-
const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--page-edges', '--pack-shape']);
|
|
540
|
+
const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--source', '--page-edges', '--pack-shape']);
|
|
520
541
|
if (typeof f === 'string') return usage(f);
|
|
521
542
|
const rig = f.get('--rig') as string;
|
|
522
543
|
const out = f.get('--out') as string;
|
|
@@ -526,7 +547,7 @@ function cmdCheck(args: string[]): number {
|
|
|
526
547
|
if (!isPackShape(shape)) return usage(shape);
|
|
527
548
|
try {
|
|
528
549
|
const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
529
|
-
const r = checkStage({ rig, parts: f.get('--parts')
|
|
550
|
+
const r = checkStage({ rig, parts: f.get('--parts'), source: f.get('--source'), out, pageEdges: edges, packShape: shape }, rigcRunner(bin), bin, console.log);
|
|
530
551
|
return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
|
|
531
552
|
} catch (err) {
|
|
532
553
|
return printRefusal(err);
|
|
@@ -815,7 +836,11 @@ function cmdInputs(args: string[]): number {
|
|
|
815
836
|
mkdirSync(out, { recursive: true });
|
|
816
837
|
writePng(join(out, 'st_input_full.png'), r.full);
|
|
817
838
|
console.log(`spine-parts inputs: ${source} (${painting.width}x${painting.height}) -> ${out}`);
|
|
818
|
-
console.log(
|
|
839
|
+
console.log(
|
|
840
|
+
r.padTop > 0
|
|
841
|
+
? ` st_input_full.png ${r.full.width}x${r.full.height}: the painting at y ${r.padTop}, white above and below (${r.padTop} + ${r.full.height - painting.height - r.padTop} px)`
|
|
842
|
+
: ` st_input_full.png ${r.full.width}x${r.full.height}: the painting at x ${r.padLeft}, white either side (${r.padLeft} + ${r.full.width - painting.width - r.padLeft} px)`,
|
|
843
|
+
);
|
|
819
844
|
if (r.head !== null && r.headBox !== null) {
|
|
820
845
|
writePng(join(out, 'st_input_head.png'), r.head);
|
|
821
846
|
console.log(` st_input_head.png ${r.head.width}x${r.head.height}: seethrough.head_box [${r.headBox.join(', ')}]`);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -47,9 +47,18 @@ examples use, and the one this guide assumes:
|
|
|
47
47
|
|
|
48
48
|
`src/config.ts` is the schema and the loader. Every key is known or refused
|
|
49
49
|
(`CONFIG_KEY_KNOWN`); a required key that is missing is `CONFIG_FIELD_PRESENT`.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
reason there, beside the value it corrected
|
|
50
|
+
There are two open doors, and nothing reads what stands behind either. An
|
|
51
|
+
annotation: `note`, and any key ending in `_note`, may hold a string anywhere an
|
|
52
|
+
object is — write a hand correction's reason there, beside the value it corrected
|
|
53
|
+
(the examples do). A record: any key beginning `x-` with a name after it may hold
|
|
54
|
+
any JSON value in the same places — a project's own provenance, such as the gate
|
|
55
|
+
results of a past build (`"x-status": {…}`) or the seeds a search rejected, with
|
|
56
|
+
reasons (`generation.x-seeds_tried`). The record test comes first, so
|
|
57
|
+
`x-seed_note` is a record. `xstatus`, `x_status`, `X-status` and a bare `x-` are
|
|
58
|
+
not record names and are refused like any unknown key. The doors are not open in
|
|
59
|
+
`meshes`, `regions` or `motion.blink.still`: their keys are part names, and an
|
|
60
|
+
`x-` key there is a part like any other. Every refusal of an unknown key, of a
|
|
61
|
+
retired one and of an annotation that is not a string says where a record goes.
|
|
53
62
|
|
|
54
63
|
**Coordinates** in `bones`, `meshes` and `motion` are **rig pixels, y down, origin
|
|
55
64
|
top-left** — the parts' own space, which is the painting times `assemble.rig_scale`.
|
|
@@ -71,11 +80,11 @@ read by no CPU stage).
|
|
|
71
80
|
| `assemble.plan` | assemble; also the list every `meshes`/`regions` key must come from | **proposed** by `assemble --propose-plan`. `[part name, "full" \| "head", See-through tag]`, in draw order back to front. Names are free; roles come from the tag |
|
|
72
81
|
| `assemble.extend_below_crop` | assemble | **proposed** with the plan: `{part, run, tag}` — a head-run part that reaches the bottom of the head crop is continued from a full-run layer, whole connected components |
|
|
73
82
|
| `assemble.patches` | assemble; `meshes`/`regions` keys may name them too | **authored**, optional: `[{name, box, alpha, draw}]` — an extra part cut from the **painting** itself, for figure no See-through layer holds (a hem both runs dropped). `box` is `[x0, y0, x1, y1]` in **rig** pixels, `x1`/`y1` exclusive — the space of `parts.json` and `recomposite_rig.png`, where the hole is found; `alpha` is `"silhouette"` (the painting's figure silhouette inside the box, the one `--seam silhouette` uses) or `"box"` (the whole box); `draw` is `"back"`, `"front"` or `{"before": "<plan part>"}`. A patch is always a region: its bone is `regions.<name>`, in the one place every region's bone lives, and a `meshes` entry for it is refused. See §5 for how to place one |
|
|
74
|
-
| `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip}` whose links are named `<chain>0 … <chain>n`. Parents come before children |
|
|
83
|
+
| `bones` | rig; `propose --from-config` | **proposed** by `propose`, then corrected. A single bone `{name, parent, at, tip?}` or a chain `{chain, parent, points, tip}` whose links are named `<chain>0 … <chain>n`. Parents come before children. In `rig.json` each link is **turned along its chain** — `rotation` is the direction from its origin to the next link's (the last link's to `tip`) and `length` that distance (issue #73), so a physics constraint added downstream finds a lever; under `--idle-keys ctl` the link's `<link>_ctl` carries the turn and the length and the link sits at local rotation 0 beneath it. A single bone is never turned; one whose parent is a link is turned back upright |
|
|
75
84
|
| `meshes.<part>` | rig | **proposed**, then corrected. `grid` (lattice cell size, px), `r` (added to every distance before weighting: `w = 1/(d + r)²`), `segments` (a chain name, a bone name with a `tip`, or `[bone, [x0,y0], [x1,y1]]`). The segment list is the one authored decision about a layer: which bones may pull it. The slot's bone is the first segment's |
|
|
76
85
|
| `regions.<part>` | rig | **proposed**: the bone a rigid part rides. Every plan part is exactly one of a mesh or a region (`CONFIG_PART_ATTACHED`) |
|
|
77
86
|
| `motion.duration` | rig; check (the loop is measured at this time) | proposed as 4 s; a whole number of 1/12 s ticks, because `check` renders at 12 fps |
|
|
78
|
-
| `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`). A bone property is keyed by one track (`CONFIG_BONE_PROPERTY_KEYED_ONCE`): a chain keys `rotate` on every link, the blink's `eyes` group `scaley` and its `brows` group `translatey` on every member, so a single track on any of those is a second track on it — rigc would refuse it one stage later, `animation "idle" has two tracks on eye.scaley` |
|
|
87
|
+
| `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`). A bone property is keyed by one track (`CONFIG_BONE_PROPERTY_KEYED_ONCE`): a chain keys `rotate` on every link, the blink's `eyes` group `scaley` and its `brows` group `translatey` on every member, so a single track on any of those is a second track on it — rigc would refuse it one stage later, `animation "idle" has two tracks on eye.scaley`. A chain link is turned along its chain (the `bones` row), so a `translatex`/`translatey` key on a bone whose parent is a turned link, or a `scale`/`shear` key on a turned link, would move or stretch along the link and not along the picture's axis: the rig stage refuses it (`RIG_KEY_FRAME_UNTURNED`); `rotate` is keyed on any bone |
|
|
79
88
|
| `motion.blink` | rig | **proposed**: `{t, eyes, brows, squash, brow_drop}`; the whole blink, `t` to `t + 0.364` s (`t + 0.314` s without brows: the brows' keys are the ones that end last), must fit inside the idle (`RIG_BLINK_INSIDE_IDLE`). The `eyes` group's `scaley` squashes every part on the eye bones about the bone's origin, the eyewhite's centre. Optional: a figure with no `eyewhite-r`/`-l` part has no eye bone, so `propose` writes no `blink` and notes `no blink: no eyewhite part (looked for: eyewhite-r, eyewhite-l), …`; with eyes and no `eyebrow-r`/`-l` part it writes the blink without `brows` and `brow_drop` and notes `blink without brows: …`. `brows` and `brow_drop` are stated together or not at all (`CONFIG_BLINK_BROWS_PAIRED`), and a group that names no bone is refused (`CONFIG_BLINK_GROUP_MEMBERS`) — rigc would refuse it one stage later, `group "eyes" declares no members` — as is a group that names one bone twice (`CONFIG_BLINK_GROUP_UNIQUE`; rigc: `group "eyes" names member "eye" twice`). An `irides-*` or `eyelash-*` part on a side with no eyewhite has no eye bone to ride: `propose` places it on `head` as a region, which the blink does not move, and notes `no eyewhite part for eye_r (looked for: eyewhite-r): iris (head:irides-r) is placed on head as a region, …`. The timing is fixed, not a field (table below) |
|
|
80
89
|
| `motion.blink.still` | rig | **proposed** only for a lash `propose` notes (below), then checked: `{<part>: {row, bone}}` — the rows of that region part above `row` (rig px, y down) are drawn by a second slot `<part>_still` on `bone` and do not blink; rows from `row` down keep the part's slot and blink. The part must be a region on a bone `eyes` names, `bone` one it does not (`CONFIG_STILL_OFF_THE_BLINK`), and `row` a row of the part with no art across its whole width (`RIG_STILL_ROW_CLEAR`) |
|
|
81
90
|
|
|
@@ -112,6 +121,23 @@ chain — or, when the accessory has hanging strands, one pendulum chain per str
|
|
|
112
121
|
swing, hair of another shape, whether an accessory swings — is yours to add, and
|
|
113
122
|
`notes` in `proposal.json` says where it guessed.
|
|
114
123
|
|
|
124
|
+
**A figure with no `face` part** (turned away, or half hidden behind a partner,
|
|
125
|
+
so See-through found hair and a neck and no face) still gets a proposal. The face
|
|
126
|
+
box every rule scales by is derived from the head run's hair (`front hair`,
|
|
127
|
+
`back hair`: their highest top) and its `neck` (its top, and its centre x for the
|
|
128
|
+
axis): with `L` the span from the hair's top to the neck's top, the face is
|
|
129
|
+
1.067 `L` tall, its top 0.131 `L` below the hair's, its width 0.798 of its height.
|
|
130
|
+
Those ratios are the means of the two public examples (`demo` 1.073 / 0.105 /
|
|
131
|
+
0.820, `sample` 1.061 / 0.158 / 0.777) — a sample of two. On each example
|
|
132
|
+
re-assembled without its face, the derived box put `head` and `neck` within
|
|
133
|
+
5.0 px (0.038 face heights) of the with-face proposal's, `chest` and `hip` at
|
|
134
|
+
the same pixel. The first note says so —
|
|
135
|
+
`no face part: face box derived from the head run's hair (…; top y=…) and neck (…), a span of … px — … (ratios measured on the two public examples); …`
|
|
136
|
+
— and names each face-reading rule left with nothing to read (eye bones and blink,
|
|
137
|
+
brows, mouth). Correct `head` and `neck` against the overlay. With no head-run
|
|
138
|
+
hair or neck there is nothing to derive from, and `PROPOSE_FACE_PRESENT` still
|
|
139
|
+
refuses (§6).
|
|
140
|
+
|
|
115
141
|
Two rules read the figure rather than one tag, and each says so in a note:
|
|
116
142
|
|
|
117
143
|
- **The hip.** It sits 0.14 face heights below `bottomwear`'s top edge — unless
|
|
@@ -216,9 +242,13 @@ full loader before its assemble stage writes anything.
|
|
|
216
242
|
generates `painting_<seed>.png` on a ComfyUI box; any other route to a painting
|
|
217
243
|
skips this step and leaves `generation` out. Config: the `paint` row.
|
|
218
244
|
1. **The full image.** `spine-parts inputs --source inputs/painting.png --config config.json --out inputs`
|
|
219
|
-
writes `st_input_full.png`, the painting centred on a white square
|
|
220
|
-
|
|
221
|
-
|
|
245
|
+
writes `st_input_full.png`, the painting centred on a white square of its longer
|
|
246
|
+
side: white left and right of a portrait painting, above and below a landscape
|
|
247
|
+
one. The pad is See-through's input only; `propose --head-box` and `assemble` map
|
|
248
|
+
the full run back through it, so the rig stays in the painting's pixels — unlike
|
|
249
|
+
a painting padded to a square by hand, whose padding becomes part of the rig.
|
|
250
|
+
Config: the `layers` row, `head_box` not yet. A translucent painting is refused
|
|
251
|
+
(`INPUTS_PAINTING_OPAQUE`).
|
|
222
252
|
2. **See-through, full run** (external).
|
|
223
253
|
3. `spine-parts layers inputs/layers/full` — read it. A refusal here is about the
|
|
224
254
|
files, not the art (§6, *Reading the inputs*). A `WARN` line is about the art: a
|
|
@@ -265,9 +295,9 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
|
|
|
265
295
|
| `layers` | the table, then the `WARN` lines | every tag the plan will need has opaque pixels; one `face` in the head run; `0 WARN line(s)` | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`); a `WARN PLAN_LAYER_…` line (§6, *Plausibility*) |
|
|
266
296
|
| `sheet` of both runs | the tile list (and the sheet, if you can see) | eyes, irises, lashes and brows as left/right pairs in the head run | a head box that cut off an ornament: move `head_box`, re-run the head crop |
|
|
267
297
|
| `assemble` | one line per part, the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`; and look at `render/recomposite_error_rig.png` | on the examples: `sample` 0.84 / 98.0 % / 4,512 / 1,185, 259 holes, the largest 94 px; `demo` 2.39 / 95.8 % / 11,050 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (the demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. When neither run holds it at all, an `assemble.patches` entry cuts it from the painting (below) |
|
|
268
|
-
| `propose` | `note:` lines, `LINT` lines, `landmarks.png` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter | a link off the art (a bone on the background) — move it onto the layer; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head` |
|
|
269
|
-
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`) | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages) | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
|
|
270
|
-
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:`, `seam:`, the six judgement lines and `
|
|
298
|
+
| `propose` | `note:` lines, `LINT` lines, `landmarks.png` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter | a link off the art (a bone on the background) — move it onto the layer; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head`; a `no face part: face box derived from …` note (always the first) — `head`, `neck` and every face-height scale come from a box guessed off the hair and neck (§3): check `head` and `neck` on `landmarks_head.png` and move them; when it adds `the blink shuts the eyes over no face part`, read `BLINK_NO_HOLE` after `check` — an eye with no face under it can open a hole (the `sample` with only its face removed: 587 px) |
|
|
299
|
+
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`); in `rig.json`, each chain link's (or its `_ctl`'s) `length` and `rotation` | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages); every link's `length` the distance to the next link and `rotation` its direction (Spine degrees, counter-clockwise, y up, local to the parent), every offset under it — a child bone's `x, y`, a weight's bind `x, y`, a region's `x, y` — in that turned frame, and a region on a link carrying `rotation` that turns it back upright. Nothing moved: `flattenRig` (`src/rig.ts`) turns every offset back and gives the unturned numbers | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
|
|
300
|
+
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:`, `seam:`, the six judgement lines, `RECOMPOSITE_HOLES` and, under `--source`, `SETUP_POSE_VS_SOURCE` (§7), `check.json`; the last line says how many of the nine bars measured and names the ones that said SKIP | `check: PASS; 9 of 9 bar(s) measured, 0 skipped` on the examples, and a judgement line SKIP only where the character lacks what it reads; on a rig spec with no `parts.json` or no `idle` (a merged rig, §7 *Measuring a rig spine-parts did not assemble*), PASS with the skipped bars named | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6) |
|
|
271
301
|
| `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line, each file's line, then `loop: idle.png N B (lossless); idle-indexed.png N B (max …, mean …); idle.gif N B (max …, mean …)` | `f0048.png equals f0000.png byte for byte, so it is dropped` | `LOOP_ENCODE` (§6) |
|
|
272
302
|
|
|
273
303
|
`loop` writes three files from one frame set, and they are not interchangeable.
|
|
@@ -570,7 +600,7 @@ See-through with another seed.
|
|
|
570
600
|
| rule | means | change |
|
|
571
601
|
| --- | --- | --- |
|
|
572
602
|
| `CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON` | no such file, or not JSON | `--config` |
|
|
573
|
-
| `CONFIG_KEY_KNOWN`, `CONFIG_KEY_RETIRED`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE` | an unknown key (a retired one says what replaces it), a missing one, a wrong type | the named field |
|
|
603
|
+
| `CONFIG_KEY_KNOWN`, `CONFIG_KEY_RETIRED`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE` | an unknown key (a retired one says what replaces it), a missing one, a wrong type; a project's own record under a plain key, or an annotation (`note`, `<name>_note`) holding something other than a string | the named field; a record goes under a key beginning `x-` (any JSON, read by nothing), a remark under `note` or `<name>_note` (a string) — §3 |
|
|
574
604
|
| `CONFIG_HEAD_BOX_SQUARE` | `seethrough.head_box` is not a non-empty square | `head_box` — take `propose --head-box`'s |
|
|
575
605
|
| `CONFIG_TAG_KNOWN`, `CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` | a plan entry names no v3 tag, a part name that is not a file name, or a part or layer twice — a patch named like a plan part or another patch included | `assemble.plan`, `assemble.patches` |
|
|
576
606
|
| `CONFIG_NAME_RESOLVES` | a parent, segment, region, track, blink member, blink still part or bone, extend part or patch `draw.before` names nothing declared above it | the named reference, or the declaration it needs |
|
|
@@ -590,7 +620,7 @@ See-through with another seed.
|
|
|
590
620
|
| rule | means | change |
|
|
591
621
|
| --- | --- | --- |
|
|
592
622
|
| `INPUTS_SOURCE_PRESENT` | no such painting | `--source` |
|
|
593
|
-
| `
|
|
623
|
+
| `INPUTS_PAINTING_OPAQUE` | the painting has a pixel below alpha 255 | the painting |
|
|
594
624
|
| `INPUTS_HEAD_BOX_INSIDE` | `seethrough.head_box` leaves the painting (the crop is not padded) | `head_box` — take `propose --head-box`'s |
|
|
595
625
|
|
|
596
626
|
### The optional ComfyUI adapter (`comfy paint`, `comfy seethrough`)
|
|
@@ -609,7 +639,6 @@ See-through with another seed.
|
|
|
609
639
|
| rule | means | change |
|
|
610
640
|
| --- | --- | --- |
|
|
611
641
|
| `ASSEMBLE_FIELD_PRESENT` | `seethrough.head_box` is missing | `propose --head-box` proposes it (§3) |
|
|
612
|
-
| `ASSEMBLE_SOURCE_PORTRAIT` | the painting is wider than tall | the painting |
|
|
613
642
|
| `ASSEMBLE_RIG_SIZE` | `rig_scale` makes an empty rig | `assemble.rig_scale` |
|
|
614
643
|
| `ASSEMBLE_HEAD_BOX_INSIDE` | the head box is outside the painting | `head_box` — `propose --head-box` holds it inside |
|
|
615
644
|
| `ASSEMBLE_RUN_CANVAS` | a run's canvas is not `resolution` square | `seethrough.resolution`, or the run |
|
|
@@ -623,9 +652,9 @@ See-through with another seed.
|
|
|
623
652
|
|
|
624
653
|
| rule | means | change |
|
|
625
654
|
| --- | --- | --- |
|
|
626
|
-
| `HEADBOX_FACE_PRESENT`, `HEADBOX_RUN_SQUARE`, `HEADBOX_CANVAS_SIZE`, `
|
|
655
|
+
| `HEADBOX_FACE_PRESENT`, `HEADBOX_RUN_SQUARE`, `HEADBOX_CANVAS_SIZE`, `HEADBOX_FITS_CANVAS` | the full run has no face or is not square; `--canvas` is not a positive size; or the painting cannot hold the proposed box at its size — its shorter side is the bound (shrinking it would cut the head) | `--full`, `--canvas`, or the painting |
|
|
627
656
|
| `PROPOSE_SOURCE_PRESENT`, `PROPOSE_PNG_PRESENT`, `PROPOSE_PNG_MATCHES_BOX` | the painting or a part PNG is missing, or a PNG is not its box | `--source`, `--parts` (re-run assemble) |
|
|
628
|
-
| `PROPOSE_FACE_PRESENT` | no part comes from a `face` layer
|
|
657
|
+
| `PROPOSE_FACE_PRESENT` | no part comes from a `face` layer, and the face-less fallback cannot derive a face box either: it needs the head run's `neck` and at least one head-run `front hair`/`back hair` part whose top is above the neck's, and the message names the half that is missing. With both, `propose` does not refuse — it derives the box and says so in its first note (§5) | `assemble.plan`: take the face, or the head run's hair and neck |
|
|
629
658
|
| `PROPOSE_ACCESSORY_BODY` | an accessory has nothing above its pendant rows to hang its bone on | that part's plan entry, or author its bones by hand |
|
|
630
659
|
| `PARTS_*` (`PARTS_FILE_PRESENT`, `PARTS_IS_JSON`, `PARTS_KEY_KNOWN`, `PARTS_FIELD_PRESENT`, `PARTS_FIELD_TYPE`, `PARTS_NAME_UNIQUE`, `PARTS_FROM_KNOWN`, `PARTS_BOX_INSIDE_RIG`, `PARTS_COUNTS_ADD_UP`, `PARTS_HOLES_LARGEST_FIRST`, `PARTS_HOLE_PART_KNOWN`) | the `parts.json` read is not assemble's contract (`PARTS_COUNTS_ADD_UP`: `visible_px + occluded_px` is not `opaque_px`, `visible_not_projected_px` is above `visible_px`, or the `recomposite` block's figures disagree — more uncovered than error pixels, a hole larger than its box, the wrong number of holes listed, listed areas that do not sum to `uncovered_error_px`; the last two codes are the block's order and its border names, §5) | re-run assemble; do not edit `parts.json` |
|
|
631
660
|
|
|
@@ -638,6 +667,7 @@ See-through with another seed.
|
|
|
638
667
|
| `RIG_NAME_RESOLVES`, `RIG_SEGMENT_DEFINED`, `RIG_CHAIN_POINTS` | a segment names an undeclared bone, a bone with no `tip` and no next link, or a chain too short to make a link | that bone's `tip`, or write the segment out as `[bone, [x0,y0], [x1,y1]]` |
|
|
639
668
|
| `RIG_LATTICE_ONE_LOOP` | the lattice over a part does not close into one outline even after the repair passes | that mesh's `grid` |
|
|
640
669
|
| `RIG_CONTROL_NAME_FREE` | a keyed, mesh-weighted bone needs `<bone>_ctl` and that name is taken | rename the declared bone |
|
|
670
|
+
| `RIG_KEY_FRAME_UNTURNED` | a `translate` key on a bone whose parent is a chain link turned along its chain, or a `scale`/`shear` key (a track, or the blink's `eyes`/`brows` group) on a turned link: the key would act along the link's axis, not the picture's; the detail names the track, the bone and the turn | key the chain's own parent, or key `rotate`; a link whose turn is 0 (a level chain) keeps every key |
|
|
641
671
|
| `RIG_BLINK_INSIDE_IDLE` | the blink runs outside the idle | `motion.blink.t` |
|
|
642
672
|
| `RIG_BLINK_HOLD_SPANS_A_FRAME` | the tree's `BLINK.hold` is shorter than one frame at `IDLE_FPS`, so for some `motion.blink.t` no idle frame — and no frame of the loop — shows the closed eye; the detail counts the phases that miss and names the first | nothing in the config: `BLINK.hold` in `src/motion.ts`, at least `1/IDLE_FPS` s (§3) |
|
|
643
673
|
| `RIG_STILL_ROW_INSIDE_PART`, `RIG_STILL_ROW_CLEAR`, `RIG_STILL_PIECES_HAVE_ART`, `RIG_STILL_NAME_FREE` | a `motion.blink.still` row that is not strictly inside the part, that crosses art (a cut through art changes the render even at rest), that leaves one piece with no art, or whose `<part>_still` slot name another part already has | that entry's `row` — a row with no art between the crease and the lash line — or rename the other part |
|
|
@@ -648,8 +678,11 @@ See-through with another seed.
|
|
|
648
678
|
| rule | means | change |
|
|
649
679
|
| --- | --- | --- |
|
|
650
680
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
651
|
-
| `CHECK_INPUT_PRESENT` on `…/parts.json` | `--parts` named a directory without `parts.json` — most often `parts/` itself. The detail says it: "--parts names the directory holding parts.json and parts/, not parts/ itself (after build, that is build's --out, whose rig is <out>/rig); without --parts it is --rig", and names the parent when the parent holds `parts.json` | `--parts` — the directory above `parts/` |
|
|
652
|
-
| `CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_PART_SLOT_PRESENT`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` (`CHECK_PART_SLOT_PRESENT`: a part with no slot of its own name, which the judgement lines render it by) | re-run rig (`build` does both) |
|
|
681
|
+
| `CHECK_INPUT_PRESENT` on `…/parts.json` | `--parts` named a directory without `parts.json` — most often `parts/` itself — or, without `--parts`, the rig directory has none while the directory above it does (build's layout, `<out>/rig` under `<out>/parts.json`). The detail says it: "--parts names the directory holding parts.json and parts/, not parts/ itself (after build, that is build's --out, whose rig is <out>/rig); without --parts it is --rig", and names the parent when the parent holds `parts.json`. With neither (no `--parts`, and no `parts.json` beside `rig.json` or above it), the rig is measured without parts and nothing is refused (§7) | `--parts` — the directory above `parts/` |
|
|
682
|
+
| `CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_PART_SLOT_PRESENT`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` (`CHECK_PART_SLOT_PRESENT`: a part with no slot of its own name, which the judgement lines render it by). `motion.json` is required even with no animation in it, because `rigc build` takes `--motion` (a spec with `"animations": {}` builds green); `CHECK_IDLE_PRESENT` is an `idle` that is there with no positive `duration` — an absent `idle` is measured around (§7) | re-run rig (`build` does both) |
|
|
683
|
+
| `CHECK_SOURCE_SIZE` | the painting `--source` names is not this rig's: no single `rig_scale` takes its width and height to the stage's (assemble makes the canvas `trunc(w × rig_scale)` x `trunc(h × rig_scale)`), or, with `parts.json` read, its `scale_rig_per_source` does not; or the stage is not whole pixels. Refused before anything is built | the painting the rig was made from — the stage's own size, or the painting `assemble` was given |
|
|
684
|
+
| `CHECK_SOURCE_PNG` | `--source` names a file that does not read as a PNG | a PNG |
|
|
685
|
+
| `CHECK_SOURCE_GRID` | the setup-pose still and its black-tinted twin (the coverage `SETUP_POSE_VS_SOURCE` reads) did not come back on one grid over one opaque background | a rigc problem; report it |
|
|
653
686
|
| `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^2.10.1), or put a newer `rigc` first on `PATH` |
|
|
654
687
|
| `CHECK_RIGC_ENTRY`, `CHECK_RIGC_ENTRY_READS` | `rigc --version` names no entry (a rigc below 2.0.0), or names one in neither launcher form (`entry: cli.ts — @esotericsoftware/spine-core <v> present`, `entry: cli_core.ts — @esotericsoftware/spine-core absent — …`); `check.json`'s `rigc_entry` records the one that gated the build | `bun install` |
|
|
655
688
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
@@ -672,7 +705,9 @@ See-through with another seed.
|
|
|
672
705
|
## 7. The bars `check` enforces, and what only an eye answers today
|
|
673
706
|
|
|
674
707
|
`check.json`'s `PASS` is true exactly when all of these hold (the reference
|
|
675
|
-
implementation's bars)
|
|
708
|
+
implementation's bars) — each one that measured; a rig with no `parts.json` or no
|
|
709
|
+
`idle` skips the ones that read it, by name (*Measuring a rig spine-parts did not
|
|
710
|
+
assemble*, at the end of this section):
|
|
676
711
|
|
|
677
712
|
- `gate_spine-html.txt`: every summary line `(N passed, 0 failed)` — the packed build
|
|
678
713
|
under the `spine-html` profile, gated once over the compile and once over the packed
|
|
@@ -703,12 +738,12 @@ twice the worse one's, so the weaker example clears it by a factor of two; a bar
|
|
|
703
738
|
model itself fixes (a part on an unkeyed bone does not move, a lag is above 0, a hole
|
|
704
739
|
is 0 pixels) is that value, not a margin — and where the model's value is an error
|
|
705
740
|
the rig itself measures (a face the head carries is still in the head's frame up to
|
|
706
|
-
the resampler's error), the bar is that measurement times the same two. The figures are this port's, measured on
|
|
741
|
+
the resampler's error), the bar is that measurement times the same two. A bar is set once, from the figures of the day it was set, and is not chased: the torso floor 3.809 is half of sample's 7.618, which reads 7.617 since the chain links are turned (issue #73), a factor of 1.9997. The figures are this port's, measured on
|
|
707
742
|
the two examples [observed]:
|
|
708
743
|
|
|
709
744
|
| line | measures | bar | `demo` | `sample` | SKIP when |
|
|
710
745
|
| --- | --- | --- | --- | --- | --- |
|
|
711
|
-
| `BREATH_VISIBLE` | `topwear` parts rendered alone (`rigc render --slot`): heat mean over their box; `footwear` parts alone: heat max over theirs | torso mean ≥ 3.809; feet max ≤ 0 | 15.
|
|
746
|
+
| `BREATH_VISIBLE` | `topwear` parts rendered alone (`rigc render --slot`): heat mean over their box; `footwear` parts alone: heat max over theirs | torso mean ≥ 3.809; feet max ≤ 0 | 15.251; 0 | 7.617; 0 | no `topwear` or no `footwear` part |
|
|
712
747
|
| `BLINK_NO_HOLE` | the setup pose with every `scaley` track on the eyewhite slots' bones held at its closed value, against the setup pose, at full size: pixels in the eyewhite box that show the page where the open eye had art | 0 px | 0 | 0 | no `eyewhite` part, or no `scaley` track on its bones goes below its first key |
|
|
713
748
|
| ″ (reported) | the same box: each closed-eye pixel's max-channel distance to the nearest colour the open eye's box holds — max, and pixels over 40 | none | 15; 0 | 11; 0 | as above |
|
|
714
749
|
| `CHAIN_LAG` | `motion.json`'s rotate tracks read as sines (DFT of the keys: period, amplitude, phase) and arranged by the bone tree — a keyed bone's parent is its nearest keyed ancestor | every lag ≥ 0.001 cycle; amplitude non-decreasing down each unbranched chain | lags 0.040 (neck to head) to 0.120; 12 chains | lags 0.040 to 0.100; 9 chains | no rotate track under another of the same period |
|
|
@@ -733,6 +768,24 @@ part edges, and whether a hole matters depends on where it is, which the box and
|
|
|
733
768
|
bordering parts say and a count cannot. Read it as a figure, quote it in a report, and
|
|
734
769
|
look at `recomposite_error_rig.png` when the largest hole is more than a sliver.
|
|
735
770
|
|
|
771
|
+
Under `--source <painting.png>`, a second reported line, also never a FAIL
|
|
772
|
+
(issue #77):
|
|
773
|
+
|
|
774
|
+
| line | reports | bar | `demo` | `sample` | SKIP when |
|
|
775
|
+
| --- | --- | --- | --- | --- | --- |
|
|
776
|
+
| `SETUP_POSE_VS_SOURCE` | the setup pose against the painting, by `assemble`'s own `measureRecomposite` over the stage's pixels — `mean_abs` and `within_share` (mean channel, within 8), `error_px` (max channel over 40), `uncovered_error_px` (of those, where the pose has alpha 128 or less), `hole_count` and the largest hole's box — with `painting_px`, `stage_px`, `render_px` and `render_scale` beside them. The painting is resampled onto the stage as assemble resamples it (lanczos3, alpha dropped). rigc renders onto an opaque grey, so the pose over white and its alpha are read off a second still of the same pose with every slot tinted black; both are carried onto the stage with the seam's map turned round (bilinear) | none — status `REPORTED`; assemble applies no bar to the figures it measures, so none is derived here | 3.146 / 93.43 % / 12,399 / 1,732, 518 holes, the largest 61 px at 351,157 6x20 (assemble's recomposite: 2.391 / 95.81 % / 11,050 / 1,564, 453) | 1.095 / 97.30 % / 5,348 / 1,239, 313 holes, the largest 88 px at 479,416 6x29 (assemble's: 0.835 / 97.95 % / 4,512 / 1,185, 259) | not written without `--source` |
|
|
777
|
+
|
|
778
|
+
The figures are assemble's by construction, not a second definition beside them;
|
|
779
|
+
what differs is what they are taken of — the pose rigc draws, not the flat stack —
|
|
780
|
+
and the gap between the two columns above is that drawing: the render's resampling at
|
|
781
|
+
its scale (0.942 on the demo, 1.012 on the sample) and the warp back onto the stage,
|
|
782
|
+
which soften every part edge by a pixel or two. It moves with the render's grid
|
|
783
|
+
[observed, the demo's still rendered at four sizes: mean 2.663 at scale 1.0003, 3.091
|
|
784
|
+
at 1.0073, 3.146 at 0.942, 3.186 at 1.884], so compare the line with itself across
|
|
785
|
+
rigs or edits, and with `RECOMPOSITE_HOLES` for where a hole is, not digit for digit
|
|
786
|
+
with the recomposite. On the examples the line reads the same with and without
|
|
787
|
+
`parts.json` (the still's grid does not move).
|
|
788
|
+
|
|
736
789
|
What each figure is, and is not:
|
|
737
790
|
|
|
738
791
|
- **The seam is the answer to "at rest, no gap, white rim or doubled line between
|
|
@@ -779,8 +832,9 @@ What each figure is, and is not:
|
|
|
779
832
|
- **The face is measured in the head's frame, and its bar is the model's (issue #33).**
|
|
780
833
|
Until #33 the face half read the screen-space heat map, where the head's intended
|
|
781
834
|
roll lights the outline on both examples (16.988 and 11.475, the figures now
|
|
782
|
-
reported as `screen_heat_mean`; sample's
|
|
783
|
-
|
|
835
|
+
reported as `screen_heat_mean`; sample's read 11.472 once the page edges
|
|
836
|
+
defaulted to `free`, *Page edges* in §5, and 11.475 again since the chain links
|
|
837
|
+
are turned, issue #73, which moved both torso means down by 0.001 too), so its ceiling was twice the worse example,
|
|
784
838
|
33.976 — a ceiling a smooth fixture face sliding two rig pixels stayed under (24.8;
|
|
785
839
|
eight pixels measured 85.5). The judgement means *nothing moves on the face that
|
|
786
840
|
the head does not move*, so the heat is now taken after each frame's face region is
|
|
@@ -831,6 +885,32 @@ Nothing is left that only an eye answers. "No visible texture stretch" left this
|
|
|
831
885
|
with issue #31 (`TEXTURE_STRETCH`), and "the face outline held still in the head's own
|
|
832
886
|
frame" with issue #33 (the face half of `STILL_REGIONS_DARK`, above).
|
|
833
887
|
|
|
888
|
+
### Measuring a rig spine-parts did not assemble
|
|
889
|
+
|
|
890
|
+
`check` takes a rig spec — `rig.json` and `motion.json`, the `rigc-rig/1` and
|
|
891
|
+
`rigc-motion/1` pair `rigc build` compiles — not a compiled `skeleton.json`, because
|
|
892
|
+
the gate is `rigc build`. A rig merged from several spine-parts outputs outside the
|
|
893
|
+
package (prefixed names, one root, a plate region at slot 0) is such a spec, with no
|
|
894
|
+
`parts.json` beside it and possibly no `idle`. Each bar then measures exactly when what
|
|
895
|
+
it reads is there, and says SKIP with the reason when it is not:
|
|
896
|
+
|
|
897
|
+
| bar or line | reads | SKIP when |
|
|
898
|
+
| --- | --- | --- |
|
|
899
|
+
| the gate | the rig spec | never — it always runs, on whatever the spec declares |
|
|
900
|
+
| loop | the `idle` | no `idle` in `motion.json` (`check.json`: `loop_max_diff` null, `skipped.loop` the reason) |
|
|
901
|
+
| seam | `parts.json` and `parts/` | no `parts.json` (`seam_*` null, `skipped.seam` the reason) |
|
|
902
|
+
| `BREATH_VISIBLE`, `BLINK_NO_HOLE`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK` | `parts.json`'s tags and the idle | either missing, or as in the table above |
|
|
903
|
+
| `CHAIN_LAG`, `TEXTURE_STRETCH` | the idle (and `rig.json`) | no `idle`, or as in the table above |
|
|
904
|
+
| `RECOMPOSITE_HOLES` | `parts.json`'s `recomposite` block | no `parts.json`, or no block |
|
|
905
|
+
| `SETUP_POSE_VS_SOURCE` | the rig spec and the painting | no `--source` |
|
|
906
|
+
|
|
907
|
+
`PASS` reads every bar that measured, and the last console line says how many did:
|
|
908
|
+
`check: PASS; 1 of 9 bar(s) measured, 8 skipped (loop, seam, …)` is a rig whose gate is
|
|
909
|
+
green and nothing else was looked at. Without an idle no idle is rendered
|
|
910
|
+
(`idle_frames/`, `contact.png` and `motion_heat.png` are not written). The demo's own
|
|
911
|
+
rig directory, copied away from its `parts.json`, reads `4 of 9`: the gate, the loop,
|
|
912
|
+
`CHAIN_LAG` and `TEXTURE_STRETCH`.
|
|
913
|
+
|
|
834
914
|
## 8. What one character costs
|
|
835
915
|
|
|
836
916
|
| step | sample | demo | source |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-parts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "AI-authored Spine 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through spine-rigc before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/assemble.ts
CHANGED
|
@@ -32,8 +32,13 @@
|
|
|
32
32
|
* a part left with no opaque pixel (the reference printed `EMPTY` and wrote a
|
|
33
33
|
* `parts.json` without it), a head box outside the painting (the reference
|
|
34
34
|
* read row `-1` as the last row), a run whose canvas is not the configured
|
|
35
|
-
* resolution (the reference assumed 1024)
|
|
36
|
-
*
|
|
35
|
+
* resolution (the reference assumed 1024).
|
|
36
|
+
* - **A landscape painting is mapped back, not refused.** The reference
|
|
37
|
+
* refused one as a bare exit; here the full run's input is the painting on a
|
|
38
|
+
* square of its longer side (`squarePad`, `src/inputs.ts`), and the full
|
|
39
|
+
* run's map carries the vertical pad exactly as it carries the horizontal
|
|
40
|
+
* one. A portrait painting's vertical pad is 0, so its map is the
|
|
41
|
+
* reference's.
|
|
37
42
|
* - **`seamRule: 'silhouette'`** is available beside the faithful
|
|
38
43
|
* `'near-white'` rule. The reference's seam override skips every pixel whose
|
|
39
44
|
* painting colour is near-white (min channel above 235), which protects the
|
|
@@ -60,6 +65,7 @@
|
|
|
60
65
|
import type { CharacterConfig, EarlyConfig, Extend, Patch, PlanEntry, Run, SeeThrough } from './config.ts';
|
|
61
66
|
import { type Problem, refuseIfAny } from './errors.ts';
|
|
62
67
|
import { figuresPhrase, implausibleRules, type Layer, type LayerFigures, layerFigures, type LayerSet, NEAR_WHITE_MIN, OPAQUE_ALPHA_ABOVE, ruleSummary } from './layers.ts';
|
|
68
|
+
import { squarePad } from './inputs.ts';
|
|
63
69
|
import { PAINTING_RUN, type PartRecord, type PartsFile, type RecompositeRecord } from './parts.ts';
|
|
64
70
|
import {
|
|
65
71
|
alphaComposite,
|
|
@@ -200,8 +206,10 @@ interface Frame {
|
|
|
200
206
|
W: number;
|
|
201
207
|
H: number;
|
|
202
208
|
S: number;
|
|
203
|
-
/** Full run: the painting was centred on a white `side x side` square, padded this much on the left
|
|
209
|
+
/** Full run: the painting was centred on a white `side x side` square (`squarePad`), padded this much on the left… */
|
|
204
210
|
fullPad: number;
|
|
211
|
+
/** …and this much on top, in source px. At most one of the two is non-zero. */
|
|
212
|
+
fullPadTop: number;
|
|
205
213
|
fullK: number;
|
|
206
214
|
headBox: [number, number, number, number];
|
|
207
215
|
headK: number;
|
|
@@ -219,10 +227,7 @@ export function checkGeometry(g: Geometry, runs?: { full: LayerSet; head: LayerS
|
|
|
219
227
|
problems.push({ code, object, detail });
|
|
220
228
|
};
|
|
221
229
|
const { sourceW: w, sourceH: h, resolution, headBox, rigScale: S } = g;
|
|
222
|
-
const
|
|
223
|
-
if (h !== side) {
|
|
224
|
-
fail('ASSEMBLE_SOURCE_PORTRAIT', 'source painting', `is ${w}x${h}, wider than tall; the full run pads the painting to a square horizontally only, so height >= width is required`);
|
|
225
|
-
}
|
|
230
|
+
const pad = squarePad(w, h);
|
|
226
231
|
const W = Math.trunc(w * S);
|
|
227
232
|
const H = Math.trunc(h * S);
|
|
228
233
|
if (W < 1 || H < 1) fail('ASSEMBLE_RIG_SIZE', 'config.assemble.rig_scale', `${S} makes a ${W}x${H} rig from a ${w}x${h} painting; at least 1x1 is required`);
|
|
@@ -251,8 +256,9 @@ export function checkGeometry(g: Geometry, runs?: { full: LayerSet; head: LayerS
|
|
|
251
256
|
W,
|
|
252
257
|
H,
|
|
253
258
|
S,
|
|
254
|
-
fullPad:
|
|
255
|
-
|
|
259
|
+
fullPad: pad.left,
|
|
260
|
+
fullPadTop: pad.top,
|
|
261
|
+
fullK: pad.side / resolution,
|
|
256
262
|
headBox,
|
|
257
263
|
headK: (x1 - x0) / resolution,
|
|
258
264
|
headBottom,
|
|
@@ -347,7 +353,8 @@ export function runLayers(set: LayerSet, resolution: number): RunLayer[] {
|
|
|
347
353
|
export function runMap(frame: Frame, run: Run): { k: number; sx: number; tx: number; ty: number } {
|
|
348
354
|
if (run === 'full') {
|
|
349
355
|
const k = frame.fullK * frame.S;
|
|
350
|
-
|
|
356
|
+
// A portrait painting's top pad is 0, and its ty is the reference's literal 0 rather than f32(-0).
|
|
357
|
+
return { k, sx: f32(k), tx: f32(-frame.fullPad * frame.S), ty: frame.fullPadTop === 0 ? 0 : f32(-frame.fullPadTop * frame.S) };
|
|
351
358
|
}
|
|
352
359
|
const k = frame.headK * frame.S;
|
|
353
360
|
return { k, sx: f32(k), tx: f32(frame.headBox[0] * frame.S), ty: f32(frame.headBox[1] * frame.S) };
|
|
@@ -1528,7 +1535,8 @@ export function proposePlan(full: LayerSet, head: LayerSet, g: Geometry, minPx =
|
|
|
1528
1535
|
}
|
|
1529
1536
|
}
|
|
1530
1537
|
}
|
|
1531
|
-
|
|
1538
|
+
// The run row maps to the rig as the warp maps it: k * row, less the top pad in rig px.
|
|
1539
|
+
if (last >= 0 && (last + 1) * frame.fullK * frame.S - frame.fullPadTop * frame.S > frame.headBottom + 8) extend.push({ part: PROPOSE_ALIAS[t], run: 'full', tag: t });
|
|
1532
1540
|
}
|
|
1533
1541
|
return { plan, extend_below_crop: extend, notes: [...notes, ...dropNotes] };
|
|
1534
1542
|
}
|
package/src/build.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:
|
|
|
44
44
|
import { dirname, join } from 'node:path';
|
|
45
45
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
46
46
|
import { assemble, type AssembleResult, figuresLine, holeLines, type ProjectRule, type SeamRule, stageFields } from './assemble.ts';
|
|
47
|
-
import { causeLines, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK } from './check.ts';
|
|
47
|
+
import { BARS, causeLines, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_MEAN_BAR, SOURCE_LINE, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK } from './check.ts';
|
|
48
48
|
import { loadConfig, loadEarlyConfig } from './config.ts';
|
|
49
49
|
import { PartsError, type Problem, problemLine } from './errors.ts';
|
|
50
50
|
import { encodeGif } from './gif.ts';
|
|
@@ -288,9 +288,15 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
288
288
|
export interface CheckStageInput {
|
|
289
289
|
/** The directory holding rig.json and motion.json. */
|
|
290
290
|
rig: string;
|
|
291
|
-
/**
|
|
292
|
-
|
|
291
|
+
/**
|
|
292
|
+
* The directory holding parts.json and parts/ (`--parts`). Absent, it is the
|
|
293
|
+
* rig directory, where a missing parts.json is measured without (issue #77);
|
|
294
|
+
* named, a missing one is refused.
|
|
295
|
+
*/
|
|
296
|
+
parts?: string;
|
|
293
297
|
out: string;
|
|
298
|
+
/** The painting `--source` names (issue #77): adds the setup pose against it. */
|
|
299
|
+
source?: string;
|
|
294
300
|
/** The packed build's `--page-edges`; {@link DEFAULT_PAGE_EDGES} when absent. */
|
|
295
301
|
pageEdges?: PageEdges;
|
|
296
302
|
/** The packed build's `--pack-shape`; {@link DEFAULT_PACK_SHAPE} when absent. */
|
|
@@ -312,6 +318,16 @@ function showFigure(v: unknown): string {
|
|
|
312
318
|
return String(v);
|
|
313
319
|
}
|
|
314
320
|
|
|
321
|
+
/**
|
|
322
|
+
* How many of the {@link BARS} measured and how many said SKIP, by name — the
|
|
323
|
+
* summary's second half (issue #77): `PASS` reads only the bars that measured,
|
|
324
|
+
* so a run that measured one bar says so on the line that says PASS.
|
|
325
|
+
*/
|
|
326
|
+
export function barsSummary(r: CheckReport): string {
|
|
327
|
+
const { measured, skipped } = r.bars;
|
|
328
|
+
return `${measured.length} of ${BARS.length} bar(s) measured, ${skipped.length} skipped${skipped.length === 0 ? '' : ` (${skipped.map((s) => s.bar).join(', ')})`}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
315
331
|
/** A judgement line as the console prints it: its name, its status, then its figures and bars as check.json holds them (a SKIP prints its reason). */
|
|
316
332
|
export function judgementLine(name: string, line: JudgementLine | ReportedLine): string {
|
|
317
333
|
if (line.status === 'SKIP') return `${name}: SKIP — ${String(line.reason)}`;
|
|
@@ -333,22 +349,28 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
333
349
|
const entryLine = versionLines.find((l) => l.startsWith('entry:'));
|
|
334
350
|
log(` rigc ${versionLines[0]} at ${bin}${entryLine === undefined ? '' : `; ${entryLine}`}`);
|
|
335
351
|
const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
|
|
336
|
-
const r = runCheck(input.rig, input.out, rigc, input.parts, mode);
|
|
352
|
+
const r = runCheck(input.rig, input.out, rigc, input.parts, mode, input.source);
|
|
337
353
|
log(` gate spine-html (rigc ${packedBuildLabel(mode)}), verbatim:`);
|
|
338
354
|
for (const l of r.gateHtml) log(l);
|
|
339
355
|
for (const l of packLines(r)) log(` ${l}`);
|
|
340
356
|
const fig = r.figures;
|
|
341
|
-
|
|
342
|
-
log(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
357
|
+
if (r.idle === null) log(` loop: SKIP — ${fig.skipped?.loop ?? ''}`);
|
|
358
|
+
else log(` loop: idle ${r.idle.frames} frame(s) at ${r.idle.fps} fps, f0000 vs f${String(r.idle.lastIndex).padStart(4, '0')} (t = ${r.idle.duration}s): max |d| ${fig.loop_max_diff} (0 required)`);
|
|
359
|
+
if (r.seamViewport === null || fig.seam_mean === null) log(` seam: SKIP — ${fig.skipped?.seam ?? ''}`);
|
|
360
|
+
else {
|
|
361
|
+
log(
|
|
362
|
+
` seam: setup pose at ${r.seamViewport.pixelWidth}x${r.seamViewport.pixelHeight}, scale ${r.seamViewport.scale.toFixed(4)}: mean |d| ${fig.seam_mean} (<= ${SEAM_MEAN_BAR.toFixed(1)}), ` +
|
|
363
|
+
`${fig.seam_px_over_40} px over ${SEAM_PX_LEVEL} (<= ${SEAM_PX_BAR}), ${fig.seam_px_over_80} px over ${SEAM_PX_LEVEL_HIGH} (reported)`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
346
366
|
for (const name of JUDGEMENT_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
347
367
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
368
|
+
const vsSource = fig[SOURCE_LINE];
|
|
369
|
+
if (vsSource !== undefined) log(` ${judgementLine(SOURCE_LINE, vsSource)}`);
|
|
348
370
|
log(` gate: spine-html ${fig.gate_spine_html_green ? 'green' : 'RED'} (${fig.rigc_entry.entry}${fig.rigc_entry.spine_core === null ? ', rigc\'s own validator' : `, the spine-core ${fig.rigc_entry.spine_core} round trip`})`);
|
|
349
|
-
log(` wrote ${r.written.map((w) => join(input.out, w)).join(', ')}, ${join(input.out, 'build')}
|
|
371
|
+
log(` wrote ${r.written.map((w) => join(input.out, w)).join(', ')}, ${join(input.out, 'build')}/${r.idle === null ? '' : `, ${join(input.out, 'idle_frames')}/`}`);
|
|
350
372
|
for (const p of r.problems) log(` FAIL ${problemLine(p)}`);
|
|
351
|
-
log(fig.PASS ? 'check: PASS' : `check: FAIL — ${r.problems.length} bar(s) not met`);
|
|
373
|
+
log(`${fig.PASS ? 'check: PASS' : `check: FAIL — ${r.problems.length} bar(s) not met`}; ${barsSummary(r)}`);
|
|
352
374
|
return r;
|
|
353
375
|
}
|
|
354
376
|
|