spine-parts 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +3 -1
- package/README.md +29 -5
- package/cli.ts +44 -11
- package/docs/AUTHORING.md +155 -37
- package/package.json +2 -2
- package/skills/spine-parts/SKILL.md +7 -5
- package/src/build.ts +20 -4
- package/src/check.ts +456 -34
- package/src/config.ts +42 -4
- package/src/instruments.ts +358 -3
- package/src/motion.ts +123 -26
- package/src/propose.ts +66 -17
- package/src/rig.ts +84 -8
package/NOTICE.md
CHANGED
|
@@ -39,12 +39,14 @@ wrapper code.
|
|
|
39
39
|
| Weights | Licence as stated on the model card | Observed |
|
|
40
40
|
| --- | --- | --- |
|
|
41
41
|
| [`layerdifforg/seethroughv0.0.2_layerdiff3d`](https://huggingface.co/layerdifforg/seethroughv0.0.2_layerdiff3d) | `license:apache-2.0` tag | 2026-09-27, Hugging Face model API |
|
|
42
|
+
| (same card, after its 2026-09-28 commit *"Add licence terms, NOTICE and base model metadata"*) | `license:openrail++` tag; the card's *Licence* section says the maintainers' own contributions are Apache-2.0 and the weights also inherit the licences of the models they are derived from — Animagine XL 4.0 / Stable Diffusion XL 1.0 (CreativeML Open RAIL++-M) and LayerDiffuse (CreativeML Open RAIL-M) | 2026-09-28, Hugging Face model API and card |
|
|
42
43
|
| [`layerdifforg/seethroughv0.0.1_marigold`](https://huggingface.co/layerdifforg/seethroughv0.0.1_marigold) | no licence tag on the card | 2026-09-27, Hugging Face model API |
|
|
44
|
+
| (same card, after its 2026-09-28 revision) | `license:openrail++` tag | 2026-09-28, Hugging Face model API |
|
|
43
45
|
|
|
44
46
|
Maintainer statement, on the Hugging Face discussion
|
|
45
47
|
[`layerdifforg/seethroughv0.0.2_layerdiff3d/discussions/1`](https://huggingface.co/layerdifforg/seethroughv0.0.2_layerdiff3d/discussions/1)
|
|
46
48
|
(user `24yearsold`, 2026-04-07): *"Yes all our models are aligned with our main
|
|
47
|
-
repo to use Apache 2.0."*
|
|
49
|
+
repo to use Apache 2.0."* The 2026-09-28 card revision above narrows that: Apache-2.0 for the maintainers' contributions, with the base models' Open RAIL terms inherited. spine-parts does not distribute these weights; a user who fetches them accepts the terms the card states on the day they fetch.
|
|
48
50
|
|
|
49
51
|
### Training data
|
|
50
52
|
|
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>
|
|
@@ -87,7 +90,7 @@ page** (issue #2):
|
|
|
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
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
|
-
| `check/` | both gate files verbatim, the idle's frames, `contact.png`, `motion_heat.png`, `check.json` |
|
|
93
|
+
| `check/` | both gate files verbatim, the idle's frames and their `geometry.json` (skinned vertices per frame), `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 |
|
|
92
95
|
|
|
93
96
|
The last lines of a green build are the pack line, printed beside the Spine example
|
|
@@ -129,6 +132,27 @@ spine-parts at what it wrote:
|
|
|
129
132
|
| Upstream CLI | `python inference/scripts/inference_psd.py --srcp <image> --save_to_psd` in a checkout of [shitagaki-lab/see-through](https://github.com/shitagaki-lab/see-through) | the `.psd` in `workspace/layerdiff_output/` |
|
|
130
133
|
| ComfyUI wrapper | [jtydhr88/ComfyUI-See-through](https://github.com/jtydhr88/ComfyUI-See-through) on your own ComfyUI box; the optional `comfy` adapter (`spine-parts comfy seethrough`) drives it | the `layers.json` + PNGs it writes |
|
|
131
134
|
|
|
135
|
+
**The head run is needed on every route that was read.** See-through's model
|
|
136
|
+
already separates the head in a second stage of its own: it crops the head the
|
|
137
|
+
first stage found, runs the face tags on that crop at the same `resolution`, and
|
|
138
|
+
pastes the result back **resized to the full image's scale**. So the face comes
|
|
139
|
+
back at the full run's pixel density whichever route ran it, and the separate
|
|
140
|
+
head run on `st_input_head.png` is what gives the eyes more pixels
|
|
141
|
+
(read on 2026-09-28 at see-through `a25a549` and wrapper 0.5.0 `98d754b`):
|
|
142
|
+
|
|
143
|
+
- *Hugging Face Space* — the same two-stage code, with `resolution` capped at
|
|
144
|
+
1280 (768 by default): the head run is still needed.
|
|
145
|
+
- *ModelScope demo* — its code was not read, so what the head run adds on that
|
|
146
|
+
route has not been measured.
|
|
147
|
+
- *Upstream CLI* — the `.psd` frame is the `resolution`-sided square the first
|
|
148
|
+
stage ran on (1280 by default), and the head stage is pasted into it at that
|
|
149
|
+
scale: the head run is still needed.
|
|
150
|
+
- *ComfyUI wrapper* — `SeeThrough_GenerateLayers` runs both stages with no input
|
|
151
|
+
to change the second, at most `resolution` 2048, which on the examples' 2432 px
|
|
152
|
+
square is 1.19 source px per layer px against 0.77 and 0.47 for their head
|
|
153
|
+
crops; at the 1024 the examples ran, the full run's right eye white is 22×13 px
|
|
154
|
+
and the head run's 60×38: the head run is still needed.
|
|
155
|
+
|
|
132
156
|
**See-through is required; ComfyUI is not.** The wrapper is one route among
|
|
133
157
|
four, and `spine-parts comfy` is only a convenience for that route: every
|
|
134
158
|
stage after See-through reads files, whichever route wrote them. On the two
|
|
@@ -218,8 +242,8 @@ agent skill.
|
|
|
218
242
|
| `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
|
|
219
243
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
|
|
220
244
|
| `propose … --from-config <config>` | draw and LINT the config's current bones |
|
|
221
|
-
| `rig --config --parts --out` | author `rig.json` + `motion.json`, written only after spine-rigc's round trip is green |
|
|
222
|
-
| `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam, loop and the
|
|
245
|
+
| `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 |
|
|
246
|
+
| `check --rig --out [--parts]` | build packed, gate under both profiles, 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` |
|
|
223
247
|
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
224
248
|
| `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
225
249
|
|
package/cli.ts
CHANGED
|
@@ -18,12 +18,13 @@ import { basename, dirname, extname, join } from 'node:path';
|
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
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
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';
|
|
21
|
+
import { findRigc, 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';
|
|
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 { DEFAULT_IDLE_KEYS, IDLE_KEYS, type IdleKeys } from './src/rig.ts';
|
|
27
28
|
import { figuresPhrase, implausibleRules, layerFigures, type LayerSet, pct, readLayers, ruleSummary, times } from './src/layers.ts';
|
|
28
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';
|
|
@@ -74,7 +75,10 @@ usage:
|
|
|
74
75
|
holds parts.json and parts/). Roles come from each part's See-through tag,
|
|
75
76
|
never its name. Writes <out>/proposal.json (config-shaped: bones, meshes,
|
|
76
77
|
regions, motion with its blink — and a blink.still cut for a lash that
|
|
77
|
-
reaches far above its eyewhite, when a clear row allows one
|
|
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; an iris or lash on a side with no
|
|
81
|
+
eyewhite rides head, said in a note — and notes)
|
|
78
82
|
and the overlay to correct against, <out>/render/landmarks.png and
|
|
79
83
|
landmarks_head.png. Prints every
|
|
80
84
|
note and a LINT line for each chain link that lies off its mesh's art, for a
|
|
@@ -93,18 +97,30 @@ usage:
|
|
|
93
97
|
layers for a painting of WxH px, held inside the painting; a shift is
|
|
94
98
|
printed when one was needed.
|
|
95
99
|
|
|
96
|
-
spine-parts rig --config <config.json> --parts <dir> --out <dir>
|
|
100
|
+
spine-parts rig --config <config.json> --parts <dir> --out <dir> [--idle-keys ctl|direct]
|
|
97
101
|
Author the rig: unrotated bones at the config's landmarks (a chain makes
|
|
98
102
|
<chain>0..n), a square lattice mesh over every part in config.meshes
|
|
99
103
|
weighted by distance to its candidate bone segments, a region for every
|
|
100
104
|
part in config.regions (a motion.blink.still part as two: the rows above
|
|
101
105
|
its row on a second slot <part>_still, which the blink does not move),
|
|
102
|
-
and one idle of sines and a blink
|
|
106
|
+
and one idle of sines and a blink whose closed hold is at least one
|
|
107
|
+
12 fps frame, so the idle frames and the loop show the eyes shut
|
|
108
|
+
(RIG_BLINK_HOLD_SPANS_A_FRAME otherwise); no blink when the config
|
|
109
|
+
states no motion.blink, and a blink group that names no bone, or one
|
|
110
|
+
bone twice, is refused by the loader, CONFIG_BLINK_GROUP_MEMBERS or
|
|
111
|
+
CONFIG_BLINK_GROUP_UNIQUE, before rigc starts. --parts is the
|
|
103
112
|
directory holding parts.json and parts/<name>.png. The result is built
|
|
104
113
|
through spine-rigc (profile spine-html, packed, then validated under
|
|
105
114
|
profile spine) in a scratch directory first, and --out receives
|
|
106
115
|
images/*.png, rig.json, motion.json and mesh_report.json only when both
|
|
107
116
|
are green. Prints one line per mesh and the rigc gate lines.
|
|
117
|
+
--idle-keys says where the idle's keys on a bone a mesh is weighted to go:
|
|
118
|
+
ctl (the default) keys a same-origin <bone>_ctl parent instead, which
|
|
119
|
+
passes A15_IDLE_NO_MESH_BONE_KEYS on any spine-rigc; direct keys the bone
|
|
120
|
+
itself and declares invariants.idleDrivesMeshes in rig.json, which needs
|
|
121
|
+
spine-rigc 1.3.0 or later and makes A15 a SKIP that prints its cost (the
|
|
122
|
+
stage prints that SKIP line). The pose is the same to one level of float
|
|
123
|
+
rounding, and so is the per-frame mesh work (AUTHORING §5).
|
|
108
124
|
spine-parts check --rig <dir> --out <dir> [--parts <dir>]
|
|
109
125
|
Build, gate, render and measure a rig through spine-rigc's CLI (the rigc at
|
|
110
126
|
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json
|
|
@@ -113,19 +129,30 @@ usage:
|
|
|
113
129
|
Both are only read. Into --out:
|
|
114
130
|
build/ (rigc build --profile spine-html --pack: the packed atlas is the
|
|
115
131
|
artifact), gate_spine-html.txt and gate_spine.txt (the gate lines
|
|
116
|
-
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
132
|
+
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
133
|
+
--geometry: the frames and geometry.json, the skinned vertices and
|
|
134
|
+
every bone's world transform per frame),
|
|
117
135
|
contact.png, motion_heat.png and check.json. PASS needs both gates
|
|
118
136
|
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
119
137
|
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
|
|
120
|
-
frame at t = duration) at max |d| 0. Then
|
|
138
|
+
frame at t = duration) at max |d| 0. Then six judgement lines, each in
|
|
121
139
|
check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
|
|
122
140
|
bars (AUTHORING §7): BREATH_VISIBLE (the topwear moves, the footwear does
|
|
123
141
|
not, each rendered alone), BLINK_NO_HOLE (the setup pose with the blink
|
|
124
142
|
held shut shows no background inside the eyewhite box), CHAIN_LAG (every
|
|
125
143
|
rotate track lags its keyed ancestor and amplitude grows down each
|
|
126
144
|
chain, read off motion.json), TIP_OVER_ROOT (each handwear/bottomwear
|
|
127
|
-
part's lower half travels further than its upper half)
|
|
128
|
-
STILL_REGIONS_DARK (the
|
|
145
|
+
part's lower half travels further than its upper half),
|
|
146
|
+
STILL_REGIONS_DARK (the face outline in the frame of the bone the face's
|
|
147
|
+
slot rides, at most twice the resampler's own error on this rig — read
|
|
148
|
+
off the whole rig moved rigidly as the idle moves that bone — with the
|
|
149
|
+
screen-space figure reported beside it; the feet in screen space) and
|
|
150
|
+
TEXTURE_STRETCH (every mesh triangle's edges over the idle against their
|
|
151
|
+
rest length, max(ratio, 1/ratio) <= ${TEXTURE_STRETCH_CEILING}; the worst
|
|
152
|
+
triangle is named by slot, triangle, vertices, edge and frame).
|
|
153
|
+
Needs spine-rigc ${RIGC_GEOMETRY_VERSION} or later, whose render writes
|
|
154
|
+
geometry.json; an older rigc is refused, CHECK_RIGC_VERSION, before
|
|
155
|
+
anything is built.
|
|
129
156
|
Regions come from parts.json's See-through tags; a line with nothing to
|
|
130
157
|
read says SKIP and why — neither a pass nor a failure — and PASS needs
|
|
131
158
|
every line that measured to be PASS. Then RECOMPOSITE_HOLES: REPORTED,
|
|
@@ -375,22 +402,28 @@ function cmdRig(args: string[]): number {
|
|
|
375
402
|
let config: string | null = null;
|
|
376
403
|
let partsDir: string | null = null;
|
|
377
404
|
let out: string | null = null;
|
|
405
|
+
let idleKeys: string | null = null;
|
|
378
406
|
for (let i = 0; i < args.length; i++) {
|
|
379
407
|
const flag = args[i];
|
|
380
408
|
const value = args[i + 1];
|
|
381
|
-
if (!['--config', '--parts', '--out'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
409
|
+
if (!['--config', '--parts', '--out', '--idle-keys'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
382
410
|
if (value === undefined) return usage(`${flag} needs a value`);
|
|
383
411
|
i++;
|
|
384
412
|
if (flag === '--config') config = value;
|
|
385
413
|
else if (flag === '--parts') partsDir = value;
|
|
386
|
-
else
|
|
414
|
+
else if (flag === '--idle-keys') {
|
|
415
|
+
if (idleKeys !== null) return usage('--idle-keys is given twice');
|
|
416
|
+
idleKeys = value;
|
|
417
|
+
} else out = value;
|
|
387
418
|
}
|
|
388
419
|
if (config === null) return usage('rig needs --config <config.json>');
|
|
389
420
|
if (partsDir === null) return usage('rig needs --parts <dir> (the directory holding parts.json and parts/)');
|
|
390
421
|
if (out === null) return usage('rig needs --out <dir>');
|
|
422
|
+
const keys = idleKeys ?? DEFAULT_IDLE_KEYS;
|
|
423
|
+
if (!(IDLE_KEYS as readonly string[]).includes(keys)) return usage(`--idle-keys ${keys}; one of ${IDLE_KEYS.join(', ')} is required`);
|
|
391
424
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-rig-'));
|
|
392
425
|
try {
|
|
393
|
-
rigStage({ config, parts: partsDir, out }, rigGateRunner(), scratch, console.log);
|
|
426
|
+
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys }, rigGateRunner(), scratch, console.log);
|
|
394
427
|
return EXIT_OK;
|
|
395
428
|
} catch (err) {
|
|
396
429
|
return printRefusal(err);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -76,11 +76,33 @@ read by no CPU stage).
|
|
|
76
76
|
| `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
77
|
| `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
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`) |
|
|
79
|
-
| `motion.blink` | rig | **proposed**: `{t, eyes, brows, squash, brow_drop}`; the whole blink 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 |
|
|
79
|
+
| `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
80
|
| `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
81
|
|
|
82
|
+
The blink's timing is the tree's (`BLINK` in `src/motion.ts`), in seconds after
|
|
83
|
+
`motion.blink.t`:
|
|
84
|
+
|
|
85
|
+
| phase | eyes (`scaley` 1 → `squash` → 1) | brows (`translatey` 0 → `-brow_drop` → 0) | easing |
|
|
86
|
+
| --- | --- | --- | --- |
|
|
87
|
+
| shut | 0.07 | 0.08 | `shut` |
|
|
88
|
+
| hold | **0.084** (the reference: 0.04) | **0.084** (the reference: 0.04) | none |
|
|
89
|
+
| open | 0.16 | 0.20 | `open` |
|
|
90
|
+
|
|
91
|
+
The holds are the one departure from the reference implementation, and the reason is
|
|
92
|
+
the loop (issue #32). `check` renders the idle at 12 fps, a frame every 0.083333 s, and
|
|
93
|
+
the loop is encoded from those frames. A 0.04 s hold is shorter than a frame, so it
|
|
94
|
+
held a frame only for some `t`: at the examples' 2.3 s the eyes were shut from 2.37 s to
|
|
95
|
+
2.41 s, between frames 28 (2.333 s, `scaley` 0.8356) and 29 (2.417 s, 0.2435), and over
|
|
96
|
+
every key time the 6-decimal grid can write, 129,999 of the 250,000 phases against the
|
|
97
|
+
frame grid showed no closed frame. A closed window at least one frame long holds a frame
|
|
98
|
+
wherever it starts, so the hold is 1/12 s rounded up to the third place; the brows' hold
|
|
99
|
+
grew by the same 0.044 s, so they still reach their drop 0.01 s after the lids shut and
|
|
100
|
+
leave it 0.01 s after the lids open. The rig stage refuses a hold under one frame by
|
|
101
|
+
name (`RIG_BLINK_HOLD_SPANS_A_FRAME`, §6), counted over the same 250,000 phases.
|
|
102
|
+
|
|
82
103
|
The proposer's reach (from `src/propose.ts`): roles come from each part's tag —
|
|
83
|
-
`face` makes `hip`/`chest`/`neck`/`head`, the eyewhites make the eye bones
|
|
104
|
+
`face` makes `hip`/`chest`/`neck`/`head`, the eyewhites make the eye bones (irides and
|
|
105
|
+
lashes ride them, or `head` on a side with no eyewhite), brows are
|
|
84
106
|
told apart by position, `bottomwear` makes the hip and three skirt chains,
|
|
85
107
|
`handwear` makes sleeve chains (one blob or two), `front hair` makes fringe chains
|
|
86
108
|
and a lock chain for each strand that hangs below the chin, `back hair` makes a bun
|
|
@@ -205,7 +227,11 @@ full loader before its assemble stage writes anything.
|
|
|
205
227
|
4. `spine-parts propose --head-box --full inputs/layers/full --canvas <W>x<H>` →
|
|
206
228
|
`seethrough.head_box`. Run `spine-parts inputs` again: it now also writes
|
|
207
229
|
`st_input_head.png`, the painting cropped to that box at its exact size.
|
|
208
|
-
5. **See-through, head run** (external), on that crop.
|
|
230
|
+
5. **See-through, head run** (external), on that crop. See-through's own second,
|
|
231
|
+
head stage does not replace it: that stage's layers are pasted back at the full
|
|
232
|
+
run's scale on every route that was read (README *Getting See-through layers*),
|
|
233
|
+
so the full run's eyes stay at its density — 22×13 px for the demo's right eye
|
|
234
|
+
white at `resolution` 1024, against 60×38 px in the head run.
|
|
209
235
|
6. `spine-parts assemble --propose-plan --source … --full … --head … --config config.json`
|
|
210
236
|
→ paste `plan` and `extend_below_crop` into `config.assemble`. Config: the
|
|
211
237
|
`layers` row, with `head_box`.
|
|
@@ -239,9 +265,9 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
|
|
|
239
265
|
| `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*) |
|
|
240
266
|
| `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 |
|
|
241
267
|
| `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) |
|
|
242
|
-
| `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) |
|
|
243
|
-
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; then rigc's gate lines | `cover 1.00000` on every mesh, both gates `0 failed` | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
|
|
244
|
-
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:`, `seam:`, the
|
|
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 gates `0 failed` | `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 `RECOMPOSITE_HOLES` (§7), `check.json` | `check: PASS`, and a judgement line SKIP only where the character lacks what it reads | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6) |
|
|
245
271
|
| `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) |
|
|
246
272
|
|
|
247
273
|
`loop` writes three files from one frame set, and they are not interchangeable.
|
|
@@ -253,9 +279,12 @@ entry, no dithering, filter None) — and is the small file to show. `idle.gif`
|
|
|
253
279
|
(`loop --out x.gif`) is the same median cut in a GIF. The indexed APNG and the GIF
|
|
254
280
|
print their palette error, per channel over R, G and B of every frame (and alpha's
|
|
255
281
|
max for the APNG); a figure is a measurement of the file, not a bar. On the demo:
|
|
256
|
-
13,645,
|
|
282
|
+
13,645,519 B lossless, 1,706,785 B indexed and 1,812,625 B GIF, both palette files at
|
|
257
283
|
max 57, mean 1.601 — the two share one quantiser, so their error is the same by
|
|
258
|
-
construction and the size is the difference.
|
|
284
|
+
construction and the size is the difference. The loop shows the blink closed: the
|
|
285
|
+
eyes' hold is at least one 12 fps frame (below), so whatever `motion.blink.t` is, one
|
|
286
|
+
idle frame lands inside it, and `check.json`'s `BLINK_NO_HOLE.idle_frames_closed`
|
|
287
|
+
names it — `[29]` on both examples.
|
|
259
288
|
|
|
260
289
|
`--seam near-white` (the default) is the reference implementation's rule: where the
|
|
261
290
|
flat stack of parts differs from the painting by more than 60, the top part takes the
|
|
@@ -276,6 +305,29 @@ error pixels from 4,512 to 4,116 and 11,050 to 9,820, and the check seam from 0.
|
|
|
276
305
|
0.206 and 0.326 to 0.325, with no other check figure changed. The default stays the
|
|
277
306
|
reference's so the examples stay comparable with it. `build` takes both flags.
|
|
278
307
|
|
|
308
|
+
**`rig --idle-keys ctl|direct`.** spine-rigc's `A15_IDLE_NO_MESH_BONE_KEYS`
|
|
309
|
+
(profile `spine-html`) refuses an idle that keys a bone a mesh is weighted to. `ctl`
|
|
310
|
+
(the default, the reference's answer) gives every such bone a same-origin
|
|
311
|
+
`<bone>_ctl` parent and moves its keys there, which passes A15 on any spine-rigc;
|
|
312
|
+
the stage prints `idle keys ctl: N mesh-driving bone(s) keyed …`. `direct` keys the
|
|
313
|
+
bones themselves and writes `invariants.idleDrivesMeshes: { "why": … }` into
|
|
314
|
+
`rig.json` (spine-rigc 1.3.0 or later), so A15 reports
|
|
315
|
+
`SKIP A15_IDLE_NO_MESH_BONE_KEYS: declared by the rig (…): idle keys N bone(s) that
|
|
316
|
+
drive M mesh attachment(s) totalling V vertices …`, which the stage prints; with no
|
|
317
|
+
mesh-driving bone keyed it declares nothing, because rigc refuses a declaration
|
|
318
|
+
that switches nothing off. The two write the same slots, skins and keys (`RG18`).
|
|
319
|
+
Measured on the examples with `bun tools/idle_cost.ts` (spine-parts #13): `direct`
|
|
320
|
+
has 41 bones for the demo's 72 and 32 for the sample's 56; every shown mesh moves on
|
|
321
|
+
every idle frame under both (8 of 8 and 6 of 6, so 0 a dirty-skip renderer could
|
|
322
|
+
skip); the pose costs 14.8 against 15.3 us per frame (demo) and 8.3 against 8.8 us
|
|
323
|
+
(sample), the difference all in `updateWorldTransform`; the rendered idle frames
|
|
324
|
+
differ by at most one level in a channel. The default stays `ctl`: the examples'
|
|
325
|
+
`expected/rig.json` and `motion.json` are the reference's output, and the
|
|
326
|
+
declaration is in the rig spec only, so `rigc validate <build> --profile spine-html`
|
|
327
|
+
run on a `direct` build (it has no rig spec to read) refuses A15 once per keyed
|
|
328
|
+
bone. `check` validates the build under `spine` only, where A15 is not in the
|
|
329
|
+
profile, and is green on both.
|
|
330
|
+
|
|
279
331
|
**Patching a hole no layer holds.** When an `uncovered hole` stays large after the
|
|
280
332
|
plan is right — both See-through runs dropped a piece of the figure, red in
|
|
281
333
|
`recomposite_error_rig.png` — add an `assemble.patches` entry:
|
|
@@ -434,6 +486,9 @@ See-through with another seed.
|
|
|
434
486
|
| `CONFIG_PATCH_BOX` | an `assemble.patches` box with `x1 <= x0` or `y1 <= y0` | that patch's `box` (`x1`, `y1` are exclusive) |
|
|
435
487
|
| `CONFIG_AMPS_MATCH_CHAIN` | a chain track's `amps` is not one per link | that track's `amps` |
|
|
436
488
|
| `CONFIG_PERIOD_DIVIDES_DURATION` | a period that is not a whole fraction of the idle — the loop could not close | that track's `period`, or `motion.duration` |
|
|
489
|
+
| `CONFIG_BLINK_GROUP_MEMBERS` | `motion.blink.eyes` or `motion.blink.brows` is `[]`: a group with no members, which rigc refuses at the gate | a figure with no eyewhite part has nothing to blink: leave `motion.blink` out; one with no eyebrow part: leave `brows` and `brow_drop` out |
|
|
490
|
+
| `CONFIG_BLINK_GROUP_UNIQUE` | `motion.blink.eyes` or `motion.blink.brows` names one bone more than once; the detail gives the bone and every index it sits at. rigc refuses it at the gate (`group "eyes" names member "eye" twice`) | name each bone once: the group keys every member it names |
|
|
491
|
+
| `CONFIG_BLINK_BROWS_PAIRED` | `motion.blink.brows` without `brow_drop`, or `brow_drop` without `brows` | state both or neither |
|
|
437
492
|
| `CONFIG_STILL_OFF_THE_BLINK` | a `motion.blink.still` entry names a region on a bone the blink's `eyes` does not name (nothing to hold still), or its `bone` is one the blink's `eyes` names (the still piece would blink) | that entry's part, or its `bone` — the eye bone's parent, `head` as proposed |
|
|
438
493
|
|
|
439
494
|
### inputs
|
|
@@ -490,6 +545,7 @@ See-through with another seed.
|
|
|
490
545
|
| `RIG_LATTICE_ONE_LOOP` | the lattice over a part does not close into one outline even after the repair passes | that mesh's `grid` |
|
|
491
546
|
| `RIG_CONTROL_NAME_FREE` | a keyed, mesh-weighted bone needs `<bone>_ctl` and that name is taken | rename the declared bone |
|
|
492
547
|
| `RIG_BLINK_INSIDE_IDLE` | the blink runs outside the idle | `motion.blink.t` |
|
|
548
|
+
| `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) |
|
|
493
549
|
| `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 |
|
|
494
550
|
| `RIG_RIGC_GREEN` | spine-rigc refused the rig; its own FAIL or compile-error line is quoted, and nothing was written | the field rigc's line names — spine-rigc's own AUTHORING §5 maps each of its assertions (`node_modules/spine-rigc/docs/AUTHORING.md`) |
|
|
495
551
|
|
|
@@ -500,6 +556,7 @@ See-through with another seed.
|
|
|
500
556
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
501
557
|
| `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/` |
|
|
502
558
|
| `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) |
|
|
559
|
+
| `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 ^1.5.1), or put a newer `rigc` first on `PATH` |
|
|
503
560
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
504
561
|
| `CHECK_LOOP_LAST_FRAME_AT_DURATION` | the idle's last frame does not sit at `duration` | `motion.duration` — a whole number of 1/12 s |
|
|
505
562
|
| `CHECK_LOOP_CLOSES` | frame 0 and the frame at `duration` differ (max and first pixel quoted) | a track whose last key is not its first |
|
|
@@ -508,8 +565,9 @@ See-through with another seed.
|
|
|
508
565
|
| `CHECK_BLINK_NO_HOLE` | with the blink held shut, the eyewhite box shows the page where the open eye had art | the layer under the eye: the `face` part has no art there. Take the face from the other run, or add a part under the eye; `motion.blink.squash` only hides the hole less |
|
|
509
566
|
| `CHECK_CHAIN_LAG` | a rotate track leads (or does not lag) the keyed bone above it, or a chain link swings less than the link above | that chain track's `phase`/`lag` (a positive `lag`, a child `phase` above its parent's) or its `amps` (non-decreasing toward the tip) |
|
|
510
567
|
| `CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's lower half travels less than 1.4725 times as far as its upper half | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them) |
|
|
511
|
-
| `CHECK_STILL_REGIONS_DARK` | the heat map is brighter than the ceiling
|
|
512
|
-
| `
|
|
568
|
+
| `CHECK_STILL_REGIONS_DARK` | over the face outline, carried into the frame of the bone the face's slot rides: something moves there that the head does not carry, above twice the resampler's own error on this rig (both figures quoted, with the screen-space one beside them); over the feet, the heat map is brighter than the ceiling | on the face: a mesh over it weighted to a bone the head does not carry (`segments`), or a swinging part (a bang, a chain) whose art crosses the face outline; on the feet: a mesh weighted to a swinging bone, or a region on the wrong bone |
|
|
569
|
+
| `CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other |
|
|
570
|
+
| `CHECK_SEAM_FRAME_SIZE`, `FRAMES_SIDECAR`, `CHECK_GEOMETRY_FILE` | rigc's render is not what its `frames.json` says, or its `geometry.json` is not a whole `rigc-geometry/1` export of the frames beside it (a frame count, a mesh whose vertex count differs from its rest entry; for the face half, another viewport than `frames.json`'s, other frame indices, the head bone or a feature's bone missing from a frame, a non-finite transform or time) | a rigc problem; report it |
|
|
513
571
|
| `LOOP_ENCODE` | the loop encoder refused a frame (translucent pixel in a GIF, a size change) | the frames; for a translucent frame write the lossless or the indexed APNG, which keep alpha |
|
|
514
572
|
| `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
|
|
515
573
|
|
|
@@ -528,8 +586,8 @@ implementation's bars):
|
|
|
528
586
|
On the examples: `sample` 23/23 and 14/14, seam 0.207 with 0 pixels over 40, loop 0;
|
|
529
587
|
`demo` (default rule) 23/23 and 14/14, seam 0.326 with 2 pixels over 40, loop 0.
|
|
530
588
|
|
|
531
|
-
A green gate cannot see a wrong animation, so `check` also writes
|
|
532
|
-
lines** (issue #11), each a key of `check.json` and a console line
|
|
589
|
+
A green gate cannot see a wrong animation, so `check` also writes six **judgement
|
|
590
|
+
lines** (issue #11; `TEXTURE_STRETCH`, issue #31), each a key of `check.json` and a console line
|
|
533
591
|
`NAME: PASS|FAIL|SKIP — <figures and bars>`, read the same way as the lines above: a
|
|
534
592
|
FAIL makes `PASS` false and prints its own `FAIL CHECK_<NAME>` line (§6); a SKIP
|
|
535
593
|
says why the rig gave the line nothing to read, and is neither a pass nor a failure —
|
|
@@ -540,7 +598,9 @@ change from idle frame 0, in levels of 255, on the idle's 640-pixel grid.
|
|
|
540
598
|
Each bar follows one rule: a floor is half the weaker example's figure and a ceiling
|
|
541
599
|
twice the worse one's, so the weaker example clears it by a factor of two; a bar the
|
|
542
600
|
model itself fixes (a part on an unkeyed bone does not move, a lag is above 0, a hole
|
|
543
|
-
is 0 pixels) is that value, not a margin
|
|
601
|
+
is 0 pixels) is that value, not a margin — and where the model's value is an error
|
|
602
|
+
the rig itself measures (a face the head carries is still in the head's frame up to
|
|
603
|
+
the resampler's error), the bar is that measurement times the same two. The figures are this port's, measured on
|
|
544
604
|
the two examples [observed]:
|
|
545
605
|
|
|
546
606
|
| line | measures | bar | `demo` | `sample` | SKIP when |
|
|
@@ -550,7 +610,8 @@ the two examples [observed]:
|
|
|
550
610
|
| ″ (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 |
|
|
551
611
|
| `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 |
|
|
552
612
|
| `TIP_OVER_ROOT` | each `handwear`/`bottomwear` part alone: how far the centroid of its art travels in the lower half of its box against the upper half | ratio ≥ 1.4725 | `bottomwear` 2.945, `sleeves` 3.716 | `bottomwear` 4.396, `sleeves` 12.475 | no such part |
|
|
553
|
-
| `STILL_REGIONS_DARK` | the
|
|
613
|
+
| `STILL_REGIONS_DARK` | the face outline **in the head's own frame**: each idle frame sampled (bilinear) where the head bone has carried each setup-pose pixel of the region, the heat taken over those samples. The head bone is the bone the `face` parts' slot rides in `rig.json`, and its per-frame world transform is `rigc render --geometry`'s. The region is the frame pixels whose whole footprint — 1 + 1/scale rig px each way, what the rasteriser's and this sampler's bilinear taps can reach — is `face` on top of the flat stack, clear by that reach of where `eyewhite`, `irides`, `eyelash`, `eyebrow` and `mouth` go in the head's frame over the idle. `resampler_heat_mean` is the same figure off the whole rig moved rigidly as the idle moves the head (the root keyed to the head's motion, every other key dropped), where every pixel is still in the head's frame by construction. `screen_heat_mean`/`max` over `screen_px` are the screen-space figures this line held until issue #33 (where `face` is on top, less the features' setup boxes), reported. The feet (where `footwear` is on top): the idle's heat in screen space — they ride the root, which the idle does not move, so the screen is their own frame | face `head_frame_heat_mean` ≤ 2 × `resampler_heat_mean` (`mean_ceiling`); feet mean ≤ 3.244 | face 1.321 ≤ 2.292 (resampler 1.146; screen 16.988); feet 1.622 | face 0.884 ≤ 1.765 (resampler 0.883; screen 11.475); feet 0 | no `face` and no `footwear` part (one of the two absent leaves that half unmeasured; so do face parts on two slot bones, or idle frames with no `geometry.json`) |
|
|
614
|
+
| `TEXTURE_STRETCH` | every mesh triangle's three edges in every idle frame, read from the `geometry.json` the idle render writes (`rigc render --geometry`, spine-rigc 1.4.0 or later): skinned length over rest length, the rest being the setup pose's bones with no deform. The figure is the rig's worst max(ratio, 1/ratio); each mesh's largest and smallest ratio, with triangle, vertices, edge and frame, is the detail | max(ratio, 1/ratio) ≤ 1.926544 | 1.388: `hair_back` triangle 449, edge 42-43 at 0.720 in frame 26 (largest stretch 1.291, frame 2) | 1.227: `sleeves` triangle 169, edge 126-51 at 0.815 in frame 28 (largest stretch 1.196, frame 5) | the rig draws no mesh, or the render wrote no `geometry.json` |
|
|
554
615
|
|
|
555
616
|
After them, one **reported line** with no bar:
|
|
556
617
|
|
|
@@ -575,13 +636,15 @@ What each figure is, and is not:
|
|
|
575
636
|
layers".** A gap shows the page, a rim a colour no part has there, a doubled line a
|
|
576
637
|
part drawn off its place; each changes the setup-pose render against the flat stack,
|
|
577
638
|
which is what the seam bar measures. No separate line is written for it.
|
|
578
|
-
- **The blink is measured at the setup pose, not in an idle frame.**
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
rigc's `render` takes no time, so the closed pose is a throwaway
|
|
583
|
-
the blink tracks' closed value, built and rendered beside the
|
|
584
|
-
|
|
639
|
+
- **The blink is measured at the setup pose, not in an idle frame.** Since issue #32
|
|
640
|
+
an idle frame does fall inside the closed window — on both examples the eyes are
|
|
641
|
+
fully shut from 2.37 s to 2.454 s and `idle_frames_closed` is `[29]` (2.417 s) — but
|
|
642
|
+
that frame also carries the idle's sway, so it is not a comparison of what the blink
|
|
643
|
+
alone changed. rigc's `render` takes no time, so the closed pose is a throwaway
|
|
644
|
+
animation holding the blink tracks' closed value, built and rendered beside the
|
|
645
|
+
seam's still on the same grid. `idle_frames_closed` is what says the loop shows the
|
|
646
|
+
closed eye; before #32 it was `[]` on both examples, with the 0.04 s hold between
|
|
647
|
+
frames 28 and 29.
|
|
585
648
|
- **The colour-patch figure is not reliable enough for a bar.** "Nearest colour in the
|
|
586
649
|
open eye's box" counts a legitimate colour the open eye never showed (skin under the
|
|
587
650
|
lid) as a patch, and a patch the open eye happened to contain as none. It is
|
|
@@ -599,22 +662,70 @@ What each figure is, and is not:
|
|
|
599
662
|
mean heat is 1.1 times the upper half's while its centroid travels 3.7 times as far.
|
|
600
663
|
The halves split the box across its rows, which assumes the part hangs — true of a
|
|
601
664
|
front-facing standing figure, the only input this tool takes.
|
|
602
|
-
- **
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
665
|
+
- **Stretch is read on edges, both ways, and the ceiling doubles |ln r|.** Compression
|
|
666
|
+
distorts a texture as much as stretch — an edge at half its rest length squeezes
|
|
667
|
+
the texels on it by the factor an edge at twice its length spreads them — so the
|
|
668
|
+
figure is max(ratio, 1/ratio), and on both examples the worst triangle is a
|
|
669
|
+
compression (demo 0.720, sample 0.815). The ceiling rule is applied to the quantity
|
|
670
|
+
whose zero means "nothing happened", as it is for heat: for a ratio that is |ln r|,
|
|
671
|
+
so twice demo's |ln 1.388| is ln(1.388²) = ln 1.926544 and sample clears it by 3.2
|
|
672
|
+
times. Doubling the ratio itself (2.776) was rejected: its zero is not at 1, and it
|
|
673
|
+
passes an edge drawn at 2.7 times its rest length. The rest is the art's own
|
|
674
|
+
proportions — on both examples every rest edge equals its `uvs` edge times the
|
|
675
|
+
attachment's size to within 2.4e-5, the uvs' six-decimal rounding.
|
|
676
|
+
- **The face is measured in the head's frame, and its bar is the model's (issue #33).**
|
|
677
|
+
Until #33 the face half read the screen-space heat map, where the head's intended
|
|
678
|
+
roll lights the outline on both examples (16.988 and 11.475, the figures now
|
|
679
|
+
reported as `screen_heat_mean`), so its ceiling was twice the worse example,
|
|
680
|
+
33.976 — a ceiling a smooth fixture face sliding two rig pixels stayed under (24.8;
|
|
681
|
+
eight pixels measured 85.5). The judgement means *nothing moves on the face that
|
|
682
|
+
the head does not move*, so the heat is now taken after each frame's face region is
|
|
683
|
+
carried back through the head bone's transform. A region the head carries is then
|
|
684
|
+
still up to the resampler's error, and nothing more. That error depends on the art
|
|
685
|
+
and the scale — 1.146 on the demo, 0.883 on the sample, 0.47 on the selftest's
|
|
686
|
+
smooth fixture face — so no one constant can be the bar: it is measured on the same
|
|
687
|
+
rig, off a throwaway animation that moves the whole rig rigidly as the idle moves
|
|
688
|
+
the head, and the ceiling is twice that (the tree's one margin). The calibration is
|
|
689
|
+
its own `rigc build`: sharing the setup still's build widened the still's viewport
|
|
690
|
+
(rigc fits it to every animation a skeleton holds) and moved the seam.
|
|
691
|
+
- **The region is the pixels that read the face and nothing else.** A pixel at the
|
|
692
|
+
outline's rim, or next to a feature, reads a neighbour's art through the two
|
|
693
|
+
bilinear taps, and that neighbour moving lights it with nothing on the face moving:
|
|
694
|
+
measured over the rim, a fixture face that does not move at all over a breathing
|
|
695
|
+
torso read 1.533 against a bar of 0 (its head never moves, so its resampler error is
|
|
696
|
+
exactly 0), and the proposer's own strand fixture went red on the brows the blink
|
|
697
|
+
drops below their setup boxes. So the region keeps only pixels whose whole
|
|
698
|
+
footprint is the face (228 of the demo's 515 screen-space pixels, 271 of the
|
|
699
|
+
sample's 453), clear of where each feature goes in the head's frame over the idle
|
|
700
|
+
(its setup box carried by its own bone and back through the head's). Measured over
|
|
701
|
+
the rim instead, both examples sat at 1.31 and 1.34 times their calibration, and
|
|
702
|
+
that excess was the bangs' art sliding over the rim.
|
|
703
|
+
- **What the examples show in the head's frame.** The sample is still to its
|
|
704
|
+
calibration (0.884 against 0.883). The demo reads 1.15 times it (1.321 against
|
|
705
|
+
1.146), and most of that is the calibration's own noise, not motion: the face
|
|
706
|
+
rendered alone (`--slot face`), rigid on the head by construction, reads 1.296 on
|
|
707
|
+
the idle's grid against 1.146 on the calibration's — rigc fits each render's
|
|
708
|
+
viewport to its own animation, so the two grids sit at a different sub-pixel phase
|
|
709
|
+
on the art (13 %). The rest, 0.025, is `hair_front`: rendered without it the figure
|
|
710
|
+
is the face-alone one. Both clear the bar, the demo by a factor of 1.74 and the sample by 2.00.
|
|
711
|
+
- **What the line now catches, and where it stops.** The selftest's fixture face,
|
|
712
|
+
a mesh weighted wholly to a bone that slides two rig pixels under its head, reads
|
|
713
|
+
27.886 against a ceiling of 0 on a still head (screen space 26.901, which the
|
|
714
|
+
retired 33.976 passed) and 27.936 against 0.94 on a rolling one. The same plant on
|
|
715
|
+
the real examples, at their render scale of about 0.5 (one rig pixel is half a frame
|
|
716
|
+
pixel): two pixels read 3.216 on the sample (ceiling 1.765, red) and 2.181 on the
|
|
717
|
+
demo (2.292, green); one pixel reads 1.614 and 1.526 (both green). A smooth skin
|
|
718
|
+
interior sliding half a frame pixel changes little, and the rim that shows it most is
|
|
719
|
+
the rim the region leaves out. The line also does not judge a face that moves with
|
|
720
|
+
the bone its own slot rides: that is the head moving, whatever the bone, so a face
|
|
721
|
+
region on the wrong bone is the feet half's and the eye's to catch, not this line's.
|
|
722
|
+
- **The feet stay in screen space.** They ride the root, which the idle does not key,
|
|
723
|
+
so the screen is their own frame. On the demo the long skirt swings over the shoes,
|
|
724
|
+
which lights 1.622.
|
|
725
|
+
|
|
726
|
+
Nothing is left that only an eye answers. "No visible texture stretch" left this list
|
|
727
|
+
with issue #31 (`TEXTURE_STRETCH`), and "the face outline held still in the head's own
|
|
728
|
+
frame" with issue #33 (the face half of `STILL_REGIONS_DARK`, above).
|
|
618
729
|
|
|
619
730
|
## 8. What one character costs
|
|
620
731
|
|
|
@@ -628,3 +739,10 @@ Still only an eye answers, each a missing instrument rather than a question to a
|
|
|
628
739
|
About 6.5 minutes of GPU and under a minute of CPU per character, before any hand
|
|
629
740
|
correction. The GPU figures are one run each on one machine; the painting's figure
|
|
630
741
|
excludes choosing among seeds (the demo was chosen from eighteen candidates).
|
|
742
|
+
Since issue #33 `check` runs one more `rigc build` and one more idle render (the face
|
|
743
|
+
calibration, §7); the idle's `geometry.json` is the one issue #31 already writes. The `build` row
|
|
744
|
+
was not re-measured: A/B against the tree before that change, alternating on one
|
|
745
|
+
machine under a load average of 5 to 11, the difference in user + sys was +5.6 and
|
|
746
|
+
+0.8 s on the demo and +6.5 and −1.6 s on the sample over two rounds — inside what
|
|
747
|
+
the load moves the same build by (the pre-change demo build alone took 43.6 to
|
|
748
|
+
45.8 s there).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-parts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"ag-psd": "^31.0.2",
|
|
60
|
-
"spine-rigc": "^1.
|
|
60
|
+
"spine-rigc": "^1.5.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.4.2",
|
|
@@ -26,9 +26,9 @@ stage writes are the whole interface.
|
|
|
26
26
|
the value found and the value required; AUTHORING §6 maps every rule to the field
|
|
27
27
|
or input that has to change.
|
|
28
28
|
- **Green is not "looks right".** `check` measures the gates, the seam, the loop and
|
|
29
|
-
|
|
30
|
-
`TIP_OVER_ROOT`, `STILL_REGIONS_DARK`
|
|
31
|
-
|
|
29
|
+
six judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`,
|
|
30
|
+
`TIP_OVER_ROOT`, `STILL_REGIONS_DARK` — its face half in the head's own frame —
|
|
31
|
+
and `TEXTURE_STRETCH`) (AUTHORING §7). A judgement line that says
|
|
32
32
|
SKIP was not verified: report it as such, with its reason, never as passed.
|
|
33
33
|
- **A hole in the layers is invisible to every bar.** Painting that no part holds is
|
|
34
34
|
missing from the setup pose and from the flat stack alike, so the seam passes over
|
|
@@ -68,8 +68,10 @@ stage writes are the whole interface.
|
|
|
68
68
|
7. `spine-parts build --config … --source … --full … --head … --out out --loop`.
|
|
69
69
|
It stops at the first stage that refuses, printing that stage's lines under
|
|
70
70
|
`[assemble]`, `[rig]` or `[check]`. Fix what the FAIL line names and build again.
|
|
71
|
-
8. Read `out/check/check.json` — the
|
|
72
|
-
and the loop: a FAIL names the field to change (AUTHORING §6)
|
|
71
|
+
8. Read `out/check/check.json` — the six judgement lines the same way as the seam
|
|
72
|
+
and the loop: a FAIL names the field to change (AUTHORING §6);
|
|
73
|
+
`TEXTURE_STRETCH` names the worst mesh triangle (slot, triangle, vertices, edge,
|
|
74
|
+
frame), so quote it even on a PASS. Quote
|
|
73
75
|
`RECOMPOSITE_HOLES` (uncovered px, hole count, the largest hole's box) in the
|
|
74
76
|
report whatever `PASS` says. The artifact is the three paths the build prints
|
|
75
77
|
last: `check/build/skeleton.json`, `skeleton.atlas` and the packed page. Report
|