spine-parts 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 and links
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 links `spine-core`, and spine-parts depends on spine-rigc, so the
77
- same obligation applies to running the stages of spine-parts that call it.
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
@@ -34,18 +34,17 @@ was shifted down into the canvas (its proposal ran 118 px above the top edge;
34
34
  <code>propose --head-box</code> now makes that shift itself), and <code>hair_back</code>
35
35
  is taken from the full run, because the twin tails leave the head crop sideways
36
36
  (a plan edit). <code>check</code> printed, verbatim:<br/>
37
- <code>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/>
37
+ <code>50 assertions: 24 measured (24 passed, 0 failed), 26 skipped, 0 not in profile "spine-html"</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 (&lt;= 1.0), 2 px over 40 (&lt;= 50), 0 px over 80 (reported)</code><br/>
41
- <code>pack: skeleton.png 967x1338, 22 region(s), 91.2% covered, padding 2, page edges free</code><br/>
40
+ <code>pack: skeleton.png 922x1348, 22 region(s), 95.0% covered, padding 2, page edges free, shape polygon</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,520 bytes, one 256-entry palette at a measured error of max 57, mean 1.601
45
+ 12 fps, 1,706,719 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,350 bytes; the GIF, at the same error, 1,812,238). Frame 29 (2.417 s) is the
47
+ 13,645,503 bytes; the GIF, at the same error, 1,812,288). 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,10 +86,10 @@ 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 both profiles |
89
+ | `check/build/skeleton.json`, `skeleton.atlas`, `skeleton.png` | **the artifact** — Spine 4.3 skeleton data and one packed page, written by `rigc build --pack --page-edges free --pack-shape polygon` and gated under `spine-html`, which holds every rule `spine` measures (on the demo, rigc 2.1.3: the 14 rules `validate --profile spine` measures are among the build's 24; 16 are not in `spine`) |
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
- | `check/` | both gate files verbatim, the idle's frames and their `geometry.json` (skinned vertices per frame), `contact.png`, `motion_heat.png`, `check.json` |
92
+ | `check/` | both gate files verbatim, the idle's frames and their `geometry.json` (skinned vertices per frame), `contact.png`, `motion_heat.png`, `check.json` (with `pack_mode`, the `--page-edges` and `--pack-shape` the page was packed under) |
94
93
  | `idle.png`, `idle-indexed.png`, `idle.gif` | with `--loop`: the idle as a lossless APNG (the exactness record), an indexed APNG with one shared palette (the small one) and a GIF; the last two print their palette error |
95
94
 
96
95
  The last lines of a green build are the pack line, printed beside the Spine example
@@ -99,15 +98,15 @@ 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 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
101
+ pack: skeleton.png 477x1151, 20 region(s), 95.0% covered, padding 2, page edges free, shape polygon; page opaque 54.8% (alpha > 0) — spineboy yardstick 1024x256, 40 region(s), 45.8% opaque (alpha > 0), a reference and not a bar
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
 
108
107
  The page's edges are free by default: rigc picks the least-area page the parts need,
109
- not the next power of two. On the two examples that is 967x1338 and 479x1166, where a
110
- power-of-two page is 1024x2048 and 512x2048, and covered goes from 56.3 % to 91.2 % and
108
+ not the next power of two. On the two examples, packed by rectangles, that is 967x1338
109
+ and 479x1166, where a power-of-two page is 1024x2048 and 512x2048, and covered goes from 56.3 % to 91.2 % and
111
110
  from 49.7 % to 93.4 %. The cost is the one rigc's help states: *"region attachments
112
111
  sampling within 1 LSB of the loose build rather than exactly"*. Measured on both
113
112
  examples, every idle frame stays within 1 level of the power-of-two build's. The atlas says
@@ -116,6 +115,16 @@ consumer that mipmaps or repeats the page; it writes exactly the page builds wro
116
115
  before the flag existed. The measurements are in
117
116
  [AUTHORING §5, *Page edges*](docs/AUTHORING.md#5-after-each-stage-what-to-read).
118
117
 
118
+ The pack shape is `polygon` by default (spine-rigc 2.1): a region only meshes draw is
119
+ packed by its emitted hull, so a neighbour may sit inside its rectangle where the hull
120
+ is not; a region attachment stays its rectangle. On the two examples that takes the
121
+ free page from 967x1338 to 922x1348 (covered 91.2 % to 95.0 %, 3.9 % less area) and
122
+ from 479x1166 to 477x1151 (93.4 % to 95.0 %, 1.7 % less), at the cost class `free`
123
+ already has: every idle frame within 1 level of the `rect` build's, and no figure in
124
+ `check.json` but `pack_mode` moves. `--pack-shape rect` writes the page and atlas
125
+ 0.7.0 wrote. The measurements are in
126
+ [AUTHORING §5, *Pack shape*](docs/AUTHORING.md#5-after-each-stage-what-to-read).
127
+
119
128
  The packer is spine-rigc's, and no other: a packed region is a lossless copy, and an
120
129
  atlas written by anything else would have no oracle behind it.
121
130
 
@@ -124,7 +133,7 @@ atlas written by anything else would have no oracle behind it.
124
133
  | stage | tool | what it owns |
125
134
  | --- | --- | --- |
126
135
  | 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`, the named assertions, the packer, the renderer |
136
+ | 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
137
  | 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
138
 
130
139
  spine-parts never writes Spine data itself. Everything on disk under `check/build/`
@@ -253,8 +262,8 @@ agent skill.
253
262
  | `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
254
263
  | `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
255
264
  | `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 round trip is green; `--idle-keys` says whether the idle's keys on mesh-driving bones go through `<bone>_ctl` parents (default) or stay on the bones with `invariants.idleDrivesMeshes` declared |
257
- | `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json` |
265
+ | `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 |
266
+ | `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
267
  | `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
259
268
  | `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
260
269
 
@@ -319,13 +328,19 @@ the flag, puts 813 of `sample`'s 993 still-unprojected pixels and 879 of `demo`'
319
328
 
320
329
  [Bun](https://bun.sh) 1.2 or later. `npm install -g spine-parts` installs the
321
330
  `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.
331
+ on `PATH`. spine-rigc comes with it as a dependency. No Spine runtime does: since
332
+ spine-rigc 2.0 the installed `rigc` gates every build with its own validator over the
333
+ compiled model document, and `rigc --version` says `entry: cli_core.ts —
334
+ @esotericsoftware/spine-core absent`. With `@esotericsoftware/spine-core` installed
335
+ beside it, the same `rigc` runs the spine-core round trip instead, as this repository's
336
+ selftest and CI do; `check.json`'s `rigc_entry` records which one gated a build.
323
337
 
324
338
  ## Licence posture
325
339
 
326
- spine-parts is MIT, and it depends on spine-rigc, which links Esoteric Software's
327
- `spine-core`: using what it produces in a product requires a Spine Editor licence, as
328
- any Spine data does. [NOTICE.md](NOTICE.md) records that chain and every other
340
+ spine-parts is MIT, and it depends on spine-rigc, whose output is Spine skeleton data:
341
+ using what it produces in a product requires a Spine Editor licence, as any Spine data
342
+ does. Neither package installs Esoteric Software's `spine-core` any more; this
343
+ 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
344
  third-party term this package touches, See-through's included.
330
345
 
331
346
  ## 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 { DEFAULT_PAGE_EDGES, findRigc, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
20
+ import { DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, findRigc, PACK_SHAPES, type PackShape, PAGE_EDGES, type PageEdges, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
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,7 +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]
101
- [--page-edges pot|free]
100
+ [--page-edges pot|free] [--pack-shape rect|polygon]
102
101
  Author the rig: unrotated bones at the config's landmarks (a chain makes
103
102
  <chain>0..n), a square lattice mesh over every part in config.meshes
104
103
  weighted by distance to its candidate bone segments, a region for every
@@ -111,10 +110,11 @@ usage:
111
110
  bone twice, is refused by the loader, CONFIG_BLINK_GROUP_MEMBERS or
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
- through spine-rigc (profile spine-html, packed with --page-edges as for
115
- check, then validated under profile spine) in a scratch directory first, and --out receives
116
- images/*.png, rig.json, motion.json and mesh_report.json only when both
117
- are green. Prints one line per mesh and the rigc gate lines.
113
+ through spine-rigc (profile spine-html, packed with --page-edges and
114
+ --pack-shape as for check; rigc's build gates the compile and the packed pages on disk) in a
115
+ 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
@@ -123,17 +123,19 @@ usage:
123
123
  stage prints that SKIP line). The pose is the same to one level of float
124
124
  rounding, and so is the per-frame mesh work (AUTHORING §5).
125
125
  spine-parts check --rig <dir> --out <dir> [--parts <dir>] [--page-edges pot|free]
126
+ [--pack-shape rect|polygon]
126
127
  Build, gate, render and measure a rig through spine-rigc's CLI (the rigc at
127
128
  node_modules/.bin/rigc, or on PATH). --rig holds rig.json and motion.json
128
129
  (with an "idle").
129
130
  ${PARTS_HOME_SENTENCE}.
130
131
  Both are only read. Into --out:
131
- build/ (rigc build --profile spine-html --pack --page-edges <value>: the
132
- packed atlas is the artifact), gate_spine-html.txt and gate_spine.txt (the gate lines
132
+ build/ (rigc build --profile spine-html --pack --page-edges <value>
133
+ --pack-shape <value>: the packed atlas is the artifact), gate_spine-html.txt (the gate lines
133
134
  verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
134
135
  --geometry: the frames and geometry.json, the skinned vertices and
135
136
  every bone's world transform per frame),
136
- contact.png, motion_heat.png and check.json. PASS needs both gates
137
+ contact.png, motion_heat.png and check.json (with rigc_entry, the rigc
138
+ entry that gated the build, and pack_mode, the two pack flags). PASS needs every gate summary
137
139
  "0 failed", the seam (setup pose vs the flat composite of parts/) at mean
138
140
  |d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
139
141
  frame at t = duration) at max |d| 0. Then six judgement lines, each in
@@ -165,15 +167,25 @@ usage:
165
167
  a bar. Exit 0 on PASS, 1 on FAIL — every FAIL line names the bar, the value
166
168
  and the value required.
167
169
  --page-edges is handed to rigc verbatim and defaults to ${DEFAULT_PAGE_EDGES}: the
168
- least-area page the parts need (on the two public examples 967x1338 and
169
- 479x1166, where pot writes 1024x2048 and 512x2048), at the cost rigc
170
+ least-area page the parts need (on the two public examples, rect, 967x1338
171
+ and 479x1166, where pot writes 1024x2048 and 512x2048), at the cost rigc
170
172
  states — region attachments sample within 1 LSB of the loose build
171
173
  rather than exactly (measured on both examples: every idle frame within
172
174
  1 level of the pot build's; AUTHORING §5, Page edges). pot is a power of two on both edges, for a consumer
173
175
  that mipmaps or repeats the page (the atlas declares filter Linear,
174
- Linear and no repeat). rigc's pack line ends ", page edges free" under
176
+ Linear and no repeat). rigc's pack line carries ", page edges free" under
175
177
  free; a line that disagrees with the value passed, or a pot page that is
176
178
  not a power of two, is refused, CHECK_PACK_PAGE_EDGES.
179
+ --pack-shape is handed to rigc verbatim and defaults to ${DEFAULT_PACK_SHAPE}:
180
+ a region only meshes draw is packed by its emitted hull, so a neighbour
181
+ may sit inside its rectangle where the hull is not (on the two public
182
+ examples, under free, 922x1348 and 477x1151 where rect writes 967x1338
183
+ and 479x1166), at the cost of the same 1-level class as free (measured
184
+ on both examples: every idle frame within 1 level of the rect build's;
185
+ AUTHORING §5, Pack shape). rect keeps every region's cell apart and
186
+ writes the page earlier releases wrote. rigc's pack line ends
187
+ ", shape rect" or ", shape polygon"; a line that disagrees with the
188
+ value passed is refused, CHECK_PACK_SHAPE.
177
189
 
178
190
  spine-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
179
191
  Encode a frame set rigc render wrote (its --out directory, or the set
@@ -258,7 +270,8 @@ usage:
258
270
 
259
271
  spine-parts build --config <config.json> --source <painting.png> --full <dir|psd>
260
272
  --head <dir|psd> --out <dir> [--seam near-white|silhouette]
261
- [--project core|visible] [--page-edges pot|free] [--loop]
273
+ [--project core|visible] [--page-edges pot|free]
274
+ [--pack-shape rect|polygon] [--loop]
262
275
  assemble, then rig, then check, in one process, each stage's own lines
263
276
  printed under [assemble], [rig] and [check]; the first stage that refuses
264
277
  stops the build with its own FAIL lines. The config must already carry
@@ -274,8 +287,9 @@ usage:
274
287
  three artifact paths — skeleton .json, .atlas and the packed page: the
275
288
  packed atlas is the result, the loose parts are the intermediate it was
276
289
  made from. --seam defaults to ${DEFAULT_SEAM_RULE}, --project to
277
- ${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES}
278
- (as for check; both packed builds, rig's gate and check's artifact, take it).
290
+ ${DEFAULT_PROJECT_RULE} (both as for assemble), --page-edges to ${DEFAULT_PAGE_EDGES} and
291
+ --pack-shape to ${DEFAULT_PACK_SHAPE} (as for check; both packed builds, rig's gate
292
+ and check's artifact, take them).
279
293
 
280
294
  spine-parts --version
281
295
  spine-parts --help
@@ -396,20 +410,6 @@ function cmdSheet(args: string[]): number {
396
410
  }
397
411
  }
398
412
 
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
413
  /** `--page-edges`, or its default; a value spine-rigc does not take is a usage error naming the two it does. */
414
414
  function pageEdgesOf(value: string | undefined): PageEdges | string {
415
415
  const edges = value ?? DEFAULT_PAGE_EDGES;
@@ -420,16 +420,27 @@ function isPageEdges(v: PageEdges | string): v is PageEdges {
420
420
  return (PAGE_EDGES as readonly string[]).includes(v);
421
421
  }
422
422
 
423
+ /** `--pack-shape`, or its default; a value spine-rigc does not take is a usage error naming the two it does. */
424
+ function packShapeOf(value: string | undefined): PackShape | string {
425
+ const shape = value ?? DEFAULT_PACK_SHAPE;
426
+ return (PACK_SHAPES as readonly string[]).includes(shape) ? (shape as PackShape) : `--pack-shape ${shape}; one of ${PACK_SHAPES.join(', ')} is required`;
427
+ }
428
+
429
+ function isPackShape(v: PackShape | string): v is PackShape {
430
+ return (PACK_SHAPES as readonly string[]).includes(v);
431
+ }
432
+
423
433
  function cmdRig(args: string[]): number {
424
434
  let config: string | null = null;
425
435
  let partsDir: string | null = null;
426
436
  let out: string | null = null;
427
437
  let idleKeys: string | null = null;
428
438
  let pageEdges: string | null = null;
439
+ let packShape: string | null = null;
429
440
  for (let i = 0; i < args.length; i++) {
430
441
  const flag = args[i];
431
442
  const value = args[i + 1];
432
- if (!['--config', '--parts', '--out', '--idle-keys', '--page-edges'].includes(flag)) return usage(`rig does not take "${flag}"`);
443
+ if (!['--config', '--parts', '--out', '--idle-keys', '--page-edges', '--pack-shape'].includes(flag)) return usage(`rig does not take "${flag}"`);
433
444
  if (value === undefined) return usage(`${flag} needs a value`);
434
445
  i++;
435
446
  if (flag === '--config') config = value;
@@ -440,6 +451,9 @@ function cmdRig(args: string[]): number {
440
451
  } else if (flag === '--page-edges') {
441
452
  if (pageEdges !== null) return usage('--page-edges is given twice');
442
453
  pageEdges = value;
454
+ } else if (flag === '--pack-shape') {
455
+ if (packShape !== null) return usage('--pack-shape is given twice');
456
+ packShape = value;
443
457
  } else out = value;
444
458
  }
445
459
  if (config === null) return usage('rig needs --config <config.json>');
@@ -449,9 +463,17 @@ function cmdRig(args: string[]): number {
449
463
  if (!(IDLE_KEYS as readonly string[]).includes(keys)) return usage(`--idle-keys ${keys}; one of ${IDLE_KEYS.join(', ')} is required`);
450
464
  const edges = pageEdgesOf(pageEdges ?? undefined);
451
465
  if (!isPageEdges(edges)) return usage(edges);
466
+ const shape = packShapeOf(packShape ?? undefined);
467
+ if (!isPackShape(shape)) return usage(shape);
468
+ let bin: string;
469
+ try {
470
+ bin = findRigc(import.meta.dir, process.env.PATH ?? '');
471
+ } catch (err) {
472
+ return printRefusal(err);
473
+ }
452
474
  const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-rig-'));
453
475
  try {
454
- rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys, pageEdges: edges }, rigGateRunner(), scratch, console.log);
476
+ rigStage({ config, parts: partsDir, out, idleKeys: keys as IdleKeys, pageEdges: edges, packShape: shape }, rigcRunner(bin), scratch, console.log);
455
477
  return EXIT_OK;
456
478
  } catch (err) {
457
479
  return printRefusal(err);
@@ -460,7 +482,16 @@ function cmdRig(args: string[]): number {
460
482
  }
461
483
  }
462
484
 
463
- /** The rigc process: `src/check.ts` is pure and takes the spawn from here. */
485
+ /**
486
+ * The rigc process, for every stage that runs rigc — `rig`'s gate, `check`, and
487
+ * both inside `build`: the `rigc` binary {@link findRigc} located, spawned as
488
+ * it is, so spine-rigc's own launcher (`bin/rigc.cjs`) chooses the entry —
489
+ * `cli.ts` and the spine-core round trip where the runtime resolves beside it,
490
+ * `cli_core.ts` and rigc's own validator where it does not. No stage runs a
491
+ * rigc source file by path: under spine-rigc 2.0, `cli.ts` by path is the
492
+ * spine-core entry whether or not spine-core is there. `src/` is pure and takes
493
+ * the spawn from here.
494
+ */
464
495
  function rigcRunner(bin: string): RigcRunner {
465
496
  return (args) => {
466
497
  const r = spawnSync(bin, [...args], { encoding: 'utf8', maxBuffer: 1 << 28 });
@@ -484,15 +515,17 @@ function flags(args: string[], known: readonly string[], command: string, option
484
515
  }
485
516
 
486
517
  function cmdCheck(args: string[]): number {
487
- const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--page-edges']);
518
+ const f = flags(args, ['--rig', '--out'], 'check', ['--parts', '--page-edges', '--pack-shape']);
488
519
  if (typeof f === 'string') return usage(f);
489
520
  const rig = f.get('--rig') as string;
490
521
  const out = f.get('--out') as string;
491
522
  const edges = pageEdgesOf(f.get('--page-edges'));
492
523
  if (!isPageEdges(edges)) return usage(edges);
524
+ const shape = packShapeOf(f.get('--pack-shape'));
525
+ if (!isPackShape(shape)) return usage(shape);
493
526
  try {
494
527
  const bin = findRigc(import.meta.dir, process.env.PATH ?? '');
495
- const r = checkStage({ rig, parts: f.get('--parts') ?? rig, out, pageEdges: edges }, rigcRunner(bin), bin, console.log);
528
+ const r = checkStage({ rig, parts: f.get('--parts') ?? rig, out, pageEdges: edges, packShape: shape }, rigcRunner(bin), bin, console.log);
496
529
  return r.figures.PASS ? EXIT_OK : EXIT_REFUSED;
497
530
  } catch (err) {
498
531
  return printRefusal(err);
@@ -797,7 +830,7 @@ function cmdInputs(args: string[]): number {
797
830
  function cmdBuild(args: string[]): number {
798
831
  const flags = new Map<string, string>();
799
832
  let loop = false;
800
- const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges'];
833
+ const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project', '--page-edges', '--pack-shape'];
801
834
  for (let i = 0; i < args.length; i++) {
802
835
  const flag = args[i];
803
836
  if (flag === '--loop') {
@@ -819,6 +852,8 @@ function cmdBuild(args: string[]): number {
819
852
  if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
820
853
  const edges = pageEdgesOf(flags.get('--page-edges'));
821
854
  if (!isPageEdges(edges)) return usage(edges);
855
+ const shape = packShapeOf(flags.get('--pack-shape'));
856
+ if (!isPackShape(shape)) return usage(shape);
822
857
  const [config, source, full, head, out] = ['--config', '--source', '--full', '--head', '--out'].map((f) => flags.get(f) as string);
823
858
  let bin: string;
824
859
  try {
@@ -829,8 +864,8 @@ function cmdBuild(args: string[]): number {
829
864
  const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-build-'));
830
865
  try {
831
866
  const r = build(
832
- { config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges },
833
- { rig: rigGateRunner(), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
867
+ { config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop, pageEdges: edges, packShape: shape },
868
+ { rig: rigcRunner(bin), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
834
869
  console.log,
835
870
  );
836
871
  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 gates `0 failed` | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
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,11 +279,12 @@ entry, no dithering, filter None) — and is the small file to show. `idle.gif`
279
279
  (`loop --out x.gif`) is the same median cut in a GIF. The indexed APNG and the GIF
280
280
  print their palette error, per channel over R, G and B of every frame (and alpha's
281
281
  max for the APNG); a figure is a measurement of the file, not a bar. On the demo:
282
- 13,645,350 B lossless, 1,706,520 B indexed and 1,812,238 B GIF, both palette files at
282
+ 13,645,503 B lossless, 1,706,719 B indexed and 1,812,288 B GIF, both palette files at
283
283
  max 57, mean 1.601 — the two share one quantiser, so their error is the same by
284
284
  construction and the size is the difference. That is `--seam silhouette` (the README's
285
- animation); with `--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
+ animation, spine-rigc 2.1.3); with `--pack-shape rect` the same build writes 13,645,350,
286
+ 1,706,520 and 1,812,238 B, and with `--page-edges pot --pack-shape rect` 13,645,519,
287
+ 1,706,785 and 1,812,625 B, all at the same error. The loop shows the blink closed: the
287
288
  eyes' hold is at least one 12 fps frame (below), so whatever `motion.blink.t` is, one
288
289
  idle frame lands inside it, and `check.json`'s `BLINK_NO_HOLE.idle_frames_closed`
289
290
  names it — `[29]` on both examples.
@@ -300,8 +301,11 @@ the reference's so the examples stay comparable with it.
300
301
  --page-edges <v>`, and hand the value to rigc verbatim. The default is `free`,
301
302
  the least-area page the parts need. `--page-edges pot` is the opt-out: a power of
302
303
  two on both edges, which is the page every build wrote before this flag. The pack
303
- line says which one ran, because rigc ends it with `, page edges free` under `free`.
304
- Measured with rigc 1.5.1 on the two examples, by `tools/atlas_population.ts`:
304
+ line says which one ran, because rigc writes `, page edges free` after the padding under
305
+ `free`. Measured on the two examples by `tools/atlas_population.ts` with rigc 1.5.1, and
306
+ again with rigc 2.0.3 and with rigc 2.1.3 under `--pack-shape rect` (the frames table
307
+ by the same idle render), which reproduced every figure in both tables and the one
308
+ check figure below:
305
309
 
306
310
  | build | page | covered | page opaque | page / figure |
307
311
  | --- | --- | --- | --- | --- |
@@ -326,6 +330,56 @@ barred figure and every status is the same under both. The atlas rigc writes say
326
330
  `filter: Linear, Linear` and has no `repeat` line, so choose `pot` for a consumer
327
331
  that mipmaps or repeats the page.
328
332
 
333
+ **Pack shape.** `rig`, `check` and `build` also hand rigc `--pack-shape <v>`
334
+ verbatim (spine-rigc 2.1.0 and later). `rect` keeps every region's cell apart, the
335
+ only packing before 2.1. `polygon`, the default here (rigc's own is `rect`), packs a
336
+ region that only meshes draw by its emitted hull, so a neighbour may sit inside its
337
+ rectangle where the hull is not, with the padding kept between footprints; a region
338
+ attachment stays its rectangle. rigc gates the result on the packed pages with
339
+ `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` (SKIP on the compile pass, where every part has
340
+ its own page; PASS on the packed pass, both shapes, both examples). The pack line ends
341
+ `, shape rect` or `, shape polygon` under either, and `check.json`'s `pack_mode` records
342
+ both flags. Measured on the two examples with spine-rigc 2.1.3 (the full entry,
343
+ spine-core 4.3.13 beside it), `spine-parts build` with `--page-edges free` and each
344
+ shape, the table columns by `tools/atlas_population.ts`:
345
+
346
+ | build | page | page area | covered | page opaque | page / figure |
347
+ | --- | --- | --- | --- | --- | --- |
348
+ | demo, `rect` | 967x1338 | 1,293,846 | 91.2 % | 59.7 % | 1.636 |
349
+ | demo, `polygon` | 922x1348 | 1,242,856 (−3.9 %) | 95.0 % | 62.1 % | 1.571 |
350
+ | sample, `rect` | 479x1166 | 558,514 | 93.4 % | 53.9 % | 1.755 |
351
+ | sample, `polygon` | 477x1151 | 549,027 (−1.7 %) | 95.0 % | 54.8 % | 1.725 |
352
+
353
+ Covered is Σ region rectangles over page area, as rigc prints it; under `polygon` the
354
+ rectangles may overlap where a hull leaves room, so it is not a union and can exceed
355
+ 100 %. Page opaque is the measure of what the page holds. Under `--page-edges pot` the
356
+ shape changes nothing on these two examples: both shapes write 1024x2048 and 512x2048,
357
+ and the idle frames are identical.
358
+
359
+ The cost, from `rect` to `polygon` (the idle `check` renders, `rigc render --animation
360
+ idle --fps 12 --max 640`, compared frame by frame, max over R, G, B and A):
361
+
362
+ | example | idle frames that differ | pixels that differ (all 49 frames) | max \|d\| |
363
+ | --- | --- | --- | --- |
364
+ | demo | 49 of 49 | 2,594 | 1 |
365
+ | sample | 49 of 49 | 740 | 1 |
366
+
367
+ That is the class `free` already costs (the table above, measured by the same
368
+ comparison), a little larger. No figure in `check.json` moved but `pack_mode`: every
369
+ bar, every status and every reported figure is the same under both shapes.
370
+ `--pack-shape rect` gives back the page and atlas 0.7.0 wrote: on both examples
371
+ they are byte-identical to rigc 2.0.3's `--page-edges free` build of the same rig (the
372
+ skeleton JSON differs only in its relative `images` path), and selftest `CK53` holds
373
+ `rect` to rigc's own rectangle page.
374
+
375
+ The hull ceiling (spine-parts #58: hull area over rectangle area, 0.812 on demo and
376
+ 0.814 on sample) is not what the packer realises here: the hollows inside the meshes'
377
+ rectangles are worth only what fits into them. On the polygon pages, read off the
378
+ atlas, demo's packed rectangles overlap in 21 pairs (17 a region attachment inside a
379
+ mesh's rectangle, 4 a mesh inside another's) and sample's in 12 (10 and 2); no two
380
+ region attachments overlap. More would take tighter hulls, meshes in place of
381
+ regions, or a stronger nesting search in rigc, each its own card.
382
+
329
383
  `--project core` (the default) is the reference's projection rule: a layer takes the
330
384
  painting's pixel only inside its top-most `alpha >= 250` area eroded by a 5x5 square,
331
385
  so a part a few pixels wide (a lash, a brow, an iris) takes none. `--project visible`
@@ -357,8 +411,8 @@ differ by at most one level in a channel. The default stays `ctl`: the examples'
357
411
  `expected/rig.json` and `motion.json` are the reference's output, and the
358
412
  declaration is in the rig spec only, so `rigc validate <build> --profile spine-html`
359
413
  run on a `direct` build (it has no rig spec to read) refuses A15 once per keyed
360
- bone. `check` validates the build under `spine` only, where A15 is not in the
361
- profile, and is green on both.
414
+ bone. No stage runs `validate`: the gate is `rigc build`, which reads the rig spec
415
+ and its declaration, and is green on both (`RG19`).
362
416
 
363
417
  **Patching a hole no layer holds.** When an `uncovered hole` stays large after the
364
418
  plan is right — both See-through runs dropped a piece of the figure, red in
@@ -588,7 +642,8 @@ See-through with another seed.
588
642
  | `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
589
643
  | `CHECK_INPUT_PRESENT` on `…/parts.json` | `--parts` named a directory without `parts.json` — most often `parts/` itself. The detail says it: "--parts names the directory holding parts.json and parts/, not parts/ itself (after build, that is build's --out, whose rig is <out>/rig); without --parts it is --rig", and names the parent when the parent holds `parts.json` | `--parts` — the directory above `parts/` |
590
644
  | `CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_PART_SLOT_PRESENT`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` (`CHECK_PART_SLOT_PRESENT`: a part with no slot of its own name, which the judgement lines render it by) | re-run rig (`build` does both) |
591
- | `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^1.5.1), or put a newer `rigc` first on `PATH` |
645
+ | `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^2.1.3), or put a newer `rigc` first on `PATH` |
646
+ | `CHECK_RIGC_ENTRY`, `CHECK_RIGC_ENTRY_READS` | `rigc --version` names no entry (a rigc below 2.0.0), or names one in neither launcher form (`entry: cli.ts — @esotericsoftware/spine-core <v> present`, `entry: cli_core.ts — @esotericsoftware/spine-core absent — …`); `check.json`'s `rigc_entry` records the one that gated the build | `bun install` |
592
647
  | `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
593
648
  | `CHECK_LOOP_LAST_FRAME_AT_DURATION` | the idle's last frame does not sit at `duration` | `motion.duration` — a whole number of 1/12 s |
594
649
  | `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 |
@@ -601,8 +656,9 @@ See-through with another seed.
601
656
  | `CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other |
602
657
  | `CHECK_SEAM_FRAME_SIZE`, `FRAMES_SIDECAR`, `CHECK_GEOMETRY_FILE` | rigc's render is not what its `frames.json` says, or its `geometry.json` is not a whole `rigc-geometry/1` export of the frames beside it (a frame count, a mesh whose vertex count differs from its rest entry; for the face half, another viewport than `frames.json`'s, other frame indices, the head bone or a feature's bone missing from a frame, a non-finite transform or time) | a rigc problem; report it |
603
658
  | `LOOP_ENCODE` | the loop encoder refused a frame (translucent pixel in a GIF, a size change) | the frames; for a translucent frame write the lossless or the indexed APNG, which keep alpha |
604
- | `CHECK_PACK_LINE_READS` | rigc printed a `pack:` line that is not `pack: <page> <W>x<H>, <N> region(s), <P>% covered, padding <D>` with `, page edges free` after it or nothing; the line is quoted | a rigc this package does not know the output of; report it, with `rigc --version` |
605
- | `CHECK_PACK_PAGE_EDGES` | the pack line disagrees with the `--page-edges` the build was run with — its `, page edges free` suffix is there under `pot` or missing under `free`, or a `pot` page is not a power of two on both edges | a rigc problem; report it, with `rigc --version` |
659
+ | `CHECK_PACK_LINE_READS` | rigc printed a `pack:` line that is not `pack: <page> <W>x<H>, <N> region(s), <P>% covered, padding <D>`, then `, page edges free` or nothing, then `, shape rect`, `, shape polygon` or nothing (spine-rigc 2.1 prints the shape; 1.5–2.0 printed none, read as `rect`); an unknown shape is this refusal; the line is quoted | a rigc this package does not know the output of; report it, with `rigc --version` |
660
+ | `CHECK_PACK_PAGE_EDGES` | the pack line disagrees with the `--page-edges` the build was run with — its `, page edges free` clause is there under `pot` or missing under `free`, or a `pot` page is not a power of two on both edges | a rigc problem; report it, with `rigc --version` |
661
+ | `CHECK_PACK_SHAPE` | the pack line disagrees with the `--pack-shape` the build was run with — it ends `, shape rect` under `polygon` or `, shape polygon` under `rect`, or it has no shape clause (the 1.5–2.0 form, read as `rect`) under `polygon` | a rigc older than 2.1.0 on `PATH`: rigc 2.0.3 takes `--pack-shape` without a word, packs by rectangles and prints the form with no shape clause, so this refusal is what catches it — `bun install` (this package depends on spine-rigc ^2.1.3); otherwise a rigc problem, report it with `rigc --version` |
606
662
  | `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
607
663
 
608
664
  ## 7. The bars `check` enforces, and what only an eye answers today
@@ -611,14 +667,19 @@ See-through with another seed.
611
667
  implementation's bars):
612
668
 
613
669
  - `gate_spine-html.txt`: every summary line `(N passed, 0 failed)` — the packed build
614
- under the `spine-html` profile;
615
- - `gate_spine.txt`: the same, `rigc validate` of that build under the `spine` profile;
670
+ under the `spine-html` profile, gated once over the compile and once over the packed
671
+ pages on disk. There is no `spine` gate of its own: `spine-html` holds every rule
672
+ `spine` measures (on the demo, rigc 1.5.1 and 2.0.3: `validate --profile spine`'s 14
673
+ measured rules are all among the build's 23, the 15 others not in `spine`; rigc
674
+ 2.1.3: the same 14 among the build's 24, which adds
675
+ `A49_PACKED_FOOTPRINTS_DO_NOT_OVERLAP` on the packed pass, 16 not in `spine`; `CH09`
676
+ holds it on every fetched example);
616
677
  - **seam**: the setup-pose render against the flat composite of `parts/`: mean
617
678
  max-channel |d| ≤ 1.0 of 255, and at most 50 pixels over 40;
618
679
  - **loop**: idle frame 0 against the frame at `t = duration`: max |d| exactly 0.
619
680
 
620
- On the examples: `sample` 23/23 and 14/14, seam 0.207 with 0 pixels over 40, loop 0;
621
- `demo` (default rule) 23/23 and 14/14, seam 0.326 with 2 pixels over 40, loop 0.
681
+ On the examples: `sample` 23/23, seam 0.207 with 0 pixels over 40, loop 0;
682
+ `demo` (default rule) 23/23, seam 0.326 with 2 pixels over 40, loop 0.
622
683
 
623
684
  A green gate cannot see a wrong animation, so `check` also writes six **judgement
624
685
  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.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "AI-authored Spine 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through spine-rigc before anything is written. A CLI for agents that cannot see the image.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,9 +57,10 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "ag-psd": "^31.0.2",
60
- "spine-rigc": "^1.5.1"
60
+ "spine-rigc": "^2.1.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",