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 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 - Rig compiler for Spine" width="100%" />
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
- **Rig compiler for Spine.** Declarative rig specs in, Spine 4.3 skeleton data out,
12
- verified by a `spine-core` round-trip. Built so AI agents can author rigs and check
13
- their own work.
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 `--padding` tune it |
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
  ],