spine-parts 0.4.0 → 0.6.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 +39 -7
- package/cli.ts +66 -21
- package/docs/AUTHORING.md +130 -29
- package/package.json +2 -2
- package/skills/spine-parts/SKILL.md +7 -5
- package/src/build.ts +18 -13
- package/src/check.ts +547 -39
- package/src/config.ts +14 -0
- package/src/instruments.ts +358 -3
- package/src/propose.ts +32 -7
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
|
@@ -38,14 +38,14 @@ is taken from the full run, because the twin tails leave the head crop sideways
|
|
|
38
38
|
<code>49 assertions: 14 measured (14 passed, 0 failed), 20 skipped, 15 not in profile "spine"</code><br/>
|
|
39
39
|
<code>loop: idle 49 frame(s) at 12 fps, f0000 vs f0048 (t = 4s): max |d| 0 (0 required)</code><br/>
|
|
40
40
|
<code>seam: setup pose at 714x1216, scale 0.9422: mean |d| 0.324 (<= 1.0), 2 px over 40 (<= 50), 0 px over 80 (reported)</code><br/>
|
|
41
|
-
<code>pack: skeleton.png
|
|
41
|
+
<code>pack: skeleton.png 967x1338, 22 region(s), 91.2% covered, padding 2, page edges free</code><br/>
|
|
42
42
|
<code>--seam silhouette</code> repaired the navy blobs the default rule leaves on the white
|
|
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,520 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,350 bytes; the GIF, at the same error, 1,812,238). Frame 29 (2.417 s) is the
|
|
49
49
|
closed eye: the blink holds for 0.084 s, one 12 fps frame rounded up, where the
|
|
50
50
|
reference implementation held 0.04 s and no frame of its loop showed the eyes shut
|
|
51
51
|
(issue #32); the painting is shown at half size, resampled and written
|
|
@@ -87,10 +87,10 @@ page** (issue #2):
|
|
|
87
87
|
|
|
88
88
|
| path under `--out` | what |
|
|
89
89
|
| --- | --- |
|
|
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 |
|
|
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 --page-edges free` and gated under both profiles |
|
|
91
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 |
|
|
92
92
|
| `rig/` | `rig.json` and `motion.json` in spine-rigc's spec, `mesh_report.json`, the padded `images/` |
|
|
93
|
-
| `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` |
|
|
94
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 |
|
|
95
95
|
|
|
96
96
|
The last lines of a green build are the pack line, printed beside the Spine example
|
|
@@ -99,12 +99,23 @@ artifact paths — here for `examples/sample`:
|
|
|
99
99
|
|
|
100
100
|
```
|
|
101
101
|
build: PASS — the packed atlas is the artifact; parts/ and rig/ are the intermediates it was made from
|
|
102
|
-
pack: skeleton.png
|
|
102
|
+
pack: skeleton.png 479x1166, 20 region(s), 93.4% covered, padding 2, page edges free; page opaque 53.9% (alpha > 0) — spineboy yardstick 1024x256, 40 region(s), 45.8% opaque (alpha > 0), a reference and not a bar
|
|
103
103
|
out/check/build/skeleton.json
|
|
104
104
|
out/check/build/skeleton.atlas
|
|
105
105
|
out/check/build/skeleton.png
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
The page's edges are free by default: rigc picks the least-area page the parts need,
|
|
109
|
+
not the next power of two. On the two examples that is 967x1338 and 479x1166, where a
|
|
110
|
+
power-of-two page is 1024x2048 and 512x2048, and covered goes from 56.3 % to 91.2 % and
|
|
111
|
+
from 49.7 % to 93.4 %. The cost is the one rigc's help states: *"region attachments
|
|
112
|
+
sampling within 1 LSB of the loose build rather than exactly"*. Measured on both
|
|
113
|
+
examples, every idle frame stays within 1 level of the power-of-two build's. The atlas says
|
|
114
|
+
`filter: Linear, Linear` and has no `repeat` line, so `--page-edges pot` is for a
|
|
115
|
+
consumer that mipmaps or repeats the page; it writes exactly the page builds wrote
|
|
116
|
+
before the flag existed. The measurements are in
|
|
117
|
+
[AUTHORING §5, *Page edges*](docs/AUTHORING.md#5-after-each-stage-what-to-read).
|
|
118
|
+
|
|
108
119
|
The packer is spine-rigc's, and no other: a packed region is a lossless copy, and an
|
|
109
120
|
atlas written by anything else would have no oracle behind it.
|
|
110
121
|
|
|
@@ -132,6 +143,27 @@ spine-parts at what it wrote:
|
|
|
132
143
|
| 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/` |
|
|
133
144
|
| 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 |
|
|
134
145
|
|
|
146
|
+
**The head run is needed on every route that was read.** See-through's model
|
|
147
|
+
already separates the head in a second stage of its own: it crops the head the
|
|
148
|
+
first stage found, runs the face tags on that crop at the same `resolution`, and
|
|
149
|
+
pastes the result back **resized to the full image's scale**. So the face comes
|
|
150
|
+
back at the full run's pixel density whichever route ran it, and the separate
|
|
151
|
+
head run on `st_input_head.png` is what gives the eyes more pixels
|
|
152
|
+
(read on 2026-09-28 at see-through `a25a549` and wrapper 0.5.0 `98d754b`):
|
|
153
|
+
|
|
154
|
+
- *Hugging Face Space* — the same two-stage code, with `resolution` capped at
|
|
155
|
+
1280 (768 by default): the head run is still needed.
|
|
156
|
+
- *ModelScope demo* — its code was not read, so what the head run adds on that
|
|
157
|
+
route has not been measured.
|
|
158
|
+
- *Upstream CLI* — the `.psd` frame is the `resolution`-sided square the first
|
|
159
|
+
stage ran on (1280 by default), and the head stage is pasted into it at that
|
|
160
|
+
scale: the head run is still needed.
|
|
161
|
+
- *ComfyUI wrapper* — `SeeThrough_GenerateLayers` runs both stages with no input
|
|
162
|
+
to change the second, at most `resolution` 2048, which on the examples' 2432 px
|
|
163
|
+
square is 1.19 source px per layer px against 0.77 and 0.47 for their head
|
|
164
|
+
crops; at the 1024 the examples ran, the full run's right eye white is 22×13 px
|
|
165
|
+
and the head run's 60×38: the head run is still needed.
|
|
166
|
+
|
|
135
167
|
**See-through is required; ComfyUI is not.** The wrapper is one route among
|
|
136
168
|
four, and `spine-parts comfy` is only a convenience for that route: every
|
|
137
169
|
stage after See-through reads files, whichever route wrote them. On the two
|
|
@@ -222,7 +254,7 @@ agent skill.
|
|
|
222
254
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
|
|
223
255
|
| `propose … --from-config <config>` | draw and LINT the config's current bones |
|
|
224
256
|
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after spine-rigc's round trip is green; `--idle-keys` says whether the idle's keys on mesh-driving bones go through `<bone>_ctl` parents (default) or stay on the bones with `invariants.idleDrivesMeshes` declared |
|
|
225
|
-
| `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam, loop and the
|
|
257
|
+
| `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` |
|
|
226
258
|
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
227
259
|
| `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
228
260
|
|
package/cli.ts
CHANGED
|
@@ -18,7 +18,7 @@ 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 { DEFAULT_PAGE_EDGES, findRigc, 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';
|
|
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';
|
|
@@ -77,7 +77,8 @@ usage:
|
|
|
77
77
|
regions, motion with its blink — and a blink.still cut for a lash that
|
|
78
78
|
reaches far above its eyewhite, when a clear row allows one; no blink
|
|
79
79
|
when no part is an eyewhite, and no brows in it when no part is an
|
|
80
|
-
eyebrow, each said in a note
|
|
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)
|
|
81
82
|
and the overlay to correct against, <out>/render/landmarks.png and
|
|
82
83
|
landmarks_head.png. Prints every
|
|
83
84
|
note and a LINT line for each chain link that lies off its mesh's art, for a
|
|
@@ -97,6 +98,7 @@ usage:
|
|
|
97
98
|
printed when one was needed.
|
|
98
99
|
|
|
99
100
|
spine-parts rig --config <config.json> --parts <dir> --out <dir> [--idle-keys ctl|direct]
|
|
101
|
+
[--page-edges pot|free]
|
|
100
102
|
Author the rig: unrotated bones at the config's landmarks (a chain makes
|
|
101
103
|
<chain>0..n), a square lattice mesh over every part in config.meshes
|
|
102
104
|
weighted by distance to its candidate bone segments, a region for every
|
|
@@ -105,11 +107,12 @@ usage:
|
|
|
105
107
|
and one idle of sines and a blink whose closed hold is at least one
|
|
106
108
|
12 fps frame, so the idle frames and the loop show the eyes shut
|
|
107
109
|
(RIG_BLINK_HOLD_SPANS_A_FRAME otherwise); no blink when the config
|
|
108
|
-
states no motion.blink, and a blink group that names no bone
|
|
109
|
-
by the loader, CONFIG_BLINK_GROUP_MEMBERS
|
|
110
|
+
states no motion.blink, and a blink group that names no bone, or one
|
|
111
|
+
bone twice, is refused by the loader, CONFIG_BLINK_GROUP_MEMBERS or
|
|
112
|
+
CONFIG_BLINK_GROUP_UNIQUE, before rigc starts. --parts is the
|
|
110
113
|
directory holding parts.json and parts/<name>.png. The result is built
|
|
111
|
-
through spine-rigc (profile spine-html, packed
|
|
112
|
-
profile spine) in a scratch directory first, and --out receives
|
|
114
|
+
through spine-rigc (profile spine-html, packed with --page-edges as for
|
|
115
|
+
check, then validated under profile spine) in a scratch directory first, and --out receives
|
|
113
116
|
images/*.png, rig.json, motion.json and mesh_report.json only when both
|
|
114
117
|
are green. Prints one line per mesh and the rigc gate lines.
|
|
115
118
|
--idle-keys says where the idle's keys on a bone a mesh is weighted to go:
|
|
@@ -119,27 +122,38 @@ usage:
|
|
|
119
122
|
spine-rigc 1.3.0 or later and makes A15 a SKIP that prints its cost (the
|
|
120
123
|
stage prints that SKIP line). The pose is the same to one level of float
|
|
121
124
|
rounding, and so is the per-frame mesh work (AUTHORING §5).
|
|
122
|
-
spine-parts check --rig <dir> --out <dir> [--parts <dir>]
|
|
125
|
+
spine-parts check --rig <dir> --out <dir> [--parts <dir>] [--page-edges pot|free]
|
|
123
126
|
Build, gate, render and measure a rig through spine-rigc's CLI (the rigc at
|
|
124
127
|
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json
|
|
125
128
|
(with an "idle").
|
|
126
129
|
${PARTS_HOME_SENTENCE}.
|
|
127
130
|
Both are only read. Into --out:
|
|
128
|
-
build/ (rigc build --profile spine-html --pack
|
|
129
|
-
artifact), gate_spine-html.txt and gate_spine.txt (the gate lines
|
|
130
|
-
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
131
|
+
build/ (rigc build --profile spine-html --pack --page-edges <value>: the
|
|
132
|
+
packed atlas is the artifact), gate_spine-html.txt and gate_spine.txt (the gate lines
|
|
133
|
+
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
134
|
+
--geometry: the frames and geometry.json, the skinned vertices and
|
|
135
|
+
every bone's world transform per frame),
|
|
131
136
|
contact.png, motion_heat.png and check.json. PASS needs both gates
|
|
132
137
|
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
133
138
|
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
|
|
134
|
-
frame at t = duration) at max |d| 0. Then
|
|
139
|
+
frame at t = duration) at max |d| 0. Then six judgement lines, each in
|
|
135
140
|
check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
|
|
136
141
|
bars (AUTHORING §7): BREATH_VISIBLE (the topwear moves, the footwear does
|
|
137
142
|
not, each rendered alone), BLINK_NO_HOLE (the setup pose with the blink
|
|
138
143
|
held shut shows no background inside the eyewhite box), CHAIN_LAG (every
|
|
139
144
|
rotate track lags its keyed ancestor and amplitude grows down each
|
|
140
145
|
chain, read off motion.json), TIP_OVER_ROOT (each handwear/bottomwear
|
|
141
|
-
part's lower half travels further than its upper half)
|
|
142
|
-
STILL_REGIONS_DARK (the
|
|
146
|
+
part's lower half travels further than its upper half),
|
|
147
|
+
STILL_REGIONS_DARK (the face outline in the frame of the bone the face's
|
|
148
|
+
slot rides, at most twice the resampler's own error on this rig — read
|
|
149
|
+
off the whole rig moved rigidly as the idle moves that bone — with the
|
|
150
|
+
screen-space figure reported beside it; the feet in screen space) and
|
|
151
|
+
TEXTURE_STRETCH (every mesh triangle's edges over the idle against their
|
|
152
|
+
rest length, max(ratio, 1/ratio) <= ${TEXTURE_STRETCH_CEILING}; the worst
|
|
153
|
+
triangle is named by slot, triangle, vertices, edge and frame).
|
|
154
|
+
Needs spine-rigc ${RIGC_GEOMETRY_VERSION} or later, whose render writes
|
|
155
|
+
geometry.json; an older rigc is refused, CHECK_RIGC_VERSION, before
|
|
156
|
+
anything is built.
|
|
143
157
|
Regions come from parts.json's See-through tags; a line with nothing to
|
|
144
158
|
read says SKIP and why — neither a pass nor a failure — and PASS needs
|
|
145
159
|
every line that measured to be PASS. Then RECOMPOSITE_HOLES: REPORTED,
|
|
@@ -150,6 +164,16 @@ usage:
|
|
|
150
164
|
beside the spineboy yardstick (${SPINEBOY_YARDSTICK}), a reference and not
|
|
151
165
|
a bar. Exit 0 on PASS, 1 on FAIL — every FAIL line names the bar, the value
|
|
152
166
|
and the value required.
|
|
167
|
+
--page-edges is handed to rigc verbatim and defaults to ${DEFAULT_PAGE_EDGES}: the
|
|
168
|
+
least-area page the parts need (on the two public examples 967x1338 and
|
|
169
|
+
479x1166, where pot writes 1024x2048 and 512x2048), at the cost rigc
|
|
170
|
+
states — region attachments sample within 1 LSB of the loose build
|
|
171
|
+
rather than exactly (measured on both examples: every idle frame within
|
|
172
|
+
1 level of the pot build's; AUTHORING §5, Page edges). pot is a power of two on both edges, for a consumer
|
|
173
|
+
that mipmaps or repeats the page (the atlas declares filter Linear,
|
|
174
|
+
Linear and no repeat). rigc's pack line ends ", page edges free" under
|
|
175
|
+
free; a line that disagrees with the value passed, or a pot page that is
|
|
176
|
+
not a power of two, is refused, CHECK_PACK_PAGE_EDGES.
|
|
153
177
|
|
|
154
178
|
spine-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
|
|
155
179
|
Encode a frame set rigc render wrote (its --out directory, or the set
|
|
@@ -234,7 +258,7 @@ usage:
|
|
|
234
258
|
|
|
235
259
|
spine-parts build --config <config.json> --source <painting.png> --full <dir|psd>
|
|
236
260
|
--head <dir|psd> --out <dir> [--seam near-white|silhouette]
|
|
237
|
-
[--project core|visible] [--loop]
|
|
261
|
+
[--project core|visible] [--page-edges pot|free] [--loop]
|
|
238
262
|
assemble, then rig, then check, in one process, each stage's own lines
|
|
239
263
|
printed under [assemble], [rig] and [check]; the first stage that refuses
|
|
240
264
|
stops the build with its own FAIL lines. The config must already carry
|
|
@@ -250,7 +274,8 @@ usage:
|
|
|
250
274
|
three artifact paths — skeleton .json, .atlas and the packed page: the
|
|
251
275
|
packed atlas is the result, the loose parts are the intermediate it was
|
|
252
276
|
made from. --seam defaults to ${DEFAULT_SEAM_RULE}, --project to
|
|
253
|
-
${DEFAULT_PROJECT_RULE} (both as for assemble)
|
|
277
|
+
${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES}
|
|
278
|
+
(as for check; both packed builds, rig's gate and check's artifact, take it).
|
|
254
279
|
|
|
255
280
|
spine-parts --version
|
|
256
281
|
spine-parts --help
|
|
@@ -385,15 +410,26 @@ function rigGateRunner(): RigcRunner {
|
|
|
385
410
|
};
|
|
386
411
|
}
|
|
387
412
|
|
|
413
|
+
/** `--page-edges`, or its default; a value spine-rigc does not take is a usage error naming the two it does. */
|
|
414
|
+
function pageEdgesOf(value: string | undefined): PageEdges | string {
|
|
415
|
+
const edges = value ?? DEFAULT_PAGE_EDGES;
|
|
416
|
+
return (PAGE_EDGES as readonly string[]).includes(edges) ? (edges as PageEdges) : `--page-edges ${edges}; one of ${PAGE_EDGES.join(', ')} is required`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function isPageEdges(v: PageEdges | string): v is PageEdges {
|
|
420
|
+
return (PAGE_EDGES as readonly string[]).includes(v);
|
|
421
|
+
}
|
|
422
|
+
|
|
388
423
|
function cmdRig(args: string[]): number {
|
|
389
424
|
let config: string | null = null;
|
|
390
425
|
let partsDir: string | null = null;
|
|
391
426
|
let out: string | null = null;
|
|
392
427
|
let idleKeys: string | null = null;
|
|
428
|
+
let pageEdges: string | null = null;
|
|
393
429
|
for (let i = 0; i < args.length; i++) {
|
|
394
430
|
const flag = args[i];
|
|
395
431
|
const value = args[i + 1];
|
|
396
|
-
if (!['--config', '--parts', '--out', '--idle-keys'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
432
|
+
if (!['--config', '--parts', '--out', '--idle-keys', '--page-edges'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
397
433
|
if (value === undefined) return usage(`${flag} needs a value`);
|
|
398
434
|
i++;
|
|
399
435
|
if (flag === '--config') config = value;
|
|
@@ -401,6 +437,9 @@ function cmdRig(args: string[]): number {
|
|
|
401
437
|
else if (flag === '--idle-keys') {
|
|
402
438
|
if (idleKeys !== null) return usage('--idle-keys is given twice');
|
|
403
439
|
idleKeys = value;
|
|
440
|
+
} else if (flag === '--page-edges') {
|
|
441
|
+
if (pageEdges !== null) return usage('--page-edges is given twice');
|
|
442
|
+
pageEdges = value;
|
|
404
443
|
} else out = value;
|
|
405
444
|
}
|
|
406
445
|
if (config === null) return usage('rig needs --config <config.json>');
|
|
@@ -408,9 +447,11 @@ function cmdRig(args: string[]): number {
|
|
|
408
447
|
if (out === null) return usage('rig needs --out <dir>');
|
|
409
448
|
const keys = idleKeys ?? DEFAULT_IDLE_KEYS;
|
|
410
449
|
if (!(IDLE_KEYS as readonly string[]).includes(keys)) return usage(`--idle-keys ${keys}; one of ${IDLE_KEYS.join(', ')} is required`);
|
|
450
|
+
const edges = pageEdgesOf(pageEdges ?? undefined);
|
|
451
|
+
if (!isPageEdges(edges)) return usage(edges);
|
|
411
452
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-rig-'));
|
|
412
453
|
try {
|
|
413
|
-
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys }, rigGateRunner(), scratch, console.log);
|
|
454
|
+
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys, pageEdges: edges }, rigGateRunner(), scratch, console.log);
|
|
414
455
|
return EXIT_OK;
|
|
415
456
|
} catch (err) {
|
|
416
457
|
return printRefusal(err);
|
|
@@ -443,13 +484,15 @@ function flags(args: string[], known: readonly string[], command: string, option
|
|
|
443
484
|
}
|
|
444
485
|
|
|
445
486
|
function cmdCheck(args: string[]): number {
|
|
446
|
-
const f = flags(args, ['--rig', '--out'], 'check', ['--parts']);
|
|
487
|
+
const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--page-edges']);
|
|
447
488
|
if (typeof f === 'string') return usage(f);
|
|
448
489
|
const rig = f.get('--rig') as string;
|
|
449
490
|
const out = f.get('--out') as string;
|
|
491
|
+
const edges = pageEdgesOf(f.get('--page-edges'));
|
|
492
|
+
if (!isPageEdges(edges)) return usage(edges);
|
|
450
493
|
try {
|
|
451
494
|
const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
452
|
-
const r = checkStage({ rig, parts: f.get('--parts') ?? rig, out }, rigcRunner(bin), bin, console.log);
|
|
495
|
+
const r = checkStage({ rig, parts: f.get('--parts') ?? rig, out, pageEdges: edges }, rigcRunner(bin), bin, console.log);
|
|
453
496
|
return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
|
|
454
497
|
} catch (err) {
|
|
455
498
|
return printRefusal(err);
|
|
@@ -754,7 +797,7 @@ function cmdInputs(args: string[]): number {
|
|
|
754
797
|
function cmdBuild(args: string[]): number {
|
|
755
798
|
const flags = new Map<string, string>();
|
|
756
799
|
let loop = false;
|
|
757
|
-
const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project'];
|
|
800
|
+
const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges'];
|
|
758
801
|
for (let i = 0; i < args.length; i++) {
|
|
759
802
|
const flag = args[i];
|
|
760
803
|
if (flag === '--loop') {
|
|
@@ -774,6 +817,8 @@ function cmdBuild(args: string[]): number {
|
|
|
774
817
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
775
818
|
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
776
819
|
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
820
|
+
const edges = pageEdgesOf(flags.get('--page-edges'));
|
|
821
|
+
if (!isPageEdges(edges)) return usage(edges);
|
|
777
822
|
const [config, source, full, head, out] = ['--config', '--source', '--full', '--head', '--out'].map((f) => flags.get(f) as string);
|
|
778
823
|
let bin: string;
|
|
779
824
|
try {
|
|
@@ -784,7 +829,7 @@ function cmdBuild(args: string[]): number {
|
|
|
784
829
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-build-'));
|
|
785
830
|
try {
|
|
786
831
|
const r = build(
|
|
787
|
-
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop },
|
|
832
|
+
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges },
|
|
788
833
|
{ rig: rigGateRunner(), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
|
|
789
834
|
console.log,
|
|
790
835
|
);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -76,7 +76,7 @@ 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, `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
|
|
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
82
|
The blink's timing is the tree's (`BLINK` in `src/motion.ts`), in seconds after
|
|
@@ -101,7 +101,8 @@ leave it 0.01 s after the lids open. The rig stage refuses a hold under one fram
|
|
|
101
101
|
name (`RIG_BLINK_HOLD_SPANS_A_FRAME`, §6), counted over the same 250,000 phases.
|
|
102
102
|
|
|
103
103
|
The proposer's reach (from `src/propose.ts`): roles come from each part's tag —
|
|
104
|
-
`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
|
|
105
106
|
told apart by position, `bottomwear` makes the hip and three skirt chains,
|
|
106
107
|
`handwear` makes sleeve chains (one blob or two), `front hair` makes fringe chains
|
|
107
108
|
and a lock chain for each strand that hangs below the chin, `back hair` makes a bun
|
|
@@ -226,7 +227,11 @@ full loader before its assemble stage writes anything.
|
|
|
226
227
|
4. `spine-parts propose --head-box --full inputs/layers/full --canvas <W>x<H>` →
|
|
227
228
|
`seethrough.head_box`. Run `spine-parts inputs` again: it now also writes
|
|
228
229
|
`st_input_head.png`, the painting cropped to that box at its exact size.
|
|
229
|
-
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.
|
|
230
235
|
6. `spine-parts assemble --propose-plan --source … --full … --head … --config config.json`
|
|
231
236
|
→ paste `plan` and `extend_below_crop` into `config.assemble`. Config: the
|
|
232
237
|
`layers` row, with `head_box`.
|
|
@@ -260,9 +265,9 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
|
|
|
260
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*) |
|
|
261
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 |
|
|
262
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) |
|
|
263
|
-
| `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 |
|
|
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` |
|
|
264
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` |
|
|
265
|
-
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:`, `seam:`, the
|
|
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) |
|
|
266
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) |
|
|
267
272
|
|
|
268
273
|
`loop` writes three files from one frame set, and they are not interchangeable.
|
|
@@ -274,9 +279,11 @@ entry, no dithering, filter None) — and is the small file to show. `idle.gif`
|
|
|
274
279
|
(`loop --out x.gif`) is the same median cut in a GIF. The indexed APNG and the GIF
|
|
275
280
|
print their palette error, per channel over R, G and B of every frame (and alpha's
|
|
276
281
|
max for the APNG); a figure is a measurement of the file, not a bar. On the demo:
|
|
277
|
-
13,645,
|
|
282
|
+
13,645,350 B lossless, 1,706,520 B indexed and 1,812,238 B GIF, both palette files at
|
|
278
283
|
max 57, mean 1.601 — the two share one quantiser, so their error is the same by
|
|
279
|
-
construction and the size is the difference.
|
|
284
|
+
construction and the size is the difference. That is `--seam silhouette` (the README's
|
|
285
|
+
animation); with `--page-edges pot` the same build writes 13,645,519, 1,706,785 and
|
|
286
|
+
1,812,625 B at the same error. The loop shows the blink closed: the
|
|
280
287
|
eyes' hold is at least one 12 fps frame (below), so whatever `motion.blink.t` is, one
|
|
281
288
|
idle frame lands inside it, and `check.json`'s `BLINK_NO_HOLE.idle_frames_closed`
|
|
282
289
|
names it — `[29]` on both examples.
|
|
@@ -289,6 +296,36 @@ outside the figure. On the demo it lowered recomposite error pixels from 11,050
|
|
|
289
296
|
9,540 and changed no check bar; on the sample it changed nothing. The default stays
|
|
290
297
|
the reference's so the examples stay comparable with it.
|
|
291
298
|
|
|
299
|
+
**Page edges.** `rig`, `check` and `build` pack through `rigc build --pack
|
|
300
|
+
--page-edges <v>`, and hand the value to rigc verbatim. The default is `free`,
|
|
301
|
+
the least-area page the parts need. `--page-edges pot` is the opt-out: a power of
|
|
302
|
+
two on both edges, which is the page every build wrote before this flag. The pack
|
|
303
|
+
line says which one ran, because rigc ends it with `, page edges free` under `free`.
|
|
304
|
+
Measured with rigc 1.5.1 on the two examples, by `tools/atlas_population.ts`:
|
|
305
|
+
|
|
306
|
+
| build | page | covered | page opaque | page / figure |
|
|
307
|
+
| --- | --- | --- | --- | --- |
|
|
308
|
+
| demo, `pot` | 1024x2048 | 56.3 % | 36.8 % | 2.651 |
|
|
309
|
+
| demo, `free` | 967x1338 | 91.2 % | 59.7 % | 1.636 |
|
|
310
|
+
| sample, `pot` | 512x2048 | 49.7 % | 28.7 % | 3.294 |
|
|
311
|
+
| sample, `free` | 479x1166 | 93.4 % | 53.9 % | 1.755 |
|
|
312
|
+
|
|
313
|
+
rigc's help states the cost: *"a smaller page, at the cost of region attachments
|
|
314
|
+
sampling within 1 LSB of the loose build rather than exactly"*. On these builds
|
|
315
|
+
every one of the 49 idle frames differs from the `pot` build, by at most 1 level
|
|
316
|
+
in any channel:
|
|
317
|
+
|
|
318
|
+
| example | idle frames that differ | pixels that differ (all 49 frames) | max \|d\| |
|
|
319
|
+
| --- | --- | --- | --- |
|
|
320
|
+
| demo | 49 of 49 | 2,373 | 1 |
|
|
321
|
+
| sample | 49 of 49 | 651 | 1 |
|
|
322
|
+
|
|
323
|
+
Only one check figure moved. It has no bar: sample's reported screen-space face
|
|
324
|
+
mean in `STILL_REGIONS_DARK`, 11.475 under `pot` and 11.472 under `free`. Every
|
|
325
|
+
barred figure and every status is the same under both. The atlas rigc writes says
|
|
326
|
+
`filter: Linear, Linear` and has no `repeat` line, so choose `pot` for a consumer
|
|
327
|
+
that mipmaps or repeats the page.
|
|
328
|
+
|
|
292
329
|
`--project core` (the default) is the reference's projection rule: a layer takes the
|
|
293
330
|
painting's pixel only inside its top-most `alpha >= 250` area eroded by a 5x5 square,
|
|
294
331
|
so a part a few pixels wide (a lash, a brow, an iris) takes none. `--project visible`
|
|
@@ -482,6 +519,7 @@ See-through with another seed.
|
|
|
482
519
|
| `CONFIG_AMPS_MATCH_CHAIN` | a chain track's `amps` is not one per link | that track's `amps` |
|
|
483
520
|
| `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` |
|
|
484
521
|
| `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 |
|
|
522
|
+
| `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 |
|
|
485
523
|
| `CONFIG_BLINK_BROWS_PAIRED` | `motion.blink.brows` without `brow_drop`, or `brow_drop` without `brows` | state both or neither |
|
|
486
524
|
| `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 |
|
|
487
525
|
|
|
@@ -550,6 +588,7 @@ See-through with another seed.
|
|
|
550
588
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
551
589
|
| `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/` |
|
|
552
590
|
| `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) |
|
|
591
|
+
| `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` |
|
|
553
592
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
554
593
|
| `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 |
|
|
555
594
|
| `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 |
|
|
@@ -558,9 +597,12 @@ See-through with another seed.
|
|
|
558
597
|
| `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 |
|
|
559
598
|
| `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) |
|
|
560
599
|
| `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) |
|
|
561
|
-
| `CHECK_STILL_REGIONS_DARK` | the heat map is brighter than the ceiling
|
|
562
|
-
| `
|
|
600
|
+
| `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 |
|
|
601
|
+
| `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 |
|
|
602
|
+
| `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 |
|
|
563
603
|
| `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 |
|
|
604
|
+
| `CHECK_PACK_LINE_READS` | rigc printed a `pack:` line that is not `pack: <page> <W>x<H>, <N> region(s), <P>% covered, padding <D>` with `, page edges free` after it or nothing; the line is quoted | a rigc this package does not know the output of; report it, with `rigc --version` |
|
|
605
|
+
| `CHECK_PACK_PAGE_EDGES` | the pack line disagrees with the `--page-edges` the build was run with — its `, page edges free` suffix is there under `pot` or missing under `free`, or a `pot` page is not a power of two on both edges | a rigc problem; report it, with `rigc --version` |
|
|
564
606
|
| `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
|
|
565
607
|
|
|
566
608
|
## 7. The bars `check` enforces, and what only an eye answers today
|
|
@@ -578,8 +620,8 @@ implementation's bars):
|
|
|
578
620
|
On the examples: `sample` 23/23 and 14/14, seam 0.207 with 0 pixels over 40, loop 0;
|
|
579
621
|
`demo` (default rule) 23/23 and 14/14, seam 0.326 with 2 pixels over 40, loop 0.
|
|
580
622
|
|
|
581
|
-
A green gate cannot see a wrong animation, so `check` also writes
|
|
582
|
-
lines** (issue #11), each a key of `check.json` and a console line
|
|
623
|
+
A green gate cannot see a wrong animation, so `check` also writes six **judgement
|
|
624
|
+
lines** (issue #11; `TEXTURE_STRETCH`, issue #31), each a key of `check.json` and a console line
|
|
583
625
|
`NAME: PASS|FAIL|SKIP — <figures and bars>`, read the same way as the lines above: a
|
|
584
626
|
FAIL makes `PASS` false and prints its own `FAIL CHECK_<NAME>` line (§6); a SKIP
|
|
585
627
|
says why the rig gave the line nothing to read, and is neither a pass nor a failure —
|
|
@@ -590,7 +632,9 @@ change from idle frame 0, in levels of 255, on the idle's 640-pixel grid.
|
|
|
590
632
|
Each bar follows one rule: a floor is half the weaker example's figure and a ceiling
|
|
591
633
|
twice the worse one's, so the weaker example clears it by a factor of two; a bar the
|
|
592
634
|
model itself fixes (a part on an unkeyed bone does not move, a lag is above 0, a hole
|
|
593
|
-
is 0 pixels) is that value, not a margin
|
|
635
|
+
is 0 pixels) is that value, not a margin — and where the model's value is an error
|
|
636
|
+
the rig itself measures (a face the head carries is still in the head's frame up to
|
|
637
|
+
the resampler's error), the bar is that measurement times the same two. The figures are this port's, measured on
|
|
594
638
|
the two examples [observed]:
|
|
595
639
|
|
|
596
640
|
| line | measures | bar | `demo` | `sample` | SKIP when |
|
|
@@ -600,7 +644,8 @@ the two examples [observed]:
|
|
|
600
644
|
| ″ (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 |
|
|
601
645
|
| `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 |
|
|
602
646
|
| `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 |
|
|
603
|
-
| `STILL_REGIONS_DARK` | the
|
|
647
|
+
| `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`) |
|
|
648
|
+
| `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` |
|
|
604
649
|
|
|
605
650
|
After them, one **reported line** with no bar:
|
|
606
651
|
|
|
@@ -651,22 +696,71 @@ What each figure is, and is not:
|
|
|
651
696
|
mean heat is 1.1 times the upper half's while its centroid travels 3.7 times as far.
|
|
652
697
|
The halves split the box across its rows, which assumes the part hangs — true of a
|
|
653
698
|
front-facing standing figure, the only input this tool takes.
|
|
654
|
-
- **
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
699
|
+
- **Stretch is read on edges, both ways, and the ceiling doubles |ln r|.** Compression
|
|
700
|
+
distorts a texture as much as stretch — an edge at half its rest length squeezes
|
|
701
|
+
the texels on it by the factor an edge at twice its length spreads them — so the
|
|
702
|
+
figure is max(ratio, 1/ratio), and on both examples the worst triangle is a
|
|
703
|
+
compression (demo 0.720, sample 0.815). The ceiling rule is applied to the quantity
|
|
704
|
+
whose zero means "nothing happened", as it is for heat: for a ratio that is |ln r|,
|
|
705
|
+
so twice demo's |ln 1.388| is ln(1.388²) = ln 1.926544 and sample clears it by 3.2
|
|
706
|
+
times. Doubling the ratio itself (2.776) was rejected: its zero is not at 1, and it
|
|
707
|
+
passes an edge drawn at 2.7 times its rest length. The rest is the art's own
|
|
708
|
+
proportions — on both examples every rest edge equals its `uvs` edge times the
|
|
709
|
+
attachment's size to within 2.4e-5, the uvs' six-decimal rounding.
|
|
710
|
+
- **The face is measured in the head's frame, and its bar is the model's (issue #33).**
|
|
711
|
+
Until #33 the face half read the screen-space heat map, where the head's intended
|
|
712
|
+
roll lights the outline on both examples (16.988 and 11.475, the figures now
|
|
713
|
+
reported as `screen_heat_mean`; sample's reads 11.472 since the page edges
|
|
714
|
+
default to `free`, *Page edges* in §5), so its ceiling was twice the worse example,
|
|
715
|
+
33.976 — a ceiling a smooth fixture face sliding two rig pixels stayed under (24.8;
|
|
716
|
+
eight pixels measured 85.5). The judgement means *nothing moves on the face that
|
|
717
|
+
the head does not move*, so the heat is now taken after each frame's face region is
|
|
718
|
+
carried back through the head bone's transform. A region the head carries is then
|
|
719
|
+
still up to the resampler's error, and nothing more. That error depends on the art
|
|
720
|
+
and the scale — 1.146 on the demo, 0.883 on the sample, 0.47 on the selftest's
|
|
721
|
+
smooth fixture face — so no one constant can be the bar: it is measured on the same
|
|
722
|
+
rig, off a throwaway animation that moves the whole rig rigidly as the idle moves
|
|
723
|
+
the head, and the ceiling is twice that (the tree's one margin). The calibration is
|
|
724
|
+
its own `rigc build`: sharing the setup still's build widened the still's viewport
|
|
725
|
+
(rigc fits it to every animation a skeleton holds) and moved the seam.
|
|
726
|
+
- **The region is the pixels that read the face and nothing else.** A pixel at the
|
|
727
|
+
outline's rim, or next to a feature, reads a neighbour's art through the two
|
|
728
|
+
bilinear taps, and that neighbour moving lights it with nothing on the face moving:
|
|
729
|
+
measured over the rim, a fixture face that does not move at all over a breathing
|
|
730
|
+
torso read 1.533 against a bar of 0 (its head never moves, so its resampler error is
|
|
731
|
+
exactly 0), and the proposer's own strand fixture went red on the brows the blink
|
|
732
|
+
drops below their setup boxes. So the region keeps only pixels whose whole
|
|
733
|
+
footprint is the face (228 of the demo's 515 screen-space pixels, 271 of the
|
|
734
|
+
sample's 453), clear of where each feature goes in the head's frame over the idle
|
|
735
|
+
(its setup box carried by its own bone and back through the head's). Measured over
|
|
736
|
+
the rim instead, both examples sat at 1.31 and 1.34 times their calibration, and
|
|
737
|
+
that excess was the bangs' art sliding over the rim.
|
|
738
|
+
- **What the examples show in the head's frame.** The sample is still to its
|
|
739
|
+
calibration (0.884 against 0.883). The demo reads 1.15 times it (1.321 against
|
|
740
|
+
1.146), and most of that is the calibration's own noise, not motion: the face
|
|
741
|
+
rendered alone (`--slot face`), rigid on the head by construction, reads 1.296 on
|
|
742
|
+
the idle's grid against 1.146 on the calibration's — rigc fits each render's
|
|
743
|
+
viewport to its own animation, so the two grids sit at a different sub-pixel phase
|
|
744
|
+
on the art (13 %). The rest, 0.025, is `hair_front`: rendered without it the figure
|
|
745
|
+
is the face-alone one. Both clear the bar, the demo by a factor of 1.74 and the sample by 2.00.
|
|
746
|
+
- **What the line now catches, and where it stops.** The selftest's fixture face,
|
|
747
|
+
a mesh weighted wholly to a bone that slides two rig pixels under its head, reads
|
|
748
|
+
27.886 against a ceiling of 0 on a still head (screen space 26.901, which the
|
|
749
|
+
retired 33.976 passed) and 27.936 against 0.94 on a rolling one. The same plant on
|
|
750
|
+
the real examples, at their render scale of about 0.5 (one rig pixel is half a frame
|
|
751
|
+
pixel): two pixels read 3.216 on the sample (ceiling 1.765, red) and 2.181 on the
|
|
752
|
+
demo (2.292, green); one pixel reads 1.614 and 1.526 (both green). A smooth skin
|
|
753
|
+
interior sliding half a frame pixel changes little, and the rim that shows it most is
|
|
754
|
+
the rim the region leaves out. The line also does not judge a face that moves with
|
|
755
|
+
the bone its own slot rides: that is the head moving, whatever the bone, so a face
|
|
756
|
+
region on the wrong bone is the feet half's and the eye's to catch, not this line's.
|
|
757
|
+
- **The feet stay in screen space.** They ride the root, which the idle does not key,
|
|
758
|
+
so the screen is their own frame. On the demo the long skirt swings over the shoes,
|
|
759
|
+
which lights 1.622.
|
|
760
|
+
|
|
761
|
+
Nothing is left that only an eye answers. "No visible texture stretch" left this list
|
|
762
|
+
with issue #31 (`TEXTURE_STRETCH`), and "the face outline held still in the head's own
|
|
763
|
+
frame" with issue #33 (the face half of `STILL_REGIONS_DARK`, above).
|
|
670
764
|
|
|
671
765
|
## 8. What one character costs
|
|
672
766
|
|
|
@@ -680,3 +774,10 @@ Still only an eye answers, each a missing instrument rather than a question to a
|
|
|
680
774
|
About 6.5 minutes of GPU and under a minute of CPU per character, before any hand
|
|
681
775
|
correction. The GPU figures are one run each on one machine; the painting's figure
|
|
682
776
|
excludes choosing among seeds (the demo was chosen from eighteen candidates).
|
|
777
|
+
Since issue #33 `check` runs one more `rigc build` and one more idle render (the face
|
|
778
|
+
calibration, §7); the idle's `geometry.json` is the one issue #31 already writes. The `build` row
|
|
779
|
+
was not re-measured: A/B against the tree before that change, alternating on one
|
|
780
|
+
machine under a load average of 5 to 11, the difference in user + sys was +5.6 and
|
|
781
|
+
+0.8 s on the demo and +6.5 and −1.6 s on the sample over two rounds — inside what
|
|
782
|
+
the load moves the same build by (the pre-change demo build alone took 43.6 to
|
|
783
|
+
45.8 s there).
|