spine-parts 0.5.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 +29 -13
- package/cli.ts +64 -30
- package/docs/AUTHORING.md +53 -12
- package/package.json +3 -2
- package/skills/spine-parts/SKILL.md +4 -2
- package/src/build.ts +62 -35
- package/src/check.ts +223 -35
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,17 +35,16 @@ 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
|
-
<code>pack: skeleton.png
|
|
40
|
+
<code>pack: skeleton.png 967x1338, 22 region(s), 91.2% covered, padding 2, page edges free</code><br/>
|
|
42
41
|
<code>--seam silhouette</code> repaired the navy blobs the default rule leaves on the white
|
|
43
42
|
blouse (recomposite error pixels 11,050 → 9,540); the default stays
|
|
44
43
|
<code>near-white</code>, the reference implementation's rule, so the examples stay
|
|
45
44
|
comparable with it. The loop is <code>spine-parts loop --palette</code>'s indexed APNG, 48 frames at
|
|
46
|
-
12 fps, 1,706,
|
|
45
|
+
12 fps, 1,706,520 bytes, one 256-entry palette at a measured error of max 57, mean 1.601
|
|
47
46
|
per channel over every frame (the lossless APNG beside it, the exactness record, is
|
|
48
|
-
13,645,
|
|
47
|
+
13,645,350 bytes; the GIF, at the same error, 1,812,238). Frame 29 (2.417 s) is the
|
|
49
48
|
closed eye: the blink holds for 0.084 s, one 12 fps frame rounded up, where the
|
|
50
49
|
reference implementation held 0.04 s and no frame of its loop showed the eyes shut
|
|
51
50
|
(issue #32); the painting is shown at half size, resampled and written
|
|
@@ -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` 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` |
|
|
@@ -99,12 +98,23 @@ artifact paths — here for `examples/sample`:
|
|
|
99
98
|
|
|
100
99
|
```
|
|
101
100
|
build: PASS — the packed atlas is the artifact; parts/ and rig/ are the intermediates it was made from
|
|
102
|
-
pack: skeleton.png
|
|
101
|
+
pack: skeleton.png 479x1166, 20 region(s), 93.4% covered, padding 2, page edges free; page opaque 53.9% (alpha > 0) — spineboy yardstick 1024x256, 40 region(s), 45.8% opaque (alpha > 0), a reference and not a bar
|
|
103
102
|
out/check/build/skeleton.json
|
|
104
103
|
out/check/build/skeleton.atlas
|
|
105
104
|
out/check/build/skeleton.png
|
|
106
105
|
```
|
|
107
106
|
|
|
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 and 479x1166, where a
|
|
109
|
+
power-of-two page is 1024x2048 and 512x2048, and covered goes from 56.3 % to 91.2 % and
|
|
110
|
+
from 49.7 % to 93.4 %. The cost is the one rigc's help states: *"region attachments
|
|
111
|
+
sampling within 1 LSB of the loose build rather than exactly"*. Measured on both
|
|
112
|
+
examples, every idle frame stays within 1 level of the power-of-two build's. The atlas says
|
|
113
|
+
`filter: Linear, Linear` and has no `repeat` line, so `--page-edges pot` is for a
|
|
114
|
+
consumer that mipmaps or repeats the page; it writes exactly the page builds wrote
|
|
115
|
+
before the flag existed. The measurements are in
|
|
116
|
+
[AUTHORING §5, *Page edges*](docs/AUTHORING.md#5-after-each-stage-what-to-read).
|
|
117
|
+
|
|
108
118
|
The packer is spine-rigc's, and no other: a packed region is a lossless copy, and an
|
|
109
119
|
atlas written by anything else would have no oracle behind it.
|
|
110
120
|
|
|
@@ -113,7 +123,7 @@ atlas written by anything else would have no oracle behind it.
|
|
|
113
123
|
| stage | tool | what it owns |
|
|
114
124
|
| --- | --- | --- |
|
|
115
125
|
| painting + See-through layers → parts and specs | **spine-parts** | the merge of two runs, the measurements, the rig spec and motion spec |
|
|
116
|
-
| 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 |
|
|
117
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` |
|
|
118
128
|
|
|
119
129
|
spine-parts never writes Spine data itself. Everything on disk under `check/build/`
|
|
@@ -242,8 +252,8 @@ agent skill.
|
|
|
242
252
|
| `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
|
|
243
253
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
|
|
244
254
|
| `propose … --from-config <config>` | draw and LINT the config's current bones |
|
|
245
|
-
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after spine-rigc's
|
|
246
|
-
| `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` |
|
|
247
257
|
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
248
258
|
| `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
249
259
|
|
|
@@ -308,13 +318,19 @@ the flag, puts 813 of `sample`'s 993 still-unprojected pixels and 879 of `demo`'
|
|
|
308
318
|
|
|
309
319
|
[Bun](https://bun.sh) 1.2 or later. `npm install -g spine-parts` installs the
|
|
310
320
|
`spine-parts` command; it hands off to Bun and says so in one sentence if Bun is not
|
|
311
|
-
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.
|
|
312
327
|
|
|
313
328
|
## Licence posture
|
|
314
329
|
|
|
315
|
-
spine-parts is MIT, and it depends on spine-rigc,
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
318
334
|
third-party term this package touches, See-through's included.
|
|
319
335
|
|
|
320
336
|
## Development
|
package/cli.ts
CHANGED
|
@@ -15,10 +15,9 @@ 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
|
-
import { findRigc, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
|
|
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';
|
|
22
21
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
23
22
|
import { type CharacterConfig, loadConfig, loadEarlyConfig } from './src/config.ts';
|
|
24
23
|
import { PartsError, problemLine } from './src/errors.ts';
|
|
@@ -98,6 +97,7 @@ usage:
|
|
|
98
97
|
printed when one was needed.
|
|
99
98
|
|
|
100
99
|
spine-parts rig --config <config.json> --parts <dir> --out <dir> [--idle-keys ctl|direct]
|
|
100
|
+
[--page-edges pot|free]
|
|
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,10 +110,11 @@ 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
|
|
114
|
-
|
|
115
|
-
images/*.png, rig.json,
|
|
116
|
-
|
|
113
|
+
through spine-rigc (profile spine-html, packed with --page-edges as for
|
|
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.
|
|
117
118
|
--idle-keys says where the idle's keys on a bone a mesh is weighted to go:
|
|
118
119
|
ctl (the default) keys a same-origin <bone>_ctl parent instead, which
|
|
119
120
|
passes A15_IDLE_NO_MESH_BONE_KEYS on any spine-rigc; direct keys the bone
|
|
@@ -121,18 +122,19 @@ usage:
|
|
|
121
122
|
spine-rigc 1.3.0 or later and makes A15 a SKIP that prints its cost (the
|
|
122
123
|
stage prints that SKIP line). The pose is the same to one level of float
|
|
123
124
|
rounding, and so is the per-frame mesh work (AUTHORING §5).
|
|
124
|
-
spine-parts check --rig <dir> --out <dir> [--parts <dir>]
|
|
125
|
+
spine-parts check --rig <dir> --out <dir> [--parts <dir>] [--page-edges pot|free]
|
|
125
126
|
Build, gate, render and measure a rig through spine-rigc's CLI (the rigc at
|
|
126
127
|
node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json
|
|
127
128
|
(with an "idle").
|
|
128
129
|
${PARTS_HOME_SENTENCE}.
|
|
129
130
|
Both are only read. Into --out:
|
|
130
|
-
build/ (rigc build --profile spine-html --pack
|
|
131
|
-
artifact), gate_spine-html.txt
|
|
131
|
+
build/ (rigc build --profile spine-html --pack --page-edges <value>: the
|
|
132
|
+
packed atlas is the artifact), gate_spine-html.txt (the gate lines
|
|
132
133
|
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
133
134
|
--geometry: the frames and geometry.json, the skinned vertices and
|
|
134
135
|
every bone's world transform per frame),
|
|
135
|
-
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
|
|
136
138
|
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
137
139
|
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
|
|
138
140
|
frame at t = duration) at max |d| 0. Then six judgement lines, each in
|
|
@@ -163,6 +165,16 @@ usage:
|
|
|
163
165
|
beside the spineboy yardstick (${SPINEBOY_YARDSTICK}), a reference and not
|
|
164
166
|
a bar. Exit 0 on PASS, 1 on FAIL — every FAIL line names the bar, the value
|
|
165
167
|
and the value required.
|
|
168
|
+
--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 and
|
|
170
|
+
479x1166, where pot writes 1024x2048 and 512x2048), at the cost rigc
|
|
171
|
+
states — region attachments sample within 1 LSB of the loose build
|
|
172
|
+
rather than exactly (measured on both examples: every idle frame within
|
|
173
|
+
1 level of the pot build's; AUTHORING §5, Page edges). pot is a power of two on both edges, for a consumer
|
|
174
|
+
that mipmaps or repeats the page (the atlas declares filter Linear,
|
|
175
|
+
Linear and no repeat). rigc's pack line ends ", page edges free" under
|
|
176
|
+
free; a line that disagrees with the value passed, or a pot page that is
|
|
177
|
+
not a power of two, is refused, CHECK_PACK_PAGE_EDGES.
|
|
166
178
|
|
|
167
179
|
spine-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
|
|
168
180
|
Encode a frame set rigc render wrote (its --out directory, or the set
|
|
@@ -247,7 +259,7 @@ usage:
|
|
|
247
259
|
|
|
248
260
|
spine-parts build --config <config.json> --source <painting.png> --full <dir|psd>
|
|
249
261
|
--head <dir|psd> --out <dir> [--seam near-white|silhouette]
|
|
250
|
-
[--project core|visible] [--loop]
|
|
262
|
+
[--project core|visible] [--page-edges pot|free] [--loop]
|
|
251
263
|
assemble, then rig, then check, in one process, each stage's own lines
|
|
252
264
|
printed under [assemble], [rig] and [check]; the first stage that refuses
|
|
253
265
|
stops the build with its own FAIL lines. The config must already carry
|
|
@@ -263,7 +275,8 @@ usage:
|
|
|
263
275
|
three artifact paths — skeleton .json, .atlas and the packed page: the
|
|
264
276
|
packed atlas is the result, the loose parts are the intermediate it was
|
|
265
277
|
made from. --seam defaults to ${DEFAULT_SEAM_RULE}, --project to
|
|
266
|
-
${DEFAULT_PROJECT_RULE} (both as for assemble)
|
|
278
|
+
${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES}
|
|
279
|
+
(as for check; both packed builds, rig's gate and check's artifact, take it).
|
|
267
280
|
|
|
268
281
|
spine-parts --version
|
|
269
282
|
spine-parts --help
|
|
@@ -384,18 +397,14 @@ function cmdSheet(args: string[]): number {
|
|
|
384
397
|
}
|
|
385
398
|
}
|
|
386
399
|
|
|
387
|
-
/** spine-rigc
|
|
388
|
-
function
|
|
389
|
-
|
|
400
|
+
/** `--page-edges`, or its default; a value spine-rigc does not take is a usage error naming the two it does. */
|
|
401
|
+
function pageEdgesOf(value: string | undefined): PageEdges | string {
|
|
402
|
+
const edges = value ?? DEFAULT_PAGE_EDGES;
|
|
403
|
+
return (PAGE_EDGES as readonly string[]).includes(edges) ? (edges as PageEdges) : `--page-edges ${edges}; one of ${PAGE_EDGES.join(', ')} is required`;
|
|
390
404
|
}
|
|
391
405
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return (args) => {
|
|
395
|
-
const r = spawnSync(process.execPath, [rigcCli(), ...args], { encoding: 'utf8', maxBuffer: 1 << 28 });
|
|
396
|
-
if (r.error !== undefined) return { status: 127, out: `could not start ${rigcCli()}: ${r.error.message}` };
|
|
397
|
-
return { status: r.status ?? 1, out: `${r.stdout ?? ''}${r.stderr ?? ''}` };
|
|
398
|
-
};
|
|
406
|
+
function isPageEdges(v: PageEdges | string): v is PageEdges {
|
|
407
|
+
return (PAGE_EDGES as readonly string[]).includes(v);
|
|
399
408
|
}
|
|
400
409
|
|
|
401
410
|
function cmdRig(args: string[]): number {
|
|
@@ -403,10 +412,11 @@ function cmdRig(args: string[]): number {
|
|
|
403
412
|
let partsDir: string | null = null;
|
|
404
413
|
let out: string | null = null;
|
|
405
414
|
let idleKeys: string | null = null;
|
|
415
|
+
let pageEdges: string | null = null;
|
|
406
416
|
for (let i = 0; i < args.length; i++) {
|
|
407
417
|
const flag = args[i];
|
|
408
418
|
const value = args[i + 1];
|
|
409
|
-
if (!['--config', '--parts', '--out', '--idle-keys'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
419
|
+
if (!['--config', '--parts', '--out', '--idle-keys', '--page-edges'].includes(flag)) return usage(`rig does not take "${flag}"`);
|
|
410
420
|
if (value === undefined) return usage(`${flag} needs a value`);
|
|
411
421
|
i++;
|
|
412
422
|
if (flag === '--config') config = value;
|
|
@@ -414,6 +424,9 @@ function cmdRig(args: string[]): number {
|
|
|
414
424
|
else if (flag === '--idle-keys') {
|
|
415
425
|
if (idleKeys !== null) return usage('--idle-keys is given twice');
|
|
416
426
|
idleKeys = value;
|
|
427
|
+
} else if (flag === '--page-edges') {
|
|
428
|
+
if (pageEdges !== null) return usage('--page-edges is given twice');
|
|
429
|
+
pageEdges = value;
|
|
417
430
|
} else out = value;
|
|
418
431
|
}
|
|
419
432
|
if (config === null) return usage('rig needs --config <config.json>');
|
|
@@ -421,9 +434,17 @@ function cmdRig(args: string[]): number {
|
|
|
421
434
|
if (out === null) return usage('rig needs --out <dir>');
|
|
422
435
|
const keys = idleKeys ?? DEFAULT_IDLE_KEYS;
|
|
423
436
|
if (!(IDLE_KEYS as readonly string[]).includes(keys)) return usage(`--idle-keys ${keys}; one of ${IDLE_KEYS.join(', ')} is required`);
|
|
437
|
+
const edges = pageEdgesOf(pageEdges ?? undefined);
|
|
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
|
+
}
|
|
424
445
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-rig-'));
|
|
425
446
|
try {
|
|
426
|
-
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys },
|
|
447
|
+
rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys, pageEdges: edges }, rigcRunner(bin), scratch, console.log);
|
|
427
448
|
return EXIT_OK;
|
|
428
449
|
} catch (err) {
|
|
429
450
|
return printRefusal(err);
|
|
@@ -432,7 +453,16 @@ function cmdRig(args: string[]): number {
|
|
|
432
453
|
}
|
|
433
454
|
}
|
|
434
455
|
|
|
435
|
-
/**
|
|
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
|
+
*/
|
|
436
466
|
function rigcRunner(bin: string): RigcRunner {
|
|
437
467
|
return (args) => {
|
|
438
468
|
const r = spawnSync(bin, [...args], { encoding: 'utf8', maxBuffer: 1 << 28 });
|
|
@@ -456,13 +486,15 @@ function flags(args: string[], known: readonly string[], command: string, option
|
|
|
456
486
|
}
|
|
457
487
|
|
|
458
488
|
function cmdCheck(args: string[]): number {
|
|
459
|
-
const f = flags(args, ['--rig', '--out'], 'check', ['--parts']);
|
|
489
|
+
const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--page-edges']);
|
|
460
490
|
if (typeof f === 'string') return usage(f);
|
|
461
491
|
const rig = f.get('--rig') as string;
|
|
462
492
|
const out = f.get('--out') as string;
|
|
493
|
+
const edges = pageEdgesOf(f.get('--page-edges'));
|
|
494
|
+
if (!isPageEdges(edges)) return usage(edges);
|
|
463
495
|
try {
|
|
464
496
|
const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
|
|
465
|
-
const r = checkStage({ rig, parts: f.get('--parts') ?? rig, out }, rigcRunner(bin), bin, console.log);
|
|
497
|
+
const r = checkStage({ rig, parts: f.get('--parts') ?? rig, out, pageEdges: edges }, rigcRunner(bin), bin, console.log);
|
|
466
498
|
return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
|
|
467
499
|
} catch (err) {
|
|
468
500
|
return printRefusal(err);
|
|
@@ -767,7 +799,7 @@ function cmdInputs(args: string[]): number {
|
|
|
767
799
|
function cmdBuild(args: string[]): number {
|
|
768
800
|
const flags = new Map<string, string>();
|
|
769
801
|
let loop = false;
|
|
770
|
-
const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project'];
|
|
802
|
+
const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges'];
|
|
771
803
|
for (let i = 0; i < args.length; i++) {
|
|
772
804
|
const flag = args[i];
|
|
773
805
|
if (flag === '--loop') {
|
|
@@ -787,6 +819,8 @@ function cmdBuild(args: string[]): number {
|
|
|
787
819
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
788
820
|
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
789
821
|
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
822
|
+
const edges = pageEdgesOf(flags.get('--page-edges'));
|
|
823
|
+
if (!isPageEdges(edges)) return usage(edges);
|
|
790
824
|
const [config, source, full, head, out] = ['--config', '--source', '--full', '--head', '--out'].map((f) => flags.get(f) as string);
|
|
791
825
|
let bin: string;
|
|
792
826
|
try {
|
|
@@ -797,8 +831,8 @@ function cmdBuild(args: string[]): number {
|
|
|
797
831
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-build-'));
|
|
798
832
|
try {
|
|
799
833
|
const r = build(
|
|
800
|
-
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop },
|
|
801
|
-
{ rig:
|
|
834
|
+
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges },
|
|
835
|
+
{ rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
|
|
802
836
|
console.log,
|
|
803
837
|
);
|
|
804
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
|
|
|
@@ -279,9 +279,11 @@ 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,350 B lossless, 1,706,520 B indexed and 1,812,238 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
|
-
construction and the size is the difference.
|
|
284
|
+
construction and the size is the difference. That is `--seam silhouette` (the README's
|
|
285
|
+
animation); with `--page-edges pot` the same build writes 13,645,519, 1,706,785 and
|
|
286
|
+
1,812,625 B at the same error. The loop shows the blink closed: the
|
|
285
287
|
eyes' hold is at least one 12 fps frame (below), so whatever `motion.blink.t` is, one
|
|
286
288
|
idle frame lands inside it, and `check.json`'s `BLINK_NO_HOLE.idle_frames_closed`
|
|
287
289
|
names it — `[29]` on both examples.
|
|
@@ -294,6 +296,38 @@ outside the figure. On the demo it lowered recomposite error pixels from 11,050
|
|
|
294
296
|
9,540 and changed no check bar; on the sample it changed nothing. The default stays
|
|
295
297
|
the reference's so the examples stay comparable with it.
|
|
296
298
|
|
|
299
|
+
**Page edges.** `rig`, `check` and `build` pack through `rigc build --pack
|
|
300
|
+
--page-edges <v>`, and hand the value to rigc verbatim. The default is `free`,
|
|
301
|
+
the least-area page the parts need. `--page-edges pot` is the opt-out: a power of
|
|
302
|
+
two on both edges, which is the page every build wrote before this flag. The pack
|
|
303
|
+
line says which one ran, because rigc ends it with `, page edges free` under `free`.
|
|
304
|
+
Measured 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:
|
|
307
|
+
|
|
308
|
+
| build | page | covered | page opaque | page / figure |
|
|
309
|
+
| --- | --- | --- | --- | --- |
|
|
310
|
+
| demo, `pot` | 1024x2048 | 56.3 % | 36.8 % | 2.651 |
|
|
311
|
+
| demo, `free` | 967x1338 | 91.2 % | 59.7 % | 1.636 |
|
|
312
|
+
| sample, `pot` | 512x2048 | 49.7 % | 28.7 % | 3.294 |
|
|
313
|
+
| sample, `free` | 479x1166 | 93.4 % | 53.9 % | 1.755 |
|
|
314
|
+
|
|
315
|
+
rigc's help states the cost: *"a smaller page, at the cost of region attachments
|
|
316
|
+
sampling within 1 LSB of the loose build rather than exactly"*. On these builds
|
|
317
|
+
every one of the 49 idle frames differs from the `pot` build, by at most 1 level
|
|
318
|
+
in any channel:
|
|
319
|
+
|
|
320
|
+
| example | idle frames that differ | pixels that differ (all 49 frames) | max \|d\| |
|
|
321
|
+
| --- | --- | --- | --- |
|
|
322
|
+
| demo | 49 of 49 | 2,373 | 1 |
|
|
323
|
+
| sample | 49 of 49 | 651 | 1 |
|
|
324
|
+
|
|
325
|
+
Only one check figure moved. It has no bar: sample's reported screen-space face
|
|
326
|
+
mean in `STILL_REGIONS_DARK`, 11.475 under `pot` and 11.472 under `free`. Every
|
|
327
|
+
barred figure and every status is the same under both. The atlas rigc writes says
|
|
328
|
+
`filter: Linear, Linear` and has no `repeat` line, so choose `pot` for a consumer
|
|
329
|
+
that mipmaps or repeats the page.
|
|
330
|
+
|
|
297
331
|
`--project core` (the default) is the reference's projection rule: a layer takes the
|
|
298
332
|
painting's pixel only inside its top-most `alpha >= 250` area eroded by a 5x5 square,
|
|
299
333
|
so a part a few pixels wide (a lash, a brow, an iris) takes none. `--project visible`
|
|
@@ -325,8 +359,8 @@ differ by at most one level in a channel. The default stays `ctl`: the examples'
|
|
|
325
359
|
`expected/rig.json` and `motion.json` are the reference's output, and the
|
|
326
360
|
declaration is in the rig spec only, so `rigc validate <build> --profile spine-html`
|
|
327
361
|
run on a `direct` build (it has no rig spec to read) refuses A15 once per keyed
|
|
328
|
-
bone. `
|
|
329
|
-
|
|
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`).
|
|
330
364
|
|
|
331
365
|
**Patching a hole no layer holds.** When an `uncovered hole` stays large after the
|
|
332
366
|
plan is right — both See-through runs dropped a piece of the figure, red in
|
|
@@ -556,7 +590,8 @@ See-through with another seed.
|
|
|
556
590
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
557
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/` |
|
|
558
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) |
|
|
559
|
-
| `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^
|
|
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` |
|
|
560
595
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
561
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 |
|
|
562
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 |
|
|
@@ -569,6 +604,8 @@ See-through with another seed.
|
|
|
569
604
|
| `CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other |
|
|
570
605
|
| `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 |
|
|
571
606
|
| `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>` with `, page edges free` after it or nothing; the line is quoted | a rigc this package does not know the output of; report it, with `rigc --version` |
|
|
608
|
+
| `CHECK_PACK_PAGE_EDGES` | the pack line disagrees with the `--page-edges` the build was run with — its `, page edges free` suffix is there under `pot` or missing under `free`, or a `pot` page is not a power of two on both edges | a rigc problem; report it, with `rigc --version` |
|
|
572
609
|
| `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
|
|
573
610
|
|
|
574
611
|
## 7. The bars `check` enforces, and what only an eye answers today
|
|
@@ -577,14 +614,17 @@ See-through with another seed.
|
|
|
577
614
|
implementation's bars):
|
|
578
615
|
|
|
579
616
|
- `gate_spine-html.txt`: every summary line `(N passed, 0 failed)` — the packed build
|
|
580
|
-
under the `spine-html` profile
|
|
581
|
-
|
|
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);
|
|
582
622
|
- **seam**: the setup-pose render against the flat composite of `parts/`: mean
|
|
583
623
|
max-channel |d| ≤ 1.0 of 255, and at most 50 pixels over 40;
|
|
584
624
|
- **loop**: idle frame 0 against the frame at `t = duration`: max |d| exactly 0.
|
|
585
625
|
|
|
586
|
-
On the examples: `sample` 23/23
|
|
587
|
-
`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.
|
|
588
628
|
|
|
589
629
|
A green gate cannot see a wrong animation, so `check` also writes six **judgement
|
|
590
630
|
lines** (issue #11; `TEXTURE_STRETCH`, issue #31), each a key of `check.json` and a console line
|
|
@@ -676,7 +716,8 @@ What each figure is, and is not:
|
|
|
676
716
|
- **The face is measured in the head's frame, and its bar is the model's (issue #33).**
|
|
677
717
|
Until #33 the face half read the screen-space heat map, where the head's intended
|
|
678
718
|
roll lights the outline on both examples (16.988 and 11.475, the figures now
|
|
679
|
-
reported as `screen_heat_mean
|
|
719
|
+
reported as `screen_heat_mean`; sample's reads 11.472 since the page edges
|
|
720
|
+
default to `free`, *Page edges* in §5), so its ceiling was twice the worse example,
|
|
680
721
|
33.976 — a ceiling a smooth fixture face sliding two rig pixels stayed under (24.8;
|
|
681
722
|
eight pixels measured 85.5). The judgement means *nothing moves on the face that
|
|
682
723
|
the head does not move*, so the heat is now taken after each frame's face region is
|
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, JUDGEMENT_LINES, type JudgementLine, 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,31 +180,33 @@ 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
|
-
* the
|
|
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
|
-
function gateThroughRigc(out: RigOutput, texts: Array<[string, string]>, rigc: RigcRunner, scratch: string): GateRun[] {
|
|
202
|
+
function gateThroughRigc(out: RigOutput, texts: Array<[string, string]>, rigc: RigcRunner, scratch: string, edges: PageEdges): GateRun[] {
|
|
192
203
|
rmSync(scratch, { recursive: true, force: true });
|
|
193
204
|
try {
|
|
194
205
|
mkdirSync(join(scratch, 'images'), { recursive: true });
|
|
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
|
-
'build', '--rig', join(scratch, 'rig.json'), '--motion', join(scratch, 'motion.json'), '--out', build, '--profile', 'spine-html', '--pack',
|
|
200
|
-
]);
|
|
201
|
-
if (html.status !== 0) return [html];
|
|
202
|
-
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)])];
|
|
203
210
|
} finally {
|
|
204
211
|
rmSync(scratch, { recursive: true, force: true });
|
|
205
212
|
}
|
|
@@ -212,9 +219,11 @@ export interface RigStageInput {
|
|
|
212
219
|
out: string;
|
|
213
220
|
/** Where the idle's keys on mesh-driving bones go; `ctl` when absent. See `IDLE_KEYS` in `src/rig.ts`. */
|
|
214
221
|
idleKeys?: IdleKeys;
|
|
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
|
+
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'));
|
|
@@ -249,7 +258,7 @@ export function rigStage(input: RigStageInput, rigc: RigcRunner, scratch: string
|
|
|
249
258
|
? ` idle keys ctl: ${rig.meshKeyed.length} mesh-driving bone(s) keyed by the idle, each keyed through a same-origin <bone>_ctl parent`
|
|
250
259
|
: ` 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'}`,
|
|
251
260
|
);
|
|
252
|
-
const gate = gateThroughRigc(rig, texts, rigc, scratch);
|
|
261
|
+
const gate = gateThroughRigc(rig, texts, rigc, scratch, input.pageEdges ?? DEFAULT_PAGE_EDGES);
|
|
253
262
|
for (const g of gate) {
|
|
254
263
|
log(` rigc ${g.label}: exit ${g.status}`);
|
|
255
264
|
for (const l of g.lines) log(` ${l.trim()}`);
|
|
@@ -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
|
}
|
|
@@ -280,6 +289,8 @@ export interface CheckStageInput {
|
|
|
280
289
|
/** The directory holding parts.json and parts/; the rig directory when they sit there. */
|
|
281
290
|
parts: string;
|
|
282
291
|
out: string;
|
|
292
|
+
/** The packed build's `--page-edges`; {@link DEFAULT_PAGE_EDGES} when absent. */
|
|
293
|
+
pageEdges?: PageEdges;
|
|
283
294
|
}
|
|
284
295
|
|
|
285
296
|
/** The pack line as it is printed: rigc's own, then the page's opaque share beside the spineboy yardstick. */
|
|
@@ -314,12 +325,13 @@ export function judgementLine(name: string, line: JudgementLine | ReportedLine):
|
|
|
314
325
|
export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string, log: Log): CheckReport {
|
|
315
326
|
const v = rigc(['--version']);
|
|
316
327
|
log(`spine-parts check: ${input.rig} -> ${input.out}`);
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
log(
|
|
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}`}`);
|
|
331
|
+
const edges = input.pageEdges ?? DEFAULT_PAGE_EDGES;
|
|
332
|
+
const r = runCheck(input.rig, input.out, rigc, input.parts, edges);
|
|
333
|
+
log(` gate spine-html (rigc ${packedBuildLabel(edges)}), verbatim:`);
|
|
320
334
|
for (const l of r.gateHtml) log(l);
|
|
321
|
-
log(' gate spine (rigc validate build --profile spine), verbatim:');
|
|
322
|
-
for (const l of r.gateSpine) log(l);
|
|
323
335
|
for (const l of packLines(r)) log(` ${l}`);
|
|
324
336
|
const fig = r.figures;
|
|
325
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)`);
|
|
@@ -329,7 +341,7 @@ export function checkStage(input: CheckStageInput, rigc: RigcRunner, bin: string
|
|
|
329
341
|
);
|
|
330
342
|
for (const name of JUDGEMENT_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
331
343
|
for (const name of REPORTED_LINES) log(` ${judgementLine(name, fig[name])}`);
|
|
332
|
-
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`})`);
|
|
333
345
|
log(` wrote ${r.written.map((w) => join(input.out, w)).join(', ')}, ${join(input.out, 'build')}/, ${join(input.out, 'idle_frames')}/`);
|
|
334
346
|
for (const p of r.problems) log(` FAIL ${problemLine(p)}`);
|
|
335
347
|
log(fig.PASS ? 'check: PASS' : `check: FAIL — ${r.problems.length} bar(s) not met`);
|
|
@@ -427,9 +439,16 @@ export interface BuildInput {
|
|
|
427
439
|
seam: SeamRule;
|
|
428
440
|
project: ProjectRule;
|
|
429
441
|
loop: boolean;
|
|
442
|
+
/** `--page-edges` for both packed builds, the rig stage's gate and the check's artifact. */
|
|
443
|
+
pageEdges: PageEdges;
|
|
430
444
|
}
|
|
431
445
|
|
|
432
|
-
/**
|
|
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
|
+
*/
|
|
433
452
|
export interface BuildRunners {
|
|
434
453
|
rig: RigcRunner;
|
|
435
454
|
check: RigcRunner;
|
|
@@ -452,20 +471,28 @@ export interface BuildResult {
|
|
|
452
471
|
artifact: string[];
|
|
453
472
|
}
|
|
454
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
|
+
|
|
455
482
|
/** The three artifact files of a packed build, each refused by name when absent. */
|
|
456
|
-
function artifactPaths(buildDir: string,
|
|
483
|
+
export function artifactPaths(buildDir: string, pack: readonly PackLine[]): string[] {
|
|
457
484
|
const problems: Problem[] = [];
|
|
458
485
|
const names = existsSync(buildDir) ? readdirSync(buildDir).sort() : [];
|
|
459
|
-
const json = names.filter((n) => n.endsWith('.json'));
|
|
486
|
+
const json = names.filter((n) => n.endsWith('.json') && n !== RIGC_MODEL_DOCUMENT);
|
|
460
487
|
const atlas = names.filter((n) => n.endsWith('.atlas'));
|
|
461
|
-
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}` });
|
|
462
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` });
|
|
463
|
-
if (
|
|
464
|
-
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) {
|
|
465
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` });
|
|
466
493
|
}
|
|
467
494
|
if (problems.length > 0) throw new PartsError(problems);
|
|
468
|
-
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))];
|
|
469
496
|
}
|
|
470
497
|
|
|
471
498
|
/**
|
|
@@ -487,7 +514,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
487
514
|
const out = input.out;
|
|
488
515
|
mkdirSync(out, { recursive: true });
|
|
489
516
|
for (const p of BUILD_OWNS) rmSync(join(out, p), { recursive: true, force: true });
|
|
490
|
-
log(`spine-parts build: ${input.config} -> ${out}, seam rule ${input.seam}, projection rule ${input.project}${input.loop ? ', with the idle loop' : ''}`);
|
|
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' : ''}`);
|
|
491
518
|
|
|
492
519
|
try {
|
|
493
520
|
// build runs rig next, which needs the whole config, so the full loader is
|
|
@@ -505,14 +532,14 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
505
532
|
}
|
|
506
533
|
|
|
507
534
|
try {
|
|
508
|
-
rigStage({ config: input.config, parts: out, out: join(out, 'rig') }, run.rig, run.scratch, prefixed('rig'));
|
|
535
|
+
rigStage({ config: input.config, parts: out, out: join(out, 'rig'), pageEdges: input.pageEdges }, run.rig, run.scratch, prefixed('rig'));
|
|
509
536
|
} catch (err) {
|
|
510
537
|
return refused('rig', err);
|
|
511
538
|
}
|
|
512
539
|
|
|
513
540
|
let report: CheckReport;
|
|
514
541
|
try {
|
|
515
|
-
report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check') }, run.check, run.checkBin, prefixed('check'));
|
|
542
|
+
report = checkStage({ rig: join(out, 'rig'), parts: out, out: join(out, 'check'), pageEdges: input.pageEdges }, run.check, run.checkBin, prefixed('check'));
|
|
516
543
|
} catch (err) {
|
|
517
544
|
return refused('check', err);
|
|
518
545
|
}
|
|
@@ -537,7 +564,7 @@ export function build(input: BuildInput, run: BuildRunners, log: Log): BuildResu
|
|
|
537
564
|
|
|
538
565
|
let artifact: string[];
|
|
539
566
|
try {
|
|
540
|
-
artifact = artifactPaths(join(out, 'check', 'build'), report);
|
|
567
|
+
artifact = artifactPaths(join(out, 'check', 'build'), report.pack);
|
|
541
568
|
} catch (err) {
|
|
542
569
|
return refused('artifact', err, report);
|
|
543
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
|
|
@@ -23,9 +25,8 @@
|
|
|
23
25
|
*
|
|
24
26
|
* | output | what |
|
|
25
27
|
* | --- | --- |
|
|
26
|
-
* | `build/` | `rigc build --profile spine-html --pack` — 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 |
|
|
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 |
|
|
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 |
|
|
@@ -184,6 +185,40 @@ export const GEOMETRY_SPEC = 'rigc-geometry/1';
|
|
|
184
185
|
/** Issue #2's yardstick, measured on the Spine example export `spineboy.png` (alpha > 0). */
|
|
185
186
|
export const SPINEBOY_YARDSTICK = '1024x256, 40 region(s), 45.8% opaque (alpha > 0)';
|
|
186
187
|
|
|
188
|
+
/**
|
|
189
|
+
* What a packed page's edges may be: spine-rigc's `build --pack --page-edges`,
|
|
190
|
+
* handed to it verbatim (the flag is in spine-rigc's CLI since 1.4.0, the
|
|
191
|
+
* floor `check` already requires; 1.5.0 made `free` search every width).
|
|
192
|
+
* `pot` is a power of two on both edges; `free` is the least-area page the
|
|
193
|
+
* placement needs, at the cost rigc's help states: region attachments sample
|
|
194
|
+
* within 1 LSB of the loose build rather than exactly.
|
|
195
|
+
*
|
|
196
|
+
* `free` is the default here although rigc's own default is `pot`: the packed
|
|
197
|
+
* page is this package's final artifact, and on the two public examples `free`
|
|
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.0.3 alike. The cost was
|
|
200
|
+
* measured on the same two builds. Each of the 49 idle frames differs from the
|
|
201
|
+
* `pot` build's, by at most 1 level in any channel (demo 2,373 px, sample 651
|
|
202
|
+
* px over all 49). On sample that moves STILL_REGIONS_DARK's reported
|
|
203
|
+
* screen-space face mean from 11.475 to 11.472; no bar or status moves. The
|
|
204
|
+
* atlas rigc writes says `filter: Linear, Linear` and has no `repeat` line, so
|
|
205
|
+
* a consumer that mipmaps or repeats the page itself is the one that wants
|
|
206
|
+
* `pot`. AUTHORING §5, *Page edges*, holds the tables.
|
|
207
|
+
*/
|
|
208
|
+
export const PAGE_EDGES = ['pot', 'free'] as const;
|
|
209
|
+
export type PageEdges = (typeof PAGE_EDGES)[number];
|
|
210
|
+
export const DEFAULT_PAGE_EDGES: PageEdges = 'free';
|
|
211
|
+
|
|
212
|
+
/** The packed build's rigc arguments after `--out <dir>`, as every stage that packs passes them. */
|
|
213
|
+
export function packedBuildArgs(edges: PageEdges): string[] {
|
|
214
|
+
return ['--profile', 'spine-html', '--pack', '--page-edges', edges];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The packed build as a label: the rigc command line that was run, after `rigc`. */
|
|
218
|
+
export function packedBuildLabel(edges: PageEdges): string {
|
|
219
|
+
return `build ${packedBuildArgs(edges).join(' ')}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
187
222
|
// ---------------------------------------------------------------------------
|
|
188
223
|
// the rigc process, injected
|
|
189
224
|
// ---------------------------------------------------------------------------
|
|
@@ -197,6 +232,81 @@ export interface RigcCall {
|
|
|
197
232
|
|
|
198
233
|
export type RigcRunner = (args: readonly string[]) => RigcCall;
|
|
199
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
|
+
|
|
200
310
|
/**
|
|
201
311
|
* Where the `rigc` binary is: `node_modules/.bin/rigc` in `from` or any
|
|
202
312
|
* directory above it (a clone has it beside the package; an install has it in
|
|
@@ -393,14 +503,34 @@ export function readCheckInputs(rigDir: string, partsHome: string = rigDir): Che
|
|
|
393
503
|
// gate lines
|
|
394
504
|
// ---------------------------------------------------------------------------
|
|
395
505
|
|
|
396
|
-
/**
|
|
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
|
+
*/
|
|
397
518
|
export function buildGateLines(out: string): string[] {
|
|
398
|
-
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'));
|
|
399
522
|
}
|
|
400
523
|
|
|
401
|
-
/**
|
|
402
|
-
|
|
403
|
-
|
|
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;
|
|
404
534
|
}
|
|
405
535
|
|
|
406
536
|
const SUMMARY = /\((\d+) passed, (\d+) failed\)/;
|
|
@@ -423,19 +553,68 @@ export interface PackLine {
|
|
|
423
553
|
regions: number;
|
|
424
554
|
coveredPct: number;
|
|
425
555
|
padding: number;
|
|
556
|
+
/** `free` when the line ends `, page edges free`, `pot` when it carries no suffix — spine-rigc prints the suffix exactly when it packed under `--page-edges free` (its `cli.ts`, the pack line). */
|
|
557
|
+
pageEdges: PageEdges;
|
|
426
558
|
}
|
|
427
559
|
|
|
428
|
-
|
|
560
|
+
/** 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)?$/;
|
|
429
562
|
|
|
563
|
+
/**
|
|
564
|
+
* Every pack line among the gate lines — a line that reads `pack:` once its
|
|
565
|
+
* `..` gutter is taken off. A pack line spine-rigc 1.5.1 to 2.0.3 would not print (an
|
|
566
|
+
* unknown suffix, a missing field) is refused, `CHECK_PACK_LINE_READS`, naming
|
|
567
|
+
* the line and the form required: a line half-read would lose the one field
|
|
568
|
+
* that changed.
|
|
569
|
+
*/
|
|
430
570
|
export function parsePackLines(lines: readonly string[]): PackLine[] {
|
|
431
571
|
const out: PackLine[] = [];
|
|
572
|
+
const problems: Problem[] = [];
|
|
432
573
|
for (const l of lines) {
|
|
433
|
-
const
|
|
434
|
-
if (
|
|
574
|
+
const text = l.trim().replace(/^\.\.\s+/, '');
|
|
575
|
+
if (!text.startsWith('pack:')) continue;
|
|
576
|
+
const m = PACK.exec(text);
|
|
577
|
+
if (m === null) {
|
|
578
|
+
problems.push({
|
|
579
|
+
code: 'CHECK_PACK_LINE_READS',
|
|
580
|
+
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>", with ", page edges free" after it or nothing; that form is required',
|
|
582
|
+
});
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
out.push({ line: text, page: m[1], width: Number(m[2]), height: Number(m[3]), regions: Number(m[4]), coveredPct: Number(m[5]), padding: Number(m[6]), pageEdges: m[7] === undefined ? 'pot' : 'free' });
|
|
435
586
|
}
|
|
587
|
+
refuseIfAny(problems);
|
|
436
588
|
return out;
|
|
437
589
|
}
|
|
438
590
|
|
|
591
|
+
function powerOfTwo(n: number): boolean {
|
|
592
|
+
return Number.isInteger(n) && n > 0 && (n & (n - 1)) === 0;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Where a pack line disagrees with the `--page-edges` the build was given:
|
|
597
|
+
* the line's own suffix must say the same, and a `pot` page must be a power of
|
|
598
|
+
* two on both edges, as rigc's help defines it. One problem per disagreement,
|
|
599
|
+
* `CHECK_PACK_PAGE_EDGES`; empty is agreement.
|
|
600
|
+
*/
|
|
601
|
+
export function packEdgeProblems(pack: readonly PackLine[], edges: PageEdges): Problem[] {
|
|
602
|
+
const problems: Problem[] = [];
|
|
603
|
+
for (const p of pack) {
|
|
604
|
+
const object = `packed page ${p.page}`;
|
|
605
|
+
if (p.pageEdges !== edges) {
|
|
606
|
+
problems.push({
|
|
607
|
+
code: 'CHECK_PACK_PAGE_EDGES',
|
|
608
|
+
object,
|
|
609
|
+
detail: `rigc's pack line says page edges ${p.pageEdges} (${p.pageEdges === 'free' ? 'it ends ", page edges free"' : 'no ", page edges free" suffix'}); the build was run with --page-edges ${edges}, so page edges ${edges} is required`,
|
|
610
|
+
});
|
|
611
|
+
} else if (edges === 'pot' && !(powerOfTwo(p.width) && powerOfTwo(p.height))) {
|
|
612
|
+
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
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return problems;
|
|
616
|
+
}
|
|
617
|
+
|
|
439
618
|
/** Share of a page's pixels with alpha above 0 — the measure issue #2's spineboy figure is in. rigc's "covered" is region rectangles, not pixels. */
|
|
440
619
|
export function opaqueShare(r: Raster): number {
|
|
441
620
|
let n = 0;
|
|
@@ -704,7 +883,8 @@ export const REPORTED_LINES = ['RECOMPOSITE_HOLES'] as const;
|
|
|
704
883
|
/** check.json — the reference's fields in its order, then the judgement lines, then PASS. */
|
|
705
884
|
export interface CheckFigures {
|
|
706
885
|
gate_spine_html_green: boolean;
|
|
707
|
-
|
|
886
|
+
/** Which rigc entry gated the build ({@link readRigcEntry}): provenance, not a bar. */
|
|
887
|
+
rigc_entry: RigcEntryRecord;
|
|
708
888
|
loop_max_diff: number;
|
|
709
889
|
seam_mean: number;
|
|
710
890
|
seam_px_over_40: number;
|
|
@@ -722,7 +902,6 @@ export interface CheckFigures {
|
|
|
722
902
|
export interface CheckReport {
|
|
723
903
|
figures: CheckFigures;
|
|
724
904
|
gateHtml: string[];
|
|
725
|
-
gateSpine: string[];
|
|
726
905
|
pack: PackLine[];
|
|
727
906
|
/** Opaque share (alpha > 0) of each packed page, by page file name. */
|
|
728
907
|
packOpaque: Array<{ page: string; share: number }>;
|
|
@@ -733,10 +912,17 @@ export interface CheckReport {
|
|
|
733
912
|
written: string[];
|
|
734
913
|
}
|
|
735
914
|
|
|
736
|
-
|
|
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[] {
|
|
737
921
|
const named = lines.filter((l) => l.includes('FAIL') || l.includes('compile error'));
|
|
738
|
-
const quoted = named.length > 0 ? named
|
|
739
|
-
|
|
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}` }));
|
|
740
926
|
}
|
|
741
927
|
|
|
742
928
|
/**
|
|
@@ -745,11 +931,13 @@ function rigcFailed(what: string, call: RigcCall, lines: readonly string[]): Pro
|
|
|
745
931
|
* gate file is written first, so the refusal's evidence is on disk. Otherwise
|
|
746
932
|
* it measures everything, writes every output including `check.json`, and
|
|
747
933
|
* returns the report; `report.problems` names each bar that was not met, and
|
|
748
|
-
* `figures.PASS` is true exactly when it is empty.
|
|
934
|
+
* `figures.PASS` is true exactly when it is empty. `pageEdges` reaches rigc's
|
|
935
|
+
* `--page-edges` verbatim, and a pack line that disagrees with it is refused
|
|
936
|
+
* (`CHECK_PACK_PAGE_EDGES`).
|
|
749
937
|
*/
|
|
750
|
-
export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome: string = rigDir): CheckReport {
|
|
938
|
+
export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, partsHome: string = rigDir, pageEdges: PageEdges = DEFAULT_PAGE_EDGES): CheckReport {
|
|
751
939
|
const inp = readCheckInputs(rigDir, partsHome);
|
|
752
|
-
requireRigcVersion(rigc);
|
|
940
|
+
const rigcEntry = readRigcEntry(requireRigcVersion(rigc));
|
|
753
941
|
const out = resolve(outDir);
|
|
754
942
|
mkdirSync(out, { recursive: true });
|
|
755
943
|
const buildDir = join(out, 'build');
|
|
@@ -764,19 +952,18 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
764
952
|
};
|
|
765
953
|
|
|
766
954
|
// 1. build, packed, under the policy profile
|
|
767
|
-
const build = rigc(['build', '--rig', inp.rigPath, '--motion', inp.motionPath, '--out', buildDir,
|
|
955
|
+
const build = rigc(['build', '--rig', inp.rigPath, '--motion', inp.motionPath, '--out', buildDir, ...packedBuildArgs(pageEdges)]);
|
|
768
956
|
const gateHtml = buildGateLines(build.out);
|
|
769
957
|
write('gate_spine-html.txt', `${gateHtml.join('\n')}\n`);
|
|
770
|
-
if (build.status !== 0) refuseIfAny(rigcFailed(
|
|
958
|
+
if (build.status !== 0) refuseIfAny(rigcFailed(packedBuildLabel(pageEdges), build, gateHtml));
|
|
771
959
|
const pack = parsePackLines(gateHtml);
|
|
960
|
+
refuseIfAny(packEdgeProblems(pack, pageEdges));
|
|
772
961
|
const packOpaque = pack.filter((p) => existsSync(join(buildDir, p.page))).map((p) => ({ page: p.page, share: opaqueShare(readPng(join(buildDir, p.page))) }));
|
|
773
962
|
|
|
774
|
-
// 2. validate
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
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
|
|
778
966
|
const htmlGreen = gateGreen(build.status, gateHtml);
|
|
779
|
-
const spineGreen = gateGreen(validate.status, gateSpine);
|
|
780
967
|
|
|
781
968
|
// 3. the idle, the loop, the heat
|
|
782
969
|
const render = rigc(['render', '--candidate', buildDir, '--animation', 'idle', '--fps', String(IDLE_FPS), '--max', String(IDLE_MAX_PX), '--geometry', '--out', idleDir]);
|
|
@@ -892,7 +1079,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
892
1079
|
|
|
893
1080
|
const figures: CheckFigures = {
|
|
894
1081
|
gate_spine_html_green: htmlGreen,
|
|
895
|
-
|
|
1082
|
+
rigc_entry: rigcEntry,
|
|
896
1083
|
loop_max_diff: loop.max,
|
|
897
1084
|
seam_mean: round3(seam.mean),
|
|
898
1085
|
seam_px_over_40: seam.over40,
|
|
@@ -907,8 +1094,7 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
907
1094
|
PASS: false,
|
|
908
1095
|
};
|
|
909
1096
|
const barProblems: Problem[] = [];
|
|
910
|
-
if (!htmlGreen) barProblems.push(...rigcFailed(
|
|
911
|
-
if (!spineGreen) barProblems.push(...rigcFailed('validate --profile spine', validate, gateSpine));
|
|
1097
|
+
if (!htmlGreen) barProblems.push(...rigcFailed(packedBuildLabel(pageEdges), build, gateHtml));
|
|
912
1098
|
if (figures.loop_max_diff !== LOOP_MAX_BAR) {
|
|
913
1099
|
barProblems.push({
|
|
914
1100
|
code: 'CHECK_LOOP_CLOSES',
|
|
@@ -931,7 +1117,6 @@ export function runCheck(rigDir: string, outDir: string, rigc: RigcRunner, parts
|
|
|
931
1117
|
return {
|
|
932
1118
|
figures,
|
|
933
1119
|
gateHtml,
|
|
934
|
-
gateSpine,
|
|
935
1120
|
pack,
|
|
936
1121
|
packOpaque,
|
|
937
1122
|
idle: { frames: idle.frames.length, fps: idle.fps, duration: idle.duration, lastIndex: last.index, loopAt: { x: loop.x, y: loop.y } },
|
|
@@ -1369,10 +1554,12 @@ export function parseRigcVersion(out: string): [number, number, number] | null {
|
|
|
1369
1554
|
/**
|
|
1370
1555
|
* Refuse, before anything is built, a rigc older than
|
|
1371
1556
|
* {@link RIGC_GEOMETRY_VERSION}: its `render` has no `--geometry`, so the idle
|
|
1372
|
-
* render would fail on the flag after the build
|
|
1557
|
+
* render would fail on the flag after the build had run, and
|
|
1373
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.
|
|
1374
1561
|
*/
|
|
1375
|
-
export function requireRigcVersion(rigc: RigcRunner):
|
|
1562
|
+
export function requireRigcVersion(rigc: RigcRunner): string {
|
|
1376
1563
|
const v = rigc(['--version']);
|
|
1377
1564
|
const got = v.status === 0 ? parseRigcVersion(v.out) : null;
|
|
1378
1565
|
const need = parseRigcVersion(RIGC_GEOMETRY_VERSION) as [number, number, number];
|
|
@@ -1386,6 +1573,7 @@ export function requireRigcVersion(rigc: RigcRunner): void {
|
|
|
1386
1573
|
},
|
|
1387
1574
|
]);
|
|
1388
1575
|
}
|
|
1576
|
+
return v.out;
|
|
1389
1577
|
}
|
|
1390
1578
|
|
|
1391
1579
|
export interface IdleGeometry {
|