spine-rigc 1.2.2 → 1.3.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 +7 -5
- package/cli.ts +63 -6
- package/docs/AUTHORING.md +309 -18
- package/docs/FACE.md +15 -0
- package/docs/INGEST.md +1 -1
- package/package.json +25 -3
- package/src/atlas.ts +120 -2
- package/src/compile.ts +335 -0
- package/src/mesh.ts +405 -1
- package/src/pose.ts +213 -0
- package/src/rig.ts +140 -7
- package/src/types.ts +30 -0
- package/src/validate.ts +101 -24
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/banner.svg" alt="rigc -
|
|
2
|
+
<img src="https://raw.githubusercontent.com/firejune/rigc/main/assets/banner.svg" alt="rigc — AI-authored Spine 2D rigging and animation, verified before it is written" width="100%" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-38BDF8.svg?style=flat-square" alt="license" /></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
**
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
**AI-authored Spine 2D rigging and animation, verified before it is written.** rigc is
|
|
12
|
+
a rig compiler for Spine: a rig spec and a motion spec in, Spine 4.3 skeleton data out,
|
|
13
|
+
round-tripped through `spine-core` and a list of named assertions before a byte is
|
|
14
|
+
written. Built so AI agents can author rigs and check their own work; it ships as an
|
|
15
|
+
agent skill.
|
|
14
16
|
|
|
15
17
|
## What you get
|
|
16
18
|
|
|
@@ -528,7 +530,7 @@ commands take it and what its default is.
|
|
|
528
530
|
| Command | Does |
|
|
529
531
|
| --- | --- |
|
|
530
532
|
| `build --rig … --motion … --out …` | compiles, gates, and **writes only if the gate is green**. `--images <dir>` says where the rig spec's `image` names resolve, `--manifest` adds measured art, and `--copy-images` copies every page PNG into `--out` so the directory is self-contained, with `skeleton.images` pointing at it so the editor's import finds the parts |
|
|
531
|
-
| `build … --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` — losslessly, and gated a second time as the pair that ships. `--page-size` and `--
|
|
533
|
+
| `build … --pack` | the same build with every part arranged onto **shared** atlas pages, written into `--out` — losslessly, and gated a second time as the pair that ships. `--page-size`, `--padding` and `--page-edges` tune it |
|
|
532
534
|
| `build … --atlas-in <file.atlas>` | the same build with every part resolved to a **region of an existing pack** instead of a loose PNG; a name the atlas lacks, a size the spec disagrees with or a rectangle off its page is refused by name |
|
|
533
535
|
| `validate <dir>` | re-gates artifacts already on disk |
|
|
534
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 |
|
package/cli.ts
CHANGED
|
@@ -101,8 +101,12 @@ import { NotAPngError } from './src/png.ts';
|
|
|
101
101
|
import { copyAtlasPages } from './src/emit.ts';
|
|
102
102
|
import {
|
|
103
103
|
DEFAULT_PADDING,
|
|
104
|
+
DEFAULT_PAGE_EDGES,
|
|
104
105
|
DEFAULT_PAGE_SIZE,
|
|
106
|
+
FREE_EDGE_STEP,
|
|
107
|
+
PAGE_EDGES,
|
|
105
108
|
packAtlas,
|
|
109
|
+
type PageEdges,
|
|
106
110
|
pageFootprint,
|
|
107
111
|
parseAtlasText,
|
|
108
112
|
type AtlasRegion,
|
|
@@ -612,9 +616,37 @@ const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> =
|
|
|
612
616
|
ribbon: 'ribbon entry row pinned, rows share their weights so the strip lengthens without widening',
|
|
613
617
|
contour: 'contour the art\'s own silhouette, every vertex pinned to the slot bone (geometry, not a deformation)',
|
|
614
618
|
grid: 'grid a lattice over the part window at stated column and row positions, every vertex pinned to the slot bone',
|
|
619
|
+
segments: 'segments a lattice over the part\'s alpha, every vertex weighted by distance to the bone segments it names',
|
|
615
620
|
authored: 'authored geometry rigc did not build; it assumes nothing about the topology',
|
|
616
621
|
};
|
|
617
622
|
|
|
623
|
+
/**
|
|
624
|
+
* The line under a `segments` mesh: how its bones share the vertices.
|
|
625
|
+
*
|
|
626
|
+
* ⭐ The weights are this generator's whole output, and the per-vertex numbers
|
|
627
|
+
* are nowhere a reader can see them — so the three figures that say whether the
|
|
628
|
+
* falloff did what was meant are printed where the mesh is: the most bones any
|
|
629
|
+
* vertex binds (against `maxBones`), the mean, and how many vertices a single
|
|
630
|
+
* bone owns outright. A named bone no vertex binds is said by name, because it
|
|
631
|
+
* is in no weight and so in no other figure either.
|
|
632
|
+
*/
|
|
633
|
+
function meshInfluenceNote(m: CompileResult['meshes'][number]): string {
|
|
634
|
+
const inf = m.influence;
|
|
635
|
+
if (inf === undefined) return '';
|
|
636
|
+
const unbound = m.bones.filter((b) => !inf.bound.includes(b));
|
|
637
|
+
const single = m.vertices === 0 ? 0 : (inf.singleBone / m.vertices) * 100;
|
|
638
|
+
const joined = inf.keptCells - inf.artCells;
|
|
639
|
+
return (
|
|
640
|
+
`\n influence max ${inf.maxBones} bone(s) per vertex, mean ${inf.meanBones.toFixed(2)}, ` +
|
|
641
|
+
`${inf.singleBone} of ${m.vertices} vertices (${single.toFixed(2)}%) on a single bone` +
|
|
642
|
+
(unbound.length ? `; named and bound by no vertex: ${unbound.join(', ')}` : '') +
|
|
643
|
+
`\n lattice cell ${inf.cell}px, ${inf.cols}x${inf.rows} cells, ${inf.artCells} with art, ${inf.keptCells} kept` +
|
|
644
|
+
(inf.islands > 1 || joined > 0
|
|
645
|
+
? ` (${inf.islands} island(s) joined into one outline, ${joined} cell(s) added that hold no art)`
|
|
646
|
+
: '')
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
618
650
|
/**
|
|
619
651
|
* Why a figure taken off a part's texels is withheld on a page whose file is
|
|
620
652
|
* not the size its atlas declares — the tail of every line that withholds one
|
|
@@ -1260,6 +1292,21 @@ function readIntFlag(flags: Record<string, string>, name: string, fallback: numb
|
|
|
1260
1292
|
return Number(raw);
|
|
1261
1293
|
}
|
|
1262
1294
|
|
|
1295
|
+
/**
|
|
1296
|
+
* Read `--page-edges`, or its default — `pot`.
|
|
1297
|
+
*
|
|
1298
|
+
* An unknown value is a usage error for `readProfile`'s reason: a typo that fell
|
|
1299
|
+
* back to `pot` would hand the caller who asked for the smaller page the bigger
|
|
1300
|
+
* one, green, and say nothing.
|
|
1301
|
+
*/
|
|
1302
|
+
function readPageEdges(flags: Record<string, string>): PageEdges {
|
|
1303
|
+
const raw = flags['page-edges'];
|
|
1304
|
+
if (raw === undefined) return DEFAULT_PAGE_EDGES;
|
|
1305
|
+
const found = PAGE_EDGES.find((e) => e === raw);
|
|
1306
|
+
if (!found) throw new UsageError(`--page-edges ${JSON.stringify(raw)}; known values: ${PAGE_EDGES.join(', ')}`);
|
|
1307
|
+
return found;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1263
1310
|
/**
|
|
1264
1311
|
* One `DROP` line, written once because two outcomes print it.
|
|
1265
1312
|
*
|
|
@@ -1343,7 +1390,7 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1343
1390
|
// now a build like any other — and it is the only one that puts the renderer's
|
|
1344
1391
|
// own rulebook over shared-page sampling.
|
|
1345
1392
|
if (!packing) {
|
|
1346
|
-
for (const name of ['page-size', 'padding'] as const) {
|
|
1393
|
+
for (const name of ['page-size', 'padding', 'page-edges'] as const) {
|
|
1347
1394
|
if (flags[name] !== undefined) throw new UsageError(`--${name} only means something with --pack`);
|
|
1348
1395
|
}
|
|
1349
1396
|
}
|
|
@@ -1394,7 +1441,8 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1394
1441
|
console.log(
|
|
1395
1442
|
` MESH ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
1396
1443
|
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}` +
|
|
1397
|
-
meshDepthNote(m)
|
|
1444
|
+
meshDepthNote(m) +
|
|
1445
|
+
meshInfluenceNote(m),
|
|
1398
1446
|
);
|
|
1399
1447
|
}
|
|
1400
1448
|
for (const ph of result.physics) {
|
|
@@ -1445,6 +1493,7 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1445
1493
|
const packOpts = {
|
|
1446
1494
|
pageSize: readIntFlag(flags, 'page-size', DEFAULT_PAGE_SIZE),
|
|
1447
1495
|
padding: readIntFlag(flags, 'padding', DEFAULT_PADDING),
|
|
1496
|
+
pageEdges: readPageEdges(flags),
|
|
1448
1497
|
pageStem: 'skeleton',
|
|
1449
1498
|
};
|
|
1450
1499
|
const inputs = result.images.map((img) => ({
|
|
@@ -1460,7 +1509,8 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
1460
1509
|
console.log(
|
|
1461
1510
|
` .. pack: ${page.name} ${page.width}x${page.height}, ` +
|
|
1462
1511
|
`${packed.placements.filter((p) => packed.pages[p.page].name === page.name).length} region(s), ` +
|
|
1463
|
-
`${(page.occupancy * 100).toFixed(1)}% covered, padding ${packed.padding}
|
|
1512
|
+
`${(page.occupancy * 100).toFixed(1)}% covered, padding ${packed.padding}` +
|
|
1513
|
+
(packOpts.pageEdges === 'free' ? ', page edges free' : ''),
|
|
1464
1514
|
);
|
|
1465
1515
|
}
|
|
1466
1516
|
for (const place of packed.placements) {
|
|
@@ -3327,7 +3377,8 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
3327
3377
|
console.log(
|
|
3328
3378
|
` ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
3329
3379
|
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}` +
|
|
3330
|
-
meshDepthNote(m)
|
|
3380
|
+
meshDepthNote(m) +
|
|
3381
|
+
meshInfluenceNote(m),
|
|
3331
3382
|
);
|
|
3332
3383
|
}
|
|
3333
3384
|
}
|
|
@@ -3737,6 +3788,10 @@ const FLAG_MEANINGS: Record<string, string> = {
|
|
|
3737
3788
|
'one written is the smallest that holds the pack, spilling to more pages only when the set will not fit',
|
|
3738
3789
|
padding: `gutter each region reserves on every side, --pack only (default ${DEFAULT_PADDING}); it is filled by ` +
|
|
3739
3790
|
"extending the region's own edge pixels outwards, which is what stops a neighbour bleeding in",
|
|
3791
|
+
'page-edges': `what a page's edges may be, --pack only (default ${DEFAULT_PAGE_EDGES}): pot is a power of two on ` +
|
|
3792
|
+
`both; free tries every width on a ${FREE_EDGE_STEP}px step, takes the height the placement needs and keeps ` +
|
|
3793
|
+
'the least area — a smaller page, at the cost of region attachments sampling within 1 LSB of the loose ' +
|
|
3794
|
+
'build rather than exactly',
|
|
3740
3795
|
'atlas-in':
|
|
3741
3796
|
'resolve every part against the regions of this pre-packed .atlas instead of against loose PNGs — region ' +
|
|
3742
3797
|
'geometry (bounds/offsets/rotate) is read from the file and the atlas is re-emitted into --out, re-anchored',
|
|
@@ -3855,6 +3910,7 @@ const FLAG_VALUES: Record<string, string> = {
|
|
|
3855
3910
|
'atlas-in': '<file.atlas>',
|
|
3856
3911
|
'page-size': '<px>',
|
|
3857
3912
|
padding: '<px>',
|
|
3913
|
+
'page-edges': 'pot|free',
|
|
3858
3914
|
'texture-from': '<path>',
|
|
3859
3915
|
reference: '<dir|skeleton.json>',
|
|
3860
3916
|
'reference-atlas': '<path>',
|
|
@@ -3941,7 +3997,7 @@ const COMMANDS: CommandDoc[] = [
|
|
|
3941
3997
|
name: 'build',
|
|
3942
3998
|
usage: [
|
|
3943
3999
|
'rigc build --rig <path> --motion <path> --out <dir> [--manifest <path>] [--images <dir>] [--profile spine|spine-html] [--copy-images]',
|
|
3944
|
-
`rigc build … --pack [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] (parts onto shared pages, written into --out)`,
|
|
4000
|
+
`rigc build … --pack [--page-size ${DEFAULT_PAGE_SIZE}] [--padding ${DEFAULT_PADDING}] [--page-edges pot|free] (parts onto shared pages, written into --out)`,
|
|
3945
4001
|
'rigc build … --atlas-in <skeleton.atlas> (resolve the parts against a pack somebody already made)',
|
|
3946
4002
|
'rigc build --cut <name> --cuts <cuts.json>',
|
|
3947
4003
|
],
|
|
@@ -3955,6 +4011,7 @@ const COMMANDS: CommandDoc[] = [
|
|
|
3955
4011
|
'pack',
|
|
3956
4012
|
'page-size',
|
|
3957
4013
|
'padding',
|
|
4014
|
+
'page-edges',
|
|
3958
4015
|
'atlas-in',
|
|
3959
4016
|
'cut',
|
|
3960
4017
|
'cuts',
|
|
@@ -3973,7 +4030,7 @@ const COMMANDS: CommandDoc[] = [
|
|
|
3973
4030
|
'this line said "the same arguments as build, minus --profile" and was false in both',
|
|
3974
4031
|
'directions (issue #697): --atlas-in was not listed here, so the one flag that lets this',
|
|
3975
4032
|
'command read what `ingest --art none` writes was reachable and undocumented, while',
|
|
3976
|
-
'--pack, --page-size, --padding and --copy-images are build\'s and do nothing here —',
|
|
4033
|
+
'--pack, --page-size, --padding, --page-edges and --copy-images are build\'s and do nothing here —',
|
|
3977
4034
|
'they decide what is WRITTEN, and this command writes nothing. What it takes is listed',
|
|
3978
4035
|
'above, and that is now the whole of it.',
|
|
3979
4036
|
],
|