spine-parts 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -9
- package/cli.ts +52 -19
- package/docs/AUTHORING.md +66 -11
- package/package.json +2 -2
- package/src/build.ts +17 -11
- package/src/check.ts +109 -28
package/README.md
CHANGED
|
@@ -34,17 +34,17 @@ was shifted down into the canvas (its proposal ran 118 px above the top edge;
|
|
|
34
34
|
<code>propose --head-box</code> now makes that shift itself), and <code>hair_back</code>
|
|
35
35
|
is taken from the full run, because the twin tails leave the head crop sideways
|
|
36
36
|
(a plan edit). <code>check</code> printed, verbatim:<br/>
|
|
37
|
-
<code>
|
|
37
|
+
<code>50 assertions: 24 measured (24 passed, 0 failed), 26 skipped, 0 not in profile "spine-html"</code><br/>
|
|
38
38
|
<code>loop: idle 49 frame(s) at 12 fps, f0000 vs f0048 (t = 4s): max |d| 0 (0 required)</code><br/>
|
|
39
39
|
<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/>
|
|
40
|
-
<code>pack: skeleton.png
|
|
40
|
+
<code>pack: skeleton.png 922x1348, 22 region(s), 95.0% covered, padding 2, page edges free, shape polygon</code><br/>
|
|
41
41
|
<code>--seam silhouette</code> repaired the navy blobs the default rule leaves on the white
|
|
42
42
|
blouse (recomposite error pixels 11,050 → 9,540); the default stays
|
|
43
43
|
<code>near-white</code>, the reference implementation's rule, so the examples stay
|
|
44
44
|
comparable with it. The loop is <code>spine-parts loop --palette</code>'s indexed APNG, 48 frames at
|
|
45
|
-
12 fps, 1,706,
|
|
45
|
+
12 fps, 1,706,719 bytes, one 256-entry palette at a measured error of max 57, mean 1.601
|
|
46
46
|
per channel over every frame (the lossless APNG beside it, the exactness record, is
|
|
47
|
-
13,645,
|
|
47
|
+
13,645,503 bytes; the GIF, at the same error, 1,812,288). Frame 29 (2.417 s) is the
|
|
48
48
|
closed eye: the blink holds for 0.084 s, one 12 fps frame rounded up, where the
|
|
49
49
|
reference implementation held 0.04 s and no frame of its loop showed the eyes shut
|
|
50
50
|
(issue #32); the painting is shown at half size, resampled and written
|
|
@@ -86,10 +86,10 @@ page** (issue #2):
|
|
|
86
86
|
|
|
87
87
|
| path under `--out` | what |
|
|
88
88
|
| --- | --- |
|
|
89
|
-
| `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 `spine-html`, which holds every rule `spine` measures (on the demo, rigc
|
|
89
|
+
| `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 --pack-shape polygon` and gated under `spine-html`, which holds every rule `spine` measures (on the demo, rigc 2.1.3: the 14 rules `validate --profile spine` measures are among the build's 24; 16 are not in `spine`) |
|
|
90
90
|
| `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 |
|
|
91
91
|
| `rig/` | `rig.json` and `motion.json` in spine-rigc's spec, `mesh_report.json`, the padded `images/` |
|
|
92
|
-
| `check/` | both gate files verbatim, the idle's frames and their `geometry.json` (skinned vertices per frame), `contact.png`, `motion_heat.png`, `check.json` |
|
|
92
|
+
| `check/` | both gate files verbatim, the idle's frames and their `geometry.json` (skinned vertices per frame), `contact.png`, `motion_heat.png`, `check.json` (with `pack_mode`, the `--page-edges` and `--pack-shape` the page was packed under) |
|
|
93
93
|
| `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 |
|
|
94
94
|
|
|
95
95
|
The last lines of a green build are the pack line, printed beside the Spine example
|
|
@@ -98,15 +98,15 @@ artifact paths — here for `examples/sample`:
|
|
|
98
98
|
|
|
99
99
|
```
|
|
100
100
|
build: PASS — the packed atlas is the artifact; parts/ and rig/ are the intermediates it was made from
|
|
101
|
-
pack: skeleton.png
|
|
101
|
+
pack: skeleton.png 477x1151, 20 region(s), 95.0% covered, padding 2, page edges free, shape polygon; page opaque 54.8% (alpha > 0) — spineboy yardstick 1024x256, 40 region(s), 45.8% opaque (alpha > 0), a reference and not a bar
|
|
102
102
|
out/check/build/skeleton.json
|
|
103
103
|
out/check/build/skeleton.atlas
|
|
104
104
|
out/check/build/skeleton.png
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
The page's edges are free by default: rigc picks the least-area page the parts need,
|
|
108
|
-
not the next power of two. On the two examples that is 967x1338
|
|
109
|
-
power-of-two page is 1024x2048 and 512x2048, and covered goes from 56.3 % to 91.2 % and
|
|
108
|
+
not the next power of two. On the two examples, packed by rectangles, that is 967x1338
|
|
109
|
+
and 479x1166, where a power-of-two page is 1024x2048 and 512x2048, and covered goes from 56.3 % to 91.2 % and
|
|
110
110
|
from 49.7 % to 93.4 %. The cost is the one rigc's help states: *"region attachments
|
|
111
111
|
sampling within 1 LSB of the loose build rather than exactly"*. Measured on both
|
|
112
112
|
examples, every idle frame stays within 1 level of the power-of-two build's. The atlas says
|
|
@@ -115,6 +115,16 @@ consumer that mipmaps or repeats the page; it writes exactly the page builds wro
|
|
|
115
115
|
before the flag existed. The measurements are in
|
|
116
116
|
[AUTHORING §5, *Page edges*](docs/AUTHORING.md#5-after-each-stage-what-to-read).
|
|
117
117
|
|
|
118
|
+
The pack shape is `polygon` by default (spine-rigc 2.1): a region only meshes draw is
|
|
119
|
+
packed by its emitted hull, so a neighbour may sit inside its rectangle where the hull
|
|
120
|
+
is not; a region attachment stays its rectangle. On the two examples that takes the
|
|
121
|
+
free page from 967x1338 to 922x1348 (covered 91.2 % to 95.0 %, 3.9 % less area) and
|
|
122
|
+
from 479x1166 to 477x1151 (93.4 % to 95.0 %, 1.7 % less), at the cost class `free`
|
|
123
|
+
already has: every idle frame within 1 level of the `rect` build's, and no figure in
|
|
124
|
+
`check.json` but `pack_mode` moves. `--pack-shape rect` writes the page and atlas
|
|
125
|
+
0.7.0 wrote. The measurements are in
|
|
126
|
+
[AUTHORING §5, *Pack shape*](docs/AUTHORING.md#5-after-each-stage-what-to-read).
|
|
127
|
+
|
|
118
128
|
The packer is spine-rigc's, and no other: a packed region is a lossless copy, and an
|
|
119
129
|
atlas written by anything else would have no oracle behind it.
|
|
120
130
|
|
package/cli.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { tmpdir } from 'node:os';
|
|
|
17
17
|
import { basename, dirname, extname, join } from 'node:path';
|
|
18
18
|
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
19
19
|
import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
20
|
-
import { DEFAULT_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';
|
|
20
|
+
import { DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
|
|
21
21
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
22
22
|
import { type CharacterConfig, loadConfig, loadEarlyConfig } from './src/config.ts';
|
|
23
23
|
import { PartsError, problemLine } from './src/errors.ts';
|
|
@@ -97,7 +97,7 @@ usage:
|
|
|
97
97
|
printed when one was needed.
|
|
98
98
|
|
|
99
99
|
spine-parts rig --config <config.json> --parts <dir> --out <dir> [--idle-keys ctl|direct]
|
|
100
|
-
[--page-edges pot|free]
|
|
100
|
+
[--page-edges pot|free] [--pack-shape rect|polygon]
|
|
101
101
|
Author the rig: unrotated bones at the config's landmarks (a chain makes
|
|
102
102
|
<chain>0..n), a square lattice mesh over every part in config.meshes
|
|
103
103
|
weighted by distance to its candidate bone segments, a region for every
|
|
@@ -110,8 +110,8 @@ usage:
|
|
|
110
110
|
bone twice, is refused by the loader, CONFIG_BLINK_GROUP_MEMBERS or
|
|
111
111
|
CONFIG_BLINK_GROUP_UNIQUE, before rigc starts. --parts is the
|
|
112
112
|
directory holding parts.json and parts/<name>.png. The result is built
|
|
113
|
-
through spine-rigc (profile spine-html, packed with --page-edges
|
|
114
|
-
check; rigc's build gates the compile and the packed pages on disk) in a
|
|
113
|
+
through spine-rigc (profile spine-html, packed with --page-edges and
|
|
114
|
+
--pack-shape as for check; rigc's build gates the compile and the packed pages on disk) in a
|
|
115
115
|
scratch directory first, and --out receives images/*.png, rig.json,
|
|
116
116
|
motion.json and mesh_report.json only when it is green. Prints one line
|
|
117
117
|
per mesh and the rigc gate lines.
|
|
@@ -123,18 +123,19 @@ usage:
|
|
|
123
123
|
stage prints that SKIP line). The pose is the same to one level of float
|
|
124
124
|
rounding, and so is the per-frame mesh work (AUTHORING §5).
|
|
125
125
|
spine-parts check --rig <dir> --out <dir> [--parts <dir>] [--page-edges pot|free]
|
|
126
|
+
[--pack-shape rect|polygon]
|
|
126
127
|
Build, gate, render and measure a rig through spine-rigc's CLI (the rigc at
|
|
127
128
|
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json
|
|
128
129
|
(with an "idle").
|
|
129
130
|
${PARTS_HOME_SENTENCE}.
|
|
130
131
|
Both are only read. Into --out:
|
|
131
|
-
build/ (rigc build --profile spine-html --pack --page-edges <value
|
|
132
|
-
packed atlas is the artifact), gate_spine-html.txt (the gate lines
|
|
132
|
+
build/ (rigc build --profile spine-html --pack --page-edges <value>
|
|
133
|
+
--pack-shape <value>: the packed atlas is the artifact), gate_spine-html.txt (the gate lines
|
|
133
134
|
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
134
135
|
--geometry: the frames and geometry.json, the skinned vertices and
|
|
135
136
|
every bone's world transform per frame),
|
|
136
137
|
contact.png, motion_heat.png and check.json (with rigc_entry, the rigc
|
|
137
|
-
entry that gated the build). PASS needs every gate summary
|
|
138
|
+
entry that gated the build, and pack_mode, the two pack flags). PASS needs every gate summary
|
|
138
139
|
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
139
140
|
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
|
|
140
141
|
frame at t = duration) at max |d| 0. Then six judgement lines, each in
|
|
@@ -166,15 +167,25 @@ usage:
|
|
|
166
167
|
a bar. Exit 0 on PASS, 1 on FAIL — every FAIL line names the bar, the value
|
|
167
168
|
and the value required.
|
|
168
169
|
--page-edges is handed to rigc verbatim and defaults to ${DEFAULT_PAGE_EDGES}: the
|
|
169
|
-
least-area page the parts need (on the two public examples 967x1338
|
|
170
|
-
479x1166, where pot writes 1024x2048 and 512x2048), at the cost rigc
|
|
170
|
+
least-area page the parts need (on the two public examples, rect, 967x1338
|
|
171
|
+
and 479x1166, where pot writes 1024x2048 and 512x2048), at the cost rigc
|
|
171
172
|
states — region attachments sample within 1 LSB of the loose build
|
|
172
173
|
rather than exactly (measured on both examples: every idle frame within
|
|
173
174
|
1 level of the pot build's; AUTHORING §5, Page edges). pot is a power of two on both edges, for a consumer
|
|
174
175
|
that mipmaps or repeats the page (the atlas declares filter Linear,
|
|
175
|
-
Linear and no repeat). rigc's pack line
|
|
176
|
+
Linear and no repeat). rigc's pack line carries ", page edges free" under
|
|
176
177
|
free; a line that disagrees with the value passed, or a pot page that is
|
|
177
178
|
not a power of two, is refused, CHECK_PACK_PAGE_EDGES.
|
|
179
|
+
--pack-shape is handed to rigc verbatim and defaults to ${DEFAULT_PACK_SHAPE}:
|
|
180
|
+
a region only meshes draw is packed by its emitted hull, so a neighbour
|
|
181
|
+
may sit inside its rectangle where the hull is not (on the two public
|
|
182
|
+
examples, under free, 922x1348 and 477x1151 where rect writes 967x1338
|
|
183
|
+
and 479x1166), at the cost of the same 1-level class as free (measured
|
|
184
|
+
on both examples: every idle frame within 1 level of the rect build's;
|
|
185
|
+
AUTHORING §5, Pack shape). rect keeps every region's cell apart and
|
|
186
|
+
writes the page earlier releases wrote. rigc's pack line ends
|
|
187
|
+
", shape rect" or ", shape polygon"; a line that disagrees with the
|
|
188
|
+
value passed is refused, CHECK_PACK_SHAPE.
|
|
178
189
|
|
|
179
190
|
spine-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
|
|
180
191
|
Encode a frame set rigc render wrote (its --out directory, or the set
|
|
@@ -259,7 +270,8 @@ usage:
|
|
|
259
270
|
|
|
260
271
|
spine-parts build --config <config.json> --source <painting.png> --full <dir|psd>
|
|
261
272
|
--head <dir|psd> --out <dir> [--seam near-white|silhouette]
|
|
262
|
-
[--project core|visible] [--page-edges pot|free]
|
|
273
|
+
[--project core|visible] [--page-edges pot|free]
|
|
274
|
+
[--pack-shape rect|polygon] [--loop]
|
|
263
275
|
assemble, then rig, then check, in one process, each stage's own lines
|
|
264
276
|
printed under [assemble], [rig] and [check]; the first stage that refuses
|
|
265
277
|
stops the build with its own FAIL lines. The config must already carry
|
|
@@ -275,8 +287,9 @@ usage:
|
|
|
275
287
|
three artifact paths — skeleton .json, .atlas and the packed page: the
|
|
276
288
|
packed atlas is the result, the loose parts are the intermediate it was
|
|
277
289
|
made from. --seam defaults to ${DEFAULT_SEAM_RULE}, --project to
|
|
278
|
-
${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES}
|
|
279
|
-
(as for check; both packed builds, rig's gate
|
|
290
|
+
${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES} and
|
|
291
|
+
--pack-shape to ${DEFAULT_PACK_SHAPE} (as for check; both packed builds, rig's gate
|
|
292
|
+
and check's artifact, take them).
|
|
280
293
|
|
|
281
294
|
spine-parts --version
|
|
282
295
|
spine-parts --help
|
|
@@ -407,16 +420,27 @@ function isPageEdges(v: PageEdges | string): v is PageEdges {
|
|
|
407
420
|
return (PAGE_EDGES as readonly string[]).includes(v);
|
|
408
421
|
}
|
|
409
422
|
|
|
423
|
+
/** `--pack-shape`, or its default; a value spine-rigc does not take is a usage error naming the two it does. */
|
|
424
|
+
function packShapeOf(value: string | undefined): PackShape | string {
|
|
425
|
+
const shape = value ?? DEFAULT_PACK_SHAPE;
|
|
426
|
+
return (PACK_SHAPES as readonly string[]).includes(shape) ? (shape as PackShape) : `--pack-shape ${shape}; one of ${PACK_SHAPES.join(', ')} is required`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function isPackShape(v: PackShape | string): v is PackShape {
|
|
430
|
+
return (PACK_SHAPES as readonly string[]).includes(v);
|
|
431
|
+
}
|
|
432
|
+
|
|
410
433
|
function cmdRig(args: string[]): number {
|
|
411
434
|
let config: string | null = null;
|
|
412
435
|
let partsDir: string | null = null;
|
|
413
436
|
let out: string | null = null;
|
|
414
437
|
let idleKeys: string | null = null;
|
|
415
438
|
let pageEdges: string | null = null;
|
|
439
|
+
let packShape: string | null = null;
|
|
416
440
|
for (let i = 0; i < args.length; i++) {
|
|
417
441
|
const flag = args[i];
|
|
418
442
|
const value = args[i + 1];
|
|
419
|
-
if (!['--config', '--parts', '--out', '--idle-keys', '--page-edges'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
443
|
+
if (!['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
420
444
|
if (value === undefined) return usage(`${flag} needs a value`);
|
|
421
445
|
i++;
|
|
422
446
|
if (flag === '--config') config = value;
|
|
@@ -427,6 +451,9 @@ function cmdRig(args: string[]): number {
|
|
|
427
451
|
} else if (flag === '--page-edges') {
|
|
428
452
|
if (pageEdges !== null) return usage('--page-edges is given twice');
|
|
429
453
|
pageEdges = value;
|
|
454
|
+
} else if (flag === '--pack-shape') {
|
|
455
|
+
if (packShape !== null) return usage('--pack-shape is given twice');
|
|
456
|
+
packShape = value;
|
|
430
457
|
} else out = value;
|
|
431
458
|
}
|
|
432
459
|
if (config === null) return usage('rig needs --config <config.json>');
|
|
@@ -436,6 +463,8 @@ function cmdRig(args: string[]): number {
|
|
|
436
463
|
if (!(IDLE_KEYS as readonly string[]).includes(keys)) return usage(`--idle-keys ${keys}; one of ${IDLE_KEYS.join(', ')} is required`);
|
|
437
464
|
const edges = pageEdgesOf(pageEdges ?? undefined);
|
|
438
465
|
if (!isPageEdges(edges)) return usage(edges);
|
|
466
|
+
const shape = packShapeOf(packShape ?? undefined);
|
|
467
|
+
if (!isPackShape(shape)) return usage(shape);
|
|
439
468
|
let bin: string;
|
|
440
469
|
try {
|
|
441
470
|
bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
@@ -444,7 +473,7 @@ function cmdRig(args: string[]): number {
|
|
|
444
473
|
}
|
|
445
474
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-rig-'));
|
|
446
475
|
try {
|
|
447
|
-
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys, pageEdges: edges }, rigcRunner(bin), scratch, console.log);
|
|
476
|
+
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys, pageEdges: edges, packShape: shape }, rigcRunner(bin), scratch, console.log);
|
|
448
477
|
return EXIT_OK;
|
|
449
478
|
} catch (err) {
|
|
450
479
|
return printRefusal(err);
|
|
@@ -486,15 +515,17 @@ function flags(args: string[], known: readonly string[], command: string, option
|
|
|
486
515
|
}
|
|
487
516
|
|
|
488
517
|
function cmdCheck(args: string[]): number {
|
|
489
|
-
const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--page-edges']);
|
|
518
|
+
const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--page-edges', '--pack-shape']);
|
|
490
519
|
if (typeof f === 'string') return usage(f);
|
|
491
520
|
const rig = f.get('--rig') as string;
|
|
492
521
|
const out = f.get('--out') as string;
|
|
493
522
|
const edges = pageEdgesOf(f.get('--page-edges'));
|
|
494
523
|
if (!isPageEdges(edges)) return usage(edges);
|
|
524
|
+
const shape = packShapeOf(f.get('--pack-shape'));
|
|
525
|
+
if (!isPackShape(shape)) return usage(shape);
|
|
495
526
|
try {
|
|
496
527
|
const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
497
|
-
const r = checkStage({ rig, parts: f.get('--parts') ?? rig, out, pageEdges: edges }, rigcRunner(bin), bin, console.log);
|
|
528
|
+
const r = checkStage({ rig, parts: f.get('--parts') ?? rig, out, pageEdges: edges, packShape: shape }, rigcRunner(bin), bin, console.log);
|
|
498
529
|
return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
|
|
499
530
|
} catch (err) {
|
|
500
531
|
return printRefusal(err);
|
|
@@ -799,7 +830,7 @@ function cmdInputs(args: string[]): number {
|
|
|
799
830
|
function cmdBuild(args: string[]): number {
|
|
800
831
|
const flags = new Map<string, string>();
|
|
801
832
|
let loop = false;
|
|
802
|
-
const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges'];
|
|
833
|
+
const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape'];
|
|
803
834
|
for (let i = 0; i < args.length; i++) {
|
|
804
835
|
const flag = args[i];
|
|
805
836
|
if (flag === '--loop') {
|
|
@@ -821,6 +852,8 @@ function cmdBuild(args: string[]): number {
|
|
|
821
852
|
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
822
853
|
const edges = pageEdgesOf(flags.get('--page-edges'));
|
|
823
854
|
if (!isPageEdges(edges)) return usage(edges);
|
|
855
|
+
const shape = packShapeOf(flags.get('--pack-shape'));
|
|
856
|
+
if (!isPackShape(shape)) return usage(shape);
|
|
824
857
|
const [config, source, full, head, out] = ['--config', '--source', '--full', '--head', '--out'].map((f) => flags.get(f) as string);
|
|
825
858
|
let bin: string;
|
|
826
859
|
try {
|
|
@@ -831,7 +864,7 @@ function cmdBuild(args: string[]): number {
|
|
|
831
864
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-build-'));
|
|
832
865
|
try {
|
|
833
866
|
const r = build(
|
|
834
|
-
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges },
|
|
867
|
+
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges, packShape: shape },
|
|
835
868
|
{ rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
|
|
836
869
|
console.log,
|
|
837
870
|
);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -279,11 +279,12 @@ entry, no dithering, filter None) — and is the small file to show. `idle.gif`
|
|
|
279
279
|
(`loop --out x.gif`) is the same median cut in a GIF. The indexed APNG and the GIF
|
|
280
280
|
print their palette error, per channel over R, G and B of every frame (and alpha's
|
|
281
281
|
max for the APNG); a figure is a measurement of the file, not a bar. On the demo:
|
|
282
|
-
13,645,
|
|
282
|
+
13,645,503 B lossless, 1,706,719 B indexed and 1,812,288 B GIF, both palette files at
|
|
283
283
|
max 57, mean 1.601 — the two share one quantiser, so their error is the same by
|
|
284
284
|
construction and the size is the difference. That is `--seam silhouette` (the README's
|
|
285
|
-
animation); with `--
|
|
286
|
-
1,812,
|
|
285
|
+
animation, spine-rigc 2.1.3); with `--pack-shape rect` the same build writes 13,645,350,
|
|
286
|
+
1,706,520 and 1,812,238 B, and with `--page-edges pot --pack-shape rect` 13,645,519,
|
|
287
|
+
1,706,785 and 1,812,625 B, all at the same error. The loop shows the blink closed: the
|
|
287
288
|
eyes' hold is at least one 12 fps frame (below), so whatever `motion.blink.t` is, one
|
|
288
289
|
idle frame lands inside it, and `check.json`'s `BLINK_NO_HOLE.idle_frames_closed`
|
|
289
290
|
names it — `[29]` on both examples.
|
|
@@ -300,10 +301,11 @@ the reference's so the examples stay comparable with it.
|
|
|
300
301
|
--page-edges <v>`, and hand the value to rigc verbatim. The default is `free`,
|
|
301
302
|
the least-area page the parts need. `--page-edges pot` is the opt-out: a power of
|
|
302
303
|
two on both edges, which is the page every build wrote before this flag. The pack
|
|
303
|
-
line says which one ran, because rigc
|
|
304
|
-
Measured on the two examples by `tools/atlas_population.ts` with rigc 1.5.1, and
|
|
305
|
-
with rigc 2.0.3
|
|
306
|
-
|
|
304
|
+
line says which one ran, because rigc writes `, page edges free` after the padding under
|
|
305
|
+
`free`. Measured on the two examples by `tools/atlas_population.ts` with rigc 1.5.1, and
|
|
306
|
+
again with rigc 2.0.3 and with rigc 2.1.3 under `--pack-shape rect` (the frames table
|
|
307
|
+
by the same idle render), which reproduced every figure in both tables and the one
|
|
308
|
+
check figure below:
|
|
307
309
|
|
|
308
310
|
| build | page | covered | page opaque | page / figure |
|
|
309
311
|
| --- | --- | --- | --- | --- |
|
|
@@ -328,6 +330,56 @@ barred figure and every status is the same under both. The atlas rigc writes say
|
|
|
328
330
|
`filter: Linear, Linear` and has no `repeat` line, so choose `pot` for a consumer
|
|
329
331
|
that mipmaps or repeats the page.
|
|
330
332
|
|
|
333
|
+
**Pack shape.** `rig`, `check` and `build` also hand rigc `--pack-shape <v>`
|
|
334
|
+
verbatim (spine-rigc 2.1.0 and later). `rect` keeps every region's cell apart, the
|
|
335
|
+
only packing before 2.1. `polygon`, the default here (rigc's own is `rect`), packs a
|
|
336
|
+
region that only meshes draw by its emitted hull, so a neighbour may sit inside its
|
|
337
|
+
rectangle where the hull is not, with the padding kept between footprints; a region
|
|
338
|
+
attachment stays its rectangle. rigc gates the result on the packed pages with
|
|
339
|
+
`A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` (SKIP on the compile pass, where every part has
|
|
340
|
+
its own page; PASS on the packed pass, both shapes, both examples). The pack line ends
|
|
341
|
+
`, shape rect` or `, shape polygon` under either, and `check.json`'s `pack_mode` records
|
|
342
|
+
both flags. Measured on the two examples with spine-rigc 2.1.3 (the full entry,
|
|
343
|
+
spine-core 4.3.13 beside it), `spine-parts build` with `--page-edges free` and each
|
|
344
|
+
shape, the table columns by `tools/atlas_population.ts`:
|
|
345
|
+
|
|
346
|
+
| build | page | page area | covered | page opaque | page / figure |
|
|
347
|
+
| --- | --- | --- | --- | --- | --- |
|
|
348
|
+
| demo, `rect` | 967x1338 | 1,293,846 | 91.2 % | 59.7 % | 1.636 |
|
|
349
|
+
| demo, `polygon` | 922x1348 | 1,242,856 (−3.9 %) | 95.0 % | 62.1 % | 1.571 |
|
|
350
|
+
| sample, `rect` | 479x1166 | 558,514 | 93.4 % | 53.9 % | 1.755 |
|
|
351
|
+
| sample, `polygon` | 477x1151 | 549,027 (−1.7 %) | 95.0 % | 54.8 % | 1.725 |
|
|
352
|
+
|
|
353
|
+
Covered is Σ region rectangles over page area, as rigc prints it; under `polygon` the
|
|
354
|
+
rectangles may overlap where a hull leaves room, so it is not a union and can exceed
|
|
355
|
+
100 %. Page opaque is the measure of what the page holds. Under `--page-edges pot` the
|
|
356
|
+
shape changes nothing on these two examples: both shapes write 1024x2048 and 512x2048,
|
|
357
|
+
and the idle frames are identical.
|
|
358
|
+
|
|
359
|
+
The cost, from `rect` to `polygon` (the idle `check` renders, `rigc render --animation
|
|
360
|
+
idle --fps 12 --max 640`, compared frame by frame, max over R, G, B and A):
|
|
361
|
+
|
|
362
|
+
| example | idle frames that differ | pixels that differ (all 49 frames) | max \|d\| |
|
|
363
|
+
| --- | --- | --- | --- |
|
|
364
|
+
| demo | 49 of 49 | 2,594 | 1 |
|
|
365
|
+
| sample | 49 of 49 | 740 | 1 |
|
|
366
|
+
|
|
367
|
+
That is the class `free` already costs (the table above, measured by the same
|
|
368
|
+
comparison), a little larger. No figure in `check.json` moved but `pack_mode`: every
|
|
369
|
+
bar, every status and every reported figure is the same under both shapes.
|
|
370
|
+
`--pack-shape rect` gives back the page and atlas 0.7.0 wrote: on both examples
|
|
371
|
+
they are byte-identical to rigc 2.0.3's `--page-edges free` build of the same rig (the
|
|
372
|
+
skeleton JSON differs only in its relative `images` path), and selftest `CK53` holds
|
|
373
|
+
`rect` to rigc's own rectangle page.
|
|
374
|
+
|
|
375
|
+
The hull ceiling (spine-parts #58: hull area over rectangle area, 0.812 on demo and
|
|
376
|
+
0.814 on sample) is not what the packer realises here: the hollows inside the meshes'
|
|
377
|
+
rectangles are worth only what fits into them. On the polygon pages, read off the
|
|
378
|
+
atlas, demo's packed rectangles overlap in 21 pairs (17 a region attachment inside a
|
|
379
|
+
mesh's rectangle, 4 a mesh inside another's) and sample's in 12 (10 and 2); no two
|
|
380
|
+
region attachments overlap. More would take tighter hulls, meshes in place of
|
|
381
|
+
regions, or a stronger nesting search in rigc, each its own card.
|
|
382
|
+
|
|
331
383
|
`--project core` (the default) is the reference's projection rule: a layer takes the
|
|
332
384
|
painting's pixel only inside its top-most `alpha >= 250` area eroded by a 5x5 square,
|
|
333
385
|
so a part a few pixels wide (a lash, a brow, an iris) takes none. `--project visible`
|
|
@@ -590,7 +642,7 @@ See-through with another seed.
|
|
|
590
642
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
591
643
|
| `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/` |
|
|
592
644
|
| `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) |
|
|
593
|
-
| `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^2.
|
|
645
|
+
| `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^2.1.3), or put a newer `rigc` first on `PATH` |
|
|
594
646
|
| `CHECK_RIGC_ENTRY`, `CHECK_RIGC_ENTRY_READS` | `rigc --version` names no entry (a rigc below 2.0.0), or names one in neither launcher form (`entry: cli.ts — @esotericsoftware/spine-core <v> present`, `entry: cli_core.ts — @esotericsoftware/spine-core absent — …`); `check.json`'s `rigc_entry` records the one that gated the build | `bun install` |
|
|
595
647
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
596
648
|
| `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 |
|
|
@@ -604,8 +656,9 @@ See-through with another seed.
|
|
|
604
656
|
| `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 |
|
|
605
657
|
| `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 |
|
|
606
658
|
| `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 |
|
|
607
|
-
| `CHECK_PACK_LINE_READS` | rigc printed a `pack:` line that is not `pack: <page> <W>x<H>, <N> region(s), <P>% covered, padding <D
|
|
608
|
-
| `CHECK_PACK_PAGE_EDGES` | the pack line disagrees with the `--page-edges` the build was run with — its `, page edges free`
|
|
659
|
+
| `CHECK_PACK_LINE_READS` | rigc printed a `pack:` line that is not `pack: <page> <W>x<H>, <N> region(s), <P>% covered, padding <D>`, then `, page edges free` or nothing, then `, shape rect`, `, shape polygon` or nothing (spine-rigc 2.1 prints the shape; 1.5–2.0 printed none, read as `rect`); an unknown shape is this refusal; the line is quoted | a rigc this package does not know the output of; report it, with `rigc --version` |
|
|
660
|
+
| `CHECK_PACK_PAGE_EDGES` | the pack line disagrees with the `--page-edges` the build was run with — its `, page edges free` clause 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` |
|
|
661
|
+
| `CHECK_PACK_SHAPE` | the pack line disagrees with the `--pack-shape` the build was run with — it ends `, shape rect` under `polygon` or `, shape polygon` under `rect`, or it has no shape clause (the 1.5–2.0 form, read as `rect`) under `polygon` | a rigc older than 2.1.0 on `PATH`: rigc 2.0.3 takes `--pack-shape` without a word, packs by rectangles and prints the form with no shape clause, so this refusal is what catches it — `bun install` (this package depends on spine-rigc ^2.1.3); otherwise a rigc problem, report it with `rigc --version` |
|
|
609
662
|
| `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
|
|
610
663
|
|
|
611
664
|
## 7. The bars `check` enforces, and what only an eye answers today
|
|
@@ -617,7 +670,9 @@ implementation's bars):
|
|
|
617
670
|
under the `spine-html` profile, gated once over the compile and once over the packed
|
|
618
671
|
pages on disk. There is no `spine` gate of its own: `spine-html` holds every rule
|
|
619
672
|
`spine` measures (on the demo, rigc 1.5.1 and 2.0.3: `validate --profile spine`'s 14
|
|
620
|
-
measured rules are all among the build's 23, the 15 others not in `spine`;
|
|
673
|
+
measured rules are all among the build's 23, the 15 others not in `spine`; rigc
|
|
674
|
+
2.1.3: the same 14 among the build's 24, which adds
|
|
675
|
+
`A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` on the packed pass, 16 not in `spine`; `CH09`
|
|
621
676
|
holds it on every fetched example);
|
|
622
677
|
- **seam**: the setup-pose render against the flat composite of `parts/`: mean
|
|
623
678
|
max-channel |d| ≤ 1.0 of 255, and at most 50 pixels over 40;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-parts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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": "^2.
|
|
60
|
+
"spine-rigc": "^2.1.3"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@esotericsoftware/spine-core": "4.3.13",
|
package/src/build.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:
|
|
|
44
44
|
import { dirname, join } from 'node:path';
|
|
45
45
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
46
46
|
import { assemble, type AssembleResult, figuresLine, holeLines, type ProjectRule, type SeamRule, stageFields } from './assemble.ts';
|
|
47
|
-
import { causeLines, type CheckReport, type PackLine, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PageEdges, readFrameSet, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK } from './check.ts';
|
|
47
|
+
import { causeLines, type CheckReport, type PackLine, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, JUDGEMENT_LINES, type JudgementLine, packedBuildArgs, packedBuildLabel, type PackMode, type PackShape, type PageEdges, readFrameSet, REPORTED_LINES, type ReportedLine, type RigcRunner, runCheck, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SEAM_PX_LEVEL_HIGH, SPINEBOY_YARDSTICK } from './check.ts';
|
|
48
48
|
import { loadConfig, loadEarlyConfig } from './config.ts';
|
|
49
49
|
import { PartsError, type Problem, problemLine } from './errors.ts';
|
|
50
50
|
import { encodeGif } from './gif.ts';
|
|
@@ -192,21 +192,21 @@ function gateRun(label: string, rigc: RigcRunner, args: string[]): GateRun {
|
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
194
|
* spine-rigc's gate over the rig in a scratch directory: `build` under the
|
|
195
|
-
* spine-html profile with `--pack --page-edges <edges>`, which runs the gate
|
|
195
|
+
* spine-html profile with `--pack --page-edges <edges> --pack-shape <shape>`, which runs the gate
|
|
196
196
|
* once over the compile and once over the packed pages on disk. There is no
|
|
197
197
|
* second `validate --profile spine` run: spine-html holds every rule spine
|
|
198
198
|
* measures (selftest `CH09`). The files are staged exactly as `--out` will
|
|
199
199
|
* receive them, so what passed is what is written. The scratch directory is
|
|
200
200
|
* the caller's, and is emptied here before and after.
|
|
201
201
|
*/
|
|
202
|
-
function gateThroughRigc(out: RigOutput, texts: Array<[string, string]>, rigc: RigcRunner, scratch: string,
|
|
202
|
+
function gateThroughRigc(out: RigOutput, texts: Array<[string, string]>, rigc: RigcRunner, scratch: string, mode: PackMode): GateRun[] {
|
|
203
203
|
rmSync(scratch, { recursive: true, force: true });
|
|
204
204
|
try {
|
|
205
205
|
mkdirSync(join(scratch, 'images'), { recursive: true });
|
|
206
206
|
for (const [file, img] of out.images) writeFileSync(join(scratch, 'images', file), encodePngBytes(img));
|
|
207
207
|
for (const [file, text] of texts) writeFileSync(join(scratch, file), text);
|
|
208
208
|
const build = join(scratch, 'build');
|
|
209
|
-
return [gateRun(packedBuildLabel(
|
|
209
|
+
return [gateRun(packedBuildLabel(mode), rigc, ['build', '--rig', join(scratch, 'rig.json'), '--motion', join(scratch, 'motion.json'), '--out', build, ...packedBuildArgs(mode)])];
|
|
210
210
|
} finally {
|
|
211
211
|
rmSync(scratch, { recursive: true, force: true });
|
|
212
212
|
}
|
|
@@ -221,6 +221,8 @@ export interface RigStageInput {
|
|
|
221
221
|
idleKeys?: IdleKeys;
|
|
222
222
|
/** The gate build's `--page-edges`; {@link DEFAULT_PAGE_EDGES} when absent. The pack is scratch here; the gate is what it is for. */
|
|
223
223
|
pageEdges?: PageEdges;
|
|
224
|
+
/** The gate build's `--pack-shape`; {@link DEFAULT_PACK_SHAPE} when absent. */
|
|
225
|
+
packShape?: PackShape;
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
/** Author the rig, gate it through rigc in `scratch`, and write `out` only when the gate is green. */
|
|
@@ -258,7 +260,7 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
258
260
|
? ` idle keys ctl: ${rig.meshKeyed.length} mesh-driving bone(s) keyed by the idle, each keyed through a same-origin <bone>_ctl parent`
|
|
259
261
|
: ` idle keys direct: ${rig.meshKeyed.length} mesh-driving bone(s) keyed in place, ${rig.rig.invariants === undefined ? 'so no invariants.idleDrivesMeshes is declared (it would switch nothing off)' : 'invariants.idleDrivesMeshes declared'}`,
|
|
260
262
|
);
|
|
261
|
-
const gate = gateThroughRigc(rig, texts, rigc, scratch, input.pageEdges ?? DEFAULT_PAGE_EDGES);
|
|
263
|
+
const gate = gateThroughRigc(rig, texts, rigc, scratch, { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE });
|
|
262
264
|
for (const g of gate) {
|
|
263
265
|
log(` rigc ${g.label}: exit ${g.status}`);
|
|
264
266
|
for (const l of g.lines) log(` ${l.trim()}`);
|
|
@@ -291,6 +293,8 @@ export interface CheckStageInput {
|
|
|
291
293
|
out: string;
|
|
292
294
|
/** The packed build's `--page-edges`; {@link DEFAULT_PAGE_EDGES} when absent. */
|
|
293
295
|
pageEdges?: PageEdges;
|
|
296
|
+
/** The packed build's `--pack-shape`; {@link DEFAULT_PACK_SHAPE} when absent. */
|
|
297
|
+
packShape?: PackShape;
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
/** The pack line as it is printed: rigc's own, then the page's opaque share beside the spineboy yardstick. */
|
|
@@ -328,9 +332,9 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
328
332
|
const versionLines = v.out.trim().split('\n');
|
|
329
333
|
const entryLine = versionLines.find((l) => l.startsWith('entry:'));
|
|
330
334
|
log(` rigc ${versionLines[0]} at ${bin}${entryLine === undefined ? '' : `; ${entryLine}`}`);
|
|
331
|
-
const
|
|
332
|
-
const r = runCheck(input.rig, input.out, rigc, input.parts,
|
|
333
|
-
log(` gate spine-html (rigc ${packedBuildLabel(
|
|
335
|
+
const mode: PackMode = { pageEdges: input.pageEdges ?? DEFAULT_PAGE_EDGES, packShape: input.packShape ?? DEFAULT_PACK_SHAPE };
|
|
336
|
+
const r = runCheck(input.rig, input.out, rigc, input.parts, mode);
|
|
337
|
+
log(` gate spine-html (rigc ${packedBuildLabel(mode)}), verbatim:`);
|
|
334
338
|
for (const l of r.gateHtml) log(l);
|
|
335
339
|
for (const l of packLines(r)) log(` ${l}`);
|
|
336
340
|
const fig = r.figures;
|
|
@@ -441,6 +445,8 @@ export interface BuildInput {
|
|
|
441
445
|
loop: boolean;
|
|
442
446
|
/** `--page-edges` for both packed builds, the rig stage's gate and the check's artifact. */
|
|
443
447
|
pageEdges: PageEdges;
|
|
448
|
+
/** `--pack-shape` for both packed builds, as `pageEdges`; {@link DEFAULT_PACK_SHAPE} when absent. */
|
|
449
|
+
packShape?: PackShape;
|
|
444
450
|
}
|
|
445
451
|
|
|
446
452
|
/**
|
|
@@ -514,7 +520,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
514
520
|
const out = input.out;
|
|
515
521
|
mkdirSync(out, { recursive: true });
|
|
516
522
|
for (const p of BUILD_OWNS) rmSync(join(out, p), { recursive: true, force: true });
|
|
517
|
-
log(`spine-parts build: ${input.config} -> ${out}, seam rule ${input.seam}, projection rule ${input.project}, page edges ${input.pageEdges}${input.loop ? ', with the idle loop' : ''}`);
|
|
523
|
+
log(`spine-parts build: ${input.config} -> ${out}, seam rule ${input.seam}, projection rule ${input.project}, page edges ${input.pageEdges}, pack shape ${input.packShape ?? DEFAULT_PACK_SHAPE}${input.loop ? ', with the idle loop' : ''}`);
|
|
518
524
|
|
|
519
525
|
try {
|
|
520
526
|
// build runs rig next, which needs the whole config, so the full loader is
|
|
@@ -532,14 +538,14 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
532
538
|
}
|
|
533
539
|
|
|
534
540
|
try {
|
|
535
|
-
rigStage({ config: input.config, parts: out, out: join(out, 'rig'), pageEdges: input.pageEdges }, run.rig, run.scratch, prefixed('rig'));
|
|
541
|
+
rigStage({ config: input.config, parts: out, out: join(out, 'rig'), pageEdges: input.pageEdges, packShape: input.packShape }, run.rig, run.scratch, prefixed('rig'));
|
|
536
542
|
} catch (err) {
|
|
537
543
|
return refused('rig', err);
|
|
538
544
|
}
|
|
539
545
|
|
|
540
546
|
let report: CheckReport;
|
|
541
547
|
try {
|
|
542
|
-
report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges }, run.check, run.checkBin, prefixed('check'));
|
|
548
|
+
report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges, packShape: input.packShape }, run.check, run.checkBin, prefixed('check'));
|
|
543
549
|
} catch (err) {
|
|
544
550
|
return refused('check', err);
|
|
545
551
|
}
|
package/src/check.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*
|
|
26
26
|
* | output | what |
|
|
27
27
|
* | --- | --- |
|
|
28
|
-
* | `build/` | `rigc build --profile spine-html --pack --page-edges free` (or `pot`, when the caller names
|
|
28
|
+
* | `build/` | `rigc build --profile spine-html --pack --page-edges free --pack-shape polygon` (or `pot`, `rect`, when the caller names them; {@link PAGE_EDGES}, {@link PACK_SHAPES}) — the packed atlas is the final artifact |
|
|
29
29
|
* | `gate_spine-html.txt` | that build's gate lines, verbatim — the one gate: rigc's `build` runs it once over the compile and once over the packed pages on disk, and `spine-html` holds every rule `spine` measures (selftest `CH09`) |
|
|
30
30
|
* | `idle_frames/` | `rigc render --animation idle --fps 12 --max 640 --geometry`: `frames.json` + `idle/f*.png` + `idle/geometry.json` (each mesh's skinned vertices, which `TEXTURE_STRETCH` reads, and every bone's world transform, which the face half of `STILL_REGIONS_DARK` reads) |
|
|
31
31
|
* | `contact.png` | rigc's own contact sheet of that render, copied out |
|
|
@@ -196,7 +196,7 @@ export const SPINEBOY_YARDSTICK = '1024x256, 40 region(s), 45.8% opaque (alpha >
|
|
|
196
196
|
* `free` is the default here although rigc's own default is `pot`: the packed
|
|
197
197
|
* page is this package's final artifact, and on the two public examples `free`
|
|
198
198
|
* takes the page from 1024x2048 to 967x1338 (covered 56.3 % -> 91.2 %) and
|
|
199
|
-
* from 512x2048 to 479x1166 (49.7 % -> 93.4 %), rigc 1.5.1 and 2.
|
|
199
|
+
* from 512x2048 to 479x1166 (49.7 % -> 93.4 %), rigc 1.5.1, 2.0.3 and 2.1.3 (`rect`) alike. The cost was
|
|
200
200
|
* measured on the same two builds. Each of the 49 idle frames differs from the
|
|
201
201
|
* `pot` build's, by at most 1 level in any channel (demo 2,373 px, sample 651
|
|
202
202
|
* px over all 49). On sample that moves STILL_REGIONS_DARK's reported
|
|
@@ -209,14 +209,53 @@ export const PAGE_EDGES = ['pot', 'free'] as const;
|
|
|
209
209
|
export type PageEdges = (typeof PAGE_EDGES)[number];
|
|
210
210
|
export const DEFAULT_PAGE_EDGES: PageEdges = 'free';
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* What two packed rectangles may share: spine-rigc's `build --pack
|
|
214
|
+
* --pack-shape`, handed to it verbatim (in spine-rigc's CLI since 2.1.0; this
|
|
215
|
+
* package's range starts at 2.1.3). `rect` keeps every region's cell apart;
|
|
216
|
+
* `polygon` packs a region that only meshes draw by its emitted hull, so a
|
|
217
|
+
* neighbour may sit inside its rectangle where the hull is not, with the
|
|
218
|
+
* padding kept between footprints — a region attachment stays its rectangle
|
|
219
|
+
* (rigc's help).
|
|
220
|
+
*
|
|
221
|
+
* `polygon` is the default here although rigc's own default is `rect`, for the
|
|
222
|
+
* reason {@link DEFAULT_PAGE_EDGES} is `free`: the packed page is this
|
|
223
|
+
* package's final artifact, rigc gates the footprints on the pages on disk
|
|
224
|
+
* (`A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP`), and the cost is the class already
|
|
225
|
+
* accepted for `free`. Measured with spine-rigc 2.1.3 (the full entry,
|
|
226
|
+
* spine-core 4.3.13 beside it), `spine-parts build` on the two public examples
|
|
227
|
+
* under `--page-edges free`: `polygon` takes demo's page from 967x1338 (91.2 %
|
|
228
|
+
* covered) to 922x1348 (95.0 %), 3.9 % less area, and sample's from 479x1166
|
|
229
|
+
* (93.4 %) to 477x1151 (95.0 %), 1.7 % less. Each of the 49 idle frames at 12
|
|
230
|
+
* fps differs from the `rect` build's by at most 1 level in any channel (demo
|
|
231
|
+
* 2,594 px, sample 740 px over all 49), and no figure in `check.json` but
|
|
232
|
+
* `pack_mode` moves. Under `--page-edges pot` both shapes give the same page
|
|
233
|
+
* size and the same frames. AUTHORING §5, *Pack shape*, holds the tables.
|
|
234
|
+
* `--pack-shape rect` writes the page and atlas rigc 2.0.3 wrote for 0.7.0
|
|
235
|
+
* (measured byte for byte on both examples; selftest `CK53` holds rect to
|
|
236
|
+
* rigc's own rect page).
|
|
237
|
+
*/
|
|
238
|
+
export const PACK_SHAPES = ['rect', 'polygon'] as const;
|
|
239
|
+
export type PackShape = (typeof PACK_SHAPES)[number];
|
|
240
|
+
export const DEFAULT_PACK_SHAPE: PackShape = 'polygon';
|
|
241
|
+
|
|
242
|
+
/** How a packed build packs: its `--page-edges` and its `--pack-shape`, each handed to rigc verbatim. */
|
|
243
|
+
export interface PackMode {
|
|
244
|
+
pageEdges: PageEdges;
|
|
245
|
+
packShape: PackShape;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Both defaults, {@link DEFAULT_PAGE_EDGES} and {@link DEFAULT_PACK_SHAPE}. */
|
|
249
|
+
export const DEFAULT_PACK_MODE: PackMode = { pageEdges: DEFAULT_PAGE_EDGES, packShape: DEFAULT_PACK_SHAPE };
|
|
250
|
+
|
|
212
251
|
/** The packed build's rigc arguments after `--out <dir>`, as every stage that packs passes them. */
|
|
213
|
-
export function packedBuildArgs(
|
|
214
|
-
return ['--profile', 'spine-html', '--pack', '--page-edges',
|
|
252
|
+
export function packedBuildArgs(mode: PackMode): string[] {
|
|
253
|
+
return ['--profile', 'spine-html', '--pack', '--page-edges', mode.pageEdges, '--pack-shape', mode.packShape];
|
|
215
254
|
}
|
|
216
255
|
|
|
217
256
|
/** The packed build as a label: the rigc command line that was run, after `rigc`. */
|
|
218
|
-
export function packedBuildLabel(
|
|
219
|
-
return `build ${packedBuildArgs(
|
|
257
|
+
export function packedBuildLabel(mode: PackMode): string {
|
|
258
|
+
return `build ${packedBuildArgs(mode).join(' ')}`;
|
|
220
259
|
}
|
|
221
260
|
|
|
222
261
|
// ---------------------------------------------------------------------------
|
|
@@ -553,19 +592,32 @@ export interface PackLine {
|
|
|
553
592
|
regions: number;
|
|
554
593
|
coveredPct: number;
|
|
555
594
|
padding: number;
|
|
556
|
-
/** `free` when the line
|
|
595
|
+
/** `free` when the line carries `, page edges free` after the padding, `pot` when it does not — spine-rigc prints the clause exactly when it packed under `--page-edges free` (its `src/cli/shared.ts`, the pack line). */
|
|
557
596
|
pageEdges: PageEdges;
|
|
597
|
+
/**
|
|
598
|
+
* The line's last clause, `, shape rect` or `, shape polygon`: spine-rigc
|
|
599
|
+
* 2.1 names the `--pack-shape` it packed under on every pack line, its
|
|
600
|
+
* default included. A line with no shape clause is the 1.5.1–2.0.3 form,
|
|
601
|
+
* from a rigc that had no other shape, and reads as `rect`.
|
|
602
|
+
*/
|
|
603
|
+
packShape: PackShape;
|
|
558
604
|
}
|
|
559
605
|
|
|
560
606
|
/** rigc's pack line, whole: the line is this and nothing else, or it is refused. */
|
|
561
|
-
const PACK = /^pack: (\S+) (\d+)x(\d+), (\d+) region\(s\), (\d+(?:\.\d+)?)% covered, padding (\d+)(, page edges free)?$/;
|
|
607
|
+
const PACK = /^pack: (\S+) (\d+)x(\d+), (\d+) region\(s\), (\d+(?:\.\d+)?)% covered, padding (\d+)(, page edges free)?(?:, shape (rect|polygon))?$/;
|
|
608
|
+
|
|
609
|
+
/** A pack line that names its shape: the spine-rigc 2.1 form. */
|
|
610
|
+
const PACK_SHAPE_CLAUSE = /, shape (?:rect|polygon)$/;
|
|
562
611
|
|
|
563
612
|
/**
|
|
564
613
|
* Every pack line among the gate lines — a line that reads `pack:` once its
|
|
565
|
-
* `..` gutter is taken off.
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
614
|
+
* `..` gutter is taken off. Two forms are read, each by name: spine-rigc
|
|
615
|
+
* 2.1's, which ends `, shape rect` or `, shape polygon`, and the 1.5.1–2.0.3
|
|
616
|
+
* form, which has no shape clause and reads as `rect`; in both,
|
|
617
|
+
* `, page edges free` sits between the padding and the shape or is absent. Any
|
|
618
|
+
* other line (an unknown shape, an unknown suffix, a missing field, the clauses
|
|
619
|
+
* out of order) is refused, `CHECK_PACK_LINE_READS`, naming the line and the
|
|
620
|
+
* form required: a line half-read would lose the field that changed.
|
|
569
621
|
*/
|
|
570
622
|
export function parsePackLines(lines: readonly string[]): PackLine[] {
|
|
571
623
|
const out: PackLine[] = [];
|
|
@@ -578,11 +630,21 @@ export function parsePackLines(lines: readonly string[]): PackLine[] {
|
|
|
578
630
|
problems.push({
|
|
579
631
|
code: 'CHECK_PACK_LINE_READS',
|
|
580
632
|
object: `rigc's pack line ${JSON.stringify(text)}`,
|
|
581
|
-
detail: 'does not read as "pack: <page> <W>x<H>, <N> region(s), <P>% covered, padding <D>",
|
|
633
|
+
detail: 'does not read as "pack: <page> <W>x<H>, <N> region(s), <P>% covered, padding <D>", then ", page edges free" or nothing, then ", shape rect", ", shape polygon" or nothing; that form is required',
|
|
582
634
|
});
|
|
583
635
|
continue;
|
|
584
636
|
}
|
|
585
|
-
out.push({
|
|
637
|
+
out.push({
|
|
638
|
+
line: text,
|
|
639
|
+
page: m[1],
|
|
640
|
+
width: Number(m[2]),
|
|
641
|
+
height: Number(m[3]),
|
|
642
|
+
regions: Number(m[4]),
|
|
643
|
+
coveredPct: Number(m[5]),
|
|
644
|
+
padding: Number(m[6]),
|
|
645
|
+
pageEdges: m[7] === undefined ? 'pot' : 'free',
|
|
646
|
+
packShape: m[8] === 'polygon' ? 'polygon' : 'rect',
|
|
647
|
+
});
|
|
586
648
|
}
|
|
587
649
|
refuseIfAny(problems);
|
|
588
650
|
return out;
|
|
@@ -593,12 +655,20 @@ function powerOfTwo(n: number): boolean {
|
|
|
593
655
|
}
|
|
594
656
|
|
|
595
657
|
/**
|
|
596
|
-
* Where a pack line disagrees with the
|
|
597
|
-
*
|
|
598
|
-
* two on both edges, as rigc's help defines it
|
|
599
|
-
* `CHECK_PACK_PAGE_EDGES
|
|
658
|
+
* Where a pack line disagrees with the pack mode the build was given. The
|
|
659
|
+
* line's own page-edges clause must say the `--page-edges` passed, and a `pot`
|
|
660
|
+
* page must be a power of two on both edges, as rigc's help defines it,
|
|
661
|
+
* `CHECK_PACK_PAGE_EDGES`. By the same rule the line's shape must be the
|
|
662
|
+
* `--pack-shape` passed, `CHECK_PACK_SHAPE` — a line with no shape clause
|
|
663
|
+
* reads as `rect`, so under `polygon` it disagrees. That is the case that
|
|
664
|
+
* matters: rigc 2.0.3 takes `--pack-shape` without a word, packs by
|
|
665
|
+
* rectangles and prints the form with no shape clause (measured), so a rigc
|
|
666
|
+
* older than 2.1.0 first on `PATH` is refused here rather than shipping a
|
|
667
|
+
* rect page as a polygon one. One problem per disagreement; empty is
|
|
668
|
+
* agreement.
|
|
600
669
|
*/
|
|
601
|
-
export function packEdgeProblems(pack: readonly PackLine[],
|
|
670
|
+
export function packEdgeProblems(pack: readonly PackLine[], mode: PackMode): Problem[] {
|
|
671
|
+
const edges = mode.pageEdges;
|
|
602
672
|
const problems: Problem[] = [];
|
|
603
673
|
for (const p of pack) {
|
|
604
674
|
const object = `packed page ${p.page}`;
|
|
@@ -606,11 +676,18 @@ export function packEdgeProblems(pack: readonly PackLine[], edges: PageEdges): P
|
|
|
606
676
|
problems.push({
|
|
607
677
|
code: 'CHECK_PACK_PAGE_EDGES',
|
|
608
678
|
object,
|
|
609
|
-
detail: `rigc's pack line says page edges ${p.pageEdges} (${p.pageEdges === 'free' ? 'it
|
|
679
|
+
detail: `rigc's pack line says page edges ${p.pageEdges} (${p.pageEdges === 'free' ? 'it carries ", page edges free"' : 'no ", page edges free" clause'}); the build was run with --page-edges ${edges}, so page edges ${edges} is required`,
|
|
610
680
|
});
|
|
611
681
|
} else if (edges === 'pot' && !(powerOfTwo(p.width) && powerOfTwo(p.height))) {
|
|
612
682
|
problems.push({ code: 'CHECK_PACK_PAGE_EDGES', object, detail: `is ${p.width}x${p.height}; the build was run with --page-edges pot, so a power of two on both edges is required` });
|
|
613
683
|
}
|
|
684
|
+
if (p.packShape !== mode.packShape) {
|
|
685
|
+
problems.push({
|
|
686
|
+
code: 'CHECK_PACK_SHAPE',
|
|
687
|
+
object,
|
|
688
|
+
detail: `rigc's pack line says shape ${p.packShape} (${PACK_SHAPE_CLAUSE.test(p.line) ? `it ends ", shape ${p.packShape}"` : 'no ", shape" clause: the 1.5.1-2.0.3 form, read as rect'}); the build was run with --pack-shape ${mode.packShape}, so shape ${mode.packShape} is required`,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
614
691
|
}
|
|
615
692
|
return problems;
|
|
616
693
|
}
|
|
@@ -885,6 +962,8 @@ export interface CheckFigures {
|
|
|
885
962
|
gate_spine_html_green: boolean;
|
|
886
963
|
/** Which rigc entry gated the build ({@link readRigcEntry}): provenance, not a bar. */
|
|
887
964
|
rigc_entry: RigcEntryRecord;
|
|
965
|
+
/** How the artifact's page was packed: the `--page-edges` and `--pack-shape` rigc was handed, which its pack line was read to agree with ({@link packEdgeProblems}). Provenance, not a bar. */
|
|
966
|
+
pack_mode: { page_edges: PageEdges; pack_shape: PackShape };
|
|
888
967
|
loop_max_diff: number;
|
|
889
968
|
seam_mean: number;
|
|
890
969
|
seam_px_over_40: number;
|
|
@@ -931,11 +1010,12 @@ export function rigcFailed(what: string, call: RigcCall, lines: readonly string[
|
|
|
931
1010
|
* gate file is written first, so the refusal's evidence is on disk. Otherwise
|
|
932
1011
|
* it measures everything, writes every output including `check.json`, and
|
|
933
1012
|
* returns the report; `report.problems` names each bar that was not met, and
|
|
934
|
-
* `figures.PASS` is true exactly when it is empty. `
|
|
935
|
-
* `--page-edges` verbatim,
|
|
936
|
-
* (`CHECK_PACK_PAGE_EDGES`)
|
|
1013
|
+
* `figures.PASS` is true exactly when it is empty. `mode` reaches rigc's
|
|
1014
|
+
* `--page-edges` and `--pack-shape` verbatim, a pack line that disagrees with
|
|
1015
|
+
* either is refused (`CHECK_PACK_PAGE_EDGES`, `CHECK_PACK_SHAPE`), and
|
|
1016
|
+
* `check.json` records it as `pack_mode`.
|
|
937
1017
|
*/
|
|
938
|
-
export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome: string = rigDir,
|
|
1018
|
+
export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome: string = rigDir, mode: PackMode = DEFAULT_PACK_MODE): CheckReport {
|
|
939
1019
|
const inp = readCheckInputs(rigDir, partsHome);
|
|
940
1020
|
const rigcEntry = readRigcEntry(requireRigcVersion(rigc));
|
|
941
1021
|
const out = resolve(outDir);
|
|
@@ -952,12 +1032,12 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
952
1032
|
};
|
|
953
1033
|
|
|
954
1034
|
// 1. build, packed, under the policy profile
|
|
955
|
-
const build = rigc(['build', '--rig', inp.rigPath, '--motion', inp.motionPath, '--out', buildDir, ...packedBuildArgs(
|
|
1035
|
+
const build = rigc(['build', '--rig', inp.rigPath, '--motion', inp.motionPath, '--out', buildDir, ...packedBuildArgs(mode)]);
|
|
956
1036
|
const gateHtml = buildGateLines(build.out);
|
|
957
1037
|
write('gate_spine-html.txt', `${gateHtml.join('\n')}\n`);
|
|
958
|
-
if (build.status !== 0) refuseIfAny(rigcFailed(packedBuildLabel(
|
|
1038
|
+
if (build.status !== 0) refuseIfAny(rigcFailed(packedBuildLabel(mode), build, gateHtml));
|
|
959
1039
|
const pack = parsePackLines(gateHtml);
|
|
960
|
-
refuseIfAny(packEdgeProblems(pack,
|
|
1040
|
+
refuseIfAny(packEdgeProblems(pack, mode));
|
|
961
1041
|
const packOpaque = pack.filter((p) => existsSync(join(buildDir, p.page))).map((p) => ({ page: p.page, share: opaqueShare(readPng(join(buildDir, p.page))) }));
|
|
962
1042
|
|
|
963
1043
|
// 2. no second gate: `rigc validate --profile spine` of this build measured no rule the build's own two passes
|
|
@@ -1080,6 +1160,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
1080
1160
|
const figures: CheckFigures = {
|
|
1081
1161
|
gate_spine_html_green: htmlGreen,
|
|
1082
1162
|
rigc_entry: rigcEntry,
|
|
1163
|
+
pack_mode: { page_edges: mode.pageEdges, pack_shape: mode.packShape },
|
|
1083
1164
|
loop_max_diff: loop.max,
|
|
1084
1165
|
seam_mean: round3(seam.mean),
|
|
1085
1166
|
seam_px_over_40: seam.over40,
|
|
@@ -1094,7 +1175,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
1094
1175
|
PASS: false,
|
|
1095
1176
|
};
|
|
1096
1177
|
const barProblems: Problem[] = [];
|
|
1097
|
-
if (!htmlGreen) barProblems.push(...rigcFailed(packedBuildLabel(
|
|
1178
|
+
if (!htmlGreen) barProblems.push(...rigcFailed(packedBuildLabel(mode), build, gateHtml));
|
|
1098
1179
|
if (figures.loop_max_diff !== LOOP_MAX_BAR) {
|
|
1099
1180
|
barProblems.push({
|
|
1100
1181
|
code: 'CHECK_LOOP_CLOSES',
|