spine-rigc 0.25.5 → 0.25.6
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 +5 -2
- package/cli.ts +124 -7
- package/docs/AUTHORING.md +200 -28
- package/docs/INGEST.md +10 -2
- package/package.json +1 -1
- package/src/atlas.ts +11 -2
- package/src/check.ts +59 -1
- package/src/compile.ts +109 -32
- package/src/emit.ts +76 -41
- package/src/mesh.ts +40 -0
- package/src/rig.ts +5 -1
- package/src/slots.ts +169 -27
- package/src/types.ts +16 -0
- package/src/validate.ts +32 -0
package/README.md
CHANGED
|
@@ -413,8 +413,11 @@ that does not belong to it. See
|
|
|
413
413
|
against frames of itself — is the floor the rest are read against, because
|
|
414
414
|
`check` grades nothing and has no pass mark. It is the same instrument and the
|
|
415
415
|
same commands; what changes is that the reference is a build of yours you have
|
|
416
|
-
already looked at, so what it measures is **what your edit did**.
|
|
417
|
-
|
|
416
|
+
already looked at, so what it measures is **what your edit did**. Every drift
|
|
417
|
+
the report prints carries the bound its own match gives it on the line under
|
|
418
|
+
it, so a figure is read against that rather than against a number from a page;
|
|
419
|
+
AUTHORING.md §9.2 says what the two halves of that bound are and why the floor
|
|
420
|
+
is not zero.
|
|
418
421
|
- [docs/LADDER.md](https://github.com/firejune/rigc/blob/main/docs/LADDER.md) is the benchmark: the same job, from a brief
|
|
419
422
|
and rendered frames, scored. [docs/PILOT.md](https://github.com/firejune/rigc/blob/main/docs/PILOT.md) is how to run an
|
|
420
423
|
agent through it and score what comes back.
|
package/cli.ts
CHANGED
|
@@ -68,8 +68,8 @@ import {
|
|
|
68
68
|
} from './src/deformmeasure.ts';
|
|
69
69
|
import { diffLines, diffSkeletons, reportedFigures, sectionFigures, type DiffReport } from './src/diff.ts';
|
|
70
70
|
import { ingest, IngestError, INGEST_GUTTERS, type IngestStage } from './src/ingest.ts';
|
|
71
|
-
import {
|
|
72
|
-
import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, packAtlas } from './src/atlas.ts';
|
|
71
|
+
import { copyAtlasPages } from './src/emit.ts';
|
|
72
|
+
import { DEFAULT_PADDING, DEFAULT_PAGE_SIZE, packAtlas, parseAtlasText } from './src/atlas.ts';
|
|
73
73
|
import { parseJsonWithPosition } from './src/json-position.ts';
|
|
74
74
|
import { KEY_TIME_EPSILON } from './src/timelines.ts';
|
|
75
75
|
import { findRung, RUNG_IDS, type RungSkeleton } from './src/ladder.ts';
|
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
} from './src/render.ts';
|
|
118
118
|
import {
|
|
119
119
|
assertionCountForProfile,
|
|
120
|
+
attachmentRegionJoins,
|
|
120
121
|
CLI_DEFAULT_PROFILE,
|
|
121
122
|
reportLines,
|
|
122
123
|
validate,
|
|
@@ -148,6 +149,14 @@ export type CutTable = Record<string, CutEntry>;
|
|
|
148
149
|
|
|
149
150
|
class UsageError extends Error {}
|
|
150
151
|
|
|
152
|
+
/**
|
|
153
|
+
* `explain` refusing a pair it cannot pose — a usage error in kind, printed
|
|
154
|
+
* without the usage block for `PoseError`'s and `IngestError`'s reason: the
|
|
155
|
+
* message names an attachment, a region and two flags, and reprinting every
|
|
156
|
+
* command's usage under it buries the one line that says what to change.
|
|
157
|
+
*/
|
|
158
|
+
class ExplainError extends Error {}
|
|
159
|
+
|
|
151
160
|
// ---------------------------------------------------------------------------
|
|
152
161
|
// package metadata — the installed version and repository, for `--version`
|
|
153
162
|
// and for naming a remedy `bench` can only give from a repo checkout.
|
|
@@ -1242,14 +1251,21 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1242
1251
|
// correct for a build sitting beside the project it came from and breaks the
|
|
1243
1252
|
// moment the directory is zipped, committed or moved on its own (issue #217).
|
|
1244
1253
|
// Opt-in only: the default stays exactly what it has always been.
|
|
1254
|
+
//
|
|
1255
|
+
// What is copied is what the ATLAS names, not what the image list holds: under
|
|
1256
|
+
// `--atlas-in` the two are different lists, and rebuilding the text from the
|
|
1257
|
+
// second wrote a file the pack never contained — zero bytes for a rig that
|
|
1258
|
+
// declares no parts, one fabricated page per part for a rig that does, both of
|
|
1259
|
+
// them green here because the gate above had already read the compile's own
|
|
1260
|
+
// text (issue #693, `src/emit.ts`).
|
|
1245
1261
|
let atlasText = result.atlasText;
|
|
1246
1262
|
if (flags['copy-images'] !== undefined) {
|
|
1247
|
-
const copied =
|
|
1263
|
+
const copied = copyAtlasPages(atlasText, opts.outDir);
|
|
1248
1264
|
atlasText = copied.atlasText;
|
|
1249
1265
|
console.log(` .. copy-images: ${copied.pages.length} page(s) copied into ${opts.outDir}`);
|
|
1250
1266
|
for (const p of copied.pages) {
|
|
1251
|
-
const note = p.to === basename(p.from) ? '' :
|
|
1252
|
-
console.log(` .. ${p.
|
|
1267
|
+
const note = p.to === basename(p.from) ? '' : ' (renamed — basename collision)';
|
|
1268
|
+
console.log(` .. ${p.to.padEnd(24)} <- ${p.from} (${p.regions} region(s))${note}`);
|
|
1253
1269
|
}
|
|
1254
1270
|
}
|
|
1255
1271
|
|
|
@@ -2434,6 +2450,81 @@ function cmdBoneDist(flags: Record<string, string>): void {
|
|
|
2434
2450
|
if (flags.json !== undefined) writeJson(flags.json, report);
|
|
2435
2451
|
}
|
|
2436
2452
|
|
|
2453
|
+
/**
|
|
2454
|
+
* Refuse a compiled pair whose art `explain` cannot pose through, by name.
|
|
2455
|
+
*
|
|
2456
|
+
* 🚨 `explain` poses the rig — `deformReportLines` loads the emitted pair through
|
|
2457
|
+
* `spine-core` to measure what each deform key did — and a pose resolves EVERY
|
|
2458
|
+
* attachment against the atlas, whether or not anything deforms it. On the specs
|
|
2459
|
+
* `ingest --art none` writes there is nothing to resolve against: the entries
|
|
2460
|
+
* state a size and name no `image`, so the compile atlases nothing, and the load
|
|
2461
|
+
* threw the runtime's own `Region not found in atlas: rear-upper-arm (attachment:
|
|
2462
|
+
* rear-upper-arm)` with a spine-core stack trace under it and exit 1 (measured on
|
|
2463
|
+
* `examples/spineboy/export/spineboy-ess.json`, issue #697). That is the tool
|
|
2464
|
+
* telling an agent about its own internals instead of about the rig, on the one
|
|
2465
|
+
* input `docs/INGEST.md` §2.0 documents as the route through a foreign skeleton.
|
|
2466
|
+
*
|
|
2467
|
+
* ⭐ Both halves of the join are rigc's own readers rather than a second opinion
|
|
2468
|
+
* on somebody else's format: the walk is `attachmentRegionJoins`, which `PS127`
|
|
2469
|
+
* measures against the loader's own `findRegion` calls, and the region names come
|
|
2470
|
+
* from `parseAtlasText`, which is what `--atlas-in` already resolves against.
|
|
2471
|
+
* Names are compared EXACTLY — as `A08` compares them and as `findRegion`
|
|
2472
|
+
* matches them — so a padded region name is a miss on both sides.
|
|
2473
|
+
*
|
|
2474
|
+
* ⚠️ What it deliberately does not do is catch the pose. A blanket `try` around
|
|
2475
|
+
* `skeletonDataFromText` would convert any exception the runtime raises into a
|
|
2476
|
+
* sentence claiming the cause is missing art, and a rigc defect reported under
|
|
2477
|
+
* somebody else's name is the doctrine's second bullet inverted. This refuses
|
|
2478
|
+
* the case it can NAME, before a line of the report is printed, and leaves
|
|
2479
|
+
* anything else to arrive as itself.
|
|
2480
|
+
*/
|
|
2481
|
+
function refuseUnposableArt(result: CompileResult, opts: CompileOptions): void {
|
|
2482
|
+
const regionNames = parseAtlasText(result.atlasText).pages.flatMap((page) => page.regions.map((region) => region.name));
|
|
2483
|
+
const have = new Set(regionNames);
|
|
2484
|
+
const misses: Array<{ at: string; attachment: string; lookup: string }> = [];
|
|
2485
|
+
let lookups = 0;
|
|
2486
|
+
for (const join of attachmentRegionJoins(JSON.parse(result.skeletonText))) {
|
|
2487
|
+
// A `sequence` this walk will not guess at names no region it can check, and
|
|
2488
|
+
// `A08` passes over it for the same reason.
|
|
2489
|
+
if (join.lookups === null) continue;
|
|
2490
|
+
for (const lookup of join.lookups) {
|
|
2491
|
+
lookups++;
|
|
2492
|
+
if (!have.has(lookup)) {
|
|
2493
|
+
misses.push({
|
|
2494
|
+
at: `skin "${join.skin}" slot "${join.slot}" placeholder "${join.placeholder}"`,
|
|
2495
|
+
attachment: join.name,
|
|
2496
|
+
lookup,
|
|
2497
|
+
});
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (misses.length === 0) return;
|
|
2502
|
+
const first = misses[0];
|
|
2503
|
+
// Reported because it was measured, and absent where there is none — `A08`'s
|
|
2504
|
+
// own near-miss clause, in `A08`'s own words.
|
|
2505
|
+
const near = regionNames.find((region) => region.trim().toLowerCase() === first.lookup.toLowerCase());
|
|
2506
|
+
const has =
|
|
2507
|
+
regionNames.length === 0
|
|
2508
|
+
? 'it declares no region at all'
|
|
2509
|
+
: `it declares ${regionNames.length} region(s) and none of them is that${
|
|
2510
|
+
near === undefined ? '' : `, though it does have ${JSON.stringify(near)}`
|
|
2511
|
+
}`;
|
|
2512
|
+
const remedy =
|
|
2513
|
+
opts.atlasInPath === undefined
|
|
2514
|
+
? 'Art reaches a compile two ways and this run took neither: `--atlas-in <pack.atlas>` resolves the parts ' +
|
|
2515
|
+
'against a pack somebody already made, and an "image" per attachment resolves them as loose PNGs under ' +
|
|
2516
|
+
'`--images <dir>` — a spec that states a size and names no image is what `ingest --art none` writes, and ' +
|
|
2517
|
+
'`--atlas-in` is what reads it'
|
|
2518
|
+
: `Either the spec's region name or ${opts.atlasInPath} is the one that moved: fix the name, or point ` +
|
|
2519
|
+
'`--atlas-in` at the pack that has it';
|
|
2520
|
+
throw new ExplainError(
|
|
2521
|
+
`${first.at}: attachment ${JSON.stringify(first.attachment)} wants region ${JSON.stringify(first.lookup)}, ` +
|
|
2522
|
+
`which this build's atlas does not have (${has}). \`explain\` poses the rig to measure its deform keys and a ` +
|
|
2523
|
+
`pose resolves every attachment against the atlas, so there is nothing to pose it against. ${remedy}. ` +
|
|
2524
|
+
`${misses.length} of ${lookups} attachment lookup(s) here resolve to no region.`,
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2437
2528
|
function cmdExplain(flags: Record<string, string>): void {
|
|
2438
2529
|
const { label, opts } = resolveCut(flags);
|
|
2439
2530
|
console.log(`rigc explain ${label}`);
|
|
@@ -2442,6 +2533,13 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
2442
2533
|
console.log(` .. rig ${opts.rigPath}`);
|
|
2443
2534
|
console.log(` .. motion ${opts.motionPath}`);
|
|
2444
2535
|
const result = compile(opts);
|
|
2536
|
+
// Before a line of the report, rather than at the pose two hundred lines in:
|
|
2537
|
+
// the blocks that need the art — DEFORM, meshes, dropped states — all sit
|
|
2538
|
+
// BELOW the pose, so a run that printed the bone and timeline dump and then
|
|
2539
|
+
// refused would be a report missing everything the missing art decides, with
|
|
2540
|
+
// the sentence saying so scrolled off the top. It is the invocation that has
|
|
2541
|
+
// to change, so it is refused before the report it cannot finish (issue #697).
|
|
2542
|
+
refuseUnposableArt(result, opts);
|
|
2445
2543
|
// `compile` has already parsed this file, so the read below cannot fail — but
|
|
2446
2544
|
// it goes through the same parser rather than a cast, because the cast was the
|
|
2447
2545
|
// last one in the repository and issue #307 was about exactly that.
|
|
@@ -3108,8 +3206,20 @@ const COMMANDS: CommandDoc[] = [
|
|
|
3108
3206
|
},
|
|
3109
3207
|
{
|
|
3110
3208
|
name: 'explain',
|
|
3111
|
-
usage: [
|
|
3112
|
-
|
|
3209
|
+
usage: [
|
|
3210
|
+
'rigc explain --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] (it never gates, and writes nothing)',
|
|
3211
|
+
'rigc explain … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made, as build does)',
|
|
3212
|
+
'rigc explain --cut <name> --cuts <cuts.json>',
|
|
3213
|
+
],
|
|
3214
|
+
flags: ['rig', 'motion', 'out', 'manifest', 'images', 'atlas-in', 'cut', 'cuts'],
|
|
3215
|
+
notes: [
|
|
3216
|
+
'this line said "the same arguments as build, minus --profile" and was false in both',
|
|
3217
|
+
'directions (issue #697): --atlas-in was not listed here, so the one flag that lets this',
|
|
3218
|
+
'command read what `ingest --art none` writes was reachable and undocumented, while',
|
|
3219
|
+
'--pack, --page-size, --padding and --copy-images are build\'s and do nothing here —',
|
|
3220
|
+
'they decide what is WRITTEN, and this command writes nothing. What it takes is listed',
|
|
3221
|
+
'above, and that is now the whole of it.',
|
|
3222
|
+
],
|
|
3113
3223
|
},
|
|
3114
3224
|
{
|
|
3115
3225
|
name: 'validate',
|
|
@@ -3482,6 +3592,13 @@ try {
|
|
|
3482
3592
|
console.error(`rigc pose: ${err.message}`);
|
|
3483
3593
|
process.exit(2);
|
|
3484
3594
|
}
|
|
3595
|
+
// A refusal of the invocation, like the two below it, and exit 2 for the same
|
|
3596
|
+
// reason: nothing was posed and nothing was written, so it is the command line
|
|
3597
|
+
// that has to change (issue #697).
|
|
3598
|
+
if (err instanceof ExplainError) {
|
|
3599
|
+
console.error(`rigc explain: ${err.message}`);
|
|
3600
|
+
process.exit(2);
|
|
3601
|
+
}
|
|
3485
3602
|
// Same kind as a PoseError, and printed the same way for the same reason: the
|
|
3486
3603
|
// messages name a path, a bone or an attachment, and reprinting the whole
|
|
3487
3604
|
// usage under them buries the one line that says what to change.
|
package/docs/AUTHORING.md
CHANGED
|
@@ -160,11 +160,11 @@ What the flags mean:
|
|
|
160
160
|
| `--rig` | the rig spec — skeleton structure |
|
|
161
161
|
| `--motion` | the motion spec — time |
|
|
162
162
|
| `--out` | directory for `skeleton.json` + `skeleton.atlas`; atlas page paths and `skeleton.images` are written relative to it |
|
|
163
|
-
| `--copy-images` | `build` only: also copies every
|
|
163
|
+
| `--copy-images` | `build` only: also copies every page **the emitted atlas names** into `--out` and rewrites the atlas to the copies, so the directory is self-contained enough to zip or commit on its own, and points `skeleton.images` at `--out` itself so the editor's import finds the parts beside the skeleton (issue #370; §3.1 says why it is spelled `../<out>/` and not `./`). Under `--atlas-in` those pages are the pack's, not one per part (issue #693 — **§0.2**). Default is unchanged — page paths still point at the source art (issue #217) |
|
|
164
164
|
| `--pack` | `build` only: arrange every part onto **shared** atlas page(s), written into `--out` as real PNGs, instead of one page per part. Lossless — nothing is resampled, trimmed or rotated. Default is unchanged (issue #4) — **§0.1** |
|
|
165
165
|
| `--page-size` | `build --pack` only: the largest page edge (default `2048`). A ceiling, not the size: page edges are powers of two and the one written is the smallest that holds the pack — **§0.1** |
|
|
166
166
|
| `--padding` | `build --pack` only: the gutter each region reserves on every side (default `2`), filled by extending the region's own edge pixels outwards. `0` is not a legal-but-tight choice, it is bleed — **§0.1** |
|
|
167
|
-
| `--atlas-in` | `build`
|
|
167
|
+
| `--atlas-in` | `build` and `explain`: resolve every part against the **regions of a pre-packed `.atlas`** instead of against loose PNGs. Region geometry is read from the file and sizes are descaled by the page's `scale:`; `build` re-emits the atlas into `--out`, re-anchored, and `explain` writes nothing and poses through it — **§0.2**. On `explain` it is the flag that makes a **size-only** spec readable at all (`ingest --art none`), because posing resolves every attachment against an atlas; without it that pair is refused by name rather than thrown through ([#697](https://github.com/firejune/rigc/issues/697), §5.1) |
|
|
168
168
|
| `--images` | where the rig spec's `image` names resolve (overrides the rig's own `images` field, and is relative to your working directory). For `pose` it is the directory of **loose part PNGs to place** — every `.png` in it is a part, in name order. For `chainfit` it is only where each attachment's image name **resolves**: the candidate decides what the parts are, so extra PNGs are unused and a missing name is refused by name (§12.3) |
|
|
169
169
|
| `--manifest` | a cut manifest. Only for a rig with **measured art** behind it; a foreign skeleton has none |
|
|
170
170
|
| `--cut` | `build`, `explain` and `validate`: look up a named cut in `--cuts <cuts.json>`, **instead of** `--rig`/`--motion`/`--out` — the two spellings are one build stated two ways and are refused together. A `cuts.json` is `{ "<name>": { "rig": …, "motion": …, "out": …, "manifest"?: … } }`, every path in it relative to the table's own file, so the table lives with the project that owns the art |
|
|
@@ -319,6 +319,17 @@ texel count beside it so both numbers are visible:
|
|
|
319
319
|
measures the PNG. Reach for `--atlas-in` when the pack is what you were handed, or
|
|
320
320
|
when drawing through the pack's own texels is the point.
|
|
321
321
|
|
|
322
|
+
**What `--out` holds afterwards:** `skeleton.json` and a `skeleton.atlas` that is
|
|
323
|
+
the pack, page paths pointing back at the pack's own PNGs — so `rigc validate
|
|
324
|
+
<that directory>` reads it green with no flags, exactly as it reads a loose
|
|
325
|
+
build's. Add `--copy-images` and the pack's page PNGs are copied in beside the
|
|
326
|
+
skeleton and the page names become their basenames, which is the same directory
|
|
327
|
+
with nothing outside it left to resolve. ⚠️ Until
|
|
328
|
+
[#693](https://github.com/firejune/rigc/issues/693) that flag rebuilt the atlas
|
|
329
|
+
from the parts the rig declared instead of from the pack, and a rebuild through
|
|
330
|
+
`ingest --art none` declares none: the file written was **zero bytes**, on a build
|
|
331
|
+
that printed `PASS` for all four atlas assertions.
|
|
332
|
+
|
|
322
333
|
The emitted `skeleton.atlas` **is** the imported one, verbatim except for its page
|
|
323
334
|
name lines, which are paths and have to be re-anchored to `--out`. Fields rigc
|
|
324
335
|
does not re-serialise (`format:`, `repeat:`, and `scale:` itself) survive the trip
|
|
@@ -518,6 +529,17 @@ bun cli.ts pose --images path/to/parts --frame poseA.png [--out pose.json]
|
|
|
518
529
|
extremes, how far each vertex moved and whether the winding survived
|
|
519
530
|
(**§4.11.2**). It takes no `--profile`, it never gates, and it does not write
|
|
520
531
|
anything — so the figures are readable on a build the gate is refusing.
|
|
532
|
+
|
|
533
|
+
⭐ **It reads what `ingest` writes, given the pack** — `--atlas-in <pack.atlas>`,
|
|
534
|
+
with `build`'s meaning (§0.2). That matters more here than it looks: printing the
|
|
535
|
+
`DEFORM` block means **posing** the rig, a pose resolves *every* attachment
|
|
536
|
+
against the atlas whether or not anything deforms it, and a spec written by
|
|
537
|
+
`ingest --art none` states sizes and names no image. So the size-only pair
|
|
538
|
+
`build --atlas-in` gates green is readable here through the same flag, and
|
|
539
|
+
without it the pair is refused by name at exit 2 rather than posed
|
|
540
|
+
([#697](https://github.com/firejune/rigc/issues/697), §5.1). ⚠️ `--profile`,
|
|
541
|
+
`--pack`, `--page-size`, `--padding` and `--copy-images` are `build`'s and are
|
|
542
|
+
not here: four of them decide what is *written*, and this command writes nothing.
|
|
521
543
|
- **`diff`** compares two skeletons and reports **a ratio per measure** in six
|
|
522
544
|
sections (bones, slots, attachments, constraints, animations, events). It
|
|
523
545
|
deliberately does not combine them into a score: a rig with the right skeleton
|
|
@@ -964,6 +986,12 @@ A skin can also say which bones and constraints it **switches on**, and that nee
|
|
|
964
986
|
one more level, so a skin entry has a second spelling — see §3.4.1. The short one
|
|
965
987
|
above is unchanged and is what almost every rig wants.
|
|
966
988
|
|
|
989
|
+
🔸 **`default` is a name, not a requirement.** A rig may put every attachment in
|
|
990
|
+
named skins and declare no `default` at all, which is what an editor export of a
|
|
991
|
+
multi-skin character gives back; rigc still emits a `default` skin, empty, because
|
|
992
|
+
it always does. An animation keying such a slot resolves its attachment names
|
|
993
|
+
against every skin there is — §4.4 states that rule and §5.1 the one refusal left.
|
|
994
|
+
|
|
967
995
|
🔸 **A skin may fill no slot with anything that needs art, and that build is
|
|
968
996
|
green.** The atlas is built out of what the skins reference, so a rig whose skins
|
|
969
997
|
name no `image` — an empty `default`, or one carrying only a `boundingbox`, a
|
|
@@ -1213,7 +1241,7 @@ ends, so the deformation dies into the pinned rim instead of creasing against it
|
|
|
1213
1241
|
| `inner` | **required.** Where the moving ring sits between the centre (`0`) and the hull (`1`), strictly inside that interval. No default: a ring with no number here is refused, not centred |
|
|
1214
1242
|
| `size` | **required.** The part window, `[w, h]` in pixels, which the UVs and the emitted `width`/`height` are taken from |
|
|
1215
1243
|
| `bias` | optional; absent means authority is radial only. `{ "axis_deg": <screen degrees, y down>, "ramp": [d0, d1] }` — a line through `center` at that angle, with control authority 0 on its negative side and 1 on its positive side, smoothstepped across the signed distances `d0 < d1`. It is what lets a mouth open downward with the upper lip left pinned |
|
|
1216
|
-
| `controls` | **required.** The control bones, by name, at least one. Each must be a bone the rig declares |
|
|
1244
|
+
| `controls` | **required.** The control bones, by name, at least one. Each must be a bone the rig declares. **More than one splits the ring by angle**, and the angle of each is measured from where the rig put that bone relative to `center` — so the split is a consequence of the skeleton and never a number you write here |
|
|
1217
1245
|
|
|
1218
1246
|
⚠️ **`size` is stated here, not measured.** A `contour` and a `grid` take the
|
|
1219
1247
|
window off the attachment's own `image`; a `ring` and a `ribbon` are built from
|
|
@@ -1223,17 +1251,24 @@ drawing — measured: a 240x240 part declared `"size": [64, 64]` builds green un
|
|
|
1223
1251
|
`--profile spine-html`. That is R1 rather than a gap: the compiler emits the
|
|
1224
1252
|
number the spec states and does not re-measure a plate to overrule it.
|
|
1225
1253
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1254
|
+
⭐ **Several grips split the ring by where the rig put them, on this route as on
|
|
1255
|
+
the manifest one.** Each control bone's angle about `center` is measured from its
|
|
1256
|
+
own rest position — the window is centred on the slot bone (the ⭐ above), so a
|
|
1257
|
+
bone sitting below the aperture owns the arc below it — and authority is
|
|
1258
|
+
smoothstepped between the two bones either side of a vertex, so no grip creases
|
|
1259
|
+
against the next. Measured on a two-control ring whose grips sit 12px above and
|
|
1260
|
+
below the centre: 25 vertices, both grips bound, and of the 8 shared vertices off
|
|
1261
|
+
the centre line all 8 take more weight from the grip on their own side. Splitting
|
|
1262
|
+
**moves** authority rather than adding it: every vertex gives its controls the
|
|
1263
|
+
same total with one grip, two or three.
|
|
1264
|
+
|
|
1265
|
+
⚠️ **This route used to bind only the first name**
|
|
1266
|
+
([#684](https://github.com/firejune/rigc/issues/684)). It passed no angles at all,
|
|
1267
|
+
so the split collapsed onto `controls[0]` and the rest of the names reached the
|
|
1268
|
+
`MESH` line, the bone list and nothing else — and the gate was green, because a
|
|
1269
|
+
bone no vertex binds is in no weight, no sum and no index. `A20_MESH_WEIGHTS_COHERENT`
|
|
1270
|
+
now names it, so a ring that declares a grip and does not use it is a failure
|
|
1271
|
+
rather than a quiet stiffness.
|
|
1237
1272
|
|
|
1238
1273
|
**Stated limits, each a named refusal rather than a mesh that loads wrong:**
|
|
1239
1274
|
|
|
@@ -1245,6 +1280,8 @@ several grips.
|
|
|
1245
1280
|
| a hull the centre cannot see all of | `hull is not star-shaped about the aperture centre; the inner ring would fold` — the inner ring is the hull scaled toward `center`, so an edge hidden from it crosses the rim and renders as folded meat |
|
|
1246
1281
|
| a `bias` ramp that does not increase | `bias ramp must increase, got [16, 4]` |
|
|
1247
1282
|
| a control bone the rig does not declare | `mesh bone "nobody" is not in the rig's bone list` |
|
|
1283
|
+
| two or more `controls`, one of them ON `center` | `control bone "iris_aperture" sits on the aperture centre, so it has no radial direction` — the position a lone control is supposed to occupy is the one position a split cannot use. With a single control it is never asked, so this refusal cannot fire on one |
|
|
1284
|
+
| two `controls` at the same angle about `center` | `two control bones share the angle 90 degrees about the aperture centre` — further out is not elsewhere: the arc between them is empty and one of them would bind nothing |
|
|
1248
1285
|
|
|
1249
1286
|
#### `ribbon` — a strip of cross rows riding a bone chain
|
|
1250
1287
|
|
|
@@ -2871,6 +2908,36 @@ different curves. The same applies to `scale`/`scalex`/`scaley` and
|
|
|
2871
2908
|
An `attachment` key carries no easing — attachment timelines are inherently
|
|
2872
2909
|
stepped.
|
|
2873
2910
|
|
|
2911
|
+
🔑 **An attachment key resolves against every skin, not against `default` alone.**
|
|
2912
|
+
A slot's `attachment` timeline carries a name and **no skin** — the format has no
|
|
2913
|
+
field for one — so the names a key may use are the union of every skin's
|
|
2914
|
+
placeholders for that slot, the default skin's included.
|
|
2915
|
+
`Skeleton.getAttachment` resolves the keyed name at run time through the skin the
|
|
2916
|
+
skeleton is **wearing** and, failing that, through `defaultSkin` (spine-core
|
|
2917
|
+
4.3.13 `Skeleton.js:335-346`), so which skin's art a key lands on is the
|
|
2918
|
+
consumer's, decided by dressing the skeleton rather than by the animation.
|
|
2919
|
+
|
|
2920
|
+
- **A name some skins fill and others do not is accepted, and that is the
|
|
2921
|
+
format's own semantics** rather than a hole in the check: under a skin that
|
|
2922
|
+
lacks it the slot shows nothing, which is exactly what a `null` key says and a
|
|
2923
|
+
thing a rig may well mean. What is refused is a name **no** skin holds, and the
|
|
2924
|
+
refusal says which skins were searched and what the slot does have (§5.1).
|
|
2925
|
+
- A rig with **no `default` skin at all** — every attachment in named skins,
|
|
2926
|
+
which is the shape an editor export of a multi-skin character gives back — is
|
|
2927
|
+
therefore a rig whose attachment keys work. Until
|
|
2928
|
+
[#695](https://github.com/firejune/rigc/issues/695) it was not: keys were
|
|
2929
|
+
checked against the default skin alone, so a named-skin name was refused as
|
|
2930
|
+
unknown, and a rig with no default skin had **every** attachment key refused,
|
|
2931
|
+
including ones whose art is in the first named skin. The setup pose resolved
|
|
2932
|
+
across skins the whole time (§4.2), so the two halves of one slot disagreed —
|
|
2933
|
+
`slots[].attachment: "plain"` was accepted and a key naming `plain` on that
|
|
2934
|
+
same slot was not.
|
|
2935
|
+
- ⚠️ **A `deform` track is the other way round and names its skin outright**
|
|
2936
|
+
(§4.11.5), because the format keys a deform timeline on a `skin/slot/attachment`
|
|
2937
|
+
triple and a deform run is geometry for one attachment object. An attachment
|
|
2938
|
+
timeline has no such field, which is why this one is a union and that one is a
|
|
2939
|
+
lookup.
|
|
2940
|
+
|
|
2874
2941
|
### 4.5 `keys` — times, values, curves
|
|
2875
2942
|
|
|
2876
2943
|
- `t` is in seconds and **must strictly increase** after `lag`/`stagger` are added.
|
|
@@ -4395,6 +4462,7 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
4395
4462
|
| `motion spec names archetype "A" but the rig spec at … is called "B"` | make `archetype` equal the rig's `name` |
|
|
4396
4463
|
| `animation "A" declares duration Ns but its last key is at Ms` | R7 — fix whichever of the two you meant |
|
|
4397
4464
|
| `animation "A" slot "X" attachment: key at Ns is Ms past the declared duration Ds` | §4.5 — the key is past the end of the animation and nothing will sample it. Move the key onto `duration`, or raise `duration` |
|
|
4465
|
+
| `animation "A" slot "X" attachment: attachment "N" is not in slot "X" under any skin (searched: default, alt) — the slot has: plain, trim` | §4.4 — the keyed name is in **no** skin, and the two clauses say where the compiler looked and what it would have taken. Fix the spelling, or give some skin a placeholder called `N`. A name only a NAMED skin fills is not this error and never was one to fix — it compiles, and the slot shows nothing under the skins that lack it. Before [#695](https://github.com/firejune/rigc/issues/695) the message read `attachment "N" is not in slot "X"` and was raised against the **default skin alone**, so it fired on correct rigs: any key into named-skin art, and every key in a rig with no default skin. `the slot has no attachments at all` is the same message where nothing fills the slot |
|
|
4398
4466
|
| `animation "A" keys unknown bone "X"` | the track's `bone` is not in the rig |
|
|
4399
4467
|
| `animation "A" bone "X" translatex: key value must be an array of 1 number(s)` | the value shape must match the property (§4.4) |
|
|
4400
4468
|
| `animation "A" physics constraint "C" mass key at t=… is 0 (massInverse Infinity); must be > 0 — …` | §4.4 — a keyed physics value the runtime cannot use. The message names the bound and the `PhysicsConstraint.js` lines that make it one: `mass` and `strength` are `> 0`, `damping` is inside `(0, 1)`, `mix` is `0` or more, and `inertia`/`wind`/`gravity` are bounded nowhere ([#610](https://github.com/firejune/rigc/issues/610)) |
|
|
@@ -4465,6 +4533,35 @@ or the key's position in its own track. These are the frequent ones, verbatim:
|
|
|
4465
4533
|
| `slot "patch": placeholder "patch" is filled by the "default" skin AND by skins "zulu", "mike", and the Spine editor has no way to hold that … Move the default skin's entry for this slot into a named skin — call it "base"` | **R12** — do what it says: move that entry out of `default` into a named skin. The editor has no representation for a placeholder the default skin shares with a named one, in either spelling, and §3.4.2 has both measurements. Renaming the placeholder does not help; the shape is what is refused |
|
|
4466
4534
|
| `N attachment name collision(s): a placeholder that more than one skin fills is emitted with the name "<skin>/<placeholder>" … slot "patch": skin "base" placeholder "zulu/patch" and skin "zulu" placeholder "patch" would both be named "zulu/patch"` | **R12** — rename the placeholder or the skin. rigc composes an attachment name for every placeholder more than one skin fills (§3.4.2), and this fires when a composed name is one another entry in the same slot already answers to — including a plain name in the default skin, which composed nothing. Both sites are named; either rename ends it |
|
|
4467
4535
|
|
|
4536
|
+
⚠️ **One refusal in this section is not a `CompileError`, and it is `explain`'s.**
|
|
4537
|
+
`explain` prints the `DEFORM` block by **posing** the rig, and a pose resolves every
|
|
4538
|
+
attachment against the atlas — so it needs the art `build` needs, reached the same
|
|
4539
|
+
two ways (§0.2). A pair whose art it cannot resolve is refused **before a line of
|
|
4540
|
+
the report**, at **exit 2**, in rigc's own sentence:
|
|
4541
|
+
|
|
4542
|
+
```
|
|
4543
|
+
rigc explain: skin "default" slot "lamp" placeholder "shade": attachment "shade"
|
|
4544
|
+
wants region "shade", which this build's atlas does not have (it declares no region
|
|
4545
|
+
at all). `explain` poses the rig to measure its deform keys and a pose resolves
|
|
4546
|
+
every attachment against the atlas, so there is nothing to pose it against. Art
|
|
4547
|
+
reaches a compile two ways and this run took neither: `--atlas-in <pack.atlas>`
|
|
4548
|
+
resolves the parts against a pack somebody already made, and an "image" per
|
|
4549
|
+
attachment resolves them as loose PNGs under `--images <dir>` — a spec that states
|
|
4550
|
+
a size and names no image is what `ingest --art none` writes, and `--atlas-in` is
|
|
4551
|
+
what reads it. 1 of 1 attachment lookup(s) here resolve to no region.
|
|
4552
|
+
```
|
|
4553
|
+
|
|
4554
|
+
Both halves of it are measured rather than fixed text: the count is this pair's, and
|
|
4555
|
+
where the atlas has a **near miss** the sentence prints that too, in `A08`'s own
|
|
4556
|
+
clause and `A08`'s own words. When `--atlas-in` **was** given and the region is still
|
|
4557
|
+
absent, the second half names the pack instead and asks you to fix the spec's region
|
|
4558
|
+
name or point the flag at the pack that has it. Until
|
|
4559
|
+
[#697](https://github.com/firejune/rigc/issues/697) there was no rigc sentence at
|
|
4560
|
+
all: the report printed in full and the run then died inside `AtlasAttachmentLoader`
|
|
4561
|
+
with `Region not found in atlas: shade (attachment: shade)` and a spine-core stack
|
|
4562
|
+
trace, at exit 1 — the runtime's message about rigc's internals standing in for
|
|
4563
|
+
rigc's message about your two files.
|
|
4564
|
+
|
|
4468
4565
|
### 5.2 Assertions — the gate
|
|
4469
4566
|
|
|
4470
4567
|
The report prints one line per assertion:
|
|
@@ -4545,7 +4642,7 @@ Fix A00 and run it again ([#568](https://github.com/firejune/rigc/issues/568)).
|
|
|
4545
4642
|
| `A17_ATLAS_PAGE_FILES_EXIST` | both | a page the atlas declares is not a file. Check `--images` and `--out`. **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) — as it is for `A06`, `A19` and `A27`; see `A07` ([#608](https://github.com/firejune/rigc/issues/608)) |
|
|
4546
4643
|
| `A18_DETERMINISTIC_EMIT` | both | a second compile of the same inputs differed. That is a compiler bug, not a spec bug — report it |
|
|
4547
4644
|
| `A19_OVERLAY_PNGS_HAVE_ALPHA` | renderer | an overlay part image can never be transparent: no alpha channel (colour type 4 or 6) and no `tRNS` chunk either, so it would paint a solid rectangle over what is behind it. Re-export it as RGBA, or as an indexed / greyscale PNG that keeps its `tRNS`. Only the full-stage base plate may be opaque. Indexed-with-`tRNS` — the usual output of ImageMagick, "Export as PNG-8", GIMP's indexed mode, aseprite and pngquant — **passes**: it is transparent art. On a **shared** page the question is asked per REGION over the decoded page rather than per file, because a packed page's own file all but always declares transparency — its gutter is transparent — and the file-level question would then be answered by the packing rather than by the art ([#266](https://github.com/firejune/rigc/issues/266)) **SKIP** when the atlas declares no page ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
4548
|
-
| `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh,
|
|
4645
|
+
| `A20_MESH_WEIGHTS_COHERENT` | both ◑ | a weighted vertex with no bone, a negative weight, a bone index out of range, or weights that do not sum to 1. Under `spine-html` also: an unweighted mesh, a binding at weight 0, or **a bone the mesh declares that no vertex binds** — `mesh "x" declares bone "grip_b" and none of its 25 vertices binds it; the weights reference "box", "grip_a"`. Those three are one sentence about rigc's own generators: the bone set a generated mesh declares is the bone set its weights reference, so a `controls` or `chain` name that moves nothing is a defect where a foreign mesh's is not ([#684](https://github.com/firejune/rigc/issues/684)). Fix the rig spec's `controls`/`chain`, or the manifest's `control_bones`. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
4549
4646
|
| `A21_MESH_RIM_PINNED` | archetype | a generated ring's rim, a ribbon's entry row, or a contour's outline (which is all of it) is not pinned to its anchor bone at weight 1 |
|
|
4550
4647
|
| `A22_MESH_UVS_IN_UNIT_RANGE` | both | a mesh UV outside its region, or a UV array that disagrees with the vertex count. **SKIP** when the skeleton carries no mesh attachment ([#580](https://github.com/firejune/rigc/issues/580)) |
|
|
4551
4648
|
| `A23_PHYSICS_CONSTRAINT_EFFECTIVE` | both | a physics constraint that drives no component, is muted by `mix: 0`, has `mass: 0`, has `strength: 0`, or has `damping` outside `(0, 1)` so it never settles — **at rest, and on every physics timeline key** ([#610](https://github.com/firejune/rigc/issues/610)). The timeline arm reads each key through the runtime's own `PhysicsConstraint*Timeline.set`, so a keyed `mass` is judged as the `massInverse` it becomes, and the detail names the animation, the constraint, the key time, the value and the bound. One difference between the two arms, and the runtime is the reason for it: a **key** of `mix: 0` is accepted, because `update` opens with `if (mix === 0) return;` and muting a constraint for a stretch is what a mix timeline is for — the editor's own `sack-pro` example keys it there on 24 of its 36 mix keys. `inertia`, `wind`, `gravity` and the top of `mix` are bounded nowhere, at rest or keyed. **SKIP** when the skeleton declares no physics constraint ([#580](https://github.com/firejune/rigc/issues/580)) — the same sentence `A36` and `A37` have always printed for their own constraint types |
|
|
@@ -5937,6 +6034,13 @@ There are two matchers and the `how` column says which one answered:
|
|
|
5937
6034
|
confidence: how much better the winning position was than the best rival inside
|
|
5938
6035
|
the search window. This is what gives a shot like a chain of touching links any
|
|
5939
6036
|
drift at all — under connected components alone, every frame of it is ambiguous.
|
|
6037
|
+
⭐ **Only the pixels of it your own composite lets show are correlated**: a
|
|
6038
|
+
template pixel you draw something over cannot match the reference wherever the
|
|
6039
|
+
slot really is, so it adds the same residual at every offset — and, because
|
|
6040
|
+
sliding the template moves those samples onto other pixels, their gradient
|
|
6041
|
+
decides the winner wherever the visible basin is shallow. That is not a
|
|
6042
|
+
hypothetical: it is what made four of the seven examples in this repository
|
|
6043
|
+
report 0.8–2.2 px against frames rendered from themselves (issue #698).
|
|
5940
6044
|
|
|
5941
6045
|
⚠️ **Both matchers are capped, and a blank is a real answer.** A part can be
|
|
5942
6046
|
displaced by about its own size and still be that part in the picture; past that,
|
|
@@ -5946,6 +6050,23 @@ the 47 px course as its drift. The bar rises with the distance being claimed: a
|
|
|
5946
6050
|
peak sitting where you already drew the slot only has to confirm it, a peak
|
|
5947
6051
|
claiming the part moved most of a radius has to be distinctive to be believed.
|
|
5948
6052
|
|
|
6053
|
+
⚠️ **And a slot you cover completely has no drift to report at all.** If every
|
|
6054
|
+
pixel a slot draws is painted over by something later in your own draw order,
|
|
6055
|
+
none of its ink reaches the picture, so there is nothing to correlate — `check`
|
|
6056
|
+
says so by name and counts it out, rather than correlating hidden pixels against
|
|
6057
|
+
whatever is on top of them:
|
|
6058
|
+
|
|
6059
|
+
```
|
|
6060
|
+
its drift is not measurable — every one of the 2116 px this 48x54 px slot draws is
|
|
6061
|
+
covered by something the candidate draws over it, so none of its own ink reaches the
|
|
6062
|
+
picture to be correlated against
|
|
6063
|
+
```
|
|
6064
|
+
|
|
6065
|
+
That is a fact about the shot and not an error: a part behind another part is
|
|
6066
|
+
still where the rig put it, and the answer to *where did it land* is that this
|
|
6067
|
+
run cannot say. Read it beside the `slots` column, which counts it as
|
|
6068
|
+
unattributed.
|
|
6069
|
+
|
|
5949
6070
|
The `slots` column is how many of the slots you drew got an answer at all, and the
|
|
5950
6071
|
summary line carries the same denominator. `N reference component(s) no slot
|
|
5951
6072
|
reaches` means the reference frame contains something none of your slots overlaps:
|
|
@@ -5969,25 +6090,67 @@ bun cli.ts check --candidate <build> --frames <frames>
|
|
|
5969
6090
|
frames 11 on disk, candidate samples 11, 11 compared
|
|
5970
6091
|
MAE mean 0.00 worst 0.00 (exact: none of the 11 compared frame(s) differs from the reference) (0..255 over the union alpha; over the whole frame, mean 0.00)
|
|
5971
6092
|
⤷ over the REFERENCE's own drawn pixels, mean 0.00 — the union figure compares two builds of the same rig; this one is the one to optimise against, because the union is yours to grow.
|
|
5972
|
-
slot drift worst 0.
|
|
6093
|
+
slot drift worst 0.5 px "plate" at f0004
|
|
6094
|
+
⤷ bounded by 0.71 px — the correlation put this slot where the candidate drew it, so the whole figure is the sub-pixel step, clamped to 0.5 px on each axis
|
|
5973
6095
|
per-frame all 10 adjacent pair(s) change by as much as the reference's own frames do
|
|
5974
6096
|
```
|
|
5975
6097
|
|
|
5976
6098
|
⚠️ **The MAE floor is zero and the slot-drift floor is not**, and the second half of
|
|
5977
6099
|
that is the instrument's own arithmetic rather than anything about your rig. Both
|
|
5978
6100
|
sides are the same pixels, so every frame's MAE is exactly 0 — which is why the line
|
|
5979
|
-
says `(exact)` instead of naming a frame. The drift is a **correlation
|
|
5980
|
-
|
|
5981
|
-
clamped to half a pixel on each axis
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
6101
|
+
says `(exact)` instead of naming a frame. The drift is a **correlation**: it finds
|
|
6102
|
+
the whole-pixel offset that matches best and then fits a parabola through three
|
|
6103
|
+
whole-pixel residuals for the fraction, clamped to half a pixel on each axis.
|
|
6104
|
+
|
|
6105
|
+
🔑 **So the bound has two halves, and the run states both of them for you.** The
|
|
6106
|
+
`⤷ bounded by` line is not a constant off this page: it is `hypot(|dx| + 0.5,
|
|
6107
|
+
|dy| + 0.5)` for that match's own whole-pixel winner `(dx, dy)`. A winner at the
|
|
6108
|
+
offset you drew — which is the answer on every frame of a correct rig — bounds the
|
|
6109
|
+
whole figure at `hypot(0.5, 0.5) = 0.71 px`, and the line says the figure is the
|
|
6110
|
+
sub-pixel step and nothing else. A winner one pixel out bounds it at 1.58 px and
|
|
6111
|
+
says the correlation moved the part. ⇒ **Read a drift against the line under it,
|
|
6112
|
+
never against a number from a page about another rig.** A component match prints
|
|
6113
|
+
the other sentence — two centroids have no such bound, only the search radius.
|
|
6114
|
+
|
|
6115
|
+
🚨 **The clamp bounds the figure only while the whole-pixel winner is the
|
|
6116
|
+
identity, and for a while nothing checked that second half.** Issue #698: four of
|
|
6117
|
+
the seven examples in this repository read **0.81, 1.11, 2.13 and 2.21 px** against
|
|
6118
|
+
frames rendered from themselves, because the template carried the pixels the
|
|
6119
|
+
candidate draws *over itself*. Those match nothing wherever the slot really is, so
|
|
6120
|
+
they add a residual at every offset — and sliding the template moves them onto
|
|
6121
|
+
other pixels, so their gradient walks the winner off the origin wherever the
|
|
6122
|
+
visible basin is shallow. The exhaustive whole-pixel field for `flex`'s backdrop
|
|
6123
|
+
had its minimum at `(2, 0)` scoring 7.75 against the identity offset's 8.76.
|
|
6124
|
+
Correlating only what shows put all seven back on `(0, 0)`, and `C25`–`C30` of
|
|
6125
|
+
the repository's own selftest gate that over every example it ships — so the next
|
|
6126
|
+
one is gated by arriving.
|
|
6127
|
+
|
|
6128
|
+
🔸 The same page said `3.7 px` before issue #678, for an unrelated reason worth
|
|
6129
|
+
keeping: the coarse sweep started at `−radius` and stepped by its stride, so the
|
|
6130
|
+
identity offset was on the lattice only when the stride divided the radius, and
|
|
6131
|
+
the `±1` refinement around a winner two pixels out could not reach back to it.
|
|
6132
|
+
|
|
6133
|
+
**A second example, and the one the repair was measured on.** `gallery/flex` draws
|
|
6134
|
+
a banner and a leaf over a full-stage backdrop, so almost every slot of it reaches
|
|
6135
|
+
the template matcher:
|
|
6136
|
+
|
|
6137
|
+
```bash
|
|
6138
|
+
bun cli.ts build --rig gallery/flex/rig.json --motion gallery/flex/motion.json --out <build>
|
|
6139
|
+
bun cli.ts render --candidate <build> --animation wave --fps 12 --out <frames>
|
|
6140
|
+
bun cli.ts check --candidate <build> --frames <frames>
|
|
6141
|
+
```
|
|
6142
|
+
|
|
6143
|
+
```
|
|
6144
|
+
── wave — candidate animation "wave", 12 fps ──
|
|
6145
|
+
frames 30 on disk, candidate samples 30, 30 compared
|
|
6146
|
+
MAE mean 0.00 worst 0.00 (exact: none of the 30 compared frame(s) differs from the reference) (0..255 over the union alpha; over the whole frame, mean 0.00)
|
|
6147
|
+
⤷ over the REFERENCE's own drawn pixels, mean 0.00 — the union figure compares two builds of the same rig; this one is the one to optimise against, because the union is yours to grow.
|
|
6148
|
+
slot drift worst 0.2 px "plate" at f0009
|
|
6149
|
+
⤷ bounded by 0.71 px — the correlation put this slot where the candidate drew it, so the whole figure is the sub-pixel step, clamped to 0.5 px on each axis
|
|
6150
|
+
per-frame all 29 adjacent pair(s) change by as much as the reference's own frames do
|
|
6151
|
+
```
|
|
6152
|
+
|
|
6153
|
+
That figure was **2.21 px** on the same command before #698, on the same slot.
|
|
5991
6154
|
|
|
5992
6155
|
⭐ **And the same frames plus two deliberately wrong builds are what say which column
|
|
5993
6156
|
answers which question.** Each differs from the build above in exactly one way —
|
|
@@ -6029,6 +6192,15 @@ samples, so the column is correctly silent on it. ⇒ Read the pair together: a
|
|
|
6029
6192
|
MAE with `per-frame` silent is a pose in the wrong place or at the wrong moment; a
|
|
6030
6193
|
loud MAE with `per-frame` firing is a **speed**, which is a curve.
|
|
6031
6194
|
|
|
6195
|
+
🔸 Neither block above reprints its own `⤷ bounded by` line, for the same reason
|
|
6196
|
+
both carry the declaration: the spec edits are not in the tree, so those two lines
|
|
6197
|
+
would be hand-written rather than taken. What the gate measures on them is stated
|
|
6198
|
+
instead — the worst match of each sits at whole-pixel `(0, −31)` and `(0, −32)`,
|
|
6199
|
+
which bounds them at **31.50 px** and **32.50 px**. That is the contrast worth
|
|
6200
|
+
carrying away: a correct rig's drift is bounded by the clamp because its winner is
|
|
6201
|
+
the identity, and these two are bounded by nothing of the kind because a part
|
|
6202
|
+
really moved.
|
|
6203
|
+
|
|
6032
6204
|
**The `chains` block is the same two measures on the unit you actually repair.**
|
|
6033
6205
|
|
|
6034
6206
|
```
|
package/docs/INGEST.md
CHANGED
|
@@ -394,7 +394,10 @@ rigc explain examples/spineboy/export/spineboy-pro.json
|
|
|
394
394
|
rigc: give either --cut <name> --cuts <cuts.json>, or --rig/--motion/--out
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
It takes
|
|
397
|
+
It takes `--rig`, `--motion`, `--out`, and optionally `--manifest`, `--images` and
|
|
398
|
+
`--atlas-in` — `build`'s spec-reading flags, and not the ones that decide what `build`
|
|
399
|
+
*writes* (`--pack`, `--page-size`, `--padding`, `--copy-images`) or the one that gates
|
|
400
|
+
(`--profile`). It prints the resolved account
|
|
398
401
|
of **your** two spec files, and never gates. Which makes it a §2 instrument rather
|
|
399
402
|
than a §1 one — the thing you run to compare what you transcribed against the export
|
|
400
403
|
you transcribed it from, by eye:
|
|
@@ -497,7 +500,12 @@ of this section**, with its gutter, its effect on the exit code and what to do.
|
|
|
497
500
|
|
|
498
501
|
And two flags for what the skeleton also does not encode: `--art loose` (the default)
|
|
499
502
|
names an `image` per attachment resolved against loose PNGs, `--art none` states
|
|
500
|
-
`width`/`height` for `build --atlas-in
|
|
503
|
+
`width`/`height` for `build --atlas-in` — and for `explain --atlas-in`, which is the
|
|
504
|
+
same pack read for a report rather than for an artifact: `explain` **poses** the rig
|
|
505
|
+
to print its `DEFORM` block, a pose resolves every attachment against an atlas, and a
|
|
506
|
+
size-only spec carries none of its own, so without the flag that pair is refused by
|
|
507
|
+
name rather than posed ([#697](https://github.com/firejune/rigc/issues/697)); and
|
|
508
|
+
under `loose`, `--images <dir>` writes
|
|
501
509
|
the rig spec's own images directory relative to `--out`, so the rebuild is a plain
|
|
502
510
|
`build --rig … --motion … --out …` rather than one carrying `--images` forever. It is
|
|
503
511
|
refused together with `--art none`, which writes no `image` for a directory to be the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.6",
|
|
4
4
|
"description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/atlas.ts
CHANGED
|
@@ -405,10 +405,19 @@ export function parseAtlasText(text: string): ParsedAtlas {
|
|
|
405
405
|
* field it re-emits, and the ones it did not understand would quietly vanish
|
|
406
406
|
* (`scale:` is the expensive example — [`atlasScales`](render.ts) reports it, and
|
|
407
407
|
* a pack that is coarser than its drawings would stop saying so).
|
|
408
|
+
*
|
|
409
|
+
* `rename` is handed the page's INDEX as well as its name, because a name is not
|
|
410
|
+
* a key: nothing in the format forbids two pages spelling the same path, and a
|
|
411
|
+
* caller that has already decided one new name per page (`copyAtlasPages` in
|
|
412
|
+
* [`emit.ts`](emit.ts) assigns the copies' filenames in page order) would then
|
|
413
|
+
* hand both of them the first decision. The index is the page's identity here;
|
|
414
|
+
* the name is data.
|
|
408
415
|
*/
|
|
409
|
-
export function rewritePageNames(parsed: ParsedAtlas, rename: (name: string) => string): string {
|
|
416
|
+
export function rewritePageNames(parsed: ParsedAtlas, rename: (name: string, index: number) => string): string {
|
|
410
417
|
const out = parsed.lines.slice();
|
|
411
|
-
|
|
418
|
+
parsed.pages.forEach((page, index) => {
|
|
419
|
+
out[page.nameLine] = rename(page.name, index);
|
|
420
|
+
});
|
|
412
421
|
return out.join('\n');
|
|
413
422
|
}
|
|
414
423
|
|