spine-parts 0.6.0 → 0.7.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 +17 -6
- package/README.md +14 -9
- package/cli.ts +25 -23
- package/docs/AUTHORING.md +16 -10
- package/package.json +3 -2
- package/skills/spine-parts/SKILL.md +4 -2
- package/src/build.ts +48 -26
- package/src/check.ts +130 -28
package/NOTICE.md
CHANGED
|
@@ -60,10 +60,15 @@ from them beyond the layers See-through itself writes for the user's own image.
|
|
|
60
60
|
## 2. Spine Runtimes, through spine-rigc
|
|
61
61
|
|
|
62
62
|
spine-parts depends on [`spine-rigc`](https://www.npmjs.com/package/spine-rigc)
|
|
63
|
-
(MIT, same author), which compiles and validates Spine skeleton data
|
|
64
|
-
`@esotericsoftware/spine-core`, part of the
|
|
63
|
+
(MIT, same author), which compiles and validates Spine skeleton data. Its round
|
|
64
|
+
trip runs through `@esotericsoftware/spine-core`, part of the
|
|
65
65
|
[Spine Runtimes](https://github.com/EsotericSoftware/spine-runtimes), under the
|
|
66
66
|
[Spine Runtimes License Agreement](https://esotericsoftware.com/spine-runtimes-license).
|
|
67
|
+
Since spine-rigc 2.0 neither package installs the runtime: spine-rigc carries it
|
|
68
|
+
only as a development dependency, and an install of spine-rigc gates a build with
|
|
69
|
+
its own validator. spine-parts does not install it either; it is this repository's
|
|
70
|
+
development dependency, at the version spine-rigc develops against, for the round
|
|
71
|
+
trip in the selftest and CI and the selftest's posing oracle.
|
|
67
72
|
spine-rigc's [NOTICE.md](https://github.com/firejune/rigc/blob/main/NOTICE.md)
|
|
68
73
|
sets out the chain in full; briefly, as a restatement of Esoteric Software's
|
|
69
74
|
terms and not a term of this project:
|
|
@@ -73,8 +78,11 @@ terms and not a term of this project:
|
|
|
73
78
|
2. Playing Spine skeleton data in a product requires a Spine Runtime, and the
|
|
74
79
|
Spine Runtimes License requires **each user of such a product to own a
|
|
75
80
|
Spine Editor license**.
|
|
76
|
-
3. spine-rigc
|
|
77
|
-
|
|
81
|
+
3. spine-rigc no longer installs `spine-core`, and spine-parts does not either;
|
|
82
|
+
the obligation in 2 attaches to the Spine skeleton data spine-rigc writes,
|
|
83
|
+
which the stages of spine-parts that call it produce, and to `spine-core`
|
|
84
|
+
wherever it is installed beside them — this repository's development
|
|
85
|
+
dependency included.
|
|
78
86
|
|
|
79
87
|
spine-parts also imports spine-rigc's own modules directly, by deep path: its PNG codec
|
|
80
88
|
(`tools/plate.ts`, `src/png.ts`), its 5x7 label font (`tools/font5x7.ts`) and
|
|
@@ -114,10 +122,13 @@ function's comment says which call it stands in for and what was measured.
|
|
|
114
122
|
| Package | Licence | Why |
|
|
115
123
|
| --- | --- | --- |
|
|
116
124
|
| [`spine-rigc`](https://www.npmjs.com/package/spine-rigc) | MIT | compile, gate, render and check; its PNG codec, font and coordinate conversion |
|
|
117
|
-
| ↳ [`@esotericsoftware/spine-core`](https://www.npmjs.com/package/@esotericsoftware/spine-core) | Spine Runtimes License | spine-rigc's round trip (section 2) |
|
|
118
125
|
| [`ag-psd`](https://www.npmjs.com/package/ag-psd) | MIT | reading an upstream See-through `.psd` |
|
|
119
126
|
| ↳ [`pako`](https://www.npmjs.com/package/pako) | MIT AND Zlib | ag-psd's deflate |
|
|
120
127
|
| ↳ [`base64-js`](https://www.npmjs.com/package/base64-js) | MIT | ag-psd's base64 |
|
|
121
128
|
|
|
122
129
|
Development only, not installed with the package: `typescript` (Apache-2.0),
|
|
123
|
-
`eslint` (MIT), `typescript-eslint` (MIT), `@types/bun` (MIT)
|
|
130
|
+
`eslint` (MIT), `typescript-eslint` (MIT), `@types/bun` (MIT), and
|
|
131
|
+
[`@esotericsoftware/spine-core`](https://www.npmjs.com/package/@esotericsoftware/spine-core)
|
|
132
|
+
(Spine Runtimes License; section 2), pinned to the version spine-rigc develops
|
|
133
|
+
against (`TY10`): the round trip in the selftest and CI, the selftest's posing
|
|
134
|
+
oracle, and `tools/atlas_population.ts` and `tools/idle_cost.ts`.
|
package/README.md
CHANGED
|
@@ -35,7 +35,6 @@ was shifted down into the canvas (its proposal ran 118 px above the top edge;
|
|
|
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
37
|
<code>49 assertions: 23 measured (23 passed, 0 failed), 26 skipped, 0 not in profile "spine-html"</code><br/>
|
|
38
|
-
<code>49 assertions: 14 measured (14 passed, 0 failed), 20 skipped, 15 not in profile "spine"</code><br/>
|
|
39
38
|
<code>loop: idle 49 frame(s) at 12 fps, f0000 vs f0048 (t = 4s): max |d| 0 (0 required)</code><br/>
|
|
40
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/>
|
|
41
40
|
<code>pack: skeleton.png 967x1338, 22 region(s), 91.2% covered, padding 2, page edges free</code><br/>
|
|
@@ -87,7 +86,7 @@ page** (issue #2):
|
|
|
87
86
|
|
|
88
87
|
| path under `--out` | what |
|
|
89
88
|
| --- | --- |
|
|
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
|
|
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 1.5.1 and 2.0.3: the 14 rules `validate --profile spine` measures are among the build's 23; 15 are not in `spine`) |
|
|
91
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 |
|
|
92
91
|
| `rig/` | `rig.json` and `motion.json` in spine-rigc's spec, `mesh_report.json`, the padded `images/` |
|
|
93
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` |
|
|
@@ -124,7 +123,7 @@ atlas written by anything else would have no oracle behind it.
|
|
|
124
123
|
| stage | tool | what it owns |
|
|
125
124
|
| --- | --- | --- |
|
|
126
125
|
| painting + See-through layers → parts and specs | **spine-parts** | the merge of two runs, the measurements, the rig spec and motion spec |
|
|
127
|
-
| specs → Spine skeleton data | **[spine-rigc](https://github.com/firejune/rigc)** | compile, the round trip through `spine-core
|
|
126
|
+
| specs → Spine skeleton data | **[spine-rigc](https://github.com/firejune/rigc)** | compile, the gate (rigc's own validator in an install; the round trip through `spine-core` where the runtime is beside it, as in this repository's CI), the named assertions, the packer, the renderer |
|
|
128
127
|
| skeleton data → a page | **[spine-html](https://github.com/firejune/spine-html)**, a sibling project | a DOM renderer; rigc's `spine-html` profile is its policy, and every build here is gated under that profile as well as under `spine` |
|
|
129
128
|
|
|
130
129
|
spine-parts never writes Spine data itself. Everything on disk under `check/build/`
|
|
@@ -253,8 +252,8 @@ agent skill.
|
|
|
253
252
|
| `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
|
|
254
253
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
|
|
255
254
|
| `propose … --from-config <config>` | draw and LINT the config's current bones |
|
|
256
|
-
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after spine-rigc's
|
|
257
|
-
| `check --rig --out [--parts]` | build packed,
|
|
255
|
+
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after spine-rigc's gate is green; `--idle-keys` says whether the idle's keys on mesh-driving bones go through `<bone>_ctl` parents (default) or stay on the bones with `invariants.idleDrivesMeshes` declared |
|
|
256
|
+
| `check --rig --out [--parts]` | build packed, gated under `spine-html`, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json` |
|
|
258
257
|
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
259
258
|
| `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
260
259
|
|
|
@@ -319,13 +318,19 @@ the flag, puts 813 of `sample`'s 993 still-unprojected pixels and 879 of `demo`'
|
|
|
319
318
|
|
|
320
319
|
[Bun](https://bun.sh) 1.2 or later. `npm install -g spine-parts` installs the
|
|
321
320
|
`spine-parts` command; it hands off to Bun and says so in one sentence if Bun is not
|
|
322
|
-
on `PATH`. spine-rigc comes with it as a dependency.
|
|
321
|
+
on `PATH`. spine-rigc comes with it as a dependency. No Spine runtime does: since
|
|
322
|
+
spine-rigc 2.0 the installed `rigc` gates every build with its own validator over the
|
|
323
|
+
compiled model document, and `rigc --version` says `entry: cli_core.ts —
|
|
324
|
+
@esotericsoftware/spine-core absent`. With `@esotericsoftware/spine-core` installed
|
|
325
|
+
beside it, the same `rigc` runs the spine-core round trip instead, as this repository's
|
|
326
|
+
selftest and CI do; `check.json`'s `rigc_entry` records which one gated a build.
|
|
323
327
|
|
|
324
328
|
## Licence posture
|
|
325
329
|
|
|
326
|
-
spine-parts is MIT, and it depends on spine-rigc,
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
spine-parts is MIT, and it depends on spine-rigc, whose output is Spine skeleton data:
|
|
331
|
+
using what it produces in a product requires a Spine Editor licence, as any Spine data
|
|
332
|
+
does. Neither package installs Esoteric Software's `spine-core` any more; this
|
|
333
|
+
repository uses it in development, for the round trip in its selftest and CI. [NOTICE.md](NOTICE.md) records that chain and every other
|
|
329
334
|
third-party term this package touches, See-through's included.
|
|
330
335
|
|
|
331
336
|
## Development
|
package/cli.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { spawnSync } from 'node:child_process';
|
|
|
15
15
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { basename, dirname, extname, join } from 'node:path';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
19
18
|
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
20
19
|
import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
21
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';
|
|
@@ -112,9 +111,10 @@ usage:
|
|
|
112
111
|
CONFIG_BLINK_GROUP_UNIQUE, before rigc starts. --parts is the
|
|
113
112
|
directory holding parts.json and parts/<name>.png. The result is built
|
|
114
113
|
through spine-rigc (profile spine-html, packed with --page-edges as for
|
|
115
|
-
check
|
|
116
|
-
images/*.png, rig.json,
|
|
117
|
-
|
|
114
|
+
check; rigc's build gates the compile and the packed pages on disk) in a
|
|
115
|
+
scratch directory first, and --out receives images/*.png, rig.json,
|
|
116
|
+
motion.json and mesh_report.json only when it is green. Prints one line
|
|
117
|
+
per mesh and the rigc gate lines.
|
|
118
118
|
--idle-keys says where the idle's keys on a bone a mesh is weighted to go:
|
|
119
119
|
ctl (the default) keys a same-origin <bone>_ctl parent instead, which
|
|
120
120
|
passes A15_IDLE_NO_MESH_BONE_KEYS on any spine-rigc; direct keys the bone
|
|
@@ -129,11 +129,12 @@ usage:
|
|
|
129
129
|
${PARTS_HOME_SENTENCE}.
|
|
130
130
|
Both are only read. Into --out:
|
|
131
131
|
build/ (rigc build --profile spine-html --pack --page-edges <value>: the
|
|
132
|
-
packed atlas is the artifact), gate_spine-html.txt
|
|
132
|
+
packed atlas is the artifact), gate_spine-html.txt (the gate lines
|
|
133
133
|
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
134
134
|
--geometry: the frames and geometry.json, the skinned vertices and
|
|
135
135
|
every bone's world transform per frame),
|
|
136
|
-
contact.png, motion_heat.png and check.json
|
|
136
|
+
contact.png, motion_heat.png and check.json (with rigc_entry, the rigc
|
|
137
|
+
entry that gated the build). PASS needs every gate summary
|
|
137
138
|
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
138
139
|
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
|
|
139
140
|
frame at t = duration) at max |d| 0. Then six judgement lines, each in
|
|
@@ -396,20 +397,6 @@ function cmdSheet(args: string[]): number {
|
|
|
396
397
|
}
|
|
397
398
|
}
|
|
398
399
|
|
|
399
|
-
/** spine-rigc's own CLI, resolved the way this package resolves every other rigc path. */
|
|
400
|
-
function rigcCli(): string {
|
|
401
|
-
return fileURLToPath(import.meta.resolve('spine-rigc/cli.ts'));
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/** The rig stage's rigc: spine-rigc's cli.ts run by this Bun, as the stage has always gated. */
|
|
405
|
-
function rigGateRunner(): RigcRunner {
|
|
406
|
-
return (args) => {
|
|
407
|
-
const r = spawnSync(process.execPath, [rigcCli(), ...args], { encoding: 'utf8', maxBuffer: 1 << 28 });
|
|
408
|
-
if (r.error !== undefined) return { status: 127, out: `could not start ${rigcCli()}: ${r.error.message}` };
|
|
409
|
-
return { status: r.status ?? 1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
400
|
/** `--page-edges`, or its default; a value spine-rigc does not take is a usage error naming the two it does. */
|
|
414
401
|
function pageEdgesOf(value: string | undefined): PageEdges | string {
|
|
415
402
|
const edges = value ?? DEFAULT_PAGE_EDGES;
|
|
@@ -449,9 +436,15 @@ function cmdRig(args: string[]): number {
|
|
|
449
436
|
if (!(IDLE_KEYS as readonly string[]).includes(keys)) return usage(`--idle-keys ${keys}; one of ${IDLE_KEYS.join(', ')} is required`);
|
|
450
437
|
const edges = pageEdgesOf(pageEdges ?? undefined);
|
|
451
438
|
if (!isPageEdges(edges)) return usage(edges);
|
|
439
|
+
let bin: string;
|
|
440
|
+
try {
|
|
441
|
+
bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
442
|
+
} catch (err) {
|
|
443
|
+
return printRefusal(err);
|
|
444
|
+
}
|
|
452
445
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-rig-'));
|
|
453
446
|
try {
|
|
454
|
-
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys, pageEdges: edges },
|
|
447
|
+
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys, pageEdges: edges }, rigcRunner(bin), scratch, console.log);
|
|
455
448
|
return EXIT_OK;
|
|
456
449
|
} catch (err) {
|
|
457
450
|
return printRefusal(err);
|
|
@@ -460,7 +453,16 @@ function cmdRig(args: string[]): number {
|
|
|
460
453
|
}
|
|
461
454
|
}
|
|
462
455
|
|
|
463
|
-
/**
|
|
456
|
+
/**
|
|
457
|
+
* The rigc process, for every stage that runs rigc — `rig`'s gate, `check`, and
|
|
458
|
+
* both inside `build`: the `rigc` binary {@link findRigc} located, spawned as
|
|
459
|
+
* it is, so spine-rigc's own launcher (`bin/rigc.cjs`) chooses the entry —
|
|
460
|
+
* `cli.ts` and the spine-core round trip where the runtime resolves beside it,
|
|
461
|
+
* `cli_core.ts` and rigc's own validator where it does not. No stage runs a
|
|
462
|
+
* rigc source file by path: under spine-rigc 2.0, `cli.ts` by path is the
|
|
463
|
+
* spine-core entry whether or not spine-core is there. `src/` is pure and takes
|
|
464
|
+
* the spawn from here.
|
|
465
|
+
*/
|
|
464
466
|
function rigcRunner(bin: string): RigcRunner {
|
|
465
467
|
return (args) => {
|
|
466
468
|
const r = spawnSync(bin, [...args], { encoding: 'utf8', maxBuffer: 1 << 28 });
|
|
@@ -830,7 +832,7 @@ function cmdBuild(args: string[]): number {
|
|
|
830
832
|
try {
|
|
831
833
|
const r = build(
|
|
832
834
|
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges },
|
|
833
|
-
{ rig:
|
|
835
|
+
{ rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
|
|
834
836
|
console.log,
|
|
835
837
|
);
|
|
836
838
|
return r.stoppedAt === null ? EXIT_OK : EXIT_REFUSED;
|
package/docs/AUTHORING.md
CHANGED
|
@@ -18,7 +18,7 @@ into `examples/sample/inputs/`.
|
|
|
18
18
|
| what | why | how |
|
|
19
19
|
| --- | --- | --- |
|
|
20
20
|
| [Bun](https://bun.sh) 1.2 or later | spine-parts and spine-rigc are Bun programs | `npm install -g spine-parts` installs the command; it says so if Bun is missing |
|
|
21
|
-
| [spine-rigc](https://www.npmjs.com/package/spine-rigc) | compiles, gates, packs and renders every rig; spine-parts writes no Spine data itself | installed with spine-parts as a dependency (`node_modules/.bin/rigc`) |
|
|
21
|
+
| [spine-rigc](https://www.npmjs.com/package/spine-rigc) | compiles, gates, packs and renders every rig; spine-parts writes no Spine data itself | installed with spine-parts as a dependency (`node_modules/.bin/rigc`); with no Spine runtime beside it, it gates with its own validator, and `rigc --version` says `entry: cli_core.ts` |
|
|
22
22
|
| [See-through](https://github.com/shitagaki-lab/see-through), somewhere | the layer decomposition is the input | any route in README *Getting See-through layers*; it runs twice per character, outside this tool |
|
|
23
23
|
| a GPU, wherever See-through runs | See-through is a diffusion model | nothing in spine-parts itself uses a GPU |
|
|
24
24
|
|
|
@@ -266,7 +266,7 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
|
|
|
266
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 |
|
|
267
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) |
|
|
268
268
|
| `propose` | `note:` lines, `LINT` lines, `landmarks.png` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter | a link off the art (a bone on the background) — move it onto the layer; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head` |
|
|
269
|
-
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`) | `cover 1.00000` on every mesh, both
|
|
269
|
+
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`) | `cover 1.00000` on every mesh, both gate summaries `0 failed` (the compile and the packed pages) | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
|
|
270
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) |
|
|
271
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) |
|
|
272
272
|
|
|
@@ -301,7 +301,9 @@ the reference's so the examples stay comparable with it.
|
|
|
301
301
|
the least-area page the parts need. `--page-edges pot` is the opt-out: a power of
|
|
302
302
|
two on both edges, which is the page every build wrote before this flag. The pack
|
|
303
303
|
line says which one ran, because rigc ends it with `, page edges free` under `free`.
|
|
304
|
-
Measured
|
|
304
|
+
Measured on the two examples by `tools/atlas_population.ts` with rigc 1.5.1, and again
|
|
305
|
+
with rigc 2.0.3, which reproduced every figure in both tables and the one check figure
|
|
306
|
+
below:
|
|
305
307
|
|
|
306
308
|
| build | page | covered | page opaque | page / figure |
|
|
307
309
|
| --- | --- | --- | --- | --- |
|
|
@@ -357,8 +359,8 @@ differ by at most one level in a channel. The default stays `ctl`: the examples'
|
|
|
357
359
|
`expected/rig.json` and `motion.json` are the reference's output, and the
|
|
358
360
|
declaration is in the rig spec only, so `rigc validate <build> --profile spine-html`
|
|
359
361
|
run on a `direct` build (it has no rig spec to read) refuses A15 once per keyed
|
|
360
|
-
bone. `
|
|
361
|
-
|
|
362
|
+
bone. No stage runs `validate`: the gate is `rigc build`, which reads the rig spec
|
|
363
|
+
and its declaration, and is green on both (`RG19`).
|
|
362
364
|
|
|
363
365
|
**Patching a hole no layer holds.** When an `uncovered hole` stays large after the
|
|
364
366
|
plan is right — both See-through runs dropped a piece of the figure, red in
|
|
@@ -588,7 +590,8 @@ See-through with another seed.
|
|
|
588
590
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
589
591
|
| `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/` |
|
|
590
592
|
| `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 ^
|
|
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.0.3), or put a newer `rigc` first on `PATH` |
|
|
594
|
+
| `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` |
|
|
592
595
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
593
596
|
| `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 |
|
|
594
597
|
| `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 |
|
|
@@ -611,14 +614,17 @@ See-through with another seed.
|
|
|
611
614
|
implementation's bars):
|
|
612
615
|
|
|
613
616
|
- `gate_spine-html.txt`: every summary line `(N passed, 0 failed)` — the packed build
|
|
614
|
-
under the `spine-html` profile
|
|
615
|
-
|
|
617
|
+
under the `spine-html` profile, gated once over the compile and once over the packed
|
|
618
|
+
pages on disk. There is no `spine` gate of its own: `spine-html` holds every rule
|
|
619
|
+
`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`; `CH09`
|
|
621
|
+
holds it on every fetched example);
|
|
616
622
|
- **seam**: the setup-pose render against the flat composite of `parts/`: mean
|
|
617
623
|
max-channel |d| ≤ 1.0 of 255, and at most 50 pixels over 40;
|
|
618
624
|
- **loop**: idle frame 0 against the frame at `t = duration`: max |d| exactly 0.
|
|
619
625
|
|
|
620
|
-
On the examples: `sample` 23/23
|
|
621
|
-
`demo` (default rule) 23/23
|
|
626
|
+
On the examples: `sample` 23/23, seam 0.207 with 0 pixels over 40, loop 0;
|
|
627
|
+
`demo` (default rule) 23/23, seam 0.326 with 2 pixels over 40, loop 0.
|
|
622
628
|
|
|
623
629
|
A green gate cannot see a wrong animation, so `check` also writes six **judgement
|
|
624
630
|
lines** (issue #11; `TEXTURE_STRETCH`, issue #31), each a key of `check.json` and a console line
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-parts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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,9 +57,10 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"ag-psd": "^31.0.2",
|
|
60
|
-
"spine-rigc": "^
|
|
60
|
+
"spine-rigc": "^2.0.3"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
+
"@esotericsoftware/spine-core": "4.3.13",
|
|
63
64
|
"@types/bun": "^1.4.2",
|
|
64
65
|
"eslint": "^10.11.0",
|
|
65
66
|
"typescript": "^5.9.3",
|
|
@@ -15,9 +15,11 @@ stage writes are the whole interface.
|
|
|
15
15
|
|
|
16
16
|
## Non-negotiables
|
|
17
17
|
|
|
18
|
-
- **spine-rigc's
|
|
19
|
-
itself; `rig` writes only after rigc's build
|
|
18
|
+
- **spine-rigc's gate is never bypassed.** spine-parts writes no Spine data
|
|
19
|
+
itself; `rig` writes only after rigc's `build` gate is green, and `build`'s
|
|
20
20
|
artifact is what rigc wrote. There is no flag that skips it, and none may be added.
|
|
21
|
+
`check.json`'s `rigc_entry` names which gate ran: rigc's own validator in an
|
|
22
|
+
install, the spine-core round trip where the runtime is installed beside rigc.
|
|
21
23
|
- **No value is invented.** A missing config field is a refusal naming it. Take a
|
|
22
24
|
value from the command that proposes it (`propose --head-box`,
|
|
23
25
|
`assemble --propose-plan`, `propose`) or decide it yourself — never guess one
|
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 { type CheckReport, 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_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';
|
|
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';
|
|
@@ -162,7 +162,12 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
162
162
|
interface GateRun {
|
|
163
163
|
label: string;
|
|
164
164
|
status: number;
|
|
165
|
-
/**
|
|
165
|
+
/**
|
|
166
|
+
* rigc's FAIL lines, its assertion summary, the core entry's `here:` line and
|
|
167
|
+
* the one SKIP this stage's own declaration causes, as it printed them — or,
|
|
168
|
+
* for a run that exited non-zero with none of those, the lines
|
|
169
|
+
* {@link causeLines} says name why.
|
|
170
|
+
*/
|
|
166
171
|
lines: string[];
|
|
167
172
|
}
|
|
168
173
|
|
|
@@ -175,18 +180,24 @@ interface GateRun {
|
|
|
175
180
|
*/
|
|
176
181
|
const DECLARED_SKIP = /^ {2}SKIP {2}A15_IDLE_NO_MESH_BONE_KEYS: declared by the rig/;
|
|
177
182
|
|
|
183
|
+
/** The core entry's line after its summary (spine-rigc 2.0.0 and later): which rules ran, and that the round trip did not. */
|
|
184
|
+
const HERE_LINE = /^ {2}\.\. {4}here: /;
|
|
185
|
+
|
|
178
186
|
function gateRun(label: string, rigc: RigcRunner, args: string[]): GateRun {
|
|
179
187
|
const r = rigc(args);
|
|
180
|
-
const lines = r.out.split('\n').filter((l) => /^ {2}FAIL {2}/.test(l) || /assertions: \d+ measured/.test(l) || /^rigc compile error/.test(l) || DECLARED_SKIP.test(l));
|
|
181
|
-
|
|
188
|
+
const lines = r.out.split('\n').filter((l) => /^ {2}FAIL {2}/.test(l) || /assertions: \d+ measured/.test(l) || /^rigc compile error/.test(l) || DECLARED_SKIP.test(l) || HERE_LINE.test(l));
|
|
189
|
+
const failed = lines.some((l) => /^ {2}FAIL {2}/.test(l) || /^rigc compile error/.test(l));
|
|
190
|
+
return { label, status: r.status, lines: r.status !== 0 && !failed ? [...lines, ...causeLines(r.out)] : lines };
|
|
182
191
|
}
|
|
183
192
|
|
|
184
193
|
/**
|
|
185
|
-
* spine-rigc's
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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
|
|
196
|
+
* once over the compile and once over the packed pages on disk. There is no
|
|
197
|
+
* second `validate --profile spine` run: spine-html holds every rule spine
|
|
198
|
+
* measures (selftest `CH09`). The files are staged exactly as `--out` will
|
|
199
|
+
* receive them, so what passed is what is written. The scratch directory is
|
|
200
|
+
* the caller's, and is emptied here before and after.
|
|
190
201
|
*/
|
|
191
202
|
function gateThroughRigc(out: RigOutput, texts: Array<[string, string]>, rigc: RigcRunner, scratch: string, edges: PageEdges): GateRun[] {
|
|
192
203
|
rmSync(scratch, { recursive: true, force: true });
|
|
@@ -195,9 +206,7 @@ function gateThroughRigc(out: RigOutput, texts: Array<[string, string]>, rigc: R
|
|
|
195
206
|
for (const [file, img] of out.images) writeFileSync(join(scratch, 'images', file), encodePngBytes(img));
|
|
196
207
|
for (const [file, text] of texts) writeFileSync(join(scratch, file), text);
|
|
197
208
|
const build = join(scratch, 'build');
|
|
198
|
-
|
|
199
|
-
if (html.status !== 0) return [html];
|
|
200
|
-
return [html, gateRun('validate --profile spine', rigc, ['validate', build, '--profile', 'spine'])];
|
|
209
|
+
return [gateRun(packedBuildLabel(edges), rigc, ['build', '--rig', join(scratch, 'rig.json'), '--motion', join(scratch, 'motion.json'), '--out', build, ...packedBuildArgs(edges)])];
|
|
201
210
|
} finally {
|
|
202
211
|
rmSync(scratch, { recursive: true, force: true });
|
|
203
212
|
}
|
|
@@ -214,7 +223,7 @@ export interface RigStageInput {
|
|
|
214
223
|
pageEdges?: PageEdges;
|
|
215
224
|
}
|
|
216
225
|
|
|
217
|
-
/** Author the rig, gate it through rigc in `scratch`, and write `out` only when
|
|
226
|
+
/** Author the rig, gate it through rigc in `scratch`, and write `out` only when the gate is green. */
|
|
218
227
|
export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string, log: Log): RigOutput {
|
|
219
228
|
const cfg = loadConfig(input.config);
|
|
220
229
|
const parts = readParts(join(input.parts, 'parts.json'));
|
|
@@ -259,7 +268,7 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
259
268
|
const problems: Problem[] = red.map((g) => ({
|
|
260
269
|
code: 'RIG_RIGC_GREEN',
|
|
261
270
|
object: `rigc ${g.label}`,
|
|
262
|
-
detail: `exited ${g.status}${g.lines.length > 0 ? `: ${g.lines.map((l) => l.trim()).join(' | ')}` : ''}; exit 0 is required before anything is written, and nothing was`,
|
|
271
|
+
detail: `exited ${g.status}${g.lines.length > 0 ? `: ${g.lines.map((l) => l.trim()).join(' | ')}` : ', and it printed nothing'}; exit 0 is required before anything is written, and nothing was`,
|
|
263
272
|
}));
|
|
264
273
|
throw new PartsError(problems);
|
|
265
274
|
}
|
|
@@ -316,13 +325,13 @@ export function judgementLine(name: string, line: JudgementLine | ReportedLine):
|
|
|
316
325
|
export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string, log: Log): CheckReport {
|
|
317
326
|
const v = rigc(['--version']);
|
|
318
327
|
log(`spine-parts check: ${input.rig} -> ${input.out}`);
|
|
319
|
-
|
|
328
|
+
const versionLines = v.out.trim().split('\n');
|
|
329
|
+
const entryLine = versionLines.find((l) => l.startsWith('entry:'));
|
|
330
|
+
log(` rigc ${versionLines[0]} at ${bin}${entryLine === undefined ? '' : `; ${entryLine}`}`);
|
|
320
331
|
const edges = input.pageEdges ?? DEFAULT_PAGE_EDGES;
|
|
321
332
|
const r = runCheck(input.rig, input.out, rigc, input.parts, edges);
|
|
322
333
|
log(` gate spine-html (rigc ${packedBuildLabel(edges)}), verbatim:`);
|
|
323
334
|
for (const l of r.gateHtml) log(l);
|
|
324
|
-
log(' gate spine (rigc validate build --profile spine), verbatim:');
|
|
325
|
-
for (const l of r.gateSpine) log(l);
|
|
326
335
|
for (const l of packLines(r)) log(` ${l}`);
|
|
327
336
|
const fig = r.figures;
|
|
328
337
|
log(` loop: idle ${r.idle.frames} frame(s) at ${r.idle.fps} fps, f0000 vs f${String(r.idle.lastIndex).padStart(4, '0')} (t = ${r.idle.duration}s): max |d| ${fig.loop_max_diff} (0 required)`);
|
|
@@ -332,7 +341,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
332
341
|
);
|
|
333
342
|
for (const name of JUDGEMENT_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
334
343
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
335
|
-
log(`
|
|
344
|
+
log(` gate: spine-html ${fig.gate_spine_html_green ? 'green' : 'RED'} (${fig.rigc_entry.entry}${fig.rigc_entry.spine_core === null ? ', rigc\'s own validator' : `, the spine-core ${fig.rigc_entry.spine_core} round trip`})`);
|
|
336
345
|
log(` wrote ${r.written.map((w) => join(input.out, w)).join(', ')}, ${join(input.out, 'build')}/, ${join(input.out, 'idle_frames')}/`);
|
|
337
346
|
for (const p of r.problems) log(` FAIL ${problemLine(p)}`);
|
|
338
347
|
log(fig.PASS ? 'check: PASS' : `check: FAIL — ${r.problems.length} bar(s) not met`);
|
|
@@ -434,7 +443,12 @@ export interface BuildInput {
|
|
|
434
443
|
pageEdges: PageEdges;
|
|
435
444
|
}
|
|
436
445
|
|
|
437
|
-
/**
|
|
446
|
+
/**
|
|
447
|
+
* The rigc process for each stage: `rig` gates through one, `check` measures
|
|
448
|
+
* through the other. `cli.ts` hands both the same runner, the `rigc` binary
|
|
449
|
+
* spine-rigc's launcher answers for; they are two fields so a caller can tell
|
|
450
|
+
* the stages' calls apart (the selftest counts them).
|
|
451
|
+
*/
|
|
438
452
|
export interface BuildRunners {
|
|
439
453
|
rig: RigcRunner;
|
|
440
454
|
check: RigcRunner;
|
|
@@ -457,20 +471,28 @@ export interface BuildResult {
|
|
|
457
471
|
artifact: string[];
|
|
458
472
|
}
|
|
459
473
|
|
|
474
|
+
/**
|
|
475
|
+
* rigc's own record of the compiled rig, which `build` writes beside the
|
|
476
|
+
* skeleton JSON and atlas (spine-rigc's AUTHORING, the `--out` row; written
|
|
477
|
+
* since 1.6): what rigc's posing core reads, not a Spine file, so it is not the
|
|
478
|
+
* artifact and is not counted as a second skeleton JSON.
|
|
479
|
+
*/
|
|
480
|
+
export const RIGC_MODEL_DOCUMENT = 'skeleton.model.json';
|
|
481
|
+
|
|
460
482
|
/** The three artifact files of a packed build, each refused by name when absent. */
|
|
461
|
-
function artifactPaths(buildDir: string,
|
|
483
|
+
export function artifactPaths(buildDir: string, pack: readonly PackLine[]): string[] {
|
|
462
484
|
const problems: Problem[] = [];
|
|
463
485
|
const names = existsSync(buildDir) ? readdirSync(buildDir).sort() : [];
|
|
464
|
-
const json = names.filter((n) => n.endsWith('.json'));
|
|
486
|
+
const json = names.filter((n) => n.endsWith('.json') && n !== RIGC_MODEL_DOCUMENT);
|
|
465
487
|
const atlas = names.filter((n) => n.endsWith('.atlas'));
|
|
466
|
-
if (json.length !== 1) problems.push({ code: 'BUILD_ARTIFACT_PRESENT', object: `${buildDir} skeleton JSON`, detail: `holds ${json.length} .json file(s) [${json.join(', ')}]; the packed build writes exactly one` });
|
|
488
|
+
if (json.length !== 1) problems.push({ code: 'BUILD_ARTIFACT_PRESENT', object: `${buildDir} skeleton JSON`, detail: `holds ${json.length} .json file(s) [${json.join(', ')}]; the packed build writes exactly one besides rigc's ${RIGC_MODEL_DOCUMENT}` });
|
|
467
489
|
if (atlas.length !== 1) problems.push({ code: 'BUILD_ARTIFACT_PRESENT', object: `${buildDir} atlas`, detail: `holds ${atlas.length} .atlas file(s) [${atlas.join(', ')}]; the packed build writes exactly one` });
|
|
468
|
-
if (
|
|
469
|
-
for (const p of
|
|
490
|
+
if (pack.length === 0) problems.push({ code: 'BUILD_ARTIFACT_PRESENT', object: `${buildDir} packed page`, detail: 'rigc printed no pack line, so no packed page is named; the build runs with --pack and must print one' });
|
|
491
|
+
for (const p of pack) {
|
|
470
492
|
if (!names.includes(p.page)) problems.push({ code: 'BUILD_ARTIFACT_PRESENT', object: `${buildDir} packed page`, detail: `the pack line names ${p.page}, which is not on disk` });
|
|
471
493
|
}
|
|
472
494
|
if (problems.length > 0) throw new PartsError(problems);
|
|
473
|
-
return [join(buildDir, json[0]), join(buildDir, atlas[0]), ...
|
|
495
|
+
return [join(buildDir, json[0]), join(buildDir, atlas[0]), ...pack.map((p) => join(buildDir, p.page))];
|
|
474
496
|
}
|
|
475
497
|
|
|
476
498
|
/**
|
|
@@ -542,7 +564,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
542
564
|
|
|
543
565
|
let artifact: string[];
|
|
544
566
|
try {
|
|
545
|
-
artifact = artifactPaths(join(out, 'check', 'build'), report);
|
|
567
|
+
artifact = artifactPaths(join(out, 'check', 'build'), report.pack);
|
|
546
568
|
} catch (err) {
|
|
547
569
|
return refused('artifact', err, report);
|
|
548
570
|
}
|
package/src/check.ts
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*
|
|
7
7
|
* ⭐ **This is the one stage that drives a subprocess, and it does not own the
|
|
8
8
|
* spawn.** Every rigc step — build, validate, render — is spine-rigc's own CLI,
|
|
9
|
-
* never a re-implementation of it, because rigc's
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* never a re-implementation of it, because rigc's gate is the only oracle
|
|
10
|
+
* behind a skeleton (CLAUDE.md, "spine-rigc's gate is not optional here
|
|
11
|
+
* either"): the spine-core round trip where the runtime resolves beside rigc,
|
|
12
|
+
* rigc's own validator where it does not, and `check.json`'s `rigc_entry` says
|
|
13
|
+
* which ({@link readRigcEntry}). But `src/` is held to no child
|
|
12
14
|
* processes (`TY06`, with `src/comfy/` its one named exception), so the process
|
|
13
15
|
* is injected: `runCheck` takes a {@link RigcRunner}, `cli.ts` hands it one
|
|
14
16
|
* that spawns the binary {@link findRigc} located, and the selftest hands it
|
|
@@ -24,8 +26,7 @@
|
|
|
24
26
|
* | output | what |
|
|
25
27
|
* | --- | --- |
|
|
26
28
|
* | `build/` | `rigc build --profile spine-html --pack --page-edges free` (or `pot`, when the caller names it; {@link PAGE_EDGES}) — the packed atlas is the final artifact |
|
|
27
|
-
* | `gate_spine-html.txt` | that build's gate lines, verbatim |
|
|
28
|
-
* | `gate_spine.txt` | `rigc validate build/ --profile spine`'s gate lines, verbatim |
|
|
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`) |
|
|
29
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) |
|
|
30
31
|
* | `contact.png` | rigc's own contact sheet of that render, copied out |
|
|
31
32
|
* | `motion_heat.png` | frame 0 in grey with each pixel's largest change across the idle in red |
|
|
@@ -195,7 +196,7 @@ export const SPINEBOY_YARDSTICK = '1024x256, 40 region(s), 45.8% opaque (alpha >
|
|
|
195
196
|
* `free` is the default here although rigc's own default is `pot`: the packed
|
|
196
197
|
* page is this package's final artifact, and on the two public examples `free`
|
|
197
198
|
* takes the page from 1024x2048 to 967x1338 (covered 56.3 % -> 91.2 %) and
|
|
198
|
-
* from 512x2048 to 479x1166 (49.7 % -> 93.4 %), rigc 1.5.1. The cost was
|
|
199
|
+
* from 512x2048 to 479x1166 (49.7 % -> 93.4 %), rigc 1.5.1 and 2.0.3 alike. The cost was
|
|
199
200
|
* measured on the same two builds. Each of the 49 idle frames differs from the
|
|
200
201
|
* `pot` build's, by at most 1 level in any channel (demo 2,373 px, sample 651
|
|
201
202
|
* px over all 49). On sample that moves STILL_REGIONS_DARK's reported
|
|
@@ -231,6 +232,81 @@ export interface RigcCall {
|
|
|
231
232
|
|
|
232
233
|
export type RigcRunner = (args: readonly string[]) => RigcCall;
|
|
233
234
|
|
|
235
|
+
/** A line in which a rigc run that stopped says why: Bun's own `error: …`, rigc's `rigc <command>: …` refusals, a `usage: …` line. */
|
|
236
|
+
const CAUSE = /^(error: |rigc[\w -]*: |usage: )/;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* What a rigc run that exited non-zero said about why, for a refusal whose own
|
|
240
|
+
* filter (FAIL lines, the assertion summary) matched nothing: every line that
|
|
241
|
+
* names a cause, or, when none does, the last three non-empty lines. Measured
|
|
242
|
+
* against spine-rigc 2.0.3: its `cli.ts` run by path without
|
|
243
|
+
* `@esotericsoftware/spine-core` beside it dies at import with one
|
|
244
|
+
* `error: Cannot find module '@esotericsoftware/spine-core' from …` line, and its
|
|
245
|
+
* core entry refuses `validate` with one `rigc validate: …` line, neither of
|
|
246
|
+
* which a gate filter reads. The last lines and not the first are the fallback
|
|
247
|
+
* because `rigc build` prints its header and inputs before it can stop. Empty
|
|
248
|
+
* only when the run printed nothing at all.
|
|
249
|
+
*/
|
|
250
|
+
export function causeLines(out: string): string[] {
|
|
251
|
+
const lines = out.split('\n').map((l) => l.trim()).filter((l) => l !== '');
|
|
252
|
+
const named = lines.filter((l) => CAUSE.test(l));
|
|
253
|
+
return named.length > 0 ? named : lines.slice(-3);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The two entries spine-rigc's launcher (`bin/rigc.cjs`, spine-rigc 2.0.0 and later) runs. */
|
|
257
|
+
export const RIGC_ENTRIES = ['cli.ts', 'cli_core.ts'] as const;
|
|
258
|
+
export type RigcEntry = (typeof RIGC_ENTRIES)[number];
|
|
259
|
+
|
|
260
|
+
/** The first spine-rigc whose `--version` names the entry that ran (its launcher's stderr line; spine-rigc 2.0.0's changelog, issue #1061 there). */
|
|
261
|
+
export const RIGC_ENTRY_VERSION = '2.0.0';
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Which rigc entry gated the build, as `check.json` records it: `cli.ts`, the
|
|
265
|
+
* spine-core round trip, with the runtime's version; `cli_core.ts`, rigc's own
|
|
266
|
+
* validator over its model document, with no runtime (`spine_core` null).
|
|
267
|
+
*/
|
|
268
|
+
export interface RigcEntryRecord {
|
|
269
|
+
entry: RigcEntry;
|
|
270
|
+
spine_core: string | null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The launcher's two `entry:` lines, whole, as spine-rigc 2.0.3's `bin/rigc.cjs` writes them. */
|
|
274
|
+
const ENTRY_FULL = /^entry: cli\.ts — @esotericsoftware\/spine-core (\S+) present$/;
|
|
275
|
+
const ENTRY_CORE = /^entry: cli_core\.ts — @esotericsoftware\/spine-core absent — /;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The entry a `rigc --version` run names. A run with no `entry:` line is a
|
|
279
|
+
* rigc older than {@link RIGC_ENTRY_VERSION}, or not rigc's launcher, and is
|
|
280
|
+
* refused `CHECK_RIGC_ENTRY`; an `entry:` line in neither form above is refused
|
|
281
|
+
* `CHECK_RIGC_ENTRY_READS`, naming the line — a third entry is not read as
|
|
282
|
+
* either of the two.
|
|
283
|
+
*/
|
|
284
|
+
export function readRigcEntry(versionOut: string): RigcEntryRecord {
|
|
285
|
+
const lines = versionOut.split('\n').map((l) => l.trim());
|
|
286
|
+
const line = lines.find((l) => l.startsWith('entry:'));
|
|
287
|
+
if (line === undefined) {
|
|
288
|
+
refuseIfAny([
|
|
289
|
+
{
|
|
290
|
+
code: 'CHECK_RIGC_ENTRY',
|
|
291
|
+
object: '`rigc --version`',
|
|
292
|
+
detail: `printed ${JSON.stringify(versionOut.trim())} and no \`entry:\` line; spine-rigc ${RIGC_ENTRY_VERSION} or later is required — its launcher names the entry that ran, and check.json records which one gated the build (\`bun install\` puts this package's own spine-rigc at node_modules/.bin/rigc)`,
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
const at = line as string;
|
|
297
|
+
const full = ENTRY_FULL.exec(at);
|
|
298
|
+
if (full !== null) return { entry: 'cli.ts', spine_core: full[1] };
|
|
299
|
+
if (ENTRY_CORE.test(at)) return { entry: 'cli_core.ts', spine_core: null };
|
|
300
|
+
refuseIfAny([
|
|
301
|
+
{
|
|
302
|
+
code: 'CHECK_RIGC_ENTRY_READS',
|
|
303
|
+
object: `rigc's entry line ${JSON.stringify(at)}`,
|
|
304
|
+
detail: 'reads as neither "entry: cli.ts — @esotericsoftware/spine-core <version> present" nor "entry: cli_core.ts — @esotericsoftware/spine-core absent — …"; one of the two is required',
|
|
305
|
+
},
|
|
306
|
+
]);
|
|
307
|
+
throw new Error('unreachable');
|
|
308
|
+
}
|
|
309
|
+
|
|
234
310
|
/**
|
|
235
311
|
* Where the `rigc` binary is: `node_modules/.bin/rigc` in `from` or any
|
|
236
312
|
* directory above it (a clone has it beside the package; an install has it in
|
|
@@ -427,14 +503,34 @@ export function readCheckInputs(rigDir: string, partsHome: string = rigDir): Che
|
|
|
427
503
|
// gate lines
|
|
428
504
|
// ---------------------------------------------------------------------------
|
|
429
505
|
|
|
430
|
-
/**
|
|
506
|
+
/**
|
|
507
|
+
* The `here:` line spine-rigc's core entry (`cli_core.ts`, 2.0.0 and later)
|
|
508
|
+
* prints after its assertion summary, saying which rules ran and that the
|
|
509
|
+
* spine-core round trip did not; `cli.ts` prints none.
|
|
510
|
+
*/
|
|
511
|
+
const HERE_LINE = /^\.\.\s+here: /;
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* `check_rig.py`'s filter for the build: summary, stats and pack lines, and
|
|
515
|
+
* anything that failed — and, under rigc's core entry, its `here:` line, so the
|
|
516
|
+
* gate file says which validator ran.
|
|
517
|
+
*/
|
|
431
518
|
export function buildGateLines(out: string): string[] {
|
|
432
|
-
return out
|
|
519
|
+
return out
|
|
520
|
+
.split('\n')
|
|
521
|
+
.filter((l) => (l.trim().startsWith('..') && (l.includes(' assertions: ') || l.includes('pages=') || l.includes('pack:') || HERE_LINE.test(l.trim()))) || l.includes('FAIL') || l.includes('compile error'));
|
|
433
522
|
}
|
|
434
523
|
|
|
435
|
-
/**
|
|
436
|
-
|
|
437
|
-
|
|
524
|
+
/**
|
|
525
|
+
* The rules a rigc run measured — every `PASS` and `FAIL` line's rule name, in
|
|
526
|
+
* the order printed, each once. A SKIP, a PROF line and a summary are not
|
|
527
|
+
* measurements. The selftest's `CH09` compares a build's set with
|
|
528
|
+
* `rigc validate --profile spine`'s on the same build.
|
|
529
|
+
*/
|
|
530
|
+
export function measuredRules(out: string): string[] {
|
|
531
|
+
const seen: string[] = [];
|
|
532
|
+
for (const m of out.matchAll(/^ {2}(?:PASS|FAIL) {2}([A-Z][A-Z0-9_]*)/gm)) if (!seen.includes(m[1])) seen.push(m[1]);
|
|
533
|
+
return seen;
|
|
438
534
|
}
|
|
439
535
|
|
|
440
536
|
const SUMMARY = /\((\d+) passed, (\d+) failed\)/;
|
|
@@ -466,7 +562,7 @@ const PACK = /^pack: (\S+) (\d+)x(\d+), (\d+) region\(s\), (\d+(?:\.\d+)?)% cove
|
|
|
466
562
|
|
|
467
563
|
/**
|
|
468
564
|
* Every pack line among the gate lines — a line that reads `pack:` once its
|
|
469
|
-
* `..` gutter is taken off. A pack line spine-rigc 1.5.1 would not print (an
|
|
565
|
+
* `..` gutter is taken off. A pack line spine-rigc 1.5.1 to 2.0.3 would not print (an
|
|
470
566
|
* unknown suffix, a missing field) is refused, `CHECK_PACK_LINE_READS`, naming
|
|
471
567
|
* the line and the form required: a line half-read would lose the one field
|
|
472
568
|
* that changed.
|
|
@@ -787,7 +883,8 @@ export const REPORTED_LINES = ['RECOMPOSITE_HOLES'] as const;
|
|
|
787
883
|
/** check.json — the reference's fields in its order, then the judgement lines, then PASS. */
|
|
788
884
|
export interface CheckFigures {
|
|
789
885
|
gate_spine_html_green: boolean;
|
|
790
|
-
|
|
886
|
+
/** Which rigc entry gated the build ({@link readRigcEntry}): provenance, not a bar. */
|
|
887
|
+
rigc_entry: RigcEntryRecord;
|
|
791
888
|
loop_max_diff: number;
|
|
792
889
|
seam_mean: number;
|
|
793
890
|
seam_px_over_40: number;
|
|
@@ -805,7 +902,6 @@ export interface CheckFigures {
|
|
|
805
902
|
export interface CheckReport {
|
|
806
903
|
figures: CheckFigures;
|
|
807
904
|
gateHtml: string[];
|
|
808
|
-
gateSpine: string[];
|
|
809
905
|
pack: PackLine[];
|
|
810
906
|
/** Opaque share (alpha > 0) of each packed page, by page file name. */
|
|
811
907
|
packOpaque: Array<{ page: string; share: number }>;
|
|
@@ -816,10 +912,17 @@ export interface CheckReport {
|
|
|
816
912
|
written: string[];
|
|
817
913
|
}
|
|
818
914
|
|
|
819
|
-
|
|
915
|
+
/**
|
|
916
|
+
* One `CHECK_RIGC_GREEN` per line that says why `rigc <what>` was not green:
|
|
917
|
+
* its FAIL and compile-error lines, or, when there are none, {@link causeLines}.
|
|
918
|
+
* A run that printed nothing is still one problem, never none.
|
|
919
|
+
*/
|
|
920
|
+
export function rigcFailed(what: string, call: RigcCall, lines: readonly string[]): Problem[] {
|
|
820
921
|
const named = lines.filter((l) => l.includes('FAIL') || l.includes('compile error'));
|
|
821
|
-
const quoted = named.length > 0 ? named
|
|
822
|
-
|
|
922
|
+
const quoted = named.length > 0 ? named.map((l) => l.trim()) : causeLines(call.out);
|
|
923
|
+
const object = `\`rigc ${what}\``;
|
|
924
|
+
if (quoted.length === 0) return [{ code: 'CHECK_RIGC_GREEN', object, detail: `exit ${call.status}, and it printed nothing` }];
|
|
925
|
+
return quoted.map((l) => ({ code: 'CHECK_RIGC_GREEN', object, detail: `exit ${call.status}: ${l}` }));
|
|
823
926
|
}
|
|
824
927
|
|
|
825
928
|
/**
|
|
@@ -834,7 +937,7 @@ function rigcFailed(what: string, call: RigcCall, lines: readonly string[]): Pro
|
|
|
834
937
|
*/
|
|
835
938
|
export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome: string = rigDir, pageEdges: PageEdges = DEFAULT_PAGE_EDGES): CheckReport {
|
|
836
939
|
const inp = readCheckInputs(rigDir, partsHome);
|
|
837
|
-
requireRigcVersion(rigc);
|
|
940
|
+
const rigcEntry = readRigcEntry(requireRigcVersion(rigc));
|
|
838
941
|
const out = resolve(outDir);
|
|
839
942
|
mkdirSync(out, { recursive: true });
|
|
840
943
|
const buildDir = join(out, 'build');
|
|
@@ -857,12 +960,10 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
857
960
|
refuseIfAny(packEdgeProblems(pack, pageEdges));
|
|
858
961
|
const packOpaque = pack.filter((p) => existsSync(join(buildDir, p.page))).map((p) => ({ page: p.page, share: opaqueShare(readPng(join(buildDir, p.page))) }));
|
|
859
962
|
|
|
860
|
-
// 2. validate
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
write('gate_spine.txt', `${gateSpine.join('\n')}\n`);
|
|
963
|
+
// 2. no second gate: `rigc validate --profile spine` of this build measured no rule the build's own two passes
|
|
964
|
+
// had not (14 of the build's 23 on the demo, rigc 1.5.1 and 2.0.3 alike; selftest CH09 holds it), and on rigc's
|
|
965
|
+
// core entry — an install without spine-core — `validate` is refused outright
|
|
864
966
|
const htmlGreen = gateGreen(build.status, gateHtml);
|
|
865
|
-
const spineGreen = gateGreen(validate.status, gateSpine);
|
|
866
967
|
|
|
867
968
|
// 3. the idle, the loop, the heat
|
|
868
969
|
const render = rigc(['render', '--candidate', buildDir, '--animation', 'idle', '--fps', String(IDLE_FPS), '--max', String(IDLE_MAX_PX), '--geometry', '--out', idleDir]);
|
|
@@ -978,7 +1079,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
978
1079
|
|
|
979
1080
|
const figures: CheckFigures = {
|
|
980
1081
|
gate_spine_html_green: htmlGreen,
|
|
981
|
-
|
|
1082
|
+
rigc_entry: rigcEntry,
|
|
982
1083
|
loop_max_diff: loop.max,
|
|
983
1084
|
seam_mean: round3(seam.mean),
|
|
984
1085
|
seam_px_over_40: seam.over40,
|
|
@@ -994,7 +1095,6 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
994
1095
|
};
|
|
995
1096
|
const barProblems: Problem[] = [];
|
|
996
1097
|
if (!htmlGreen) barProblems.push(...rigcFailed(packedBuildLabel(pageEdges), build, gateHtml));
|
|
997
|
-
if (!spineGreen) barProblems.push(...rigcFailed('validate --profile spine', validate, gateSpine));
|
|
998
1098
|
if (figures.loop_max_diff !== LOOP_MAX_BAR) {
|
|
999
1099
|
barProblems.push({
|
|
1000
1100
|
code: 'CHECK_LOOP_CLOSES',
|
|
@@ -1017,7 +1117,6 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
1017
1117
|
return {
|
|
1018
1118
|
figures,
|
|
1019
1119
|
gateHtml,
|
|
1020
|
-
gateSpine,
|
|
1021
1120
|
pack,
|
|
1022
1121
|
packOpaque,
|
|
1023
1122
|
idle: { frames: idle.frames.length, fps: idle.fps, duration: idle.duration, lastIndex: last.index, loopAt: { x: loop.x, y: loop.y } },
|
|
@@ -1455,10 +1554,12 @@ export function parseRigcVersion(out: string): [number, number, number] | null {
|
|
|
1455
1554
|
/**
|
|
1456
1555
|
* Refuse, before anything is built, a rigc older than
|
|
1457
1556
|
* {@link RIGC_GEOMETRY_VERSION}: its `render` has no `--geometry`, so the idle
|
|
1458
|
-
* render would fail on the flag after the build
|
|
1557
|
+
* render would fail on the flag after the build had run, and
|
|
1459
1558
|
* the refusal would name a flag rather than the version that lacks it.
|
|
1559
|
+
* Returns what `--version` printed, stdout then stderr, which is where
|
|
1560
|
+
* {@link readRigcEntry} reads the entry.
|
|
1460
1561
|
*/
|
|
1461
|
-
export function requireRigcVersion(rigc: RigcRunner):
|
|
1562
|
+
export function requireRigcVersion(rigc: RigcRunner): string {
|
|
1462
1563
|
const v = rigc(['--version']);
|
|
1463
1564
|
const got = v.status === 0 ? parseRigcVersion(v.out) : null;
|
|
1464
1565
|
const need = parseRigcVersion(RIGC_GEOMETRY_VERSION) as [number, number, number];
|
|
@@ -1472,6 +1573,7 @@ export function requireRigcVersion(rigc: RigcRunner): void {
|
|
|
1472
1573
|
},
|
|
1473
1574
|
]);
|
|
1474
1575
|
}
|
|
1576
|
+
return v.out;
|
|
1475
1577
|
}
|
|
1476
1578
|
|
|
1477
1579
|
export interface IdleGeometry {
|