spine-rigc 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cli.ts +66 -16
- package/docs/AUTHORING.md +293 -30
- package/package.json +1 -1
- package/src/atlas.ts +173 -21
- package/src/check.ts +22 -0
- package/src/pose.ts +314 -18
- package/src/render.ts +515 -22
package/README.md
CHANGED
|
@@ -535,7 +535,7 @@ commands take it and what its default is.
|
|
|
535
535
|
| `validate <dir>` | re-gates artifacts already on disk |
|
|
536
536
|
| `ingest <skeleton.json> --out <dir>` | `build` run backwards: reads a Spine 4.3 skeleton and writes the rig spec and motion spec that **rebuild it**, plus a findings report naming everything it could not carry. a skeleton that declares no stage is carried as declaring none, `--stage x,y,w,h` adds a box to one — and is refused, rather than ignored, beside one that declares a box — and `--images <dir>` writes the spec's own images directory — the opposite direction from `build --images`, which overrides it — so the rebuild carries no flag at all |
|
|
537
537
|
| `explain --rig … --motion …` | the compiled rig as a table — every bone with its resolved parent, the slots in draw order, every timeline key by key. Writes nothing. What to reach for when a rig compiles and still looks wrong |
|
|
538
|
-
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference |
|
|
538
|
+
| `render --candidate <dir>` | PNG frames plus a contact sheet, in `render/`. `--hide <slot,…>` or `--slot <slot,…>` draws part of the rig on the **same grid** as the whole, so the two frames overlay and the difference is the part; `frames.json` records the subset and `check` refuses such a set as a reference. `--geometry` adds a `geometry.json` per set: every frame's bone world transforms and skinned attachment vertices, with the rest geometry, on the frames' own grid |
|
|
539
539
|
| `preview --candidate <dir>` | one self-contained `.html` that plays it, headed by the line `validate <dir>` prints for it and the rigc version — a refused candidate is still previewed, and its header says so in the gate's words. Repeat `--candidate` for one page with a pane per candidate, in the order given; a green `build` ends by naming this command for its own `--out` |
|
|
540
540
|
| `vote --candidate a --candidate b` | one `.html` that asks a human which; `vote --record <file>` checks the answer into `votes.jsonl` |
|
|
541
541
|
| `pose --images <dir> --frame <png>` | reads part placements **out of** a picture |
|
package/cli.ts
CHANGED
|
@@ -103,7 +103,6 @@ import {
|
|
|
103
103
|
DEFAULT_PADDING,
|
|
104
104
|
DEFAULT_PAGE_EDGES,
|
|
105
105
|
DEFAULT_PAGE_SIZE,
|
|
106
|
-
FREE_EDGE_STEP,
|
|
107
106
|
PAGE_EDGES,
|
|
108
107
|
packAtlas,
|
|
109
108
|
type PageEdges,
|
|
@@ -145,6 +144,10 @@ import {
|
|
|
145
144
|
FRAMES_SIDECAR,
|
|
146
145
|
FRAMES_SPEC,
|
|
147
146
|
framingViewport,
|
|
147
|
+
GEOMETRY_FILE,
|
|
148
|
+
GeometryError,
|
|
149
|
+
geometryFileOf,
|
|
150
|
+
geometryText,
|
|
148
151
|
loadPosable,
|
|
149
152
|
PROTOCOL_FPS,
|
|
150
153
|
renderFrame,
|
|
@@ -153,6 +156,7 @@ import {
|
|
|
153
156
|
SETUP_POSE_DIR,
|
|
154
157
|
SHEET_FILE,
|
|
155
158
|
SHEET_TILE,
|
|
159
|
+
sidecarViewport,
|
|
156
160
|
SlotSubsetError,
|
|
157
161
|
slotSubsetOf,
|
|
158
162
|
type Frame,
|
|
@@ -259,7 +263,7 @@ function repositoryUrl(): string {
|
|
|
259
263
|
* needs a value` (issue #328). `CLI10`/`CLI11` in `selftest.ts` now hold the two
|
|
260
264
|
* halves together by reading `--help` rather than by naming a flag.
|
|
261
265
|
*/
|
|
262
|
-
const BOOLEAN_FLAGS = new Set(['all-frames', 'all-bones', 'help', 'copy-images', 'again', 'pack', 'copy']);
|
|
266
|
+
const BOOLEAN_FLAGS = new Set(['all-frames', 'all-bones', 'help', 'copy-images', 'again', 'pack', 'copy', 'geometry']);
|
|
263
267
|
|
|
264
268
|
/**
|
|
265
269
|
* The flags a command is allowed to spell more than once.
|
|
@@ -2010,6 +2014,24 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
2010
2014
|
const fps = readPositiveNumber(flags, 'fps', PROTOCOL_FPS, 1);
|
|
2011
2015
|
const maxSide = readPositiveNumber(flags, 'max', 256, 16);
|
|
2012
2016
|
const outRoot = resolve(flags.out ?? 'render');
|
|
2017
|
+
const geometry = flags.geometry !== undefined;
|
|
2018
|
+
// Refused together rather than one of them ignored (issue #864). The export
|
|
2019
|
+
// records every slot's whole geometry, because a subset is a statement about
|
|
2020
|
+
// which pixels are DRAWN and the pose is the same whatever a picture leaves
|
|
2021
|
+
// out — so a geometry.json beside a subset's frames would describe slots
|
|
2022
|
+
// those frames do not draw, and one that dropped them would stop being the
|
|
2023
|
+
// pose. The whole-rig run writes the same file either way.
|
|
2024
|
+
if (geometry) {
|
|
2025
|
+
for (const flag of ['slot', 'hide'] as const) {
|
|
2026
|
+
if (flags[flag] !== undefined) {
|
|
2027
|
+
throw new UsageError(
|
|
2028
|
+
`--geometry takes no --${flag}: it records every slot's whole geometry, which a subset of the drawn slots ` +
|
|
2029
|
+
`does not change. Run \`rigc render --geometry\` on the whole rig for the file, and --${flag} ` +
|
|
2030
|
+
`${flags[flag]} without --geometry for the pictures — both land on the same grid`,
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2013
2035
|
|
|
2014
2036
|
console.log('rigc render');
|
|
2015
2037
|
console.log(` .. skeleton ${skeletonPath}`);
|
|
@@ -2031,6 +2053,9 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
2031
2053
|
if (skin !== undefined) console.log(` .. skin ${skin}`);
|
|
2032
2054
|
if (subset !== undefined) console.log(` .. ${subset.mode.padEnd(8)} ${subset.names.join(', ')}`);
|
|
2033
2055
|
|
|
2056
|
+
// `null` is a skeleton that posed no vertex at all. One that posed a vertex
|
|
2057
|
+
// it cannot frame — Infinity or NaN — is thrown from the framing as a
|
|
2058
|
+
// `GeometryError` naming the number (issue #873), and never reaches this.
|
|
2034
2059
|
const viewport = framingViewport(data, maxSide, pose);
|
|
2035
2060
|
if (!viewport) {
|
|
2036
2061
|
throw new UsageError(
|
|
@@ -2043,8 +2068,24 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
2043
2068
|
// `sampleAll` covers the skeleton with no animation at all, which files its one
|
|
2044
2069
|
// setup-pose frame under the reserved name. Narrowing to one animation reuses
|
|
2045
2070
|
// the same sampler rather than a second path through it.
|
|
2071
|
+
//
|
|
2072
|
+
// `--geometry` rides on the SAME call (issue #864): the bones and the whole
|
|
2073
|
+
// attachments are read off the skeleton at the step that drew each frame, so
|
|
2074
|
+
// the export's grid is this frame set's by construction.
|
|
2075
|
+
const sampling = geometry ? { ...pose, bones: true, geometry: true } : pose;
|
|
2046
2076
|
const sampled: Map<string, Frame[]> =
|
|
2047
|
-
only === undefined
|
|
2077
|
+
only === undefined
|
|
2078
|
+
? sampleAll(data, fps, sampling)
|
|
2079
|
+
: new Map([[only, sampleAnimation(data, only, fps, sampling)]]);
|
|
2080
|
+
// Every file's text before the first write, so a refused number leaves the
|
|
2081
|
+
// output directory as it was rather than half of a frame set behind it.
|
|
2082
|
+
const geometryTexts = new Map<string, string>();
|
|
2083
|
+
if (geometry) {
|
|
2084
|
+
for (const [name, frames] of sampled) {
|
|
2085
|
+
const animation = name === SETUP_POSE_DIR && data.animations.length === 0 ? null : name;
|
|
2086
|
+
geometryTexts.set(name, geometryText(geometryFileOf(data, animation, fps, frames, viewport, skin)));
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2048
2089
|
console.log(` .. ${viewport.width}x${viewport.height}px at ${fps} fps, ${sampled.size} set(s) -> ${outRoot}`);
|
|
2049
2090
|
if (!declaresSetupStage(data)) console.log(` .. ${STAGELESS_FRAMING.render}`);
|
|
2050
2091
|
|
|
@@ -2067,6 +2108,8 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
2067
2108
|
// would be the same picture with a border and a "0" on it.
|
|
2068
2109
|
const sheet = frames.length > 1;
|
|
2069
2110
|
if (sheet) contactSheet(frames, pages, viewport, SHEET_TILE).writePng(join(dir, SHEET_FILE));
|
|
2111
|
+
const geometryOut = geometryTexts.get(name);
|
|
2112
|
+
if (geometryOut !== undefined) writeFileSync(join(dir, GEOMETRY_FILE), geometryOut);
|
|
2070
2113
|
const duration = frames[frames.length - 1].time;
|
|
2071
2114
|
sets.push({
|
|
2072
2115
|
dir: dirName,
|
|
@@ -2078,7 +2121,8 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
2078
2121
|
duration,
|
|
2079
2122
|
});
|
|
2080
2123
|
const how = frames.length === 1 ? 'a single pose' : `${duration.toFixed(3)}s`;
|
|
2081
|
-
|
|
2124
|
+
const extras = `${sheet ? ` + ${SHEET_FILE}` : ''}${geometryOut === undefined ? '' : ` + ${GEOMETRY_FILE}`}`;
|
|
2125
|
+
console.log(` .. ${name.padEnd(16)} ${frames.length} frame(s), ${how}${extras} -> ${dir}`);
|
|
2082
2126
|
}
|
|
2083
2127
|
|
|
2084
2128
|
// The sidecar is what makes this a frame SET rather than a pile of pictures:
|
|
@@ -2095,15 +2139,9 @@ function cmdRender(flags: Record<string, string>): void {
|
|
|
2095
2139
|
// every slot says nothing and stays the bytes it always was (issue #835).
|
|
2096
2140
|
...subsetFields(subset),
|
|
2097
2141
|
background: BACKGROUND,
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
width: viewport.maxX - viewport.minX,
|
|
2102
|
-
height: viewport.maxY - viewport.minY,
|
|
2103
|
-
scale: viewport.scale,
|
|
2104
|
-
pixelWidth: viewport.width,
|
|
2105
|
-
pixelHeight: viewport.height,
|
|
2106
|
-
},
|
|
2142
|
+
// The one spelling `geometry.json` repeats, so the two files cannot state
|
|
2143
|
+
// two boxes for one grid.
|
|
2144
|
+
viewport: sidecarViewport(viewport),
|
|
2107
2145
|
sets: [...sets].sort((a, b) => a.dir.localeCompare(b.dir)),
|
|
2108
2146
|
};
|
|
2109
2147
|
writeFileSync(join(outRoot, FRAMES_SIDECAR), `${JSON.stringify(sidecar, null, 2)}\n`);
|
|
@@ -3789,7 +3827,7 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
3789
3827
|
padding: `gutter each region reserves on every side, --pack only (default ${DEFAULT_PADDING}); it is filled by ` +
|
|
3790
3828
|
"extending the region's own edge pixels outwards, which is what stops a neighbour bleeding in",
|
|
3791
3829
|
'page-edges': `what a page's edges may be, --pack only (default ${DEFAULT_PAGE_EDGES}): pot is a power of two on ` +
|
|
3792
|
-
|
|
3830
|
+
'both; free tries every width from the widest part up, takes the height the placement needs and keeps ' +
|
|
3793
3831
|
'the least area — a smaller page, at the cost of region attachments sampling within 1 LSB of the loose ' +
|
|
3794
3832
|
'build rather than exactly',
|
|
3795
3833
|
'atlas-in':
|
|
@@ -3808,6 +3846,10 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
3808
3846
|
'derived: a candidate is entitled to its own vocabulary, so a mapping worked out here would be a guess reported ' +
|
|
3809
3847
|
'as a measurement',
|
|
3810
3848
|
'all-bones': 'print every bone pair, not just the worst by position',
|
|
3849
|
+
geometry:
|
|
3850
|
+
`also write ${GEOMETRY_FILE} into each frame directory: per frame, every bone's world transform and every ` +
|
|
3851
|
+
"slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry — on the " +
|
|
3852
|
+
'frames\' own grid and viewport. Not with --slot/--hide: the geometry is the whole pose whatever is drawn',
|
|
3811
3853
|
'texture-from':
|
|
3812
3854
|
"also measure this run through this atlas's texels, keeping the candidate's own geometry, and report how much " +
|
|
3813
3855
|
'of the MAE is texture resampling rather than the rig — pass the atlas the reference frames were rendered ' +
|
|
@@ -4148,10 +4190,10 @@ const COMMANDS: CommandDoc[] = [
|
|
|
4148
4190
|
{
|
|
4149
4191
|
name: 'render',
|
|
4150
4192
|
usage: [
|
|
4151
|
-
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--skin <name>] [--fps 12] [--max 256] [--out render/]',
|
|
4193
|
+
'rigc render --candidate <dir | skeleton.json> [--animation <name>] [--skin <name>] [--fps 12] [--max 256] [--geometry] [--out render/]',
|
|
4152
4194
|
'rigc render … --slot <name[,name…]> | --hide <name[,name…]> (a subset of the slots, on the whole rig\'s grid)',
|
|
4153
4195
|
],
|
|
4154
|
-
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'out'],
|
|
4196
|
+
flags: ['candidate', 'atlas', 'animation', 'skin', 'slot', 'hide', 'fps', 'max', 'geometry', 'out'],
|
|
4155
4197
|
overrides: {
|
|
4156
4198
|
out: { value: '<dir>', meaning: 'directory to write the frame series into (default `render/`)' },
|
|
4157
4199
|
fps: { meaning: `frames per second to sample the animation at (default ${PROTOCOL_FPS})` },
|
|
@@ -4499,6 +4541,14 @@ try {
|
|
|
4499
4541
|
// the one reader's and already names the file, what it is and what rigc
|
|
4500
4542
|
// reads; a stack under it is the tool describing its own internals instead.
|
|
4501
4543
|
// Exit 1, like a compile error: the invocation was fine, a file was not.
|
|
4544
|
+
// A pose that is not finite (issues #864, #873): the invocation was fine and
|
|
4545
|
+
// the skeleton posed a NaN or an infinity, so exit 1 like a file that is not
|
|
4546
|
+
// a PNG. Raised by the geometry export and by the framing — the one sentence
|
|
4547
|
+
// naming the bone or vertex and its value — before the first file is written.
|
|
4548
|
+
if (err instanceof GeometryError) {
|
|
4549
|
+
console.error(`rigc render: ${err.message}`);
|
|
4550
|
+
process.exit(1);
|
|
4551
|
+
}
|
|
4502
4552
|
if (err instanceof NotAPngError) {
|
|
4503
4553
|
console.error(`rigc: ${err.message}`);
|
|
4504
4554
|
process.exit(1);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -209,6 +209,7 @@ What the flags mean:
|
|
|
209
209
|
| `--profile` | `spine` = the 34 validity rules (**the default**) · `spine-html` = all 49, opt-in |
|
|
210
210
|
| `--candidate` | `check`, `bench`, `render`, `preview`, `chainfit` and `vote` only: a **compiled** artifact — the directory `build --out` wrote, or a `skeleton.json` path. `--atlas <path>` names the atlas when it does not sit beside the skeleton. **`vote` and `preview` take it more than once**: `vote` 2–4 times, one per pane, labelled A, B, C, D in the order given; `preview` any number of times, one pane per candidate in the order given, each headed by its path and its gate line, and the same skeleton twice (a directory and its `skeleton.json` are one) is refused by name. `--atlas` goes with one candidate only. Everywhere else a repeat is a typo and is refused |
|
|
211
211
|
| `--animation` | `render`, `preview` and `vote` only: which animation to show. The default is **every** one for `render`, the **first** for `preview` (each candidate's own first, with several), and for `vote` the first of candidate A. A name the skeleton does not have is refused, with the ones it does have listed — and for `vote` and a several-candidate `preview`, so is a name that only *some* candidates have, naming the one that lacks it |
|
|
212
|
+
| `--geometry` | `render` only: also write a `geometry.json` into each frame directory — per frame, every bone's world transform and every slot's region or mesh vertices in world units after skinning, plus each attachment's rest geometry and topology, on the frames' own grid and viewport. Every other file is byte for byte what the render writes without it. Refused with `--slot`/`--hide`, because the geometry is the whole pose whatever is drawn — **§8.2** |
|
|
212
213
|
| `--record` | `vote` only: a saved vote to check against its ballot and append to the ledger, instead of writing a ballot. This is the command's second mode; it takes no `--candidate` |
|
|
213
214
|
| `--ballot` | `vote --record` only: the ballot the vote answers (default `ballot.html`). Its embedded manifest is what the vote is checked against, so the ballot file is the record of the question |
|
|
214
215
|
| `--ledger` | `vote --record` only: the append-only JSONL the vote lands in (default `votes.jsonl`), one vote per line |
|
|
@@ -318,6 +319,34 @@ a gap:
|
|
|
318
319
|
regions `rotate: 90` lifted every region back byte for byte and drew its
|
|
319
320
|
region attachments with 0 differing pixels — and made neither page smaller,
|
|
320
321
|
because both were already on the smallest power-of-two page their area allows.
|
|
322
|
+
Measured again on twelve region sets (issue #866: those two rigs and ten
|
|
323
|
+
production rigs of 20 to 22 parts), packing each with the same search twice —
|
|
324
|
+
as shipped, and with every part also tried turned under the same placement
|
|
325
|
+
score:
|
|
326
|
+
|
|
327
|
+
| Set | `pot` page | turned | `free` page | turned |
|
|
328
|
+
| --- | --- | --- | --- | --- |
|
|
329
|
+
| 22-part painting | 1024x2048 | 0.00 % | 1888x697 | −1.14 % |
|
|
330
|
+
| 20-part painting | 512x2048 | 0.00 % | 480x1166 | +2.31 % |
|
|
331
|
+
| P1 | 512x2048 | 0.00 % | 416x1633 | +1.43 % |
|
|
332
|
+
| P2 | 1024x1024 | 0.00 % | 1184x810 | +0.70 % |
|
|
333
|
+
| P3 | 512x1024 | 0.00 % | 480x1000 | +2.96 % |
|
|
334
|
+
| P4 | 1024x2048 | 0.00 % | 864x1346 | −6.16 % |
|
|
335
|
+
| P5 | 2048x2048 | 0.00 % | 1344x1768 | −13.37 % |
|
|
336
|
+
| P6 | 1024x2048 | 0.00 % | 800x1934 | −1.62 % |
|
|
337
|
+
| P7 | 512x2048 | 0.00 % | 1024x928 | −0.05 % |
|
|
338
|
+
| P8 | 512x2048 | 0.00 % | 608x1250 | −4.96 % |
|
|
339
|
+
| P9 | 1024x2048 | +100.00 % | 1024x2037 | +6.51 % |
|
|
340
|
+
| P10 | 512x2048 | 0.00 % | 352x1433 | +0.70 % |
|
|
341
|
+
|
|
342
|
+
Under `pot` turning shrank no page. Under `free` the largest gain, 13.37 %, is
|
|
343
|
+
inside the search's own noise: the neighbouring width on the 32-pixel grid
|
|
344
|
+
`free` then searched moves that set's unturned page by 18.13 %, and trying
|
|
345
|
+
every width instead finds an unturned page 10.22 % smaller. With every width
|
|
346
|
+
tried, no set gains more than 5.05 % from turning — so the packer still never
|
|
347
|
+
turns a region. (The `free` column is that day's 32-pixel grid. `free` has
|
|
348
|
+
tried every width since issue #872 — below — so the 5.05 % is the turn gain on
|
|
349
|
+
the search that ships.)
|
|
321
350
|
- **no re-ordering of anything the skeleton says.** `skeleton.json` from a packed
|
|
322
351
|
build is **byte-identical** to the unpacked one, because sizes are still
|
|
323
352
|
measured from the loose PNGs and packing is an output arrangement.
|
|
@@ -357,7 +386,7 @@ every power-of-two pair up to `--page-size` in order of area and writes the firs
|
|
|
357
386
|
the pack fits — it never doubles past a smaller page that would do. So when the
|
|
358
387
|
parts' own area already exceeds the next power-of-two page down, no `pot` packer
|
|
359
388
|
can do better, and `free` is the lever left. Under `free` the candidate widths
|
|
360
|
-
are
|
|
389
|
+
are **every width** from the widest padded part up to `--page-size`; at each
|
|
361
390
|
width the parts are placed by the same MaxRects pass on a page `--page-size`
|
|
362
391
|
tall, and the height is the bottom edge of the lowest part. The page with the
|
|
363
392
|
least area wins, then the squarer one (smaller |width − height|), then the
|
|
@@ -365,13 +394,65 @@ narrower one — every candidate has a different width, so the order is total an
|
|
|
365
394
|
two builds of the same parts write the same bytes. Spilling, the packing order
|
|
366
395
|
and `--page-size` as the ceiling are the same as under `pot`.
|
|
367
396
|
|
|
397
|
+
Every width is affordable because of two bounds, and neither can change the
|
|
398
|
+
page: a width is not tried when the parts' area cannot fit it or when its
|
|
399
|
+
tallest part alone already makes it larger than the best page so far, and a
|
|
400
|
+
placement stops as soon as its lowest part makes it larger than that best page.
|
|
401
|
+
Both are strict, so a width that could tie the best is always placed to the end.
|
|
402
|
+
|
|
403
|
+
**Why every width, measured (issue #872).** Until then the widths were the
|
|
404
|
+
multiples of 32. On twelve region sets — the two painting rigs below and ten
|
|
405
|
+
production rigs of 20 to 22 parts, the sets of the rotation table above — and,
|
|
406
|
+
as a held-out check, on the ten atlases of the fetched `examples/` corpus, each
|
|
407
|
+
search was run against the every-width page (padding 2, `--page-size` 2048).
|
|
408
|
+
Area change against the 32-pixel grid:
|
|
409
|
+
|
|
410
|
+
| Set | 32 px (before) | step 8 | step 4 | coarse 32 → ±31 px of the best 2 | coarse 12 → ±11 px of the best | every width (now) |
|
|
411
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
412
|
+
| 22-part painting | 1888x697 | −1.58 % | −1.58 % | −1.68 % | −1.68 % | 967x1338, −1.68 % |
|
|
413
|
+
| 20-part painting | 480x1166 | 0.00 % | 0.00 % | −0.21 % | −0.21 % | 479x1166, −0.21 % |
|
|
414
|
+
| P1 | 416x1633 | −1.92 % | −2.88 % | −2.88 % | −2.88 % | 404x1633, −2.88 % |
|
|
415
|
+
| P2 | 1184x810 | 0.00 % | 0.00 % | −0.08 % | −0.08 % | 1183x810, −0.08 % |
|
|
416
|
+
| P3 | 480x1000 | 0.00 % | 0.00 % | 0.00 % | 0.00 % | 480x1000, 0.00 % |
|
|
417
|
+
| P4 | 864x1346 | −3.07 % | −3.07 % | −2.66 % | −3.15 % | 1231x915, −3.15 % |
|
|
418
|
+
| P5 | 1344x1768 | −9.90 % | −10.16 % | −10.22 % | −10.22 % | 1427x1495, −10.22 % |
|
|
419
|
+
| P6 | 800x1934 | −1.00 % | −1.50 % | −1.50 % | −1.50 % | 788x1934, −1.50 % |
|
|
420
|
+
| P7 | 1024x928 | −0.78 % | −0.78 % | −0.88 % | −0.88 % | 1015x928, −0.88 % |
|
|
421
|
+
| P8 | 608x1250 | 0.00 % | 0.00 % | −0.16 % | −0.16 % | 607x1250, −0.16 % |
|
|
422
|
+
| P9 | 1024x2037 | −2.34 % | −2.73 % | −2.93 % | −2.93 % | 994x2037, −2.93 % |
|
|
423
|
+
| P10 | 352x1433 | 0.00 % | −1.14 % | −1.70 % | −1.70 % | 346x1433, −1.70 % |
|
|
424
|
+
| worst miss of the every-width page, twelve sets | 11.38 % | 1.73 % | 0.58 % | 0.50 % | 0.00 % | 0.00 % |
|
|
425
|
+
| worst miss, the ten `examples/` atlases | 4.12 % | 2.67 % | 0.77 % | 0.82 % | 0.82 % | 0.00 % |
|
|
426
|
+
| cell placements, twelve sets | 12,361 | 49,063 | 98,021 | 40,230 | 38,371 | 47,978 |
|
|
427
|
+
|
|
428
|
+
The rule was the cheapest search that lands on the every-width page. Flipping
|
|
429
|
+
the packer's own last tie-break (leftmost before topmost) changes no page on any
|
|
430
|
+
of the 22 sets, so there is no tie-break noise to land "within": the target is
|
|
431
|
+
the page itself. The coarse-to-fine search that met it on all twelve sets missed
|
|
432
|
+
it on the first held-out atlas, and its failures do not follow its grid — on
|
|
433
|
+
the twelve sets, refining around the best width of a 9-pixel grid misses where
|
|
434
|
+
10 to 12 do not, and a 14-pixel grid misses again —
|
|
435
|
+
because the area as a function of the width has minima one pixel wide (a 4-pixel
|
|
436
|
+
grid refined around its best four widths still misses one). Every width is exact
|
|
437
|
+
by construction. Its placement count overstates its cost, because an abandoned
|
|
438
|
+
placement stops early: all twelve sets take 53 ms in the measuring harness,
|
|
439
|
+
against 22 ms for the 32-pixel grid and 810 ms for every width unbounded, and a
|
|
440
|
+
whole `build --pack --page-edges free` of the 22-part rig takes the same wall
|
|
441
|
+
time either way. ⚠️ The cost grows with the part count faster than the grid's:
|
|
442
|
+
on generated sets of 100 and 300 small parts, where no bound prunes much, the
|
|
443
|
+
search places 15 times the grid's cells.
|
|
444
|
+
|
|
368
445
|
The default stays `pot` because every runtime accepts a power-of-two page and
|
|
369
446
|
the editor's own packer writes one by default: 9 of the 10 atlases in the
|
|
370
447
|
fetched `examples/` corpus are power-of-two on both edges. What `free` costs is
|
|
371
448
|
the exactness above. `x / pageWidth` is no longer exact, so a **region**
|
|
372
449
|
attachment's samples join a mesh's at one least significant bit against the
|
|
373
450
|
loose build instead of 0. No region's bytes change: every one lifts back byte for
|
|
374
|
-
byte, as under `pot`. Observed on two painting rigs of 22 and 20 parts
|
|
451
|
+
byte, as under `pot`. Observed on two painting rigs of 22 and 20 parts, on the
|
|
452
|
+
32-pixel grid `free` shipped with (issue #860); every width now packs them at
|
|
453
|
+
967x1338 = 1,293,846 (−38.3 %, 91.2 % covered, 59.7 % opaque) and 479x1166 =
|
|
454
|
+
558,514 (−46.7 %, 93.4 % covered, 53.9 % opaque), and the render columns are
|
|
455
|
+
held to their bound of one by `PK72` on every build rather than re-measured here:
|
|
375
456
|
|
|
376
457
|
| Rig | `pot` page | `free` page | Area | Covered | Opaque (alpha > 0) | Render vs loose, region attachments only | Render vs loose, whole rig |
|
|
377
458
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
@@ -827,6 +908,7 @@ bun cli.ts check --candidate path/to/spine --frames path/to/frames [--skin
|
|
|
827
908
|
bun cli.ts bench 3 --candidate path/to/spine [--frames path/to/frames]
|
|
828
909
|
bun cli.ts render --candidate path/to/spine [--animation …] [--skin …] [--fps 12] [--max 256]
|
|
829
910
|
bun cli.ts render --candidate path/to/spine --slot <name,…> | --hide <name,…> # part of the rig, same grid
|
|
911
|
+
bun cli.ts render --candidate path/to/spine --geometry # + geometry.json per set: the numbers the frames were drawn from
|
|
830
912
|
bun cli.ts preview --candidate path/to/spine [--candidate path/to/another …] [--animation …] [--out preview.html]
|
|
831
913
|
bun cli.ts vote --candidate path/to/a --candidate path/to/b [--out ballot.html]
|
|
832
914
|
bun cli.ts vote --record vote-<id>.json [--ballot ballot.html] [--ledger votes.jsonl]
|
|
@@ -921,7 +1003,11 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
|
921
1003
|
in draw order; a slot whose art lives only under another skin is refused naming
|
|
922
1004
|
that skin (pass `--skin`); the two flags together are refused. `frames.json`
|
|
923
1005
|
records the subset as `slots` or `hidden`, and **`check` refuses such a set as
|
|
924
|
-
a reference** (§9). `
|
|
1006
|
+
a reference** (§9). `--geometry` writes the numbers each frame was drawn from
|
|
1007
|
+
beside it — every bone's world transform and every attachment's world vertices
|
|
1008
|
+
after skinning, with their rest geometry — for a judgement the pixels cannot
|
|
1009
|
+
make: a mesh triangle's stretch, a region read in its own bone's frame (§8.2).
|
|
1010
|
+
`preview`
|
|
925
1011
|
writes one self-contained `.html`: your skeleton, atlas and page PNGs are
|
|
926
1012
|
embedded in it as data URIs and played by the official **Spine Web Player**, so
|
|
927
1013
|
double-clicking it is also the interop proof — what plays there was played by
|
|
@@ -1412,6 +1498,20 @@ renderers and reports its step 5 as a **SKIP** naming that, and the round trip
|
|
|
1412
1498
|
comes back green on the strength of `validate` and `diff`. A skin only **one** side
|
|
1413
1499
|
can draw is the other case entirely, and stays red.
|
|
1414
1500
|
|
|
1501
|
+
🔢 **"Nothing to draw" is said only of a skeleton that posed no vertex at all.** One
|
|
1502
|
+
whose attachment is there but posed to `Infinity` or `NaN` is refused by the bone or
|
|
1503
|
+
the vertex that holds it and the value — `rigc render: the setup pose: bone "head" has
|
|
1504
|
+
worldX Infinity; a world transform is finite`, or `… animation "turn" frame 31 at 60
|
|
1505
|
+
fps (t=0.5167s): bone "head" has a NaN; …` for a key, or `… slot "x" attachment "y"
|
|
1506
|
+
vertex 3 has x NaN; a posed vertex is finite` — exit 1, before anything is written.
|
|
1507
|
+
It is the sentence `render --geometry` refuses on (§8.2), from the same code, and
|
|
1508
|
+
`rigc check` prints it after `the candidate is posed to a number that is not finite,
|
|
1509
|
+
so no frame of it can be compared:`. The bone is named before its vertices because
|
|
1510
|
+
it is the cause; look for its value in the skeleton file — a setup `x`/`y`/`rotation`,
|
|
1511
|
+
or a timeline key at the time given — and, for a rig spec you wrote, in `rigc explain`,
|
|
1512
|
+
which prints every bone's local transform. A frame's time is the framing pass's own
|
|
1513
|
+
(every animation at 60 fps); the key that overflowed is at or just before it.
|
|
1514
|
+
|
|
1415
1515
|
**Region attachment** ([Spine: region attachments](http://esotericsoftware.com/spine-regions)),
|
|
1416
1516
|
the default `type`:
|
|
1417
1517
|
|
|
@@ -5328,6 +5428,13 @@ rather than linked, like the references at the top of this page: that page cites
|
|
|
5328
5428
|
corpus inventory, and a ladder run reads this guide without following its references
|
|
5329
5429
|
out of it.)
|
|
5330
5430
|
|
|
5431
|
+
🔢 **A fourth surface is a pose that is not finite**, which `render` and `check`
|
|
5432
|
+
refuse before drawing anything, naming the bone or vertex and its value. `validate`
|
|
5433
|
+
does not always get there first: a bone `rotation` of `1e309`, in the setup pose or
|
|
5434
|
+
in a key, was measured leaving `A10_NO_NAN_AFTER_STEPPING` green, because the bone's
|
|
5435
|
+
world position stays finite while its matrix is `NaN`. The sentence, and where to
|
|
5436
|
+
look for the number, is under *nothing to draw* in §3.4.
|
|
5437
|
+
|
|
5331
5438
|
### 5.1 Compile errors — before the gate
|
|
5332
5439
|
|
|
5333
5440
|
A `CompileError` names the object and the field, and nothing is written.
|
|
@@ -6262,6 +6369,124 @@ the loop with **§9**: the fit's own number says how near this pose is to this f
|
|
|
6262
6369
|
and only `check` says whether the shot is the shot. **§9.3** is the list of what even
|
|
6263
6370
|
that cannot see.
|
|
6264
6371
|
|
|
6372
|
+
### 8.2 The numbers the frames were drawn from — `render --geometry`
|
|
6373
|
+
|
|
6374
|
+
A frame set is pixels, and some judgements about a rig are not about pixels: how far
|
|
6375
|
+
a mesh triangle is stretched over its rest shape, or whether a region holds still in
|
|
6376
|
+
its own bone's frame. Both need the posed numbers, and both need them **on the frames'
|
|
6377
|
+
own grid** — a second tool re-sampling the animation would step the runtime its own
|
|
6378
|
+
way and drift from the pictures it is meant to explain. So `render --geometry` writes
|
|
6379
|
+
them beside the pictures, off the very frames it drew:
|
|
6380
|
+
|
|
6381
|
+
```bash
|
|
6382
|
+
bun cli.ts render --candidate path/to/spine --geometry [--animation …] [--skin …] [--fps 12]
|
|
6383
|
+
```
|
|
6384
|
+
|
|
6385
|
+
Each frame directory gains a `geometry.json` beside its `f####.png`. Nothing else
|
|
6386
|
+
changes: the PNGs, `contact.png` and `frames.json` are byte for byte what the same
|
|
6387
|
+
command writes without the flag. The export costs a small fraction of the render —
|
|
6388
|
+
measured on `spineboy-pro` at 12 fps, 190 frames: about 30 ms for the eleven files
|
|
6389
|
+
against about 610 ms for the rasterising alone — which is why there is no
|
|
6390
|
+
geometry-only mode: a consumer judging frames wants the frames, and skipping them
|
|
6391
|
+
would save little.
|
|
6392
|
+
|
|
6393
|
+
⛔ **Not with `--slot`/`--hide`.** The two are refused together by name, exit 2,
|
|
6394
|
+
nothing written. A subset is a statement about which pixels are *drawn*; the
|
|
6395
|
+
geometry is the whole pose whatever a picture leaves out, so beside a subset's frames
|
|
6396
|
+
it would describe slots those frames do not draw. The whole-rig run writes the same
|
|
6397
|
+
file either way.
|
|
6398
|
+
|
|
6399
|
+
**The file, field by field.** `spec` is `rigc-geometry/1`. The header fields sit one
|
|
6400
|
+
per line and each `rest` entry and each frame on a line of its own, so a diff of two
|
|
6401
|
+
exports names the frame that moved. Two exports of one skeleton are byte-identical.
|
|
6402
|
+
|
|
6403
|
+
| Field | What it holds |
|
|
6404
|
+
| --- | --- |
|
|
6405
|
+
| `coordinates` | the string `spine world, y up, world units` — every position below is in the skeleton's world, not frame pixels |
|
|
6406
|
+
| `animation` | the animation, or `null` for a skeleton with none (its one frame is the setup pose) |
|
|
6407
|
+
| `skin` | the skin these frames were posed under — absent when none was set, exactly as in `frames.json` |
|
|
6408
|
+
| `fps` | the rate the frames were sampled at |
|
|
6409
|
+
| `viewport` | `frames.json`'s own `viewport` object, the same numbers: world box `x`, `y`, `width`, `height`, `scale` (frame pixels per world unit), `pixelWidth`, `pixelHeight`. World to frame pixel is `px = (x − viewport.x) · scale`, `py = (viewport.y + viewport.height − y) · scale`, which is the projection the renderer used |
|
|
6410
|
+
| `bones` | every bone in declaration order: `{ name, parent }`, `parent` `null` for the root |
|
|
6411
|
+
| `slots` | every slot in declaration order: `{ name, bone }` — the bone the slot hangs from |
|
|
6412
|
+
| `rest` | one entry per `(slot, attachment)` any frame shows, in order of first appearance: `slot`, `attachment`, `kind` (`region` or `mesh`), `vertices` at rest, `triangles` (vertex index triplets — a region's are the runtime's `0 1 2 2 3 0`), and for a mesh `hull` (the number of hull vertices, which are the first ones) and `uvs` (the attachment's own, `u, v` per vertex over the untrimmed drawing, y down) |
|
|
6413
|
+
| `setup` | the setup pose, sampled the way a skeleton with no animation is rendered: `{ bones, attachments }` |
|
|
6414
|
+
| `frames` | one per PNG, in index order: `{ index, time, bones, attachments }`. `index` is the number in `f####.png`, `time` is `index / fps` in seconds |
|
|
6415
|
+
|
|
6416
|
+
Inside `setup` and each frame:
|
|
6417
|
+
|
|
6418
|
+
- `bones` — every bone in declaration order: `{ name, a, b, c, d, worldX, worldY }`,
|
|
6419
|
+
the world transform, so a point `(lx, ly)` in the bone's own frame lands at
|
|
6420
|
+
`(a·lx + b·ly + worldX, c·lx + d·ly + worldY)`. The matrix is complete — rotation,
|
|
6421
|
+
scale and shear all live in `a b c d`.
|
|
6422
|
+
- `attachments` — every slot showing a region or a mesh, in the frame's **draw
|
|
6423
|
+
order**: `{ slot, attachment, vertices, color }`. `vertices` are `x, y` per vertex
|
|
6424
|
+
in world units after skinning and deform — the runtime's own
|
|
6425
|
+
`computeWorldVertices` over the **whole** attachment. A region's four are its
|
|
6426
|
+
corners in the order bottom-left, top-left, top-right, bottom-right; a mesh's are
|
|
6427
|
+
in the attachment's own order, so vertex `i` is the same vertex in every frame and
|
|
6428
|
+
in `rest`. `color` is the slot colour times the attachment colour, straight RGBA
|
|
6429
|
+
`0..1` — the same tint the frame was drawn with.
|
|
6430
|
+
|
|
6431
|
+
What `rest` means, precisely: the **setup pose's bones with the slot's deform
|
|
6432
|
+
empty**, taken for every attachment the file shows — including one the setup pose
|
|
6433
|
+
does not show, which a slot only swaps to later. For an attachment the setup pose
|
|
6434
|
+
does show, its `rest` vertices and its `setup` vertices are the same numbers bit for
|
|
6435
|
+
bit, because they come off one posed skeleton.
|
|
6436
|
+
|
|
6437
|
+
⚠️ **What is deliberately not in it.** Slots `--slot`/`--hide` would leave out are in
|
|
6438
|
+
it (see above). A clip is not applied: a clipping attachment removes pixels, and the
|
|
6439
|
+
clipper's output is a new triangle list whose vertices are not the attachment's and
|
|
6440
|
+
are not numbered like them — so `vertices` are the attachment's own, and the frames
|
|
6441
|
+
show what the clip left of them. Bounding boxes, points, paths and clipping polygons
|
|
6442
|
+
draw nothing and are not listed. Numbers are printed the way every JSON file this
|
|
6443
|
+
tool writes prints them — the shortest decimal that reads back as the same double —
|
|
6444
|
+
so a vertex in the file *is* the runtime's vertex, not a rounding of it; a pose that
|
|
6445
|
+
is not finite is refused rather than written as `null`, naming where — the setup pose,
|
|
6446
|
+
`animation "a" frame i at F fps (t=…s)`, or the rest table — and then the bone and
|
|
6447
|
+
its world-transform field, or the slot, the attachment and the vertex, with the
|
|
6448
|
+
value. Bones come first, so a bone that overflowed is named as itself rather than as
|
|
6449
|
+
its first vertex. Since issue #873 plain `render` refuses the same pose in the same
|
|
6450
|
+
sentence while framing it (§3.4's note under *nothing to draw*), so the flag is not
|
|
6451
|
+
needed to find out which number it was.
|
|
6452
|
+
|
|
6453
|
+
**The two judgements the file was designed for**, written once so a consumer
|
|
6454
|
+
implements what the file promises:
|
|
6455
|
+
|
|
6456
|
+
1. **Texture stretch of a mesh triangle.** For each `rest` entry of kind `mesh` and
|
|
6457
|
+
each triangle `(i, j, k)` in its `triangles`, over every frame whose `attachments`
|
|
6458
|
+
carry the same `slot` and `attachment`:
|
|
6459
|
+
|
|
6460
|
+
`stretch = max over frames, max over the edges (i,j) (j,k) (k,i) of |posed edge| / |rest edge|`
|
|
6461
|
+
|
|
6462
|
+
where an edge's length is `hypot(x_j − x_i, y_j − y_i)` over that frame's
|
|
6463
|
+
`vertices` and over `rest`'s. The worst triangle is named by its `slot`, its
|
|
6464
|
+
triangle index and its three vertex indices. A value of exactly `1` is a
|
|
6465
|
+
triangle no frame moved. It is measured against rest, so it counts what the
|
|
6466
|
+
bones do to a weighted mesh as well as what a deform key does — unlike the
|
|
6467
|
+
`DEFORM` block, which compares a key against the same bones with the deform
|
|
6468
|
+
cleared.
|
|
6469
|
+
2. **A region still in its bone's own frame.** Take the region's `slot`, look its
|
|
6470
|
+
`bone` up in `slots`, and for each frame carry its four corners into that bone's
|
|
6471
|
+
frame by the inverse of the bone's world transform:
|
|
6472
|
+
|
|
6473
|
+
`det = a·d − b·c`, `dx = x − worldX`, `dy = y − worldY`,
|
|
6474
|
+
`lx = (d·dx − b·dy) / det`, `ly = (−c·dx + a·dy) / det`
|
|
6475
|
+
|
|
6476
|
+
Against the same four carried from `setup`, the largest difference over frames is
|
|
6477
|
+
the region's motion *relative to its bone*: zero, to double rounding, for a region
|
|
6478
|
+
nothing but its bone moves. A face region measured against a head bone this way
|
|
6479
|
+
is the face with the head's own turn, translation and scale taken out.
|
|
6480
|
+
|
|
6481
|
+
**Measured on the example corpus** at 12 fps (these are one run's figures, not
|
|
6482
|
+
thresholds): `spineboy-pro`'s eleven files total 5.1 MB, from 72 kB (`aim`, one
|
|
6483
|
+
frame) to 1.46 MB (`death`, 60 frames), and `sack-pro`'s four 1.4 MB. The largest
|
|
6484
|
+
stretch is `sack-pro` `fall-in`, `cape-back` triangle 15, ×6.744; the largest
|
|
6485
|
+
still-in-frame residual over every region of `spineboy-pro` is 4.7e-12 world units
|
|
6486
|
+
(`portal-streaks2`, whose bone scales; `sack-pro` has no region).
|
|
6487
|
+
Topology and UVs sit in `rest` rather than on every frame because they do not
|
|
6488
|
+
change: repeating them per frame would add 35 % to `spineboy-pro`'s files.
|
|
6489
|
+
|
|
6265
6490
|
---
|
|
6266
6491
|
|
|
6267
6492
|
## 9. Checking against the frames — `rigc check`
|
|
@@ -8588,7 +8813,7 @@ ambiguous one ends with a sentence saying which reading its figures support:
|
|
|
8588
8813
|
|
|
8589
8814
|
- **below the floor** — a 40x28 part of one flat colour, cut from a plain block it fits anywhere inside:
|
|
8590
8815
|
|
|
8591
|
-
> part is 40x28 px (span 40 frame px, opaque 1) with texture 0, detail 0;
|
|
8816
|
+
> part is 40x28 px (span 40 frame px, opaque 1) with texture 0, detail 0; 17 candidate(s), best 0.0000, next 0.0000, spread 0.0000 — below the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up): pose placed 7% of the measured parts this plain, so this part is too plain for pose to tell its placements apart; that says nothing about whether the cut is right
|
|
8592
8817
|
|
|
8593
8818
|
- **above the floor** — a 200x150 black-and-white checker the frame does not hold:
|
|
8594
8819
|
|
|
@@ -8615,24 +8840,24 @@ Found within 2 px of the truth, out of 15 trials per cell (free search, default
|
|
|
8615
8840
|
|
|
8616
8841
|
| texture level (mean) | 24 px | 32 px | 48 px | 64 px | 96 px | 128 px | 192 px |
|
|
8617
8842
|
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
8618
|
-
| `native` (detail 3.49) |
|
|
8619
|
-
| `blur1` (detail 1.58) |
|
|
8620
|
-
| `blur2` (detail 0.59) |
|
|
8621
|
-
| `flat` (detail 0.00) | 0/15 (14 amb, 1 wrong) | 0/15 (14 amb, 1 wrong) | 0/15 (
|
|
8843
|
+
| `native` (detail 3.49) | 13/15 (2 amb) | 15/15 | 15/15 | 15/15 | 13/15 (1 amb, 1 wrong) | 14/15 (1 wrong) | 14/15 (1 wrong) |
|
|
8844
|
+
| `blur1` (detail 1.58) | 12/15 (3 amb) | 12/15 (2 amb, 1 wrong) | 13/15 (2 amb) | 12/15 (3 amb) | 12/15 (2 amb, 1 wrong) | 11/15 (1 amb, 3 wrong) | 11/15 (2 amb, 2 wrong) |
|
|
8845
|
+
| `blur2` (detail 0.59) | 9/15 (5 amb, 1 wrong) | 9/15 (4 amb, 2 wrong) | 9/15 (4 amb, 2 wrong) | 8/15 (3 amb, 4 wrong) | 7/15 (3 amb, 5 wrong) | 6/15 (4 amb, 5 wrong) | 7/15 (3 amb, 5 wrong) |
|
|
8846
|
+
| `flat` (detail 0.00) | 0/15 (14 amb, 1 wrong) | 0/15 (14 amb, 1 wrong) | 0/15 (13 amb, 2 wrong) | 0/15 (11 amb, 4 wrong) | 0/15 (13 amb, 2 wrong) | 0/15 (12 amb, 3 wrong) | 0/15 (12 amb, 3 wrong) |
|
|
8622
8847
|
|
|
8623
8848
|
The same trials, grouped by `detail`:
|
|
8624
8849
|
|
|
8625
8850
|
| detail | found | ambiguous | wrong | refused |
|
|
8626
8851
|
| --- | --- | --- | --- | --- |
|
|
8627
|
-
| under 0.25 | 8/119 |
|
|
8628
|
-
| 0.25–0.5 |
|
|
8629
|
-
| 0.5–0.75 |
|
|
8630
|
-
| 0.75–1 |
|
|
8631
|
-
| 1–1.5 |
|
|
8632
|
-
| 1.5–2 |
|
|
8633
|
-
| 2–3 |
|
|
8634
|
-
| 3–4 |
|
|
8635
|
-
| 4 and over |
|
|
8852
|
+
| under 0.25 | 8/119 | 89 | 22 | 0 |
|
|
8853
|
+
| 0.25–0.5 | 1/3 | 1 | 1 | 0 |
|
|
8854
|
+
| 0.5–0.75 | 48/81 | 14 | 19 | 0 |
|
|
8855
|
+
| 0.75–1 | 17/34 | 14 | 3 | 0 |
|
|
8856
|
+
| 1–1.5 | 25/25 | 0 | 0 | 0 |
|
|
8857
|
+
| 1.5–2 | 34/48 | 12 | 2 | 0 |
|
|
8858
|
+
| 2–3 | 40/44 | 1 | 3 | 0 |
|
|
8859
|
+
| 3–4 | 34/34 | 0 | 0 | 0 |
|
|
8860
|
+
| 4 and over | 30/32 | 2 | 0 | 0 |
|
|
8636
8861
|
|
|
8637
8862
|
**Method.** The truth set is `examples/spineboy`: its setup pose and one frame of
|
|
8638
8863
|
`walk` (a third of the way in). For every trial the figure is rendered twice with one
|
|
@@ -8654,25 +8879,63 @@ part has a low per-pixel gradient and still places — and size alone has no
|
|
|
8654
8879
|
measurable effect across 24–192 px at any level.
|
|
8655
8880
|
|
|
8656
8881
|
**The edge**, derived by `deriveFloor` as the largest rung of a fixed ladder under
|
|
8657
|
-
which at most a tenth of the trials were found: **`detail` 0.5**. Under it, **
|
|
8658
|
-
of the trials were found (
|
|
8882
|
+
which at most a tenth of the trials were found: **`detail` 0.5**. Under it, **7.4%**
|
|
8883
|
+
of the trials were found (9/122); at or over it, **76.5%** (228/298). A part is below
|
|
8659
8884
|
the floor when its `detail` is under 0.5, or when its own longest side is under 24 px,
|
|
8660
8885
|
the smallest size measured (there, nothing measured says `pose` can place it).
|
|
8661
8886
|
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8887
|
+
**What the search carries, and why.** The coarse pass scores the part at anchor
|
|
8888
|
+
cells an **eighth** of its own span apart, and every scale rung sends its **five**
|
|
8889
|
+
best distinct places down to the finer levels. Both were set on this grid (issue
|
|
8890
|
+
#865): of the 65 failures whose truth scored better than every answer reported, 38
|
|
8891
|
+
had a truth that beat its whole scale rung at the coarse level while the anchor cell
|
|
8892
|
+
holding it did not — at a quarter-span stride a thin gun or a gapped fist falls between
|
|
8893
|
+
two anchors — and only 3 had it ranked under the old cutoff of three. The finer grid
|
|
8894
|
+
took the grid from 185 to 227 trials found and those misses from 65 to 14, for about
|
|
8895
|
+
10–14% more CPU over the grid (615 s of user time before it, 678 s and 704 s on two runs after).
|
|
8896
|
+
|
|
8897
|
+
The refinement then polishes each place down the pyramid, and at full resolution a
|
|
8898
|
+
polish that has stopped tries its scale **up by half a coarse rung** (2^(1/6)), re-fits
|
|
8899
|
+
the position there, and restarts if that scores better; fifteen places survive each
|
|
8900
|
+
level. Both were set on this grid too (issue #877): of the 14 misses left after #865,
|
|
8901
|
+
12 reached full resolution with the truth's place ranked first or second and the
|
|
8902
|
+
full-resolution objective preferring the truth, and stopped 2–7 px off it at a shrunk
|
|
8903
|
+
scale — the objective couples scale to position along a diagonal, so every
|
|
8904
|
+
one-axis step from there is worse while a joint one is better. None of them had
|
|
8905
|
+
accepted a worse residual. One more was the 24 px shin, its truth's place ranked
|
|
8906
|
+
15th at the 2x level and cut at twelve. Up only because the objective charges part
|
|
8907
|
+
pixels that land off the figure and nothing for figure left uncovered, so the
|
|
8908
|
+
reduced levels err small: every stuck point sat below scale 1. It took the grid from
|
|
8909
|
+
227 to 237 found and the misses from 14 to 8 (12 trials gained, 2 lost — each a
|
|
8910
|
+
blurred part now reported with a second placement at the same spot and another
|
|
8911
|
+
scale, one of them with the truth itself as its best answer), for 43% more
|
|
8912
|
+
refinement samples and the coarse pass untouched.
|
|
8913
|
+
|
|
8914
|
+
⚠️ **What this is and is not.** The grid is not a reliability line: five native cells
|
|
8915
|
+
place 14 or 15 of 15 and the trials at `detail` 3 and over place 64 of 66, but the
|
|
8916
|
+
goggles still place 12 of 42, and a threshold read off two dozen parts of one figure
|
|
8917
|
+
would be a claim about that figure. What the grid can separate is **whose** failure a
|
|
8918
|
+
failure is. `bun tools/pose_floor.ts` prints the residual at the truth for every
|
|
8919
|
+
failed trial and marks it **missed** when the truth scores better than everything
|
|
8920
|
+
reported and nothing reported is on it — a failure the search could fix. The other
|
|
8921
|
+
failures are the objective preferring another placement (a blurred part shrunk into
|
|
8922
|
+
its own region scores better than its truth) or the truth reported and tied, and no
|
|
8923
|
+
search can fix those. After #877, 8 of the 183 failed trials are misses, and 7 of
|
|
8924
|
+
those sit within 3.1 px of the truth rather than on another hill; the `--from`
|
|
8925
|
+
listing prints, for each, where the refinement left the truth level by level. All this was
|
|
8926
|
+
measured on **ideal cuts**, the frame's own pixels, where a residual at the truth is
|
|
8927
|
+
near zero. A generated cut also disagrees with its picture, which can only make it
|
|
8928
|
+
harder to place. So the edge is where `pose` fails even on ideal cuts, and never a
|
|
8929
|
+
guarantee above it.
|
|
8671
8930
|
|
|
8672
8931
|
Re-run it with `bun tools/pose_floor.ts --textures <level> --json <out>` (one level per
|
|
8673
8932
|
process parallelises it), then `bun tools/pose_floor.ts --from <a.json>,<b.json>,…` to
|
|
8674
|
-
print the table
|
|
8675
|
-
its cells
|
|
8933
|
+
print the table, derive the edge and list the misses per cell. `PO22` in
|
|
8934
|
+
`bun run selftest` re-measures three of its cells, `PO24` holds those three cells to
|
|
8935
|
+
no miss at all, and `PO23` places the 48 px fist #865 was made for; `PO28` and
|
|
8936
|
+
`PO29` place the four trials #877 was made for and read where the shin survived the
|
|
8937
|
+
cutoff, and `PO26` holds every polish to strictly descending residuals, read through
|
|
8938
|
+
the same trace.
|
|
8676
8939
|
|
|
8677
8940
|
## 12. Reading the half of that picture `pose` refuses — `rigc chainfit`
|
|
8678
8941
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "AI-authored Spine 2D rigging and animation, verified before it is written. rigc compiles a rig spec into Spine 4.3 skeleton data, round-trips it through spine-core and named assertions, and emits nothing that fails. Output imports into the Spine editor. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|